> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfa.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Python: one-line setup

> Send a Python MCP server's live tool calls to Surfa with with_surfa(server)

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.

<Info>
  **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.
</Info>

## 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:

```text Prompt theme={null}
Add Surfa analytics to this MCP server by following https://docs.surfa.dev/python-setup.md exactly (plain Markdown; read the whole page): install surfa-ingest, wrap the server where it's created with with_surfa(...), and add SURFA_API_KEY wherever this server's environment is configured, using the placeholder sk_live_your_key_here (if nothing in the project sets environment variables, don't create a file nothing reads: tell me where the key should go). Don't change any tool code or add decorators. When you're done, list the files you changed and tell me how to check it's working.
```

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](https://app.surfa.dev/login?mode=signup)) and an ingest key from **Settings → API Keys** in the dashboard. Keys start with `sk_live_`.

## Setup

<Steps>
  <Step title="Install the package">
    In your server's project:

    <CodeGroup>
      ```bash pip theme={null}
      pip install surfa-ingest
      ```

      ```bash uv theme={null}
      uv add surfa-ingest
      ```

      ```bash poetry theme={null}
      poetry add surfa-ingest
      ```
    </CodeGroup>

    You need version 0.3.0 or later.
  </Step>

  <Step title="Wrap your server (the one line)">
    Find where your server is created and wrap it with `with_surfa`, **before** the server starts:

    <CodeGroup>
      ```python FastMCP theme={null}
      from fastmcp import FastMCP
      from surfa_ingest import with_surfa

      mcp = with_surfa(FastMCP("my-server"))

      # Register tools and run exactly as before
      ```

      ```python Official SDK 1.x theme={null}
      from mcp.server.fastmcp import FastMCP
      from surfa_ingest import with_surfa

      mcp = with_surfa(FastMCP("my-server"))
      ```

      ```python Official SDK 2.x theme={null}
      from mcp.server.mcpserver import MCPServer
      from surfa_ingest import with_surfa

      mcp = with_surfa(MCPServer("my-server"))
      ```

      ```python Low-level Server theme={null}
      from mcp.server import Server
      from surfa_ingest import with_surfa

      server = with_surfa(Server("my-server"))
      ```
    </CodeGroup>

    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.
  </Step>

  <Step title="Set your API key">
    Set `SURFA_API_KEY` in the environment your server runs in:

    ```bash theme={null}
    SURFA_API_KEY=sk_live_your_key_here
    ```

    For a stdio server started by Claude Desktop, Cursor or another client, add it to that client's MCP config under `env`:

    ```json theme={null}
    {
      "mcpServers": {
        "my-server": {
          "command": "uv",
          "args": ["run", "/absolute/path/to/your-server/server.py"],
          "env": { "SURFA_API_KEY": "sk_live_your_key_here" }
        }
      }
    }
    ```

    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).

    <Note>
      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.
    </Note>
  </Step>

  <Step title="Check it's working">
    Start your server and call two or three tools from your MCP client or [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

    <CodeGroup>
      ```bash python theme={null}
      npx @modelcontextprotocol/inspector -e SURFA_API_KEY=sk_live_your_key_here python server.py
      ```

      ```bash uv theme={null}
      npx @modelcontextprotocol/inspector -e SURFA_API_KEY=sk_live_your_key_here uv run server.py
      ```
    </CodeGroup>

    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.

    <Check>
      You're done when your tool calls show up under **Sessions** with the right tool names.
    </Check>
  </Step>
</Steps>

## Configuration

| Environment variable | Required | What it does |
| - | - | - |
| `SURFA_API_KEY` | Yes | Your workspace ingest key. Without it, `with_surfa` does nothing and prints one warning. |
| `SURFA_API_URL` | No | Surfa's address. Default: `https://app.surfa.dev`. |

If you don't use environment variables, pass the same settings as keyword arguments:

```python theme={null}
mcp = with_surfa(
    FastMCP("my-server"),
    api_key=settings.surfa_key,
    redact_keys=["ssn", "account_number"],  # extra argument names to mask
)
```

<Accordion title="All options">
  | Option | Default | What it does |
  | - | - | - |
  | `api_key` | `SURFA_API_KEY` | Workspace ingest key |
  | `api_url` | `SURFA_API_URL`, then `https://app.surfa.dev` | Where events are sent |
  | `capture_args` | `True` | Send tool arguments (with secrets masked). `False` sends tool names and timings only |
  | `redact_keys` | none | More argument names to mask, on top of the built-in list |
  | `session_idle_timeout_s` | 900 (15 minutes) | End a Surfa session after this long without tool calls. `0` ends sessions only on disconnect or exit |
  | `max_queue_size` | 1000 | Most events held in memory while Surfa is slow or unreachable |
  | `flush_delay_s` | 0.25 | Upload this long after a tool call, batching what arrives meanwhile. `0` uploads only when you call `flush_surfa()` |
  | `timeout_s` | 5 | Upload request timeout |
  | `debug` | `False` | Log upload activity to stderr |
</Accordion>

## 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).

<Note>
  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.
</Note>

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](/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:

```python theme={null}
import sys
from surfa_ingest import shutdown_surfa

def on_sigterm(signum, frame):
    shutdown_surfa()  # add this
    sys.exit(0)
```

<Accordion title="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.
</Accordion>

## 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:

```python theme={null}
from surfa_ingest import flush_surfa

# at the end of your handler
flush_surfa()  # waits at most 5 seconds

# in async code
await asyncio.to_thread(flush_surfa)
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing shows up in Surfa">
    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.
  </Accordion>

  <Accordion title="'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.
  </Accordion>

  <Accordion title="'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(...)`.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Using TypeScript?">
    See the [TypeScript one-line setup](/typescript-setup).
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.