Как стать автором
Обновить
94.29
red_mad_robot
№1 в разработке цифровых решений для бизнеса

MCP своими руками

Время на прочтение9 мин
Количество просмотров2.8K

Привет! Это Влад Шевченко, технический директор направления искусственного интеллекта red_mad_robot. Сегодня я хочу рассказать, что из себя представляет протокол MCP от Anthropic — для этого лучше всего создать его аналог собственными руками.

Большие языковые модели (LLM) стали важной частью современных технологий, но они ограничены в своей способности взаимодействовать с внешним миром. Model Context Protocol (MCP) — это протокол, разработанный Anthropic, который позволяет AI-моделям взаимодействовать с внешними сервисами и инструментами. Эта статья предназначена для разработчиков, которые хотят глубже понять, как работает MCP, чтобы научиться создавать подобные механизмы самостоятельно.

Function calling

AI-модель закрыта сама в себе, она ограничена знаниями, которые получила при обучении, поэтому ей необходимы средства взаимодействия с внешним миром. Так появился инструмент function calling — он добавляет в контекстное окно модели информацию о возможности вызова внешней функции для поиска ответов на вопросы.

Диаграмма от OpenAI, иллюстрирующая работу function calling
Диаграмма от OpenAI, иллюстрирующая работу function calling

На диаграмме видно, как на запрос (1) от пользователя модель возвращает ответ (2) о необходимости вызова сторонней функции. И уже в следующем запросе (4) к модели передаётся ответ от этой функции.

Таким образом, ответственность за вызов внешней функции лежит на клиенте или агенте, сама модель этого делать не умеет. В API это называется «tools» — расскажем о вызове внешних инструментов подробнее.

Примеры описания внешних инструментов

Чтобы узнать цену акций — для моделей Anthropic.

{
  "name": "получить_цену_акции",
  "description": "Получает текущую цену акции для заданного тикерного символа. Тикерный символ должен быть действительным символом для публично торгуемой компании на крупной американской фондовой бирже, такой как NYSE или NASDAQ. Инструмент вернет последнюю цену сделки в долларах США. Его следует использовать, когда пользователь спрашивает о текущей или самой последней цене конкретной акции. Он не предоставит никакой другой информации об акции или компании.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Тикерный символ акции, например, AAPL для Apple Inc."
      }
    },
    "required": ["ticker"]
  }
}

Чтобы отправить email — для моделей OpenAI.

{
    "type": "function",
    "name": "send_email",
    "description": "Отправить электронное письмо указанному получателю с темой и сообщением.",
    "parameters": {
        "type": "object",
        "properties": {
            "to": {
                "type": "string",
                "description": "Адрес электронной почты получателя."
            },
            "subject": {
                "type": "string",
                "description": "Тема электронного письма."
            },
            "body": {
                "type": "string",
                "description": "Текст сообщения электронного письма."
            }
        },
        "required": [
            "to",
            "subject",
            "body"
        ],
        "additionalProperties": False
    }
}

С помощью внешних инструментов можно повысить качество ответов, используя размышления Chain of thought и reasoning, а также структурированный вывод Structured Outputs. Более подробное описание работы tools читайте в статьях от Anthropic и OpenAI.

Важные особенности function calling

Именно модель решает — вызывать внешний инструмент или нет. Поэтому всегда есть вероятность, что сперва модель сама попробует ответить на вопрос пользователя. Однако в API можно принудительно вызвать конкретный инструмент, если вызов обязателен. Это ограничение важно учитывать при разработке AI-агентов.

Function calling позволяет AI-моделям запрашивать внешние данные через специально описанные инструменты, но ответственность за выполнение этих запросов лежит на клиентской стороне.

Идея протокола MCP

Как показала практика, многим агентам нужны одни и те же инструменты для взаимодействия с внешним миром и обогащения контекста: чтение файлов в конкретной директории, доступ к GitHub и базам данных. Чтобы не разрабатывать их постоянно и не хранить рядом с кодом агента, в Anthropic решили создать отдельные сервисы. Таким образом, одним инструментом может пользоваться сразу рой агентов.

Делаем свою реализацию

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

Архитектура решения

Наша реализация состоит из трёх основных компонентов:

  1. MCP сервер — предоставляет API для доступа к инструментам

  2. Агент — использует сервер для получения данных

  3. Ollama — локальный сервер с моделью AI

Схема взаимодействия основных компонентов решения
Схема взаимодействия основных компонентов решения

Конкретная реализация требует ввести часть ограничений:

  • Мы используем локальную модель

  • API от OpenAI

  • Сервер Ollama

  • Код пишем на Python

  • Опускаем всё, что связано с развертыванием и установкой зависимостей

Серверная часть

Создадим сервер, который будет предоставлять информацию о доступных инструментах и их API.

# server.py
# - Установите зависимости: pip install fastapi uvicorn requests
# - Запустите сервер: python server.py
# - Документация доступна по адресу http://0.0.0.0:8000/docs
from fastapi import FastAPI, HTTPException
import uvicorn
import requests
import xml.etree.ElementTree as ET

app = FastAPI()

