Part 7 of the MCP series. Start at part 1.
- same tools for agents and clients
- read-only checked live
- one schema field for "why"
- a writer that never drops a row
The problem
Once the MCP tools existed, the analytics agent inside the dashboard should use them too, not a private side door: live tiles, pages and reports through the same tools and checks as any external client.
That raised a second question. When an AI calls a tool, the logs say what it called. Nobody can tell why. For debugging, trust and support, "why" is the part you actually want.
Live tiles, pages and reports
A tile used to be one SQL query. I let a tile read an MCP tool instead. At view time the dashboard mints a short-lived token on the server, which never reaches the browser, and calls the edge. Before any network call or cache lookup, the tool must be on a reviewed allowlist and its live readOnlyHint must be true. Each run has a deadline, a size cap and a per-store circuit breaker, and it runs as the tile's creator, not the viewer. Every excluded server has a written reason, such as logs that carry personal data.
The agent writes tiles through one gate on every path: ownership, template parse, source allowlist and a short dry run. When I first asked it to build a dashboard, it made zero tool calls. It didn't know the board tools existed, so its instructions had to teach them.
A page is an AI-written template plus a list of read-only MCP sources, and a report is a frozen page. The agent gets read tools and write tools for both. Writes are create-then-poll, because a capture can take longer than the load balancer allows a request to live. Editor rights are checked live on every write. A page can be a full HTML document, run in a sandboxed iframe with scripts only, a per-page CSP, and data only through a narrow bridge.
Recording why: one field at the front door
The edge is the single entry point for every client, so hooking it covers every server behind the gateway with nothing changed downstream.
The AI fills in the record itself. When the edge returns tools/list, it adds one context field to every tool's input schema (the user's prompt, the goal, and the reason for this call) and marks it required. That steers the model, but nothing enforces it. A call without the field is forwarded and recorded with empty context. The rewrite can never break a session: a tool that already has the field is left alone, unexpected shapes pass through, and the function never throws.
The gateway rebuilds arguments before they reach a server, so the field never leaves our side. Not even a third-party vendor sees it.
What a record holds
who store, user, OAuth client, client name, session, route
what method, tool, target server
why prompt, goal, reason
args hash and byte count only, never raw values
result ok / denied / error / upstream error, status, latencyPrompts never go to stdout. The log lines that print a record strip them.
Redaction that can't stall the dashboard
Stored prompts are text people typed, so two masking passes run before insert: one for secrets next to a key name, and one for bare tokens with well-known shapes. Each rule needs a digit or a fixed length, so ordinary prose survives.
Redaction runs on the dashboard's shared event loop, and some patterns backtrack badly on long input. I bounded it three ways: long runs without whitespace are masked whole, pathological lines are split before matching, and the handler yields between records.
Ingest is idempotent on request id, falls back to row by row if a batch fails, and repairs malformed Unicode instead of rejecting it.
A writer that never drops a row
On the edge, submitting a record is synchronous, constant time and can't throw. Records are batched. A failed batch goes back to the front of the queue, retries back off, and only "bad request" and "too large" are permanent.
Each destination has its own queue, bounded by rows and bytes, so one unhealthy dashboard can't starve a healthy one. A batch that times out halves its size, and successes grow it back. A single row that times out alone is sent alone after that, at the back of the line. The destination comes from the verified token issuer, never from a field in the record.
What I'd do differently
- Redact personal data, not just secrets, and set a retention window. Both masking passes target credentials. Personal details in prompts deserve the same care.
- Hold risky agent writes for approval. Live permission checks are the floor. I'd want a "pending your approval" step for anything destructive.
Takeaway
Put your own agent on the same MCP tools as everyone else, with the same checks. Then ask the model to say why it is calling each tool, record that at the one door everything passes through, and build the log so it can never slow down or break the thing it is watching.
Next: back to the start of the series