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