Advanced 25 min

Build Your Own MCP Server

Published · Updated

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.

Frequently Asked Questions

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.

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.

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`.

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.

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.

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.