Product analytics for MCP servers

Baton captures user intent straight from their agents, finds the friction, and generates fixes to improve your product.

Get started

Your customer, to their agent

Claude
ChatGPT
Copilot
Baton Acme/ projects-mcp/ All surfaces OverviewInsightsSessionsLive
Session 5b917f6c · Claude Code · 1 task · 42 calls · tool call failure  14 retry loop  6 unrecovered failure +1 more satisfaction 2 of 5
First intent Build a dashboard tracking the Northwind launch for leadership
Expected result A dashboard showing each project's health, dates and progress
Written by a language model satisfaction 2 of 5 Covers 42 calls
What they came for

They wanted a leadership dashboard tracking the Northwind launch: every project's health, dates, progress, and the flagged work needing intervention.

What happened

The Northwind project set was pulled with status, progress and dates, and each project summarised in turn, including the ones marked In Trouble and At Risk.18 calls

Every attempt to add a project-level table was rejected for an invalid field path. The agent tried the owner field seven ways, ran a field search, and tried again with the path the search returned. Same rejection.19 calls

It never recovered. The last five calls were the same rejected query with the field name reworded.5 calls

Where it ended

The session stopped still retrying the same rejected field path, leaving a dashboard with counts and a chart but no project-level table.

Baton Acme/ projects-mcp/ All surfaces OverviewInsightsSessionsLive
Recurring failed intents
User intentSessions Sessions with frictionMost common error
Build a leadership dashboard 26invalid field path
Find a project by name9 invalid field path
Send assets to the asset library12 folder search failed: 400
Attach an approval workflow4 missing project_id
Recurring errors
Error patternToolSessionsCallsFix location
Invalid field path(s) — call get_field_paths() to retrieve valid paths. projects_find_data164 tool description
Upstream service returned HTTP {n} Bad Request projects_get_field_paths40 tool implementation
Baton Acme/ projects-mcp/ All surfaces OverviewInsightsSessionsLive
Recurring errors
Error patternToolSessionsCallsFix location
Invalid field path(s) — call get_field_paths() to retrieve valid paths. projects_find_data164 tool description

The rule is already in the server's own docs

CRITICAL — every `field_path` must be a verbatim path from `get_field_paths()`.

from the projects-mcp condition guide

Prompt Copy prompt
You are working in the repository that contains the projects MCP server. Goal: stop a recurring agent error by putting a documented rule into the tool description, where every agent receives it automatically. The error, on the tool projects_find_data: Invalid field path(s) — call get_field_paths() to retrieve valid paths. It occurred on 164 calls across 100 sessions in the last observed window. The rule that prevents it already exists in the server's own docs: From: the projects-mcp condition guide CRITICAL — every `field_path` must be a verbatim path from `get_field_paths()`. Steps: 1. Find the tool definition for projects_find_data (name, description, input schema). 2. Append these lines to its description. Change nothing else.

Copy as a prompt for your coding agent, verification steps included.

Upstream service returned HTTP {n} Bad Request projects_get_field_paths40 tool implementation
{n} validation errors for call[find_data] projects_find_data37 tool description
Tool (search_records) input validation failed: array found, [object, null] expected projects_search_records34 tool description
calling {v}: MCP error {n}: Unknown docs URI {v} projects_read_docs18 tool description
Show all 49 rows

Every session, explained

More than just tool calls and HTTP response codes. When your MCP server can't satisfy a user request, your logs often show a 200 anyway. Baton writes the plain-language account: what the customer came for, what happened, and where it ended.

Session 5b917f6c · Claude Code · 1 task · 42 calls · tool call failure  14 retry loop  6 unrecovered failure +1 more satisfaction 2 of 5
First intent Build a dashboard tracking the Northwind launch for leadership
Expected result A dashboard showing each project's health, dates and progress
Written by a language model satisfaction 2 of 5 Covers 42 calls
What they came for

They wanted a leadership dashboard tracking the Northwind launch: every project's health, dates, progress, and the flagged work needing intervention.

What happened

The Northwind project set was pulled with status, progress and dates, and each project summarised in turn, including the ones marked In Trouble and At Risk.18 calls

Showing the 18 of 42 calls this part was written from · in 1 of 1 taskshow all calls
+0s
projects_find_id_by_name
Find the Northwind project GUID for further data retrieval
expected: A project GUID and name matching "Northwind"
▸ request & response
1.1s · 5 KB
+0s
projects_get_current_user
Get the base URL for building deep links in the dashboard
▸ request & response
504ms · < 1 KB
+9s
projects_find_data
Get all Northwind projects with status, percent complete, and dates
expected: List of all Northwind projects with statuses and date fields
▸ request & response
2.7s · 38 KB
+10s
projects_find_id_by_name
Find any portfolio named Northwind that may contain these projects
expected: A portfolio GUID for Northwind
▸ request & response
699ms · < 1 KB
+ 14 more calls

Every attempt to add a project-level table was rejected for an invalid field path. The agent tried the owner field seven ways, ran a field search, and tried again with the path the search returned. Same rejection.19 calls

