Introduction
Model Context Protocol (MCP) servers allow you to expose any API, database, or service to AI models through a standardized interface. In this tutorial, we will build a practical MCP server that connects to a project management tool, giving your AI assistant the ability to create tasks, query project status, and manage workflows.
Prerequisites
- Node.js 18+ or Python 3.10+
- An MCP-compatible client (Claude Desktop, Claude Code)
- Basic TypeScript or Python knowledge
Step 1: Project Setup
mkdir mcp-project-manager
cd mcp-project-manager
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --initStep 2: Define Your Tools
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "project-manager",
version: "1.0.0",
});
// Tool: Create a new task
server.tool(
"create-task",
"Create a new task in the project",
{
title: z.string().describe("Task title"),
description: z.string().describe("Task description"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
assignee: z.string().optional().describe("Person to assign"),
},
async ({ title, description, priority, assignee }) => {
// Call your project management API here
const task = await createTask({ title, description, priority, assignee });
return {
content: [{
type: "text",
text: `Created task #${task.id}: "${title}" (Priority: ${priority})`,
}],
};
}
);
// Tool: List tasks
server.tool(
"list-tasks",
"List all tasks with optional filtering",
{
status: z.enum(["open", "in-progress", "done", "all"]).default("all"),
assignee: z.string().optional(),
},
async ({ status, assignee }) => {
const tasks = await getTasks({ status, assignee });
const formatted = tasks
.map((t: any) => `- #${t.id} [${t.status}] ${t.title} (@${t.assignee})`)
.join("\n");
return {
content: [{ type: "text", text: formatted || "No tasks found." }],
};
}
);Step 3: Add Resources
// Expose project data as readable resources
server.resource(
"project-summary",
"project://summary",
async () => {
const summary = await getProjectSummary();
return {
contents: [{
uri: "project://summary",
mimeType: "text/plain",
text: `Project: ${summary.name}
Open tasks: ${summary.openTasks}
In progress: ${summary.inProgress}
Completed: ${summary.completed}
Team size: ${summary.teamSize}`,
}],
};
}
);Step 4: Configure and Test
Add to your Claude Desktop config:
{
"mcpServers": {
"project-manager": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"API_TOKEN": "your-api-token"
}
}
}
}Step 5: Error Handling
server.tool(
"update-task",
"Update an existing task",
{
taskId: z.number().describe("Task ID"),
status: z.enum(["open", "in-progress", "done"]).optional(),
title: z.string().optional(),
},
async ({ taskId, status, title }) => {
try {
const updated = await updateTask(taskId, { status, title });
return {
content: [{
type: "text",
text: `Updated task #${taskId} successfully.`,
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed to update task #${taskId}: ${error instanceof Error ? error.message : "Unknown error"}`,
}],
isError: true,
};
}
}
);Troubleshooting
- Server crashes on startup: Check Node.js version and ensure all dependencies are installed
- Tools not showing in client: Restart the client after configuration changes
- Authentication errors: Verify API tokens in environment variables
- Timeout on long operations: Implement progress reporting for long-running tools
Conclusion
MCP servers are a powerful way to give AI models access to your tools and data. By building custom servers, you can create AI workflows that integrate seamlessly with your existing infrastructure.
Next Steps
- Add authentication and rate limiting
- Implement caching for frequently accessed resources
- Build MCP servers for your other internal tools
- Share your servers with the MCP community