A production MCP Server is a policy-enforced capability service, not a transport wrapper around arbitrary functions. Under MCP 2026-07-28, every Request must be independently routable, authenticated when required, authorized, bounded, cancellable and observable. The protocol no longer supplies a handshake or Session ID that can carry those responsibilities.

This guide assumes the reader already understands Host, Client, Server, Tool, Resource and Prompt. For the protocol model, start with the MCP Protocol Guide. For identity-provider integration, use the enterprise OAuth guide.

TL;DR

  • Use stdio for a local process boundary and Streamable HTTP for a remote MCP endpoint.
  • In 2026-07-28, every remote message is a new POST. There is no initialize, notifications/initialized, Mcp-Session-Id, GET stream or resumable SSE.
  • Keep business continuity in explicit State Handles. A Connection, Process or SSE Stream is not a user, task, tenant or authorization context.
  • Admit requests through ordered gates: transport limits, identity, protocol headers, schema, object authorization, side-effect policy and execution budget.
  • Treat Tool Definition, Arguments, Resource Content and Result Content as untrusted input.
  • Separate read, propose and execute capabilities. An Annotation describes expected behavior but never grants permission.
  • Retry only when safety is proven. A timeout after dispatch can mean Unknown Effect, not failure.
  • Return bounded data and bind Private Cache entries to the full Authorization Context.
  • Run Legacy protocols on explicit compatibility routes or adapters; never let an untrusted Request choose a weaker path.

Define the Production Contract

Production readiness means one Request can be explained without relying on hidden connection history. The operator should be able to answer:

Boundary Required evidence Unsafe shortcut
Protocol revision, Method, Name, Request ID, Descriptor Hash trust a Connection negotiated earlier
Identity Resource Server, Issuer, Principal, Tenant, credential age accept any token from a familiar IdP
Authorization Tool/Resource/Prompt, object, arguments, purpose, effect equate Scope or Schema with permission
Execution deadline, byte budget, concurrency class, idempotency key use one global timeout and retry policy
Result Result Type, output validation, provenance, effect status assume a transport error means rollback
Audit policy revision, decision, latency, redacted outcome log raw Tokens or complete sensitive payloads

MCP standardizes a protocol boundary. It does not supply tenant isolation, transaction semantics, business authorization, secret management or incident response. Those controls remain application responsibilities even when an SDK handles JSON-RPC framing.

Choose the Transport by Trust Boundary

MCP 2026-07-28 defines two standard transports with different operational models.

Local stdio

Use stdio when the Host launches a local Server process. Each message is one line of JSON-RPC on standard input or output; logs belong on standard error.

Local does not mean harmless. The process can inherit environment variables, file permissions, network access and executable paths. Pin the package and command, verify its provenance, run it with the minimum OS identity, restrict directories and egress, and show the exact launch command before installation. A malicious local Server can be more dangerous than a remote API because it may start inside the user's trust boundary.

Remote Streamable HTTP

Use Streamable HTTP when the Server is independently deployed. The modern flow is intentionally request-scoped:

  1. The Client sends one JSON-RPC Request in a new HTTP POST.
  2. The POST carries MCP-Protocol-Version and Mcp-Method.
  3. tools/call, resources/read and prompts/get also carry Mcp-Name.
  4. The body carries the same protocol revision, Client Capabilities and optional Client Info in _meta.
  5. The Server returns one JSON response or a request-scoped SSE stream ending in the final response.
http
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json,text/event-stream
Authorization: Bearer <access-token>
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: reports.publish

{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"name":"reports.publish","arguments":{"reportId":"rpt_72","idempotencyKey":"pub_8f3"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"operations-host","version":"4.2.0"}}}}

The JSON-RPC body remains the source of truth. Reject a header/body mismatch before dispatch. Validate Origin when present and return 403 for a disallowed Origin. Bind a local HTTP Server to loopback unless remote exposure is intentional.

SSE is only a response representation for related progress or logging Notifications followed by the final Result. Closing that response stream cancels the Request; the Server should stop work as soon as practical and must not emit further messages. There is no standalone GET stream, Protocol Session, Last-Event-ID recovery or independent Server Request on that stream.

