Chapter 7 of 8 · 9 min read
Name the threshold that flips the decision, or it is just opinion
On this page
The design doc was two pages and recommended keeping the pricing lookup as an in-process tool. It had the latency numbers: the call runs at 60 ms p50 against a product budget of 800 ms p95 end to end, and the platform group's proposed MCP server had been benchmarked at 60 ms plus an 18 ms hop, which fits with 700 ms to spare. Two reviewers approved it. The third left one comment: "This reads as a preference with a benchmark attached. What would change your mind?"
That comment is the difference between a recommendation and a position, and it is not a rhetorical question. Two other teams had asked to use the pricing lookup that month, which is the single strongest argument for a server that exists, and the doc had answered it by measuring the hop. The benchmark passed, and it had measured the one cost that was never in dispute. An internal hop is always tens of milliseconds; nobody proposes an MCP server and loses on latency. What the doc owed its readers was the set of conditions under which its own answer stops holding, stated precisely enough that someone could check them in six months without re-running the argument.
What the second and third consumer actually change
Two more teams wanting the pricing lookup is a genuine change in the situation, and it is not the same as a decision. The claim "three consumers means a server" skips a step, because serving is one way to share code and not the only one. The other is publishing a client library, and the comparison between them is about how the consumers are coupled rather than about how many there are.
| Three copies of a library | One in-process library, published | One MCP server | |
|---|---|---|---|
| Sharing achieved | No; three divergent implementations | Yes, at the package version | Yes, at the wire schema |
| Consumers must share a runtime | — | Yes (all three are Python here) | No |
| Who holds the pricing credential | Each consumer | Each consumer | The server only |
| Update propagation | Three PRs | Version bump per consumer, on their cadence | Immediate, whether they are ready or not |
| New failure modes | None | None | Availability, timeout, version skew |
| Operational burden | None beyond each agent | Package release process | Deploy, auth, rotation, runbook |
All three consuming agents are Python services in the same monorepo on a weekly release train, so the middle column is available and costs a package release process the org already has. The library gets the sharing benefit — one implementation of the pricing rules, one place to fix a bug — without acquiring an availability dependency. That is why the number of consumers on its own does not decide: it makes duplication unacceptable, and duplication has two cures.
The server becomes the right cure when the library stops working as one, and the specific reasons it stops are the thresholds worth writing down.
The thresholds, written as conditions someone can check
A threshold is only useful if it is falsifiable by a person who was not in the room. "If this becomes a bigger deal we will revisit" is not a threshold; it is the absence of one, and it converts every future argument back into a matter of seniority. The version in the revised doc read as follows, and each item names an observable.
## Conditions that flip this recommendation to a server
1. A consumer outside the Python monorepo. A Go, TypeScript or
third-party-hosted agent cannot import `pricing-client`. One such
consumer flips this immediately; the wire protocol is the only
remaining sharing mechanism.
2. A caller that must not hold the pricing credential. Today all three
agents run in the internal tier. If the customer-facing assistant
(tier-3, handles untrusted input) needs pricing, it must not carry
the pricing DB role, and a server that authenticates it and returns
only quotes is the mechanism that removes the credential from it.
3. Pricing-rule change rate above the consumers' release cadence.
Rules changed 4 times in the last 12 months against a weekly train,
so the library propagates in time. Above roughly one change per
week, consumers would be running stale rules more often than not.
4. Budget headroom below 150 ms p95. Not a reason to take the server;
a reason to stop treating the hop as free. Below this we re-measure
before any relocation.
Not thresholds: number of consumers on its own; the platform group's
willingness to build it; the capability being "a platform concern".Each of those has a number or a named system in it, which is what lets a reviewer disagree productively — an argument about whether the rule change rate is really four a year is a better argument than one about whether pricing is platform-shaped. The last block matters as much as the first four, because the conditions people will actually invoke in future meetings are the ones ruled out there.
Two costs belong in the write-up that no latency benchmark will surface. The first is version skew, which chapter 3's Jira incident showed arriving as a silent behavioural regression rather than a build failure: the server ships a schema change, the consumers' prompts and eval fixtures still describe the old shape, and the model reconciles the two badly on some fraction of runs. The second is availability multiplied into the product's own. At 99.9% for the server, three consuming products each acquire about 43 minutes a month of a capability that used to be as available as the process itself, and someone has to be awake for that. There is a third worth listing, established in chapter 6: a broad server's full schema set becomes resident context on every turn, so adopting a nine-method server to use one method is a token cost paid in every session including the ones that never touch pricing.
Where the 800 ms actually goes
Part of why the original benchmark was uninformative is visible as soon as the budget is decomposed. The 800 ms p95 is an end-to-end number for a quote request, and it is dominated by things that have nothing to do with where the pricing body runs.
| Segment | p50 | p95 | Share of the 800 ms budget |
|---|---|---|---|
| Request parsing and context assembly | 12 ms | 30 ms | 3.8% |
| Model call 1 (plan + tool selection) | 240 ms | 380 ms | 47.5% |
| Pricing lookup | 60 ms | 140 ms | 17.5% |
| Model call 2 (compose answer) | 190 ms | 310 ms | 38.8% |
| Serialisation and transport out | 6 ms | 14 ms | 1.8% |
The segments do not sum to the end-to-end p95, because tail events do not co-occur; what the table gives is where the time is. Two model calls account for roughly 86% of the budget, and an 18 ms hop added to the pricing segment moves the end-to-end p95 by about 2%. A benchmark aimed at that segment was testing a constraint that could not bind, which is why it passed and why passing meant nothing. The fourth threshold in the list exists precisely to say when this stops being true: if a future version of the product adds a third model call and headroom falls below 150 ms, the hop stops being noise and gets re-measured before anything moves.
The costs that would have discriminated between the two options are the ones chapter 6's harness records and the ones a person has to log by hand. Resident schema tokens, if the platform group's server carries its other methods alongside the pricing one. Operational hours per quarter, which for the flag-check comparison ran 19 against 1.5 over six weeks and would be larger here because the pricing data is customer-visible and needs an authorisation story. Error class distribution under fault injection, which means deliberately failing the dependency in a test environment — dropping its connections, delaying its responses, returning a changed schema — and recording what the agent does with each, since a timeout during a deploy and a schema mismatch after one are both "the pricing call failed" and need entirely different responses. A doc that reported those three and said nothing about the hop would have been a better doc than the one that was submitted.
The refusal to staff it is a precondition, not a risk
The platform group offered to run the server and would not commit to an on-call rotation for it. That fact usually appears in a doc under "risks", phrased as "operational ownership TBD", and risks are things a reviewer skims. It belongs in the conditions instead, as something that must be true before the option is on the table at all.
An unowned server is worse than either alternative, in a way that is easy to establish from the failure-containment column. A server that is down errors loudly and attributably, which is only useful if the error reaches someone who can act on it; with no rotation, the loud signal lands in a channel and stays there, and the 38-minute outage from chapter 5 becomes the two-and-a-half-day one. It also inverts the sharing argument. The reason to centralise is that one team keeps the implementation correct for everyone; if no team is accountable for it, you have centralised the failure without centralising the maintenance, and three teams now depend on something with no one to page.
The productive response is to write the ownership requirement as an acceptance condition — a named team, a rotation, a stated availability objective, and a deprecation policy for schema changes — and to note plainly that until those exist the recommendation is the library regardless of how many consumers appear. That converts an awkward organisational fact into a specific thing the platform group can either supply or decline, and their declining is itself a decision that the doc has now recorded.
The schema does not change when the body moves
The reason all of this can be written as thresholds rather than as a bet is that crossing one does not invalidate the work. What the model interacts with is a name, a description and an input schema, and that surface is identical whether the body is a function in the process or a method on a remote server. The relocation is behind the interface.
Keeping that true is a small amount of discipline taken now rather than a rewrite taken later. The pricing logic lives in a module with no agent-specific imports, the schema is declared once and used by both the local registration and any future server, and the dispatch layer looks the capability up by name rather than calling the function directly:
# pricing/schema.py — one definition, two hosts
QUOTE = {
"name": "pricing_quote",
"description": "Current list and contracted price for a SKU on an account.",
"input_schema": {
"type": "object",
"properties": {"account_id": {"type": "string"}, "sku": {"type": "string"}},
"required": ["account_id", "sku"],
},
}
# agent/registry.py — today
registry.register(QUOTE, handler=pricing.quote)
# agent/registry.py — after relocation, same schema, same call sites
registry.register(QUOTE, handler=mcp_client("pricing").method("pricing_quote"))The parts that genuinely change on relocation are the ones the thresholds are about: an authentication story, a timeout and retry policy, a deprecation window for schema edits, and an on-call rotation. Estimated at roughly two engineer-weeks in the doc, against the four to six weeks the original proposal had assumed for building the server from the start, because the schema, the tests and the eval fixtures all survive.
Writing the migration path down also disarms the argument that most often wins these meetings, which is that building the tool now means doing the work twice. It does not, and saying so with the code in front of the reader is more persuasive than saying so in the abstract. The work that is genuinely duplicated is the operational work, and that work is exactly what you are declining to do until it is justified.
What a threshold is not
It is not a plan to build the server later, and it should not be scheduled. Three of the four conditions above may never occur, and putting "extract pricing server" on next year's roadmap converts a conditional into a commitment that will get built whether or not the condition arrives.
It is also not a way to avoid deciding. The doc says the library, now, with reasons; the conditions are what would make a future person right to disagree, not a hedge that lets the author be right either way. A recommendation that lists conditions and then declines to make a call is worse than one that simply picks, because it hands the decision to whoever is most confident in the next meeting.
And thresholds do not remove judgement from the borderline cases. A consumer that is mostly outside the monorepo, or a rule change rate that is drifting upward, still requires someone to decide. What the written threshold does is make that decision small and local, because the surrounding reasoning is already agreed and only one fact is in question.
A team that writes these conditions makes each capability decision once. A team that does not re-argues from zero every time, and re-arguing from zero has a characteristic outcome, because the heaviest mechanism is the one that sounds most like engineering. The next chapter inherits that team: five MCP servers built in a quarter, one of which returns a string.
The extensibility landscape — read the current version at https://daviddominguez.dev/learn/en/extensibility-landscape/name-the-threshold-that-flips-the-decision-or-it-is-just-opinion