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

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.