MCP сервер на Node.js с нуля: node-llama-cpp + GGUF туториал 2026 | AiManual
AiManual Logo Ai / Manual.
25 Май 2026 Инструмент

MCP from Scratch: пишем локального агента на node-llama-cpp и GGUF моделях за вечер

Полный гайд по созданию MCP-сервера на Node.js с локальной LLM через node-llama-cpp. Научу писать агентный цикл, JSON-RPC и stdio transport без готовых решений.

Все говорят про 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 (последняя на момент написания)
  • Терпеливый вайб — будем писать с нуля, без копипасты
💡
Если вы не хотите мучиться со сборкой моделей — посмотрите LM Studio MCP: Запускаем AI-агента для автоматизации новостей. Там подход через GUI, но мы идём другим путём.

Шаг 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, который:

  1. Загружает модель через node-llama-cpp
  2. Запускает дочерний процесс с нашим MCP-сервером
  3. Формирует системный промпт с описанием инструментов
  4. В цикле генерирует ответ, парсит вызовы инструментов, выполняет их через сервер и отправляет результат обратно модели
// 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.

Прогноз: к концу 2026 года MCP-серверы станут настолько же стандартными, как REST API. Но разница будет в том, что REST API вы покупаете у провайдеров, а MCP-сервер можно собрать самому — и контролировать всё до последнего байта. Те, кто напишет свой сейчас, через год будут заклёвывать новичков фразой «я это делал ещё до того, как это стало мейнстримом». И будут совершенно правы.

Подписаться на канал