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

  • Самый главный был о том, как же 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 базы данных.

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

Комментарии (0)