mirror of
https://github.com/wolfpld/tracy.git
synced 2026-09-01 07:58:31 +00:00
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.
This commit is contained in:
1
extra/mcp/requirements.txt
Normal file
1
extra/mcp/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
mcp>=2.0.0,<3
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user