Showing the 19 of 42 calls this part was written from · in 1 of 1 taskshow all calls
+6m 21s
projects_find_data
Add a project-level table grouped by owner
expected: Rows grouped by owner
Invalid field path(s) — call get_field_paths() to retrieve valid paths.
▸ request & response
1.3s · < 1 KB
+6m 32s
projects_get_field_paths
Retrieve valid field paths for the owner field
Upstream service returned HTTP 400 Bad Request
▸ request & response
757ms · < 1 KB
+6m 42s
projects_find_datarepeat of an earlier call
Add the table with the path the search returned
Invalid field path(s) — call get_field_paths() to retrieve valid paths.
▸ request & response
1.4s · < 1 KB
+6m 50s
projects_find_datarepeat of an earlier call
Invalid field path(s) — call get_field_paths() to retrieve valid paths.
▸ request & response
1.2s · < 1 KB
+ 15 more calls

It never recovered. The last five calls were the same rejected query with the field name reworded.5 calls

Showing the 5 of 42 calls this part was written from · in 1 of 1 taskshow all calls
+7m 02s
projects_find_datarepeat of an earlier call
Retry with the field name reworded as ownerName
Invalid field path(s) — call get_field_paths() to retrieve valid paths.
▸ request & response
1.3s · < 1 KB
+7m 09s
projects_find_datarepeat of an earlier call
Retry with the field name reworded as owner.name
Invalid field path(s) — call get_field_paths() to retrieve valid paths.
▸ request & response
1.1s · < 1 KB
+ 3 more calls
Where it ended

The session stopped still retrying the same rejected field path, leaving a dashboard with counts and a chart but no project-level table.

How capture works

Baton adds three parameters to every tool you already have: the goal, the expected result, and the task the agent is working through. The agent fills them in with each call, and Baton strips them out before your code runs. One added tool lets the agent send feedback directly: annotate, for friction as it happens. Your customers never see Baton.

What Baton adds

params
user_goal  ·  expected_result  ·  overall_task

Three parameters added to every tool you already have. The agent fills them in with each call; Baton strips them out before your code runs.

projects_find_data {
  entity: "task",
  fields: ["owner:name", "status"],
  user_goal: "Add a project-level table to the dashboard",
  expected_result: "Rows grouped by owner",
  overall_task: "Build a leadership dashboard"
}
tool
acme_annotate

The agent reports friction as it happens: what it tried, what it expected, what happened instead.

Choose your Baton configuration

For a single MCP server you control

import { withBaton, HttpSink } from "@goodtiming/baton-sdk";

withBaton(server, {
  vendorId: "acme",
  vendorDisplayName: "Acme",
  consentToken: process.env.BATON_CONSENT_TOKEN,
  sink: new HttpSink(BATON_CONSOLE_URL, {
    apiKey: process.env.BATON_API_KEY,
  }),
});
Customer's agent Claude · ChatGPT · Copilot MCP withBaton( server ) Your MCP server your code unchanged user_goal · expected_result · annotate · session_report emits events Baton Console, or your own sink Customer's agent Claude · ChatGPT · Copilot MCP Your gateway Baton sidecar deployed at the edge MCP server MCP server emits events Baton Console, or your own sink Customer's agent Claude · ChatGPT · Copilot MCP Baton proxy no code changes Your MCP server untouched emits events Baton Console, or your own sink

You control what's captured. You can audit the rest.

PII never leaves raw

Every payload is scrubbed before it leaves your process: emails, keys, tokens, card numbers, and sensitive fields, on by default.

Deploy where you're comfortable

Keep everything inside your perimeter, send events to an endpoint you run, or use Baton's cloud, hosted single-tenant. The proxy can write straight to your own S3.

Auditable end to end

Every event is logged and inspectable. You define the capture schema, so what you don't capture can never reach us.

Your telemetry, if you want it

Events can also route to your own OpenTelemetry sink. Possible, supported, and rarely the place to start.

Questions, answered

I can already see tool calls on my server. How is this different from my logs and observability products?

Your logs show the call and the status code. Baton captures what the customer was trying to do, whether they got it, and the fix. It runs alongside Sentry and LangSmith and sees the layer they don't: intent.

Do I have to rebuild anything?

No. The SDK builds in with about five lines and your tools stay exactly as they are. If your customers reach you through agent-generated code, a Skill calling your API, the SDK's Library API instruments those calls instead.

Where does my data go?

Nowhere, by default. The open-source capture writes to a local file or a sink you choose: your own OpenTelemetry endpoint or your S3. PII is scrubbed before anything leaves. You only send to the Console if you choose to.

What's free, and what's paid?

Everything is free to start, including the Console. Capture is open source, and light use of the Console costs nothing. You pay once you pass a monthly session threshold.

Do my customers' agents have to change?

No. You instrument your side, once. Whatever agent your customers use, Claude, ChatGPT, Copilot, or anything else, keeps working exactly as before.

See your first session today

Get started

Prefer a human? Book 30 min with a founder