Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Query vs Command
Summary
Split your MCP tools into two disjoint categories: queries that only read state, and commands that change it. Each category gets different consent handling, different idempotency guarantees, and different error semantics, and each category declares itself through tool annotations so the host can tell them apart without guessing. A tool that does both is a tool that does neither well.
Problem addressed
When tools mix reads and writes, the host cannot tell which calls are safe to retry, which need explicit approval, and which can run freely in an agent loop. A model-generated call to a hybrid tool may produce an unintended side effect on retry, fail consent because the user did not know it would write, or get over-prompted because the host assumes every call is destructive.
Serverless makes this worse, not better. On Vercel, a Vercel Function invocation can be terminated at its maxDuration mid-call, clients retry on cold starts and network blips, and there is no resident process to remember that a write already happened. Separating queries from commands lets the host treat each call appropriately: queries flow freely and retry safely, commands stop for approval and carry idempotency keys, and the failure model of each is obvious from the schema.
When to use
- The same backend supports both reads and writes (most do).
- The model needs to inspect state before acting on it, the standard shape of an agent loop.
- The host wants different UI for “look up” and “do” (different icons, different consent dialogs, different rendering).
- Retries are expected at any layer (network, model, agent loop, serverless timeout), and you cannot afford a retry to double-charge, double-send, or double-create.
- You want auditability: read access and write access must be distinguishable in logs.
When not to use
- The integration is genuinely read-only or genuinely write-only. The distinction is moot.
- Tools are trivial wrappers over an idempotent backend API where reads and writes are already commutative. Even then, naming conventions (
get_*vscreate_*) and annotations cost nothing and aid both the model and the host.
Architecture / flow diagram
Diagram source (Mermaid)
sequenceDiagram
autonumber
participant Host
participant Client
participant Server
participant Backend
Host->>Client: tools/call get_item (query)
Client->>Server: tools/call get_item
Server->>Backend: GET /items/42
Backend-->>Server: 200 OK
Server-->>Client: result (safe to retry)
Client-->>Host: render
Host->>Host: ask user for approval
Host->>Client: tools/call create_item (command)
Client->>Server: tools/call create_item
Server->>Backend: POST /items (idempotency key)
Backend-->>Server: 201 Created
Server-->>Client: result plus new state
Client-->>Host: render plus auditProtocol implications
- Tools are declared via
tools/listand invoked viatools/call. The protocol does not enforce a query/command distinction; the pattern is a discipline the server applies on top. - Declare the category with
ToolAnnotations:readOnlyHint: trueon queries;destructiveHintandidempotentHintset honestly on commands. The spec definesreadOnlyHint,destructiveHint,idempotentHint, andopenWorldHint; there is no dedicated “requires approval” field, so the host derives the consent requirement from these hints plus the read/write split. Annotations are untrusted hints, not enforcement: a host must not skip consent because a server claimedreadOnlyHint. - Name tools so the category is obvious without reading the schema:
get_*,list_*,search_*for queries;create_*,update_*,delete_*,send_*for commands. MCP’s official tool-naming guidance (SEP-986) applies; align your conventions with it so the split is legible to both the model and the host. - Return
tools/listin deterministic order, which 2026-07-28 recommends for client caching and LLM prompt-cache hit rates. A stable order also keeps the split legible: group queries and commands consistently instead of interleaving them by registration accident, and the model’s prompt prefix stays cacheable as tools are added. - Queries should be safe to retry and should not require user approval beyond the host’s standing consent for the connection.
- Commands should accept and honor an idempotency key so the same call retried produces the same effect, not a duplicated one. First write wins; the replay returns the original result.
- Errors from queries are informational; errors from commands must indicate whether the side effect occurred. Succeeded-then-lost-the-response is a different failure from never-ran, and on a platform that can terminate an invocation at the deadline, your command results must let the caller tell them apart.
- Per SEP-1303 (adopted in MCP 2025-11-25 and unchanged in 2026-07-28), return input-validation failures as tool execution errors (a
tools/callresult withisError: true), not JSON-RPC protocol errors, so the model can read the rejection and self-correct. This applies to both categories but matters most for commands, where strict argument validation is the gate in front of a side effect. The TypeScript SDK v2 follows this: schema-invalid arguments come back asisError: trueresults, andcallTooldoes not throw for them. Calling a tool that does not exist is the opposite case, a protocol error: SDK v2 rejects thetools/calloutright rather than returning a tool result.
Vercel mapping
- Annotations are one line of registration. With the TypeScript SDK v2 under mcp-handler 2.x,
server.registerTool(name, { description, inputSchema: z.object({ ... }), annotations: { readOnlyHint: true } }, handler)declares a query; commands setdestructiveHintandidempotentHintinstead. The split costs nothing at runtime; it is pure contract. - Serverless is a retry machine, so idempotency is not optional. A function can hit
maxDurationafter the backend write but before the response is sent, and the client’s natural response is to retry. Every command needs an idempotency key with first-write-wins replay. - The replay store cannot live in module scope. Fluid compute reuses instances best-effort; an in-memory idempotency cache works on the instance that took the first call and silently fails on every other one. Production replay state belongs in a Marketplace Redis or Postgres store keyed by principal plus idempotency key. Read Serverless sessions before trusting any module-scope state.
- Environments split naturally along the same line. Give preview deployments a credential that can only serve queries, or a separate scratch backend for commands; the environment-scoped variable is the enforcement point. A preview URL that can run production commands is an incident waiting for a crawler.
The platform will retry your commands whether you designed for it or not. A retry you did not design for is a write you did not intend.
Security considerations
- Every command must require explicit user approval before invocation; queries may run under broader session consent. Derive the requirement from the read/write split, never from the server’s self-reported hints alone. See Consent & user approval.
- Approval prompts for commands must include the tool name, the resolved arguments, and the target system. See Consent & user approval.
- Commands must enforce per-principal authorization server-side using the authenticated principal; queries should too, but commands are the higher-blast-radius case. See Authorization & scoping.
- Validate command arguments more strictly than query arguments: a malformed query returns wrong data; a malformed command writes wrong data. See Input validation.
- Log queries and commands at the same level but tag them distinctly so audit reviews can prioritize commands. See Monitoring & audit.
- Pagination and size caps apply to queries; idempotency keys, side-effect-occurred semantics, and rollback on cancellation apply to commands. See Session handling.
Example implementation
examples/query-command-server(in the repository) - the paired implementation of this page: read-onlylist_items/get_itemqueries alongside acreate_itemcommand, each declaring honestToolAnnotations(readOnlyHinton the queries,destructiveHint/idempotentHinton the command). The command takes an idempotency key backed by a first-write-wins replay store keyed by principal plus idempotency key (the principal comes from the verified token viactx.http.authInfo, falling back to ananonymousnamespace, never from a tool argument): a retried call returns the original item, not a duplicate, and another principal replaying the same key gets its own item rather than the first caller’s; the tests assert both negatives. Thenameandidempotency_keybounds (1..64 and 1..128 characters) live in the zod schema and are advertised in the command’sinputSchemaasminLength/maxLength. The in-memory store keeps the tests deterministic and offline; the README says what a production deployment moves to Redis or Postgres.examples/secure-tools-server(in the repository) - the server-side controls a command needs once it exists: input validation, default-deny authorization, and output minimization.examples/minimal-server(in the repository) - a singleechotool showing the bare request/response shape; because it reads no external state it is not a true query, only the simplest possible tool.
Trade-offs
| Pros | Cons |
|---|---|
| Host can give queries low-friction consent and commands strong consent. | Two tool variants per operation roughly doubles the surface. |
| Retries are safe by construction; idempotency lives in the command path. | Discipline must be maintained; a hybrid tool added later silently breaks the contract. |
| Audit logs cleanly separate “what was looked at” from “what was changed”. | Naming and annotation conventions must be enforced in review; the protocol does not check them. |
| The model can plan aggressively because read-only exploration is cheap. | Idempotency needs a durable replay store on serverless; module scope is not one. |
Related patterns
- adapter - the natural place to apply this split; every adapter should declare its tools as queries or commands, not both.
- async-jobs - long-running commands graduate to async jobs with explicit handles; the start tool is a command, the status tool a query.
- least-privilege - commands typically require broader credential scope than queries; split credentials accordingly when feasible.
- trust-boundaries - commands cross more trust boundaries than queries and deserve correspondingly stricter validation.
- facade - the central place to enforce naming and annotation conventions across many backends.
Vercel deployment (Terraform)
There is deliberately no terraform/patterns/query-vs-command/. This is a tool-design pattern: the split lives in tool names, annotations, and handler code, not in provisioned infrastructure, and an infra file would only show generic project config that teaches nothing this page does not. See the “What’s deliberately not here” section of terraform/README.md (in the repository).
Bibliography
- Model Context Protocol Specification, Tools, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol Specification, Server features, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server
- Vercel Documentation, Fluid compute - https://vercel.com/docs/fluid-compute
- Vercel Documentation, Configuring Maximum Duration for Vercel Functions - https://vercel.com/docs/functions/configuring-functions/duration
- Vercel Documentation, Redis - https://vercel.com/docs/redis
- Vercel Documentation, Postgres - https://vercel.com/docs/postgres
- OWASP Top 10 for Large Language Model Applications - https://owasp.org/www-project-top-10-for-large-language-model-applications/