Skip to main content
Back to Blog
Analytics
Featured
Updated Aug 25, 2026

Why an MCP Server Is Not a Product

Wrapping an API in MCP takes an afternoon. Shipping an MCP a business can run on is everything between your database and the model's first token: context economics, semantics, reconciliation, guardrails and observability.

A
·CEO at Nova AnalyticsLinkedIn

Antoine founded Nova Analytics to empower Amazon sellers with enterprise-grade analytics. He specializes in data architecture and building scalable solutions for e-commerce businesses.

Aug 25, 2026·13 min

TL;DR - Key Takeaways

  • You can wrap an API in the Model Context Protocol in an afternoon. Everything that makes it survive contact with a real user happens somewhere else.
  • Tool definitions load on every conversation, so discovery has to be two-step: list keys cheaply, describe one metric on demand.
  • A column name is not a definition. Two numbers sharing a word will be averaged by a model that has no reason to doubt either.
  • Constraints the model cannot discover from the data belong in the tool description, because the model will not ask.
  • Ranking and filtering belong in the database. Sorting a 2,000-product catalogue in context costs about 225,000 tokens for an answer that fits in ten rows.
  • The model is not the bottleneck. Context is.

An MCP server is a transport. It advertises some tools, accepts calls, returns JSON. Building one is a weekend project. Building one a business can actually run on is not, because the hard part was never exposing data to a model. The hard part is making sure what the model reads is unambiguous, reconciled, current, and small enough to reason about. A protocol is not a product.

We run an MCP server for Amazon sellers. It exposes 14 tools over a data model covering 21 marketplaces, and it is queried by Claude, ChatGPT and anything else that speaks the Model Context Protocol. Every number in this article comes from that system. None of the failures below were protocol problems, and none of them would have been fixed by a better model.

Your schema is a query, not a preamble

Every tool definition you advertise is loaded into the model's context at the start of every conversation, before the user has asked anything. That is a fixed tax, paid on conversations that never touch the tool.

Our metric catalogue has 281 keys. Each one carries a definition, a unit type, a directionality and a set of aliases. Serialised in full, that catalogue is 73,322 characters across 2,058 lines, roughly 19,000 tokens.

If our discovery tool returned the full catalogue, every conversation would open about 19,000 tokens down. On a large context window that is a tenth of the budget spent before the first question. On a smaller one it is most of it. And it is charged again on every new conversation, forever.

So discovery is two steps, not one. list_metrics returns the key, the label, the type, the directionality and a one-line description. describe_metric returns the full card for the one metric actually in play. The model pays for the definitions it needs and nothing else.

The general rule: progressive disclosure is a design requirement of MCP, not an optimisation. If the size of your tool surface grows linearly with the size of your data model, you have shipped a bug, not a catalogue.

A column name is not a definition

A model cannot infer what a number means from what it is called. It will not stop to ask. It will pick the reading that makes the sentence work.

Our system has two separate metric universes that a user would describe with the same English word. Advertising metrics come from Sponsored Products at serve-time attribution. Product-side advertising metrics cover Sponsored Products plus Sponsored Display plus Sponsored Brands, allocated down to products. Both are correct. They are different numbers, computed over different populations, and the gap between them is real rather than an error.

Hand a model both under the word "sales" and it will not fail. It will average them, or difference them, or reconcile the gap, and it will explain its reasoning fluently while doing it.

A usable definition has to carry four things a column name does not:

  • What it measures, in a sentence, including what is excluded.
  • Its unit type, so a currency is never averaged like a ratio.
  • Its directionality, so the model knows whether up is good. A model that does not know this will congratulate a seller on a rising advertising cost of sale.
  • Its aliases, so "revenue", "net sales" and "netsales" all resolve to one key instead of three guesses.

The failure mode of missing semantics is not an error message. It is fluency.

The same fact, two sources, two names

This is the part that does not appear in any architecture diagram, and it is most of the work.

Amazon reports money in more than one place. Settlement reports and the SP-API Finances API describe overlapping sets of the same transactions, under different keys, with different coverage. Neither is a superset of the other.

Our reconciliation mapping is 370 distinct transaction lines, collapsing into 154 normalised profit-and-loss lines. Of those 370:

  • 187 appear in both the settlement report and the Finance API, under different names in each. Read both surfaces without reconciling them and you count that money twice.
  • 183 appear only in the Finance API. They are not in the settlement report at all.

The judgement calls no schema captures

  • A disbursement is a cash movement, not a cost. It must never touch a profit-and-loss statement, even though it is a large number sitting in a financial feed.
  • A shipping-label credit is genuinely a credit, but bucket it into total marketplace fees and fee totals turn positive on some days, which reads as the marketplace paying the seller.
  • When the two surfaces disagree on a line, one of them is authoritative, and which one depends on the line.

