From e670bd18f7c6a9d2b8ca181d7c0bc4601e263e96 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 21 Aug 2026 20:42:28 -0700 Subject: [PATCH] feat(mcp): migrate to MCP Python SDK v2 `pip install mcp` now installs 2.x by default (v1 is maintenance-only going forward), and nothing in this repo pinned a version, so a fresh install already broke: v2 removed mcp.server.fastmcp entirely, no compat shim. - Import/class: mcp.server.fastmcp.FastMCP -> mcp.server.mcpserver.MCPServer. - Transport options (host, port, sse_path, streamable_http_path) moved off the constructor and the settings object onto run(); mutating mcp_server.settings.port now raises ValueError. - _http_ping and the startup message read _SSE_PATH/ _STREAMABLE_HTTP_PATH constants instead of settings.sse_path/ settings.streamable_http_path -- v2 no longer exposes them to read back. Values match the SDK's own defaults in both versions ("/sse", "/mcp"), so served paths are unchanged. - Adds extra/mcp/requirements.txt (mcp>=2.0.0,<3) -- there was no dependency manifest at all before this. Decorator API is unchanged; every tool/resource here was already async def, so the "sync handlers now run on worker threads" change doesn't apply. Verified in an isolated venv (mcp 2.0.0): module imports cleanly with all 10 tools and 2 resources registered, the server starts and serves streamable-http on the expected path, and _http_ping correctly reports a running instance as alive. --- extra/mcp/requirements.txt | 1 + extra/mcp/start_mcp.sh | 2 ++ extra/mcp/tracy_mcp.py | 29 +++++++++++++++++------------ 3 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 extra/mcp/requirements.txt diff --git a/extra/mcp/requirements.txt b/extra/mcp/requirements.txt new file mode 100644 index 00000000..29b14f61 --- /dev/null +++ b/extra/mcp/requirements.txt @@ -0,0 +1 @@ +mcp>=2.0.0,<3 diff --git a/extra/mcp/start_mcp.sh b/extra/mcp/start_mcp.sh index 2b50afbe..6bed2fea 100644 --- a/extra/mcp/start_mcp.sh +++ b/extra/mcp/start_mcp.sh @@ -1,6 +1,8 @@ #!/bin/sh # Start the Tracy MCP server. # +# First run: pip install -r requirements.txt (next to this script). +# # Set PYTHONPATH to the directory containing TracyServerBindings.so/.pyd. # Adjust the Release/Debug suffix to match your CMake build configuration. PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$(dirname "$0")/../../build/python/Release" diff --git a/extra/mcp/tracy_mcp.py b/extra/mcp/tracy_mcp.py index a185c549..5826ad65 100644 --- a/extra/mcp/tracy_mcp.py +++ b/extra/mcp/tracy_mcp.py @@ -20,7 +20,7 @@ import urllib.request import uuid from contextlib import asynccontextmanager, redirect_stdout -import mcp.server.fastmcp as fastmcp +from mcp.server.mcpserver import MCPServer # Suppress noisy ASGI shutdown errors known to occur with SSE and Control-C. # These occur when Starlette attempts to send a 500 error after the loop is cancelled @@ -35,6 +35,10 @@ _PID_FILE = os.path.join(_HERE, "tracy_mcp.pid") _CRASH_LOG_FILE = os.path.join(_HERE, "tracy_mcp.crash.log") _PREFERRED_PORT = int(os.environ.get("TRACY_MCP_PORT", "47380")) _TRANSPORT = os.environ.get("TRACY_MCP_TRANSPORT", "streamable-http").strip().lower() +# MCPServer.run()'s own defaults -- tracked here too since v2's Settings no +# longer exposes them for us to read back (SDK v1 -> v2 migration). +_SSE_PATH = "/sse" +_STREAMABLE_HTTP_PATH = "/mcp" # Shared documentation surfaces. system.prompt.md is Tracy Assist's source # system prompt; exposing it as an MCP resource keeps analysis guidance in @@ -166,7 +170,7 @@ def _http_ping(port: int, timeout_s: float = 2.0) -> bool: error one, proves the transport is alive; only a timeout or refused connection means it's not. """ - path = mcp_server.settings.sse_path if _TRANSPORT == "sse" else mcp_server.settings.streamable_http_path + path = _SSE_PATH if _TRANSPORT == "sse" else _STREAMABLE_HTTP_PATH try: urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=timeout_s) return True @@ -379,7 +383,7 @@ async def _sweep_loop() -> None: @asynccontextmanager -async def _lifespan(_server: fastmcp.FastMCP): +async def _lifespan(_server: MCPServer): sweep_task = asyncio.create_task(_sweep_loop()) try: yield @@ -391,7 +395,7 @@ async def _lifespan(_server: fastmcp.FastMCP): pass -mcp_server = fastmcp.FastMCP("Tracy Profiler", lifespan=_lifespan) +mcp_server = MCPServer("Tracy Profiler", lifespan=_lifespan) executor = concurrent.futures.ThreadPoolExecutor(max_workers=4) instances: dict[str, TracyInstance] = {} @@ -919,17 +923,18 @@ if __name__ == "__main__": port = _find_free_port() _write_pid_and_port(port) - path = ( - mcp_server.settings.sse_path - if _TRANSPORT == "sse" - else mcp_server.settings.streamable_http_path - ) + path = _SSE_PATH if _TRANSPORT == "sse" else _STREAMABLE_HTTP_PATH print(f"Tracy MCP listening on http://127.0.0.1:{port}{path}", file=sys.stderr) - mcp_server.settings.host = "127.0.0.1" - mcp_server.settings.port = port + # v2's MCPServer takes transport options on run(), not the constructor + # or a mutable settings object (SDK v1 -> v2 migration). + run_kwargs = {"host": "127.0.0.1", "port": port} + if _TRANSPORT == "sse": + run_kwargs["sse_path"] = _SSE_PATH + else: + run_kwargs["streamable_http_path"] = _STREAMABLE_HTTP_PATH try: - mcp_server.run(transport=_TRANSPORT) + mcp_server.run(transport=_TRANSPORT, **run_kwargs) except KeyboardInterrupt: print("\nTracy MCP server stopped.", file=sys.stderr) sys.exit(0)