jsmanifest logojsmanifest

Writing Your First Claude Code MCP Server in TypeScript: A Practical Guide

Writing Your First Claude Code MCP Server in TypeScript: A Practical Guide

Build production-ready MCP servers for Claude Desktop with TypeScript. Learn the architecture, implement tools and resources, and ship your first server in under an hour.

Most teams building AI tooling overlook the Model Context Protocol entirely—then wonder why their Claude integrations feel fragile and limited. The pattern that unlocks extensible AI workflows is building your own MCP server, a standardized interface that gives Claude Desktop capabilities you define. The difference between integrating Claude as a chatbot and as a programmable platform starts here.

What Is MCP and Why Build Your Own Server?

The Model Context Protocol is Anthropic's specification for extending Claude's capabilities through external servers. Rather than writing custom API integrations or prompt engineering workarounds, developers implement MCP servers that expose tools, resources, and prompts to Claude Desktop. The client-server architecture means your server runs locally or remotely, Claude connects via stdio or SSE transport, and the protocol handles bidirectional communication.

The value proposition is immediate: build once, integrate everywhere. An MCP server that reads your codebase becomes available to Claude Desktop automatically. No fragile prompt chains, no API wrapper maintenance, no vendor lock-in. This matters because AI tooling requirements change faster than integration code can keep up—the protocol abstracts that complexity.

The failure mode here is building point solutions. Teams write bespoke Claude scripts, hardcode context into prompts, or rely on manual copy-paste workflows. When requirements evolve, the entire integration breaks. MCP servers decouple capabilities from consumption, making your AI tooling composable and testable.

Understanding the Model Context Protocol Architecture

The protocol defines three core primitives: tools, resources, and prompts. Tools are functions Claude can invoke—reading files, querying databases, calling APIs. Resources are contextual data Claude can request—documentation, schemas, project metadata. Prompts are reusable templates Claude can apply—code review checklists, refactoring patterns, debugging workflows.

The server-client relationship is strictly defined. Claude Desktop acts as the MCP client, discovering capabilities through initialization handshakes. Your server advertises available tools and resources via the initialize response, handles method calls through JSON-RPC 2.0, and returns typed responses. The protocol enforces schema validation, error handling, and versioning—production concerns baked into the specification.

%% alt: MCP architecture showing client-server communication flow
flowchart TD
    ClaudeDesktop["Claude Desktop Client"]
    MCPServer["Your MCP Server"]
    Tools["Tools: invoke functions"]
    Resources["Resources: provide context"]
    Prompts["Prompts: reusable templates"]
    
    ClaudeDesktop -->|"initialize request"| MCPServer
    MCPServer -->|"capabilities response"| ClaudeDesktop
    ClaudeDesktop -->|"call tool/resource"| MCPServer
    MCPServer --> Tools
    MCPServer --> Resources
    MCPServer --> Prompts
    MCPServer -->|"typed response"| ClaudeDesktop
    
    classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
    classDef uiComponent fill:#2a1840,stroke:#c084fc,color:#f3e8ff
    classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
    
    class ClaudeDesktop framework
    class MCPServer uiComponent
    class Tools,Resources,Prompts userAction

Transport options are stdio (standard input/output) or SSE (server-sent events). For local development, stdio is simpler—your server runs as a subprocess of Claude Desktop, communicating via stdin/stdout. For remote deployments, SSE enables HTTP-based connections with persistent streaming. The choice impacts deployment strategy but not server implementation.

MCP server architecture diagram

Setting Up Your TypeScript MCP Server Project

The official @modelcontextprotocol/sdk package provides TypeScript bindings and server scaffolding. Start with a minimal package structure: src/index.ts as the entry point, tsconfig.json for compiler settings, and package.json declaring the SDK dependency. The server runs as a Node.js process, so target Node 18+ for ESM support and native fetch.

%% alt: MCP server project setup and initialization flow
flowchart TD
    Init["npm init + install SDK"]
    Config["Configure TypeScript"]
    Server["Create Server instance"]
    Transport["Setup stdio transport"]
    Connect["Connect and start"]
    
    Init --> Config
    Config --> Server
    Server --> Transport
    Transport --> Connect
    
    classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
    classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
    
    class Init,Config userAction
    class Server,Transport,Connect framework

Installation and initialization are three commands:

