Model Context Protocol (MCP): Difference between revisions
No edit summary |
|||
Line 178: | Line 178: | ||
}, | }, | ||
... | ... | ||
</syntaxhighlight> | |||
=Writing a Server (Kotlin)= | |||
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. [https://github.com/bibble235/ktmcptest here]. The main part is shown below | |||
<syntaxhighlight lang="kotlin"> | |||
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!"))) | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> |
Revision as of 04:54, 24 March 2025
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)
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 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!")))
}
}