Find my path ↗
FREE LIVE WORKSHOP FOR WOMEN · OCTOBER 24Bring your idea. Make your first move. Reserve my free seat ↗
All articles

How to Build Your First MCP Server in 30 Minutes

MCP sounds like something you need a platform team for. It isn't. Here's a complete, working server, start to finish, in five steps.

How to Build Your First MCP Server in 30 Minutes
In this article

"MCP" sounds like something you need a platform team for. It isn't. It's a server that tells Claude "here's a new tool you can use," and you can have one running, from an empty folder, in the time it takes to watch an episode of something.

This is pure build, no theory — five steps from empty directory to a working server with one custom tool.

What you're building

A server that exposes one tool: get_team_status. It returns a fake team dashboard, so you can see the whole wiring end-to-end without needing a real system to plug into yet. Once it works, you swap the fake data for something real — your own database, an internal API, a CLI you already run.

Step 1: scaffold it

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

Then a tsconfig.json:

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

Step 2: the server itself

src/index.ts — this is the whole thing:

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 happening: ListTools advertises what your server can do. CallTool runs it and hands back the result. The transport is stdio — Claude Code talks to your server the same way it'd pipe to any command-line tool.

Step 3: tell Claude Code it exists

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. Then ask: "What's the team status right now? Use the team-status MCP."

Claude calls your tool, gets the JSON back, and answers in plain language. That's it — you just built a Claude integration.

Step 4: make it real

Replace the fake status object with something that actually matters:

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

The shape of the tool stays identical. You're only ever swapping what's inside the function. Claude doesn't know or care where the data actually comes from — that's the whole point.

Step 5: ship it

For your team: push to GitHub, add an install line to your README, done — anyone can clone it and drop it into their own mcp.json.

For the world: publish to npm, submit to mcpservers.org. Now anyone using Claude anywhere can install what you just built.

What to do next

A useful server usually has 3–10 related tools, not one — think of it as a "department" of Claude with a coherent set of capabilities. MCP also supports resources (letting Claude read files from your system on request) and prompts (pre-baked prompt templates users can invoke). Both are worth exploring once the basic tool pattern above feels natural.

This exact pattern — a tool with a clear input/output contract that Claude can call without knowing what's underneath — is how Cameron connects to your actual business data. You just built a smaller version of the same idea. See what Cameron can do →

Ready to put this into action? Waymaker helps you go from idea to your first paying customer, with AI doing the heavy lifting alongside you.

Start building free
Ashley Kays

Ashley Kays

Founder

Founder of Waymaker. BigCo veteran (NCR, Walt Disney World, Wyndham Worldwide) turned solo operator. Building the operating layer above AI building tools.

Related articles