How to Build an MCP Server from Scratch (2026)
Create a custom Model Context Protocol server step by step
Start Building with Hypereal
Access Kling, Flux, Sora, Veo & more through a single API. Free credits to start, scale to millions.
No credit card required • 100k+ developers • Enterprise ready
How to Build an MCP Server from Scratch (2026)
The Model Context Protocol (MCP) is an open standard created by Anthropic that lets AI assistants connect to external tools and data sources through a unified interface. Instead of building custom integrations for every AI client, you build one MCP server and it works with Claude Desktop, Cursor, VS Code, and any other MCP-compatible client.
This guide walks you through building a complete MCP server from scratch using TypeScript. By the end, you will have a working server that exposes custom tools, resources, and prompts to any MCP client.
What You Will Build
We will build an MCP server for a hypothetical project management system that:
- Exposes tools that the AI can call (create task, list tasks, update status)
- Exposes resources that provide context (project data, team info)
- Exposes prompts that define reusable interaction patterns
Prerequisites
| Requirement | Details |
|---|---|
| Node.js | v18 or higher |
| TypeScript | v5+ |
| npm or yarn | For package management |
| Basic TypeScript knowledge | Functions, types, async/await |
| An MCP client | Claude Desktop, Cursor, or VS Code |
Step 1: Initialize the Project
Create a new project and install the MCP SDK:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Set up TypeScript:
npx tsc --init
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
Update package.json to add the build script and set the type:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"bin": {
"my-mcp-server": "./dist/index.js"
}
}
Step 2: Create the Basic Server
Create src/index.ts with the MCP server scaffold:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// In-memory task storage (replace with a database in production)
interface Task {
id: string;
title: string;
description: string;
status: "todo" | "in_progress" | "done";
assignee: string;
createdAt: string;
}
const tasks: Map<string, Task> = new Map();
let nextId = 1;
// Create the MCP server
const server = new McpServer({
name: "project-manager",
version: "1.0.0",
});
console.error("MCP server initialized"); // stderr for logs (stdout is for MCP protocol)
The key thing to understand: MCP servers communicate over stdio by default. All protocol messages go through stdout, so any logging must go to stderr.
Step 3: Add Tools
Tools are functions that the AI assistant can call. They are the core of most MCP servers. Add these to src/index.ts:
// Tool: Create a new task
server.tool(
"create_task",
"Create a new task in the project",
{
title: z.string().describe("The task title"),
description: z.string().describe("Detailed description of the task"),
assignee: z.string().describe("Name of the person assigned to the task"),
},
async ({ title, description, assignee }) => {
const id = `TASK-${nextId++}`;
const task: Task = {
id,
title,
description,
status: "todo",
assignee,
createdAt: new Date().toISOString(),
};
tasks.set(id, task);
return {
content: [
{
type: "text",
text: `Created task ${id}: "${title}" assigned to ${assignee}`,
},
],
};
}
);
// Tool: List all tasks
server.tool(
"list_tasks",
"List all tasks, optionally filtered by status or assignee",
{
status: z
.enum(["todo", "in_progress", "done"])
.optional()
.describe("Filter by status"),
assignee: z.string().optional().describe("Filter by assignee name"),
},
async ({ status, assignee }) => {
let filtered = Array.from(tasks.values());
if (status) {
filtered = filtered.filter((t) => t.status === status);
}
if (assignee) {
filtered = filtered.filter((t) =>
t.assignee.toLowerCase().includes(assignee.toLowerCase())
);
}
if (filtered.length === 0) {
return {
content: [{ type: "text", text: "No tasks found matching the criteria." }],
};
}
const taskList = filtered
.map(
(t) =>
`- [${t.id}] ${t.title} (${t.status}) - Assigned to: ${t.assignee}`
)
.join("\n");
return {
content: [{ type: "text", text: taskList }],
};
}
);
// Tool: Update task status
server.tool(
"update_task_status",
"Update the status of an existing task",
{
taskId: z.string().describe("The task ID (e.g., TASK-1)"),
status: z
.enum(["todo", "in_progress", "done"])
.describe("The new status"),
},
async ({ taskId, status }) => {
const task = tasks.get(taskId);
if (!task) {
return {
content: [{ type: "text", text: `Task ${taskId} not found.` }],
isError: true,
};
}
const oldStatus = task.status;
task.status = status;
return {
content: [
{
type: "text",
text: `Updated ${taskId}: ${oldStatus} → ${status}`,
},
],
};
}
);
Anatomy of a Tool
Each tool registration has four parts:
- Name -- a unique identifier the AI uses to call the tool
- Description -- tells the AI when and why to use this tool
- Schema -- Zod schema defining the parameters (validated automatically)
- Handler -- async function that executes the tool and returns results
Step 4: Add Resources
Resources provide read-only context to the AI. Unlike tools, they do not perform actions -- they provide data.
// Resource: Project summary
server.resource(
"project-summary",
"project://summary",
async (uri) => {
const totalTasks = tasks.size;
const byStatus = {
todo: Array.from(tasks.values()).filter((t) => t.status === "todo").length,
in_progress: Array.from(tasks.values()).filter(
(t) => t.status === "in_progress"
).length,
done: Array.from(tasks.values()).filter((t) => t.status === "done").length,
};
const summary = `# Project Summary
Total tasks: ${totalTasks}
- To Do: ${byStatus.todo}
- In Progress: ${byStatus.in_progress}
- Done: ${byStatus.done}
Completion rate: ${totalTasks > 0 ? Math.round((byStatus.done / totalTasks) * 100) : 0}%`;
return {
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: summary,
},
],
};
}
);
Step 5: Add Prompts
Prompts are reusable templates that the AI client can offer to users. They define structured interaction patterns:
// Prompt: Sprint planning
server.prompt(
"sprint-planning",
"Generate a sprint planning summary based on current tasks",
{
sprintName: z.string().describe("Name of the sprint (e.g., Sprint 23)"),
},
async ({ sprintName }) => {
const todoTasks = Array.from(tasks.values()).filter(
(t) => t.status === "todo"
);
const taskListText =
todoTasks.length > 0
? todoTasks.map((t) => `- ${t.id}: ${t.title} (${t.assignee})`).join("\n")
: "No pending tasks.";
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Create a sprint planning document for "${sprintName}". Here are the current unassigned/todo tasks:\n\n${taskListText}\n\nPlease organize them by priority, estimate story points, and suggest a sprint goal.`,
},
},
],
};
}
);
Step 6: Start the Server
Add the server startup code at the bottom of src/index.ts:
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Project Manager MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Build and test:
# Build
npm run build
# Test by running directly (you will see the server waiting for MCP messages on stdin)
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' | node dist/index.js
Step 7: Connect to Claude Desktop
Add your server to Claude Desktop's configuration file.
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"project-manager": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You should see the tools icon showing your project-manager server with three available tools.
Step 8: Connect to Cursor
For Cursor, add the MCP server in .cursor/mcp.json at the root of your workspace:
{
"mcpServers": {
"project-manager": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Cursor and the tools will be available in the AI chat.
Testing Your Server
The MCP SDK includes an inspector tool for debugging:
npx @modelcontextprotocol/inspector node dist/index.js
This opens a web UI where you can:
- See all registered tools, resources, and prompts
- Call tools with test inputs
- View raw MCP protocol messages
- Debug errors in real time
Project Structure
Your final project should look like this:
my-mcp-server/
src/
index.ts # Main server file
dist/ # Compiled output
package.json
tsconfig.json
For larger servers, split into separate files:
my-mcp-server/
src/
index.ts # Server setup and startup
tools/
tasks.ts # Task-related tools
reports.ts # Report generation tools
resources/
project.ts # Project resources
prompts/
planning.ts # Planning prompt templates
types.ts # Shared types
dist/
package.json
tsconfig.json
Common Patterns
Connecting to a Database
Replace the in-memory Map with a database client:
import Database from "better-sqlite3";
const db = new Database("./tasks.db");
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'todo',
assignee TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
Adding Authentication
If your MCP server accesses external APIs, pass credentials via environment variables:
{
"mcpServers": {
"project-manager": {
"command": "node",
"args": ["/path/to/dist/index.js"],
"env": {
"API_KEY": "your-secret-key",
"DATABASE_URL": "postgresql://localhost/mydb"
}
}
}
}
Access them in your server:
const apiKey = process.env.API_KEY;
if (!apiKey) {
console.error("API_KEY environment variable is required");
process.exit(1);
}
Wrapping Up
Building an MCP server is straightforward once you understand the three primitives: tools (actions), resources (data), and prompts (templates). The MCP SDK handles all the protocol details, so you can focus on the logic that matters for your application.
MCP is quickly becoming the standard way AI assistants interact with the outside world. Building your own server gives you full control over what the AI can access and do.
If your AI projects also involve media generation like images, video, or talking avatars, check out Hypereal AI for a unified API that covers all major AI media models.
Try Hypereal AI free -- 35 credits, no credit card required.
Related Articles
Start Building Today
Get 35 free credits on signup. No credit card required. Generate your first image in under 5 minutes.
