Limited founding-tier pricing $299/mo locked for life with code FOUNDING50 · Claim your spot →

IntermediateClaude Code

Build your first MCP server in 30 minutes

A walking tour from empty repo to a custom tool Claude can call.

MCP makes Claude programmable. This is the practical path from zero to a working server: the SDK, the protocol, a real tool, and how to install it locally for testing.

12 min read
claude-codemcpsdktypescript

The Model Context Protocol intro covers why MCP matters. This is how to build one. We''ll go from empty directory to a working server with one custom tool, all in TypeScript.

What you''re building

A server that exposes one tool: get_team_status. It returns a fake team dashboard so you can see end-to-end how MCP wiring works. Once it''s running, replace the fake data with whatever real system you want Claude to talk to.

Step 1: scaffold

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

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"]
}

Step 2: the server

src/index.ts:

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: "team-status", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_team_status",
      description: "Get the current status of the team — who is on, what they are working on, blockers.",
      inputSchema: {
        type: "object",
        properties: {
          include_blockers: { type: "boolean", default: true }
        }
      }
    }
  ]
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const args = z.object({ include_blockers: z.boolean().optional() }).parse(req.params.arguments ?? {});

  const status = {
    online: ["alice", "bob"],
    offline: ["carol"],
    in_progress: { alice: "auth refactor", bob: "billing migration" },
    ...(args.include_blockers ? { blockers: { bob: "waiting on infra review" } } : {})
  };

  return {
    content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
  };
});

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

Three things to notice:

  1. ListTools advertises what the server can do.
  2. CallTool runs the tool and returns text content.
  3. The transport is stdio — Claude Code talks to your server via stdin/stdout.

Step 3: install in Claude Code

Add it to ~/.claude/mcp.json:

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

Restart Claude Code. In a session, ask: "What''s the team status right now? Use the team-status MCP."

Claude calls get_team_status, gets back the JSON, formats it. Done.

Step 4: replace the fake data

Now the fun part. Replace the status object with whatever real system you want:

  • Hit your internal API: await fetch("https://internal/api/team-status")
  • Query your database: await db.query("SELECT ...")
  • Wrap a CLI: await execAsync("kubectl get pods")

The shape of the tool stays the same; just swap the implementation. Claude doesn''t know or care where the data comes from.

Step 5: ship it

For your team:

  • Push to GitHub
  • Add an install script in your README
  • Anyone can clone + add to their mcp.json

For the world:

  • Publish to npm
  • Submit to mcpservers.org directory
  • Now Claude users globally can install your tool

What to do next

  • Add more tools. A useful server has 3-10 related tools, not 1. Think of your server as a "department" of Claude — billing, engineering, sales — with a coherent set of capabilities.
  • Add resources. MCP also supports a resources capability — let Claude read files from your system on request, not just call tools.
  • Add prompts. Pre-baked prompts users can invoke. Useful for "summarize this incident" patterns.

Where to go next

Share:

Keep learning

Apply this with Waymaker

Get this article surfaced where you work

Inside Waymaker, this article shows up next to the right Signal page — so the lesson lands when you need it, not before.

No credit card required.