Every Server must implement server/discover, but a Client does not have to call it before a business Method. If a Request names an unsupported revision, return UnsupportedProtocolVersionError with the supported revisions instead of silently downgrading.

Long-lived list and Resource change Notifications use an explicit subscriptions/listen Request. MRTR carries Sampling, Elicitation or Roots input inside InputRequiredResult; it does not recreate a hidden bidirectional Session.

Scale Without Protocol Sessions

Stateless Core removes transport affinity, not application state. Any compatible instance can process a Request because the protocol revision and Client Capabilities travel with that Request. A plain round-robin load balancer no longer needs sticky routing or a shared MCP Session Store.

Business workflows still need continuity. A browser automation run, draft, cart, export job or approval can outlive one Request. Represent it with an explicit opaque State Handle returned by a Tool and supplied as an Argument on later calls.

typescript
import { createHash, randomBytes } from "node:crypto";

type HandleRecord = {
  principalId: string;
  tenantId: string;
  workflowType: string;
  policyRevision: string;
  expiresAt: number;
};

const stateHandles = new Map<string, HandleRecord>();

function digest(value: string): string {
  return createHash("sha256").update(value).digest("hex");
}

export function issueStateHandle(
  record: Omit<HandleRecord, "expiresAt">,
  ttlMs: number,
): string {
  if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
    throw new Error("invalid_ttl");
  }

  const handle = randomBytes(32).toString("base64url");
  stateHandles.set(digest(handle), {
    ...record,
    expiresAt: Date.now() + ttlMs,
  });
  return handle;
}

export function requireStateHandle(
  handle: string,
  principalId: string,
  tenantId: string,
): HandleRecord {
  const record = stateHandles.get(digest(handle));
  if (
    !record ||
    record.expiresAt <= Date.now() ||
    record.principalId !== principalId ||
    record.tenantId !== tenantId
  ) {
    throw new Error("state_handle_not_authorized");
  }
  return record;
}

This in-memory store illustrates the contract, not a distributed implementation. A real store needs atomic expiry, revocation, bounded capacity and race-safe updates.

Treat a Handle as an attacker-controlled object reference, even when it is random and opaque. Do not put credentials or trusted Claims inside the visible value. Bind the stored record to Principal, Tenant, workflow type, permitted operations, policy revision and expiry; reauthorize it on every use. Rotate or revoke it when ownership, consent or credentials change.

Build an Ordered Request Admission Pipeline

The safest request path rejects cheap failures before expensive work and records exactly which boundary made the decision.

flowchart LR A["HTTP or stdio input"] --> B["Size, rate and deadline"] B --> C["Identity and tenant"] C --> D["Protocol and header checks"] D --> E["Schema and business validation"] E --> F["Object and effect authorization"] F --> G["Bounded execution"] G --> H["Output validation and redaction"] H --> I["Result and audit event"]
  1. Transport admission: limit body bytes, decompressed bytes, concurrent streams and header count. Reject invalid Origin and malformed JSON before Tool lookup.
  2. Identity: validate the credential for this Resource Server and derive a small Principal. Never pass the raw Token into a Tool.
  3. Protocol: compare MCP-Protocol-Version, Mcp-Method and Mcp-Name with the body. Reject unsupported revisions and Methods explicitly.
  4. Shape: validate JSON Schema and reject undeclared or malformed Arguments. Network $ref resolution should remain disabled.
  5. Business policy: authorize Tenant, object, Action, Purpose, data class and side effect. Schema validation cannot prove ownership.
  6. Execution: apply Tool-specific deadline, concurrency, egress, row and cost budgets.
  7. Result: validate structuredContent against outputSchema when declared; cap all Content and redact Secrets.
  8. Audit: emit a stable outcome without storing unrestricted Prompts, Tokens or private payloads.

Keep the Policy Decision and Tool Handler separate. This makes denials testable and prevents a model-generated Argument from silently selecting an administrative code path.

Authenticate the Resource, Then Authorize the Operation

