DEV Community

Cover image for Complete Tutorial: Creating an MCP Task Manager Server from Scratch
Fairen
Fairen

Posted on

Complete Tutorial: Creating an MCP Task Manager Server from Scratch

πŸ“– Table of Contents

  1. Introduction to Model Context Protocol (MCP)
  2. Prerequisites and Installation
  3. Project Structure
  4. Basic Configuration
  5. MCP Server Implementation
  6. Endpoints: Tools
  7. Endpoints: Resources
  8. Endpoints: Prompts
  9. Error Handling
  10. Main Server
  11. Code Organization and Patterns
  12. Testing and Validation
  13. Docker Deployment
  14. Additional Resources

1. Introduction to Model Context Protocol (MCP)

What is MCP?

The Model Context Protocol (MCP) is a standardized communication protocol that allows Large Language Models (LLMs) to interact with external systems in a secure and structured manner. It uses JSON-RPC 2.0 for communication.

Main Components

  • Tools: Actions that the LLM can execute (create, modify, delete)
  • Resources: Data that the LLM can read (files, databases)
  • Prompts: Predefined templates to guide interactions

MCP Session Lifecycle

sequenceDiagram
    Client->>Server: initialize
    Server->>Client: capabilities
    Client->>Server: tools/list
    Client->>Server: resources/list
    Client->>Server: prompts/list
    Client->>Server: tools/call | resources/read | prompts/get
Enter fullscreen mode Exit fullscreen mode

2. Prerequisites and Installation

Required Technologies

  • Node.js 18+ with TypeScript
  • @modelcontextprotocol/sdk for MCP implementation
  • Zod for schema validation
  • Database (SQLite for this tutorial)

Installation

# Create the project
mkdir mcp-task-manager
cd mcp-task-manager

# Initialize Node.js
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod sqlite3
npm install -D typescript @types/node ts-node nodemon

# TypeScript configuration
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

3. Project Structure

mcp-task-manager/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server.ts           # MCP server entry point
β”‚   β”œβ”€β”€ database/
β”‚   β”‚   β”œβ”€β”€ db.ts          # Database configuration
β”‚   β”‚   └── schema.sql     # Table schema
β”‚   β”œβ”€β”€ tools/
β”‚   β”‚   β”œβ”€β”€ index.ts       # Tools export
β”‚   β”‚   β”œβ”€β”€ create-task.ts
β”‚   β”‚   β”œβ”€β”€ update-task.ts
β”‚   β”‚   β”œβ”€β”€ complete-task.ts
β”‚   β”‚   └── delete-task.ts
β”‚   β”œβ”€β”€ resources/
β”‚   β”‚   β”œβ”€β”€ index.ts       # Resources export
β”‚   β”‚   β”œβ”€β”€ task-list.ts
β”‚   β”‚   β”œβ”€β”€ task-stats.ts
β”‚   β”‚   └── task-detail.ts
β”‚   β”œβ”€β”€ prompts/
β”‚   β”‚   β”œβ”€β”€ index.ts       # Prompts export
β”‚   β”‚   β”œβ”€β”€ weekly-review.ts
β”‚   β”‚   └── sprint-planning.ts
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   └── task.ts        # TypeScript types
β”‚   └── utils/
β”‚       └── errors.ts      # Error handling
β”œβ”€β”€ Dockerfile             # Multi-stage Docker configuration
β”œβ”€β”€ docker-compose.yml     # Docker orchestration
β”œβ”€β”€ .dockerignore          # Files to ignore for Docker
β”œβ”€β”€ .env.example           # Environment variables example
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
└── README.md
Enter fullscreen mode Exit fullscreen mode

4. Basic Configuration

package.json

