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

# TypeScript: one-line setup

> Send a TypeScript MCP server's live tool calls to Surfa with withSurfa(server)

Wrap your MCP server with `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.

<Info>
  **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.
</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/typescript-setup.md exactly (plain Markdown; read the whole page): install surfa-ingest, wrap the server where it's created with withSurfa(...), 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 written in TypeScript or JavaScript using `@modelcontextprotocol/sdk` 1.5.0 or later (`McpServer` or `Server`). Tested from 1.5.0 to 1.32.1.
* Node.js 18 or later.
* 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 folder:

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

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

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

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

    ```ts theme={null}
    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { withSurfa } from 'surfa-ingest';

    const server = withSurfa(new McpServer({ name: 'my-server', version: '1.0.0' }));

    // Register tools and connect exactly as before
    ```

    Tools registered before or after this line are all captured. If you use the low-level API, wrap that instead: `withSurfa(new Server(...))`.

    <Tip>
      If your HTTP server creates a new server for each session (a `getServer()` factory, as in the SDK's Streamable HTTP examples), put `withSurfa` inside the factory. Each MCP session then becomes one Surfa session.
    </Tip>
  </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": "node",
          "args": ["/absolute/path/to/your-server/dist/index.js"],
          "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 Vercel or Railway dashboard).

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

    ```bash theme={null}
    npx @modelcontextprotocol/inspector -e SURFA_API_KEY=sk_live_your_key_here node dist/index.js
    ```

    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, `withSurfa` 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 options:

```ts theme={null}
const server = withSurfa(new McpServer({ name: 'my-server', version: '1.0.0' }), {
  apiKey: config.surfaKey,
  redactKeys: ['ssn', 'account_number'], // extra argument names to mask
});
```

<Accordion title="All options">
  | Option | Default | What it does |
  | - | - | - |
  | `apiKey` | `SURFA_API_KEY` | Workspace ingest key |
  | `apiUrl` | `SURFA_API_URL`, then `https://app.surfa.dev` | Where events are sent |
  | `captureArgs` | `true` | Send tool arguments (with secrets masked). `false` sends tool names and timings only |
  | `redactKeys` | none | More argument names to mask, on top of the built-in list |
  | `sessionIdleTimeoutMs` | 15 minutes | End a Surfa session after this long without tool calls. `0` ends sessions only on disconnect |
  | `maxQueueSize` | 1000 | Most events held in memory while Surfa is slow or unreachable |
  | `flushDelayMs` | 250 | Upload this long after a tool call, batching what arrives meanwhile. `0` uploads only when you call `flushSurfa()` |
  | `timeoutMs` | 5000 | 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. Long strings are shortened.
* Latency in milliseconds
* Success or error, with the error message
* Response size in bytes

**For each MCP connection:** a session, with the client's name and version (for example `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).

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

* `withSurfa` only 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 `maxQueueSize` events 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 with `SIGTERM`. 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.

**If your server has its own `SIGINT` or `SIGTERM` handler**, `withSurfa` leaves exiting to you. Add one line before you exit, so the session ends cleanly:

```ts theme={null}
import { shutdownSurfa } from 'surfa-ingest';

process.on('SIGTERM', async () => {
  await server.close();
  await shutdownSurfa(); // add this
  process.exit(0);
});
```

## 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 `withSurfa` inside 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:

  ```ts theme={null}
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
  import { withSurfa, flushSurfa } from 'surfa-ingest';
  import { createServer } from './server'; // your function that builds the McpServer

  export async function POST(request: Request) {
    const server = withSurfa(createServer());
    const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
    await server.connect(transport);
    const response = await transport.handleRequest(request);
    await flushSurfa(); // upload before the function is frozen
    return response;
  }
  ```

<Warning>
  Cloudflare Workers haven't been tested yet. `surfa-ingest` uses Node.js APIs, so it needs the `nodejs_compat` compatibility flag at least, and may not work.
</Warning>

## 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. Set `debug: true` in the options to log every upload.
    3. Make sure `withSurfa(...)` wraps the server **before** `connect()` is called on it.
    4. 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="Using Python?">
    See the [Python one-line setup](/python-setup).
  </Accordion>
</AccordionGroup>


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