Model Context Protocol (MCP): Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
No edit summary
 
(7 intermediate revisions by the same user not shown)
Line 8: Line 8:
=Servers=
=Servers=
Example server site is [https://github.com/appcypher/awesome-mcp-servers awesome-mcp-servers]
Example server site is [https://github.com/appcypher/awesome-mcp-servers awesome-mcp-servers]
=Installing on Ubuntu 24.04=
=Installing Claude on Ubuntu 24.04=
This was reasonably painless. Goto [https://github.com/aaddrick/claude-desktop-debian here]. When installing the instructions are
This was reasonably painless. Goto [https://github.com/aaddrick/claude-desktop-debian here]. When installing the instructions are
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Line 22: Line 22:
sudo chmod 4755 /usr/local/lib/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /usr/local/lib/node_modules/electron/dist/chrome-sandbox
</syntaxhighlight>
</syntaxhighlight>
=Writing a Server (Python)=
To demonstrate how easy this is [https://youtu.be/wa_A0qY0anA?si=8sWmPyiltuOBhJgB Matthew Berman]. There is just this code and a config
<syntaxhighlight lang="py">
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)
</syntaxhighlight>
Build an executable as you cannot just use pip3 or pipx to install globally.
<syntaxhighlight lang="py">
pip3 install pyinstaller
pyinstaller --onefile main.py
</syntaxhighlight>
This produces a file in the dist directory.<br>
<br>
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
<syntaxhighlight lang="py">
{
  "mcpServers": {
    "count-r": {
      "command": "/home/iwiseman/dev/projects/mcpServer/pyFastMCPTest/dist/main",
      "args": [
        ""
      ],
      "host": "127.0.0.1",
      "port": 8080,
      "timeout": 30000
    }
  }
}
</syntaxhighlight>
If all goes well, when you restart Claude it will show<br>
[[File:Claude1.png]]<br>
=Writing a Server (Typescript)=
Well always a challenge but they will never win. The challenge is to avoid standard out. First the server.
<syntaxhighlight lang="ts">
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);
</syntaxhighlight>
All went well and you can test this and other MCPs with the mcp inspector
<syntaxhighlight lang="bash">
npx @modelcontextprotocol/inspector
</syntaxhighlight>
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
<syntaxhighlight lang="text">
[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)"
}
</syntaxhighlight>
This is caused by npm run start going to standard out. If you want to run node you can do this.
<syntaxhighlight lang="json">
{
  "mcpServers": {
    "add": {
      "command": "node",
      "args": [
        "/home/iwiseman/dev/projects/mcpServer/tsMcpTest/dist/app.js"
      ]
    },
...
</syntaxhighlight>
=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. [https://github.com/bibble235/ktmcptest here].
==The code==
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>
==The Struggle==
Well it didn't work first go because I am an idiot.
<syntaxhighlight lang="bash">
./gradlew run
</syntaxhighlight>
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
<syntaxhighlight lang="kotlin">
tasks.jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    manifest {
        attributes["Main-Class"] = application.mainClass.get()
    }
    from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
</syntaxhighlight>
Then you can run this with
<syntaxhighlight lang="kotlin">
java -jar ./build/libs/huelight-mcp-1.0-SNAPSHOT.jar
</syntaxhighlight>
==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
<syntaxhighlight lang="bash">
./gradlew app:installDist
</syntaxhighlight>
This then generates a binary in build/install/app/bin/app which you can use

Latest revision as of 05:07, 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)

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