That mapping is not a schema. It is an opinion, maintained over years, about what each of Amazon's own numbers means. An MCP served straight off raw tables inherits every one of those conflicts and hands them to a model that has no way to resolve them and, worse, no way to know that it should.

The rule: if two of your tables can answer the same question differently, your server has to choose before the model does.

The traps have to live in the tool contract

A model cannot discover a constraint that is not in the data. It will not ask. If the rule lives in your team's heads, or in a wiki, the rule does not exist.

Five constraints from our own tool contracts, each one measured, each one a wrong answer waiting:

  1. Search query volume is repeated on every parent product that ranks on the query. Sum the rows and you double count. We measured the inflation at 2.4x. The tool returns a pre-deduplicated summary block and tells the model not to sum.
  2. 35% of served search queries have several spellings, and those variants carry 60% of the volume. Match one string and you silently miss real campaigns. The tool returns the variants each query merges.
  3. Comparing period totals reads +34% where the like-for-like truth is -3%, because the keyword set changed underneath the comparison. The fix is a comparison metric computed only over keywords present in both periods, returning null below a reliability floor rather than a confident number.
  4. Weeks in that report run Sunday to Saturday, not Monday to Sunday. Cross it against an ISO week and the periods silently fail to line up. Weekly data lands about 10 days behind, monthly about 7 weeks. The tool returns the exact calendar window it served, to be passed verbatim into any other tool.
  5. Fulfilment quantities are per product and cannot be summed across products, because a shared fulfilment pool double counts. Separate pool columns exist for totals.

Notice what all five have in common. None of them produces an error. Each produces a number.

This is why a tool description is a prompt, not documentation. It is the only part of your system that is re-read on every single call, which makes it the highest-leverage prompt surface you own. Most teams write it like an API reference.

Before:

Get search query performance data. Returns search query metrics for your products.

After:

Amazon search query performance, curated. What shoppers type, the whole
market's funnel on each query, and your slice of it. This is organic demand,
not advertising, and not your own ad search terms. Grain is required and
periods are fixed calendar weeks or months: omit the dates and you get the
latest complete period, which is almost always what you want. Do not sum
market volumes across rows, a query repeats on every parent product that ranks
on it and adding rows double counts, measured at 2.4x. Use the summary block,
it is already deduplicated. Filter by query type or the answer is noise, broad
category terms dominate any unfiltered total. Comparison is a metric, not a
mode: use the like-for-like change metric rather than differencing two period
totals.

What the second version buys you, clause by clause

ClauseEmpty descriptionWritten as a prompt
Scope of the dataModel confuses it with ad search termsOrganic demand, stated and bounded
Summing rowsTotals inflated 2.4xSumming forbidden, summary block supplied
Period comparisonTwo totals differenced, +34% vs -3%Like-for-like metric named as the only route
FilteringBroad category terms swamp the answerQuery-type filter required

The second version is longer, and it costs a few hundred tokens on every call. It also removes four distinct ways to be confidently wrong. That is the trade, and it is not close.

See what a guarded MCP contract looks like in production

Nova's server is the working example of everything in this article: two-step discovery, defined metrics, reconciled sources, server-side ranking.

Look at the tool surface

Ranking and filtering belong in the database

The instinct when connecting a model to data is to give it the rows and let it think. The arithmetic kills you.

A single product row carrying its identifiers, its display attributes and one metric with a period comparison costs 451 characters minified, roughly 112 tokens. That is a conservative figure: pretty-printed it is 633.

Now scale it. A model asked "which products lost the most revenue last month" against a 2,000-product catalogue, working from raw rows, needs about 902,000 characters, roughly 225,000 tokens, moved into context before it has reasoned about anything. That does not fit in most context windows. At 10,000 products it is not close.

And then it discards 99.5% of it, because the answer was ten rows.

The same question, answered server-side, is one call that returns ten rows with the variance already computed. The work that makes that possible is unglamorous and entirely on your side of the wire:

  • Rank server-side. Sort by any metric, on the value or on the variance, ascending or descending. "Top N by X" should never mean "fetch everything".
  • Filter on thresholds server-side. Products where net profit is below zero should be a predicate in the query, not a scan in the context window.
  • Return a whole series in one call. A grain of "month" over a year returns twelve rows once. It should never be twelve calls.
  • Sub-segment in one call. Top products per marketplace, in a single round trip, rather than one call per marketplace.

"How many round trips does our most common question take?" is a product metric, and most teams have never measured it. Every operation a model performs on rows it did not need is a bug you shipped.

Scope, identity and blast radius

Multi-tenancy stops being a footnote the moment an agency connects an assistant to a portfolio of client accounts.