# Описание инструмента получения цены акции на MOEX
STOCK_PRICE_TOOL = {
    "type": "function",
    "function": {
        "name": "get_stock_price",
        "description": "Получает текущую цену акции на Московской бирже по её тикеру",
        "parameters": {
            "type": "object",
            "properties": {
                "ticker": {
                    "type": "string",
                    "description": "Тикерный символ акции, например, SBER для Сбербанка или GAZP для Газпрома",
                }
            },
            "required": ["ticker"],
        },
    },
}


@app.get("/tools")
async def get_tools():
    """Возвращает описание доступных инструментов"""
    return [STOCK_PRICE_TOOL]


@app.get("/get_stock_price")
async def get_stock_price(ticker: str):
    """Получает текущую цену акции на Московской бирже по её тикеру"""

    # Запрос к API Московской биржи
    url = f"https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities.xml?iss.meta=off&iss.only=marketdata&marketdata.columns=SECID,LAST"

    try:
        response = requests.get(url)
        response.raise_for_status()

        # Парсинг XML и поиск акции по тикеру
        root = ET.fromstring(response.text)
        for row in root.findall(".//row"):
            if row.get("SECID") == ticker:
                price = row.get("LAST")
                return {
                    "ticker": ticker,
                    "price": float(price) if price else None,
                    "currency": "RUB",
                }

        return {"error": f"Акция с тикером {ticker} не найдена"}

    except requests.RequestException as e:
        raise HTTPException(
            status_code=500, detail=f"Ошибка при запросе к API MOEX: {str(e)}"
        )


if __name__ == "__main__":
    uvicorn.run("server:app", host="0.0.0.0", port=8000, reload=True)

Сервер запущен, и мы можем посмотреть цену акции на сегодня, например, у Сбера.

curl "http://localhost:8000/get_stock_price?ticker=SBER"

Пример ответа сервера.

{
    "ticker": "SBER",
    "price": 283.99,
    "currency": "RUB"
}

Клиентская часть

Теперь напишем простого агента, который будет использовать наш сервер для получения актуальной информации.

# agent.py
# - Установите зависимости: pip install openai asyncio
# - Запустите агента: python agent.py
import requests
from openai import OpenAI
import json
import asyncio


# Глобальная переменная для модели
MODEL_NAME = "qwen2.5:14b"

# URL для сервера Ollama
OLLAMA_API_URL = "http://localhost:11434/v1"

# URL для нашего MCP сервера
MCP_SERVER_URL = "http://localhost:8000"


# Функция для получения инструментов с сервера
def get_tools():
    try:
        response = requests.get(f"{MCP_SERVER_URL}/tools")
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"Ошибка при получении инструментов с MCP сервера: {e}")
        return []


# Создаем клиента OpenAI с указанием базового URL для Ollama
client = OpenAI(
    base_url=OLLAMA_API_URL,
    api_key="ollama",  # Ollama не требует API ключа, но поле обязательно
)


# Функция для вызова инструмента
def call_tool(tool_name, params):
    """
    Вызывает инструмент на сервере с заданными параметрами.
    
    Args:
        tool_name: Имя инструмента для вызова
        params: Параметры в виде словаря
        
    Returns:
        dict: Результат вызова инструмента
    """
    try:
        response = requests.get(f"{MCP_SERVER_URL}/{tool_name}", params=params)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"Ошибка при вызове инструмента {tool_name}: {e}")
        return {"error": str(e)}


# Функция для обработки сообщений и вызова инструментов
async def process_message(messages):
    """
    Обрабатывает сообщения, отправляет их модели и обрабатывает вызовы инструментов.
    
    Args:
        messages: Список сообщений для отправки модели
        
    Returns:
        str: Ответ модели или результат вызова инструментов
    """
    tools = get_tools()

    # Выполняем запрос к API
    response = client.chat.completions.create(
        model=MODEL_NAME,
        messages=messages,
        tools=tools,
        temperature=0.7,
    )

    # Получаем ответ модели
    assistant_message = response.choices[0].message

    # Если есть вызовы инструментов, обрабатываем их
    if assistant_message.tool_calls:
        for tool_call in assistant_message.tool_calls:
            # Извлекаем имя инструмента и параметры
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            # Вызываем инструмент
            tool_result = call_tool(function_name, function_args)

            # Добавляем результат вызова инструмента в сообщения
            messages.append(assistant_message)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": function_name,
                    "content": json.dumps(tool_result, ensure_ascii=False),
                }
            )

        # Делаем еще один запрос с обновленными сообщениями
        return await process_message(messages)

    # Если нет вызовов инструментов, возвращаем ответ
    messages.append(assistant_message)
    return assistant_message.content


# Функция для взаимодействия с агентом
async def ask_investment_agent(query):
    """
    Основной интерфейс для взаимодействия с инвестиционным агентом.
    
    Args:
        query: Запрос пользователя
        
    Returns:
        str: Ответ агента
    """
    messages = [
        {
            "role": "system",
            "content": """Вы — инвестиционный помощник, который помогает 
            пользователям получать актуальную информацию о ценах акций 
            на Московской бирже. Используйте инструмент get_stock_price 
            для получения цены акции в рублях, без копеек.
            Всегда отвечайте на русском языке.""",
        },
        {"role": "user", "content": query},
    ]

    return await process_message(messages)


