Back to blog
Engineering

MCP Is the USB-C of AI: Building Your First MCP Server in 30 Minutes

The Model Context Protocol is what gives an AI agent senses, the ability to read your database, drive a browser, hit your production logs. This is the tutorial. Thirty minutes from a blank file to a working custom MCP server your Claude Code agent can call. Plus the gotchas that wasted a thousand dollars of our token spend.

Hunter Hodnett Co-founder & CTPO, Chipp 13 min read

Every USB cable I owned in 2018 was different. Mini, micro, lightning, USB-A, USB-B, the weird trapezoidal one for old printers. None of them worked with each other. Owning a cable for one device meant nothing for any other device.

USB-C ended that. One cable, one port, one protocol, and overnight, every device manufacturer’s hardware became compatible with everyone else’s. The standard didn’t make any individual device better. It made the ecosystem better.

The Model Context Protocol (MCP) is doing the same thing for AI agents. Before MCP, every framework had its own format for tools. Each integration was bespoke; each integration was three integrations. After MCP, you write one server, and any MCP-compatible client. Claude Code, Cursor, Goose, increasingly anything, can use it.

This post is the tutorial I wish I’d had when I started. By the end of 30 minutes you’ll have a working custom MCP server, registered to Claude Code, with one real tool that does something useful. Then we’ll talk about what to build after the tutorial.

If you want the conceptual case for why MCP matters at all, the manifesto covers it. This post assumes you’re convinced and want to build.

Part 1: What MCP actually is

A language model only does one thing: output text. Every demo of an “AI agent”, the ones that browse the web, query a database, send Slack messages, those are not the model doing anything. They’re the model emitting text, and software around the model interpreting that text as an instruction.

The conventional way for the model to describe what it wants is a tool call. The model emits something like:

I want to call a tool named "read_file" with the argument {"path": "src/index.ts"}

The framework around the model parses that, runs the actual read_file tool on your machine, gets the result, and feeds the result back into the model’s next turn. The model now “knows” the contents of src/index.ts. It didn’t read the file. The framework read the file. The model just got text saying “here’s what was in it.”

This is the loop. Every interesting capability of every modern AI agent reduces to this loop.

“The model never executes anything. It just describes what it wants executed, and software outside the model does the executing.” — Hunter Hodnett, Chipp CTPO

MCP is the standardized way for the framework to find out what tools exist, what parameters they take, and how to call them. Before MCP, every framework had its own format. After MCP, you build a small server that conforms to the protocol, and any MCP-compatible client can use it.

The protocol itself is conceptually simple: an MCP server speaks JSON-RPC over either standard input/output (for local servers) or HTTP (for remote servers). It exposes a few methods, tools/list to enumerate what’s available, tools/call to invoke one. Claude Code calls those methods on your behalf.

You don’t need to memorize any of this. The TypeScript SDK handles the protocol details for you. You just declare your tools, write the implementations, and the SDK does the JSON-RPC plumbing.

Part 2: What we’re building

To make this tutorial useful instead of abstract, we’ll build a real MCP server: a database query tool for a hypothetical app’s users table.

In 30 minutes, you’ll have:

  • A local MCP server written in TypeScript.
  • A query_users tool the agent can call to look up users by email or ID.
  • A safe-column allowlist so the agent never accidentally exfiltrates passwords.
  • The server registered with Claude Code so you can use it interactively.

This is the shape of every internal MCP server we’ve built at Chipp. By the end you’ll know how to write your own for your billing system, your job queue, your feature flag store, anything where a custom tool would beat a generic one.

Part 3: Setup (5 minutes)

Make a fresh directory and initialize a Node project. We’ll use TypeScript and the official MCP SDK.

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

Create a minimal tsconfig.json:

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

And add a "type": "module" to your package.json so we can use ESM imports.

That’s the entire setup. You’re done.

Part 4: The smallest working server (10 minutes)

Create src/server.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";

// 1. Define the input schema for the tool.
const QueryUsersArgs = z.object({
  email: z.string().optional(),
  id: z.string().optional(),
});

// 2. Define the tool itself.
const QUERY_USERS_TOOL = {
  name: "query_users",
  description:
    "Look up a user from the application's users table. " +
    "Pass either `email` or `id`. Returns the user's id, email, name, " +
    "and account_status. Sensitive columns (password_hash, " +
    "oauth_tokens) are filtered at this layer and never exposed.",
  inputSchema: {
    type: "object",
    properties: {
      email: { type: "string", description: "Email address to look up" },
      id: { type: "string", description: "User ID to look up" },
    },
  },
};

