25 min read
Intermediate
Server Development

Building Your First MCP Server

Deep dive into building production-ready MCP servers with advanced patterns

MCPgee Team

MCP Expert

Completed 'Setting Up Your First MCP Server' tutorialSolid understanding of TypeScript/JavaScriptFamiliarity with async programmingBasic knowledge of design patterns

Building Your First MCP Server

Introduction

While the getting started tutorial covered the basics, this guide dives deep into building production-ready MCP servers with proper architecture, error handling, testing, and deployment patterns. We will build a task management server that demonstrates real-world MCP development practices.

For the Python perspective, see our guide on building an MCP server in Python.

Architecture Overview

Production MCP Server Layers

A well-structured MCP server separates concerns into layers:

plaintext
┌─────────────────────────────────────────┐
│       MCP Protocol Layer (McpServer)    │
├─────────────────────────────────────────┤
│       Tool & Resource Handlers          │
├─────────────────────────────────────────┤
│         Business Logic / Services       │
├─────────────────────────────────────────┤
│       Data Access / Storage Layer       │
└─────────────────────────────────────────┘

Design Principles

  1. Separation of Concerns: Protocol handling separate from business logic
  2. Type Safety: Zod schemas for runtime validation, TypeScript for compile-time checks
  3. Error Boundaries: Graceful error handling at each layer
  4. Testability: Business logic decoupled from MCP transport
  5. Observability: Logging to stderr for debugging

Project Structure

Recommended Directory Layout

plaintext
my-mcp-server/
├── src/
│   ├── index.ts              # Entry point - server startup
│   ├── server.ts             # McpServer configuration and tool registration
│   ├── services/
│   │   └── task.service.ts   # Business logic
│   ├── storage/
│   │   └── task.store.ts     # Data persistence
│   ├── types/
│   │   └── task.ts           # Type definitions and Zod schemas
│   └── utils/
│       └── logger.ts         # Logging utility
├── tests/
│   ├── task.service.test.ts
│   └── server.test.ts
├── package.json
├── tsconfig.json
└── README.md

Core Implementation

1. Type Definitions and Schemas

Create src/types/task.ts:

typescript
import { z } from 'zod';

export const TaskPriority = z.enum(['low', 'medium', 'high', 'urgent']);
export const TaskStatus = z.enum(['todo', 'in_progress', 'review', 'done', 'archived']);

