· mcp / claude-code / typescript

How to build and use MCP servers with Claude Code (2026)

MCP is the fastest way to give Claude Code project-specific context without pasting files every session. Here is a working recipe from scaffold to running tool.

By

1,600 words · 8 min read

MCP is the fastest way to give Claude Code project-specific context without pasting files every session. You write a small TypeScript server once, register it in .mcp.json, and every future session has access to your schema, your internal APIs, and whatever else you expose — without burning context on static pastes.

This tutorial ends with you having a working custom MCP server wired into Claude Code. You will also know when to use MCP instead of CLAUDE.md or a pre-tool-use hook.

Who this is for

Claude Code users who want persistent tool context: DB schema, CLI wrappers, internal APIs. If you already understand the basics of Claude Code and know TypeScript, you are ready. If you are still evaluating AI coding CLIs, see the best AI coding CLIs of 2026 first. If you want a chat interface instead of a CLI, this is not for you.

Prerequisites

  • Node.js 20+
  • Claude Code installed (npm install -g @anthropic-ai/claude-code)
  • Basic TypeScript

What MCP is

Model Context Protocol (MCP) is an open protocol that lets a language model call external tools and read external resources. Claude Code spawns your MCP server as a child process, sends it JSON-RPC messages over stdio, and uses the tools and resources your server declares. You define what the model can call; the model decides when to call it.

That is the entire mental model. MCP is not a plugin system or an agent framework. It is a structured way for the model to reach outside its context window and do things.

Walkthrough

Step 1 — Scaffold the server

Create a directory for your server and install the dependencies:

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/[email protected] zod@3
npm install -D typescript @types/node
mkdir src && touch src/index.ts

Add these fields to package.json:

{
  "type": "module",
  "scripts": { "build": "tsc && chmod 755 build/index.js" },
  "files": ["build"]
}

Create tsconfig.json at the root:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Common failure: module: "Node16" is required for .js imports to resolve correctly with ESM. Using "CommonJS" here causes runtime import errors with @modelcontextprotocol/sdk.

Step 2 — Write a minimal tool

Open src/index.ts and write a server with one tool:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

