Skip to main content
Wrap your MCP server with 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
Then put your real key (from Settings → API Keys) where the agent left the placeholder, and check it’s working (step 4 below). To do it by hand instead, follow the steps below.

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): FastMCP from mcp.server.fastmcp (SDK 1.x), MCPServer from mcp.server.mcpserver (SDK 2.x), or the low-level Server. Tested with 1.9 to 2.3.
  • 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 with_surfa, before the server starts:
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(mcp) on the next line instead.
3

Set your API key

Set SURFA_API_KEY in the environment your server runs in:
For a stdio server started by Claude Desktop, Cursor or another client, add it to that client’s MCP config under env:
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 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:

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_key or credential is replaced with [REDACTED], at any depth. Numbers and booleans under those keys (like max_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
For each session: the client’s name and version (for example 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.
Surfa also redacts email addresses, phone numbers, card numbers, US Social Security numbers and key-like strings on its side before storing events. This is on by default and can be changed under Settings → Privacy. See Privacy and PII.

Your server is never affected

  • with_surfa only 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_size events 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.
If your server has its own SIGTERM handler, with_surfa leaves it alone. Add one line to it so the session ends cleanly:
  • 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

  1. Check your server’s stderr for a line starting with [surfa]. SURFA_API_KEY is not set means the key isn’t reaching the server process. For stdio servers, put it in the MCP client’s env config, not your shell.
  2. Pass debug=True to log every upload.
  3. Make sure with_surfa(...) runs before the server starts (mcp.run()).
  4. Check pip show surfa-ingest says 0.3.0 or later.
  5. Sessions appear after the first tool call. Connecting and listing tools alone doesn’t create a session.
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 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(...).
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.