Our first MCP server worked beautifully in development and fell over the first week it saw real agent traffic. Three of the mistakes were interesting enough to write down.
Mistake one: treating every call as stateless
The clean design is one tool call, one resolved context package, no memory between calls. It is also wasteful, because agents in a conversation ask about the same entities repeatedly.
An analytics agent working through a churn question might make eleven calls in ninety seconds, and nine of them touch the same three definitions. Resolving the graph from scratch each time meant we were doing the same traversal over and over.
The fix was a session-scoped cache keyed on entity set, invalidated on any knowledge update touching those entities. Not complicated — we just did not think we needed it.
Mistake two: returning everything the scope allowed
Early on, a call to get_context returned every item in the caller's scope. If the scope had forty definitions, the agent got forty definitions.
This felt safe — the agent can decide what it needs — and it defeated the entire point of composition. We had rebuilt context stuffing with extra steps.
The correction was to make intent a required parameter rather than an optional hint. An agent must say what it is trying to do, and it gets the items relevant to that intent within its scope. Agents that pass a vague intent get a disambiguation response rather than a data dump.
Mistake three: no backpressure signal
When a workflow agent looped over 2,000 accounts and issued a context call per account, our server dutifully answered all of them. Latency for every other caller degraded, and we found out from a customer rather than from monitoring.
What we added: per-caller rate limits with a structured retry_after in the MCP error payload, plus a batch endpoint for exactly this pattern. Agents that respect the signal now degrade gracefully instead of hammering.
The shape we settled on
Three tools rather than one, each with a narrow contract:
- resolve_context — takes an intent and optional entity hints, returns a composed package with provenance. The workhorse.
- list_definitions — returns names and one-line summaries within scope, no bodies. Cheap, cacheable, lets an agent orient before asking for detail.
- check_conflict — asks whether a given concept has unresolved conflicts. Agents use it to decide whether to answer or escalate.
Splitting these out meant agents stopped over-fetching to compensate for a coarse interface. Median payload dropped by roughly two-thirds without any change to the underlying composition logic.
What we would tell someone starting out
- Make intent mandatory from day one; retrofitting it is a breaking change
- Instrument per-caller traffic before you need it, not after a customer tells you
- Cache on entity sets, not on query strings — the same question phrased two ways should hit the same cache
- Return provenance in every response, even when nobody asks for it yet. It becomes the debugging surface later.
