Your First Server with the TypeScript SDK
Build a working MCP server from scratch: McpServer, registerTool with zod schemas, resources, prompts, and StdioServerTransport.
Lesson content is currently available in Russian.
What you will learn
- →Настраивать проект: ESM, tsconfig, зависимости @modelcontextprotocol/sdk и zod.
- →Регистрировать инструменты, ресурсы и промпты через высокоуровневый API McpServer.
- →Запускать сервер на StdioServerTransport и соблюдать дисциплину stdout/stderr.
Каркас проекта
Официальный TypeScript SDK — пакет @modelcontextprotocol/sdk. Он реализует весь протокол: рукопожатие, маршрутизацию методов, валидацию, оба транспорта. Ваша работа — только доменная логика. Схемы аргументов описываются через zod, поэтому ставим обе зависимости:
mkdir orders-mcp && cd orders-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
SDK распространяется как ES-модули, поэтому в package.json обязательно "type": "module", а в tsconfig.json — резолюция NodeNext:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}
Обратите внимание на суффиксы .js в импортах ниже — при NodeNext-резолюции это требование ESM, а не опечатка: TypeScript-файл импортирует путь так, как он будет выглядеть после компиляции.
Структура минимальна: один src/index.ts на старте, позже разнесёте инструменты по файлам. Скрипт сборки — обычный tsc; никакого бандлера для stdio-сервера не нужно.
McpServer и первый инструмент
Высокоуровневый класс McpServer скрывает JSON-RPC полностью. Регистрируете возможности — SDK сам отвечает на tools/list, валидирует аргументы и упаковывает результат:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({ name: 'orders-server', version: '1.0.0' });
server.registerTool(
'search_orders',
{
title: 'Поиск заказов',
description: 'Ищет заказы по статусу. Возвращает не более limit записей (по умолчанию 20).',
inputSchema: {
status: z.enum(['new', 'paid', 'shipped']),
limit: z.number().int().min(1).max(100).optional(),
},
},
async ({ status, limit }) => {
const orders = await db.findOrders(status, limit ?? 20);
return { content: [{ type: 'text', text: JSON.stringify(orders) }] };
}
);
Здесь работают сразу три механизма. Во-первых, zod-схема автоматически конвертируется в JSON Schema для tools/list — модель увидит и enum, и ограничения. Во-вторых, аргументы валидируются до вызова хендлера: если модель передаст status: "cancelled", хендлер даже не запустится, а клиент получит ошибку invalid params. В-третьих, внутри хендлера аргументы уже типизированы — TypeScript выведет status: 'new' | 'paid' | 'shipped'.
Описание инструмента — это промпт-инжиниринг: модель выбирает инструмент и заполняет аргументы, читая именно description. Пишите его для модели, а не для человека: что делает, что возвращает, какие ограничения.
Ресурсы и промпты
Ресурс регистрируется с URI или шаблоном URI. Для семейства «карточка заказа по id» используем ResourceTemplate:
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource(
'order',
new ResourceTemplate('orders://{orderId}', { list: undefined }),
{
title: 'Заказ',
description: 'Карточка заказа по идентификатору',
mimeType: 'application/json',
},
async (uri, { orderId }) => ({
contents: [{ uri: uri.href, text: JSON.stringify(await db.getOrder(String(orderId))) }],
})
);
SDK сам отвечает на resources/templates/list, а при resources/read разбирает URI по шаблону и передаёт переменные в колбэк. Чтение ресурса обязано быть без побочных эффектов — это контракт протокола, на который полагается host.
Промпт — заготовленный сценарий, который пользователь запустит из интерфейса клиента:
server.registerPrompt(
'weekly-report',
{
title: 'Недельный отчёт',
description: 'Сводка по заказам за неделю',
argsSchema: { week: z.string().describe('Номер недели, например 2026-W30') },
},
({ week }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: 'Собери отчёт по заказам за неделю ' + week + ': выручка, топ-товары, проблемные доставки.',
},
}],
})
);
Заметьте: текст собран конкатенацией, а не шаблонной строкой — дело вкуса, но важно, что промпт возвращает готовые messages, которые клиент подставит в разговор от имени пользователя.
Транспорт, запуск и дисциплина stdout
Осталось подключить транспорт. Для локального сервера это три строки в конце src/index.ts:
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[orders-mcp] server started on stdio');
}
main().catch((error) => {
console.error('[orders-mcp] fatal:', error);
process.exit(1);
});
После npm run build сервер запускается командой node dist/index.js — и молча ждёт кадры на stdin. Это правильное поведение: stdio-сервер не печатает приветствий в stdout, потому что stdout целиком принадлежит протоколу.
Правила, которые сэкономят вам часы отладки:
- Стартовое сообщение и все логи — строго
console.error(stderr). Одинconsole.log— и клиент рвёт соединение на parse error. - Проверьте зависимости: некоторые библиотеки печатают баннеры в stdout при импорте. Такие — изолировать или заглушать.
- Ошибки доменного уровня возвращайте как
{ content: [...], isError: true }, а не бросайте исключение: брошенное исключение SDK превратит в ошибку вызова, но модель получит меньше контекста для восстановления. - Процесс не должен писать в stdout даже при завершении — никаких прощальных сообщений.
Полноценно пощупать сервер руками мы сможем в уроке про Inspector — а пока убедитесь, что процесс запускается и не падает.
Practice
Сервер заметок notes-mcp
Соберите с нуля stdio-сервер notes-mcp: инструмент add_note (title, text) с zod-валидацией, инструмент search_notes (query, limit), ресурс notes://all со списком заметок и промпт summarize-notes. Хранилище — обычный Map в памяти. Соблюдайте дисциплину stderr.
Deliverable
Репозиторий с собирающимся через tsc сервером: два инструмента, один ресурс, один промпт; node dist/index.js запускается без вывода в stdout.
Checkpoint
Покажите свой registerTool и объясните, что произойдёт, если модель передаст аргументы, не проходящие zod-схему. Дойдёт ли вызов до вашего хендлера?
- ✓ Указано, что SDK валидирует аргументы по схеме до хендлера и возвращает ошибку invalid params.
- ✓ Объяснено, что внутри хендлера аргументы уже типизированы согласно zod-схеме.
- ✓ Различены ошибка валидации (протокольная) и доменная ошибка через isError: true.
Check yourself
Sources