Побудительный мотив был как обычно в виде хакатона, в котором я, если признаться честно, участвовать не стал по ряду причин, но тем не менее в новой технологии решил разобраться и сформировать минимальные представления. Все что попадалось на глаза выглядело не очень обнадеживающе и скорее однообразно. С места все сдвинулось после публикации. Пример оказался несложным и легко повторяемым, что и позволило начать копать вглубь темы. Вопросы на которые все таки хотелось ответить выглядели так:
-
Самый главный был о том, как же 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):
-
session(): Создание сессии для конкретного сервера -
get_tools(): Получение списка инструментов со всех серверов -
get_prompt(): Получение промпта с сервера -
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/
Добавить комментарий