В этом туториале мы сделаем нормальный первый MCP-сервер в current 2026 рамке: не перегруженный remote deployment, не catalogue of every feature, а понятный local stdio server на TypeScript, который можно проверить через Inspector и потом без боли подготовить к Streamable HTTP.
Это сознательно лучший старт, чем сразу прыгать в hosted remote server. Для первого MCP-проекта важнее понять lifecycle, contracts и test path, чем строить сложную инфраструктуру.
tools, resources, transport и тестирование.Нужны:
Создайте проект:
mkdir mcp-notes-server
cd mcp-notes-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
Обновите tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
И добавьте скрипты в package.json:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Создайте src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
type Note = {
id: string;
title: string;
content: string;
tags: string[];
createdAt: string;
};
const DATA_DIR = join(process.cwd(), ".data");
const NOTES_FILE = join(DATA_DIR, "notes.json");
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
function loadNotes(): Note[] {
if (!existsSync(NOTES_FILE)) return [];
return JSON.parse(readFileSync(NOTES_FILE, "utf-8"));
}
function saveNotes(notes: Note[]) {
writeFileSync(NOTES_FILE, JSON.stringify(notes, null, 2));
}
function makeId() {
return Math.random().toString(36).slice(2, 10);
}
const server = new McpServer({
name: "notes-server",
version: "1.0.0",
});
Это уже рабочая основа: storage плюс McpServer.
toolsСначала сделаем два полезных инструмента: создание и поиск.
server.tool(
"create_note",
"Create a note with title, content, and optional tags.",
{
title: z.string(),
content: z.string(),
tags: z.array(z.string()).optional(),
},
async ({ title, content, tags }) => {
const notes = loadNotes();
const note: Note = {
id: makeId(),
title,
content,
tags: tags ?? [],
createdAt: new Date().toISOString(),
};
notes.push(note);
saveNotes(notes);
return {
content: [
{
type: "text",
text: `Note created: ${note.title} (${note.id})`,
},
],
};
}
);
server.tool(
"search_notes",
"Search notes by keyword or tag. Use when the user asks to find existing notes.",
{
query: z.string().optional(),
tag: z.string().optional(),
},
async ({ query, tag }) => {
let notes = loadNotes();
if (query) {
const q = query.toLowerCase();
notes = notes.filter(
(note) =>
note.title.toLowerCase().includes(q) ||
note.content.toLowerCase().includes(q)
);
}
if (tag) {
notes = notes.filter((note) => note.tags.includes(tag));
}
return {
content: [
{
type: "text",
text: JSON.stringify(notes.slice(0, 10), null, 2),
},
],
};
}
);
Обратите внимание на descriptions. Для MCP tools это не косметика, а часть контракта с моделью.
resourceТеперь дадим модели read-only summary.
server.resource(
"notes://summary",
"Summary information about saved notes",
async (uri) => {
const notes = loadNotes();
const tagCounts = notes.reduce<Record<string, number>>((acc, note) => {
for (const tag of note.tags) {
acc[tag] = (acc[tag] ?? 0) + 1;
}
return acc;
}, {});
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
totalNotes: notes.length,
tags: tagCounts,
},
null,
2
),
},
],
};
}
);
Это хороший пример, где resource лучше, чем tool: никакого side effect, просто контекст для чтения.
Для первого сервера берём stdio.
const transport = new StdioServerTransport();
await server.connect(transport);
Полный файл src/index.ts на этом этапе уже достаточен для первого запуска.
Соберите проект:
npm run build
Это лучший следующий шаг, чем сразу подключать Claude Desktop.
Inspector позволяет:
Типовой flow:
node dist/index.js;create_note, search_notes, notes://summary.Если сервер странно ведёт себя в host, а в Inspector всё уже ломается, значит проблема почти точно в вашем server surface, а не в клиенте.
После Inspector можно идти в Claude Desktop или другой MCP-compatible host.
Логика конфигурации везде примерно одна:
node dist/index.js;stdio.Главное здесь не конкретный путь к JSON-конфигу, а то, что first connection вы делаете уже после ручной проверки capabilities.
Streamable HTTPКогда local server уже работает, следующий шаг может быть remote path.
Обычно это нужно, если:
Практически это означает:
Но core server contracts при этом должны остаться теми же. Именно поэтому полезно сначала сделать хороший stdio server.