MCP (Model Context Protocol) для неискушенных

от автора

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

  • Самый главный был о том, как же llm понимает какие сервера ей доступны и что эти сервера могут.

  • Второй вопрос был о том все ли llm дружат с mcp технологией. В первую очередь интересны модели, которые можно запускать локально.

  • Третий вопрос был о клиенте, который мог бы позволить работать не в консольном режиме но и не был бы при этом  Claude Desktop.

Изменив несколько код mcp сервера, добавил еще несколько инструментов:

Код простого mcp сервера
  import os from mcp.server.fastmcp import FastMCP  # Создаем MCP сервер mcp = FastMCP("FileTools")  # Базовая директория для файлов (вместо CFG.BASE_DIR) BASE_DIR = os.path.join(os.path.dirname(__file__), "files")  # Создаем базовую директорию если её нет os.makedirs(BASE_DIR, exist_ok=True)    @mcp.tool() def create_folder(folder_name: str) -> str:     """Создаёт папку с указанным именем в базовой директории."""     folder_path = os.path.join(BASE_DIR, folder_name)     try:         os.makedirs(folder_path, exist_ok=True)         return f"Папка '{folder_name}' успешно создана в {folder_path}"     except Exception as e:         return f"Ошибка при создании папки: {e}"  @mcp.tool() def create_text_file(file_name: str, content: str) -> str:     """Создаёт текстовый файл с указанным именем и содержимым в базовой директории."""     file_path = os.path.join(BASE_DIR, file_name)     try:         with open(file_path, 'w', encoding='utf-8') as file:             file.write(content)         return f"Файл '{file_name}' успешно создан в {file_path}"     except Exception as e:         return f"Ошибка при создании файла: {e}"  @mcp.tool() def read_text_file(file_name: str) -> str:     """Читает содержимое текстового файла."""     file_path = os.path.join(BASE_DIR, file_name)     try:         with open(file_path, 'r', encoding='utf-8') as file:             content = file.read()         return f"Содержимое файла '{file_name}':\n{content}"     except FileNotFoundError:         return f"Файл '{file_name}' не найден"     except Exception as e:         return f"Ошибка при чтении файла: {e}"  @mcp.tool() def list_files() -> str:     """Показывает список всех файлов и папок в базовой директории."""     try:         items = os.listdir(BASE_DIR)         if not items:             return "Директория пуста"                  files = []         folders = []         for item in items:             item_path = os.path.join(BASE_DIR, item)             if os.path.isfile(item_path):                 files.append(f"📄 {item}")             else:                 folders.append(f"📁 {item}")                  result = f"Содержимое директории {BASE_DIR}:\n"         if folders:             result += "Папки:\n" + "\n".join(folders) + "\n"         if files:             result += "Файлы:\n" + "\n".join(files)                  return result     except Exception as e:         return f"Ошибка при получении списка файлов: {e}"  @mcp.tool() def delete_file(file_name: str) -> str:     """Удаляет файл из базовой директории."""     file_path = os.path.join(BASE_DIR, file_name)     try:         if os.path.exists(file_path):             if os.path.isfile(file_path):                 os.remove(file_path)                 return f"Файл '{file_name}' успешно удален"             else:                 return f"'{file_name}' является папкой, не файлом"         else:             return f"Файл '{file_name}' не найден"     except Exception as e:         return f"Ошибка при удалении файла: {e}"  if __name__ == "__main__":     print(f"Запуск файлового сервера. Базовая директория: {BASE_DIR}")     mcp.run(transport="stdio")  

А для того, что бы понять взаимодействие с сервером использовал простой фрагмент кода в котором используются библиотеки MultiServerMCPClient из langchain_mcp_adapters.client import

Простой модуль для взаимодействия с mcp сервером без llm
#При использовании кода измените "args": [r".\mcp_server\server_2.py"], указав путь к вашей версии mcp сервера import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient  async def main():     client = MultiServerMCPClient(         {             "db_sqlite": {                 "command": "python",                 "args": [r".\mcp_server\server_2.py"],                 "transport": "stdio",                 # при необходимости можно добавить "env": {...}             }         }     )     try:         async with client.session("db_sqlite") as session:             tools = await client.get_tools(server_name="db_sqlite")             if tools:                 print("Сервер запущен успешно. Доступные инструменты:")                 for tool in tools:                     print(f"- {tool.name}: {tool.description}")             else:                 print("Сервер запущен, но инструменты не найдены.")     except Exception as e:         print("Ошибка при запуске или подключении к серверу:", e)  if __name__ == "__main__":     asyncio.run(main())

Код выполняет следующие действия:

С помощью MultiServerMCPClient  запускает заявленный mcp сервер, то есть отдельный запуск сервера не требуется!