A protected remote MCP Server acts as an OAuth Resource Server. The Client is an OAuth Client, and an Authorization Server issues Access Tokens. For delegated HTTP access, the current MCP profile uses Protected Resource Metadata, Authorization Server Metadata, PKCE, RFC 9207 Issuer validation and RFC 8707 Resource Indicators.

The boundary is narrower than “OAuth makes the Tool safe”:

  • send the Token only in Authorization: Bearer;
  • reject Tokens issued for another Resource or Audience;
  • never accept or transit a downstream Token as MCP credentials;
  • bind Client credentials to the Issuer that created them;
  • challenge only for the Scopes needed by the current operation;
  • return 401 for a missing, invalid or expired Token and 403 for insufficient permission;
  • perform Tenant, object, Argument, Purpose and side-effect authorization after Token validation.

Authorization is optional at the protocol level because local stdio and private workload topologies differ. A network deployment still needs an explicit identity and authorization boundary. If it does not use the MCP OAuth profile, document the alternative, its Principal mapping, rotation, revocation and interoperability cost.

OAuth metadata is untrusted network input. Validate HTTPS destinations, redirects and resolved addresses; block private, loopback, link-local and cloud metadata ranges unless a tightly scoped development policy allows them. Pin trusted Issuers and use controlled egress to resist metadata-driven SSRF.

The enterprise OAuth guide covers PKCE, JWKS rotation, multi-IdP Claims and delegated downstream access in depth.

Design Tools Around Effects

A production MCP Tool should expose one understandable capability with a narrow authority boundary.

Prefer:

text
reports.read(reportId)
reports.preparePublication(reportId)
reports.publish(reportId, approvalId, idempotencyKey)

Avoid:

text
api.request(method, url, headers, body)
database.execute(sql)
shell.run(command)

Generic proxies collapse destination, ownership and effect policy into model-generated strings. They amplify SSRF, injection, data egress and audit ambiguity.

Separate read, propose and execute steps for consequential work. The proposal should show the exact object, destination, amount, recipients and irreversible effects. Approval must bind to those salient parameters, the Principal, Tool Descriptor Hash, policy revision and a short expiry. If any material field changes, require a new decision.

readOnlyHint, destructiveHint, idempotentHint and openWorldHint are Annotations, not evidence. A buggy or malicious Server can mislabel behavior. The Policy Engine must classify actual code paths and backend effects.

Make Retry Semantics Explicit

Transport success and business success are different facts. Once a write reaches a backend, losing the response does not prove that the effect failed or rolled back.

Observation Meaning Default action
rejected before dispatch no backend effect started fix or retry when policy permits
read timed out result unknown, no intended mutation bounded retry within deadline
write rejected by idempotency store duplicate already known return the recorded outcome
write timed out after dispatch Unknown Effect query operation status; do not blindly retry
cancellation received stop requested propagate cancellation; verify downstream effect
connection lost transport ended do not infer rollback

For a consequential Tool:

  1. require a caller-supplied or Server-issued idempotency key;
  2. atomically bind it to Principal, Tenant, Tool and salient Argument Digest;
  3. store pending, succeeded, failed or unknown_effect;
  4. return the original outcome for an exact duplicate;
  5. reject key reuse with different Arguments;
  6. provide a status lookup when the backend can complete asynchronously.

Do not retry merely because an Annotation says idempotentHint: true. Verify the backend contract, retention window and duplicate behavior. Retry budgets must fit inside the total Request Deadline so layers do not multiply attempts.

Bound Results and Treat Them as Data

Large Results consume memory, network, Context Window and disclosure budget. Use the smallest representation that preserves the user's task:

  1. bounded structuredContent for machine-validated fields;
  2. a summary plus an opaque Cursor for additional pages;
  3. an authorized MCP Resource or Resource Link for larger artifacts;
  4. a short-lived application download for binary data when the product owns that channel.

Every Page, Resource Read or download must recheck Principal, Tenant, object and expiry. A Cursor is an object reference, not authorization. Bind it to query semantics and policy revision; reject tampering and cross-tenant reuse.

