How to Build Your First MCP Server in Under an Hour (2026)

TL;DR

You can build a working MCP server in about 50 minutes using Node.js, TypeScript, and the MCP SDK. This tutorial creates a task management server with tools, resources, validation, testing, and Claude Desktop support — the same pattern most production MCP servers use.

You Can Build This in 50 Minutes

The most common misconception about MCP servers: they are complex, require deep protocol knowledge, and take days to build.

Reality: a working MCP server with real tools, proper error handling, and testing takes about 50 minutes if you have Node.js installed. The learning curve is shallow.

This tutorial walks you through building a production-quality MCP server from scratch — not a toy example. We’ll build a task management server that exposes tools to create, list, and complete tasks. By the end, you’ll have a server you can connect to Claude Desktop and actually use.

New to MCP? Read our guide to Model Context Protocol (MCP) before building your first server.


What We’re Building

A Task Management MCP Server with:

  • Two tools: create_task and complete_task
  • One resource: task list (readable data)
  • One prompt: task review template
  • Full error handling and validation
  • Testing before deployment
  • Configuration for Claude Desktop

Why this example? Because it is realistic. Most MCP servers you will build follow this exact pattern: expose some business logic as tools, add read-only resources for context, add prompts for guidance.

If you want to see how custom MCP servers fit into a real development workflow, also read our guide to the best MCP servers for software teams.


Prerequisites (Check These First)

  • Node.js 18+ — download from nodejs.org
  • npm — comes with Node.js
  • A code editor — VS Code, Cursor, or any text editor
  • Bash or PowerShell — terminal access
  • Claude Desktop — for testing (optional; MCP Inspector works offline)

Verify Node.js installation:

bash

node --version  # should output v18 or higher
npm --version   # should output v9 or higher

Part 1: Setup (5 minutes)

Create a new directory and initialize the project:

bash

mkdir task-mcp-server
cd task-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node ts-node

Create the TypeScript configuration (tsconfig.json):

json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Update package.json to add type: “module” and build script:

json

{
  "type": "module",
  "name": "task-mcp-server",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc && chmod +x build/index.js",
    "dev": "ts-node --esm src/index.ts",
    "start": "node build/index.js"
  },
  "files": ["build"]
}

Create the source directory:

bash

mkdir src
touch src/index.ts

Part 2: Build the Server (25 minutes)

This is the full server code. Read through it, then we’ll break down each part.

typescript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

// Data store (in-memory for demo; use database in production)
interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
  completedAt?: Date;
}

const tasks: Map<string, Task> = new Map();
let taskCounter = 1;

// Input validation schemas
const CreateTaskSchema = z.object({
  title: z.string().describe("Task title"),
  description: z.string().optional().describe("Task description"),
});

const CompleteTaskSchema = z.object({
  taskId: z.string().describe("Task ID to mark complete"),
});

// Initialize MCP server
const server = new McpServer(
  {
    name: "task-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// Handler for listing available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "create_task",
        description: "Create a new task with a title and optional description",
        inputSchema: {
          type: "object" as const,
          properties: {
            title: {
              type: "string",
              description: "Task title",
            },
            description: {
              type: "string",
              description: "Optional task description",
            },
          },
          required: ["title"],
        },
      },
      {
        name: "complete_task",
        description: "Mark a task as complete by its ID",
        inputSchema: {
          type: "object" as const,
          properties: {
            taskId: {
              type: "string",
              description: "The ID of the task to complete",
            },
          },
          required: ["taskId"],
        },
      },
    ],
  };
});

// Handler for tool invocation
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "create_task") {
    const { title, description } = CreateTaskSchema.parse(args);
    const taskId = `task-${taskCounter++}`;
    const task: Task = {
      id: taskId,
      title,
      description: description || "",
      completed: false,
      createdAt: new Date(),
    };
    tasks.set(taskId, task);

    return {
      content: [
        {
          type: "text",
          text: `Task created: ${taskId}\nTitle: ${title}${description ? `\nDescription: ${description}` : ""}`,
        },
      ],
    };
  } else if (name === "complete_task") {
    const { taskId } = CompleteTaskSchema.parse(args);
    const task = tasks.get(taskId);

    if (!task) {
      return {
        content: [
          {
            type: "text",
            text: `Error: Task ${taskId} not found`,
          },
        ],
        isError: true,
      };
    }

    task.completed = true;
    task.completedAt = new Date();

    return {
      content: [
        {
          type: "text",
          text: `Task ${taskId} marked as complete.`,
        },
      ],
    };
  }

  return {
    content: [
      {
        type: "text",
        text: `Unknown tool: ${name}`,
      },
    ],
    isError: true,
  };
});

