Начало / Как написать свой MCP-сервер на Python / Свой MCP-сервер на TypeScript
Свой MCP-сервер на TypeScript
Тот же сервер каталога на TypeScript SDK v2: stdio и Streamable HTTP
TypeScript SDK второй версии разделён на пакеты: @modelcontextprotocol/server для серверов и @modelcontextprotocol/client для клиентов. Прежний пакет @modelcontextprotocol/sdk — это первая версия, её поддерживают в отдельной ветке.
TypeScript SDK v2: фабрика createServer регистрирует возможности; serveStdio обслуживает локальный запуск, createMcpHandler — HTTP.
Проект#
Нужен Node.js 20 или новее. SDK поставляется только как ES-модули.
mkdir catalog && cd catalog
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server @modelcontextprotocol/node zod tsx
mkdir srctsx запускает TypeScript без сборки. Схемы параметров пишутся на Zod 4 (zod/v4); подходят и другие библиотеки со стандартом Standard Schema.
Код сервера#
// MCP-сервер «Каталог» на TypeScript SDK v2: инструмент, ресурс, шаблон запроса
import { createServer as createHttpServer } from 'node:http';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { localhostHostValidation, localhostOriginValidation, toNodeHandler } from '@modelcontextprotocol/node';
import * as z from 'zod/v4';
const PRODUCTS = [
{ sku: 'A-100', name: 'Стол обеденный', price: 18900, stock: 4 },
{ sku: 'A-200', name: 'Стул мягкий', price: 5400, stock: 0 },
{ sku: 'B-310', name: 'Полка настенная', price: 2300, stock: 17 }
];
function createServer(): McpServer {
const server = new McpServer({ name: 'catalog', version: '1.0.0' });
server.registerTool(
'find_products',
{
description: 'Найти товары по части названия. price — цена в рублях, stock — остаток.',
inputSchema: z.object({
query: z.string().min(1),
in_stock_only: z.boolean().default(false)
})
},
async ({ query, in_stock_only }) => {
const q = query.toLowerCase();
const found = PRODUCTS.filter(p => p.name.toLowerCase().includes(q) && (p.stock > 0 || !in_stock_only));
return {
content: [{ type: 'text', text: JSON.stringify(found) }],
structuredContent: { result: found }
};
}
);
server.registerResource('categories', 'catalog://categories',
{ title: 'Разделы каталога', mimeType: 'text/plain' },
async uri => ({ contents: [{ uri: uri.href, text: 'Столы\nСтулья\nПолки' }] })
);
server.registerPrompt(
'compare',
{
description: 'Сравнить два товара по артикулу',
argsSchema: z.object({ first_sku: z.string(), second_sku: z.string() })
},
({ first_sku, second_sku }) => ({
messages: [{
role: 'user',
content: { type: 'text', text: `Сравни товары ${first_sku} и ${second_sku}: цена, наличие, назначение.` }
}]
})
);
return server;
}
if (process.argv.includes('--http')) {
// Удалённый режим: Streamable HTTP на http://127.0.0.1:3000/mcp
const handler = toNodeHandler(createMcpHandler(createServer));
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createHttpServer((req, res) => {
if (!validateHost(req, res) || !validateOrigin(req, res)) return;
void handler(req, res);
}).listen(3000, '127.0.0.1');
console.error('catalog: http://127.0.0.1:3000/mcp');
} else {
// Локальный режим: stdio. В stdout писать нельзя — только в stderr
void serveStdio(createServer);
}createServer— фабрика: по HTTP она вызывается на каждый запрос, поэтому сервер ничего не хранит между запросами. Пулы соединений и кэши создавайте вне фабрики.registerToolпринимает имя, описание со схемой Zod и обработчик. SDK проверит аргументы до вызова обработчика и вернёт модели понятную ошибку.- Для HTTP перед обработчиком стоят проверки
HostиOrigin: на локальном адресе они защищают от DNS rebinding — когда вредная страница в браузере обращается к вашему серверу на127.0.0.1.
Запуск и проверка#
npx tsx src/server.ts # stdio
npx tsx src/server.ts --http # http://127.0.0.1:3000/mcp
npx @modelcontextprotocol/inspector npx tsx src/server.tsКлиент для проверки ставится отдельным пакетом @modelcontextprotocol/client:
// Клиент: запускает сервер как дочерний процесс и вызывает инструмент
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const client = new Client({ name: 'catalog-check', version: '1.0.0' });
await client.connect(new StdioClientTransport({ command: 'npx', args: ['tsx', 'src/server.ts'] }));
const { tools } = await client.listTools();
console.log(tools.map(t => t.name));
const result = await client.callTool({ name: 'find_products', arguments: { query: 'стол' } });
console.log(result.structuredContent);
await client.close();[ 'find_products' ]
{
result: [ { sku: 'A-100', name: 'Стол обеденный', price: 18900, stock: 4 } ]
}Подключение к клиенту#
Команда запуска для клиентов — npx tsx /абсолютный/путь/src/server.ts; для рабочего варианта соберите проект в JavaScript и запускайте node dist/server.js. Форматы настроек клиентов — в разделе «Подключение».
Авторизация по HTTP в TypeScript SDK передаётся в фабрику через authInfo: токен проверяет промежуточный обработчик до SDK. Подход и требования одинаковы для обоих языков — в руководстве «Авторизация».