The fastest way to understand MCP is to build something with it. This tutorial creates a minimal MCP server that exposes one tool — a simple note-taking API — and connects it to Claude.
You need Node.js 18+ and an Anthropic API key.
What we are building
A tiny MCP server that exposes a save_note tool. Claude can call it to store notes during a conversation. Simple enough to finish in 15 minutes, real enough to teach the core pattern.
1. Set up the project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
Add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true
}
}
2. Write the server
Create src/server.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import path from "path";
const NOTES_FILE = path.join(process.cwd(), "notes.json");
// Load or initialize notes storage
function loadNotes(): Record<string, string> {
if (fs.existsSync(NOTES_FILE)) {
return JSON.parse(fs.readFileSync(NOTES_FILE, "utf-8"));
}
return {};
}
function saveNotes(notes: Record<string, string>) {
fs.writeFileSync(NOTES_FILE, JSON.stringify(notes, null, 2));
}
// Create the MCP server
const server = new Server(
{ name: "notes-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "save_note",
description: "Save a note with a title and content",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "The note title" },
content: { type: "string", description: "The note content" },
},
required: ["title", "content"],
},
},
{
name: "get_note",
description: "Retrieve a saved note by title",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "The note title to retrieve" },
},
required: ["title"],
},
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "save_note") {
const { title, content } = args as { title: string; content: string };
const notes = loadNotes();
notes[title] = content;
saveNotes(notes);
return {
content: [{ type: "text", text: `Note "${title}" saved successfully.` }],
};
}
if (name === "get_note") {
const { title } = args as { title: string };
const notes = loadNotes();
const content = notes[title];
if (!content) {
return {
content: [{ type: "text", text: `No note found with title "${title}".` }],
};
}
return {
content: [{ type: "text", text: `**${title}**\n\n${content}` }],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notes MCP server running on stdio");
3. Run it
npx tsx src/server.ts
The server starts and waits for MCP clients to connect over stdio. You will not see much in the terminal — it communicates through structured JSON over stdin/stdout.
4. Connect it to Claude Desktop
Open your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server:
{
"mcpServers": {
"notes": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/my-mcp-server/src/server.ts"]
}
}
}
Restart Claude Desktop. You should see “notes” appear in the MCP tools panel.
Now test it — ask Claude:
“Save a note called ‘MCP ideas’ with the content: build a calendar MCP server next”
Claude will call save_note with the right arguments. Check notes.json in your project root — the note will be there.
5. Connect it to Claude Code
Add the server to your Claude Code config (~/.claude/mcp.json):
{
"servers": {
"notes": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/my-mcp-server/src/server.ts"],
"transport": "stdio"
}
}
}
Now inside any Claude Code session, Claude can use save_note and get_note as tools.
What just happened
You defined a server with:
ListToolsRequestSchemahandler — tells MCP clients what tools exist and what parameters they takeCallToolRequestSchemahandler — receives tool calls and executes the logic- StdioServerTransport — the communication channel (stdin/stdout for local tools)
That is the entire MCP server pattern. For a production server, you would swap StdioServerTransport for an HTTP transport and deploy it as a service.
Going further
Add more tools. Each tool follows the same pattern: define it in ListTools, handle it in CallTool.
Switch to HTTP transport. For a shared server multiple clients can connect to:
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
// ... attach to an Express/Fastify route
Expose resources. Resources let Claude read data without calling a tool — good for static content, file listings, or read-only APIs.
Next steps
- MCP Explained: How Claude Connects to Any Tool or Data Source — conceptual overview
- Official MCP TypeScript SDK — full API reference
- MCP server examples — reference implementations for databases, file systems, and more
Related Reading.
Vercel AI SDK Tools: One API for Claude and OpenAI Skills
Vercel AI SDK's unified tool interface works with Claude, OpenAI, and Gemini. Write your skill once and switch AI providers without rewriting the agent loop.
Chaining Agent Skills: Research, Summarize, and Save
Build a skill chain where an agent searches the web, summarizes findings, and saves results to a file — all from a single prompt. Full Node.js walkthrough.
Agent Skills with the Claude API: tool_use From Scratch
Learn how to give Claude tools using the Anthropic API — define tools, handle tool_use responses, execute functions, and return results. Full Node.js working example.