// 3. The fake "database" so this tutorial runs standalone.
const FAKE_DB = [
  { id: "u_1", email: "alice@example.com", name: "Alice", account_status: "active" },
  { id: "u_2", email: "bob@example.com", name: "Bob", account_status: "suspended" },
];

// 4. Boot the server, register handlers.
const server = new Server(
  { name: "my-first-mcp", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [QUERY_USERS_TOOL],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name !== "query_users") {
    throw new Error(`Unknown tool: ${req.params.name}`);
  }
  const args = QueryUsersArgs.parse(req.params.arguments);
  const user = FAKE_DB.find(
    (u) => (args.email && u.email === args.email) || (args.id && u.id === args.id)
  );
  return {
    content: [
      {
        type: "text",
        text: user ? JSON.stringify(user, null, 2) : "No user found.",
      },
    ],
  };
});

// 5. Wire stdio transport.
const transport = new StdioServerTransport();
await server.connect(transport);

That’s the full server. About 50 lines. Save it.

You can run it locally to make sure it doesn’t crash:

npx tsx src/server.ts

It’ll print nothing because it’s waiting for JSON-RPC messages on stdin. Hit Ctrl-C.

Part 5: Register with Claude Code (3 minutes)

Tell Claude Code about the server by editing ~/.claude.json (or your project’s .mcp.json if you want it scoped to one project):

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

Replace /absolute/path/to/ with the real path. Restart Claude Code.

Verify the server is loaded:

$ claude
> /mcp

You should see my-first-mcp in the list, with one tool available.

Part 6: Try it out (5 minutes)

In a Claude Code session, ask:

Look up the user with email alice@example.com and tell me their account status.

The agent should call query_users with {"email": "alice@example.com"}, get back the JSON for Alice, and report that her account is active.

Try the negative case:

Look up the user with email nobody@example.com.

The agent should call the tool, get back “No user found.”, and report that.

Congratulations, you have a working MCP server. The protocol plumbing is handled. The tool is yours.

Part 7: The discipline that makes this useful (7 minutes)

Now the part that separates a tutorial MCP from a production MCP.

Tool descriptions are prompt engineering

The model decides whether to call a tool based on the description. Not the name. Not the parameters. The description.

The description we wrote above is honest but generic. A production version of the same tool would look more like this:

const QUERY_USERS_TOOL = {
  name: "query_users",
  description:
    "Look up a user from the application's users table. " +
    "Pass either `email` (case-insensitive, exact match) or `id` (UUID). " +
    "Returns: id, email, name, account_status. " +
    "Sensitive columns (password_hash, oauth_tokens, payment_methods) " +
    "are filtered at this layer and are NEVER exposed in any session. " +
    "Use this when investigating customer-reported issues, debugging " +
    "auth problems, or verifying user state. " +
    "Do NOT use this to enumerate users (no list/scan capability) or " +
    "to modify users (read-only). For mutations, use `update_user` instead.",
  ...
};

That description does five things the original didn’t:

  1. Tells the model exactly what data shape to expect.
  2. Explicitly names the columns that are filtered out, so the model doesn’t ask for them.
  3. Tells the model when to use the tool (and when not to).
  4. References sibling tools the model should use instead for related operations.
  5. Sets expectations about behavior (case-insensitive, exact match, etc.) so the model doesn’t guess.

This level of detail is the difference between an MCP that the agent calls correctly and one that the agent calls in confusion. The investment compounds, every session that uses the tool benefits.

A heuristic I use: if the model would have to guess about anything, the description is too short. Add a sentence.

Have Claude write your descriptions

The model is much better at writing descriptions for itself than you are. Once you’ve drafted the implementation, paste the schema into Claude Code and say “Write a tool description for this MCP tool. The agent will read this description to decide when to call the tool. Be specific. Mention edge cases. Tell the agent when not to use this tool.”

The first draft is usually 80% of the way there. Tighten and ship.

Filter sensitive data at the MCP layer

This is the big one. Anything you don’t want the agent to see should be filtered out at the server layer, not at the prompt layer.

If you tell the agent “don’t look at password hashes” via a system prompt, the agent might still look at password hashes when it’s confused. If your MCP server cannot return password hashes, because the SQL has a hardcoded column allowlist that doesn’t include them, the agent literally cannot see them. There’s no failure mode where it accidentally gets exposed.

