claudekit / guides / build-mcp-server
[ Guide · Advanced · 25 min ]

Build Your Own MCP Server

published · updated

How to build an MCP server that connects Claude to external services. Covers transport choices, OAuth handling, and how to wire it into Claude Code.

What is MCP?

Model Context Protocol (MCP) lets Claude access external services in a safe, standardized way. Build the server once and it works in Claude Code, Claude Desktop, and other MCP-compatible IDEs.

Pick a transport

TransportGood forAuth
HTTPRemote SaaS, multi-userOAuth, API keys
stdioLocal CLI tools, system accessEnv vars, tokens
SSESome legacy remote serversOAuth

New remote servers usually pick HTTP.

Prerequisites

  • Node.js 18+ (or Python 3.10+)
  • Claude Code installed
  • A service or data source to expose

Initialize (TypeScript example)

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

Server building blocks

An MCP server has three primitives:

  1. Tools — functions Claude can call
  2. Resources — data Claude can read (files, DB rows, etc.)
  3. Prompts — predefined prompt templates

Most servers only need Tools.

Define a Tool (weather example)

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

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

server.registerTool(
  "get-weather",
  {
    title: "Get current weather",
    description: "Returns current weather for a city",
    inputSchema: {
      city: z.string().describe("City name in English"),
    },
  },
  async ({ city }) => {
    const weather = await fetchWeather(city);
    return {
      content: [{ type: "text", text: JSON.stringify(weather, null, 2) }],
    };
  }
);

The description is the key signal Claude uses to decide when to call the tool. Make it specific.

Run on stdio transport

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const transport = new StdioServerTransport();
await server.connect(transport);

Build and run:

npx tsc
node dist/index.js

Connect to Claude Code

1. Local stdio server

claude mcp add weather -- node /absolute/path/to/dist/index.js

Add --scope user to make it available across all projects, or --scope project to share with the team via .mcp.json.

2. Remote HTTP server

claude mcp add --transport http weather https://mcp.example.com/mcp

If OAuth is required, run /mcp in Claude Code to get the auth link.

3. Share at the project level (.mcp.json)

A .mcp.json at the project root lets your team share MCP config:

{
  "mcpServers": {
    "weather": {
      "type": "http",
      "url": "https://mcp.example.com/mcp"
    }
  }
}

Connect to Claude Desktop

Claude Desktop loads stdio MCP servers from its own config file. Instead of claude mcp add, you edit the JSON directly.

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Create the file if it doesn’t exist.

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}

After saving, fully quit (Cmd+Q) and relaunch the Desktop app — closing the window is not enough. Once it’s back, you can see the exposed tools from the tools panel below the chat input.

Designing permission-sensitive tools

When you expose anything powerful — shell execution, the file system, outbound network — split the surface into narrow tools. Claude Desktop’s permission model is per-tool allow/deny, so the wider one tool’s input schema is, the more dangerous a single “always allow” click becomes.

Bad — exposing a generic shell

server.registerTool("run_shell", {
  description: "Runs an arbitrary shell command",
  inputSchema: { command: z.string() },
}, async ({ command }) => { /* ... */ });

This one tool covers everything from ls to rm -rf and curl under the same permission grant. Once a user clicks “always allow,” it’s effectively unrestricted shell access.

Good — split by intent

server.registerTool("list_files", {
  description: "Lists files in a directory",
  inputSchema: { path: z.string() },
}, /* ... */);

server.registerTool("read_file", {
  description: "Reads a text file",
  inputSchema: { path: z.string() },
}, /* ... */);

server.registerTool("git_status", {
  description: "Returns git status for the current repo",
  inputSchema: {},
}, /* ... */);

Each tool has a constrained input schema, so unintended operations don’t slip through, and users can set different approval policies per tool.

Design principles

  • Constrain with the schema, not the prompt — narrow inputSchema beats telling the model “don’t do dangerous things” in description.
  • Make tools with side effects (writes, executes) visually distinct from read-only ones by name and return value.
  • Re-validate path and URL arguments server-side against an allowlist — don’t trust whatever Claude passes in.

Test

claude
/mcp

The added server’s status and exposed tools show up. Then ask Claude in plain language to call the tool and confirm the result.

Distribution patterns

  • Official SaaS — host an HTTP MCP server at your domain (e.g. https://mcp.notion.com/mcp) with OAuth login.
  • Personal tool — publish as an npm package; document claude mcp add in the README.
  • Marketplace listing — list on claude.com/plugins for discoverability and one-click install.

Next steps

  • Read the implementations of Notion MCP, GitHub MCP, and Slack MCP for patterns.
  • See the official MCP docs for advanced features (Resources, Prompts, OAuth providers).
  • Token usage and latency matter — keep tool responses short and return structured JSON.
§ 14

Frequently Asked Questions

frequently asked
§ 14.1
What is an MCP server?
A server that lets Claude access external services (DBs, APIs, SaaS) through a standardized protocol. You define Tools (functions), Resources (data), and Prompts (templates) that Claude can call.
§ 14.2
Which transport should I pick?
HTTP for remote services, stdio for local tools. HTTP fits remotely hosted, multi-user services. Use stdio for personal tooling or anything that needs local system access.
§ 14.3
How do I connect it to Claude Code?
Remote HTTP: `claude mcp add --transport http <name> <url>`. Local stdio: `claude mcp add <name> -- <command>`. Project-level sharing goes in `.mcp.json`.
§ 14.4
Can I use the same server in Claude Desktop?
For stdio MCP servers, add the same `mcpServers` block to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Fully quit and relaunch the Desktop app to pick up changes.
§ 14.5
How do I handle OAuth?
The MCP SDK standardizes OAuth 2.1. Expose `/.well-known/oauth-authorization-server` metadata, then `/mcp` in Claude Code surfaces the auth link for browser sign-in.
§ 14.6
Why publish my tool as MCP?
One server works in Claude Code, Claude Desktop, and any other MCP-compatible IDE. You don't have to build per-IDE integrations, which cuts maintenance cost.