An empty filter must mean nothing, not everything. Our internal stock service treats an empty account list as "no filter". Inside a normal web session that is harmless, because the session already constrains the user to their own data. Reached through an MCP tool, the same default would return every seller's stock. So the MCP asserts scope before it calls anything downstream rather than trusting the default. One assertion stands between a working product and a data breach, and the only reason it exists is that someone went looking for it.

Rate limit the caller, not the workspace and not the IP. A workspace is often many humans at once, so keying there makes an agency throttle itself. Keying on IP is worse, because assistants call from shared egress addresses, which would make every customer of that assistant share a single bucket.

Our take

Read-only is a decision, not a gap

Ours cannot write. The assistant reads the books, it does not run the account. That is a smaller product and a much smaller blast radius, and it should be stated plainly rather than hidden behind a roadmap.

Best fit if

  • Teams that need an assistant approved by someone other than the person who built it
  • Agencies connecting one assistant across a portfolio of client accounts
  • Anyone whose worst case should be a wrong sentence, not a changed campaign

Skip if

  • Workflows that expect the assistant to push bids or edit listings unattended
  • Write paths with no explicit approval step and no per-call log

Why observability is what makes it a product

Our tool schema carries an argument that is not data at all. Every call takes a reason: the model's own stated account of why it reached for that tool. We log it, alongside the tool, the caller and the outcome.

That is not an audit trail, although it is also that. It is a feedback loop. It tells you which tool descriptions are being misread, which questions route to the wrong tool, which arguments get filled in wrong, and which capability people keep trying to use that you never built.

Without it you are guessing at the behaviour of a non-deterministic client against an interface you wrote once and never revised. A server you cannot observe is a script that happens to speak MCP. A server you can observe, version and improve against real usage is a product.

Is your MCP a product? Ten questions

  1. Does your tool surface cost the same whether the user asks one question or fifty?
  2. Can a model learn what a metric means without guessing?
  3. Does every metric declare its unit and whether higher is better?
  4. If two of your tables can answer one question differently, have you decided which wins?
  5. Are your domain traps written into the tool descriptions, or held in a wiki the model never sees?
  6. Can the model rank, filter and aggregate on your side of the wire?
  7. How many round trips does your most common question take?
  8. Does an empty filter mean nothing, or everything?
  9. Are you rate limiting the caller, or the IP address the assistant happens to share?
  10. Do you know what people asked yesterday, and why the model chose the tool it chose?

Answer no to most of these and you have an MCP server. Answer yes and you might have a product.

The model is not the bottleneck

Every failure described here happens after the model is already good enough. None is solved by a better one. They are solved by somebody deciding what a number means before the machine reads it, writing that decision into a contract the machine actually re-reads, and then watching what happens.

That work does not demo well. It is the product.

If you want to see the argument implemented rather than described, read how the Amazon MCP surface is designed, browse every data point it exposes, or walk the Claude connection steps and ask it something hard. For a deeper read on writing tools models can use well, Anthropic's engineering notes on writing tools for agents cover much of the same ground from the client side.

Frequently asked questions

MCP server design, in practice

An MCP server is a program that exposes tools and data to an AI assistant over the Model Context Protocol, an open standard for connecting models to external systems. The server advertises what it can do, the assistant calls those tools, and the server returns structured results.
Building one is straightforward and can be done in a day over an existing API. Building one that returns trustworthy answers under real use is the hard part, and it is mostly data modelling, semantics and constraint design rather than protocol work.
Usually because the data reaching the model is ambiguous, unreconciled or missing a domain constraint. A model handed two differently-defined columns with similar names, or rows that cannot legitimately be summed, will produce a fluent and confident answer rather than an error.
More than most teams measure. Tool definitions load on every conversation, so a large schema is a fixed cost per conversation. Our full metric catalogue would be roughly 19,000 tokens if returned in one call, which is why discovery is split into a shallow list and an on-demand detail lookup.
The server. A single product row with attributes and a comparison costs roughly 112 tokens, so ranking a 2,000-product catalogue in context means moving about 225,000 tokens before any reasoning happens, and then discarding almost all of it.
Treat it as a prompt rather than documentation. State what the tool covers and excludes, name the units, forbid the operations that produce silently wrong results, and point at the correct sibling tool when a question belongs elsewhere. It is re-read on every call, which makes it the highest-leverage text in the system.
It depends on the blast radius you are willing to accept. Read-only servers are smaller, safer and easier to reason about. If you do allow writes, gate them behind an explicit approval step and log every one.

See a production MCP, not a demo

Nova's MCP server hands Claude, ChatGPT and Gemini a defined, reconciled, guarded view of an Amazon business. Connect it and ask it something you would not trust a demo with.