Technical guide · 12 October 2026
MCP without the hand-waving
What it is, why it isn’t “just an API,” how the versions differ, and what makes a server safe enough to trust.
A clean response can still be a wrong answer.
The hard part of MCP isn’t wiring up tools/list or parsing JSON-RPC. It’s the boundary between a probabilistic caller and production data. That boundary has to hold when inputs are vague, clients are old, capacity is tight, and a technically valid answer is still misleading.
Everything below is about closing that gap. No framework worship and no magic middleware—just where authentication belongs, how tools should communicate meaning, what needs a hard limit, and how to catch answers that look right but aren’t.
First: what MCP actually is
MCP is a common language between an AI application and the systems it can use. A server exposes a menu of tools, resources, and prompts. The client reads that menu, gives it to the model, and sends back a structured call when the model chooses a tool.
MCP does not decide how a model reasons, and it does not replace the service behind it. It standardizes discovery and tool calling. The business logic can still live in an ordinary application or behind an ordinary API.
MCP and an API are not rivals
An API is built for software that already knows which endpoint to call. MCP adds a model-facing contract on top: a discoverable tool menu, descriptions written for model selection, capability negotiation, and a standard result shape.
| Typical API | MCP server | |
|---|---|---|
| Caller | Application code | An AI host through an MCP client |
| Discovery | Docs, SDK, or OpenAPI | Built into the protocol |
| Choice | The developer picks an endpoint | The model selects from described tools |
| Interface | REST, GraphQL, RPC, events | JSON-RPC with MCP primitives |
| Best fit | Known workflows between services | One capability used by many AI hosts |
The practical pattern is often MCP in front, API underneath. The API remains the source of business behavior. MCP turns a safe part of it into a tool surface that different AI clients can understand.
The versions, without the changelog soup
MCP versions are dates, not semantic version numbers. A client and server must agree on the same protocol shape. The important releases mark four distinct stages:
The remote-server release
Introduced Streamable HTTP, OAuth 2.1 authorization, JSON-RPC batching, tool behavior annotations, and audio content.
Structured answers and tighter OAuth
Added structured tool output, elicitation, resource links, and the HTTP protocol-version header. JSON-RPC batching was removed.
Richer interaction
Added URL-mode elicitation, tool calling during sampling, improved authorization discovery, icons, and experimental durable tasks.
The stateless reset
Removed protocol sessions and the initialization handshake. Requests carry version and capabilities, server/discover handles discovery, and multi-round trips replace server-initiated requests.
Compatibility rule: do not assume the newest spec is the one your client speaks. Check the client SDK, negotiate deliberately, and test every version you claim to support.
Where MCP earns its keep
Developer tools
Search code, inspect builds, query errors, read documentation, or run safe maintenance actions from an editor or coding agent.
Business data
Let an assistant answer bounded questions over inventory, orders, analytics, support tickets, or internal knowledge.
Operations
Expose diagnostics, deployment status, logs, and runbooks while keeping destructive actions behind separate controls.
Creative software
Give a model structured access to design files, scenes, timelines, assets, or document operations instead of screen-scraping the UI.
A reliable MCP implementation is still a public API. Authentication, authorization, validation, hard resource limits, observability, and compatibility are not optional extras.
Start with the wire
MCP uses JSON-RPC 2.0 messages. The 2025 releases used an initialization handshake. The current 2026 protocol is stateless: a client can discover capabilities first, then every request carries the version and capabilities it needs.
server/discover
→ tools/list { version, capabilities }
→ tools/call { name, arguments }
→ result or error A hosted service commonly uses Streamable HTTP. Plain JSON responses are sufficient when server-sent events are unnecessary. Avoid keeping streams open on infrastructure with a small synchronous worker pool.
Support should be based on the revisions used by intended clients. Negotiate handshake-era clients deliberately, validate the per-request version used by stateless clients, and define a clear response for unsupported versions. Only the March 2025 revision in this comparison supports JSON-RPC batching; if it is accepted, set a strict maximum batch size.
Keep the layers boring
Keep HTTP handling, authentication, protocol dispatch, tool execution, and response shaping as distinct layers. Tool implementations should accept validated domain inputs and return domain results without knowing about bearer tokens, HTTP headers, or JSON-RPC envelopes.
This structure reduces accidental authorization gaps and makes tools easy to unit-test. Authentication should complete before method dispatch so newly added protocol methods cannot bypass it.
Check identity before anything else
Machine clients usually authenticate with bearer credentials or OAuth. For API keys, generate high-entropy values, display the plain value only once, and store a cryptographic hash. Compare hashes in constant time.
- Associate every credential with one tenant or security boundary.
- Use a default-deny allowlist for tool access.
- Support immediate revocation, expiry, and rotation.
- Apply authentication to initialization and discovery methods as well as tool calls.
- Never include credentials in logs, errors, analytics, or traces.
Authorization must be evaluated before tenant data is read. Write-capable tools should never be granted automatically when a new tool is registered.
Write tool contracts for the model
Tool descriptions are part of the interface. They should explain what the result means, what it does not mean, and any condition that changes how the result should be interpreted.
- Prefer small, single-purpose tools. Separate operations are easier to select correctly than one tool with unrelated modes.
- Use stable identifiers. Provide a lookup operation when users may supply names, codes, or external ids.
- State important caveats in the description. The client reads the description before deciding which tool to call.
- Represent unknown states explicitly. Use outcomes such as
not_found,ambiguous, orunavailableinstead of values that resemble valid zero results. - Keep schemas strict. Reject unknown fields, invalid types, oversized strings, malformed text, and out-of-range collections.
- Keep tool order and descriptions stable. Unnecessary changes can invalidate client-side prompt caches.
Validation errors should name the affected field and explain how to correct it. Return these as tool-level errors when the protocol request itself is valid; reserve JSON-RPC protocol errors for malformed or unsupported messages.
Response compatibility and semantics
Some clients consume structured content while others expect text blocks. When broad compatibility is required, return structured JSON and a text representation of the same data. Keep both representations semantically identical.
Publish an output schema for each tool and validate serialized responses against it in tests. If a field can legitimately be missing, model that state explicitly. Include a data-freshness timestamp when results come from periodic imports or cached sources.
Put a ceiling on everything expensive
Limits should be chosen from infrastructure capacity and measured workloads. The exact values will differ by system, but the controlled dimensions are consistent.
| Dimension | Recommended control |
|---|---|
| Request frequency | Per-credential and per-address rate limits |
| Concurrent work | A global or tenant-level slot limit; refuse excess work rather than queue indefinitely |
| Execution time | A total call deadline plus database query timeouts |
| Rows returned | A fixed page size with explicit pagination metadata |
| Response bytes | A serialized-size cap applied before sending |
| Batch inputs | A maximum number of ids, search terms, and JSON-RPC messages |
Every partial response should declare that it was truncated and provide pagination or omitted-count metadata. Silent truncation produces plausible but incomplete answers.
Make failure boring to parse
All failures should produce bounded JSON-RPC responses with the original request identifier when available. Error data should distinguish invalid input, denied access, rate limiting, unavailable capacity, upstream failure, and timeout.
Include a retryable flag and a safe retry interval where appropriate. Do not return HTML error pages from an API endpoint, and do not expose stack traces, SQL, infrastructure identifiers, or internal paths.
Hunt the wrong “OK”
Refusal tests are necessary but incomplete. A high-risk failure mode is a successful response whose meaning is wrong. Include tests for conflicting identifiers, shared inventory or quotas, absent entities, boolean coercion, nullable fields, and partial upstream data.
- Validate every supplied identity constraint rather than silently ignoring extras.
- Require real JSON booleans instead of coercing strings.
- Distinguish an unknown entity from a known entity with zero activity.
- Ensure multiple lines in one request share the same finite resource.
- Count tool-level errors separately from successful calls, even when both use HTTP 200.
Testing strategy
Use several layers of verification:
- Unit tests for domain behavior and validation.
- Contract tests that serialize responses exactly as they travel over the wire and validate them against the published schema.
- Fuzz tests for malformed, oversized, and adversarial inputs.
- Reference checks that compare selected tool answers with authoritative queries.
- Real-client tests using each supported host or SDK version.
- Load and failure tests for concurrency, timeouts, rate limits, and unavailable dependencies.
Measure the serialized size of tools/list. Tool schemas and descriptions may be included in model context repeatedly, so menu size is a performance budget as well as a documentation concern.
Observability without sensitive data
Write one structured event per call. Useful fields include outcome, method, tool, argument names, credential identifier, client name and version, duration, response bytes, row count, truncation state, protocol revision, and request id.
Do not record credential values, argument values, request bodies, returned data, or customer content. Set a retention period and protect refusal logs from crowding out successful-call telemetry during an attack.
Deployment checklist
- Authentication runs before parsing or dispatch.
- Every tool is covered by an explicit permission policy.
- All inputs and serialized outputs are bounded.
- Timeouts are enforced by the component doing the expensive work.
- Errors remain valid JSON-RPC and contain no sensitive details.
- Tool descriptions document semantic caveats.
- Supported protocol revisions are tested with real clients.
- Logs contain metadata only, never values or answers.
- A post-deployment probe verifies the endpoint and authentication path.
- Write operations remain disabled unless separately designed and reviewed.
The short version
A production MCP server is a security boundary and an API for probabilistic callers. Reliability comes from explicit semantics, strict validation, bounded execution, machine-readable failures, and tests that search for believable wrong answers—not only crashes and refusals.