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
Originheader on every connection and returns 403 if invalid (DNS-rebinding defense). - When running locally, bind to
127.0.0.1, not0.0.0.0. - Session IDs are carried in the
MCP-Session-Idheader and must be cryptographically secure (e.g. a securely generated UUID). - HTTP clients send
MCP-Protocol-Versionon every request.
// 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.
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.
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 (OWASP LLM06 "Excessive Agency"): have destructive operations return a "proposal" rather than "execute."
// ツール = 任意コード実行。破壊的操作は「提案」を返し、その場では実行しない。
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 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.
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-Authenticateon 401 or/.well-known/oauth-protected-resource. - RFC 8707 (Resource Indicators) — the client must send the server's canonical URI in
resourceon 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):
// リモート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). 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
Originon Streamable HTTP and 403 invalid ones. Bind locally to127.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
audand 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
audand 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. "Connecting fast" and "operating safely" are compatible when you design correctly.