{
  "name": "mcp-task-manager",
  "version": "1.0.0",
  "description": "MCP server for task management",
  "main": "dist/server.js",
  "scripts": {
    "build": "tsc && mkdir -p dist/database && cp src/database/schema.sql dist/database/",
    "start": "node dist/server.js",
    "dev": "nodemon src/server.ts",
    "test": "jest",
    "docker:build": "docker build -t task-manager-mcp-server .",
    "docker:run": "docker run -d --name task-manager-mcp-server -p 3000:3000 task-manager-mcp-server",
    "docker:stop": "docker stop task-manager-mcp-server && docker rm task-manager-mcp-server",
    "docker:logs": "docker logs task-manager-mcp-server"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.21.0",
    "sqlite3": "^5.1.7",
    "zod": "^3.25.76"
  },
  "devDependencies": {
    "@types/node": "^24.10.0",
    "nodemon": "^3.1.10",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    "outDir": "./dist",
    "rootDir": "./src",

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "commonjs",
    "target": "ES2022",
    "esModuleInterop": true,
    // For nodejs:
    "lib": ["ES2022"],
    "types": ["node"],
    // and npm install -D @types/node

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,

    // Recommended Options
    "strict": true,
    "skipLibCheck": true,
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

5. MCP Server Implementation

Basic Types (src/types/task.ts)

The system uses Zod for validation and TypeScript type generation:

import { z } from 'zod';

// Main schema with complete validation
export const TaskSchema = z.object({
  id: z.string().optional(),                           // Auto-generated
  title: z.string().min(1).max(200),                  // Required title (1-200 chars)
  description: z.string().optional(),                  // Optional description
  priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
  status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).default('pending'),
  due_date: z.string().datetime().optional(),         // ISO 8601 format
  tags: z.array(z.string()).default([]),              // Array of strings
  assignee: z.string().optional(),                    // Assigned user
  created_at: z.string().datetime().optional(),       // Creation timestamp
  updated_at: z.string().datetime().optional(),       // Modification timestamp
  completed_at: z.string().datetime().optional()      // Completion timestamp
});

// TypeScript type automatically inferred
export type Task = z.infer<typeof TaskSchema>;

// Derived schemas for CRUD operations
export const CreateTaskSchema = TaskSchema.omit({ 
  id: true, 
  created_at: true, 
  updated_at: true, 
  completed_at: true 
});

export const UpdateTaskSchema = TaskSchema.partial().extend({
  id: z.string()                                      // Required ID for updates
});

export const CompleteTaskSchema = z.object({
  task_id: z.string(),
  completion_notes: z.string().optional(),
  time_spent: z.number().min(0).optional()           // Time in minutes
});
Enter fullscreen mode Exit fullscreen mode

Advantages of this approach:

  1. Type Safety: Automatic runtime validation
  2. Living documentation: Schemas serve as reference
  3. Reusability: One schema, multiple uses (API, DB, validation)
  4. Explicit errors: Clear error messages for clients

Validation example:

// βœ… Valid
const task = CreateTaskSchema.parse({
  title: "New task",
  priority: "high",
  tags: ["urgent", "feature"]
});

// ❌ Validation error
const invalid = CreateTaskSchema.parse({
  title: "",  // Error: minimum 1 character
  priority: "invalid"  // Error: unauthorized value
});
Enter fullscreen mode Exit fullscreen mode

Database Configuration (src/database/db.ts)

import sqlite3 from 'sqlite3';
import path from 'path';
import fs from 'fs';
import { Task } from '../types/task';

export class TaskDatabase {
  private db: sqlite3.Database;

  constructor(dbPath: string = './tasks.db') {
    this.db = new sqlite3.Database(dbPath);
    this.initializeDatabase();
  }

  private async initializeDatabase() {
    const schemaPath = path.join(__dirname, 'schema.sql');
    const schema = fs.readFileSync(schemaPath, 'utf8');

    return new Promise<void>((resolve, reject) => {
      this.db.exec(schema, (err) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }

  async createTask(task: Omit<Task, 'id' | 'created_at' | 'updated_at'>): Promise<string> {
    const sql = `
      INSERT INTO tasks (title, description, priority, status, due_date, tags, assignee)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `;

    return new Promise((resolve, reject) => {
      this.db.run(sql, [
        task.title,
        task.description || null,
        task.priority,
        task.status || 'pending',
        task.due_date || null,
        JSON.stringify(task.tags || []),
        task.assignee || null
      ], function(err) {
        if (err) reject(err);
        else resolve(`task-${this.lastID}`);
      });
    });
  }

  async getTask(id: string): Promise<Task | null> {
    const sql = 'SELECT * FROM tasks WHERE id = ?';

    return new Promise((resolve, reject) => {
      this.db.get(sql, [id.replace('task-', '')], (err, row: any) => {
        if (err) reject(err);
        else if (!row) resolve(null);
        else {
          resolve({
            id: `task-${row.id}`,
            title: row.title,
            description: row.description,
            priority: row.priority,
            status: row.status,
            due_date: row.due_date,
            tags: JSON.parse(row.tags || '[]'),
            assignee: row.assignee,
            created_at: row.created_at,
            updated_at: row.updated_at,
            completed_at: row.completed_at
          });
        }
      });
    });
  }

  async getAllTasks(): Promise<Task[]> {
    const sql = 'SELECT * FROM tasks ORDER BY created_at DESC';

    return new Promise((resolve, reject) => {
      this.db.all(sql, [], (err, rows: any[]) => {
        if (err) reject(err);
        else {
          const tasks = rows.map(row => ({
            id: `task-${row.id}`,
            title: row.title,
            description: row.description,
            priority: row.priority,
            status: row.status,
            due_date: row.due_date,
            tags: JSON.parse(row.tags || '[]'),
            assignee: row.assignee,
            created_at: row.created_at,
            updated_at: row.updated_at,
            completed_at: row.completed_at
          }));
          resolve(tasks);
        }
      });
    });
  }

  async updateTask(id: string, updates: Partial<Task>): Promise<boolean> {
    const fields = [];
    const values: string[] = [];

    Object.entries(updates).forEach(([key, value]) => {
      if (key !== 'id' && value !== undefined) {
        fields.push(`${key} = ?`);
        values.push(key === 'tags' ? JSON.stringify(value) : String(value));
      }
    });

    if (fields.length === 0) return false;

    fields.push('updated_at = CURRENT_TIMESTAMP');
    const sql = `UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`;
    values.push(id.replace('task-', ''));

    return new Promise((resolve, reject) => {
      this.db.run(sql, values, function(err) {
        if (err) reject(err);
        else resolve(this.changes > 0);
      });
    });
  }

  async completeTask(id: string, notes?: string, timeSpent?: number): Promise<boolean> {
    const sql = `
      UPDATE tasks 
      SET status = 'completed', 
          completed_at = CURRENT_TIMESTAMP,
          completion_notes = ?,
          time_spent = ?,
          updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `;

    return new Promise((resolve, reject) => {
      this.db.run(sql, [notes || null, timeSpent || null, id.replace('task-', '')], function(err) {
        if (err) reject(err);
        else resolve(this.changes > 0);
      });
    });
  }

  async deleteTask(id: string): Promise<boolean> {
    const sql = 'DELETE FROM tasks WHERE id = ?';

    return new Promise((resolve, reject) => {
      this.db.run(sql, [id.replace('task-', '')], function(err) {
        if (err) reject(err);
        else resolve(this.changes > 0);
      });
    });
  }

  async getStats(): Promise<any> {
    const sql = `
      SELECT 
        COUNT(*) as total_tasks,
        SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
        SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
        SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
        SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
        SUM(CASE WHEN priority = 'urgent' THEN 1 ELSE 0 END) as urgent,
        SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
        SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
        SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
        AVG(time_spent) as avg_completion_time
      FROM tasks
    `;

    return new Promise((resolve, reject) => {
      this.db.get(sql, [], (err, row: any) => {
        if (err) reject(err);
        else resolve(row);
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Database Schema (src/database/schema.sql)

CREATE TABLE IF NOT EXISTS tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT,
  priority TEXT DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
  due_date TEXT,
  tags TEXT DEFAULT '[]',
  assignee TEXT,
  completion_notes TEXT,
  time_spent INTEGER,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  completed_at DATETIME
);

CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
Enter fullscreen mode Exit fullscreen mode

6. Endpoints: Tools

Task Creation (src/tools/create-task.ts)

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';
import { CreateTaskSchema } from '../types/task.js';
import { McpError, ErrorCode } from '../utils/errors.js';

export const createTaskTool: Tool = {
  name: 'create_task',
  description: 'Create a new task in the system',
  inputSchema: {
    type: 'object',
    properties: {
      title: {
        type: 'string',
        description: 'Task title',
        minLength: 1,
        maxLength: 200
      },
      description: {
        type: 'string',
        description: 'Detailed task description'
      },
      priority: {
        type: 'string',
        enum: ['low', 'medium', 'high', 'urgent'],
        default: 'medium',
        description: 'Priority level'
      },
      due_date: {
        type: 'string',
        format: 'date-time',
        description: 'Due date (ISO 8601)'
      },
      tags: {
        type: 'array',
        items: { type: 'string' },
        description: 'List of associated tags'
      },
      assignee: {
        type: 'string',
        description: 'Person assigned to the task'
      }
    },
    required: ['title'],
    additionalProperties: false
  }
};

export async function handleCreateTask(args: any, db: TaskDatabase) {
  try {
    // Argument validation
    const validatedArgs = CreateTaskSchema.parse(args);

    // Task creation
    const taskId = await db.createTask(validatedArgs);

    return {
      content: [{
        type: 'text',
        text: 'Task created successfully'
      }],
      task_id: taskId,
      created_at: new Date().toISOString(),
      status: 'pending'
    };
  } catch (error) {
    if (error instanceof Error) {
      throw new McpError(ErrorCode.INVALID_PARAMS, `Validation error: ${error.message}`);
    }
    throw new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error during creation');
  }
}
Enter fullscreen mode Exit fullscreen mode

Task Update (src/tools/update-task.ts)

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';
import { UpdateTaskSchema } from '../types/task.js';
import { McpError, ErrorCode } from '../utils/errors.js';

export const updateTaskTool: Tool = {
  name: 'update_task',
  description: 'Update an existing task',
  inputSchema: {
    type: 'object',
    properties: {
      task_id: {
        type: 'string',
        description: 'Task identifier to modify'
      },
      title: {
        type: 'string',
        minLength: 1,
        maxLength: 200
      },
      description: { type: 'string' },
      priority: {
        type: 'string',
        enum: ['low', 'medium', 'high', 'urgent']
      },
      status: {
        type: 'string',
        enum: ['pending', 'in_progress', 'completed', 'cancelled']
      },
      due_date: {
        type: 'string',
        format: 'date-time'
      },
      tags: {
        type: 'array',
        items: { type: 'string' }
      },
      assignee: { type: 'string' }
    },
    required: ['task_id'],
    additionalProperties: false
  }
};

export async function handleUpdateTask(args: any, db: TaskDatabase) {
  try {
    const { task_id, ...updates } = args;

    // Check if task exists
    const existingTask = await db.getTask(task_id);
    if (!existingTask) {
      throw new McpError(ErrorCode.TASK_NOT_FOUND, `Task ${task_id} not found`);
    }

    // Check if task is already completed
    if (existingTask.status === 'completed') {
      throw new McpError(ErrorCode.TASK_ALREADY_COMPLETED, 'Cannot modify a completed task');
    }

    // Update
    const success = await db.updateTask(task_id, updates);

    if (success) {
      const updatedTask = await db.getTask(task_id);
      return {
        content: [{
          type: 'text',
          text: 'Task updated successfully'
        }],
        task: updatedTask
      };
    } else {
      throw new McpError(ErrorCode.INTERNAL_ERROR, 'Update failed');
    }
  } catch (error) {
    if (error instanceof McpError) throw error;
    throw new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error during update');
  }
}
Enter fullscreen mode Exit fullscreen mode

Task Completion (src/tools/complete-task.ts)

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';
import { CompleteTaskSchema } from '../types/task.js';
import { McpError, ErrorCode } from '../utils/errors.js';

export const completeTaskTool: Tool = {
  name: 'complete_task',
  description: 'Mark a task as completed',
  inputSchema: {
    type: 'object',
    properties: {
      task_id: {
        type: 'string',
        description: 'Task identifier to complete'
      },
      completion_notes: {
        type: 'string',
        description: 'Notes about task completion'
      },
      time_spent: {
        type: 'number',
        minimum: 0,
        description: 'Time spent in minutes'
      }
    },
    required: ['task_id'],
    additionalProperties: false
  }
};

export async function handleCompleteTask(args: any, db: TaskDatabase) {
  try {
    const validatedArgs = CompleteTaskSchema.parse(args);

    // Check if task exists
    const existingTask = await db.getTask(validatedArgs.task_id);
    if (!existingTask) {
      throw new McpError(ErrorCode.TASK_NOT_FOUND, `Task ${validatedArgs.task_id} not found`);
    }

    // Check if task is already completed
    if (existingTask.status === 'completed') {
      throw new McpError(ErrorCode.TASK_ALREADY_COMPLETED, 'Task already completed');
    }

    // Complete the task
    const success = await db.completeTask(
      validatedArgs.task_id,
      validatedArgs.completion_notes,
      validatedArgs.time_spent
    );

    if (success) {
      return {
        content: [{
          type: 'text',
          text: 'Task completed successfully'
        }],
        completed_at: new Date().toISOString(),
        total_time: validatedArgs.time_spent
      };
    } else {
      throw new McpError(ErrorCode.INTERNAL_ERROR, 'Completion failed');
    }
  } catch (error) {
    if (error instanceof McpError) throw error;
    throw new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error during completion');
  }
}
Enter fullscreen mode Exit fullscreen mode

Task Deletion (src/tools/delete-task.ts)

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';
import { McpError, ErrorCode } from '../utils/errors.js';

export const deleteTaskTool: Tool = {
  name: 'delete_task',
  description: 'Permanently delete a task',
  inputSchema: {
    type: 'object',
    properties: {
      task_id: {
        type: 'string',
        description: 'Task identifier to delete'
      },
      confirm: {
        type: 'boolean',
        description: 'Deletion confirmation (security)',
        default: false
      }
    },
    required: ['task_id', 'confirm'],
    additionalProperties: false
  }
};

export async function handleDeleteTask(args: any, db: TaskDatabase) {
  try {
    const { task_id, confirm } = args;

    // Check confirmation
    if (!confirm) {
      throw new McpError(ErrorCode.INVALID_PARAMS, 'Confirmation required to delete a task');
    }

    // Check if task exists
    const existingTask = await db.getTask(task_id);
    if (!existingTask) {
      throw new McpError(ErrorCode.TASK_NOT_FOUND, `Task ${task_id} not found`);
    }

    // Save information before deletion
    const taskInfo = {
      id: existingTask.id,
      title: existingTask.title,
      status: existingTask.status
    };

    // Delete the task
    const success = await db.deleteTask(task_id);

    if (success) {
      return {
        content: [{
          type: 'text',
          text: `Task "${taskInfo.title}" deleted successfully`
        }],
        deleted_task: taskInfo,
        deleted_at: new Date().toISOString()
      };
    } else {
      throw new McpError(ErrorCode.INTERNAL_ERROR, 'Deletion failed');
    }
  } catch (error) {
    if (error instanceof McpError) throw error;
    throw new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error during deletion');
  }
}
Enter fullscreen mode Exit fullscreen mode

Tools Export (src/tools/index.ts)

import { createTaskTool, handleCreateTask } from './create-task.js';
import { updateTaskTool, handleUpdateTask } from './update-task.js';
import { completeTaskTool, handleCompleteTask } from './complete-task.js';
import { deleteTaskTool, handleDeleteTask } from './delete-task.js';

export const tools = [
  createTaskTool,
  updateTaskTool,
  completeTaskTool,
  deleteTaskTool
];

export const toolHandlers = {
  create_task: handleCreateTask,
  update_task: handleUpdateTask,
  complete_task: handleCompleteTask,
  delete_task: handleDeleteTask
};
Enter fullscreen mode Exit fullscreen mode

7. Endpoints: Resources

Task List (src/resources/task-list.ts)

import { Resource } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';

export const taskListResource: Resource = {
  uri: 'task://list',
  name: 'Task List',
  description: 'All active tasks in the system',
  mimeType: 'application/json'
};

export async function handleTaskListResource(db: TaskDatabase) {
  try {
    const tasks = await db.getAllTasks();

    return {
      contents: [{
        uri: 'task://list',
        mimeType: 'application/json',
        text: JSON.stringify({
          tasks: tasks.map(task => ({
            id: task.id,
            title: task.title,
            status: task.status,
            priority: task.priority,
            assignee: task.assignee,
            due_date: task.due_date,
            created_at: task.created_at
          })),
          total: tasks.length
        }, null, 2)
      }]
    };
  } catch (error) {
    throw new Error('Error retrieving tasks');
  }
}
Enter fullscreen mode Exit fullscreen mode

Statistics (src/resources/task-stats.ts)

import { Resource } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';

export const taskStatsResource: Resource = {
  uri: 'task://stats',
  name: 'Task Statistics',
  description: 'Global statistics of the task system',
  mimeType: 'application/json'
};

export async function handleTaskStatsResource(db: TaskDatabase) {
  try {
    const stats = await db.getStats();
    const tasks = await db.getAllTasks();

    // Calculate overdue tasks
    const now = new Date();
    const overdueTasks = tasks.filter(task => 
      task.due_date && 
      new Date(task.due_date) < now && 
      task.status !== 'completed'
    ).length;

    // Calculate completion rate
    const completionRate = stats.total_tasks > 0 
      ? (stats.completed / stats.total_tasks) * 100 
      : 0;

    const statsData = {
      total_tasks: stats.total_tasks,
      by_status: {
        pending: stats.pending,
        in_progress: stats.in_progress,
        completed: stats.completed,
        cancelled: stats.cancelled
      },
      by_priority: {
        urgent: stats.urgent,
        high: stats.high,
        medium: stats.medium,
        low: stats.low
      },
      overdue_tasks: overdueTasks,
      completion_rate: Math.round(completionRate * 10) / 10,
      average_completion_time: stats.avg_completion_time || 0
    };

    return {
      contents: [{
        uri: 'task://stats',
        mimeType: 'application/json',
        text: JSON.stringify(statsData, null, 2)
      }]
    };
  } catch (error) {
    throw new Error('Error retrieving statistics');
  }
}
Enter fullscreen mode Exit fullscreen mode

Task Detail (src/resources/task-detail.ts)

import { Resource } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';
import { McpError, ErrorCode } from '../utils/errors.js';

export const taskDetailResource: Resource = {
  uri: 'task://detail/{id}',
  name: 'Task Detail',
  description: 'Detailed information of a specific task',
  mimeType: 'application/json'
};

export async function handleTaskDetailResource(uri: string, db: TaskDatabase) {
  try {
    // Extract ID from URI
    const match = uri.match(/task:\/\/detail\/(.+)/);
    if (!match) {
      throw new McpError(ErrorCode.INVALID_PARAMS, 'Invalid URI for task detail');
    }

    const taskId = match[1];
    if (!taskId) {
      throw new McpError(ErrorCode.INVALID_PARAMS, 'Missing task ID in URI');
    }

    const task = await db.getTask(taskId);

    if (!task) {
      throw new McpError(ErrorCode.TASK_NOT_FOUND, `Task ${taskId} not found`);
    }

    return {
      contents: [{
        uri: uri,
        mimeType: 'application/json',
        text: JSON.stringify(task, null, 2)
      }]
    };
  } catch (error) {
    if (error instanceof McpError) throw error;
    throw new Error('Error retrieving task detail');
  }
}
Enter fullscreen mode Exit fullscreen mode

Resources Export (src/resources/index.ts)

import { taskListResource, handleTaskListResource } from './task-list.js';
import { taskStatsResource, handleTaskStatsResource } from './task-stats.js';
import { taskDetailResource, handleTaskDetailResource } from './task-detail.js';

export const resources = [
  taskListResource,
  taskStatsResource,
  taskDetailResource
];

export const resourceHandlers = {
  taskList: handleTaskListResource,
  taskStats: handleTaskStatsResource,
  taskDetail: handleTaskDetailResource
};
Enter fullscreen mode Exit fullscreen mode

8. Endpoints: Prompts

Weekly Review (src/prompts/weekly-review.ts)

import { Prompt } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';

export const weeklyReviewPrompt: Prompt = {
  name: 'weekly_review',
  description: 'Generate a weekly review report',
  arguments: [
    {
      name: 'week_number',
      description: 'Week number to analyze',
      required: false
    },
    {
      name: 'team',
      description: 'Filter by specific team',
      required: false
    }
  ]
};

export async function handleWeeklyReviewPrompt(args: any, db: TaskDatabase) {
  try {
    const weekNumber = args.week_number || getCurrentWeekNumber();
    const team = args.team || 'all teams';

    // Retrieve data
    const tasks = await db.getAllTasks();
    const stats = await db.getStats();

    // Filter by team if specified
    const filteredTasks = args.team 
      ? tasks.filter(task => task.assignee?.includes(args.team))
      : tasks;

    const description = `Weekly review ${team} - Week ${weekNumber}`;

    return {
      description,
      messages: [
        {
          role: 'user',
          content: {
            type: 'text',
            text: `Generate a detailed review for week ${weekNumber} of ${team} with:

πŸ“Š **Available Data:**
- Total tasks: ${filteredTasks.length}
- Completed tasks: ${filteredTasks.filter(t => t.status === 'completed').length}
- In progress tasks: ${filteredTasks.filter(t => t.status === 'in_progress').length}
- Pending tasks: ${filteredTasks.filter(t => t.status === 'pending').length}

πŸ“‹ **Requested Analysis:**
1. **Tasks completed this week**
2. **Tasks in progress with their progression**
3. **Identified blockers and solutions**
4. **Performance metrics**
5. **Recommendations for next week**

Use a structured format with emojis and clear sections.`
          }
        },
        {
          role: 'assistant',
          content: {
            type: 'text',
            text: `I will analyze the data for week ${weekNumber} for ${team} and generate a comprehensive report...`
          }
        }
      ]
    };
  } catch (error) {
    throw new Error('Error generating weekly review prompt');
  }
}

function getCurrentWeekNumber(): number {
  const now = new Date();
  const start = new Date(now.getFullYear(), 0, 1);
  const days = Math.floor((now.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
  return Math.ceil((days + start.getDay() + 1) / 7);
}
Enter fullscreen mode Exit fullscreen mode

Sprint Planning (src/prompts/sprint-planning.ts)

import { Prompt } from '@modelcontextprotocol/sdk/types.js';
import { TaskDatabase } from '../database/db.js';

export const sprintPlanningPrompt: Prompt = {
  name: 'sprint_planning',
  description: 'Assist with sprint planning with velocity analysis',
  arguments: [
    {
      name: 'sprint_duration',
      description: 'Sprint duration in days (default: 14)',
      required: false
    },
    {
      name: 'team_capacity',
      description: 'Team capacity in person-days',
      required: false
    }
  ]
};

export async function handleSprintPlanningPrompt(args: any, db: TaskDatabase) {
  try {
    const sprintDuration = args.sprint_duration || 14;
    const teamCapacity = args.team_capacity;

    const tasks = await db.getAllTasks();
    const stats = await db.getStats();

    // Analyze priority tasks
    const highPriorityTasks = tasks.filter(t => 
      ['urgent', 'high'].includes(t.priority) && 
      t.status === 'pending'
    );

    // Calculate average velocity
    const completedTasks = tasks.filter(t => t.status === 'completed');
    const avgCompletionTime = stats.avg_completion_time || 0;

    return {
      description: `Sprint planning - ${sprintDuration} days`,
      messages: [
        {
          role: 'user',
          content: {
            type: 'text',
            text: `Help me plan the next sprint with this data:

πŸƒβ€β™‚οΈ **Sprint Parameters:**
- Duration: ${sprintDuration} days
- Team capacity: ${teamCapacity || 'not specified'}

πŸ“Š **Task Analysis:**
- Priority tasks (urgent/high): ${highPriorityTasks.length}
- Pending tasks: ${tasks.filter(t => t.status === 'pending').length}
- Average completion time: ${avgCompletionTime} minutes
- Current completion rate: ${((stats.completed / stats.total_tasks) * 100).toFixed(1)}%

🎯 **Request:**
1. **Suggest optimal task distribution**
2. **Identify critical dependencies**
3. **Propose realistic timeline**
4. **Anticipate potential risks**
5. **Recommend tracking metrics**

Generate a detailed plan with clear milestones.`
          }
        },
        {
          role: 'assistant',
          content: {
            type: 'text',
            text: `Let's analyze the data to create an optimal sprint plan...`
          }
        }
      ]
    };
  } catch (error) {
    throw new Error('Error generating sprint planning prompt');
  }
}
Enter fullscreen mode Exit fullscreen mode

Prompts Export (src/prompts/index.ts)

import { weeklyReviewPrompt, handleWeeklyReviewPrompt } from './weekly-review.js';
import { sprintPlanningPrompt, handleSprintPlanningPrompt } from './sprint-planning.js';

export const prompts = [
  weeklyReviewPrompt,
  sprintPlanningPrompt
];

export const promptHandlers = {
  weekly_review: handleWeeklyReviewPrompt,
  sprint_planning: handleSprintPlanningPrompt
};
Enter fullscreen mode Exit fullscreen mode

9. Error Handling

Error Codes (src/utils/errors.ts)

export enum ErrorCode {
  // Standard JSON-RPC errors
  PARSE_ERROR = -32700,
  INVALID_REQUEST = -32600,
  METHOD_NOT_FOUND = -32601,
  INVALID_PARAMS = -32602,
  INTERNAL_ERROR = -32603,

  // Task Manager specific errors
  TASK_NOT_FOUND = -32000,
  PERMISSION_DENIED = -32001,
  TASK_ALREADY_COMPLETED = -32002,
  INVALID_DATE_FORMAT = -32003,
  QUOTA_EXCEEDED = -32004
}

export class McpError extends Error {
  constructor(
    public code: ErrorCode,
    message: string,
    public data?: any
  ) {
    super(message);
    this.name = 'McpError';
  }

  toJsonRpc() {
    return {
      code: this.code,
      message: this.message,
      ...(this.data && { data: this.data })
    };
  }
}

export function getErrorMessage(code: ErrorCode): string {
  switch (code) {
    case ErrorCode.TASK_NOT_FOUND:
      return 'Task not found';
    case ErrorCode.PERMISSION_DENIED:
      return 'Permission denied';
    case ErrorCode.TASK_ALREADY_COMPLETED:
      return 'Task already completed';
    case ErrorCode.INVALID_DATE_FORMAT:
      return 'Invalid date format';
    case ErrorCode.QUOTA_EXCEEDED:
      return 'Quota exceeded';
    default:
      return 'Internal error';
  }
}
Enter fullscreen mode Exit fullscreen mode

10. Main Server

Hybrid HTTP/STDIO Architecture

The TaskManagerServer supports two communication modes:

1. HTTP Mode (Recommended)

private createHttpServer() {
  return http.createServer(async (req, res) => {
    // CORS configuration for web integration
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

    // Handle OPTIONS requests (CORS preflight)
    if (req.method === 'OPTIONS') {
      res.writeHead(200);
      res.end();
      return;
    }

    // Health check endpoint
    if (req.method === 'GET' && req.url === '/health') {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ 
        status: 'healthy', 
        timestamp: new Date().toISOString() 
      }));
      return;
    }

    // Main MCP endpoint
    if (req.method === 'POST' && req.url === '/mcp') {
      // JSON-RPC request processing
      // ...
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

HTTP Mode Advantages:

  • βœ… Simplified integration with Claude Desktop
  • βœ… Easy debugging with curl, Postman, etc.
  • βœ… Health check for monitoring
  • βœ… CORS enabled for web applications
  • βœ… Scalability for multiple clients

2. STDIO Mode (Legacy)

if (process.env.MCP_TRANSPORT === 'stdio') {
  const transport = new StdioServerTransport();
  await this.server.connect(transport);
  console.error('Task Manager MCP Server started (stdio)');
}
Enter fullscreen mode Exit fullscreen mode

MCP Request Handling

The server implements a JSON-RPC translation layer:

private async handleMcpRequest(request: any) {
  const { method, params } = request;

  switch (method) {
    case 'initialize':
      return {
        jsonrpc: '2.0',
        id: request.id,
        result: {
          protocolVersion: '1.0',
          serverInfo: { name: 'task-manager-server', version: '1.0.0' },
          capabilities: { tools: {}, resources: {}, prompts: {} }
        }
      };

    case 'tools/list':
      return { jsonrpc: '2.0', id: request.id, result: { tools } };

    case 'tools/call':
      // Validation and tool execution
      const { name, arguments: args } = params;
      if (!toolHandlers[name as keyof typeof toolHandlers]) {
        throw new McpError(ErrorCode.METHOD_NOT_FOUND, `Tool ${name} not found`);
      }
      const result = await toolHandlers[name as keyof typeof toolHandlers](args, this.database);
      return { jsonrpc: '2.0', id: request.id, result };

    // Other methods...
  }
}
Enter fullscreen mode Exit fullscreen mode

Centralized Error Handling

try {
  const request = JSON.parse(body);
  const response = await this.handleMcpRequest(request);

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(response));
} catch (error) {
  res.writeHead(500, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ 
    error: 'Internal server error', 
    message: error instanceof Error ? error.message : 'Unknown error' 
  }));
}
Enter fullscreen mode Exit fullscreen mode

Main Entry Point (src/server.ts)

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { 
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import * as http from 'http';
import { TaskDatabase } from './database/db.js';
import { tools, toolHandlers } from './tools/index.js';
import { resources, resourceHandlers } from './resources/index.js';
import { prompts, promptHandlers } from './prompts/index.js';
import { McpError, ErrorCode } from './utils/errors.js';

class TaskManagerServer {
  private server: Server;
  private database: TaskDatabase;
  private httpServer?: http.Server;

  constructor() {
    this.database = new TaskDatabase();
    this.server = new Server(
      {
        name: 'task-manager-server',
        version: '1.0.0'
      },
      {
        capabilities: {
          tools: {},
          resources: {},
          prompts: {}
        }
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // Handler for tools with MCP schemas
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      if (!toolHandlers[name as keyof typeof toolHandlers]) {
        throw new McpError(ErrorCode.METHOD_NOT_FOUND, `Tool ${name} not found`);
      }

      try {
        return await toolHandlers[name as keyof typeof toolHandlers](args, this.database);
      } catch (error) {
        if (error instanceof McpError) {
          throw error;
        }
        throw new McpError(ErrorCode.INTERNAL_ERROR, 'Internal server error');
      }
    });

    // Handler for resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
      resources
    }));

    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const { uri } = request.params;

      // Route to appropriate handler based on URI
      if (uri === 'task://list') {
        return await resourceHandlers.taskList(this.database);
      } else if (uri === 'task://stats') {
        return await resourceHandlers.taskStats(this.database);
      } else if (uri.startsWith('task://detail/')) {
        return await resourceHandlers.taskDetail(uri, this.database);
      } else {
        throw new McpError(ErrorCode.METHOD_NOT_FOUND, `Resource ${uri} not found`);
      }
    });

    // Handler for prompts
    this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
      prompts
    }));

    this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      if (!promptHandlers[name as keyof typeof promptHandlers]) {
        throw new McpError(ErrorCode.METHOD_NOT_FOUND, `Prompt ${name} not found`);
      }

      return await promptHandlers[name as keyof typeof promptHandlers](args || {}, this.database);
    });
  }

  async start() {
    const port = process.env.PORT || 3000;

    if (process.env.MCP_TRANSPORT === 'stdio') {
      const transport = new StdioServerTransport();
      await this.server.connect(transport);
      console.error('Task Manager MCP Server started (stdio)');
    } else {
      this.httpServer = this.createHttpServer();
      this.httpServer.listen(port, () => {
        console.error(`Task Manager MCP Server started on port ${port} (HTTP)`);
      });
    }
  }

  async stop() {
    if (this.httpServer) {
      this.httpServer.close();
    }
  }
}

