Quickstart
Instrument a Python MCP server with spanly in one line.
This walks through mounting SpanlyMiddleware in front of a Python MCP
server that speaks ASGI (FastAPI, Starlette, or the ASGI app the mcp
package's streamable_http_app() returns). SpanlyMiddleware inspects
requests under /mcp and /sse by default, tees the request and
response bytes, and never mutates a body or delays a message.
1. Install
pip install spanly2. Set the API key
export SPANLY_API_KEY=spanly_us_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxGet a key by signing in at spanly.com, opening
your project, and going to Settings → API keys. The region is
encoded in the key prefix (spanly_us_ / spanly_eu_) and
auto-detected, so there is nothing else to configure.
3. Mount the middleware
import osfrom spanly import SpanlyMiddlewareapp.add_middleware(SpanlyMiddleware, api_key=os.environ["SPANLY_API_KEY"])add_middleware defers construction until the app builds its
middleware stack, so SpanlyMiddleware receives your ASGI app as its
first positional argument automatically. If your framework does not
expose add_middleware (a bare ASGI app, for instance), wrap it
directly instead:
import os
from spanly import SpanlyMiddleware
app = SpanlyMiddleware(app, api_key=os.environ["SPANLY_API_KEY"])Either way, traffic on /mcp and /sse is now reported to your
Spanly project.
Full example: FastMCP over Streamable HTTP
import os
from mcp.server.fastmcp import FastMCP
from spanly import SpanlyMiddleware
mcp = FastMCP("demo-http-server")
@mcp.tool()
async def echo(input: str) -> str:
return input
app = mcp.streamable_http_app()
app = SpanlyMiddleware(app, api_key=os.environ["SPANLY_API_KEY"])
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3000)Full example: FastAPI
import os
from fastapi import FastAPI
from spanly import SpanlyMiddleware
app = FastAPI()
# … mount your MCP server's ASGI app at /mcp …
app.add_middleware(SpanlyMiddleware, api_key=os.environ["SPANLY_API_KEY"])Sessionless servers
When your server runs stateless (no Mcp-Session-Id on its
initialize response), SpanlyMiddleware injects a synthetic one
(prefixed spanly-) on the response so requests still group into
sessions. This is a real, on-the-wire header, unlike the TypeScript
SDK's telemetry-only grouping. See
Session tracking for the full behavior, and
opt out with inject_session_id=False if you'd rather Spanly never
touch a response header.
Next steps
- The API reference covers every option
SpanlyMiddlewareaccepts. - Examples cover end-user attribution, error reporting, and a local test harness.
- If you'd rather not change code, the CLI wraps the same server with the same capture behavior.
- Nothing showing up in the dashboard? See the troubleshooting guide.