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 and deployment blueprints.
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
- Tools: Executable functions that allow the LLM to perform actions (e.g., executing SQL queries, scanning security logs, triggering webhooks).
- Resources: Read-only data endpoints (e.g., configuration files, system metrics, database records) exposed via URI templates (
resource://...). - 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. 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"]
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.
