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

# MCP tool calls

> Record the tool calls AI assistants such as Claude and ChatGPT make to your own MCP server with highseat/mcp, and tag the links your tools return so the visits they bring read as that assistant.

When your site runs its own MCP server, AI assistants call its tools to answer their users. `highseat/mcp` records each call as an `mcp_tool_call` event: which tool, which assistant, whether it worked, how long it took and how many items it returned. It works with servers built on the MCP TypeScript SDK v2.

## Record every call

Set `HIGHSEAT_SITE` and `HIGHSEAT_SECRET` from your site's Installation settings on the server. Then call `trackToolCalls()` once, after creating the server and before registering any tool. It throws when the server already has tools, since those would go unrecorded.

```ts theme={null}
import { McpServer } from "@modelcontextprotocol/server"
import { mcpLink, trackToolCalls } from "highseat/mcp"
import { z } from "zod"

const server = new McpServer({ name: "Acme", version: "1.0.0" })
trackToolCalls(server, { hostname: "acme.com" })

server.registerTool(
  "get_product",
  {
    description: "One product, by its slug.",
    inputSchema: z.object({ slug: z.string() }),
    annotations: { readOnlyHint: true },
  },
  async ({ slug }, context) => ({
    content: [{ type: "text", text: mcpLink(`https://acme.com/products/${slug}`, context) }],
  }),
)
```

In a serverless function, pass `waitUntil` to `trackToolCalls()`, such as `after` from `next/server`, so each event is sent before the function stops.

## What a call records

| Property | |
| - | - |
| `tool` | The tool's name. |
| `server` | The `server` option, a label such as `public` or `admin`, when the site runs more than one MCP server. |
| `client` | The name the assistant's client declares, else the product name its User-Agent starts with, such as `Claude-User`, else `unknown`. |
| `client_version` | The version the client declares, when it declares one. |
| `client_source` | `client_info`, `user_agent` or `none`: how `client` was found. A declared name is the one to trust. |
| `status` | `ok`, or the error code the assistant saw, such as `NOT_FOUND`. |
| `results` | How many items a read-only tool returned, when its result is a list. An empty list records `0`. |
| `duration_ms` | How long the call took, in milliseconds. |

A client on the 2026-07-28 MCP protocol names itself on every request. An older client names itself only when it connects, which a stateless server never sees again, so its User-Agent stands in.

## What it never records

The tool's arguments, its result, the caller's IP address and its user agent never leave your server, and neither do ChatGPT's user and conversation ids. Anthropic's and OpenAI's directory policies forbid collecting conversation data, so a server listed there stays within them. A call comes from the assistant's servers, not from a person, so it counts as an event on your plan and never as a visitor. Highseat accepts `mcp_tool_call` only from a server holding the site secret.

## Tag the links your tools return

`mcpLink(url, context)` adds `utm_source`, set to the assistant's client, and `utm_medium=mcp` to a link. Highseat reads `utm_medium=mcp` as the AI channel, so the visit, the goals and the revenue that follow a link read as that assistant. A link that already names a `utm_source` is left as it is.

A link through a redirect of your own, such as `/go/<slug>`, carries the tags to the redirect. When the redirect records the click with `track()` from `highseat/server`, pass its `url: request.url`, so the event takes the tags from the URL.


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