// Start the server
const server = new TaskManagerServer();
server.start().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Advanced Error Handling Pattern

// src/utils/error-handling.ts
export class TaskManagerError extends Error {
  constructor(
    public code: string,
    message: string,
    public details?: any
  ) {
    super(message);
    this.name = 'TaskManagerError';
  }
}

export function createErrorResponse(error: unknown, id: number | string) {
  if (error instanceof TaskManagerError) {
    return {
      jsonrpc: '2.0',
      id,
      error: {
        code: -32000,
        message: error.message,
        data: { code: error.code, details: error.details }
      }
    };
  }

  return {
    jsonrpc: '2.0',
    id,
    error: {
      code: -32603,
      message: 'Internal error',
      data: { originalError: error instanceof Error ? error.message : String(error) }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Resource Caching Pattern

// src/utils/cache.ts
export class ResourceCache {
  private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();

  set(key: string, data: any, ttlMs: number = 60000) {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl: ttlMs
    });
  }

  get(key: string): any | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return null;
    }

    return entry.data;
  }

  clear() {
    this.cache.clear();
  }
}

// Usage in resources
export async function handleTaskListResource(database: TaskDatabase): Promise<Resource> {
  const cacheKey = 'task-list';
  const cached = resourceCache.get(cacheKey);

  if (cached) {
    return {
      contents: [{
        type: 'text',
        text: cached
      }]
    };
  }

  const tasks = await database.getAllTasks();
  const result = JSON.stringify({ tasks, total: tasks.length, cached: false }, null, 2);

  resourceCache.set(cacheKey, result, 30000); // 30 seconds TTL

  return {
    contents: [{
      type: 'text',
      text: result
    }]
  };
}
Enter fullscreen mode Exit fullscreen mode

