with_surfa and every tool call it handles shows up in Surfa: tool name, arguments (secrets masked), latency, errors and response size. You don’t write any code per tool and don’t add decorators.
For coding agents: follow the steps in order. The only code change is wrapping the server constructor call (
FastMCP(...), MCPServer(...) or Server(...)) with with_surfa(...) and adding one import. Don’t change tool functions or add decorators.Set up with your coding agent
Open your server’s project in Claude Code, Cursor, Copilot, Codex or any other coding agent and paste this:Prompt
Prerequisites
- An MCP server in Python 3.10 or later, built with one of:
- FastMCP (
pip install fastmcp):from fastmcp import FastMCP. Tested with 2.8 to 4.1. - The official MCP SDK (
pip install mcp):FastMCPfrommcp.server.fastmcp(SDK 1.x),MCPServerfrommcp.server.mcpserver(SDK 2.x), or the low-levelServer. Tested with 1.9 to 2.3.
- FastMCP (
- A Surfa account (sign up) and an ingest key from Settings → API Keys in the dashboard. Keys start with
sk_live_.
Setup
1
Install the package
In your server’s project:You need version 0.3.0 or later.
2
Wrap your server (the one line)
Find where your server is created and wrap it with Tools registered before or after this line are all captured, sync and async alike. If the server is created somewhere you can’t wrap, call
with_surfa, before the server starts:with_surfa(mcp) on the next line instead.3
Set your API key
Set For a stdio server started by Claude Desktop, Cursor or another client, add it to that client’s MCP config under Use an absolute path: most clients don’t start the server from your project folder.For a hosted server, set it in your host’s secret settings (for example
SURFA_API_KEY in the environment your server runs in:env:fly secrets set, or the Railway or Render dashboard).A
.env file only works if something loads it, such as python-dotenv or uv run --env-file .env. Python doesn’t read it on its own, and a stdio server started by a client won’t see it unless your code loads it. Don’t commit the key.4
Check it's working
Start your server and call two or three tools from your MCP client or MCP Inspector:Within a minute, the calls appear in the Surfa dashboard:
- Analytics shows the tools and their latency.
- Sessions lists one session per MCP connection. Open it to see each tool call with its arguments, latency and any error.
You’re done when your tool calls show up under Sessions with the right tool names.
Configuration
If you don’t use environment variables, pass the same settings as keyword arguments:
All options
All options
What’s captured
For each tool call:- Tool name
- Arguments, with secrets masked on your server, before sending: any argument key containing
password,passwd,secret,token,api_key/apikey,authorization,cookie,private_key,access_keyorcredentialis replaced with[REDACTED], at any depth. Numbers and booleans under those keys (likemax_tokens: 500) are kept. Long strings are shortened. - Latency in milliseconds
- Success or error, with the error message the client received
- Response size in bytes
claude-ai 0.1.0) and the protocol version.
Not captured:
- Tool results. Only their size is sent.
- Your server’s environment, headers or auth tokens.
- Calls to resources and prompts (tool calls only, for now).
The official SDK 2.x
MCPServer doesn’t send exception details to clients, so a failing tool shows as Error executing tool <name> rather than the exception message. FastMCP and SDK 1.x report the message.Your server is never affected
with_surfaonly observes tool calls; it never changes arguments or results.- Uploads run on a background thread, one at a time. If Surfa is slow, down or rate limiting, events are dropped; your tool calls aren’t delayed and nothing is raised. At most
max_queue_sizeevents are held in memory. - Overhead is well under a millisecond per tool call.
- Messages go to stderr only, so stdio servers keep a clean stdout.
Sessions and shutdown
Surfa groups tool calls into sessions and computes per-session metrics, such as redundant calls and batch opportunities, when a session ends. A session ends when the client disconnects, after 15 minutes without tool calls (the next call starts a new session), or when your process exits. You don’t need to do anything for this in most setups:- Tool calls are uploaded about a quarter of a second after they happen.
- On a normal exit, including Ctrl-C, open sessions end and the last events are uploaded (waiting at most two seconds).
- On
SIGTERM(containers being stopped, Claude Code shutting a server down), the same happens within one second, then your process exits exactly as it would have without Surfa. - If a server is killed before it can say goodbye (a crash,
kill -9, a frozen serverless function), Surfa ends the session itself after 15 minutes without activity.
SIGTERM handler, with_surfa leaves it alone. Add one line to it so the session ends cleanly:
How sessions are grouped
How sessions are grouped
- stdio, and HTTP with the classic protocol: one session per client connection.
- The stateless MCP protocol (2026-07-28), which SDK 2.x clients speak, has no connection: over stdio, one server process is one session; over HTTP, calls are grouped by the HTTP session ID when the client sends one.
- Stateless HTTP with no session ID: every tool call is recorded, but each one is its own session.
- Over HTTP, a session ends after 15 minutes without tool calls, or when the server stops.
Remote and serverless servers
with_surfa works the same over stdio, Streamable HTTP and SSE: mcp.run(transport="http") needs no other change.
Serverless functions (AWS Lambda, Vercel, Google Cloud Run jobs) can freeze your process as soon as the response is sent, before the background upload runs. Upload before returning:
Troubleshooting
Nothing shows up in Surfa
Nothing shows up in Surfa
- Check your server’s stderr for a line starting with
[surfa].SURFA_API_KEY is not setmeans the key isn’t reaching the server process. For stdio servers, put it in the MCP client’senvconfig, not your shell. - Pass
debug=Trueto log every upload. - Make sure
with_surfa(...)runs before the server starts (mcp.run()). - Check
pip show surfa-ingestsays 0.3.0 or later. - Sessions appear after the first tool call. Connecting and listing tools alone doesn’t create a session.
'Surfa rejected the ingest key'
'Surfa rejected the ingest key'
The warning says why, for example
API key not found (key_not_found) or API key has been revoked (key_revoked), and shows the start of the key that was used. Make sure SURFA_API_KEY holds the full key shown once when you created it (the keys list only shows the first few characters). Create a new key under Settings → API Keys if needed, and restart your server: tracking stays off until the restart.'with_surfa() expects a FastMCP, MCPServer or Server'
'with_surfa() expects a FastMCP, MCPServer or Server'
with_surfa was given something other than the server object, for example a tool function or an ASGI app. Pass the object you created with FastMCP(...), MCPServer(...) or Server(...).Already using SurfaClient or @track_tool?
Already using SurfaClient or @track_tool?
Replace the decorators and manual
track() calls with the one with_surfa(...) line. It records the fields Surfa’s metrics read (arguments as input, call IDs, sessions per client connection), which manual events often miss. SurfaClient keeps working for code that isn’t an MCP server.Using TypeScript?
Using TypeScript?
See the TypeScript one-line setup.