Telltale for MCP servers

View as Markdown

Agents don't only work in your pages. Many reach your product through your own MCP server. Telltale for MCP servers records every tool call those agents make, per account, beside the agents in your pages and the agents using your API: one record of AI agent activity across every account and every channel.

It's one wrapper around the server you already have. Your tools, schemas and results stay as they are. Telltale never receives a call's content.

What you get

Install

Works with @modelcontextprotocol/sdk 1.x (McpServer or the low-level Server; tested with 1.17 and the latest 1.x) and @modelcontextprotocol/server 2.x, on stdio, Streamable HTTP and stateless HTTP, on Node 18+ or any runtime with fetch and Web Crypto. Tools you register after wrapping are recorded too. The adapter has no dependencies.

  1. Download the adapter, check its SHA-256 against the adapters table, and put it beside your server code. It is an ES module: in a project whose package.json says "type": "commonjs" (what npm init writes), save the two files as telltale-mcp.mjs and telltale-mcp.d.mts and import ./telltale-mcp.mjs:

    Shell
    curl -sSo telltale-mcp.js https://app.trytelltale.dev/adapters/v1/mcp.js
    curl -sSo telltale-mcp.d.ts https://app.trytelltale.dev/adapters/v1/mcp.d.ts
  2. Wrap your server where you create it, with the Site's server key (Settings › Channels › Server keys). The key stays on your server:

    JavaScript
    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { z } from 'zod';
    import { withTelltale } from './telltale-mcp.js';
    
    const server = new McpServer({ name: 'my-app', version: '1.0.0' });
    const telltale = withTelltale(server, {
      endpoint: 'https://app.trytelltale.dev',
      serverKey: process.env.TELLTALE_SERVER_KEY,
      identify: ({ authInfo }) => ({
        accountId: authInfo?.extra?.accountId,   // your own opaque IDs
        userId: authInfo?.extra?.userId,
      }),
    });
    
    server.registerTool('export_report', { inputSchema: { month: z.string() } }, async ({ month }) => { /* … */ });
    
    process.on('SIGTERM', async () => { await telltale.close(); process.exit(0); });
  3. Make a tool call from any MCP client. Settings › Channels › MCP server › Is it working? turns to "Working" with the time of the last batch, usually within five seconds.

Stateless servers and serverless functions

A stateless server often builds a new server for every request (createMcpHandler in 2.x, or a 1.x transport without sessionIdGenerator). Create the Telltale client once and wrap each server inside that factory:

JavaScript
import { createTelltale } from './telltale-mcp.js';

const telltale = createTelltale({ endpoint: 'https://app.trytelltale.dev', serverKey: process.env.TELLTALE_SERVER_KEY, identify });

export async function handle(request) {
  const server = telltale.wrap(buildServer());
  const response = await serve(server, request);
  await telltale.flush();          // before a serverless function is frozen
  return response;
}

On Cloudflare Workers, hand the flush to the platform: ctx.waitUntil(telltale.flush()). A process that ends on its own (a stdio server whose client went away) sends what is still queued first, once, within two seconds. Signals and process.exit() don't wait for that, so call close() in your shutdown handler; calls still queued when a process is killed without close() or flush() are lost (the adapter keeps nothing on disk).

Confidence

Every MCP call and rule decision is shown as AI agent with a confidence band, never a probability. The band comes from the name the MCP client declared about itself when it connected (clientInfo.name in the MCP initialize request), matched without regard to case, spaces, hyphens or underscores:

Confidence
ClientExamplesConfidence
A valid agent token your backend issued for this tool (any client name)Verified by tokenHigh
A known AI appClaude Desktop, Claude Code, Cursor, ChatGPT, Windsurf, VS Code (Copilot), Codex, Gemini, Goose, Zed, Cline, Roo Code, ContinueHigh
A debugging or test toolMCP Inspector, MCPJam Inspector, Postman, MCP CLIMedium
Any other name, or no name"Other MCP client", "Not declared"Medium

Identity

identify(ctx) returns { accountId, userId } from your own authentication: ctx.authInfo (what your transport's MCP auth middleware verified: extra.authInfo in 1.x, ctx.http.authInfo in 2.x), ctx.extra (the MCP SDK's request context; without MCP auth, read your own header there: extra.requestInfo.headers in 1.x, ctx.http.req.headers in 2.x) and ctx.sessionId. A stdio server has no headers: use the key your server already reads, for example from its environment. Send opaque IDs only, never an email address or a name.

Grouping calls into agent sessions

Calls are grouped into agent sessions, also on stateless servers:

  1. session(ctx) if you give one: your own conversation or task ID.
  2. Otherwise the transport's session (stateful Streamable HTTP and SSE).
  3. Otherwise the account, user and client together, split after 30 idle minutes. A call with no account, no user and no session is its own session: two anonymous callers are never merged.

Each key is hashed (SHA-256) before it leaves your server; the raw session ID is never sent. Some clients reset their session ID in the middle of a conversation, so your own key is the most reliable choice.

Privacy: what is and isn't sent

Sent to Telltale for each call, and nothing else:

