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 APIMCP server
CallerApplication codeAn AI host through an MCP client
DiscoveryDocs, SDK, or OpenAPIBuilt into the protocol
ChoiceThe developer picks an endpointThe model selects from described tools
InterfaceREST, GraphQL, RPC, eventsJSON-RPC with MCP primitives
Best fitKnown workflows between servicesOne 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

01

Developer tools

Search code, inspect builds, query errors, read documentation, or run safe maintenance actions from an editor or coding agent.

02

Business data

Let an assistant answer bounded questions over inventory, orders, analytics, support tickets, or internal knowledge.

03

Operations

Expose diagnostics, deployment status, logs, and runbooks while keeping destructive actions behind separate controls.

04

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.

Each layer has one job. A request only moves right after the layer before it has finished.

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.

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.

  1. Prefer small, single-purpose tools. Separate operations are easier to select correctly than one tool with unrelated modes.
  2. Use stable identifiers. Provide a lookup operation when users may supply names, codes, or external ids.
  3. State important caveats in the description. The client reads the description before deciding which tool to call.
  4. Represent unknown states explicitly. Use outcomes such as not_found, ambiguous, or unavailable instead of values that resemble valid zero results.
  5. Keep schemas strict. Reject unknown fields, invalid types, oversized strings, malformed text, and out-of-range collections.
  6. 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.

DimensionRecommended control
Request frequencyPer-credential and per-address rate limits
Concurrent workA global or tenant-level slot limit; refuse excess work rather than queue indefinitely
Execution timeA total call deadline plus database query timeouts
Rows returnedA fixed page size with explicit pagination metadata
Response bytesA serialized-size cap applied before sending
Batch inputsA 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.

Testing strategy

Use several layers of verification:

  1. Unit tests for domain behavior and validation.
  2. Contract tests that serialize responses exactly as they travel over the wire and validate them against the published schema.
  3. Fuzz tests for malformed, oversized, and adversarial inputs.
  4. Reference checks that compare selected tool answers with authoritative queries.
  5. Real-client tests using each supported host or SDK version.
  6. 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

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.