Tutorial10 min read

How to Build Your Own MCP Server: Complete Tutorial 2026

Learn how to build a custom MCP server from scratch using the TypeScript SDK. Define tools, handle requests, connect to Claude Desktop, and ship a working MCP integration in under an hour.

By MyMCPTools Teamยท

Building a custom MCP server is the fastest way to connect any tool, API, or data source to Claude, Cursor, and other MCP-compatible AI clients. Once your server is running, your AI assistant can call your custom tools just like it calls filesystem or GitHub โ€” conversationally, with context, in real time.

This tutorial walks through building a working MCP server in TypeScript from scratch. By the end, you'll have a server that Claude Desktop can connect to and use.

What Is an MCP Server, Exactly?

An MCP server is a process that exposes structured "tools" to an AI client via the Model Context Protocol. Each tool has a name, description, and input schema. The AI client discovers your tools, decides when to call them, and passes structured arguments. Your server executes the logic and returns a result.

Think of it as a type-safe function call that your AI makes on your behalf โ€” but with natural language deciding when and why.

Prerequisites

  • Node.js 18+ installed
  • Claude Desktop or another MCP client
  • Basic TypeScript familiarity

Step 1: Initialize the Project

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

Update tsconfig.json to target ES2022 with module resolution set to node:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

Step 2: Define Your Server

Create src/index.ts. This is the full skeleton of an MCP server:

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";

const server = new Server(
  { name: "my-mcp-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "hello_world",
      description: "Returns a greeting for a given name",
      inputSchema: {
        type: "object",
        properties: {
          name: { type: "string", description: "The name to greet" },
        },
        required: ["name"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "hello_world") {
    const { name } = request.params.arguments as { name: string };
    return {
      content: [{ type: "text", text: `Hello, ${name}! Your MCP server is working.` }],
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

Step 3: Add a More Useful Tool

Replace the hello world tool with something practical โ€” a tool that fetches weather data from a public API:

// In ListToolsRequestSchema handler:
{
  name: "get_weather",
  description: "Get current weather for a city",
  inputSchema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name (e.g. 'San Francisco')" },
    },
    required: ["city"],
  },
}

// In CallToolRequestSchema handler:
if (request.params.name === "get_weather") {
  const { city } = request.params.arguments as { city: string };
  const response = await fetch(
    `https://wttr.in/${encodeURIComponent(city)}?format=3`
  );
  const text = await response.text();
  return { content: [{ type: "text", text }] };
}

Step 4: Connect to Claude Desktop

Add your server to Claude Desktop's config file. On Mac, edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "npx",
      "args": ["tsx", "/path/to/my-mcp-server/src/index.ts"]
    }
  }
}

Restart Claude Desktop. In a new conversation, click the tools icon (๐Ÿ”ง) โ€” you should see get_weather listed. Ask Claude "What's the weather in Tokyo?" and watch it call your server.

Step 5: Add Input Validation with Zod

For production servers, validate inputs with Zod to get type safety and clear error messages:

const WeatherInput = z.object({
  city: z.string().min(1).max(100),
});

// In your handler:
const parsed = WeatherInput.safeParse(request.params.arguments);
if (!parsed.success) {
  return {
    content: [{ type: "text", text: `Invalid input: ${parsed.error.message}` }],
    isError: true,
  };
}
const { city } = parsed.data;

Step 6: Add Resources (Optional)

Beyond tools, MCP servers can expose "resources" โ€” persistent data that AI clients can read at any time. This is useful for configuration, documentation, or structured data:

import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "config://server-info",
      name: "Server Configuration",
      mimeType: "application/json",
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  if (request.params.uri === "config://server-info") {
    return {
      contents: [{
        uri: "config://server-info",
        mimeType: "application/json",
        text: JSON.stringify({ version: "1.0.0", tools: ["get_weather"] }),
      }],
    };
  }
  throw new Error(`Unknown resource: ${request.params.uri}`);
});

Best Practices for Production MCP Servers

  • Keep tools focused. One tool per action. AI clients pick tools based on their description โ€” precise tools get picked accurately.
  • Write clear descriptions. The tool description is the interface. "Fetches weather data" is useless. "Returns current temperature, conditions, and humidity for a city name" is actionable.
  • Return structured text. Format output as Markdown when possible โ€” AI clients render it better in conversation.
  • Handle errors gracefully. Return isError: true with a human-readable message instead of throwing โ€” the AI can recover and explain what went wrong.
  • Scope access carefully. Only expose what the AI needs. A filesystem server limited to /home/user/projects is safer than one with unrestricted access.

Publishing Your MCP Server

Once your server works locally, you can:

  • Publish to npm so others can install it with npx your-server
  • Submit it to MyMCPTools to get discovered by thousands of developers
  • Open-source it on GitHub and add it to awesome-mcp-server lists

Related guides:

๐Ÿ”ง MCP Servers Mentioned in This Article

๐Ÿ“š More from the Blog