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

# Custom Events

> Record your own events and user traits from inside your MCP server's tool handlers.

### Overview

Alpic tracks every tool call automatically, but only your server knows what happened inside one: which backend was hit, how many results came back, whether a payment went through. Custom events let you record those moments from inside your tool handlers. Each event is attached to the current session, interleaved with the tool calls on its timeline.

You can also attach traits to the user behind the request (an email, a name, a plan) with `identify`.

Events are buffered while your handler runs and shipped through private metadata configured by the Alpic runtime, so there is no extra network call. Alpic ingests and strips them at the proxy before the response reaches the MCP client. Outside Alpic, no analytics metadata is added to the response.

To capture what happens in the UI your tools render, see [View events](/analytics/view-events).

### 1. Install the package

```bash theme={null}
pnpm add @alpic-ai/insights
```

### 2. Wire it into your server

The package ships two entry points depending on which MCP framework you're using.

<Tabs>
  <Tab title="Skybridge">
    Use `analyticsMiddleware`: it returns a Skybridge `McpMiddlewareFn` you register via `mcpMiddleware()`. Add it
    before your tool/widget registrations:

    ```typescript theme={null}
    import { analyticsMiddleware } from "@alpic-ai/insights";
    import { McpServer } from "skybridge/server";

    const server = new McpServer({ name: "my-mcp-server", version: "1.0.0" }, { capabilities: {} })
      .mcpMiddleware(analyticsMiddleware())
      .registerTool(/* ... */);
    ```
  </Tab>

  <Tab title="MCP SDK">
    Use `track`, it accepts an `McpServer` (or a low-level `Server`) and patches the `tools/call` handlers in place.
    It can be called before or after your tools are registered:

    ```typescript theme={null}
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { track } from "@alpic-ai/insights";

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

    server.registerTool(/* ... */);

    track(server);
    ```
  </Tab>
</Tabs>

### 3. Capture events from your tool handlers

Once wired in, every tool handler receives an `analytics` object on its `extra` argument. Cast `extra` with the
exported `AnalyticsExtra` type to get typed access:

```typescript theme={null}
import type { AnalyticsExtra } from "@alpic-ai/insights";

server.registerTool("search_flights", {/* ... */}, async ({ from, to }, extra) => {
  const { analytics } = extra as unknown as AnalyticsExtra;

  const startedAt = performance.now();
  const flights = await flightApi.search(from, to);

  analytics.capture("flights_searched", {
    duration: performance.now() - startedAt,
    properties: { from, to, resultCount: flights.length },
  });

  return { content: [/* ... */] };
});
```

`capture(name, options)` takes the event name plus optional details:

| Option       | Type                      | Description                                                                            |
| ------------ | ------------------------- | -------------------------------------------------------------------------------------- |
| `message`    | `string`                  | A human-readable line shown with the event. Max 500 characters.                        |
| `duration`   | `number`                  | How long the operation took, in milliseconds. Fractional values are fine.              |
| `properties` | `Record<string, unknown>` | Freeform structured detail. Max 8 KB serialized.                                       |
| `isError`    | `boolean`                 | Marks the event as an error on the timeline.                                           |
| `error`      | `string`                  | An error description. Setting it also marks the event as an error. Max 500 characters. |

### 4. Identify the user

`identify` attaches string traits to the user making the current request. Alpic resolves who that is from the
request's auth context (see <a href="/analytics/unique-users" target="_blank">how users are counted</a>), so you
don't pass any id:

```typescript theme={null}
analytics.identify({
  email: user.email,
  plan: user.plan,
});
```

Each `identify` call is a complete snapshot. Calling it several times in one request keeps only the last snapshot, and
the next request's call replaces the traits stored for that user.

After Alpic has observed user-unique traits, choose one under **Project settings → Privacy → Primary user identity**.
That environment-wide choice is shown as the main user label in analytics while Alpic keeps the stable internal user
ID for grouping and navigation.

<Note>
  Traits are user data. They are only stored on a paid plan with **Collect user data** enabled for the environment, like
  the rest of the{" "}

  <a href="/analytics/privacy" target="_blank">
    privacy settings
  </a>

  . On the Free plan or with the setting off, `identify` calls are dropped.
</Note>

### 5. Deploy

Deploy on Alpic as usual. Once the new version is live, events captured by your handlers start flowing into your
project's analytics.

### Run locally or on another host

Pass a handler to receive each request's analytics batch yourself. The same option works with `analyticsMiddleware`:

```typescript theme={null}
track(server, {
  handler: async ({ events, traits }) => {
    await myAnalytics.write({ events, traits });
  },
});
```

The handler also runs on Alpic, in addition to dashboard ingestion. Handler failures are logged and never fail the
tool call.

### Good to know

* Only `tools/call` requests carry an `analytics` object; other request types pass through untouched.
* Call `capture` and `identify` before your handler returns. Calls made after the response is sent (for example from
  a detached promise) are dropped, with a warning outside production.
* At most 50 events are kept per tool call; extra ones are dropped.
* Event names are capped at 200 characters.
* Event timestamps are set when `capture` is called. Timestamps more than 24 hours old, or in the future, are
  replaced by the time Alpic received the event.
* The middleware never throws into your handler: if analytics can't be attached, your tool result is returned as-is.