Tool and Resource Content can contain Prompt Injection, malicious URLs, stale data, Secrets, executable code or misleading instructions. Validate shape and MIME, cap nesting and decompression, attach provenance, remove unnecessary fields and label external text as data. Never allow a Result to authorize a later Tool Call.

Every successful 2026-07-28 Result declares resultType: "complete" or resultType: "input_required". The latter is a suspended protocol step, not a completed business operation; persist no success outcome until the Client retries the original Method with the requested inputs. For compatibility, a Result from an earlier revision with no resultType is interpreted as complete.

Base64 changes representation, not confidentiality or size economics. Do not use it to justify embedding an unbounded file inside JSON-RPC.

Cache and Subscribe Without Crossing Tenants

MCP 2026-07-28 List Results and Resource Read Results can include ttlMs and cacheScope. These are cache hints, not permission.

For cacheScope: "private", include at least:

text
configured Server identity
protocol revision
Principal and Tenant
Authorization Context
policy revision
Method and normalized Arguments
Resource or Tool Descriptor Hash

Never downgrade a Private entry to Public because two payloads happen to match. Cap TTL by credential, consent, object and policy expiry. Invalidate on permission or Descriptor changes.

subscriptions/listen delivers selected list and Resource change Notifications. A Notification means “cached data may be stale”; it does not grant the newly advertised capability. Re-run discovery validation, Descriptor review and authorization before use. A disconnected stream creates a new Subscription; Last-Event-ID replay is not available in this revision.

Apply Backpressure, Deadlines and Cancellation

An Agent loop can generate bursts even when each user action looks small. Protect scarce dependencies with nested budgets:

  • per Principal, Tenant, Server and Tool rate limits;
  • per Tool concurrency and queue limits;
  • request-body, response-body and in-flight byte limits;
  • one total Deadline with smaller downstream timeouts;
  • retry and model-loop budgets;
  • circuit breakers and bulkheads per dependency;
  • egress allowlists and DNS controls;
  • graceful shutdown that stops admission before draining work.

Do not let one slow Tool consume the global worker pool. Return a stable overload or timeout outcome that distinguishes “not started” from “effect unknown.” Propagate cancellation to database queries, HTTP calls and workers, then observe whether the downstream system actually stopped.

For long-running business work, do not keep a request-scoped SSE stream open indefinitely. Use an explicit Job or Task extension only when the Client supports that extension, and keep ownership, status and cancellation in application state.

Observe Decisions, Not Secrets

Useful telemetry reconstructs the control path without copying sensitive content. Record:

  • Request and Trace ID;
  • Protocol Revision, Method and Name;
  • Client and Server Build;
  • hashed or tokenized Principal and Tenant;
  • Descriptor, Argument and Policy Revision Hashes;
  • Authorization Decision and reason code;
  • deadline, queue time, execution time, bytes and rows;
  • idempotency state, retry count and Effect Status;
  • Result Type, cancellation and error class.

Do not record Bearer Tokens, Refresh Tokens, raw Secrets, unrestricted Prompt text, full private Resources or complete sensitive Arguments by default. Apply field-level redaction before export, not only in the log viewer. Restrict access and retention independently from application data.

Protocol Logging is Deprecated in 2026-07-28; new systems should use ordinary structured telemetry such as OpenTelemetry instead of treating notifications/message as the production audit system.

Isolate Legacy Compatibility

Legacy support is a separate trust boundary. Versions 2025-03-26 through 2025-11-25 used initialize, optional Mcp-Session-Id, a standalone GET stream and Server-initiated Requests. HTTP+SSE 2024-11-05 used separate SSE and message endpoints.

Use explicit versioned routes, adapters or deployments:

text
/mcp            -> 2026-07-28 Stateless Streamable HTTP
/legacy/mcp     -> earlier Streamable HTTP adapter
/legacy/sse     -> 2024-11-05 HTTP+SSE

Do not let a Request Header select a weaker authentication or authorization policy. Keep Legacy traffic out of modern caches and Subscriptions. A Client probing compatibility must inspect structured JSON-RPC errors before falling back; an HTTP 400, 404 or 405 alone does not identify a Legacy Server.

