# MCP (Model Context Protocol) Production Server Guide 2026 | Architecture, Transports, Authorization, Security

> A practical guide to building production-grade Model Context Protocol (MCP) servers. Covers the host/client/server architecture, stdio/Streamable HTTP transports, the correct official TypeScript SDK (v1) API, OAuth 2.1 authorization (RFC 9728/8707, PKCE, token audience validation), tool safety design, and the official security best practices — with primary-source-faithful TypeScript code.

- Published: 2026-07-23
- Author: 友田 陽大
- Tags: MCP, Model Context Protocol, 生成AI, LLM, AIエージェント, TypeScript, セキュリティ, OAuth, アーキテクチャ設計, 技術選定
- URL: https://tomodahinata.com/en/blog/mcp-model-context-protocol-production-server-guide
- Category: 本番LLMアプリ実装（信頼できるAI）
- Pillar guide: https://tomodahinata.com/en/blog/production-llm-application-engineering-guide

## Key points

- MCP is an open standard (JSON-RPC 2.0) that connects AI models to external tools, data, and prompts. It has a host/client/server structure, and servers expose Tools/Resources/Prompts.
- There are two transports: stdio (local, subprocess) and Streamable HTTP (remote). The old HTTP+SSE transport has been deprecated and replaced.
- For production, use the stable SDK @modelcontextprotocol/sdk (v1), passing a raw Zod shape to server.registerTool. @modelcontextprotocol/server (v2) is beta with a different API.
- A remote MCP server is an OAuth 2.1 resource server. It must validate the aud (audience) of received tokens and accept only those meant for itself — and must NOT pass the token through downstream. Token passthrough is explicitly forbidden by the spec.
- Tools are 'arbitrary code execution.' Contain the blast radius of prompt injection with least privilege, propose/execute separation for destructive actions, human approval, and Origin validation.

---

The key to turning generative AI from a "smart chat" into a "system that actually does work" is connecting the model **safely to external tools, data, and business logic**. **MCP (Model Context Protocol)** standardizes that connection — a "common port" between AI models and tools. Instead of building bespoke integrations per tool, you connect through one protocol. That's why, from 2025 onward, the major AI clients and platforms adopted it all at once.

But **the decisive factor when putting an MCP server into production is not the novelty of the protocol.** It's understanding the architecture, choosing the transport, OAuth 2.1 authorization, and above all a security design that starts from the premise that **a tool is arbitrary code execution**. This article adheres strictly to the **current stable spec (2025-11-25) and the official TypeScript SDK (v1)**, and shows only what works in production, in real code.

> Every version, API, and security requirement here is grounded in primary sources verified on `modelcontextprotocol.io`. Because the spec updates by date-based revisions, always confirm the sources before implementing.

## The big picture: host / client / server and the three primitives

MCP is a stateful connection protocol built on **JSON-RPC 2.0**. There are three actors.

| Role | Responsibility (summary of the official definitions) |
|---|---|
| **Host** | The container and coordinator. Creates and manages multiple clients, controls connection permissions and lifecycle, **enforces security policy and consent (user authorization)**, and handles LLM integration and context aggregation |
| **Client** | Created by the Host, maintaining a **1:1 stateful session with a single server**. Handles protocol negotiation and bidirectional message routing |
| **Server** | Provides specialized capabilities. Exposes **Tools / Resources / Prompts** and operates independently. Must respect security constraints |

The three **primitives** a server exposes (the center of implementation):

- **Tools** — "functions the AI model executes." The entry point for side-effecting operations. **Requires the most care.**
- **Resources** — "context and data for the user or the AI to use." Files, DB records, etc.
- **Prompts** — "templated messages / workflows for users."

There are also client-side features a server can request: **Sampling (server-initiated LLM calls), Roots (URI/filesystem boundaries to operate within), and Elicitation (requests to the user for more input)**. Which features are available is decided by **capability negotiation at initialization**, where both parties explicitly declare what they support.

## Choosing a transport: stdio or Streamable HTTP

MCP has **exactly two** standard transports. Choose by use case.

| Transport | When to use | Key points |
|---|---|---|
| **stdio** | **Local**. The client launches the server as a subprocess | JSON-RPC over `stdin`/`stdout`, newline-delimited (**messages must not contain embedded newlines**). `stderr` is for logs only. Clients should prefer stdio whenever possible |
| **Streamable HTTP** | **Remote**. An independent process handles multiple connections over HTTP | A single POST/GET endpoint, optionally streaming via SSE. **Authentication and Origin validation are effectively mandatory** |

> ⚠️ The old **HTTP+SSE** transport (2024-11-05) is **deprecated** and replaced by Streamable HTTP. There's no reason to choose it for new implementations.

**Streamable HTTP security is not "optional"** (official MUST/SHOULD):

- The server **validates the `Origin` header** on every connection and returns **403** if invalid (**DNS-rebinding defense**).
- When running locally, **bind to `127.0.0.1`, not `0.0.0.0`**.
- Session IDs are carried in the **`MCP-Session-Id` header** and must be **cryptographically secure** (e.g. a securely generated UUID).
- HTTP clients send **`MCP-Protocol-Version`** on every request.