Applied patterns:

  • βœ… Promisification of SQLite callbacks
  • βœ… Data transformation (ID prefixing)
  • βœ… Explicit null handling
  • βœ… Type safety with TypeScript

11. Code Organization and Patterns

Structuring Pattern

The project follows a clear modular architecture:

src/
β”œβ”€β”€ server.ts          # Entry point and orchestration
β”œβ”€β”€ database/          # Persistence layer
β”‚   β”œβ”€β”€ db.ts         # Database logic
β”‚   └── schema.sql    # Table structure
β”œβ”€β”€ tools/            # MCP Actions (Create, Update, Delete)
β”œβ”€β”€ resources/        # MCP Data (Read-only)
β”œβ”€β”€ prompts/          # LLM interaction templates
β”œβ”€β”€ types/            # TypeScript schemas and types
└── utils/            # Cross-cutting utilities
Enter fullscreen mode Exit fullscreen mode

Export Pattern

Each module exposes a consistent pattern:

// tools/index.ts
export const tools = [createTaskTool, updateTaskTool, ...];
export const toolHandlers = {
  create_task: handleCreateTask,
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • βœ… Centralized imports from main server
  • βœ… Automatic discovery of new tools
  • βœ… Type safety with object keys
  • βœ… Simplified maintenance

Validation Pattern

Layered validation with Zod:

export async function handleCreateTask(args: any, db: TaskDatabase) {
  try {
    // 1. Schema validation
    const validatedArgs = CreateTaskSchema.parse(args);

    // 2. Business logic validation
    if (validatedArgs.due_date && new Date(validatedArgs.due_date) < new Date()) {
      throw new McpError(ErrorCode.INVALID_PARAMS, 'Due date cannot be in the past');
    }

    // 3. Database operation
    const task = await db.createTask(validatedArgs);

    return {
      task_id: `task-${task.id}`,
      status: task.status,
      created_at: task.created_at
    };
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new McpError(ErrorCode.INVALID_PARAMS, `Validation failed: ${error.message}`);
    }
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Database Pattern

Encapsulation with promisification:

async getTask(id: string): Promise<Task | null> {
  const sql = 'SELECT * FROM tasks WHERE id = ?';
  return new Promise((resolve, reject) => {
    this.db.get(sql, [id.replace('task-', '')], (err, row) => {
      if (err) reject(err);
      else if (!row) resolve(null);
      else resolve({
        ...row,
        id: `task-${row.id}`,
        tags: row.tags ? JSON.parse(row.tags) : []
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Applied patterns:

  • βœ… Promisification of SQLite callbacks
  • βœ… Data transformation (ID prefixing)
  • βœ… Explicit null handling
  • βœ… Type safety with TypeScript

12. Testing and Validation

Jest Configuration (jest.config.js)

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src', '<rootDir>/tests'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
  ],
  moduleFileExtensions: ['ts', 'js', 'json'],
};
Enter fullscreen mode Exit fullscreen mode

Integration Tests (tests/integration.test.ts)

import { TaskDatabase } from '../src/database/db';
import { handleCreateTask } from '../src/tools/create-task';
import { handleTaskListResource } from '../src/resources/task-list';

describe('Task Manager Integration Tests', () => {
  let db: TaskDatabase;

  beforeEach(() => {
    // Use in-memory database for tests
    db = new TaskDatabase(':memory:');
  });

  test('Creating and retrieving a task', async () => {
    // Create a task
    const result = await handleCreateTask({
      title: 'Test Task',
      description: 'Task for testing',
      priority: 'high',
      tags: ['test', 'automation']
    }, db);

    expect(result.task_id).toBeDefined();
    expect(result.status).toBe('pending');

    // Retrieve task list
    const listResult = await handleTaskListResource(db);
    const tasks = JSON.parse(listResult.contents[0].text);

    expect(tasks.tasks).toHaveLength(1);
    expect(tasks.tasks[0].title).toBe('Test Task');
    expect(tasks.tasks[0].priority).toBe('high');
  });

  test('Error handling - non-existent task', async () => {
    const task = await db.getTask('task-999');
    expect(task).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode

Manual Test Script (scripts/test-mcp.js)

#!/usr/bin/env node

const { spawn } = require('child_process');

function testMcpServer() {
  console.log('πŸ§ͺ Testing MCP Task Manager server...\n');

  const server = spawn('npm', ['run', 'dev']);

  // Initialization test
  const initMessage = {
    jsonrpc: '2.0',
    id: 1,
    method: 'initialize',
    params: {
      protocolVersion: '1.0',
      clientInfo: { name: 'test-client', version: '1.0.0' }
    }
  };

  server.stdin.write(JSON.stringify(initMessage) + '\n');

  server.stdout.on('data', (data) => {
    try {
      const response = JSON.parse(data.toString());
      console.log('βœ… Response received:', response);
    } catch (error) {
      console.log('πŸ“ Output:', data.toString());
    }
  });

  server.stderr.on('data', (data) => {
    console.error('❌ Error:', data.toString());
  });

  // Stop after 5 seconds
  setTimeout(() => {
    server.kill();
    console.log('\n🏁 Test completed');
  }, 5000);
}

testMcpServer();
Enter fullscreen mode Exit fullscreen mode

13. Docker Deployment

Multi-stage Dockerfile

Create an optimized Dockerfile with multi-stage build:

# Task Manager MCP Server Dockerfile
# Multi-stage build for optimized production image

# Stage 1: Build stage
FROM node:20-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies (including dev dependencies for building)
RUN npm ci

# Copy source code
COPY src/ ./src/
COPY tsconfig.json ./

# Build the application
RUN npm run build

# Stage 2: Production stage
FROM node:20-alpine AS production

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create app user for security
RUN addgroup -g 1001 -S mcpuser && \
    adduser -S mcpuser -u 1001

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production && \
    npm cache clean --force

# Copy built application from builder stage
COPY --from=builder /app/dist ./dist

# Create data directory for SQLite database
RUN mkdir -p /app/data && \
    chown -R mcpuser:mcpuser /app

USER mcpuser

# Expose the port for MCP server (HTTP mode by default)
EXPOSE 3000

# Set environment variables for HTTP mode
ENV MCP_TRANSPORT=http
ENV PORT=3000

# Health check for HTTP MCP server
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]

# Start the MCP server
CMD ["node", "dist/server.js"]
Enter fullscreen mode Exit fullscreen mode

.dockerignore File

The .dockerignore file optimizes Docker build context by excluding unnecessary files:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
dist
coverage
*.md
.nyc_output
tasks.db
Enter fullscreen mode Exit fullscreen mode

Why these exclusions?

  • node_modules/: Will be installed in the container
  • dist/: Will be generated during build
  • tasks.db: Local development database
  • .env* files: Prevents accidental inclusion of secrets
  • *.md: Documentation not needed in production

Docker Scripts in package.json

{
  "scripts": {
    "build": "tsc && mkdir -p dist/database && cp src/database/schema.sql dist/database/",
    "start": "node dist/server.js",
    "dev": "nodemon src/server.ts",
    "test": "jest",
    "docker:build": "docker build -t task-manager-mcp-server .",
    "docker:run": "docker run -d --name task-manager-mcp-server -p 3000:3000 task-manager-mcp-server",
    "docker:stop": "docker stop task-manager-mcp-server && docker rm task-manager-mcp-server",
    "docker:logs": "docker logs task-manager-mcp-server"
  }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API Endpoints

The server exposes the following endpoints:

Health Check

GET /health
Enter fullscreen mode Exit fullscreen mode

Returns the server health status.

MCP Communication

POST /mcp
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

Example call to list tools:

curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'
Enter fullscreen mode Exit fullscreen mode

Example call to create a task:

curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "create_task",
      "arguments": {
        "title": "New task",
        "description": "Task description",
        "priority": "high"
      }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Deployment Commands

# Build Docker image
npm run docker:build

# Launch container
npm run docker:run

# View logs
npm run docker:logs

# Stop container
npm run docker:stop

# Build and run in one command
npm run docker:build && npm run docker:run
Enter fullscreen mode Exit fullscreen mode

Production Configuration

Environment Variables

The .env.example file defines configuration variables:

NODE_ENV=production
MCP_TRANSPORT=http
PORT=3000
DB_PATH=/app/data/tasks.db
LOG_LEVEL=info
Enter fullscreen mode Exit fullscreen mode

Variable descriptions:

  • NODE_ENV: Runtime environment (development/production)
  • MCP_TRANSPORT: Transport mode (http/stdio)
  • PORT: HTTP server listening port
  • DB_PATH: Path to SQLite database
  • LOG_LEVEL: Logging level (debug/info/warn/error)

Usage:

# Copy and customize
cp .env.example .env.production

# Use with Docker
docker run --env-file .env.production task-manager-mcp-server
Enter fullscreen mode Exit fullscreen mode

Transport Mode

The server supports two modes:

  • HTTP (default): Communication via REST endpoints
  • STDIO: Communication via stdin/stdout (for direct integration)

Volume for Data Persistence

# Launch with persistent volume
docker run -d \
  --name task-manager-mcp-server \
  -p 3000:3000 \
  -v task-manager-data:/app/data \
  task-manager-mcp-server
Enter fullscreen mode Exit fullscreen mode

Docker Compose (optional)

The docker-compose.yml file simplifies deployment and container management:

version: '3.8'

services:
  task-manager-mcp:
    build: .
    container_name: task-manager-mcp-server
    ports:
      - "3000:3000"
    volumes:
      - task-manager-data:/app/data
    environment:
      - NODE_ENV=production
      - MCP_TRANSPORT=http
      - PORT=3000
      - DB_PATH=/app/data/tasks.db
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  task-manager-data:
    driver: local
Enter fullscreen mode Exit fullscreen mode

Docker Compose advantages:

  • Data persistence: Named volume for SQLite database
  • Automatic restart: restart: unless-stopped
  • HTTP health check: Automatic monitoring via /health
  • Centralized configuration: Environment variables defined
  • Simplified management: docker-compose up -d to start everything

Claude Desktop Integration

HTTP Configuration (recommended)

Modify Claude Desktop configuration (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "task-mcp": {
      "type": "http",
      "url": "http://localhost:3000"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Docker Configuration

To use the server via Docker:

{
  "mcpServers": {
    "task-mcp": {
      "type": "http",
      "url": "http://localhost:3000"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then launch the container:

npm run docker:build
npm run docker:run
Enter fullscreen mode Exit fullscreen mode

STDIO Configuration (alternative)

If you prefer to use stdio communication:

{
  "mcpServers": {
    "task-manager": {
      "command": "node",
      "args": ["/path/to/mcp-task-manager/dist/server.js"],
      "env": {
        "NODE_ENV": "production",
        "MCP_TRANSPORT": "stdio"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Monitoring and Maintenance

Monitoring with Health Check

The /health endpoint provides status information:

# Simple check
curl http://localhost:3000/health

# Response
{
  "status": "healthy",
  "timestamp": "2025-11-05T16:25:58.230Z"
}
Enter fullscreen mode Exit fullscreen mode

Container Logs

# View logs in real-time
docker logs -f task-manager-mcp-server
npm run docker:logs

# View last lines
docker logs --tail 100 task-manager-mcp-server
Enter fullscreen mode Exit fullscreen mode

Docker Metrics

# Container statistics
docker stats task-manager-mcp-server

# Complete inspection
docker inspect task-manager-mcp-server
Enter fullscreen mode Exit fullscreen mode

Updates

# Method 1: NPM Scripts
npm run docker:stop
npm run docker:build
npm run docker:run

# Method 2: Docker Compose
docker-compose down
docker-compose build
docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Data Backup

# Create volume backup
docker run --rm -v task-manager-data:/data -v $(pwd):/backup alpine tar czf /backup/tasks-backup.tar.gz -C /data .

# Restore backup
docker run --rm -v task-manager-data:/data -v $(pwd):/backup alpine tar xzf /backup/tasks-backup.tar.gz -C /data
Enter fullscreen mode Exit fullscreen mode

Debug and Troubleshooting

# Access container
docker exec -it task-manager-mcp-server sh

# Check processes
docker exec task-manager-mcp-server ps aux

# Test connectivity
docker exec task-manager-mcp-server wget -qO- http://localhost:3000/health
Enter fullscreen mode Exit fullscreen mode

14. Additional Resources

MCP Documentation

Development Tools

Possible Extensions

  1. Authentication: JWT, OAuth 2.0
  2. Webhooks: Real-time notifications
  3. Integrations: Slack, Teams, GitHub Issues
  4. Analytics: Advanced metrics, dashboards
  5. REST API: Complementary HTTP interface

Practical Usage Examples

API Testing with curl

# 1. Start server
npm run docker:run

# 2. Check health
curl http://localhost:3000/health

# 3. List available tools
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'

# 4. Create a task
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "create_task",
      "arguments": {
        "title": "Implement authentication",
        "description": "Add JWT auth to MCP server",
        "priority": "high",
        "tags": ["security", "backend"],
        "assignee": "john.doe@example.com"
      }
    }
  }'

# 5. List tasks
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "resources/read",
    "params": {
      "uri": "task://list"
    }
  }'

# 6. View statistics
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "resources/read",
    "params": {
      "uri": "task://stats"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Script Integration

#!/bin/bash
# create-task.sh - Script to create tasks via API

TITLE="$1"
DESCRIPTION="$2"
PRIORITY="${3:-medium}"

if [ -z "$TITLE" ]; then
  echo "Usage: $0 <title> [description] [priority]"
  exit 1
fi

curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d "{
    \"jsonrpc\": \"2.0\",
    \"id\": 1,
    \"method\": \"tools/call\",
    \"params\": {
      \"name\": \"create_task\",
      \"arguments\": {
        \"title\": \"$TITLE\",
        \"description\": \"$DESCRIPTION\",
        \"priority\": \"$PRIORITY\"
      }
    }
  }" | jq '.'
Enter fullscreen mode Exit fullscreen mode

Usage Examples

# Local development
npm run dev

# Build and test Docker image
npm run docker:build
npm run docker:run
npm run docker:logs

# With docker-compose
docker-compose up -d
docker-compose logs -f

# Production with persistent volume
docker run -d \
  --name task-manager-mcp-server \
  -v $(pwd)/data:/app/data \
  task-manager-mcp-server
Enter fullscreen mode Exit fullscreen mode

🎯 Conclusion

This tutorial has guided you through creating a complete professional MCP Task Manager server. You now have:

βœ… Complete MCP Architecture

  • 4 Tools: Create, update, complete, delete tasks
  • 3 Resources: List, statistics, task details
  • 2 Prompts: Weekly review, sprint planning
  • Zod Validation: Type safety and robust validation

βœ… Production-Ready Infrastructure

  • Hybrid HTTP/STDIO server with REST endpoints
  • SQLite database with optimized schema
  • Multi-stage Docker with non-root user
  • Health checks and integrated monitoring
  • CORS and error handling professional-grade

βœ… Automated Deployment

  • NPM scripts for Docker (build, run, stop, logs)
  • Docker Compose with persistent volumes
  • Configurable environment variables
  • HTTP health checks for monitoring

βœ… Claude Desktop Configuration

{
  "mcpServers": {
    "task-mcp": {
      "type": "http",
      "url": "http://localhost:3000"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ Implemented Features

Tools (Actions)

  • create_task: Creation with complete validation
  • update_task: Modification with completed task protection
  • complete_task: Finalization with notes and time tracking
  • delete_task: Secure deletion with confirmation

Resources (Data)

  • task://list: Overview with metadata
  • task://stats: Dashboards and metrics
  • task://detail/{id}: Detailed information per task

Prompts (LLM Templates)

  • weekly_review: Weekly performance analysis
  • sprint_planning: Task planning and distribution

πŸ”§ Extensibility

The modular architecture facilitates adding new features:

// New tool in src/tools/
export const archiveTaskTool: Tool = { /* ... */ };
export async function handleArchiveTask(args: any, db: TaskDatabase) { /* ... */ }

// New resource in src/resources/
export const taskCalendarResource: Resource = { /* ... */ };
export async function handleTaskCalendarResource(db: TaskDatabase) { /* ... */ }

// New prompt in src/prompts/
export const standupPrompt: Prompt = { /* ... */ };
export async function handleStandupPrompt(args: any, db: TaskDatabase) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

πŸ›  Suggested Next Steps

  1. Authentication and Authorization
   // JWT middleware to secure API
   import jwt from 'jsonwebtoken';

   const authMiddleware = (req, res, next) => {
     const token = req.headers.authorization?.split(' ')[1];
     if (!token || !jwt.verify(token, process.env.JWT_SECRET)) {
       return res.status(401).json({ error: 'Unauthorized' });
     }
     next();
   };
Enter fullscreen mode Exit fullscreen mode
  1. External Integrations
   // Webhooks for notifications
   export const webhookTool: Tool = {
     name: 'setup_webhook',
     description: 'Configure external notifications',
     // ...
   };

   // Calendar synchronization
   export const calendarSyncTool: Tool = {
     name: 'sync_calendar',
     description: 'Synchronize with Google Calendar/Outlook',
     // ...
   };
Enter fullscreen mode Exit fullscreen mode
  1. Web Interface (Optional)
   # React/Vue frontend for administration
   npm create vite@latest task-manager-ui -- --template react-ts
   cd task-manager-ui
   npm install axios @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode
  1. Advanced Database
   // Migration to PostgreSQL for production
   import { Pool } from 'pg';

   export class PostgresTaskDatabase extends TaskDatabase {
     private pool: Pool;
     // ...
   }
Enter fullscreen mode Exit fullscreen mode
  1. Analytics and Monitoring
   // Prometheus metrics
   import prometheus from 'prom-client';

   const taskCreationCounter = new prometheus.Counter({
     name: 'tasks_created_total',
     help: 'Total number of tasks created'
   });
Enter fullscreen mode Exit fullscreen mode

The MCP Task Manager server is now ready for production use and can serve as a solid foundation for more complex task management and productivity applications!

πŸ“š Reference Documentation

Top comments (2)

Collapse
 
vihardev profile image
Info Comment hidden by post author - thread only accessible via permalink
Vihar Dev

This tutorial uses the Future-AGI SDK to get you from zero to defensible, automated AI evaluation fast.

Start here β†’

https://github.com/future-agi/ai-evaluation

Enter fullscreen mode Exit fullscreen mode

If it helps, add a ⭐ here β†’ [

https://github.com/future-agi/ai-evaluation

Enter fullscreen mode Exit fullscreen mode
Collapse
 
fairen profile image
Fairen

Hello,
This tutorial don't use your sdk, what is the point of your comment ? ^^

Some comments have been hidden by the post's author - find out more