// package.json
{
  "name": "my-mcp-server",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  },
  "bin": {
    "my-mcp-server": "./dist/index.js"
  }
}
 
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 
const server = new Server(
  {
    name: "my-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);

The server instance declares capabilities upfront—this example enables tools but not resources or prompts. Claude Desktop queries capabilities during initialization, so only advertise what your server implements. The transport handles low-level communication, your server handles business logic.

Build configuration targets ESNext modules with Node16 resolution. The output runs in Node.js directly, no bundling required:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

Compile with tsc, then invoke via the bin entry: node dist/index.js. Claude Desktop spawns this process when your server is configured. The implication here is your server must start cleanly, connect to stdio, and respond to initialization—Claude Desktop will not retry failed connections.

Building Your First Tool: File System Operations

Tools are functions Claude can invoke with typed arguments. The SDK provides server.setRequestHandler for the tools/call method—register tool handlers that execute business logic and return results. A file reader tool takes a path argument, reads the file, and returns content or an error.

import { promises as fs } from "fs";
import { z } from "zod";
 
// Define tool schema
const ReadFileSchema = z.object({
  path: z.string().describe("Absolute path to the file"),
});
 
// Register tool
server.setRequestHandler("tools/list", async () => ({
  tools: [
    {
      name: "read_file",
      description: "Read contents of a file from the filesystem",
      inputSchema: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "Absolute path to the file",
          },
        },
        required: ["path"],
      },
    },
  ],
}));
 