Measure usage per revision, publish a retirement date, test downgrade resistance and remove compatibility only after supported Clients migrate.

Test the Failure Matrix

Happy-path Tool Calls prove little. Production tests should cover:

Layer Required negative tests
Transport invalid Origin, oversized body, malformed JSON, unsupported Method, HeaderMismatch, disconnected SSE
Version unsupported revision, missing required Metadata, accidental Legacy fallback
OAuth invalid Issuer, Resource, Audience, PKCE, expired Token, insufficient Scope, metadata SSRF
Authorization cross-Tenant object, changed Approval parameters, stale policy, revoked Principal
Tool unknown fields, boundary values, false Annotation, forbidden egress, injected description
State guessed, expired, replayed, cross-user and cross-operation Handle
Reliability overload, dependency timeout, cancellation, duplicate write, Unknown Effect
Result oversized, invalid outputSchema, poisoned text, unsafe MIME, decompression bomb
Cache Private cross-user collision, TTL beyond credential expiry, stale Descriptor

Run compatibility tests against each supported protocol revision and pinned SDK Build. Conformance does not replace business tests: a perfectly formed tools/call can still violate ownership or duplicate a payment.

Production Checklist

  • [ ] The supported MCP revisions and SDK Builds are pinned.
  • [ ] Modern Streamable HTTP uses independent POST requests with required Metadata.
  • [ ] Header and body values are cross-checked before routing.
  • [ ] Origin, size, concurrency, deadline and egress controls run before Tool execution.
  • [ ] Identity maps to a Principal and Tenant; Tokens are audience-bound and never passed through.
  • [ ] Every Tool, Resource, Prompt and object has application authorization.
  • [ ] Business continuity uses explicit, expiring and revocable State Handles.
  • [ ] Read, propose and execute capabilities are separated where consequences differ.
  • [ ] Approval binds to exact parameters, Descriptor and policy revision.
  • [ ] Writes have a real idempotency and status-query contract.
  • [ ] Unknown Effect is represented explicitly.
  • [ ] Results, Pages, Cursors and Resources are bounded and reauthorized.
  • [ ] Private caches include the complete Authorization Context.
  • [ ] Cancellation reaches downstream work and its outcome is observed.
  • [ ] Logs record decisions and effects without Tokens or unrestricted private content.
  • [ ] Legacy protocols run on isolated routes with downgrade tests and retirement telemetry.

Frequently Asked Questions

Does MCP 2026-07-28 require a session store?

No. The core has no Protocol Session. An application may still store workflow state, but it should return an explicit opaque Handle and authorize that object on every use. Hidden Connection State is neither portable nor an identity boundary.

Should a production MCP Server always return SSE?

No. Return application/json for a simple Result. Use request-scoped text/event-stream only when related progress or logging Notifications must precede the final Result. Use subscriptions/listen for selected long-lived change Notifications.

Is a valid OAuth Token enough to call a Tool?

No. It proves only the validated credential contract. The Server still checks Tenant, object, Tool, Arguments, Purpose and effect. Scope is a coarse gate, and Token Passthrough is forbidden.

When is retry safe?

Retry when the operation did not start, is read-only, or has a verified backend idempotency contract. After an ambiguous write timeout, query status by idempotency key or return Unknown Effect.

How should a Server return a large file?

Return a bounded summary and a separately authorized Resource or download reference. Reauthorize every read, cap bytes and lifetime, and treat the file as untrusted data.

Conclusion

MCP production engineering begins by removing hidden trust from the transport. A 2026-07-28 Request carries enough protocol information to reach any compatible instance, but the Server must still establish identity, authorize exact effects, control business state, bound execution and preserve evidence.

The durable design is explicit: explicit revision, explicit Principal, explicit State Handle, explicit Approval, explicit Idempotency and explicit Effect Status. That makes horizontal scaling easier and failure investigation possible without pretending that a Connection or Schema is a security boundary.

Primary Sources