Production MCP Servers with TypeScript: Auth & Security
ARTIFICIAL INTELLIGENCEMCPModel Context Protocol

Production MCP Servers with TypeScript: Auth & Security

calendar_todayAUG 1, 2026
schedule11 MIN READ
boltADVANCED LEVEL

Building Production-Ready MCP Servers with TypeScript: Security & Best Practices

As Large Language Models (LLMs) transition from conversational interfaces to autonomous software agents, a fundamental engineering challenge emerges: how can AI models safely interact with enterprise databases, internal APIs, developer toolchains, and file systems without exposing sensitive infrastructure?

Anthropic's Model Context Protocol (MCP) provides the open-source architectural standard for connecting AI hosts (such as Claude Desktop, Cursor, and custom agent orchestrators) to external data sources and tools.

While simple MCP tutorials demonstrate local stdio connections, running MCP servers in production environments requires rigorous input validation, transport security, authentication middleware, rate limiting, and robust error isolation.

In this guide, we will build a production-grade MCP server using TypeScript and @modelcontextprotocol/sdk, incorporating battle-tested security patterns, Anthropic Agent SDK integration blueprints, and token-based authentication.

[!IMPORTANT] Key Architectural Takeaway for Production MCP Security: Model Context Protocol (MCP) authentication and security architecture for Anthropic Agent SDK relies on three critical defense perimeters:

  1. Transport-Layer Security (TLS + Bearer/OAuth2 Token): Authenticating Server-Sent Events (SSE) connections before the agent handshake.
  2. Zod Strict Schema Validation: Enforcing boundary checks on all LLM tool arguments to eliminate SQL injection, prompt injection, and command injection.
  3. Path Traversal & Sandboxed Tool Execution: Strict file path de-escalation preventing agents from escaping working directories.

MCP Security & Authentication Architecture Matrix

Dimension Stdio Transport Remote SSE Transport HTTP Stream Transport
Authentication Pattern OS Process Credentials Bearer Token / JWT / mTLS API Key Header / OAuth 2.0
Security Perimeter Local Sandbox / UID isolation Network Firewall + CORS Reverse Proxy / API Gateway
Best Used For Claude Desktop, Local IDEs Cloud AI Agents, Microservices Distributed Multi-Agent Systems
Risk Factor Low (Privilege bound to user) High (Requires strict token guard) Medium (Standard web security)

1. Architectural Foundations of Model Context Protocol (MCP)

MCP follows a client-server architecture designed to isolate LLM execution from underlying system resources:

  • Host (AI Host): The runtime environment executing the LLM (e.g., Claude Desktop, Cursor IDE, LangChain agent).
  • Client (MCP Client): The client component inside the Host that negotiates protocol capabilities, manages transports, and dispatches tool calls.
  • Server (MCP Server): An independent service exposing explicit Resources, Tools, and Prompts to the client.
graph LR
    Host[AI Host / Claude / Cursor] <--> Client[MCP Client]
    Client <-->|Stdio / SSE / HTTP| Server[Production MCP Server]
    Server <--> Guard[Input Validation & Zod Schema]
    Guard <--> DB[(Production Database)]
    Guard <--> ExternalAPI[External APIs]

Core Primitives Exposed by MCP

  1. Tools: Executable functions that allow the LLM to perform actions (e.g., executing SQL queries, scanning security logs, triggering webhooks).
  2. Resources: Read-only data endpoints (e.g., configuration files, system metrics, database records) exposed via URI templates (resource://...).
  3. Prompts: Parameterized prompt templates that standardise complex agent interactions.

2. Setting Up the Production TypeScript Project

To build an enterprise-ready MCP server, begin by initializing a TypeScript project with strict type safety and schema validation dependencies:

mkdir mcp-production-server
cd mcp-production-server
npm init -y
npm install @modelcontextprotocol/sdk zod dotenv express cors helmet
npm install -D typescript @types/node @types/express tsx

Configure your tsconfig.json for strict type checking:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

3. Implementing a Production MCP Server

Let's implement a secure MCP server that exposes database querying and threat forensic scanning capabilities.

Step 1: Initialize the Server Instance

Create src/server.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const server = new Server(
  {
    name: "production-security-mcp",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

Step 2: Define Tool Schemas with Strict Zod Validation

Input validation is the first line of defense against Prompt Injection and Data Tampering. Define strict Zod schemas for all tool parameters:

const AuditLogQuerySchema = z.object({
  environment: z.enum(["production", "staging", "development"]),
  limit: z.number().int().min(1).max(100).default(20),
  severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
  searchTerm: z.string().max(200).optional(),
});

type AuditLogQuery = z.infer<typeof AuditLogQuerySchema>;

Step 3: Register Tools and Declare Capabilities

Expose available tools to the LLM during capability negotiation:

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "query_audit_logs",
        description: "Retrieve filtered security audit logs from the enterprise forensic telemetry store.",
        inputSchema: {
          type: "object",
          properties: {
            environment: {
              type: "string",
              enum: ["production", "staging", "development"],
              description: "Target environment to inspect",
            },
            limit: {
              type: "number",
              description: "Maximum number of records to return (1-100)",
            },
            severity: {
              type: "string",
              enum: ["LOW", "MEDIUM", "HIGH", "CRITICAL"],
              description: "Filter by log severity level",
            },
            searchTerm: {
              type: "string",
              description: "Optional search query string (max 200 chars)",
            },
          },
          required: ["environment"],
        },
      },
    ],
  };
});

