How We Optimized Opik’s MCP Server for Cost & Performance

Like a lot of engineering teams, earlier this year we found ourselves hitting limits on AI token spend, trying to understand where those tokens were going and why, and how to manage cost without hurting performance. We ended up tackling this problem both internally, auditing and optimizing the coding agent configurations our team uses every day, and within our product, ensuring AI-powered features in Opik don’t cost our customers more to run than they should. My colleague Collin wrote a great piece on those internal coding cost optimizations, and in this post I’ll dive deeper into our MCP server optimization process, which dramatically improved cost and performance for Opik users.

Open the tool list of almost any MCP server and you’ll find the underlying API restates one tool per endpoint. That’s been the default recipe from the start, and even the biggest names in developer tooling follow it: GitHub’s official server ships so many tools it needed a --toolsets flag to switch groups of them off.

Ours was no different. The first MCP server for Opik, our LLM observability platform, exposed about thirty tools — one per REST endpoint. Then we deleted nearly all of them. On purpose.

None of those thirty tools was broken. The damage came from the list itself. A tool list doesn’t just sit on the server waiting to be needed. It rides along in the context window of every request, before the user has typed a word: names, descriptions, parameter schemas — all thirty tools, every time. Prompt caching makes re-sending that cheaper, but no smaller. The model still has to sift through all thirty schemas before every action, and with thirty near-identical names it picks the wrong tool more often than you’d expect.

Both problems are well documented — in Anthropic’s guidance on tool selection and the MCPToolBench++ paper — and the list only ever grew. Every new entity in the platform added another tool, and every tool claimed its slice of the context window, even when the job at hand had nothing to do with Opik.

Imagine building your own agent and handing it thirty unfamiliar tools with every request. Before its reasoning ever reaches your question, it walks the whole list, deciding tool by tool whether any of it matters. It’s stuck in the restaurant where the waiter recites thirty daily specials before taking your order on every visit, even though you always get the espresso.

So we rebuilt and optimized the MCP server around one rule: ship a shape the model can hold instead of mirroring the API. All of Opik’s data management now fits in four tools. Three decisions made that possible.

The first decision was to make the tool universal and the entity a parameter. list(entity) browses any collection, filters it by name, and pages through the results. When a listing isn’t enough, read(entity, id) pulls up the full record.

Writes work the same way. Every operation the platform supports is defined up front in a single registry—entity.action pairs like trace.create, trace.update, experiment.create—and a single write(operation, data) executes any of them. The operation names form a closed enum, so the model still picks trace.create by name; the signal of a named tool just lives one level down. And when it wants the exact payload shape, schema(operation) hands it over on demand.

The model learns the pattern once, and every piece of data in the platform is within reach of the same four tools.

Decision 2: Self-Healing by Design

The second decision dealt with the risk a high-level interface carries: because the model doesn’t see the full picture up front, it will sometimes guess a field or operation wrong. Here’s a real exchange — one wrong field name:

`write("score.create", { "target": "trace", "target_id": "019789d9-…","score_name":"helpfulness", "value": 0.8 })`

`{
  "error": "validation_failed",
  "operation": "score.create",
  "issues": [
     { "field": "name", "message": "Field required", "code":"missing" },
     { "field": "score_name", "message": "Extra inputs are not permitted", "code":"extra_forbidden" }
   ],
   "expected_schema": { "required": ["target", "target_id", "name", "value"], … },
   "example": { "target": "trace", "target_id": "0193a300-…",
   "name": "helpfulness", "value": 0.8, "reason": "user-confirmed" }
}`

The rejection guides the model to the correct call, and the next one lands. That turns the risk into a feature: corrections arrive exactly when the model needs them, and nothing has to be explained in advance. Prevention matters less when recovery costs a single turn.

Decision 3: Every Token Earns its Place

The last MCP optimization decision is about what comes back. Observability data is heavy: a single RAG trace can carry hundreds of spans, with full prompts and completions in their payloads. One raw return would undo everything the smaller surface saved. Here’s how read responds when a trace weighs in at about 131,000 tokens:

[read: trace 019789d9-… | compression=SKELETON | 6,652 tok returned | 131,710 tok full]
{
"trace": { "id": "019789d9-…", "name": "rag.chat.completion" },
"spans": [
{ "id": "01970000-…", "name": "retriever.search", "type": "tool" },
{ "id": "01970001-…", "name": "openai.chat",      "type": "llm"  },
… 298 more …
],
"note": "SKELETON compression: payloads omitted. Use read('span', id) for details."
}

The response sizes itself. Small results arrive whole. Bigger ones lose their long strings, and each cut leaves a breadcrumb to the full value. Giants collapse into a skeleton, and everything sheds noise on the way out: empty fields and verbose wrappers, gone.

That’s what the three decisions buy. Data management now scales as far as Opik can grow: a new entity is a registry entry, a new operation is an enum value, and four tools stay four. The model works through a high-level interface that covers every need and corrects it when it slips. And nobody has to worry about the context window anymore: it stays clean, and the tokens it holds go to answers.

If you’re building an MCP server, the hardest part is unlearning the REST principles we’ve lived by for years. An LLM values different things: clean high-level abstractions and room to recover, because it learns from whatever comes back. Build the interface that teaches instead of the contract that punishes, and the waiter finally just brings the espresso.

Next Steps in MCP Optimization & AI Spend Management

As our team worked to optimize our MCP server and reduce token waste both internally and in Opik, we saw a number of patterns emerge in terms of where our inflated Claude Code spend was going. It turned out that developers hitting limits weren’t overusing Claude or being actively wasteful — in fact, they were able to save roughly 30% by managing context and configurations without losing any productivity.

We realized teams needed better tooling to understand which skills, tools, MCPs, and default models were loading on every call, which ones cost the most, and whether their cost was justified by the output. This exercise led us to develop Cost Intelligence, a tool that finds and fixes wasteful coding agent configurations at the developer, team, and org level — and keeps context bloat in check as environments grow and change.

Optimize Coding Agent Spend with Comet Cost Intelligence

If your engineering team is facing similar challenges with AI spend management, we’d love to share more of our optimization experience and set up a custom demo of Cost Intelligence, showing how much your team can save with one-click optimizations. Contact us with some info about your team, coding environment, and goals and we’ll help you track down wasted tokens and attribute spending to engineering outcomes.

Similar Posts

Leave a Reply