We give our autonomous agents read access to production databases. The reason this is safe is that the database MCP server has hardcoded column allowlists per table. The agent can query users, but the only columns the MCP server will return are id, email, name, account_status, created_at. Everything else is filtered at the server. Even if the agent goes off the rails and asks for password_hash, the MCP server returns the allowlisted columns and ignores the rest.

This is how you ship MCP servers that touch production data without losing sleep.

Restart Claude Code when you change the server

The most painful gotcha. Claude Code starts the MCP server process once, at session start. If you change the server’s source code mid-session, your edits won’t take effect until you restart.

You will, at some point, edit your MCP server, run it, find a bug, fix the bug, and watch Claude Code call the old version of the tool. Then you’ll spend an hour debugging “why isn’t my fix working” before you remember the gotcha.

When in doubt: restart Claude Code. There’s a cheaper way (the /mcp command lets you reconnect), but the brute-force restart is the muscle memory worth building.

Don’t console.log from your MCP server

Local MCP servers communicate with Claude Code over stdio in a structured JSON-RPC format. If you console.log anything, that text gets injected into the protocol stream, the framework chokes on the malformed bytes, and your tool stops working in confusing ways.

Use a real logger that writes to stderr, not stdout. The MCP SDK has examples. The pain of debugging a stdio-corrupted MCP server is the kind of pain you feel only once before you internalize the rule.

Part 8: Beyond the Tutorial (What to Actually Build)

The fake-database MCP we just built is a tutorial, not a production system. Here’s what to build after this tutorial, in order of leverage.

A custom database MCP for your real database

Replace the FAKE_DB array with a real connection to your application’s database. Hardcode the column allowlists per table. Add tools for the queries you actually need, query_users, query_orders, query_subscriptions, whatever your domain requires.

This will be the most-used MCP in your cluster. We dispatch it on almost every Bug Bot ticket.

A custom browser MCP

The single highest-leverage MCP in autonomous development. (Why →)

Wrap a local Chromium instance via the Chrome DevTools Protocol. Expose tools for navigate, screenshot, click, fill, console-log retrieval. Bake in your dev login flow as its own tool, browser_dev_login(role: "free" | "enterprise" | "admin") that bypasses your auth flow with seeded test credentials.

That last tool is the differentiator. Off-the-shelf browser MCPs are generic. The MCP we run for Chipp knows how to log in as any user role without going through OAuth. That domain knowledge is what makes verification fast enough to be useful in an autonomous loop.

A custom log-drain MCP

Wrap your log aggregator (we use Loki). Expose query(labels, time_range) and a higher-level user_breadcrumbs(user_id, time_range) that pulls a user’s recent interactions before an error fired.

The user_breadcrumbs tool is what lets the agent reconstruct the user journey that led to a bug, and propose fixes that match real usage, not synthetic edge cases.

A custom cron / job MCP

If you have any kind of background job system, wrap it. Tools for list jobs, query job status, trigger a job manually. The agent will use these to debug job failures without you having to baby-sit.

Don’t roll your own when an official one exists

For Stripe, GitHub, Cloudflare, Supabase, Notion, use the official MCPs. The vendors maintain them. They keep up with API changes. They handle auth.

Where to roll your own: anything internal to your codebase, anything where the off-the-shelf MCP is too generic to give the agent the right context (we found this true for databases, off-the-shelf DB MCPs hallucinated column names constantly).

Part 9: The mental model to keep

Without MCP, you have a model that emits text. That’s it. It can’t read your code, can’t look at your screen, can’t query your database, can’t verify anything it produces.

With MCP, the model can interact with the world. It can check its own work. It can correct its own mistakes. It can chain together capabilities that no single component of your system would have alone.

That’s not an upgrade. That’s a phase transition.

Every additional MCP server you build is more senses the agent has. The agent that ships your code in 2027 will have access to twenty MCP servers and will navigate them as fluently as you navigate your filesystem. The teams that build those servers earliest will have agents that ship the most reliable code, because their agents will have the most ways to verify themselves.

Build one this weekend. Then build the next one. The cluster works as well as your tools let it.

Join the Alchemist waitlist →


If you want the conceptual case for MCP, the manifesto covers why MCP is one of the five pillars of autonomous development.

If you want to see how MCP fits into a production cluster, read Building a Self-Healing Bug Bot. Component 4 walks through the four MCPs every Bug Bot setup needs.

If you want the foundational discipline for managing the context MCPs add, read Context Engineering.

#mcp #mcp-server #claude-code-mcp #tutorial #autonomous-development