From f69aa9482657ce197cfbb7b2d5be97343896c456 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 16 Mar 2026 10:32:16 -0400 Subject: [PATCH] feat(dx): add fastmcp.json configs, module-level tool registration, tool timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fastmcp.http.json and fastmcp.stdio.json declarative server configs for streamable-http (:6970) and stdio transports respectively - Move register_all_modules() to module level in server.py so `fastmcp run server.py --reload` discovers the fully-wired mcp object without going through run_server() — tools registered exactly once - Add timeout=120 to @mcp.tool() decorator as a global safety net; any hung subaction returns a clean MCP error instead of hanging forever - Document fastmcp run --reload, fastmcp list, fastmcp call in README - Bump version 1.0.1 → 1.1.0 Co-authored-by: Claude --- .claude-plugin/plugin.json | 2 +- README.md | 22 ++++++++++++++++++++++ fastmcp.http.json | 23 +++++++++++++++++++++++ fastmcp.stdio.json | 20 ++++++++++++++++++++ pyproject.toml | 2 +- unraid_mcp/server.py | 10 +++++++--- unraid_mcp/tools/unraid.py | 2 +- uv.lock | 2 +- 8 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 fastmcp.http.json create mode 100644 fastmcp.stdio.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7544193..f9d1354 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "unraid", "description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring", - "version": "1.0.1", + "version": "1.1.0", "author": { "name": "jmagar", "email": "jmagar@users.noreply.github.com" diff --git a/README.md b/README.md index 3935e76..0bdaf18 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,28 @@ uv run unraid-mcp-server # Or run via module directly uv run -m unraid_mcp.main + +# Hot-reload dev server (restarts on file changes) +fastmcp run fastmcp.http.json --reload + +# Run via named config files +fastmcp run fastmcp.http.json # streamable-http on :6970 +fastmcp run fastmcp.stdio.json # stdio transport +``` + +### Ad-hoc Tool Testing (fastmcp CLI) +```bash +# Introspect the running server +fastmcp list http://localhost:6970/mcp +fastmcp list http://localhost:6970/mcp --input-schema + +# Call a tool directly (HTTP) +fastmcp call http://localhost:6970/mcp unraid action=health subaction=check +fastmcp call http://localhost:6970/mcp unraid action=docker subaction=list + +# Call without a running server (stdio config) +fastmcp list fastmcp.stdio.json +fastmcp call fastmcp.stdio.json unraid action=health subaction=check ``` --- diff --git a/fastmcp.http.json b/fastmcp.http.json new file mode 100644 index 0000000..5dc1f90 --- /dev/null +++ b/fastmcp.http.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", + "source": { + "path": "unraid_mcp/server.py", + "entrypoint": "mcp" + }, + "environment": { + "type": "uv", + "python": "3.12", + "editable": ["."] + }, + "deployment": { + "transport": "http", + "host": "0.0.0.0", + "port": 6970, + "path": "/mcp", + "log_level": "INFO", + "env": { + "UNRAID_API_URL": "${UNRAID_API_URL}", + "UNRAID_API_KEY": "${UNRAID_API_KEY}" + } + } +} diff --git a/fastmcp.stdio.json b/fastmcp.stdio.json new file mode 100644 index 0000000..49c92ac --- /dev/null +++ b/fastmcp.stdio.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", + "source": { + "path": "unraid_mcp/server.py", + "entrypoint": "mcp" + }, + "environment": { + "type": "uv", + "python": "3.12", + "editable": ["."] + }, + "deployment": { + "transport": "stdio", + "log_level": "INFO", + "env": { + "UNRAID_API_URL": "${UNRAID_API_URL}", + "UNRAID_API_KEY": "${UNRAID_API_KEY}" + } + } +} diff --git a/pyproject.toml b/pyproject.toml index 1d6b93d..0609d63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "hatchling.build" # ============================================================================ [project] name = "unraid-mcp" -version = "1.0.1" +version = "1.1.0" description = "MCP Server for Unraid API - provides tools to interact with an Unraid server's GraphQL API" readme = "README.md" license = {file = "LICENSE"} diff --git a/unraid_mcp/server.py b/unraid_mcp/server.py index d4e7af5..623b312 100644 --- a/unraid_mcp/server.py +++ b/unraid_mcp/server.py @@ -85,6 +85,10 @@ mcp = FastMCP( # Note: SubscriptionManager singleton is defined in subscriptions/manager.py # and imported by resources.py - no duplicate instance needed here +# Register all modules at import time so `fastmcp run server.py --reload` can +# discover the fully-configured `mcp` object without going through run_server(). +# run_server() no longer calls this — tools are registered exactly once here. + def register_all_modules() -> None: """Register all tools and resources with the MCP instance.""" @@ -103,6 +107,9 @@ def register_all_modules() -> None: raise +register_all_modules() + + def run_server() -> None: """Run the MCP server with the configured transport.""" # Validate required configuration before anything else @@ -125,9 +132,6 @@ def run_server() -> None: "Only use this in trusted networks or for development." ) - # Register all modules - register_all_modules() - logger.info( f"Starting Unraid MCP Server on {UNRAID_MCP_HOST}:{UNRAID_MCP_PORT} using {UNRAID_MCP_TRANSPORT} transport..." ) diff --git a/unraid_mcp/tools/unraid.py b/unraid_mcp/tools/unraid.py index 621b45f..a236688 100644 --- a/unraid_mcp/tools/unraid.py +++ b/unraid_mcp/tools/unraid.py @@ -1722,7 +1722,7 @@ UNRAID_ACTIONS = Literal[ def register_unraid_tool(mcp: FastMCP) -> None: """Register the single `unraid` tool with the FastMCP instance.""" - @mcp.tool() + @mcp.tool(timeout=120) async def unraid( action: UNRAID_ACTIONS, subaction: str, diff --git a/uv.lock b/uv.lock index 64dc611..b766354 100644 --- a/uv.lock +++ b/uv.lock @@ -1572,7 +1572,7 @@ wheels = [ [[package]] name = "unraid-mcp" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } dependencies = [ { name = "fastapi" },