Step 4: Secure Tool Execution Handler

Implement the tool execution handler with error boundaries and data sanitization:

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "query_audit_logs") {
    // Validate arguments against Zod schema
    const parseResult = AuditLogQuerySchema.safeParse(args);
    if (!parseResult.success) {
      throw new McpError(
        ErrorCode.InvalidParams,
        `Invalid tool parameters: ${parseResult.error.message}`
      );
    }

    const { environment, limit, severity, searchTerm } = parseResult.data;

    try {
      // Execute sanitized query against infrastructure
      const logs = await fetchSecurityAuditLogs({
        environment,
        limit,
        severity,
        searchTerm,
      });

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(logs, null, 2),
          },
        ],
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "Unknown database error";
      return {
        content: [
          {
            type: "text",
            text: `Execution failed: ${errorMessage}`,
          },
        ],
        isError: true,
      };
    }
  }

  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});

4. Security Hardening for Production MCP Deployments

Running MCP servers in production introduces unique attack vectors. Implement these essential security layers:

A. Input Sanitization & Path Traversal Defense

Never pass raw strings from LLM tool arguments to file paths or shell commands:

import path from "path";

export function sanitizeFilePath(baseDir: string, userPath: string): string {
  const safePath = path.normalize(userPath).replace(/^(\.\.[\/\\])+/, "");
  const resolvedPath = path.resolve(baseDir, safePath);
  
  if (!resolvedPath.startsWith(baseDir)) {
    throw new Error("Security Alert: Path traversal attempt detected.");
  }
  return resolvedPath;
}

B. Transport Layer Security: Stdio vs SSE

  • Stdio Transport: Ideal for local execution (Claude Desktop, Cursor). Operating system process isolation provides native security boundaries.
  • Server-Sent Events (SSE) Transport: Required for remote cloud deployments. Must be secured with TLS (HTTPS), CORS restrictions, and Bearer Token / OAuth2 authentication headers.

C. Authentication Middleware for Remote SSE Transport

When exposing an MCP server over HTTP/SSE, wrap the transport in an Express authentication middleware:

import express from "express";
import helmet from "helmet";
import cors from "cors";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const app = express();
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") || ["https://gucluyumhe.dev"] }));

// Authentication Guard Middleware
app.use("/sse", (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    res.status(401).json({ error: "Unauthorized: Missing or invalid Bearer token" });
    return;
  }
  
  const token = authHeader.substring(7);
  if (token !== process.env.MCP_SECRET_KEY) {
    res.status(403).json({ error: "Forbidden: Invalid MCP secret key" });
    return;
  }
  
  next();
});

let sseTransport: SSEServerTransport;

app.get("/sse", async (req, res) => {
  sseTransport = new SSEServerTransport("/messages", res);
  await server.connect(sseTransport);
});

app.post("/messages", async (req, res) => {
  await sseTransport.handlePostMessage(req, res);
});

app.listen(3001, () => {
  console.log("Production MCP Server running on port 3001 with SSE transport");
});

5. Anthropic Agent SDK & Model Context Protocol Security Design Pattern

When orchestrating autonomous agents using the Anthropic Agent SDK (@anthropic-ai/sdk) alongside MCP in TypeScript, the agent acts as an MCP client. Enforce this secure initialization pattern:

import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

// Initialize Authenticated MCP Client Transport
const mcpTransport = new SSEClientTransport(
  new URL("https://mcp.gucluyumhe.dev/sse"),
  {
    eventSourceInit: {
      headers: {
        Authorization: `Bearer ${process.env.MCP_BEARER_TOKEN}`,
        "X-Agent-Identifier": "anthropic-agent-sdk-prod",
      },
    },
  }
);

const mcpClient = new Client(
  {
    name: "EnterpriseSecurityAgent",
    version: "1.0.0",
  },
  {
    capabilities: {
      prompts: {},
      resources: {},
      tools: {},
    },
  }
);

// Connect client before delegating tool executions
await mcpClient.connect(mcpTransport);

// Initialize Anthropic Claude Client
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

// Fetch discovered tools with validated schemas
const availableTools = await mcpClient.listTools();

console.log(`Successfully authenticated ${availableTools.tools.length} secure MCP tools.`);