Ремарка: MultiServerMCPClient — это класс из библиотеки langchain-mcp-adapters, который предназначен для подключения и взаимодействия с несколькими MCP (Model Context Protocol) серверами одновременно.

Поддерживаемые методы( я рассмотрю только использование метода get_tools):

  1. session(): Создание сессии для конкретного сервера

  2. get_tools(): Получение списка инструментов со всех серверов

  3. get_prompt(): Получение промпта с сервера

  4. get_resources(): Получение ресурсов с сервера

В результате запуска модуля получаем:

Сервер запущен успешно. Доступные инструменты:

  • create_folder: Создаёт папку с указанным именем в базовой директории.

  • create_text_file: Создаёт текстовый файл с указанным именем и содержимым в базовой директории.

  • read_text_file: Читает содержимое текстового файла.

  • list_files: Показывает список всех файлов и папок в базовой директории.

  • delete_file: Удаляет файл из базовой директории.

Можно сделать вывод, что благодаря классу MultiServerMCPClient, удалось получить как название доступных инструментов ( tools) так и описание их возможностей. Причем описание получено благодаря строке документации (docstring)! И это фактически промпт, который подскажет LLM что можно сделать с помощью конкретного инструмента. То есть, чем подробнее будет описание, тем более безупречно будет использоваться инструмент.

Отлично!
Следующий вопрос это как воспользоваться каким либо инструментом?
Для этого попробуем выполнить код:

Используем предложенный mcp сервером инструмент
#При использовании кода измените "args": [r".\mcp_server\server_2.py"], указав путь к вашей версии mcp сервера import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_mcp_adapters.tools import load_mcp_tools  async def main():     # Конфигурация MCP сервера     client = MultiServerMCPClient(         {             "files": {                 "command": "python",                 "args": [r".\mcp_server\server_2.py"],                 "transport": "stdio",             }         }     )      try:         # Открываем сессию с сервером "files"         async with client.session("files") as session:             # Загружаем инструменты с сервера             tools = await load_mcp_tools(session)             if not tools:                 print("Инструменты не найдены на сервере.")                 return              print("Доступные инструменты:")             for tool in tools:                 print(f"- {tool.name}: {tool.description}")              # Предположим, что есть инструмент для списка файлов, например "list_files"             list_files_tool = next((t for t in tools if t.name == "list_files"), None)             print ('Тип list_files_tool',type(list_files_tool))             print ('так выглядит list_files_tool: \n',list_files_tool)             print(list_files_tool.coroutine)              if not list_files_tool:                 print("Инструмент 'list_files' не найден.")                 return              # Формируем запрос к инструменту (аргументы зависят от реализации инструмента)             # Например, без аргументов или с указанием пути             input_args = {"path": "."}  # Текущая директория              # Вызываем инструмент асинхронно             result = await list_files_tool.arun(input_args)             print("\nРезультат запроса (список файлов и папок):")             print(result)      except Exception as e:         print("Ошибка при работе с MCP сервером:", e)  if __name__ == "__main__":     asyncio.run(main()) 

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

Теперь становится более понятным процесс взаимодействия mcp сервера и LLM.

Посредником в этом взаимодействии в наиболее простом варианте служат классы фреймворка Langchain и Langgraph в частности функция create_react_agent ( пример его использования есть в статье ,упоминаемой в самом начале)

Ремарка:
create_react_agent — это функция из библиотеки LangGraph, которая создает агента с архитектурой ReAct (Reasoning and Acting). Вот её описание:

Предназначение:

  • Создание интеллектуального агента, который может reasoning (рассуждать) и действовать с помощью инструментов

  • Автоматическое связывание языковой модели с набором инструментов

    Ключевые характеристики:

    • Динамический выбор инструментов

    • Генерация рассуждений перед действием

    • Поддержка сложных многошаговых задач

    • Встроенная логика обработки инструментов

То есть теперь весь процесс можно описать простыми словами :

  • стартуем сервер

  • получаем список инструментов

  • сообщаем LLM о их возможностях и способе вызова.

  • LLM по мере необходимости использует доступные инструменты

Итак, теперь второй вопрос про выбор LLM.

Ответ следующий: далеко не все LLM поддерживаются режим tools. Предложенная модель qwen вполне справляется с этой задачей. Другие модели в своем большинстве сообщают о невозможности работать с tools. Но, полагаю, это дело времени.

И вопрос про клиента. Помимо консольного режима конечно же можно использовать интерфейсы разного типа, какой вам по душе. Я пробовал PyQt5 и возможно как продолжение темы опишу свой опыт использования mcp сервера для sqllite базы данных.

Надеюсь, материал будет полезен всем, кто заинтересовался технологией.


ссылка на оригинал статьи https://habr.com/ru/articles/918390/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *