Model Context Protocol (MCP)
Introduction
This is my first foray into MCPs which works
Clients
- Claude
- Claude Code
- Cursor
- Windsurf
Servers
Example server site is awesome-mcp-servers
Installing Claude on Ubuntu 24.04
This was reasonably painless. Goto here. When installing the instructions are
git clone https://github.com/aaddrick/claude-desktop-debian.git
cd claude-desktop-debian
# Build the package
sudo ./build-deb.sh
sudo dpkg -i ./build/electron-app/claude-desktop_0.8.0_amd64.deb
This all goes well except the version of claude is now 0.8.1 and the script in the repository is 0.8.0. Search and replace the build-deb.sh and then it fails to launch because of sandbox issues which can be fixed, in my case, with
sudo chmod 4755 /usr/local/lib/node_modules/electron/dist/chrome-sandbox
Writing a Server (Python)
To demonstrate how easy this is Matthew Berman. There is just this code and a config
from mcp.server.fastmcp import FastMCP
import time
import signal
import sys
# Handle SIGINT (Ctrl+C) gracefully
def signal_handler(sig, frame):
print("Shutting down server gracefully...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
# Create an MCP server with increased timeout
mcp = FastMCP(
name="count-r",
host="127.0.0.1",
port=5000,
# Add this to make the server more resilient
timeout=30 # Increase timeout to 30 seconds
)
# Define our tool
@mcp.tool()
def count_r(word: str) -> int:
"""Count the number of 'r' letters in a given word."""
try:
# Add robust error handling
if not isinstance(word, str):
return 0
return word.lower().count("r")
except Exception as e:
# Return 0 on any error
return 0
if __name__ == "__main__":
try:
print("Starting MCP server 'count-r' on 127.0.0.1:5000")
# Use this approach to keep the server running
mcp.run()
except Exception as e:
print(f"Error: {e}")
# Sleep before exiting to give time for error logs
time.sleep(5)
Build an executable as you cannot just use pip3 or pipx to install globally.
pip3 install pyinstaller
pyinstaller --onefile main.py
This produces a file in the dist directory.
Then we change the config for claude, making sure the command is appropriate to your setup. This file can be found in the following directory. ~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"count-r": {
"command": "/home/iwiseman/dev/projects/mcpServer/pyFastMCPTest/dist/main",
"args": [
""
],
"host": "127.0.0.1",
"port": 8080,
"timeout": 30000
}
}
}
If all goes well, when you restart Claude it will show
Writing a Server (Typescript)
Well always a challenge but they will never win. The challenge is to avoid standard out. First the server.
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "Weather Service",
version: "1.0.0",
});
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// Add a dynamic greeting resource
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// Add a tool to get the weather
server.tool("getWeather", {
word: z.string()
},
async ({ word }) => {
return {
content: [
{
type: "text",
text: `The weather is ${word} nice today`
}
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
All went well and you can test this and other MCPs with the mcp inspector
npx @modelcontextprotocol/inspector
And all goes well. But adding it to Claude proved an hour of fun. To ensure I have not issues I made a shell script to cd to the working directory and run. But this fell over with
[error
] Unexpected end of JSON input {
"context": "connection",
"stack": "SyntaxError: Unexpected end of JSON input\n at JSON.parse (<anonymous>)\n at EPe (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:82:189)\n at SPe.readMessage (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:82:115)\n at TPe.processReadBuffer (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:83:1842)\n at Socket.<anonymous> (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:83:1523)\n at Socket.emit (node:events:518:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Readable.push (node:internal/streams/readable:392:5)\n at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)"
}
2025-03-22T03: 29: 54.581Z [add
] [error
] Unexpected token '>',
"> start" is not valid JSON {
"context": "connection",
"stack": "SyntaxError: Unexpected token '>', \"> start\" is not valid JSON\n at JSON.parse (<anonymous>)\n at EPe (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:82:189)\n at SPe.readMessage (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:82:115)\n at TPe.processReadBuffer (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:83:1842)\n at Socket.<anonymous> (/usr/lib/claude-desktop/app.asar/.vite/build/index.js:83:1523)\n at Socket.emit (node:events:518:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Readable.push (node:internal/streams/readable:392:5)\n at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)"
}
This is caused by npm run start going to standard out. If you want to run node you can do this.
{
"mcpServers": {
"add": {
"command": "node",
"args": [
"/home/iwiseman/dev/projects/mcpServer/tsMcpTest/dist/app.js"
]
},
...
Writing a Server (Kotlin)
Introduction
Well this was a blast from the past. It was harder than it needed to be mainly because I wanted to get better a kotlin on linux rather than android. Anyway made an MCP Server and put it on GitHub. here.
The code
The main part is shown below
val logger: Logger = LoggerFactory.getLogger("McpServer")
// Tool names as constants
private const val TOOL_GET_LIGHT_IDS = "get_light_ids"
private const val TOOL_SET_LIGHT_STATE = "set-light-state"
fun main() {
try {
logger.info("Starting MCP Server...")
// Initialize Koin
startDependencyInjection()
// Retrieve HueClient instance
val hueClient: HueClient = getKoin().get()
// Create and configure the server
val server = createServer(hueClient)
// Start the server with stdio transport
startServer(server)
} catch (e: Exception) {
logger.error("An error occurred during server setup: ${e.message}", e)
}
}
private fun startDependencyInjection() {
logger.info("Initializing Koin...")
startKoin { modules(appModule) }
logger.info("Koin initialized successfully.")
}
private fun startServer(server: Server) {
val stdioServerTransport =
StdioServerTransport(System.`in`.asSource().buffered(), System.out.asSink().buffered())
runBlocking {
logger.info("Connecting server to transport...")
server.connect(stdioServerTransport)
val job = Job()
server.onCloseCallback = {
logger.info("Server closed.")
job.complete()
}
logger.info("Waiting for server to close...")
job.join()
logger.info("Server has shut down.")
}
}
fun createServer(hueClient: HueClient): Server {
logger.info("Initializing server capabilities...")
// Server metadata
val info = Implementation(name = "huemcp", version = "0.1.0")
// Server options
val options =
ServerOptions(
capabilities =
ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
)
val server = Server(info, options)
// Add tools to the server
addGetLightIdsTool(server, hueClient)
addSetLightStateTool(server, hueClient)
logger.info("Server setup complete.")
return server
}
private fun addGetLightIdsTool(
server: Server,
hueClient: HueClient,
) {
logger.info("Adding tool: $TOOL_GET_LIGHT_IDS")
server.addTool(
name = TOOL_GET_LIGHT_IDS,
description = "Returns a list of light IDs for a given room.",
inputSchema =
Tool.Input(
properties =
JsonObject(
mapOf(
"room" to
JsonObject(
mapOf(
"type" to
JsonPrimitive(
"string",
),
"description" to
JsonPrimitive(
"Room name (e.g., Hall)",
),
),
),
),
),
required = listOf("room"),
),
) { input ->
val roomName = input.arguments["room"]?.jsonPrimitive?.contentOrNull
if (roomName.isNullOrBlank()) {
logger.error("Room name is missing or invalid.")
return@addTool CallToolResult(
content = listOf(TextContent("Error: Missing or invalid room name")),
)
}
val lightIds = hueClient.findLightIdsForRoom(roomName).joinToString()
logger.info("Found light IDs for room '$roomName': $lightIds")
CallToolResult(content = listOf(TextContent(lightIds)))
}
}
private fun addSetLightStateTool(
server: Server,
hueClient: HueClient,
) {
logger.info("Adding tool: $TOOL_SET_LIGHT_STATE")
val inputSchema =
Tool.Input(
properties =
JsonObject(
mapOf(
"lightId" to
JsonObject(
mapOf("type" to JsonPrimitive("string")),
),
"on" to
JsonObject(
mapOf(
"type" to
JsonPrimitive("boolean"),
),
),
"brightness" to
JsonObject(
mapOf("type" to JsonPrimitive("number")),
),
"colorGamutX" to
JsonObject(
mapOf("type" to JsonPrimitive("number")),
),
"colorGamutY" to
JsonObject(
mapOf("type" to JsonPrimitive("number")),
),
"dimming" to
JsonObject(
mapOf("type" to JsonPrimitive("number")),
),
),
),
required = listOf("lightId"),
)
server.addTool(
name = TOOL_SET_LIGHT_STATE,
description =
"Sets the light state for a given light. Brightness ranges from 0 to 100.",
inputSchema = inputSchema,
) { input ->
val lightId = input.arguments["lightId"]?.jsonPrimitive?.content
if (lightId.isNullOrBlank()) {
logger.error("Light ID is missing or invalid.")
return@addTool CallToolResult(
content = listOf(TextContent("Error: Missing or invalid light ID")),
)
}
val on = input.arguments["on"]?.jsonPrimitive?.booleanOrNull
val brightness = input.arguments["brightness"]?.jsonPrimitive?.doubleOrNull
val colorGamutX = input.arguments["colorGamutX"]?.jsonPrimitive?.doubleOrNull
val colorGamutY = input.arguments["colorGamutY"]?.jsonPrimitive?.doubleOrNull
val dimming = input.arguments["dimming"]?.jsonPrimitive?.doubleOrNull
hueClient.setLightState(lightId, on, brightness, colorGamutX, colorGamutY, dimming)
logger.info("Light state updated for lightId=$lightId")
CallToolResult(content = listOf(TextContent("Done!")))
}
}
The Struggle
Well it didn't work first go because I am an idiot.
./gradlew run
The server just exited with no output. This was a symptom of some leaking to standard out so I installed IntelliJ so I could use the debugger easily and sure enough it just worked.
Plan B
So it seemed clear gradle was the problem so I decided to see if I could run it from the command line. In the default behavour you get a tar and zip in the build/distributions folder but if you add this in kotlin build you then get a jar
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes["Main-Class"] = application.mainClass.get()
}
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
Then you can run this with
java -jar ./build/libs/huelight-mcp-1.0-SNAPSHOT.jar
The Real Solution
I think gradle will always be like emacs, I am not going to like it. The real solution is to run the gradle task
./gradlew app:installDist
This then generates a binary in build/install/app/bin/app which you can use