Skip to main content
POST /v1/ingest is the only route the SDK talks to.
Authenticated with an ingest key. The key resolves to exactly one MCP, and that is where every payload in the batch lands.

The rules

Why 202 before the write

Your server is mid-tool-call. Making it wait on our database to hand a result back to the model would put MCPulse on the latency path of every call it measures. So the write is scheduled and the response returns immediately. A failed write costs a data point; it must never cost a tool call.

Why invalid items are dropped silently

The envelope is checked for shape, then each item is validated individually. A whole batch is never failed for one bad item. Your server cannot fix a payload we rejected — the SDK does not retry, by design — so all a 400 would achieve is losing the 499 good payloads alongside the bad one. Drops are logged server-side with a count, so a systematically malformed client is visible to us even though it is invisible to the caller.
This means a 202 is not confirmation that every item was accepted. If you are writing your own producer rather than using the SDK, validate against the payload shapes in What is sent before you trust the numbers.

What each payload does

Startup upserts the tools table: tool name, schema_bytes, last_seen. This is the only way MCPulse learns a tool exists, and what makes dead-tool detection and schema size possible. Call does three writes:
  1. inserts into calls, the per-call log the live feed and the nightly pass read
  2. upserts tool_hours, keyed (mcp_id, hour, tool_name, client_name) — every counter on the product lives here
  3. upserts sessions, keyed (mcp_id, session_id)
Every upsert is on conflict do update set x = table.x + excluded.x. Never read-then-write, which races under load.
A payload with no client_name is bucketed as unknown before anything is grouped, so every client’s calls sum to the call total — the one property that makes the split checkable.One consequence, because it was a real bug: sessions.client_name prefers a real name over unknown, and only then does recency decide. Picking the newest name alone meant a later anonymous call renamed a session that had already identified itself.

Field limits

Free-text fields are bounded because ingest is a public endpoint. Without caps a hostile client writes whatever it likes into the metrics tables and into every dashboard reading them. v must be exactly 1. See Versioning.

Writing your own producer

Supported, and the wire format is documented — but the SDK does several things that are easy to get wrong and hard to notice:
  • one shared session per process, without which retries can never be detected
  • telling crashed from tool_error, which McpServer erases
  • empty detection, including the JSON-in-a-text-item case
  • key-sorted argument hashing
If you build your own, read What is sent closely.