server.setRequestHandler("tools/call", async (request) => {
  if (request.params.name === "read_file") {
    const args = ReadFileSchema.parse(request.params.arguments);
    
    try {
      const content = await fs.readFile(args.path, "utf-8");
      return {
        content: [
          {
            type: "text",
            text: content,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error reading file: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
  
  throw new Error(`Unknown tool: ${request.params.name}`);
});

The tools/list handler returns available tools—Claude Desktop calls this during initialization to populate its tool registry. The inputSchema follows JSON Schema specification, enabling client-side validation before tool invocation. The tools/call handler executes the tool, parsing arguments with zod for runtime safety.

Return values are content arrays containing text or image blocks. The isError flag signals failure without throwing exceptions—Claude Desktop presents errors to the user as tool output, maintaining conversation flow. This pattern handles partial failures gracefully: if a tool can return useful context even on error, include it in the response.

Adding Resources and Prompts to Your MCP Server

Resources provide read-only context that Claude can request on demand. A project documentation resource exposes README files, API schemas, or configuration metadata. The pattern mirrors tools: list available resources, handle read requests, return content.

server.setRequestHandler("resources/list", async () => ({
  resources: [
    {
      uri: "file:///project/README.md",
      name: "Project Documentation",
      mimeType: "text/markdown",
    },
  ],
}));
 
server.setRequestHandler("resources/read", async (request) => {
  const uri = request.params.uri;
  
  if (uri === "file:///project/README.md") {
    const content = await fs.readFile("./README.md", "utf-8");
    return {
      contents: [
        {
          uri,
          mimeType: "text/markdown",
          text: content,
        },
      ],
    };
  }
  
  throw new Error(`Unknown resource: ${uri}`);
});

Prompts are reusable templates that Claude can apply to conversations. A code review prompt might inject review criteria, examples, and output format requirements. The prompts/list handler returns available prompts, prompts/get returns the template with optional arguments:

server.setRequestHandler("prompts/list", async () => ({
  prompts: [
    {
      name: "code_review",
      description: "Review code changes for quality and security",
      arguments: [
        {
          name: "language",
          description: "Programming language",
          required: true,
        },
      ],
    },
  ],
}));
 
server.setRequestHandler("prompts/get", async (request) => {
  if (request.params.name === "code_review") {
    const language = request.params.arguments?.language ?? "typescript";
    
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Review this ${language} code for:\n- Security vulnerabilities\n- Performance issues\n- Code style consistency`,
          },
        },
      ],
    };
  }
  
  throw new Error(`Unknown prompt: ${request.params.name}`);
});

The distinction is critical: tools execute actions, resources provide data, prompts inject conversation templates. Claude Desktop uses tools for operations, resources for context, and prompts for structured workflows. Mixing these concerns creates confusing UX—a "review code" tool that returns a prompt instead of executing the review breaks user expectations.

MCP server capabilities diagram

Connecting Your MCP Server to Claude Desktop

Claude Desktop discovers MCP servers via a JSON configuration file in the user config directory. On macOS, that's ~/Library/Application Support/Claude/claude_desktop_config.json. The configuration maps server names to command invocations—your server's bin path and any environment variables or arguments.

%% alt: Claude Desktop server connection and initialization sequence
flowchart TD
    Config["Read config file"]
    Spawn["Spawn server process"]
    Init["Send initialize request"]
    Caps["Receive capabilities"]
    Ready["Server ready for use"]
    
    Config --> Spawn
    Spawn --> Init
    Init --> Caps
    Caps --> Ready
    
    classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
    classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
    
    class Config,Spawn userAction
    class Init,Caps,Ready framework

The configuration format is straightforward:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"],
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}

After editing the config, restart Claude Desktop completely—quit the app, not just close the window. The server process spawns on Claude Desktop launch, initialization occurs before the first conversation. Check Claude Desktop's MCP logs (accessible via the menu bar) to confirm successful connection and capability registration.

The failure mode here is subtle but expensive: if your server crashes during initialization, Claude Desktop shows no error and silently disables the server. Add error logging to stdout/stderr immediately—Claude Desktop captures both streams and displays them in the MCP logs view. Production servers should log initialization steps, capability counts, and any configuration issues.

Testing and Debugging Your MCP Server

Local testing requires running your server in stdio mode and manually sending JSON-RPC messages. The SDK includes testing utilities, but the quickest validation is starting the server directly and verifying stdio communication. A minimal test harness sends an initialize request and checks the response:

%% alt: MCP server testing and debugging workflow
flowchart TD
    Start["Start server in stdio mode"]
    Send["Send JSON-RPC request"]
    Validate["Validate response schema"]
    Logs["Check server logs"]
    Fix["Fix errors, rebuild"]
    
    Start --> Send
    Send --> Validate
    Validate --> Logs
    Logs -->|"errors found"| Fix
    Fix --> Start
    Logs -->|"success"| Start
    
    classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
    classDef dataStore fill:#3a2f0b,stroke:#fbbf24,color:#fef3c7
    
    class Start,Send,Fix userAction
    class Validate,Logs dataStore

Common issues include schema validation failures (inputSchema doesn't match tool arguments), incorrect response formats (missing content array), and transport errors (server exits before Claude Desktop connects). The debugging pattern is stdio logging: write debug messages to stderr, structured responses to stdout. Claude Desktop separates these streams in the MCP logs view.

Integration testing happens in Claude Desktop itself. Configure your server, restart the app, and verify tools appear in the tool picker during conversations. Invoke a tool and confirm the response displays correctly—text content renders as markdown, images as inline media. Resource requests should return immediately, prompts should inject properly formatted messages.

Production deployments require health checks and graceful shutdown handling. The SDK provides server.close() for cleanup—close database connections, flush logs, terminate child processes. Handle SIGTERM and SIGINT signals to ensure clean exits:

process.on("SIGTERM", async () => {
  await server.close();
  process.exit(0);
});
 
process.on("SIGINT", async () => {
  await server.close();
  process.exit(0);
});

The implication here is your server must be stateless or persist state externally. Claude Desktop may restart the server between conversations, and local storage will not survive. Use databases, file systems, or external APIs for persistent data—treat the server process as ephemeral.

Production Patterns and Next Steps

The essential pattern for production MCP servers is single-purpose capability design. A server that reads files should not also query databases and call external APIs—that creates maintenance burden and unclear failure domains. Compose multiple specialized servers instead, each registered in Claude Desktop's config. The SDK handles process isolation, Claude Desktop handles orchestration.

Error handling should return meaningful context, not generic failures. When a tool fails, include diagnostic information: what failed, why it failed, what the user should check. Claude Desktop presents tool errors directly to the user, so clear error messages improve the debugging experience without requiring server logs.

Related patterns worth exploring: Claude Agent SDK integration for multi-step workflows, packaging MCP servers as distributable plugins, and building full-stack applications with Claude Code. The MCP ecosystem is expanding rapidly—servers that integrate with popular frameworks, databases, and APIs are emerging as the standard approach to extending Claude's capabilities.

That covers the essential patterns for building MCP servers in TypeScript. Apply these in production and the difference will be immediate: Claude Desktop becomes a programmable AI platform instead of a chatbot. The initial investment is under an hour—the return compounds with every capability you add.