DEV Community

Cover image for Your MCP Server's Client Is a Language Model. Write the Contract Like You Mean It
guo king
guo king

Posted on • Originally published at spec-coding.dev

Your MCP Server's Client Is a Language Model. Write the Contract Like You Mean It

The Model Context Protocol quietly turned every internal script, database query, and admin action into an API.

The client for that API is not a developer reading your docs. It's a language model reading your tool descriptions at runtime and deciding — on its own — what to call and with what arguments.

When a human developer integrates your API, ambiguity gets absorbed by their judgment. They read the docs twice, test in staging, and email you when the 409 makes no sense. An agent does none of that. It reads the tool's name, description, and input schema, forms a belief about what the tool does, and acts on that belief immediately, in production, possibly hundreds of times an hour.

Every ambiguity in a human-facing API costs you a support thread. Every ambiguity in an agent-facing tool costs you whatever the agent decided it meant, multiplied by every session that reaches the same fork.

The fix isn't better prompting on the client side — client prompts are the one thing you don't control. The fix is a contract on the server side.

What changes when the consumer is an agent

Contract element Human developer AI agent
Documentation Read once during integration Re-read from the schema every session; the description is the docs
Validation errors Fixed during development Must name the failing field so the agent can self-correct
Retries Written deliberately, once Attempted whenever an error looks transient; idempotency is survival
Destructive actions Guarded by developer caution Guarded only by what the contract flags
Breaking changes Announced in a changelog Silently misfire until a human notices agent behavior degraded

None of this is exotic. It's the same rigor a good OpenAPI spec already demands — applied with the assumption that the reader has perfect patience, zero context outside the schema, and no ability to ask a clarifying question.

Input schemas: constrain, don't describe

JSON Schema is the enforcement layer of the contract, and agents respect it more reliably than they respect prose. Every constraint you can express in the schema, express in the schema: enums instead of free-text strings, patterns for identifiers, explicit defaults, required fields kept to the true minimum.

"amount_cents": {
  "type": "integer",
  "minimum": 1,
  "maximum": 500000,
  "description": "Refund amount in cents. Must not exceed the invoice's remaining refundable balance; the server rejects overdrafts with REFUND_EXCEEDS_BALANCE."
}
Enter fullscreen mode Exit fullscreen mode

The anti-pattern is the string-typed field with a paragraph of prose explaining the accepted values. Agents will eventually send the one variant your parser didn't expect.

Output contracts: agents parse, they don't skim

An MCP tool that returns a friendly paragraph of prose forces every consuming agent to parse English. That parse is a probabilistic operation that will go wrong somewhere.

Commit to structured output: stable field names, a machine-readable status, identifiers the agent can feed into follow-up calls. Prose belongs in one designated summary field, not in the load-bearing structure.

{
  "status": "refunded",
  "refund_id": "rf_8104",
  "amount_cents": 4200,
  "invoice_state": "closed",
  "summary": "Refunded $42.00 against invoice inv_2231."
}
Enter fullscreen mode Exit fullscreen mode

And treat output fields with public-API backward-compatibility discipline. Renaming refund_id to refundId is a breaking change even though no compiler will catch it — the agents consuming it just start failing at whatever step needed the identifier.

An error taxonomy agents can act on

A human reads "Something went wrong, please try again later" and applies judgment. An agent needs the judgment encoded: every error should map to exactly one recovery action.

Class Example code Agent action
Transient UPSTREAM_TIMEOUT Retry with backoff, same idempotency key
Invalid input INVALID_FIELD: amount_cents Fix the named field, retry once
State conflict REFUND_EXCEEDS_BALANCE Re-read state, re-plan, don't blind-retry
Permission ROLE_FORBIDDEN Stop and surface to the user
Not found INVOICE_NOT_FOUND Verify the identifier with a read tool first

Two rules keep the taxonomy honest:

  1. Codes are append-only. Agents and their prompts calcify around them; renames are breaking changes.
  2. Every validation error names the failing field in structured detail. A bare "invalid request" sends an agent into a guess-and-retry loop that looks, in your logs, exactly like an attack.

Side effects: flag them or agents will find them

Classify every tool three ways: read-only or mutating, idempotent or not, reversible or destructive. MCP has native annotations for exactly this — readOnlyHint, destructiveHint, idempotentHint — and your spec should decide their values deliberately rather than defaulting them. Client applications use these hints to decide when to ask a human for confirmation; a mislabeled tool inherits whatever trust the label promised.

Any mutating tool an agent may retry needs an idempotency_key input that makes duplicate calls return the original result.

For genuinely destructive operations — deletion, irreversible sends, money movement — the strongest contract clause is a two-step shape: a dry_run mode that returns what would happen plus a confirmation token, and an execute mode that requires that token. That turns "the agent deleted the wrong thing" from an incident into a rejected call.

Putting it together: a refund tool contract

tool: refund_invoice
purpose: refund a failed or disputed invoice, once
inputs: invoice_id (pattern inv_*), amount_cents (1..500000),
        idempotency_key (uuid, required), dry_run (bool, default true)
output: status, refund_id, amount_cents, invoice_state, summary
errors: INVALID_FIELD, INVOICE_NOT_FOUND, REFUND_EXCEEDS_BALANCE,
        ROLE_FORBIDDEN, UPSTREAM_TIMEOUT
side effects: mutating, idempotent via key, destructive
              (readOnlyHint=false, destructiveHint=true,
               idempotentHint=true)
policy: dry_run=true returns preview + confirm_token;
        execution requires a confirm_token issued within 10 minutes
version: v2 (v1 accepted amount as dollars; removed 2026-06)
Enter fullscreen mode Exit fullscreen mode

Every line answers a question an agent would otherwise answer by guessing. The version line is not decoration: the dollars-to-cents change is exactly the kind of silent semantic shift agent clients cannot detect and cannot survive.

The uncomfortable truth about agent-facing APIs: your real interface is not the code. It's the beliefs your contract induces in a language model. Write the contract first, and those beliefs are at least ones you chose.


The full version — with the versioning/review-gate process and a deeper error-taxonomy method — is on spec-coding.dev. There's also a free browser-based API spec generator that produces this contract structure from a plain-language description.

Top comments (0)