Implementing McpAuthMetadataRouter for Dynamic Agent Auth & Scoping

For enterprise systems deploying multiple autonomous agents with disparate permission boundaries, implement the McpAuthMetadataRouter pattern. This router verifies agent identity, checks cryptographic bearer signatures, and dynamically restricts tool access based on agent role metadata:

import { Request, Response, NextFunction } from "express";

export interface McpAgentAuthContext {
  agentId: string;
  role: "admin" | "security_auditor" | "readonly_agent";
  allowedTools: string[];
  expiresAt: number;
}

/**
 * McpAuthMetadataRouter
 * Handles dynamic auth, scoped permission routing, and metadata validation
 * for Anthropic Agent SDK and MCP server tool invocations.
 */
export class McpAuthMetadataRouter {
  private static sessions = new Map<string, McpAgentAuthContext>();

  public static registerSession(bearerToken: string, context: McpAgentAuthContext): void {
    this.sessions.set(bearerToken, context);
  }

  public static authorize(bearerToken: string, requestedTool: string): boolean {
    const session = this.sessions.get(bearerToken);
    if (!session) return false;

    // Check session TTL
    if (Date.now() > session.expiresAt) {
      this.sessions.delete(bearerToken);
      return false;
    }

    // Wildcard permission or explicit tool access
    return session.allowedTools.includes("*") || session.allowedTools.includes(requestedTool);
  }

  public static middleware() {
    return (req: Request, res: Response, next: NextFunction): void => {
      const authHeader = req.headers.authorization;
      if (!authHeader?.startsWith("Bearer ")) {
        res.status(401).json({ error: "McpAuthMetadataRouter: Missing Bearer Token" });
        return;
      }

      const token = authHeader.substring(7);
      const requestedTool = req.headers["x-mcp-tool-target"] as string;

      if (requestedTool && !McpAuthMetadataRouter.authorize(token, requestedTool)) {
        res.status(403).json({
          error: `McpAuthMetadataRouter: Agent forbidden from invoking tool "${requestedTool}"`,
        });
        return;
      }

      next();
    };
  }
}

Defense-in-Depth Checklist for Agent SDK Deployments

  • Zero-Trust Tool Scoping: Only register tools required for the specific agent role (Least Privilege via McpAuthMetadataRouter).
  • Execution Timeout Boundaries: Set 15-second execution limits on all tool handlers to mitigate denial-of-service from stuck child processes.
  • Audit Logging: Write every tool invocation, parameters, and return hashes to an immutable audit log.

6. Deployment Strategies (Vercel, Railway, Docker)

Deploying via Docker to Railway or AWS ECS

Create a minimal Dockerfile for process isolation:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist

USER node
EXPOSE 3001
CMD ["node", "dist/server.js"]

Frequently Asked Questions (FAQ)

How does Model Context Protocol (MCP) handle authentication in TypeScript?

In TypeScript, MCP handles authentication through transport-level middleware. While the core MCP specification does not mandate an auth layer, remote implementations using Server-Sent Events (SSE) or HTTP streams wrap endpoints in standard Bearer Token (JWT), OAuth2, or mutual TLS (mTLS) headers. Local Stdio transports inherit the operating system process UID security boundaries.

How do you secure the Anthropic Agent SDK when connecting to remote MCP servers?

To secure the Anthropic Agent SDK, pass encrypted Bearer tokens inside the SSEClientTransport headers, enforce TLS 1.3 encryption on the remote gateway, configure strict CORS policies, and implement Zod input validation schemas to scrub prompt injection payloads before tool execution.

What is the difference between Stdio and SSE transport security in MCP?

Stdio transport operates via standard input/output pipes on the local host, meaning tools execute under the local user's operating system permissions without network exposure. SSE (Server-Sent Events) transport transmits JSON-RPC payloads over HTTP/HTTPS, requiring explicit network firewalls, rate limiting, and token-based authentication guards.

How can you prevent prompt injection and path traversal attacks in MCP tools?

Prevent attacks by validating all incoming tool parameters against strict Zod schemas, rejecting unescaped file path characters (../), enforcing directory path resolution against an allowed root directory with path.resolve(), and running tool processes in non-root Docker containers.


Conclusion

Model Context Protocol represents a monumental leap forward in transforming static LLMs into enterprise-capable AI agents. By enforcing strict Zod validation, robust path sanitization, authenticated SSE transports, and isolated execution environments, you can confidently deploy MCP servers into production.

Explore the complete source code and open-source MCP tools on my GitHub or connect on gucluyumhe.dev.

Ömer Özbay
Written By

Ömer Özbay

Full-Stack Engineer specialized in bridging high-performance backend architectures with pixel-perfect frontend experiences. Building the future with AI and modern web technologies.

Architecture Continuum

Related Architectures & Deep Dives

Read All Posts