// Handler for listing resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "task://tasks/active",
        name: "Active Tasks",
        description: "List of all incomplete tasks",
        mimeType: "text/plain",
      },
      {
        uri: "task://tasks/all",
        name: "All Tasks",
        description: "List of all tasks (complete and incomplete)",
        mimeType: "text/plain",
      },
    ],
  };
});

// Handler for reading resources
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === "task://tasks/active") {
    const activeTasks = Array.from(tasks.values()).filter(
      (task) => !task.completed
    );
    const content =
      activeTasks.length === 0
        ? "No active tasks."
        : activeTasks
            .map(
              (task) =>
                `[${task.id}] ${task.title}${task.description ? ` - ${task.description}` : ""}`
            )
            .join("\n");

    return {
      contents: [
        {
          uri,
          mimeType: "text/plain",
          text: content,
        },
      ],
    };
  }

  if (uri === "task://tasks/all") {
    const allTasks = Array.from(tasks.values());
    const content =
      allTasks.length === 0
        ? "No tasks."
        : allTasks
            .map(
              (task) =>
                `[${task.id}] ${task.title} (${task.completed ? "completed" : "active"})${task.description ? ` - ${task.description}` : ""}`
            )
            .join("\n");

    return {
      contents: [
        {
          uri,
          mimeType: "text/plain",
          text: content,
        },
      ],
    };
  }

  return {
    contents: [
      {
        uri,
        mimeType: "text/plain",
        text: `Unknown resource: ${uri}`,
      },
    ],
  };
});

// Error handling
process.on("uncaughtException", (error) => {
  console.error("Uncaught exception:", error);
  process.exit(1);
});

// Connect and run
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Task MCP Server running on stdio");
}

main().catch(console.error);

Part 3: Understanding the Code (10 minutes)

Let me break down the critical pieces:

1. The Server Definition

typescript

const server = new McpServer(
  { name: "task-server", version: "1.0.0" },
  { capabilities: { tools: {}, resources: {} } }
);

This creates an MCP server that declares it supports tools and resources.

2. Tool Registration

The ListToolsRequestSchema handler tells the client what tools exist:

typescript

{
  name: "create_task",
  description: "Create a new task with a title and optional description",
  inputSchema: { /* JSON Schema describing inputs */ }
}

The description is crucial. The AI reads this to decide when to use the tool. Vague descriptions like “does stuff” lead to incorrect tool usage.

3. Tool Invocation

When the client calls a tool, the CallToolRequestSchema handler runs:

typescript

if (name === "create_task") {
  const { title, description } = CreateTaskSchema.parse(args);
  // Create the task
  // Return result
}

Zod validates the input against the schema. If validation fails, an error is thrown automatically.

4. Resources

Resources are read-only data the client can fetch:

typescript

{
  uri: "task://tasks/active",
  name: "Active Tasks",
  description: "List of all incomplete tasks"
}

When the client requests this resource, the ReadResourceRequestSchema handler returns the data. Resources are ideal for context — the AI can read them without calling tools.

5. Transport

typescript

const transport = new StdioServerTransport();
await server.connect(transport);

This runs the server over stdio (standard input/output). Claude Desktop connects this way. For remote/HTTP deployment, you would use HttpServerTransport instead.


Part 4: Build and Test (10 minutes)

Build the TypeScript:

bash

npm run build

If there are no errors, you have a compiled, working server in the build/ directory.

Testing with MCP Inspector

The easiest way to test is with the MCP Inspector — a web tool for testing MCP servers.

bash

npx @modelcontextprotocol/inspector node build/index.js

This starts your server and opens a browser window where you can:

  • See all available tools
  • Call tools with test inputs
  • View responses
  • Debug errors

Try this:

  1. Call create_task with title: “Buy groceries”
  2. Call create_task again with a different task
  3. Call complete_task with the ID from step 1
  4. Read the task://tasks/active resource

You should see your tasks being managed in real-time.

Testing with Claude Desktop

Once you confirm the server works in MCP Inspector, configure Claude Desktop.

macOS: Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

json

{
  "mcpServers": {
    "tasks": {
      "command": "node",
      "args": ["/absolute/path/to/task-mcp-server/build/index.js"]
    }
  }
}

Replace /absolute/path/to/task-mcp-server with the actual path. Use pwd in your terminal to get it.

