The commands and packages in this lesson were checked against the official documentation as of October 2026. The SDKs and Claude Code update often: if a command doesn't work, check the MCP docs for Claude Code and modelcontextprotocol.io. Current versions: What's current.
The gist
MCP is like a USB port for Claude. USB is a standard connector: plug in a mouse, a flash drive, a microphone, a printer, and the computer sees them. MCP is a standard protocol: plug in your CRM, your database, your company's API, your file system, and Claude sees them as tools. Today you'll write your own MCP server: 30-50 lines of code, and Claude gains abilities it didn't have before.
Key terms
- MCP (Model Context Protocol): Anthropic's open standard for connecting AI to external tools (modelcontextprotocol.io)
- MCP server: a program that provides tools, resources and prompts to Claude
- Transports: stdio (local), HTTP (remote, recommended), SSE (remote, deprecated), WebSocket (only through a JSON config)
- Three types of objects: tools (actions), resources (data), prompts (templates)
- Three scopes: local (the default, private), project (through
.mcp.json, for a team), user (all your projects) - Installation:
claude mcp add(CLI),.mcp.json(a file), or through a plugin - Token economics: a warning at >10,000 tokens, a default limit of 25,000 tokens per call
- TypeScript SDK:
@modelcontextprotocol/server/ Python SDK:pip install "mcp[cli]"(the older TypeScript package@modelcontextprotocol/sdkstill shows up in examples)
Theory
How MCP is built
Claude Code (Client)
│
│ Standard MCP protocol (JSON-RPC 2.0)
│ over stdio or HTTP (SSE is deprecated)
▼
MCP Server (your code)
│
├── tools → functions Claude can call
├── resources → data Claude can read
└── prompts → templates for recurring tasks
│
▼
External system (CRM, database, API, files...)The key point: an MCP server is an ordinary program. It starts when Claude Code starts in the project and stays running while the session is open. Claude calls tools through JSON-RPC requests, and the server answers with results.
What you can do with connected MCP servers (from the official documentation):
- Build a feature from an issue tracker: "Build the feature from JIRA ENG-4521 and open a PR on GitHub"
- Analyze monitoring: "Check Sentry and show me the errors from the last 24 hours"
- Query databases: "Find the users who used feature X"
- Bring in designs: "Update the template based on the new Figma mockups"
- Automate: "Draft emails to these 10 users"
Three ways to install MCP servers
Option 1: Remote HTTP server (recommended for cloud services)
claude mcp add --transport http notion https://mcp.notion.com/mcpOption 2: Remote SSE server (deprecated, use HTTP)
claude mcp add --transport sse asana https://mcp.asana.com/sseOption 3: Local stdio server
claude mcp add --transport stdio --env AIRTABLE_API_KEY=YOUR_KEY airtable \
-- npx -y airtable-mcp-serverImportant: all options (--transport, --env, --scope) go before the server name. The -- separates the name from the launch command.
Three scopes
| Scope | Where it's stored | Who can use it | When to use it |
|---|---|---|---|
local (default) |
~/.claude.json |
Only you, only in this project | Personal servers, experiments |
project |
.mcp.json in the project root |
The whole team (through git) | Tools shared across the project |
user |
~/.claude.json |
You, in every project | Personal utilities for all projects |
# Add with project scope (for the team)
claude mcp add --transport http --scope project sentry https://mcp.sentry.dev/mcpIf names conflict, the priority is: local > project > user > plugin > claude.ai connectors. Servers set by your organization's admin take priority over all of them.
Managing servers
claude mcp list # List all servers
claude mcp get github # Details for a specific server
claude mcp remove github # Remove a server
/mcp # Inside Claude Code: server status and reconnectingMCP token economics (important for business)
Every MCP server uses up tokens from the context window. This matters a lot for cost:
- A warning when a single call outputs >10,000 tokens
- The default limit: 25,000 tokens per MCP tool response
- Changing the limit:
MAX_MCP_OUTPUT_TOKENS=50000 claude - Startup timeout:
MCP_TIMEOUT=10000 claude(10 seconds)
Auto-reconnect: if an HTTP/SSE server disconnects, Claude Code reconnects automatically with exponential backoff (up to 5 attempts). Local stdio servers don't reconnect on their own: restart them through /mcp.
Three types of MCP objects
Tools: actions Claude can perform:
get_contact: get a contact from the CRMcreate_task: create a tasksend_message: send a messagequery_database: run a database query
Resources: data Claude can read:
crm://contacts/list: the contact listdb://reports/monthly: the monthly reportfile://config/settings: the app config
Prompts (templates): ready-made instructions for common tasks:
analyze_deal: a deal analysis templatewrite_followup: a follow-up email template
For most projects, tools alone are enough.
A minimal MCP server: Hello World
Install the SDK:
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
mkdir srcAdd "type": "module" to package.json. Put a tsconfig.json next to it:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"types": ["node"],
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Create src/server.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
// Create the server
const server = new McpServer({
name: "my-first-mcp",
version: "1.0.0",
});
// Add a tool: a simple function
server.registerTool(
"get_weather", // Tool name
{
description: "Get the weather for a city", // Description for Claude
inputSchema: z.object({ // Parameters (Zod schema)
city: z.string().describe("City name"),
}),
},
async ({ city }) => {
// Real logic goes here: an API call, a database query, etc.
// For the example, a stub
return {
content: [{
type: "text",
text: `Weather in ${city}: 72°F, cloudy`
}]
};
}
);
// Connect the stdio transport and start
const transport = new StdioServerTransport();
await server.connect(transport);Important: a stdio server talks to Claude through standard output, so you can't print logs with console.log in it. Use console.error for logs.
Compile and run:
npx tsc
node build/server.jsA real example: an MCP server for a CRM
A complete server that Claude Code uses to work with a fictional CRM through a REST API:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const CRM_API_URL = process.env.CRM_API_URL || "https://api.mycrm.com";
const CRM_API_KEY = process.env.CRM_API_KEY || "";
const server = new McpServer({
name: "crm-mcp-server",
version: "1.0.0",
});
// Tool 1: Get a contact by name or email
server.registerTool(
"get_contact",
{
description: "Find a contact in the CRM by name or email address",
inputSchema: z.object({
query: z.string().describe("Name or email to search for"),
}),
},
async ({ query }) => {
const response = await fetch(
`${CRM_API_URL}/contacts/search?q=${encodeURIComponent(query)}`,
{ headers: { "X-API-Key": CRM_API_KEY } }
);
const data = await response.json();
if (!data.contacts?.length) {
return { content: [{ type: "text", text: `Contact "${query}" not found` }] };
}
const contact = data.contacts[0];
return {
content: [{
type: "text",
text: JSON.stringify({
id: contact.id,
name: contact.full_name,
email: contact.email,
company: contact.company,
deal_stage: contact.deal_stage,
last_contact: contact.last_contact_date,
}, null, 2)
}]
};
}
);
// Tool 2: Create a task
server.registerTool(
"create_task",
{
description: "Create a task in the CRM linked to a contact",
inputSchema: z.object({
contact_id: z.string().describe("Contact ID"),
title: z.string().describe("Task title"),
due_date: z.string().describe("Due date in YYYY-MM-DD format"),
priority: z.enum(["low", "medium", "high"]).describe("Task priority"),
}),
},
async ({ contact_id, title, due_date, priority }) => {
const response = await fetch(`${CRM_API_URL}/tasks`, {
method: "POST",
headers: {
"X-API-Key": CRM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ contact_id, title, due_date, priority }),
});
const task = await response.json();
return {
content: [{
type: "text",
text: `Task created. ID: ${task.id}. Due: ${due_date}. Priority: ${priority}.`
}]
};
}
);
// Tool 3: Update the deal stage
server.registerTool(
"update_deal_stage",
{
description: "Update the deal stage for a contact",
inputSchema: z.object({
contact_id: z.string().describe("Contact ID"),
stage: z.enum(["lead", "qualified", "proposal", "negotiation", "closed_won", "closed_lost"])
.describe("New deal stage"),
note: z.string().optional().describe("Note about the stage change"),
}),
},
async ({ contact_id, stage, note }) => {
await fetch(`${CRM_API_URL}/contacts/${contact_id}`, {
method: "PATCH",
headers: {
"X-API-Key": CRM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ deal_stage: stage, stage_note: note }),
});
return {
content: [{
type: "text",
text: `Deal stage updated: ${stage}${note ? `. Note: ${note}` : ""}`
}]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Registering it in the project: .mcp.json
To have Claude Code start your MCP server automatically when you open the project, create .mcp.json in the project root:
{
"mcpServers": {
"crm": {
"command": "node",
"args": ["./mcp-servers/crm/build/server.js"],
"env": {
"CRM_API_URL": "https://api.mycrm.com",
"CRM_API_KEY": "${CRM_API_KEY}"
}
}
}
}After that, the server starts automatically whenever you open Claude Code in this folder. Claude sees the tools get_contact, create_task and update_deal_stage as built-in abilities.
Environment variables in .mcp.json (an official feature):
The ${VAR} and ${VAR:-default} syntax is supported in the command, args, env, url and headers fields:
{
"mcpServers": {
"api-server": {
"type": "http",
"url": "${API_BASE_URL:-https://api.example.com}/mcp",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
}
}
}This lets you commit .mcp.json to git without secrets: each developer sets their own environment variables.
Registering through the CLI (an alternative):
# As user scope (available in all projects)
claude mcp add --transport stdio --scope user crm -- node ./server.js
# Add from JSON
claude mcp add-json crm '{"command":"node","args":["./server.js"],"env":{"CRM_API_KEY":"${CRM_API_KEY}"}}'Importing from Claude Desktop (if you've already set it up there; works on macOS and WSL):
claude mcp add-from-claude-desktopTesting: MCP Inspector
The MCP project provides an interactive inspector for testing servers without Claude (it needs Node 22.19 or newer):
npx @modelcontextprotocol/inspector node build/server.jsIt opens a web interface where you can:
- See all registered tools
- Call each tool by hand with parameters
- See what the server returns
- Debug errors
There's also a command-line mode: npx @modelcontextprotocol/inspector --cli node build/server.js --method tools/list shows the list of tools and exits. This is much faster than testing through Claude Code.
Claude Code as an MCP server
Claude Code itself can act as an MCP server for other apps:
claude mcp serveConnecting from Claude Desktop:
{
"mcpServers": {
"claude-code": {
"type": "stdio",
"command": "claude",
"args": ["mcp", "serve"],
"env": {}
}
}
}This gives other AI apps access to Claude Code's tools (Read, Edit, Bash and others).
Attaching MCP servers to subagents
You can attach MCP servers to a specific subagent with the mcpServers field in its frontmatter:
---
name: browser-tester
description: Tests features in a real browser using Playwright
mcpServers:
- playwright:
type: stdio
command: npx
args: ["-y", "@playwright/mcp@latest"]
---Inline servers connect when the subagent starts and disconnect when it finishes. The main conversation doesn't see these tools, which saves context.
The Python option: FastMCP
If you prefer Python, there's a more declarative way: the SDK builds the tool description from type annotations and the docstring. In the current documentation the class is called MCPServer (older examples use FastMCP):
# pip install "mcp[cli]" or uv add "mcp[cli]"
from mcp.server import MCPServer
mcp = MCPServer("crm-server")
@mcp.tool()
def get_contact(query: str) -> str:
"""Find a contact in the CRM by name or email"""
# Search logic
return f"Contact found: {query}"
@mcp.tool()
def create_task(contact_id: str, title: str, due_date: str) -> str:
"""Create a task in the CRM"""
# Task creation logic
return f"Task created: {title} for {contact_id}"
if __name__ == "__main__":
mcp.run(transport="stdio")Registering it in .mcp.json:
{
"mcpServers": {
"crm-python": {
"command": "python",
"args": ["./mcp-servers/crm_server.py"]
}
}
}Publishing to npm
If you want to share your MCP server or use it in several projects:
# package.json
{
"name": "@yourname/mcp-crm",
"version": "1.0.0",
"type": "module",
"bin": { "mcp-crm": "./server.js" },
"main": "./server.js"
}
# Publish
npm publish --access publicOnce it's published, anyone can use it:
{
"mcpServers": {
"crm": {
"command": "npx",
"args": ["-y", "@yourname/mcp-crm"]
}
}
}MCP resources: @-mentions
MCP servers can provide resources that you reference with @:
Analyze @github:issue://123 and suggest a fix Look at the docs @docs:file://api/authentication
Resources show up in autocomplete next to files when you type @.
Updating tools on the fly
Claude Code supports list_changed notifications from MCP servers. If a server adds or removes tools, Claude Code updates the list automatically without reconnecting.
Practice
Assignment: an MCP server for working with local note files
- Create a
my-notes-mcp/folder and set up the project following the Hello World steps above:npm init -y,npm install @modelcontextprotocol/server zod,npm install -D @types/node typescript,"type": "module"andtsconfig.json - Create
src/server.tswith three tools:list_notes: lists the files in the~/Notes/folder (or any folder of yours)read_note: reads a specific file by namecreate_note: creates a new file with a note
- Compile:
npx tsc - Test it with MCP Inspector:
npx @modelcontextprotocol/inspector node build/server.js - Register it in the project's
.mcp.json - Restart Claude Code and check that the tools showed up
- Ask Claude: "Create a note about today's meeting". It should use your tool
- Bonus: add a
search_notestool that searches the contents of your notes with grep
Goal: write a working MCP server from scratch, register it and test it through Claude Code.
Tools and resources
- @modelcontextprotocol/server:
npm install @modelcontextprotocol/server, the official TypeScript SDK - mcp:
pip install "mcp[cli]", the Python SDK (classMCPServer, formerlyFastMCP) - zod:
npm install zod, typing for tool parameters (required for the TypeScript SDK) - MCP Inspector:
npx @modelcontextprotocol/inspector, testing without Claude - Documentation: modelcontextprotocol.io, the protocol specification
- The official MCP page for Claude Code: https://code.claude.com/docs/en/mcp
- GitHub: github.com/modelcontextprotocol/servers, hundreds of ready-made servers
- CLI commands:
claude mcp add: add a serverclaude mcp list: list serversclaude mcp get <name>: server detailsclaude mcp remove <name>: remove a serverclaude mcp add-from-claude-desktop: import from Claude Desktopclaude mcp add-json <name> '<json>': add from JSONclaude mcp serve: run Claude Code as an MCP server/mcp: server status inside Claude Code (including OAuth authentication)
Key takeaways
An MCP server is an ordinary program in TypeScript or Python. 30-50 lines of code give Claude new tools. The complexity grows only with the complexity of your own logic, not with the MCP protocol.
Transports: stdio (local), HTTP (remote, recommended), SSE (deprecated), WebSocket (through a JSON config). Three scopes: local (the default), project (
.mcp.jsonfor the team), user (all projects).
Token economics: MCP call results use up context (tool descriptions are loaded as needed by default). A warning at >10,000 tokens of output. The default limit is 25,000 tokens. You can change it with
MAX_MCP_OUTPUT_TOKENS.
Test with MCP Inspector before connecting to Claude: it saves debugging time.
You can attach MCP servers to subagents with the
mcpServersfield in the frontmatter: the server connects only while the subagent is working and doesn't clutter the main conversation's context.
.mcp.jsonsupports environment variables (${VAR},${VAR:-default}), so you can commit it to git without secrets.
Next lesson
The mark stays in this browser only and is never sent anywhere. My progress