# Пример использования
if __name__ == "__main__":

    async def main():
        query = input("Введите ваш вопрос об акциях: ")
        response = await ask_investment_agent(query)
        print(response)

    asyncio.run(main())

Теперь можно задать вопрос и получить нужную информацию.

> Цена на акции аэрофлота? 
Текущая цена акций Аэрофлота (AFLT) на Московской бирже составляет 75 рублей.

LLM смогла понять, что нужно запросить тикер AFLT во внешнем методе и использовать ответ сервера для формирования актуальной цены акции.

Мы создали простую имитацию MCP сервера, который предоставляет информацию о ценах акций и агента, который динамически обнаруживает инструменты и использует их для ответов на вопросы пользователей.

Весь код решения можно найти на нашем GitHub
Весь код решения можно найти на нашем GitHub

Используем фреймворк с агентами от OpenAI

OpenAI выпустила многообещающую библиотеку для создания агентов, давайте реализуем работу с сервером через неё.

# agent_openai.py
# - Установите зависимости: pip install openai-agents
# - Запустите агента: python agent_openai.py
import requests
import asyncio
from typing import Dict, Any, List
import json

from openai import AsyncOpenAI
from agents import (
    Agent,
    Runner,
    RunConfig,
    OpenAIChatCompletionsModel,
    FunctionTool,
    RunContextWrapper,
)

# Глобальная переменная для модели
MODEL_NAME = "qwen2.5:14b"

# URL для сервера Ollama
OLLAMA_API_URL = "http://localhost:11434/v1"

# URL для нашего MCP сервера
MCP_SERVER_URL = "http://localhost:8000"


# Получение информации об инструментах с MCP сервера
def get_tools() -> List[Dict[str, Any]]:
    """Получает список доступных инструментов с MCP сервера"""
    try:
        response = requests.get(f"{MCP_SERVER_URL}/tools")
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"Ошибка при получении инструментов с MCP сервера: {e}")
        return []


# Функция для создания инструментов
def create_tools():
    """
    Динамически создает инструменты на основе информации с сервера.
    Каждый инструмент оборачивается в FunctionTool от OpenAI.
    
    Returns:
        list: Список инструментов для использования агентом
    """
    tools = []
    for tool_info in get_tools():

        async def run_function(ctx: RunContextWrapper[Any], args: str) -> str:
            """Функция для выполнения вызова инструмента"""
            response = requests.get(
                f"{MCP_SERVER_URL}/{tool_info['function']['name']}",
                params=json.loads(args),
            )
            response.raise_for_status()
            return response.json()

        tool = FunctionTool(
            name=tool_info["function"]["name"],
            description=tool_info["function"]["description"],
            params_json_schema=tool_info["function"]["parameters"],
            on_invoke_tool=run_function,
        )
        tools.append(tool)
    return tools


# Функция для создания агента
def create_investment_agent(tools) -> Agent:
    """Создает и настраивает инвестиционного агента"""
    agent = Agent(
        name="Инвестиционный помощник",
        instructions="""Вы — инвестиционный помощник, который помогает 
        пользователям получать актуальную информацию о ценах акций 
        на Московской бирже. Используйте инструмент get_stock_price 
        для получения цены акции в рублях без копеек. 
        Всегда отвечайте на русском языке.""",
        tools=tools,
    )

    return agent


# Основная функция для взаимодействия с агентом
async def ask_investment_agent(query: str) -> str:
    """
    Отправляет запрос инвестиционному агенту и получает ответ

    Args:
        query: Запрос пользователя

    Returns:
        str: Ответ агента
    """
    tools = create_tools()
    agent = create_investment_agent(tools)

    # Настраиваем конфигурацию запуска
    run_config = RunConfig(
        model=OpenAIChatCompletionsModel(
            model=MODEL_NAME,
            openai_client=AsyncOpenAI(
                base_url=OLLAMA_API_URL,
                api_key="ollama",  # required, but unused
            ),
        ),
        tracing_disabled=True,
    )

    # Запускаем агента с заданным запросом
    result = await Runner.run(agent, input=query, run_config=run_config)

    return result.final_output


# Пример использования
if __name__ == "__main__":

    async def main():
        query = input("Введите ваш вопрос об акциях: ")
        response = await ask_investment_agent(query)
        print(response)

    asyncio.run(main())

Получаем ответ от агента.

> Цена на акцию Аэрофлота? 
Текущая цена акции Аэрофлота (AFLT) на Московской бирже составляет 75 рублей.

Общий итог

Мы реализовали идею MCP без использования самого протокола — сервер самостоятельно рассказывает, как и какие методы использовать.

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


Над материалом работали

текст и код — Влад Шевченко

редактура — Игорь Решетников

иллюстрации — Юля Ефимова

Теги:
Хабы:
+6
Комментарии4

Публикации

Информация

Сайт
redmadrobot.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия