Mastering MCP · Day 26 of 30

MCP with TypeScript SDK

Build type-safe, production-grade MCP servers with the official TypeScript SDK — Zod input validation, typed resources, structured error handling, and Lambda deployment via esbuild bundles.

📅 Day 26
⏳ ~30 min read
🎯 Level ASCEND
🚀 Phase TypeScript
Table of Contents

Why TypeScript for Production MCP Servers

Python is the dominant language for AI tooling, but TypeScript has compelling advantages for MCP servers in enterprise environments — especially when the server is consumed by JavaScript/TypeScript clients or when the team's primary expertise is in the JS ecosystem.

🔎
Compile-time type safety
TypeScript catches schema mismatches between your tool definition and implementation at build time, not at runtime when an agent is calling your tool. A missing required field in a tool handler is a TypeScript error, not a production incident.
Safety
🔧
IDE-first development
Full autocomplete for MCP SDK types, inline documentation, and refactoring support. When you change a tool's input schema, TypeScript immediately highlights every handler that needs updating — across the entire codebase.
DX
esbuild bundles for Lambda
TypeScript compiles to a single .js bundle with esbuild in under 200ms. Lambda cold starts with a bundled TS server are 60–80% faster than Python servers with heavy dependencies like NumPy or Pydantic.
Performance
👥
Shared types with clients
If your MCP client is also TypeScript (e.g., a Next.js AI application), you can share tool input/output types between client and server in a monorepo — zero drift between what the client sends and what the server expects.
Monorepo

Project Setup: SDK, TypeScript 5 & tsx

The official @modelcontextprotocol/sdk package provides both server and client implementations. Combined with TypeScript 5 and tsx for zero-config execution, you can have a working MCP server in minutes.

Shell — scaffold a TypeScript MCP server project# Initialize project
mkdir my-mcp-server && cd my-mcp-server
npm init -y

# Install MCP SDK + runtime deps
npm install @modelcontextprotocol/sdk zod

# Install dev tools
npm install -D typescript@5 tsx esbuild @types/node

# Generate tsconfig
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext \
        --strict true --outDir dist --rootDir src
JSON — package.json scripts{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --outfile=dist/index.js",
    "start": "node dist/index.js",
    "typecheck": "tsc --noEmit"
  }
}

Defining Typed Tools with Zod Input Schemas

The TypeScript MCP SDK uses Zod schemas to define tool inputs. Zod provides runtime validation AND TypeScript type inference from the same schema definition — you write the schema once and get both validation and types automatically.

TypeScript — complete typed MCP server with Zod toolsimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "documents-server",
  version: "1.0.0",
});

// ── Tool 1: search_documents ──────────────────────────────────────
const SearchDocumentsInput = z.object({
  query:  z.string().min(1).max(500).describe("Full-text search query"),
  limit:  z.number().int().min(1).max(50).default(10).describe("Max results"),
  filter: z.enum(["pdf", "docx", "all"]).default("all"),
});

server.tool(
  "search_documents",
  "Search the document repository using full-text search",
  SearchDocumentsInput,
  async ({ query, limit, filter }) => {
    // TypeScript knows: query is string, limit is number, filter is "pdf"|"docx"|"all"
    const results = await searchIndex(query, { limit, filter });
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify({ results, total: results.length }),
      }],
    };
  }
);

// ── Tool 2: get_document ──────────────────────────────────────────
server.tool(
  "get_document",
  "Retrieve a document by ID with optional section filter",
  {
    document_id: z.string().uuid().describe("Document UUID"),
    sections:    z.array(z.string()).optional().describe("Specific section names to return"),
  },
  async ({ document_id, sections }) => {
    const doc = await fetchDocument(document_id, sections);
    return { content: [{ type: "text" as const, text: doc.content }] };
  }
);

// ── Start server ──────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
💡
Zod .describe() = MCP tool documentation: Every .describe() call on a Zod field becomes the parameter description in the MCP tool's JSON schema — which Claude reads to understand how to call the tool correctly. Write clear, specific descriptions. z.string().describe("Search query") is much less useful than z.string().describe("Full-text search query. Supports boolean operators: AND, OR, NOT. Phrase search with quotes.").

Typed Resources & Resource Templates

MCP resources expose data that agents can read as context. The TypeScript SDK supports both static resources (fixed URI) and resource templates (URI with parameters) — both with full type safety.

TypeScript — resource and resource template definitionsimport { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

// ── Static resource: company knowledge base index ─────────────────
server.resource(
  "knowledge-index",
  "docs://knowledge/index",
  { mimeType: "application/json", description: "Index of all available documents" },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify(await getDocumentIndex()),
    }],
  })
);

// ── Resource template: individual documents by ID ─────────────────
server.resource(
  "document",
  new ResourceTemplate("docs://documents/{document_id}", {
    list: async () => ({
      resources: (await listDocuments()).map(doc => ({
        uri: `docs://documents/${doc.id}`,
        name: doc.title,
        mimeType: "text/plain",
      })),
    }),
  }),
  async (uri, { document_id }) => {
    // document_id is typed as string — extracted from URI template
    const doc = await fetchDocument(document_id as string);
    return {
      contents: [{ uri: uri.href, mimeType: "text/plain", text: doc.content }],
    };
  }
);

Error Handling with McpError and ErrorCode Enum

MCP defines a structured error format that clients use to understand what went wrong. The TypeScript SDK provides the McpError class and ErrorCode enum — always use these instead of throwing plain Error objects from tool handlers.

TypeScript — structured error handling with McpErrorimport { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

server.tool(
  "get_document",
  "Retrieve a document by ID",
  { document_id: z.string().uuid() },
  async ({ document_id }) => {
    let doc;
    try {
      doc = await fetchDocument(document_id);
    } catch (err) {
      if (err instanceof DocumentNotFoundError) {
        // Client-facing: document with this ID does not exist
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Document ${document_id} not found`
        );
      }
      if (err instanceof PermissionDeniedError) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Access denied to document ${document_id}`
        );
      }
      // Unexpected server error — use InternalError
      throw new McpError(
        ErrorCode.InternalError,
        "Failed to retrieve document due to an internal error"
      );
    }
    return { content: [{ type: "text" as const, text: doc.content }] };
  }
);

// ── ErrorCode reference ───────────────────────────────────────────
// ErrorCode.ParseError        = -32700  (invalid JSON)
// ErrorCode.InvalidRequest    = -32600  (bad params, not found)
// ErrorCode.MethodNotFound    = -32601  (unknown tool/resource)
// ErrorCode.InvalidParams     = -32602  (param validation failure)
// ErrorCode.InternalError     = -32603  (unexpected server error)

Lambda Deployment & Python fastmcp vs TypeScript SDK

TypeScript's esbuild bundling makes Lambda deployment exceptionally fast. The entire server compiles to a single JS file — no virtual environments, no dependency layers, no slow pip installs during CI.

Shell — build TypeScript MCP server for Lambda deployment# Bundle to single file (all deps inlined)
esbuild src/index.ts \
  --bundle \
  --platform=node \
  --target=node20 \
  --external:@aws-sdk/* \   # AWS SDK is provided by Lambda runtime
  --minify \
  --outfile=dist/handler.js

# Package for Lambda
cd dist && zip -r ../lambda.zip handler.js
# Result: ~180KB zip (vs 25MB+ for Python with dependencies)

# Deploy
aws lambda update-function-code \
  --function-name mcp-documents-server \
  --zip-file fileb://../lambda.zip
DimensionPython (fastmcp)TypeScript SDK
Schema definitionPython type hints + PydanticZod schemas (runtime + compile-time)
IDE supportGood (Pylance/Pyright)Excellent (native TS)
Lambda cold start~400–800ms (with deps)~80–150ms (bundled)
Bundle size~25MB (venv layer)~180KB (esbuild)
AI/ML library ecosystemUnmatched (NumPy, HuggingFace)Limited
Error handlingMcpError (via SDK)McpError + ErrorCode enum
Best forAI/ML tools, data processingAPI integrations, JS/TS shops
ℹ️
Polyglot MCP environments: You don't have to pick one language for all servers. Use Python for MCP servers that do AI/ML processing (embeddings, document parsing) and TypeScript for servers that integrate with JavaScript APIs (Slack, GitHub, Notion). The MCP protocol is language-agnostic — clients don't know or care which language your server uses.
Knowledge Check
4 questions · instant feedback · TypeScript SDK checkpoint
1. What is the key advantage of using Zod's .describe() method on tool input fields in the TypeScript MCP SDK?
2. Why does a TypeScript MCP server bundled with esbuild have a dramatically faster Lambda cold start than an equivalent Python server?
3. When should you throw McpError(ErrorCode.InternalError, ...) versus McpError(ErrorCode.InvalidRequest, ...) from a TypeScript MCP tool handler?
4. In which scenario is Python (fastmcp) clearly the better choice over TypeScript for building an MCP server?
out of 4 correct —