You've tried the popular MCP servers, integrated them into your workflow, and now you're thinking: "What if I could build one that perfectly fits my specific use case?" Maybe you need to connect to a proprietary API, access a custom database schema, or integrate with internal tools.
Building your own MCP server isn't just possible—it's surprisingly straightforward. Let's walk through creating a custom server from concept to production.
We'll build a server that connects AI assistants to a simple task management system. Think of it as a basic version of connecting to Todoist, Asana, or your team's custom task tracker.
By the end of this guide, you'll be able to say: "Create a task called 'Review MCP integration' assigned to John with high priority" and watch your AI assistant actually create that task in your system.
First, let's get the foundation ready:
mkdir mcp-task-server
cd mcp-task-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node ts-node
Create a basic TypeScript configuration:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
Before diving into code, let's clarify what we're building:
Our server will expose these capabilities through the standardized MCP protocol.
Create src/index.ts
with the basic server setup:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// Simple in-memory task storage (replace with real database)
interface Task {
id: string;
title: string;
description?: string;
assignee?: string;
priority: 'low' | 'medium' | 'high';
status: 'todo' | 'in-progress' | 'done';
createdAt: Date;
}
let tasks: Task[] = [];
const server = new Server(
{
name: "task-management-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
Now let's add the actual functionality:
// Task creation tool
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_task",
description: "Create a new task",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Task title" },
description: { type: "string", description: "Task description" },
assignee: { type: "string", description: "Person assigned to task" },
priority: {
type: "string",
enum: ["low", "medium", "high"],
description: "Task priority level"
},
},
required: ["title"],
},
},
{
name: "list_tasks",
description: "List all tasks, optionally filtered by status or assignee",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["todo", "in-progress", "done"],
description: "Filter by task status"
},
assignee: { type: "string", description: "Filter by assignee" },
},
},
},
{
name: "update_task_status",
description: "Update the status of a task",
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task ID to update" },
status: {
type: "string",
enum: ["todo", "in-progress", "done"],
description: "New task status"
},
},
required: ["taskId", "status"],
},
},
],
};
});
Implement the actual tool execution logic:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "create_task":
const newTask: Task = {
id: Date.now().toString(), // Simple ID generation
title: args.title,
description: args.description,
assignee: args.assignee,
priority: args.priority || 'medium',
status: 'todo',
createdAt: new Date(),
};
tasks.push(newTask);
return {
content: [
{
type: "text",
text: `Task created successfully!\n\nID: ${newTask.id}\nTitle: ${newTask.title}\nAssignee: ${newTask.assignee || 'Unassigned'}\nPriority: ${newTask.priority}`,
},
],
};
case "list_tasks":
let filteredTasks = tasks;
if (args.status) {
filteredTasks = filteredTasks.filter(task => task.status === args.status);
}
if (args.assignee) {
filteredTasks = filteredTasks.filter(task => task.assignee === args.assignee);
}
const taskList = filteredTasks
.map(task => `[${task.status.toUpperCase()}] ${task.title} (${task.assignee || 'Unassigned'}) - Priority: ${task.priority}`)
.join('\n');
return {
content: [
{
type: "text",
text: filteredTasks.length > 0 ? taskList : "No tasks found matching the criteria.",
},
],
};
case "update_task_status":
const taskToUpdate = tasks.find(task => task.id === args.taskId);
if (!taskToUpdate) {
return {
content: [
{
type: "text",
text: `Task with ID ${args.taskId} not found.`,
},
],
};
}
taskToUpdate.status = args.status;
return {
content: [
{
type: "text",
text: `Task "${taskToUpdate.title}" status updated to ${args.status}.`,
},
],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
});
Add the server startup logic:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Task Management MCP Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
Update your package.json
:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"bin": {
"task-server": "./dist/index.js"
}
}
Build and test locally:
npm run build
echo '{"method": "tools/list", "params": {}}' | node dist/index.js
You should see a JSON response listing your available tools.
Add your server to Claude's configuration:
{
"mcpServers": {
"task-management": {
"command": "node",
"args": ["/path/to/your/mcp-task-server/dist/index.js"]
}
}
}
Restart Claude Desktop and test:
"Create a task called 'Test MCP server' with high priority assigned to me"
Replace the in-memory storage with a real database. Here's a SQLite example:
npm install better-sqlite3 @types/better-sqlite3
import Database from 'better-sqlite3';
const db = new Database('tasks.db');
// Initialize database schema
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
assignee TEXT,
priority TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Update your task operations to use the database
const createTaskStmt = db.prepare(`
INSERT INTO tasks (id, title, description, assignee, priority, status)
VALUES (?, ?, ?, ?, ?, ?)
`);
Add robust error handling:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
// Validate inputs using Zod
const createTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
assignee: z.string().optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
switch (name) {
case "create_task":
const validatedArgs = createTaskSchema.parse(args);
// ... rest of implementation
break;
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
};
}
});
Make your server available to others:
npm publish
GitHub Repository: Create a comprehensive README with installation and configuration instructions.
Submit to MCP Server Space: Add your server to our directory for community discovery.
Authentication: Add API key or OAuth support for secure access
Rate Limiting: Prevent abuse with request throttling
Webhooks: Real-time updates when external systems change
Batch Operations: Handle multiple tasks in single requests
Custom Resources: Expose project templates, user lists, or configuration data
Docker Container: Package your server for consistent deployment Process Management: Use PM2 or similar for process monitoring Logging: Add structured logging for debugging and monitoring Health Checks: Implement endpoints for service monitoring
Building custom MCP servers unlocks the full potential of AI assistants in your specific domain. Whether you're connecting to legacy systems, proprietary APIs, or specialized workflows, the MCP protocol provides a standardized way to make any service AI-accessible.
Start simple, test thoroughly, and expand based on actual usage patterns. The goal isn't to build the most feature-rich server immediately—it's to create reliable, focused tools that solve real problems in your workflow.
Your custom MCP server could be the missing piece that transforms how your team or organization leverages AI assistants. The framework is there, the tools are ready, and the only limit is your imagination.