```ts
// Streamable HTTP: DNS リバインディング対策として Origin を必ず検証する（公式MUST）
const ALLOWED_ORIGINS = new Set(["https://app.example.com"]);

function assertTrustedOrigin(req: Request): void {
  const origin = req.headers.get("origin");
  // Origin が付く（＝ブラウザ由来）のに許可リストに無ければ拒否
  if (origin && !ALLOWED_ORIGINS.has(origin)) {
    throw new HttpError(403, "forbidden origin");
  }
}
// セッションIDは推測不能に。認証をセッションに依存させない（後述の session hijacking 対策）。
const sessionId = crypto.randomUUID();
```

## A minimal production MCP server (official SDK v1, TypeScript)

First, the smallest working form. **For production, use the stable `@modelcontextprotocol/sdk` (v1)** (the `@modelcontextprotocol/server` v2 is beta with a different API — premature for production). Register tools with **`server.registerTool`**, and pass a **raw Zod shape** (an object not wrapped in `z.object(...)`) as `inputSchema` — that's the v1 convention.

```ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "acme-tools", version: "1.0.0" });

// inputSchema は「生の Zod シェイプ」。境界で入力を型と値域で締める。
server.registerTool(
  "search-orders",
  {
    title: "注文検索",
    description: "テナント内の注文を検索する（読み取り専用）",
    inputSchema: { tenantId: z.string().uuid(), query: z.string().min(1).max(200) },
  },
  async ({ tenantId, query }) => {
    // 認可は「MCPの外」の決定的コードで強制する。プロンプトに委ねない。
    const orders = await searchOrders(tenantId, query);
    return { content: [{ type: "text", text: JSON.stringify(orders) }] };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

`registerTool(name, config, handler)`, the raw-shape `inputSchema`, the `{ content: [{ type: "text", text }] }` return, and `server.connect(transport)` are all the current API verified against the v1.29.0 official docs. If you need structured output, combine `outputSchema` with `structuredContent`. The philosophy of validating LLM output is the same as [designing reliable structured output](/blog/structured-output-reliability-constrained-decoding-semantic-validation).

## Designing tools "safely": arbitrary code execution as the premise

The most misunderstood part of MCP is Tools. The spec states it plainly — **"Tools represent arbitrary code execution and must be treated with appropriate caution."** Opening an MCP tool means **letting the LLM (and anyone who can steer it) call functions in your system**.

So the design principle is entirely continuous with [LLM app security](/blog/llm-application-security-owasp-top-10-prompt-injection-guide) (OWASP LLM06 "Excessive Agency"): **have destructive operations return a "proposal" rather than "execute."**

```ts
// ツール = 任意コード実行。破壊的操作は「提案」を返し、その場では実行しない。
server.registerTool(
  "propose-refund",
  {
    title: "返金の提案",
    description: "返金の提案レコードを作成する（実行はしない・人間承認が必要）",
    inputSchema: { orderId: z.string().uuid(), amountJpy: z.number().int().positive() },
  },
  async ({ orderId, amountJpy }) => {
    // 副作用は「提案の作成」のみ（最小権限）。実際の返金は承認済みのコード経路だけ。
    const proposalId = await createRefundProposal({ orderId, amountJpy });
    return {
      content: [{ type: "text", text: `提案を作成しました: ${proposalId}（承認待ち）` }],
    };
  },
);
```

The actual refund happens outside MCP, through **deterministic business-rule validation + human approval + an idempotency key**. This "propose/execute separation + idempotency" is exactly the [reliability and idempotency design](/blog/dynamodb-single-table-design-reliability-idempotency-patterns) I've used to keep production double-charges at zero on a payments platform. The spec also notes that **"tool descriptions/annotations should be considered untrusted unless obtained from a trusted server"** — meaning tool metadata itself can be an attack surface. For arranging and permissioning tools, see [designing AI-agent tool execution](/blog/ai-agent-tool-use-function-calling-production-design).

## Remote MCP authorization: OAuth 2.1 (token passthrough is forbidden)

Authorization is an **optional feature limited to HTTP transports** (stdio retrieves credentials from the environment, so it typically doesn't use authorization). If you expose a remote MCP server, follow the official authorization model strictly.

- **The MCP server is the OAuth 2.1 "resource server"**, the MCP client is the OAuth client, and a separate authorization server (AS) exists.
- **RFC 9728 (Protected Resource Metadata)** must be implemented by the server. Advertise the AS via `WWW-Authenticate` on 401 or `/.well-known/oauth-protected-resource`.
- **RFC 8707 (Resource Indicators)** — the client must send the server's **canonical URI** in `resource` on both the authorization and token requests.
- **PKCE (S256) is mandatory.** If AS metadata doesn't confirm support, don't proceed.
- **Token validation is non-negotiable** (official MUST):

```ts
// リモートMCP = OAuth 2.1 の resource server。
// 受け取ったトークンは「自分向け(aud)」だけ受理する。
function verifyAccessToken(rawToken: string): Claims {
  const claims = verifyJwt(rawToken, JWKS); // 署名・iss・exp を検証
  // aud に自分の正規URIが無いトークンは拒否（他サービス向けの流用を防ぐ）
  if (!claims.aud?.includes(MCP_SERVER_CANONICAL_URI)) {
    throw new HttpError(401, "token audience mismatch");
  }
  return claims;
  // ★ このトークンを下流の外部APIへ「素通し」してはならない（公式で明確に禁止）。
  //   下流呼び出しは、MCPサーバー自身の資格情報／別トークンで行う。
}
```

**Token passthrough — forwarding a received token straight downstream — is an anti-pattern explicitly forbidden by the authorization spec.** It breaks the audit trail, crosses trust boundaries, and lets security controls be bypassed.

## Production security design: threats and mitigations from the official best practices

The representative threats named by the official Security Best Practices, and the mitigation essentials.

| Threat | What happens | Mitigation essentials |
|---|---|---|
| **Confused Deputy** | A proxy with a static client_id + dynamic registration + a consent cookie lets an attacker reuse the cookie to skip consent and steal the auth code | **The proxy must implement per-client consent.** Check the approved client_id each time; exact `redirect_uri` match |
| **Token Passthrough** | Accepting tokens not meant for you and forwarding them downstream | **Don't accept tokens not issued for you.** Explicitly forbidden by the spec |
| **Session Hijacking** | Guessing/impersonating session IDs | **Don't use sessions for authentication.** Non-guessable session IDs (secure UUIDs). Verify every inbound request. Bind IDs to the user |
| **SSRF** | Discovery fields carry internal URLs (e.g. cloud metadata `169.254.169.254`) | Enforce HTTPS, block private IP ranges, validate redirect targets (beware encoding-bypass in hand-rolled IP checks) |
| **Local server launch** | A malicious startup command executes arbitrary code | Clients must **show the exact launch command and require explicit consent**; sandbox |
| **Excessive scope** | Overly broad scopes like `admin:*` enlarge the blast radius | **Least-privilege scopes, applied progressively.** Avoid wildcard/omnibus scopes |

These aren't exotic — they amount to **"defending your MCP server as an ordinary web API exposed to untrusted clients (and the LLMs behind them)."**

## Observability, resilience, idempotency: three production essentials

Because MCP tools call external systems, **observability** is indispensable. Record tool-call names, arguments (mask PII), latency, success/failure, and refusals per user/session, and detect anomalies (the foundation is [production observability with OpenTelemetry](/blog/opentelemetry-observability-production-tracing-metrics-logs)). Add **timeouts and retries (exponential backoff)** to external calls, and **idempotency keys** to side-effecting operations. An LLM may call a tool multiple times with the same intent, and **non-idempotent execution leads straight to double-charges and double-shipments.**

## Versioning and future-proofing

MCP versions are **date-based** (the date of the last backwards-incompatible change). **The current stable version is 2025-11-25**; `2026-07-28` is a release candidate (RC), not final. The SDK is also in transition — the stable `@modelcontextprotocol/sdk` (v1) coexists with the API-different beta `@modelcontextprotocol/server` (v2). **Pin production to v1**, absorb version differences via `MCP-Protocol-Version` negotiation, and prepare for breaking changes.

## Adoption checklist (before going live)

- [ ] **Transport** — stdio locally, Streamable HTTP remotely (don't use the old HTTP+SSE).
- [ ] **Origin validation** — validate `Origin` on Streamable HTTP and 403 invalid ones. Bind locally to `127.0.0.1`.
- [ ] **Sessions** — non-guessable session IDs. **Don't rely on sessions for authentication.** Verify every inbound request.
- [ ] **Authorization (remote)** — OAuth 2.1, RFC 9728/8707, PKCE (S256). **Validate the token `aud`** and accept only tokens meant for you.
- [ ] **No token passthrough** — never forward a received token downstream; use separate credentials downstream.
- [ ] **Least-privilege tools** — keep destructive operations as "proposals"; execute via a human-approved, idempotency-keyed code path.
- [ ] **Input validation** — constrain type/range with `inputSchema` (Zod), and re-check authorization in deterministic code.
- [ ] **Observability** — logs/traces for tool calls, timeouts, retries, anomaly detection.
- [ ] **Pin the SDK/spec** — v1 stable SDK, declare the spec version, track updates via primary sources.

## Summary

MCP is a powerful standard for connecting AI to business systems. But what separates production success from failure isn't the protocol's novelty — it's whether you can **start from the premise that "a tool is arbitrary code execution" and "an LLM is a probabilistic, untrusted component," and surround it with deterministic architecture.**

- **Architecture**: understand host/client/server, the three primitives, and capability negotiation.
- **Transport**: choose stdio (local) / Streamable HTTP (remote) by use case, and protect Origin and sessions.
- **Authorization**: as an OAuth 2.1 resource server, **validate `aud` and forbid token passthrough**.
- **Tool safety**: contain the blast radius with least privilege, propose/execute separation, human approval, and idempotency.

MCP server security is part of [LLM app security](/blog/llm-application-security-owasp-top-10-prompt-injection-guide). "Connecting fast" and "operating safely" are compatible when you design correctly.