export const TaskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(200),
  description: z.string().max(5000).optional(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee: z.string().optional(),
  dueDate: z.string().datetime().optional(),
  tags: z.array(z.string()).default([]),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

export type Task = z.infer<typeof TaskSchema>;
export type TaskPriorityType = z.infer<typeof TaskPriority>;
export type TaskStatusType = z.infer<typeof TaskStatus>;

2. Storage Layer

Create src/storage/task.store.ts:

typescript
import { readFileSync, writeFileSync, existsSync } from 'fs';
import type { Task } from '../types/task.js';

export class TaskStore {
  private tasks = new Map<string, Task>();
  private nextId = 1;
  private filePath: string | null;

  constructor(filePath?: string) {
    this.filePath = filePath ?? null;
    if (this.filePath && existsSync(this.filePath)) {
      const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
      for (const task of data.tasks) {
        this.tasks.set(task.id, task);
      }
      this.nextId = data.nextId ?? this.tasks.size + 1;
    }
  }

  private persist(): void {
    if (!this.filePath) return;
    writeFileSync(
      this.filePath,
      JSON.stringify({
        tasks: [...this.tasks.values()],
        nextId: this.nextId,
      }, null, 2)
    );
  }

  create(data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Task {
    const now = new Date().toISOString();
    const task: Task = {
      ...data,
      id: `task-${this.nextId++}`,
      createdAt: now,
      updatedAt: now,
    };
    this.tasks.set(task.id, task);
    this.persist();
    return task;
  }

  get(id: string): Task | undefined {
    return this.tasks.get(id);
  }

  list(filter?: {
    status?: string;
    priority?: string;
    assignee?: string;
    search?: string;
  }): Task[] {
    let results = [...this.tasks.values()];

    if (filter?.status) {
      results = results.filter((t) => t.status === filter.status);
    }
    if (filter?.priority) {
      results = results.filter((t) => t.priority === filter.priority);
    }
    if (filter?.assignee) {
      results = results.filter((t) => t.assignee === filter.assignee);
    }
    if (filter?.search) {
      const q = filter.search.toLowerCase();
      results = results.filter(
        (t) =>
          t.title.toLowerCase().includes(q) ||
          t.description?.toLowerCase().includes(q)
      );
    }
    return results.sort(
      (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
    );
  }

  update(id: string, data: Partial<Omit<Task, 'id' | 'createdAt'>>): Task | null {
    const existing = this.tasks.get(id);
    if (!existing) return null;

    const updated: Task = {
      ...existing,
      ...data,
      id: existing.id,
      createdAt: existing.createdAt,
      updatedAt: new Date().toISOString(),
    };
    this.tasks.set(id, updated);
    this.persist();
    return updated;
  }

  delete(id: string): boolean {
    const result = this.tasks.delete(id);
    if (result) this.persist();
    return result;
  }

  count(): number {
    return this.tasks.size;
  }
}

3. Business Logic Service

Create src/services/task.service.ts:

typescript
import { TaskStore } from '../storage/task.store.js';
import type { Task, TaskPriorityType, TaskStatusType } from '../types/task.js';

export class TaskService {
  constructor(private store: TaskStore) {}

  createTask(params: {
    title: string;
    description?: string;
    priority: TaskPriorityType;
    status?: TaskStatusType;
    assignee?: string;
    dueDate?: string;
    tags?: string[];
  }): Task {
    return this.store.create({
      title: params.title,
      description: params.description,
      priority: params.priority,
      status: params.status ?? 'todo',
      assignee: params.assignee,
      dueDate: params.dueDate,
      tags: params.tags ?? [],
    });
  }

  getTask(id: string): Task | null {
    return this.store.get(id) ?? null;
  }

  listTasks(filter?: {
    status?: string;
    priority?: string;
    assignee?: string;
    search?: string;
  }): { tasks: Task[]; total: number } {
    const tasks = this.store.list(filter);
    return { tasks, total: tasks.length };
  }

  updateTask(
    id: string,
    updates: Partial<Pick<Task, 'title' | 'description' | 'priority' | 'status' | 'assignee' | 'dueDate' | 'tags'>>
  ): Task | null {
    return this.store.update(id, updates);
  }

  deleteTask(id: string): boolean {
    return this.store.delete(id);
  }

  getStatistics(): {
    total: number;
    byStatus: Record<string, number>;
    byPriority: Record<string, number>;
  } {
    const tasks = this.store.list();
    const byStatus: Record<string, number> = {};
    const byPriority: Record<string, number> = {};

    for (const task of tasks) {
      byStatus[task.status] = (byStatus[task.status] ?? 0) + 1;
      byPriority[task.priority] = (byPriority[task.priority] ?? 0) + 1;
    }

    return { total: tasks.length, byStatus, byPriority };
  }
}

4. MCP Server Setup (McpServer API)

Create src/server.ts - this is where we wire everything together using the McpServer class:

typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { TaskService } from './services/task.service.js';

export function createServer(taskService: TaskService): McpServer {
  const server = new McpServer({
    name: 'task-management-server',
    version: '1.0.0',
  });

  // --- Tools ---

  server.tool(
    'create_task',
    'Create a new task with title, priority, and optional details',
    {
      title: z.string().min(1).max(200).describe('Task title'),
      description: z.string().max(5000).optional().describe('Detailed description'),
      priority: z.enum(['low', 'medium', 'high', 'urgent']).describe('Priority level'),
      status: z.enum(['todo', 'in_progress', 'review', 'done']).default('todo'),
      assignee: z.string().optional().describe('Person assigned to the task'),
      dueDate: z.string().optional().describe('Due date in ISO 8601 format'),
      tags: z.array(z.string()).optional().describe('Tags for categorization'),
    },
    async (params) => {
      const task = taskService.createTask(params);
      return {
        content: [{
          type: 'text',
          text: `Created task "${task.title}" (ID: ${task.id}, Priority: ${task.priority})`,
        }],
      };
    }
  );

  server.tool(
    'list_tasks',
    'List tasks with optional filtering by status, priority, assignee, or search term',
    {
      status: z.enum(['todo', 'in_progress', 'review', 'done', 'archived']).optional(),
      priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
      assignee: z.string().optional(),
      search: z.string().optional().describe('Search in title and description'),
    },
    async (filter) => {
      const { tasks, total } = taskService.listTasks(filter);
      if (tasks.length === 0) {
        return { content: [{ type: 'text', text: 'No tasks found matching your criteria.' }] };
      }
      const lines = tasks.map(
        (t) => `- [${t.priority.toUpperCase()}] ${t.title} (${t.status}) - ID: ${t.id}`
      );
      return {
        content: [{ type: 'text', text: `Found ${total} tasks:\n\n${lines.join('\n')}` }],
      };
    }
  );

  server.tool(
    'get_task',
    'Get detailed information about a specific task by ID',
    {
      id: z.string().describe('Task ID (e.g., task-1)'),
    },
    async ({ id }) => {
      const task = taskService.getTask(id);
      if (!task) {
        return { content: [{ type: 'text', text: `Task ${id} not found.` }] };
      }
      const details = [
        `# ${task.title}`,
        `**Status:** ${task.status} | **Priority:** ${task.priority}`,
        task.assignee ? `**Assignee:** ${task.assignee}` : null,
        task.dueDate ? `**Due:** ${task.dueDate}` : null,
        task.tags.length ? `**Tags:** ${task.tags.join(', ')}` : null,
        '',
        task.description || '_No description_',
      ].filter(Boolean).join('\n');
      return { content: [{ type: 'text', text: details }] };
    }
  );

  server.tool(
    'update_task',
    'Update fields of an existing task',
    {
      id: z.string().describe('Task ID'),
      title: z.string().min(1).max(200).optional(),
      description: z.string().max(5000).optional(),
      priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
      status: z.enum(['todo', 'in_progress', 'review', 'done', 'archived']).optional(),
      assignee: z.string().optional(),
      dueDate: z.string().optional(),
      tags: z.array(z.string()).optional(),
    },
    async ({ id, ...updates }) => {
      const task = taskService.updateTask(id, updates);
      if (!task) {
        return { content: [{ type: 'text', text: `Task ${id} not found.` }] };
      }
      return { content: [{ type: 'text', text: `Updated task "${task.title}"` }] };
    }
  );

  server.tool(
    'delete_task',
    'Delete a task by ID',
    {
      id: z.string().describe('Task ID to delete'),
    },
    async ({ id }) => {
      const success = taskService.deleteTask(id);
      return {
        content: [{
          type: 'text',
          text: success ? `Deleted task ${id}` : `Task ${id} not found.`,
        }],
      };
    }
  );

  server.tool(
    'task_statistics',
    'Get summary statistics of all tasks grouped by status and priority',
    {},
    async () => {
      const stats = taskService.getStatistics();
      const lines = [
        `**Total tasks:** ${stats.total}`,
        '',
        '**By Status:**',
        ...Object.entries(stats.byStatus).map(([k, v]) => `  - ${k}: ${v}`),
        '',
        '**By Priority:**',
        ...Object.entries(stats.byPriority).map(([k, v]) => `  - ${k}: ${v}`),
      ];
      return { content: [{ type: 'text', text: lines.join('\n') }] };
    }
  );

  // --- Resources ---

  server.resource(
    'task-list',
    'task:///list',
    'JSON list of all tasks',
    async (uri) => ({
      contents: [{
        uri: uri.href,
        mimeType: 'application/json',
        text: JSON.stringify(taskService.listTasks().tasks, null, 2),
      }],
    })
  );

  server.resource(
    'task-stats',
    'task:///stats',
    'Task statistics as JSON',
    async (uri) => ({
      contents: [{
        uri: uri.href,
        mimeType: 'application/json',
        text: JSON.stringify(taskService.getStatistics(), null, 2),
      }],
    })
  );

  return server;
}

5. Entry Point

Create src/index.ts:

typescript
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { TaskStore } from './storage/task.store.js';
import { TaskService } from './services/task.service.js';
import { createServer } from './server.js';

// Configuration from environment
const storagePath = process.env.STORAGE_PATH ?? undefined;

// Initialize layers
const store = new TaskStore(storagePath);
const service = new TaskService(store);
const server = createServer(service);

// Handle graceful shutdown
process.on('SIGINT', () => {
  console.error('Shutting down...');
  process.exit(0);
});
process.on('SIGTERM', () => {
  console.error('Shutting down...');
  process.exit(0);
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Task Management MCP Server running on stdio');

Testing Your Server

Unit Testing the Service Layer

typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { TaskStore } from '../src/storage/task.store.js';
import { TaskService } from '../src/services/task.service.js';

describe('TaskService', () => {
  let service: TaskService;

  beforeEach(() => {
    service = new TaskService(new TaskStore()); // In-memory, no file
  });

  it('should create a task', () => {
    const task = service.createTask({
      title: 'Test Task',
      priority: 'high',
    });
    expect(task.id).toBeDefined();
    expect(task.title).toBe('Test Task');
    expect(task.status).toBe('todo');
  });

  it('should filter tasks by status', () => {
    service.createTask({ title: 'Task 1', priority: 'high', status: 'todo' });
    service.createTask({ title: 'Task 2', priority: 'low', status: 'done' });

    const { tasks } = service.listTasks({ status: 'todo' });
    expect(tasks).toHaveLength(1);
    expect(tasks[0].title).toBe('Task 1');
  });

  it('should search tasks by keyword', () => {
    service.createTask({ title: 'Fix login bug', priority: 'urgent' });
    service.createTask({ title: 'Add dark mode', priority: 'medium' });

    const { tasks } = service.listTasks({ search: 'login' });
    expect(tasks).toHaveLength(1);
    expect(tasks[0].title).toBe('Fix login bug');
  });

  it('should compute statistics', () => {
    service.createTask({ title: 'T1', priority: 'high', status: 'todo' });
    service.createTask({ title: 'T2', priority: 'high', status: 'done' });
    service.createTask({ title: 'T3', priority: 'low', status: 'todo' });

    const stats = service.getStatistics();
    expect(stats.total).toBe(3);
    expect(stats.byPriority['high']).toBe(2);
    expect(stats.byStatus['todo']).toBe(2);
  });
});

Integration Testing with JSON-RPC

Test the full server by piping JSON-RPC messages:

bash
# Initialize the server
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | npx tsx src/index.ts

# List available tools
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | npx tsx src/index.ts

Python Equivalent with FastMCP

For comparison, here is the same task management server in Python using FastMCP. For a full walkthrough, read Build Your First MCP Server in Python.

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("task-management-server")

tasks: dict[str, dict] = {}
next_id = 1

@mcp.tool()
def create_task(title: str, priority: str, description: str = "", tags: list[str] | None = None) -> str:
    """Create a new task with title and priority.

    Args:
        title: Task title (required)
        priority: Priority level - low, medium, high, or urgent
        description: Detailed task description
        tags: Optional list of tags for categorization
    """
    global next_id
    task_id = f"task-{next_id}"
    next_id += 1
    tasks[task_id] = {
        "id": task_id, "title": title, "priority": priority,
        "description": description, "status": "todo",
        "tags": tags or [],
    }
    return f'Created task "{title}" (ID: {task_id}, Priority: {priority})'

@mcp.tool()
def list_tasks(status: str | None = None, priority: str | None = None) -> str:
    """List tasks with optional filtering by status or priority."""
    results = list(tasks.values())
    if status:
        results = [t for t in results if t["status"] == status]
    if priority:
        results = [t for t in results if t["priority"] == priority]
    if not results:
        return "No tasks found."
    return "\n".join(f"- [{t['priority']}] {t['title']} ({t['status']}) - {t['id']}" for t in results)

@mcp.tool()
def update_task(task_id: str, status: str | None = None, priority: str | None = None) -> str:
    """Update a task's status or priority by ID."""
    if task_id not in tasks:
        return f"Task {task_id} not found."
    if status:
        tasks[task_id]["status"] = status
    if priority:
        tasks[task_id]["priority"] = priority
    return f'Updated task "{tasks[task_id]["title"]}"'

@mcp.resource("task:///stats")
def task_stats() -> str:
    """Return task statistics as JSON."""
    import json
    return json.dumps({"total": len(tasks)})

if __name__ == "__main__":
    mcp.run()

Connecting to Claude Desktop

Add your server to claude_desktop_config.json. For complete configuration guidance, see the Claude Desktop integration tutorial.

json
{
  "mcpServers": {
    "task-manager": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"],
      "env": {
        "STORAGE_PATH": "/absolute/path/to/tasks.json"
      }
    }
  }
}

Deployment Options

1. Local with Claude Desktop

The simplest option. Configure in claude_desktop_config.json and Claude Desktop manages the process. See Claude integration guide.

2. Docker

dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
ENV STORAGE_PATH=/data/tasks.json
VOLUME ["/data"]
CMD ["node", "dist/index.js"]

3. Streamable HTTP (Remote)

For remote deployment, serve your MCP server over Streamable HTTP instead of stdio. This enables network access from any MCP client. The MCP SDK provides transport helpers for this - consult the SDK documentation for StreamableHTTPServerTransport.

4. npm Package

Publish your server as an npm package so others can use it with npx:

json
{
  "name": "@yourorg/mcp-task-server",
  "bin": { "mcp-task-server": "./dist/index.js" }
}

Users can then add it to Claude Desktop:

json
{
  "mcpServers": {
    "tasks": {
      "command": "npx",
      "args": ["-y", "@yourorg/mcp-task-server"]
    }
  }
}

Submit your server to the MCPGee servers directory to help others discover it.

Best Practices Summary

Architecture

  • Separate MCP protocol handling from business logic
  • Use Zod schemas for input validation
  • Keep tool handlers thin - delegate to service classes
  • Use the McpServer high-level API unless you need low-level control

Error Handling

  • Return user-friendly error messages from tools, not stack traces
  • Log errors to stderr for debugging
  • Handle edge cases: empty inputs, missing IDs, invalid data

Security

  • Validate all inputs with Zod schemas
  • Limit file access to specific directories
  • Never expose raw database connections
  • Use environment variables for configuration

Testing

  • Unit test service and storage layers independently
  • Integration test via JSON-RPC messages piped to the server
  • Test error cases and edge inputs

Performance

  • Lazy-load heavy dependencies
  • Implement caching for expensive operations
  • Keep server startup fast for Claude Desktop restarts

Conclusion

Building a production-ready MCP server requires careful attention to architecture, error handling, and testing. The patterns in this tutorial - layered architecture, Zod validation, service classes, and proper error handling - scale from simple tools to complex enterprise integrations.

Key takeaways:

  • Use McpServer for clean, declarative tool registration
  • Separate business logic from protocol handling for testability
  • Write comprehensive Zod schemas for automatic validation
  • Test at both unit and integration levels
  • Deploy via Claude Desktop config, Docker, or npm package

Browse the servers directory for real-world examples, or continue learning with the Python MCP server guide.

Code Examples

Complete Task Management Server (McpServer API)typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

interface Task {
  id: string;
  title: string;
  description?: string;
  status: string;
  priority: string;
  tags: string[];
  createdAt: string;
  updatedAt: string;
}

const tasks = new Map<string, Task>();
let nextId = 1;

const server = new McpServer({
  name: 'task-management-server',
  version: '1.0.0',
});

server.tool(
  'create_task',
  'Create a new task',
  {
    title: z.string().min(1).max(200),
    priority: z.enum(['low', 'medium', 'high', 'urgent']),
    description: z.string().optional(),
    tags: z.array(z.string()).optional(),
  },
  async ({ title, priority, description, tags }) => {
    const id = `task-${nextId++}`;
    const now = new Date().toISOString();
    tasks.set(id, {
      id, title, description, priority,
      status: 'todo', tags: tags ?? [],
      createdAt: now, updatedAt: now,
    });
    return { content: [{ type: 'text', text: `Created "${title}" (ID: ${id})` }] };
  }
);

server.tool(
  'list_tasks',
  'List tasks with optional filters',
  {
    status: z.string().optional(),
    priority: z.string().optional(),
    search: z.string().optional(),
  },
  async ({ status, priority, search }) => {
    let results = [...tasks.values()];
    if (status) results = results.filter(t => t.status === status);
    if (priority) results = results.filter(t => t.priority === priority);
    if (search) {
      const q = search.toLowerCase();
      results = results.filter(t =>
        t.title.toLowerCase().includes(q) ||
        t.description?.toLowerCase().includes(q)
      );
    }
    const text = results.length
      ? results.map(t => `- [${t.priority}] ${t.title} (${t.status}) - ${t.id}`).join('\n')
      : 'No tasks found.';
    return { content: [{ type: 'text', text }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Unit Test Example with Vitesttypescript
import { describe, it, expect, beforeEach } from 'vitest';
import { TaskStore } from '../src/storage/task.store.js';
import { TaskService } from '../src/services/task.service.js';

describe('TaskService', () => {
  let service: TaskService;

  beforeEach(() => {
    service = new TaskService(new TaskStore());
  });

  it('creates a task with defaults', () => {
    const task = service.createTask({ title: 'Test', priority: 'high' });
    expect(task.id).toMatch(/^task-/);
    expect(task.status).toBe('todo');
  });

  it('filters by status', () => {
    service.createTask({ title: 'A', priority: 'low', status: 'todo' });
    service.createTask({ title: 'B', priority: 'low', status: 'done' });
    const { tasks } = service.listTasks({ status: 'done' });
    expect(tasks).toHaveLength(1);
    expect(tasks[0].title).toBe('B');
  });

  it('searches by keyword', () => {
    service.createTask({ title: 'Fix login', priority: 'urgent' });
    service.createTask({ title: 'Add feature', priority: 'low' });
    const { tasks } = service.listTasks({ search: 'login' });
    expect(tasks).toHaveLength(1);
  });
});
Claude Desktop Configurationjson
{
  "mcpServers": {
    "task-manager": {
      "command": "npx",
      "args": ["tsx", "/Users/you/mcp-task-server/src/index.ts"],
      "env": {
        "STORAGE_PATH": "/Users/you/.mcp-tasks/tasks.json",
        "NODE_ENV": "production"
      }
    }
  }
}
FastMCP Python Equivalentpython
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("task-management-server")

tasks: dict[str, dict] = {}
next_id = 1

@mcp.tool()
def create_task(title: str, priority: str, description: str = "") -> str:
    """Create a new task.

    Args:
        title: Task title
        priority: low, medium, high, or urgent
        description: Optional detailed description
    """
    global next_id
    task_id = f"task-{next_id}"
    next_id += 1
    tasks[task_id] = {
        "id": task_id, "title": title,
        "priority": priority, "description": description,
        "status": "todo", "tags": [],
    }
    return f'Created "{title}" (ID: {task_id})'

@mcp.tool()
def list_tasks(status: str | None = None) -> str:
    """List all tasks, optionally filtered by status."""
    results = list(tasks.values())
    if status:
        results = [t for t in results if t["status"] == status]
    if not results:
        return "No tasks found."
    return "\n".join(
        f"- [{t['priority']}] {t['title']} ({t['status']})" for t in results
    )

if __name__ == "__main__":
    mcp.run()

Key Takeaways

  • Use McpServer from @modelcontextprotocol/sdk/server/mcp.js for clean, declarative server code
  • Separate MCP protocol handling from business logic for testability and maintainability
  • Zod schemas provide both compile-time TypeScript types and runtime validation for tool inputs
  • Test service and storage layers independently with unit tests, then integration test the full server
  • Deploy via Claude Desktop config for local use, Docker or npm for distribution
  • FastMCP provides an equivalent high-level experience for Python developers

Troubleshooting

Server crashes with 'Cannot find module' errors

Run npm install to ensure all dependencies are installed. Check that package.json has "type": "module" for ESM support. When importing from the MCP SDK, use .js extensions in import paths even for TypeScript files.

Zod validation errors when calling tools

Check that your Zod schemas match the arguments Claude is sending. Use .optional() for fields that may not always be provided. Add .default() values where appropriate. Log the raw arguments to stderr for debugging.

Data not persisting between server restarts

Set the STORAGE_PATH environment variable to a file path in your Claude Desktop configuration. Ensure the directory exists and is writable. Without STORAGE_PATH, the server uses in-memory storage that resets on restart.

High memory usage over time

If storing data in memory, implement limits on the number of tasks. Consider using file-based storage for large datasets. Clear old completed tasks periodically with a cleanup tool.

TypeScript compilation errors with MCP SDK imports

Use TypeScript 5.0+ and set moduleResolution to "node" in tsconfig.json. Always use .js extensions in MCP SDK import paths. Run with tsx for development to skip the compilation step.

Next Steps

  • Connect your server to Claude Desktop with the integration guide
  • Build a Python version following the Python MCP server blog post
  • Explore the servers directory for production examples and inspiration
  • Add authentication and security to your server

Was this helpful?

Share tutorial:

Stay Updated with MCP Insights

Join 5,000+ developers and get weekly insights on MCP development, new server releases, and implementation strategies delivered to your inbox.

We respect your privacy. Unsubscribe at any time.

MCPgee Team

We write in-depth guides, tutorials, and reviews to help developers get the most out of the Model Context Protocol ecosystem.

Frequently Asked Questions

Explore MCP Servers

Browse our directory of 33,000+ MCP servers. Find the perfect tools for your AI-powered workflows.