Windows: Edit %APPDATA%\Claude\claude_desktop_config.json:

json

{
  "mcpServers": {
    "tasks": {
      "command": "node",
      "args": ["C:\\absolute\\path\\to\\task-mcp-server\\build\\index.js"]
    }
  }
}

Restart Claude Desktop. You should see a MCP indicator in the chat input area. Test by asking: “Create a task to review the MCP spec.”


Common Mistakes and How to Fix Them

ErrorCauseFix
Cannot find module '@modelcontextprotocol/sdk'Dependencies not installedRun npm install
"module": "Node16" error in tsconfigWrong module configurationEnsure tsconfig.json has "module": "Node16"
console.log breaks the serverStdout is reserved for MCP protocolUse console.error() for logging instead
Server crashes on startupUncaught exception in async codeWrap main() in try-catch, test with MCP Inspector first
Claude Desktop sees no toolsConfig path is wrong or JSON is malformedTest with MCP Inspector first; Claude Inspector will catch errors Inspector won’t
Task ID generation repeatsIn-memory counter resets on restartAdd UUID generation or persistent storage for production

Production Patterns (Not Covered Here But Important)

This example uses in-memory storage. For production:

  • Persistence: Store tasks in PostgreSQL, SQLite, or DynamoDB
  • Authentication: Add OAuth 2.0 or API key validation
  • Logging: Write logs to stderr (safe for stdio transport) or a file
  • Error handling: Catch specific errors, return meaningful messages
  • Testing: Write unit tests for tool handlers
  • Deployment: Host on Cloudflare Workers, AWS Lambda, or a VPS

A production-quality server follows the same pattern as this tutorial but adds those layers.


Tools vs Resources vs Prompts

MCP FeaturePurposeExample in This TutorialWhen to Use It
ToolsLet the AI perform an actioncreate_task, complete_taskUse when the AI needs to change data or trigger business logic
ResourcesGive the AI read-only contexttask://tasks/activeUse when the AI needs to read data without changing anything
PromptsProvide reusable instructions or workflowsTask review templateUse when you want consistent AI guidance or formatting

The Next Steps

Now that you have a working MCP server:

  1. Add more tools — what workflows could this server automate?
  2. Add authentication — if exposing external data, require API key validation
  3. Deploy remotely — instead of stdio, use HTTP transport for cloud hosting
  4. Register it — submit to the MCP registry so others can discover it
  5. Monitor it — add logging and error tracking

What This Unlocks

With this one MCP server, you now have:

  • A working integration with Claude, ChatGPT, Cursor, and any MCP-compatible client
  • A pattern you can replicate for any API or business logic
  • Understanding of tools, resources, and prompts
  • A foundation for more complex servers

The total time investment: 50 minutes. The compound value: every integration you build from now on uses this same pattern.


FAQ

How long does it take to build an MCP server?

Most developers can build their first MCP server in 45–60 minutes if they already have Node.js and a code editor installed.

Do I need TypeScript to build an MCP server?

No, you can use plain JavaScript. However, TypeScript is recommended because it provides type safety, better autocomplete, and easier debugging.

What is the easiest way to test an MCP server?

The easiest way is with the MCP Inspector. It lets you see available tools, call them manually, and debug errors before connecting the server to Claude Desktop.

What is the difference between tools and resources in MCP?

Tools perform actions such as creating or updating data. Resources are read-only and provide context that the AI can access without changing anything.

Can I connect my MCP server to Claude Desktop?

Yes. After building the server, add its file path to the Claude Desktop configuration file and restart the app. Claude Desktop will automatically detect the new MCP server.

Should I use stdio or HTTP transport?

Use stdio while developing locally because it is simpler and works directly with Claude Desktop. Use HTTP transport when deploying your MCP server remotely or sharing it with multiple users.

What should I build after my first MCP server?

After your first MCP server works, add database storage, authentication, more tools, and remote deployment. You can also combine it with servers like GitHub or PostgreSQL for more advanced workflows.

Now that you know how to build an MCP server, the next step is deciding which existing servers to combine it with. Read our guide to the best MCP servers for development teams in 2026.

Still unsure how MCP itself works? Go back to our complete introduction to Model Context Protocol (MCP).

About SSNTPL Sword Software N Technologies helps development teams build AI-integrated software. We specialize in MCP server development and production AI agent deployments for businesses across the US, Europe, and UAE.

Discuss your MCP server build with our team →

Leave a Reply

Share