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

# Install the SDK

> @mcpulse/sdk — one import, one wrap. No runtime dependencies, Node 20.12 or newer.

`@mcpulse/sdk` is how data gets out of your MCP server. It is installed **inside your own process**, not in front of it.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @mcpulse/sdk
  ```

  ```bash pnpm theme={null}
  pnpm add @mcpulse/sdk
  ```

  ```bash yarn theme={null}
  yarn add @mcpulse/sdk
  ```

  ```bash bun theme={null}
  bun add @mcpulse/sdk
  ```
</CodeGroup>

## Requirements

| | |
| - | - |
| Node | 20.12 or newer |
| `@modelcontextprotocol/sdk` | A peer dependency — whatever version you already use, `>=1.0.0` |
| Runtime dependencies | **None.** The package brings nothing with it |

The package ships ESM and CJS builds and its own types.

<Note>
  There are no runtime dependencies on purpose. `@mcpulse/schemas` was going to be the one exception and is not: it brings Zod with it, which would mean validating our own output inside every customer's server when the API already validates it on arrival. The payload types are restated in the SDK's own `types.ts`, and a contract test pins every field name so the two cannot drift.
</Note>

## Wrap your server

```ts theme={null}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { watch } from "@mcpulse/sdk";

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

// … your registerTool calls …

watch(server, { key: process.env.MCPULSE_KEY });

await server.connect(new StdioServerTransport());
```

`watch` instruments the server in place and returns the **same object**, so the call can be dropped around an existing one without moving anything else.

Two things matter about placement:

* **After your tools are registered.** `McpServer` does not create its `tools/call` handler until the first tool exists. The SDK re-applies its wrapper after each later registration too, so both orders work — but registering first is the shape to write.
* **Once per server.** Watching the same server twice is a no-op, guarded by a `WeakSet`. Left unguarded it would open a second session and report every call under both, doubling your numbers.

## Which shape do you have?

<CardGroup cols={2}>
  <Card title="stdio" icon="terminal" href="/sdk/stdio">
    One long-lived server. Wrap it once, at the top level.
  </Card>

  <Card title="Streamable HTTP" icon="server" href="/sdk/http">
    A fresh server per request. Wrap inside the factory.
  </Card>
</CardGroup>

## Other languages

There are ten MCPulse SDKs, one for each official MCP SDK. All ten produce identical `args_hash` values for identical arguments — they run the same conformance fixtures to prove it — so a customer running servers in two languages sees one set of numbers, not two.

<CodeGroup>
  ```bash Python theme={null}
  pip install mcpulse-sdk
  ```

  ```bash Go theme={null}
  go get github.com/getmcpulse/mcpulse-go
  ```

  ```bash Rust theme={null}
  cargo add mcpulse
  ```

  ```bash Ruby theme={null}
  gem install mcpulse
  ```

  ```bash PHP theme={null}
  composer require mcpulse/mcpulse
  ```

  ```bash .NET theme={null}
  dotnet add package MCPulse
  ```

  ```swift Swift theme={null}
  .package(url: "https://github.com/getmcpulse/mcpulse-swift.git", from: "0.1.0")
  ```

  ```kotlin Java theme={null}
  implementation("com.getmcpulse:mcpulse:0.1.0")
  ```

  ```kotlin Kotlin theme={null}
  implementation("com.getmcpulse:mcpulse-kotlin:0.1.0")
  ```
</CodeGroup>

Each repository's README carries the wrapping example for that language, the options table, and what that SDK can and cannot report.

| Language | Repository |
| - | - |
| TypeScript | [mcpulse-typescript](https://github.com/getmcpulse/mcpulse-typescript) |
| Python | [mcpulse-python](https://github.com/getmcpulse/mcpulse-python) |
| Go | [mcpulse-go](https://github.com/getmcpulse/mcpulse-go) |
| Java | [mcpulse-java](https://github.com/getmcpulse/mcpulse-java) |
| Kotlin | [mcpulse-kotlin](https://github.com/getmcpulse/mcpulse-kotlin) |
| C# / .NET | [mcpulse-dotnet](https://github.com/getmcpulse/mcpulse-dotnet) |
| Rust | [mcpulse-rust](https://github.com/getmcpulse/mcpulse-rust) |
| Ruby | [mcpulse-ruby](https://github.com/getmcpulse/mcpulse-ruby) |
| PHP | [mcpulse-php](https://github.com/getmcpulse/mcpulse-php) |
| Swift | [mcpulse-swift](https://github.com/getmcpulse/mcpulse-swift) |

### What differs between them

Only TypeScript, Python and Go attach with a single call and see every tool automatically — those are the MCP SDKs with a supported interception point. The rest wrap your tool handler, which costs one thing:

**`bad_args` is not reported outside TypeScript, Python and Go.** Those SDKs validate arguments *before* the handler runs, so a rejected call never reaches the wrapper. Reporting it anyway would mean reading the difference out of an error string, which is not an interface anyone promised to keep. `ok`, `tool_error` and `crashed` are exact everywhere.

Go needs `mcpulse.WrapTool(handler)` for the full split, because it reports a schema rejection as a result with `IsError` set and no error returned — byte for byte what a failing handler produces.

**Any language can post to the ingest endpoint directly.** The packages exist to save you writing that, not to gate it: it is one authenticated `POST` with a batch of JSON objects. See [What is sent](/sdk/what-is-sent) for the payloads, [Wire format](/sdk/wire-format) for the hash, and [Ingest](/api/concepts/ingest) for the endpoint and limits.

There are no dates on the cards. A promise with a month on it is a promise to be wrong about.

## Get a key

An ingest key is minted per MCP in the dashboard and shown once. The install page creates keys itself, so setup never bounces between pages. See [Create a key](/api-keys/create).

Without a key, `watch()` is a silent no-op — a server started without its key configured should be quiet, not a source of 401s on every flush.

## Next

* [Options](/sdk/options) — everything `watch()` accepts
* [What is sent](/sdk/what-is-sent) — the exact payloads, and the outcomes
* [Wire format](/sdk/wire-format) — the hash, and what keeps ten languages agreeing
* [Troubleshooting](/sdk/troubleshooting) — when nothing arrives


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