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:

Recommended Tools

Better Stack

Free Plan

Get alerted when your APIs, browser tests, payment pipelines, or MCP server dependencies go down. Used by 100K+ developers.

Start monitoring free โ†’

1Password

14-day Free Trial

Store and inject API keys, payment credentials, tokens, and file access secrets into your MCP server configs. Trusted by 150K+ developers.

Try 1Password free โ†’

๐Ÿ”ง MCP Servers Mentioned in This Article

๐Ÿ“

Filesystem MCP Server

sandboxed read, write, edit, move and search access to an explicit whitelist of local directories, and it is the reference implementation most other filesystem MCP servers are modelled on. Shipped by Anthropic in the official modelcontextprotocol/servers monorepo (89,000+ stars, actively maintained), it is a Node.js server published to npm as @modelcontextprotocol/server-filesystem. The part worth understanding before you install is the access-control model, because there are now two ways to grant directories and they do not compose. Method one is command-line arguments: `npx -y @modelcontextprotocol/server-filesystem /path/one /path/two`. Method two, and the one the maintainers recommend, is MCP Roots โ€” a client that supports the roots protocol sends its roots at initialization, and those roots COMPLETELY REPLACE any directories passed on the command line, then get replaced again on every `notifications/roots/list_changed`. That means allowed directories can change at runtime without restarting the server, but it also means a roots-capable client silently overrides your CLI arguments. If the server starts with no arguments and the client either does not support roots or sends an empty list, initialization throws an error. The tool surface is broad: `read_text_file` (with mutually exclusive `head`/`tail` line windows), `read_media_file` returning base64 image/audio content blocks, `read_multiple_files` which keeps going when individual reads fail, `write_file`, `edit_file`, `create_directory`, `list_directory`, `list_directory_with_sizes`, `move_file`, `search_files`, `directory_tree`, `get_file_info` and `list_allowed_directories`. `edit_file` is the one to learn โ€” it does line-based and multi-line pattern matching with indentation detection and preservation, returns a git-style diff with context, and supports `dryRun: true` so you can preview a change before applying it; the maintainers recommend always running a dry run first. Every operation is refused outside the allowed set, and `list_allowed_directories` is the fastest way to confirm what the server actually believes it can touch.

Localโœ“
๐ŸŒ

Fetch

Web content fetching and conversion for efficient LLM usage. Extract readable content from any URL.

Localโœ“
๐Ÿง 

Memory

Knowledge graph-based persistent memory system. Store and retrieve contextual information.

Localโœ“
๐Ÿ’ป

Everything

Reference/test server with prompts, resources, and tools. Perfect for testing MCP implementations.

Localโœ“
๐Ÿ’ป

GitHub MCP Server

authenticated access to the whole GitHub platform โ€” repositories, files, branches, issues, pull requests, Actions runs, security alerts, discussions and notifications โ€” from Claude, Cursor, VS Code, Copilot CLI and any other MCP host. There is no npm package for this server, and that trips up most people who try to install it: `@github/mcp-server` is not published to the npm registry, so any `npx` line you find for it will fail. GitHub ships it three other ways. The easiest is the hosted remote server at https://api.githubcopilot.com/mcp/, which needs no install at all โ€” point an HTTP-transport MCP client at that URL and log in with OAuth (VS Code 1.101+, Claude Desktop, Claude Code, Cursor and Windsurf all support this). The second is the official Docker image ghcr.io/github/github-mcp-server, which is what the copy-paste command on this page runs; on github.com it now performs a browser-based OAuth login on first use and keeps the token in memory only, which is why the published Docker configs map a fixed loopback callback port (-p 127.0.0.1:8085:8085 with GITHUB_OAUTH_CALLBACK_PORT=8085) so the container can receive the callback. Prefer a token? Set GITHUB_PERSONAL_ACCESS_TOKEN instead โ€” it takes precedence over OAuth, and the minimum useful scopes are repo, read:org and read:packages. The third is the native Go binary from the repository's releases, which needs no fixed port for the OAuth flow. GitHub Enterprise Server has no hosted option: use the local server with --gh-host or GITHUB_HOST set to your instance (include the https:// scheme โ€” it defaults to http://, which GHES rejects). Toolsets can be narrowed with GITHUB_TOOLSETS, and an insiders channel is available at /mcp/insiders or via the X-MCP-Insiders header.

Auth required๐Ÿ“˜โœ“

๐Ÿ“š More from the Blog