← all articles
ai & llm

MCP Server Setup

Build a minimal Model Context Protocol server and connect it to Claude — how AI assistants call your tools and read your data, explained by building one.

4 min read·July 21, 2026

The Model Context Protocol (MCP) is a standard way for an AI assistant to call tools and read resources from an external server — your codebase, your internal API, a database — instead of being limited to what it already knows. This walks through building a minimal server and pointing an MCP-compatible client at it.

Prerequisites

Node.js (see the Node environment setup guide if you haven't already), and a package to hold your server:

mkdir mcp-demo && cd mcp-demo
npm init -y
npm install @modelcontextprotocol/sdk zod

A minimal server

MCP servers expose tools (actions the assistant can call) and resources (data it can read). Here's a server with one tool:

// server.js
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: "demo-server",
  version: "1.0.0",
});
 
server.registerTool(
  "get_time",
  {
    title: "Get current time",
    description: "Returns the current server time in ISO 8601 format",
    inputSchema: {},
  },
  async () => ({
    content: [{ type: "text", text: new Date().toISOString() }],
  }),
);
 
server.registerTool(
  "add_numbers",
  {
    title: "Add two numbers",
    description: "Returns the sum of two numbers",
    inputSchema: {
      a: z.number(),
      b: z.number(),
    },
  },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }],
  }),
);
 
const transport = new StdioServerTransport();
await server.connect(transport);

This uses stdio transport — the client launches your server as a subprocess and talks to it over stdin/stdout. That's the right choice for a local tool like this; MCP also supports HTTP-based transport for servers that run remotely, which you'd reach for once this needs to be shared rather than run locally per-user.

tip

zod schemas on tool inputs aren't just validation — the client uses them to know what arguments a tool accepts and generate the right call. Take the time to write accurate description fields too; that's literally how the assistant decides which tool to call and when.

Connect it to Claude Desktop

Edit the config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "demo-server": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-demo/server.js"]
    }
  }
}

Restart Claude Desktop. Your tools should appear in the tool list — ask it something that requires calling add_numbers and watch it use the tool instead of computing the answer itself.

Connect it to Claude Code

claude mcp add demo-server -- node /absolute/path/to/mcp-demo/server.js

Or drop a .mcp.json in your project root with the same shape as the Desktop config above, if you want it checked into the repo so the whole team gets it automatically.

Test without a full client

The MCP Inspector is a standalone debugging UI — point it at your server without needing Claude Desktop or Code at all:

npx @modelcontextprotocol/inspector node server.js

It opens a browser UI listing your tools, lets you call them manually with test arguments, and shows the raw request/response — the fastest feedback loop while you're building a server, before wiring up a real client.

Adding a resource

Tools are actions; resources are readable data, addressed by URI:

server.registerResource(
  "readme",
  "file:///project/README.md",
  { title: "Project README", mimeType: "text/markdown" },
  async () => ({
    contents: [
      {
        uri: "file:///project/README.md",
        text: await import("node:fs/promises").then((fs) =>
          fs.readFile("./README.md", "utf8"),
        ),
      },
    ],
  }),
);

Common gotchas

warning

Server doesn't show up in Claude Desktop/Code at all. Almost always a config file path issue — confirm you edited the exact file the client reads (the macOS/Windows paths above are easy to get slightly wrong), and that the command/args use an absolute path, not a relative one.

warning

Server crashes on launch with no clear error. Run it directly first — node server.js — before wiring it into a client. Stdio transport means any stray console.log in your server code corrupts the protocol stream, since stdout is the communication channel; log to console.error (stderr) instead if you need to debug.

note

The SDK's exact API shifts between versions — check the TypeScript SDK's README against the version npm install actually gave you if something here doesn't quite match.

Once one tool works end to end — registered, callable, showing up in a real client — adding more is just repeating registerTool with a new name, schema, and handler.