Complete Tutorial: Creating an MCP Task Manager Server from Scratch
๐ Table of Contents
- Introduction to Model Context Protocol (MCP)
- Prerequisites and Installation
- Project Structure
- Basic Configuration
- MCP Server Implementation
- Endpoints: Tools
- Endpoints: Resources
- Endpoints: Prompts
- Error Handling
- Main Server
- Code Organization and Patterns
- Testing and Validation
- Docker Deployment
- Additional Resources
1. Introduction to Model Context Protocol (MCP)
What is MCP?
The Model Context Protocol (MCP) is aโฆ
Complete Tutorial: Creating an MCP Task Manager Server from Scratch
๐ Table of Contents
- Introduction to Model Context Protocol (MCP)
- Prerequisites and Installation
- Project Structure
- Basic Configuration
- MCP Server Implementation
- Endpoints: Tools
- Endpoints: Resources
- Endpoints: Prompts
- Error Handling
- Main Server
- Code Organization and Patterns
- Testing and Validation
- Docker Deployment
- 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
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
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
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"
}
}
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"]
}
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
});
Advantages of this approach:
- Type Safety: Automatic runtime validation
- Living documentation: Schemas serve as reference
- Reusability: One schema, multiple uses (API, DB, validation)
- 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
});
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);
});
});
}
}
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);
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');
}
}
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');
}
}
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');
}
}
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');
}
}
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
};
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');
}
}
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');
}
}
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');
}
}
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
};
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);
}
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');
}
}
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
};
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';
}
}
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
// ...
}
});
}
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)');
}
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...
}
}
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'
}));
}
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);
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) }
}
};
}
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
}]
};
}
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
Export Pattern
Each module exposes a consistent pattern:
// tools/index.ts
export const tools = [createTaskTool, updateTaskTool, ...];
export const toolHandlers = {
create_task: handleCreateTask,
// ...
};
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;
}
}
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) : []
});
});
});
}
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'],
};
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();
});
});
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();
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"]
.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
Why these exclusions?
node_modules/: Will be installed in the containerdist/: Will be generated during buildtasks.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"
}
}
HTTP API Endpoints
The server exposes the following endpoints:
Health Check
GET /health
Returns the server health status.
MCP Communication
POST /mcp
Content-Type: application/json
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": {}
}'
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"
}
}
}'
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
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
Variable descriptions:
NODE_ENV: Runtime environment (development/production)MCP_TRANSPORT: Transport mode (http/stdio)PORT: HTTP server listening portDB_PATH: Path to SQLite databaseLOG_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
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
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
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 -dto 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"
}
}
}
Docker Configuration
To use the server via Docker:
{
"mcpServers": {
"task-mcp": {
"type": "http",
"url": "http://localhost:3000"
}
}
}
Then launch the container:
npm run docker:build
npm run docker:run
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"
}
}
}
}
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"
}
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
Docker Metrics
# Container statistics
docker stats task-manager-mcp-server
# Complete inspection
docker inspect task-manager-mcp-server
Updates
# Method 1: NPM Scripts
npm run do