MCP. Как ИИ обрести руки для взаимодействия с внешним миром?

Много ли мы могли бы сделать будь у нас разум, знания, но не было бы тела? Пожалуй наверное не много. Поговорим о том как MCP позволяет обрести LLM руки для взаимодействия с внешним миром

MCP. Как ИИ обрести руки для взаимодействия с внешним миром?
Photo by yousef samuil / Unsplash

Много ли мы могли бы сделать будь у нас разум, знания, но не было бы тела? Пожалуй наверное не много. Собственно это и есть основная проблема больших языковых моделей. У них есть "зайчатки" разума, но нет рук - инструментов с помощью которых LLM смогла бы взаимодействовать с внешним миром.

Представим ситуацию, пользователь задает нейросети простой вопрос:

- Привет нейросеть, подскажи как завтра лучше одеться с учетом погоды?
- ...

Увы, нейросеть не знает ни где пользователь находиться, ни какое сегодня число. О том какая за окном завтра будет погода, она и подавно не знает. Скорее всего она просто сочинит вам какой-то ответ, но не решит вашу задачу.

Здесь стоит отметить, что вероятно любая интеграция с языковой моделью сводиться к подмешиванию к исходному запросу некоторого контекста и "системных промптов". Представим что наше сообщение не уходит к нейросети напрямую, проходит через некоторое прокси который знает где мы находимся, какой сегодня день. Такой прокси может модифицировать исходное сообщение, например так:

- Информация о том, где находится пользователь и сегодняшняя дата записана в этом JSON { "city": "Tomsk", country: "Russian Federation", "currentDate": "03.11.2025" }, используя эти данные ответь на запрос пользователя: "Привет нейросеть, подскажи как завтра лучше одеться с учетом погоды?"

А вот собственно реальный ответ одной из моделей:

Gemma 3:4b

Уже ближе к решению нашей задачи. Вероятно многие уже догадались об основном принципе работы.

Итак, что бы нам потребовалось что бы реализовать нечто подобное:

  • Некий "прокси" - ПО способное подмешать что-то к основному запросу пользователя. Наверняка вы слышали, о таком понятии как "агент" - в нашем контексте это именно он.
  • Некий сервер - который смог бы сформировать JSON с информацией о погоде
  • LLM - в качестве "мозга", который способен обработать эту информацию и дать пользователю вразумительный ответ

Model Context Protocol

Аббревиатура MCP расшифровывается как Model Context Protocol. Подробнее о самом протоколе можно прочитать здесь, я же остановлюсь на основных моментах.

Говоря простым языком MCP это набор правил определяющий взаимодействие этих трех компонентов. Агент - в контексте протокола называют MCP клиентом, сервер возвращающий некие данные - MCP сервером. Их взаимодействие основано на JSON-RPC 2.0. Взаимоействие может проходить путем различных транспортных протоколов, например c помощью: Streamable HTTP, WebSocket или путем стандартного ввода вывода stdin/stdout.

Протокол так же вводит понятия инструментов (tools) и ресурсов (resources). Они реализуются на стороне MCP сервера. MCP клиент запрашивает их в начале своей работы. В определенном смысле это "REST API для LLM". Tools определяют какие действия доступны, ресурсы - то над чем могут выполняться действия. Например, если мы создаем агента для написания кода в проекте, ресурсами будут файлы, инструментами же будут являться различные файловые операции - нашему агенту нужно читать содержимое файлов, каталогов, записывать и создавать файлы.

Здесь стоит пояснить, так как это не очевидно для нашего упрощенного примера, который я привел в начале статьи показывающего лишь основную идею. Итак, MCP клиент запрашивает с MCP-сервера набор доступных инструментов и сообщает его LLM. Утрируем до:

-Уважаемая LLM, у нас есть вот такой набор операций доступных для использования: ... , если эта операция поможет выполнить запрос пользователя сформируй JSON в вот таком то формате ... . Запрос пользователя: ...

В общем этой основной теории нам уже хватит что бы написать несложный MCP сервер. Перейдем к практике.

Погодный MCP сервер

Продолжим тематику нашего примера в начале статьи и сделаем MCP сервер способный получать данные о погоде в определенном городе. Благо в нашем распоряжении уже есть некоторый SDK. Я буду использовать NodeJS, TypeScript и пакет NPM пакет @modelcontextprotocol/sdk, но существуют пакеты и для других языков.

Начнем с главной задачи - получать погоду. Воспользуюсь сервисом https://open-meteo.com и пакетом openmeteo, что бы сделать функции для получения погоды:

import { fetchWeatherApi } from "openmeteo";

export async function getCityCoordinates(city: string) {
  const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(
    city
  )}&count=1&language=en&format=json`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Error getting coordinates: ${res.status}`);
  const data = await res.json();

  if (!data.results?.length) {
    throw new Error(`"${city}" not found`);
  }

  const { latitude, longitude, name, country } = data.results[0];
  return { latitude, longitude, name, country };
}

export async function getDailyWeather(lat: number, lon: number) {
  const params = {
    latitude: lat,
    longitude: lon,
    daily: [
      "temperature_2m_max",
      "temperature_2m_min",
      "precipitation_sum",
      "weathercode",
    ],
    timezone: "auto",
  };

  const url = "https://api.open-meteo.com/v1/forecast";
  const responses = await fetchWeatherApi(url, params);

  const weather = responses[0];
  const daily = weather.daily();

  const result = {
    maxTemp: daily.variables(0).valuesArray()[0],
    minTemp: daily.variables(1).valuesArray()[0],
    precipitation: daily.variables(2).valuesArray()[0],
    weatherCode: daily.variables(3).valuesArray()[0],
  };

  return result;
}

export async function getDailyWeatherByCity(city: string) {
  const { latitude, longitude } = await getCityCoordinates(city);
  return getDailyWeather(latitude, longitude);
}

Код получения данных о погоде

Теперь займемся MCP сервером. В нашем случае все просто, я бы даже сказал примитивно:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { z } from "zod";
import { getDailyWeatherByCity } from "./weather";

const server = new McpServer({
  name: "weather",
  version: "1.0.0",
});

server.registerTool(
  "get-weather-in-city",
  {
    title: "Get weather",
    description: "Get the weather forecast for the next day",
    inputSchema: { city: z.string() },
  },
  async ({ city }) => {
    const result = await getDailyWeatherByCity(city);

    return {
      content: [
        {
          type: "text",
          text: `The weather in ${city} tomorrow: maximum temperature ${result.maxTemp}°C, minimum ${result.minTemp}°C, precipitation ${result.precipitation} mm, weather code ${result.weatherCode}.`,
        },
      ],
      structuredContent: { result },
    };
  }
);

const transport = new StdioServerTransport();
server.connect(transport);

Реализация MCP сервера

Создаем экзэмпляр McpServer-а, регистрируем инструмент. Даем ему имя, описание, описываем схему входных данных. Далее у нас функция обработчик возвращающая результат. В этом примере использую StdioServerTransport, что было обусловлено инструментом с помощью которого будем проверять результат.

Инструментом оказался ollama-mcp-bridge с которым пришлось повозиться. На данный момент судя по всему в OpenSource сообществе эта тема развита еще не сильно, инструментов не много их поддержка вероятно тоже не сильно активная.

P.S: в видео qwen2.5:7b безбожно тупит и не может все никак понять что от нее требуется. Вероятно это следствие того что модель сама по себе не большая, но вполне возможно что и сами промпты следует написать лучше, возможно есть какие то другие причины.

На этом пожалуй все увидимся в слудющих статьях где разберем еще что нибдуь интересное!