Privacy: what is and isn't sent
FieldWhat it is
eventIda random ID for the call, so a retried batch is never counted twice
toolthe tool name
startedAt, endedAtwhen the call started and ended (milliseconds)
outcomeok, error, refused or cancelled
errorClassfor an error: tool_error (your tool failed: it threw or returned an isError result), invalid_params (the arguments were refused before your tool ran), unknown_tool (no such tool, or a disabled one), internal_error, timeout or other. A category, never the message
refusedByfor a refusal: deny, hold or unreachable
client{name, version} the MCP client declared about itself
accountId, userIdwhat your identify returned
session{key, source}: a SHA-256 hash and where it came from (customer, transport or derived)
reasononly for tools where you turned on the why field (below)
decisionId, tokenIdthe rule decision or the agent token behind a protected call
responseBytes, responseTokensadapter 1.1.0 and later: the size of the tool's result, counted by the adapter on your server: its UTF-8 bytes serialised as JSON (what the transport sends) and an estimated token count (bytes / 4, rounded up). Used for Oversized responses in Issues. Two numbers; the result itself is never sent
capturedonly with Deep insights on (owner-enabled, off by default): the fields you listed for that tool

Each batch also names the adapter's version and your server's name and version. Telltale refuses any other field.

Never sent (unless you turn on Deep insights and list fields, and even then never headers or credentials): arguments, results, error messages and stack traces, HTTP headers, tokens, cookies, authInfo, the raw MCP session ID and the caller's IP address. The adapter sends no diagnostics about itself anywhere, and it never writes to stdout (a stdio server uses stdout for the protocol); debug: true writes to stderr.

Data stays with you

To keep the full calls for your own debugging, pass them to your own logs or OpenTelemetry. The hook gets everything (arguments, result, error, the MCP request context); Telltale gets none of it:

JavaScript
withTelltale(server, {
  endpoint: 'https://app.trytelltale.dev', serverKey: process.env.TELLTALE_SERVER_KEY, identify,
  dataStaysWithYou: (call) => logger.info({ tool: call.tool, outcome: call.outcome, args: call.arguments, ms: call.durationMs }),
});

The why field (opt in, per tool)

Off by default, and it takes two steps: name the tools in why, and turn on Understand what agents are trying to do for the workspace (Connect your channels or Home). While both are on, each tool you name gains one optional string parameter in its schema, reason, which asks the agent in one sentence why it is calling the tool:

JavaScript
withTelltale(server, { /* … */, why: ['export_report', 'delete_project'] });

Deep insights (opt in, owner-enabled)

Off by default. When the site owner turns it on and you list fields per tool in deepInsights, Telltale keeps those argument and structuredContent fields for 7 to 30 days and shows them as "Captured fields" in the session drawer. Unlisted fields never leave your server, your redact hook runs before anything is sent, and headers, tokens, passwords and other credentials are never stored, whatever you list. See Deep insights. The why field above and Deep insights are separate switches; both are off until you turn them on.

Agent feedback (opt in, since 1.3.0)

With feedback: true and agent feedback turned on for the workspace, the SDK adds one tool, agent_feedback, where an agent can say what it needed and could not find. The SDK answers it itself (it never reaches your handlers and is never counted as a call), redacts the note and sends at most 500 characters. Notes show in Issues, on the session and in the bell. See Agent feedback.

Rules for protected tools

Name the tools that should ask for your rule (Control › Rules) before they run:

JavaScript
withTelltale(server, {
  /* … */,
  protect: {
    tools: ['export_all'],            // read tools: fail open when Telltale can't be reached
    writeTools: ['delete_project'],   // write tools: fail closed when Telltale can't be reached
    // whenUnreachable: 'open' | 'closed' | { read, write } to change that
  },
  onHold: async ({ tool, decisionId, accountId, userId }) => {
    const ok = await askThePersonInYourApp(userId, tool);   // your own step-up
    return ok ? { result: 'passed', method: 'own_approval' } : 'failed';
  },
});

Batching and outages

Calls are queued in memory and sent in batches of up to 100 every five seconds. Network errors, 408, 429 and 5xx answers are retried with growing waits (up to a minute, honouring Retry-After). Any other refusal drops the batch, logged once with debug: true. The queue holds 10,000 calls; past that the oldest are dropped and counted ("dropped" in Is it working?). A Telltale outage never slows or fails a tool call. Only protected tools wait, and only up to their timeout.

Performance, tags, events and debug mode

Issues lists, beside the errors, Slow tool calls (tools whose calls ran longer than your limit, 30,000 ms by default) and Oversized responses (tools whose results were larger than your token limit, 20,000 tokens by default: a result that big crowds out the agent's earlier conversation). Set both limits for the Site, or per tool, in Issues › Limits (owners and admins). Neither is an error: error counts and error rates never change. Upgrade to adapter 1.1.0 to send result sizes; slow calls work with any version.

Tools › All tools shows each tool's agent calls, error rate and p50/p95 time, by MCP client and by tag. Add tags: ctx => ({ plan: 'pro' }) for categories, call telltale.event('report.exported', { accountId }) for moments in your product, and set debug: true (or TELLTALE_DEBUG=1) to see what is sent, on stderr only. The bell tells you when a tool starts failing, when a call is slow, when a new MCP client shows up, when an issue you resolved comes back, and when an account makes its first agent call. Details and the tag rules: Custom events, tags and debug mode.

Pricing and limits

MCP calls are included in every plan. Telltale's plans count identified users a month, once across channels, never calls or sessions. Call volume is under fair use: about 100,000 MCP calls a month on Starter and 1,000,000 on Growth. Past that we talk to you before anything changes, and collection never stops. The server accepts up to 1,200 batches and 6,000 rule checks a minute per Site; a 429 just makes the adapter wait.

Troubleshooting

See Approved route for agents for agent tokens, and Agent-usage ledger for how calls are counted.