Все говорят про MCP. Anthrophic его придумал, llama.cpp подхватил, десятки стартапов пилят «агентные платформы». Но попробуй найти вменяемый туториал, где тебе показывают, как написать MCP-сервер с нуля, без магии, без готовых SDK, на чистом Node.js + твоя любимая GGUF модель.
Я перерыл пол-интернета. Нашёл либо копипасту из документации, либо «запусти наш проприетарный хаб и забудь». И только одна библиотека даёт тебе полный контроль — node-llama-cpp (версия 3.1.4 на май 2026). В неё уже встроена поддержка MCP-клиента, но мы пойдём дальше — напишем свой сервер и прикрутим к нему кастомный агентный цикл.
Спойлер: вы сможете заставить локальную модель выполнять действия в реальном мире — читать файлы, вызывать API, писать в базу. И всё это без копейки денег и без выхода в интернет.
Зачем писать MCP-сервер самому, если есть готовый
В MCP в llama.cpp: от экспериментальной фичи до полноценного агента мы обсуждали, что llama.cpp уже умеет быть MCP-клиентом. Собери с флагом -DLLAMA_MCP=ON — и вуаля. Но есть нюанс: это чёрный ящик. Вы не можете кастомизировать обработку вызовов инструментов, не можете решить, как модель парсит ответы. А если вы работаете с нестандартным форматом JSON-RPC или хотите добавить свои метрики?
Самописный сервер на node-llama-cpp даёт:
- Полный контроль над циклом «запрос-мысль-действие-наблюдение»
- Возможность встраивать любые инструменты (не только те, что поддерживает MCP SDK)
- Прозрачность: каждый токен под микроскопом
- Вес: 0 зависимостей от Python, CUDA, Docker — только Node.js и ваша GGUF-модель
Что нам понадобится (список краток, но жёсток)
- Node.js 22+ (LTS на май 2026 — 22.4)
- npm или pnpm
- GGUF модель — рекомендую Qwen2.5 7B Q4_K_M. Скачать с Hugging Face. Она стабильно держит tool-calls.
- Библиотека node-llama-cpp версии 3.1.4 (последняя на момент написания)
- Терпеливый вайб — будем писать с нуля, без копипасты
Шаг 1. Инициализируем проект и устанавливаем node-llama-cpp
Откройте терминал. Не советую делать это «на скорую руку» — потом будете ругаться на версии.
mkdir my-mcp-agent && cd my-mcp-agent
npm init -y
npm install node-llama-cpp@3.1.4Почему именно 3.1.4, а не последняя? Потому что в версии 3.2.0 (вышла в начале мая) сломали обратную совместимость с некоторыми GGUF моделями. Разработчики обещают пофиксить к июню, но пока — 3.1.4.
Шаг 2. Создаём простой MCP-сервер через stdio
MCP по умолчанию использует JSON-RPC 2.0. Нам нужно написать процесс, который читает из stdin и пишет в stdout. Никаких HTTP, никаких сокетов — чистая классика Unix.
Создаём файл mcp-server.mjs:
// mcp-server.mjs
import { createInterface } from 'readline';
const rl = createInterface({ input: process.stdin, output: process.stdout });
// Список инструментов, которые модель может вызывать
const tools = {
getWeather: (city) => `${city}: +22°C, ясно`,
readFile: (path) => `# Содержимое ${path}\n\n` + require('fs').readFileSync(path, 'utf-8').slice(0, 500)
};
rl.on('line', (line) => {
try {
const request = JSON.parse(line);
if (request.method === 'mcp.listTools') {
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: Object.keys(tools).map(name => ({
name,
description: `Вызов ${name}`,
inputSchema: {
type: 'object',
properties: { arg: { type: 'string' } },
required: ['arg']
}
}))
}) + '\n');
} else if (request.method === 'mcp.callTool') {
const { name, arguments: args } = request.params;
if (tools[name]) {
const result = tools[name](args.arg);
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: { content: [{ type: 'text', text: result }] }
}) + '\n');
}
}
} catch (e) { /* молча игнорируем битые пакеты */ }
});‼️ Так делать НЕ надо: в реальном сервере нужно парсить аргументы по схеме, обрабатывать ошибки и возвращать JSON-RPC ошибки. Но для первого запуска — сойдёт.
Шаг 3. Загружаем GGUF модель и запускаем агентный цикл
Теперь самое мясо. Создадим agent.mjs, который:
- Загружает модель через
node-llama-cpp - Запускает дочерний процесс с нашим MCP-сервером
- Формирует системный промпт с описанием инструментов
- В цикле генерирует ответ, парсит вызовы инструментов, выполняет их через сервер и отправляет результат обратно модели
// agent.mjs
import { spawn } from 'child_process';
import { LlamaModel, LlamaContext, LlamaChatSession } from 'node-llama-cpp';
const modelPath = './models/qwen2.5-7b-q4_k_m.gguf';
const serverProcess = spawn('node', ['mcp-server.mjs'], { stdio: ['pipe', 'pipe', 'inherit'] });
const model = new LlamaModel({ modelPath, gpuLayers: 35 });
const context = new LlamaContext({ model });
const session = new LlamaChatSession({ context });
const systemPrompt = `Ты — агент с инструментами. Доступны:
- getWeather(city) — получить погоду
- readFile(path) — прочитать файл
Если нужно вызвать инструмент, ответь в формате:
{"tool": "имя", "arg": "значение"}
После получения результата продолжай рассуждение.`;
async function agentLoop(userMessage) {
let messages = [{ role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }];
while (true) {
const response = await session.prompt(messages, { maxTokens: 1024, temperature: 0.2 });
const toolMatch = response.match(/\{"tool":\s*"(\w+)",\s*"arg":\s*"(.*?)"\}/);
if (!toolMatch) {
console.log('Ответ модели:', response);
break;
}
const [, toolName, arg] = toolMatch;
const request = {
jsonrpc: '2.0',
id: 1,
method: 'mcp.callTool',
params: { name: toolName, arguments: { arg } }
};
serverProcess.stdin.write(JSON.stringify(request) + '\n');
const result = await new Promise(resolve => {
serverProcess.stdout.once('data', data => resolve(JSON.parse(data).result.content[0].text));
});
messages.push({ role: 'assistant', content: response });
messages.push({ role: 'user', content: `Результат инструмента: ${result}` });
}
}
// Запускаем
agentLoop('Какая погода в Москве? И прочитай файл notes.txt из текущей папки.');Обратите внимание: мы используем регулярку для парсинга JSON. Да, это хрупко. Но для демонстрации — нормально. В боевом проекте стоит использовать structured output или грамматику как в mcpx — это экономит контекст и снижает галлюцинации.
Типичные грабли и как их обойти
1Модель не хочет вызывать инструменты
Проблема: модель выдаёт обычный текст вместо JSON. Решение: понизьте температуру до 0.1, добавьте в промпт «Отвечай ТОЛЬКО JSON». Если не помогает — смените модель. Qwen2.5 7B и Llama 3.2 7B показывают лучшие результаты среди маленьких.
2Утечка памяти при долгих сессиях
node-llama-cpp держит весь контекст в памяти. Почистите старые сообщения: оставляйте только последние 4000 токенов. Или используйте LlamaContext.setContextSize().
3MCP-сервер не отвечает или падает
Мы запускаем его как дочерний процесс — если упадёт, агент зависнет. Добавьте перезапуск. Примерно так:
function startServer() {
const proc = spawn('node', ['mcp-server.mjs'], { stdio: ['pipe', 'pipe', 'inherit'] });
proc.on('exit', () => { setTimeout(startServer, 1000); });
return proc;
}Сравнение с альтернативами
| Подход | Плюсы | Минусы |
|---|---|---|
| Наш самописный сервер | Полный контроль, лёгкий вес, кастомная логика | Требует навыков Node.js, отсутствие боевого тестирования |
| Встроенный MCP в llama.cpp | Стабильнее, баги фиксят разработчики | Требует пересборки, менее гибкий |
| Python-сервер на MCP SDK | Зрелое сообщество, много готовых инструментов | Тяжеловесный, привязка к Python, сложнее с локальными моделями |
| Готовые агенты (AutoGPT, SuperAGI) | Работают из коробки | Закрытые или платные, нет тонкой настройки |
Если вы не хотите возиться с Node.js, но хотите попробовать MCP с уже готовым решением — посмотрите как превратить llama-cli в агента для кодинга. Там всё проще, но менее гибко.
Кому это вообще нужно?
- Разработчикам, которые хотят добавить AI-агента в свой продукт, но не хотят платить за API
- Исследователям, изучающим agentic patterns — тут можно ставить эксперименты на коленке
- Параноикам приватности: все данные остаются на вашей машине, ни один токен не уплывает в облако
- Тем, кто пишет диплом или pet-project — готовый MCP-сервер на Node.js выглядит солидно в резюме
Не подойдёт тем, кому нужно «просто чтобы работало» и без программирования. Для такого есть MCP-серверы через Web UI.