Building a production MCP server in TypeScript
Upgrade an MCP demo with runtime schemas, narrow tools, transport-safe logging, OAuth audience validation, timeouts, idempotency, and audit.
An MCP server turns a model's requested capability into real code execution or data access. A tool description is not a permission boundary. Treat every production tool as an external API with a runtime input schema, caller identity, authorization, resource limits, side-effect contract, audit record, and error model. If a model can construct an argument, that argument is untrusted. As of 2026-07-29, the official MCP TypeScript tutorial builds servers with McpServer , the SDK, and transports, and explicitly warns that stdout is the JSON-RPC channel for stdio servers— console.log corrupts it. The current MCP Authorization specification requires an HTTP resource server to validate token audience/resource and forbids passing the client token unchanged to a downstream API. Implementation steps Inventory resources, tools, and prompts. Resources expose read-only context; tools may compute or mutate; prompts are templates. Give every tool a stable, specific name and validate inputs using Zod or JSON Schema. Return only necessary fields. Separate search_orders from refund_order instead of hiding a sensitive action inside manage_order(action) . server.registerTool( "get_order", { description: "Read one order visible to the authenticated caller", inputSchema: { orderId: z.string().uuid() }, }, async ({ orderId }, context) => { const actor = requireActor(context) const order = await orders.findVisible(actor.id, orderId) if (!order) return toolError("not_found") return { content: [{ type: "text", text: JSON.stringify(redact(order)) }] } }, ) Choose transport from the deployment boundary. A local, single-user integration can use stdio with credentials from process environment or an OS secret store. Stdout carries only protocol frames and logs go to stderr. A remote multi-user server uses the officially supported HTTP transport with HTTPS, request limits, timeouts, and authentication at the edge. For HTTP authorization, implement protected resource metadata and the OAuth 2.1 flow described by MCP. Validate issuer, signature, expiry, audience/resource, and scopes. Never forward an inbound client token to a third-party API. If a downstream service is required, use the MCP server's own OAuth client or credential and map caller authorization to a separate downstream grant. Split scopes by capability, such as orders:read and orders:refund . Each handler still performs resource-level authorization; visibility of a tool does not grant permission. A multi-tenant query derives tenant and user identity on the server and never accepts an owner ID supplied by the model. Constrain outbound access with a host allowlist, DNS/IP and redirect checks, timeout, response-size limit, and content-type checks to mitigate SSRF into metadata or internal networks. Do not expose an arbitrary URL fetch tool. File tools resolve real paths inside allowed roots and reject traversal, symlink escape, and device files. Use idempotency keys, dry-run or preview output, and explicit approval metadata for side effects. Persist actor, tool, normalized argument digest, authorization decision, and result ID while redacting secrets and personal data. Never log Authorization or tokens; use console.error for stdio diagnostics. Before deployment, pin SDK and Node versions, compile, snapshot tool schemas, and run unit, integration, and contract tests. Validate configuration at startup and fail closed without credentials. A health endpoint does not call tools. Readiness may check dependencies without leaking network topology. Failure and recovery For a stdio JSON parse error, first find banners or logs on stdout and move them to stderr. Capture a protocol trace. Do not loosen the parser to ignore arbitrary text, which hides framing corruption. For remote 401, compare protected resource metadata, issuer, audience/resource, expiry, and clock. For 403, inspect scopes and resource authorization. Never disable audience validation or accept a token intended for another API to make a test pass. After a downstream timeout, a read-only tool may retry within a limit. A mutation first checks idempotency and provider state. Return a structured error and retryable marker without exposing provider stacks or tokens to the model. If prompt injection causes an unauthorized mutation, disable the capability, revoke its credential, preserve audit evidence, reconcile side effects, and roll back through the source system. Repair handler authorization, schema and allowlists, and approval gates. Editing the tool description alone is not remediation. Verification commands npm ci npm run build npm test node build/index.js 2>mcp-server.log Contract tests cover tool listing, invalid schemas, unknown tools, missing or expired tokens, wrong audience, missing scopes, cross-tenant identifiers, path traversal, SSRF redirects, timeouts, oversized responses, duplicate mutations, and log redaction. Run list and call smoke tests with a real MCP client; a stdio test should assert every stdout frame is valid JSON-RPC. Primary sources MCP: Build an MCP server MCP Authorization specification MCP Security Best Practices Internal links Browse technical articles for coding-agent permissions and Edge Functions. Build a TypeScript MCP contract test through the course catalog . Share a redacted trace through the contact page for an MCP security issue.