{"slug": "building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod", "title": "Building Your First Model Context Protocol (MCP) Server with TypeScript and Zod", "summary": "Anthropic's Model Context Protocol (MCP) provides an open standard for connecting AI models to external tools and data sources. A developer demonstrates building a production-grade MCP server using TypeScript, the official @modelcontextprotocol/sdk, and Zod for runtime validation, treating the AI architecture through the lens of modern distributed systems.", "body_md": "If you’ve been building AI agents or working with Large Language Models recently, you’ve likely hit the integration wall. Historically, connecting an LLM like Claude or GPT-4 to external environments—such as querying a production database, checking system logs, or interacting with a local file system—required building ad-hoc, brittle integration layers.\n\nEvery single AI framework required bespoke tool definitions, custom JSON-parsing loops, and hardcoded prompt engineering just to handle basic errors. Building these custom integrations felt remarkably like the early days of web development before HTTP standardization, where every browser vendor implemented proprietary rendering engines, forcing developers to maintain fragmented codebases.\n\nEnter the **Model Context Protocol (MCP)**. Developed by Anthropic, MCP establishes an open, universal standard for connecting AI models to data sources and tools.\n\nIn this comprehensive guide, we will dive deep into the architectural foundations of MCP, explore how it maps cleanly to modern web development paradigms like microservices, and build a production-grade, self-contained MCP server from scratch using TypeScript, the official `@modelcontextprotocol/sdk`\n\n, and Zod for strict runtime input validation.\n\nTo truly grasp why building an MCP server in TypeScript is a game-changer for AI application development, we have to look at how LLMs historically interacted with external environments.\n\nIn a traditional setup, when an AI model needs to fetch data, developers write custom code that wraps APIs in rigid prompt instructions. However, models are probabilistic. They generate text token by token, meaning they are prone to subtle hallucinations regarding data types, missing required arguments, or formatting structural JSON incorrectly.\n\nMCP solves this fragmentation by treating the AI architecture through the lens of modern distributed systems, mapping the **Model Context Protocol Client-Server Architecture** directly to the classic **Microservices Architecture via REST and gRPC**:\n\nWhen an AI model requires data or needs to execute an action, the LLM Host does not execute custom Python or TypeScript scripts directly. Instead, it queries the connected MCP servers to discover available tools, inspects their schemas, and delegates execution. The MCP server processes the request, interacts with the local system, and returns a standardized response payload. This decoupling ensures that security boundaries are strictly maintained: the LLM never executes arbitrary code on your system; it merely requests that an explicit, sandboxed MCP tool executes a predefined function.\n\nAt its core, MCP is not a complex machine learning framework. Rather, it is a clean, rigorously engineered **JSON-RPC 2.0 protocol** running over structured input/output streams.\n\nWhen you boot an MCP server, it does not immediately start listening for AI tokens. Instead, it enters a precise, multi-phase lifecycle governed by initialization handshakes, capability negotiations, and continuous state synchronization.\n\nCommunication between the MCP Host and the MCP Server must be decoupled from the transport mechanism. The TypeScript SDK provides abstract transport interfaces, primarily supporting two modes:\n\n`child_process.spawn`\n\n. Communication happens entirely through standard input (`process.stdin`\n\n) and standard output (`process.stdout`\n\n). This is exceptionally secure because no network ports are opened, eliminating entire classes of network-based vulnerabilities. Logs and debugging statements must be strictly routed to standard error (`process.stderr`\n\n), as any stray `console.log`\n\non stdout will corrupt the JSON-RPC message stream.Once the transport channel is established, neither side assumes the other's capabilities. The handshake proceeds through a rigid sequence of JSON-RPC messages:\n\n`initialize`\n\nRequest:`initialize`\n\nrequest to the server containing its protocol version and client capabilities.`initialize`\n\nResponse:`tools`\n\n, `resources`\n\n, or `prompts`\n\n.`notifications/initialized`\n\nNotification:A common point of confusion for developers new to MCP is distinguishing between **Tools** and **Resources**. While both expose data to the LLM, their semantic contracts are fundamentally different:\n\n`postgres://users/schema`\n\nor `file:///logs/error.log`\n\n). Resources are passive; the LLM reads them to gather context In traditional web applications, input validation is important for security and data integrity. In AI agent architectures, strict input validation is an absolute necessity. Because Large Language Models generate text probabilistically, they are prone to subtle hallucinations regarding data types, missing required arguments, or formatting structural JSON incorrectly.\n\nWhen building an MCP server in TypeScript, you do not write manual `if/else`\n\nchecks or ad-hoc regular expressions to validate incoming tool arguments. Instead, you rely on **Zod**, a TypeScript-first schema declaration and validation library.\n\nMCP bridges the gap between probabilistic text generation and deterministic backend execution by combining LLM tool declarations with strict, runtime schema enforcement via Zod. When you define an MCP tool using the TypeScript SDK, you pass a Zod schema alongside the tool's metadata:\n\n```\n// Conceptual schema definition demonstrating how Zod enforces runtime safety\nconst CreateUserSchema = z.object({\n  username: z.string().describe(\"The unique handle for the user\"),\n  age: z.number().int().positive().describe(\"The user's age in years\"),\n  email: z.string().email().optional().describe(\"Optional contact email\")\n});\n```\n\nUnder the hood, the MCP SDK serializes these Zod schemas into standard JSON Schema specifications during the tool discovery phase. When the LLM Host requests the list of available tools, it receives these explicit JSON Schemas. The host then injects these schemas into the LLM's system prompt or tool-calling grammar (such as OpenAI's function calling or Anthropic's tool use blocks), constraining the model's output generation to match the expected structure.\n\nWhen the LLM decides to invoke the tool, it sends a JSON-RPC request containing the raw argument payload. The MCP server intercepts this payload and passes it directly through the Zod schema's `.parse()`\n\nor `.safeParse()`\n\nmethod:\n\n`\"age\": \"twenty-five\"`\n\ninstead of the integer `25`\n\n, Zod catches the mismatch immediately.`Expected number, received string at \"age\"`\n\n). The MCP server catches this error, serializes it into a standardized JSON-RPC error response, and sends it back to the LLM. To truly master the mechanics of building an MCP server in TypeScript, it helps to map abstract protocol mechanics to familiar software engineering paradigms.\n\nThink of the LLM Host (such as Claude Desktop or an enterprise agent runner) as the **Operating System Kernel**. The kernel possesses high-level intelligence and scheduling capabilities (managing the conversation, deciding the overarching goal), but it is intentionally stripped of direct drivers for every possible peripheral device in the universe.\n\nAn MCP Server is analogous to a **Device Driver** (e.g., a graphics driver, a printer driver, or a file system driver). Before an operating system can interact with a specialized RAID controller, the manufacturer must write a driver that adheres to the OS's strict kernel extension interfaces. Similarly, before an AI model can interact with a proprietary SQL database or a local Git repository, a developer must write an MCP server that adheres to the Model Context Protocol specification.\n\nIf you have experience building modern web APIs, the architectural design of an MCP server will feel remarkably familiar. In a GraphQL server, you define a schema using the GraphQL SDL, mapping types, queries, and mutations. For every field in that schema, you write a **resolver function** that fetches the required data from a database, microservice, or cache.\n\nIn an MCP server, the relationship between tools and their execution handlers mirrors GraphQL resolvers precisely:\n\nLet us examine a complete, self-contained MCP server designed for a SaaS context. This example implements a simulated customer support metrics and user lookup tool. It uses the official `@modelcontextprotocol/sdk`\n\nand `zod`\n\nfor strict runtime input validation, communicating over standard input/output (stdio) streams with an MCP host.\n\n``` bash\n#!/usr/bin/env node\n/**\n * SaaS Customer Support MCP Server\n * \n * This self-contained TypeScript file sets up a Model Context Protocol (MCP) server\n * using the official @modelcontextprotocol/sdk. It exposes a tool for querying \n * user account metrics and fetching application logs, designed to integrate with \n * an AI assistant or supervisor agent running inside an MCP-compatible host.\n */\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n  CallToolRequestSchema,\n  ListToolsRequestSchema,\n  ListResourcesRequestSchema,\n  ReadResourceRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\n// ============================================================================\n// Mock Database / SaaS Data Layer\n// ============================================================================\n\ninterface SaaSUser {\n  id: string;\n  email: string;\n  plan: \"free\" | \"pro\" | \"enterprise\";\n  mrr: number; // Monthly Recurring Revenue\n  status: \"active\" | \"suspended\" | \"churned\";\n  lastActive: string;\n}\n\nconst MOCK_USERS: Record<string, SaaSUser> = {\n  \"usr_101\": {\n    id: \"usr_101\",\n    email: \"alice@acme-corp.com\",\n    plan: \"enterprise\",\n    mrr: 499.00,\n    status: \"active\",\n    lastActive: \"2023-10-25T14:32:00Z\",\n  },\n  \"usr_102\": {\n    id: \"usr_102\",\n    email: \"bob@startup-io.net\",\n    plan: \"pro\",\n    mrr: 49.00,\n    status: \"active\",\n    lastActive: \"2023-10-24T09:15:00Z\",\n  },\n};\n\nconst MOCK_SYSTEM_LOGS = `[2023-10-25T14:30:00Z] [INFO] System health nominal. DB connection pool: 12/50.\n[2023-10-25T14:31:12Z] [WARN] Rate limit approached for client usr_102 (92% capacity).\n[2023-10-25T14:32:00Z] [INFO] User usr_101 successfully authenticated via OAuth2.`;\n\n// ============================================================================\n// Server Initialization\n// ============================================================================\n\nconst server = new Server(\n  {\n    name: \"saas-support-mcp-server\",\n    version: \"1.0.0\",\n  },\n  {\n    capabilities: {\n      tools: {},\n      resources: {},\n    },\n  }\n);\n\n// ============================================================================\n// Tool Schema Definitions (Using Zod)\n// ============================================================================\n\nconst GetUserMetricsSchema = z.object({\n  userId: z.string().describe(\"The unique identifier of the user (e.g., usr_101)\"),\n});\n\n// ============================================================================\n// Request Handlers Setup\n// ============================================================================\n\nserver.setRequestHandler(ListToolsRequestSchema, async () => {\n  return {\n    tools: [\n      {\n        name: \"get_user_metrics\",\n        description: \"Retrieves billing, plan, and activity status for a given SaaS user.\",\n        inputSchema: {\n          type: \"object\",\n          properties: {\n            userId: {\n              type: \"string\",\n              description: \"The unique identifier of the user (e.g., usr_101)\",\n            },\n          },\n          required: [\"userId\"],\n        },\n      },\n    ],\n  };\n});\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n  const { name, arguments: args } = request.params;\n\n  if (name === \"get_user_metrics\") {\n    const parseResult = GetUserMetricsSchema.safeParse(args);\n\n    if (!parseResult.success) {\n      throw new Error(\n        `Invalid arguments for get_user_metrics: ${parseResult.error.message}`\n      );\n    }\n\n    const { userId } = parseResult.data;\n    const user = MOCK_USERS[userId];\n\n    if (!user) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: JSON.stringify({ error: `User with ID ${userId} not found.` }),\n          },\n        ],\n        isError: true,\n      };\n    }\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify(user, null, 2),\n        },\n      ],\n    };\n  }\n\n  throw new Error(`Unknown tool: ${name}`);\n});\n\n// ============================================================================\n// Resource Handlers Setup\n// ============================================================================\n\nserver.setRequestHandler(ListResourcesRequestSchema, async () => {\n  return {\n    resources: [\n      {\n        uri: \"saas://logs/system\",\n        name: \"System Activity Logs\",\n        description: \"Real-time diagnostic logs from the SaaS application backend.\",\n        mimeType: \"text/plain\",\n      },\n    ],\n  };\n});\n\nserver.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n  const { uri } = request.params;\n\n  if (uri === \"saas://logs/system\") {\n    return {\n      contents: [\n        {\n          uri,\n          mimeType: \"text/plain\",\n          text: MOCK_SYSTEM_LOGS,\n        },\n      ],\n    };\n  }\n\n  throw new Error(`Resource not found: ${uri}`);\n});\n\n// ============================================================================\n// Transport Connection & Execution\n// ============================================================================\n\nasync function main() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n  console.error(\"SaaS Support MCP Server running on stdio\");\n}\n\nmain().catch((error) => {\n  console.error(\"Fatal error in MCP server initialization:\", error);\n  process.exit(1);\n});\n```\n\nTo fully master the mechanics of building an MCP server in TypeScript, let us examine the critical segments of the code above:\n\n**Shebang and Environment Definition ( #!/usr/bin/env node)**:\n\n`PATH`\n\n. This is essential because MCP servers are frequently spawned as child processes by desktop host applications like Claude Desktop.**Server Instance Configuration**:\n\n`Server`\n\ninstance from `@modelcontextprotocol/sdk/server/index.js`\n\n, passing server metadata (`name`\n\nand `version`\n\n) and an explicit capability map declaring that our server supports both `tools`\n\nand `resources`\n\n.**Runtime Validation via Zod**:\n\n`GetUserMetricsSchema`\n\ndefines the expected shape of incoming arguments. When the LLM invokes `get_user_metrics`\n\n, the server intercepts the payload and executes `.safeParse(args)`\n\n. If the model hallucinates or passes incorrect types, Zod catches the mismatch immediately and formats a descriptive error message.**Request Handler Routing**:\n\n`ListToolsRequestSchema`\n\n, `CallToolRequestSchema`\n\n, `ListResourcesRequestSchema`\n\n, and `ReadResourceRequestSchema`\n\n. This clean separation ensures our server can dynamically advertise its capabilities and execute business logic safely.**Transport Connection ( StdioServerTransport)**:\n\n`main`\n\nasynchronous function instantiates a standard input/output transport layer and connects the server instance. Crucially, all diagnostic logs are sent to `process.stderr`\n\n, preserving `process.stdout`\n\nexclusively for clean JSON-RPC message passing.As AI agents transition from passive chat assistants to active automation engines capable of executing code, modifying files, and calling enterprise APIs, security becomes paramount. The Model Context Protocol establishes a robust security posture through architectural isolation and the principle of least privilege:\n\nBuilding your first Model Context Protocol server in TypeScript opens up a world of robust, secure, and standardized AI application development. By moving away from brittle, ad-hoc integration layers and adopting a clean client-server architecture powered by JSON-RPC, TypeScript, and Zod, you can transform probabilistic Large Language Models into deterministic, highly capable autonomous agents.\n\nWhether you are building internal developer tools, enterprise support automations, or specialized data connectors, mastering MCP is an essential skill for modern AI engineers. Clone the SDK, set up your Zod schemas, and start building your first custom MCP server today!\n\nThe concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the book **Model Context Protocol (MCP) & Computer Use. Standardizing Tool Integration, Vision-Driven Browser Automation, and Agent Governance in TypeScript**, you can find it [here](http://tiny.cc/ModelContextProtocol). Check also the many other [ebooks](http://tiny.cc/ProgrammingBooks).", "url": "https://wpnews.pro/news/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod", "canonical_source": "https://dev.to/programmingcentral/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod-2non", "published_at": "2026-07-24 20:00:00+00:00", "updated_at": "2026-07-24 20:01:44.323681+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Anthropic", "TypeScript", "Zod", "Model Context Protocol", "GPT-4", "Claude"], "alternates": {"html": "https://wpnews.pro/news/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod", "markdown": "https://wpnews.pro/news/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod.md", "text": "https://wpnews.pro/news/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod.txt", "jsonld": "https://wpnews.pro/news/building-your-first-model-context-protocol-mcp-server-with-typescript-and-zod.jsonld"}}