withSurfa 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.
For coding agents: follow the steps in order. The only code change is wrapping the
McpServer (or low-level Server) constructor call with withSurfa(...) and adding one import. Don’t change tool handlers.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 written in TypeScript or JavaScript using
@modelcontextprotocol/sdk1.5.0 or later (McpServerorServer). Tested from 1.5.0 to 1.32.1. - Node.js 18 or later.
- 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 folder:
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. If you use the low-level API, wrap that instead:
withSurfa, before you call connect():withSurfa(new Server(...)).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 Vercel or Railway dashboard).A
.env file only works if something loads it, such as dotenv or node --env-file=.env. Node 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 options:
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. Long strings are shortened. - Latency in milliseconds
- Success or error, with the error message
- Response size in bytes
claude-ai 0.1.0) as reported when it connects.
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).
Your server is never affected
withSurfaonly observes requests and responses; it never changes them.- Uploads run in the background, one at a time. If Surfa is slow, down or rate limiting, events are dropped; your tool calls aren’t delayed and nothing throws. At most
maxQueueSizeevents are held in memory. - Overhead is a few microseconds per tool call.
- Logs go to stderr only, so stdio servers keep a clean stdout.
Sessions and shutdown
A Surfa session ends when the MCP connection closes, or after 15 minutes without tool calls (the next call starts a new session). Surfa computes per-session metrics such as redundant calls and batch opportunities when a session ends. 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, so little is ever waiting in memory.
- When a client disconnects, the session ends and uploads straight away. This covers stdio clients closing the connection (Claude Desktop), Claude Code stopping the server with
SIGINT, and containers stopped withSIGTERM. After uploading (at most one second), 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.
SIGINT or SIGTERM handler, withSurfa leaves exiting to you. Add one line before you exit, so the session ends cleanly:
Remote and serverless servers
withSurfa works the same over stdio, Streamable HTTP and the older SSE transport.
-
Long-running HTTP servers (Fly, Railway, a VPS, Docker): if you create one server per MCP session, put
withSurfainside that factory function. Each MCP session becomes one Surfa session. - Stateless HTTP servers (a new server for every request): every tool call is recorded, but each call shows up as its own session, without the client’s name.
-
Serverless functions (Vercel, AWS Lambda, Netlify): the platform can freeze your function 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. - Set
debug: truein the options to log every upload. - Make sure
withSurfa(...)wraps the server beforeconnect()is called on it. - 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.Using Python?
Using Python?
See the Python one-line setup.