server.registerTool(
  "hello",
  {
    description: "Returns a greeting",
    inputSchema: { name: z.string().describe("Name to greet") },
  },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}!` }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio"); // stderr only — see step 3

Build it:

npm run build

Critical: never call console.log() inside a stdio-transport MCP server. The MCP protocol sends JSON-RPC messages over stdout; any extra bytes on stdout corrupt the message stream and the server fails silently. Use console.error() for any logging — it goes to stderr, which Claude Code captures separately.

Step 3 — Declare a resource

Tools are for actions the model triggers on demand. Resources are for data the model can read. The distinction matters: a resource is a URI-addressed document (like a file or a DB row); a tool is a callable function.

Here is a resource that exposes the current Drizzle schema:

import path from "node:path";
import fs from "node:fs/promises";

server.registerResource(
  "schema",
  "schema://current",
  { description: "Current Drizzle schema" },
  async (uri) => {
    const schemaPath = path.join(
      process.env.CLAUDE_PROJECT_DIR ?? ".",
      "drizzle/schema.ts"
    );
    const content = await fs.readFile(schemaPath, "utf-8");
    return {
      contents: [{ uri: uri.href, mimeType: "text/plain", text: content }],
    };
  }
);

CLAUDE_PROJECT_DIR is injected by Claude Code when it spawns your server. It points to the project root — the directory where .mcp.json lives. Use it to resolve project-relative paths without hardcoding anything.

Rebuild after adding the resource: npm run build.

Step 4 — Register the server in .mcp.json

The recommended registration method is a .mcp.json file committed to your project root. Every developer who clones the repo gets the same tools.

{
  "mcpServers": {
    "my-schema-tool": {
      "type": "stdio",
      "command": "node",
      "args": ["./mcp-servers/schema-tool/build/index.js"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}"
      }
    }
  }
}

Env var expansion syntax is ${VAR} (required) or ${VAR:-default} (with fallback). The expansion works in command, args, env, url, and headers.

One reserved name to avoid: workspace. Claude Code skips any server named workspace at load time and shows a warning asking you to rename it. Pick anything else.

Alternative for local-only use: if you do not want to commit the server to git, use the CLI instead:

claude mcp add --transport stdio my-server -- node ./build/index.js

That writes to ~/.claude.json and stays local to your machine. For team setups, .mcp.json is the right choice.

Step 5 — Verify Claude Code picks it up

Start a Claude Code session in the project directory. Run the /mcp command:

/mcp

This opens an interactive panel listing all configured servers with their status:

  • ✓ Connected — the server started and handshook successfully
  • ✗ Failed — the server process crashed on startup; check stderr output via claude mcp get <name>
  • ⏸ Pending approval — project-scoped servers from .mcp.json require a one-time approval per user before Claude Code will use them

If you see Pending approval, approve it in the panel. This is a security gate: Claude Code will not execute a project-scoped server without explicit user consent. It fires once per machine, then stays approved.

To list and inspect servers from the terminal:

claude mcp list              # all servers with status
claude mcp get my-schema-tool  # details for one server

If a server shows ✗ Failed, the most common causes are: wrong path in args, missing npm run build step, or a console.log() call corrupting stdout.

Step 6 — Real example: the get_schema tool

The anchor example from this tutorial is a get_schema tool that returns the Drizzle schema for the current project on demand. A tool rather than a resource, because the model can call it naturally (“show me the schema”) without needing to know the resource URI.

import path from "node:path";
import fs from "node:fs/promises";

server.registerTool(
  "get_schema",
  {
    description: "Returns the Drizzle ORM schema for the current project",
    inputSchema: {},
  },
  async () => {
    const dir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
    const schemaFile = path.join(dir, "drizzle/schema.ts");
    try {
      const text = await fs.readFile(schemaFile, "utf-8");
      return { content: [{ type: "text", text }] };
    } catch {
      return {
        content: [{ type: "text", text: `Schema file not found at ${schemaFile}` }],
      };
    }
  }
);

With this tool registered, you can say “what tables are in this project?” and Claude Code will call get_schema automatically, read the schema, and answer without you pasting anything.

The same pattern extends to anything project-specific: get_openapi_spec, run_query, get_env_vars. Each tool is a function that Claude Code can call when it needs that context.

When to reach for MCP vs CLAUDE.md vs hooks

Reach for…When you need…
MCP toolClaude to run actions or fetch live data (query DB, call API, read dynamic files)
MCP resourcePersistent readable data Claude can pull on demand
CLAUDE.mdStatic context that never changes (coding standards, architectural decisions, project docs)
Pre-tool-use hookIntercepting or validating existing tool calls (blocking risky bash commands, logging all edits)

The key tradeoff: CLAUDE.md content loads into every session unconditionally, burning context budget. MCP tools and resources are fetched on demand — the model pays the context cost only when it calls them. If your project-specific context is large or dynamic, MCP is the right choice.

MCP also wins when you need the same context across multiple projects. Register the server at user scope (claude mcp add --scope user ...) and every project on your machine gets it.

MCP loses when the data is purely static and small. A 20-line CLAUDE.md describing your commit message format is faster and simpler than an MCP server that reads the same file.

For a deeper architectural comparison between MCP and REST APIs in the context of AI agents, see MCP vs REST: when to use which for AI agents.

Caveats

  • MCP servers start as child processes. For large projects with many servers, each one adds startup latency. Keep servers focused and fast.
  • TypeScript servers require npm run build before any change takes effect. There is no hot-reload — you must rebuild and restart Claude Code after edits.
  • Local servers run with full process privileges. Do not register an untrusted server from the internet via .mcp.json; it has access to your filesystem, env vars, and network.
  • v2 of @modelcontextprotocol/sdk (slated for stable release July 2026) splits the package into @modelcontextprotocol/server and @modelcontextprotocol/client. The tutorial above uses v1.29.0, which is the current stable release. Check the npm page before starting a new project.

References

  1. MCP Architecture — transport types, primitives, protocol version 2025-06-18
  2. MCP Server Quickstart (TypeScript) — full TypeScript code with registerTool, StdioServerTransport
  3. MCP Resources spec — URI schemes, registerResource, templates
  4. Claude Code MCP reference.mcp.json format, scopes, /mcp command, env var expansion
  5. Claude Code MCP quickstart — add/verify/use flow, troubleshooting
  6. npm: @modelcontextprotocol/sdk — current stable version 1.29.0
  7. GitHub: modelcontextprotocol/typescript-sdk — source, v2 pre-alpha README, v1 docs