Кратко
Вызов функций дает модели способ запросить инструмент. MCP предоставляет приложению стандартный способ обнаруживать и вызывать инструменты на сервере. Используйте прямую интеграцию функций для небольшого набора инструментов, которыми владеет ваше приложение. Добавляйте MCP, когда хотите потреблять совместимую службу или выставлять инструменты через общий интерфейс. Ниже вы подключите один и тот же поиск заказа двумя способами через CometAPI, используя один полный цикл агента.
Основные выводы
- Начинайте с прямой диспетчеризации, когда нескольким инструментам служит одно приложение; используйте MCP, когда совместимая служба инструментов или общий интерфейс упрощают сопровождение интеграции.
- Обе схемы требуют полного цикла: описать инструмент, получить вызов, выполнить его, вернуть результат и дать модели ответить.
- Вы можете сохранить одну и ту же функцию, видимую для модели, меняя лишь способ ее выполнения — с прямого обработчика на MCP‑клиент.
- Держите контракты инструментов узкими и проверяйте результаты на соответствие задаче, которую должен завершить агент.
Как сочетаются вызов функций и MCP
Саппорт‑агент может объяснить задержку доставки, но ему нужен доступ к системе заказов, чтобы сказать клиенту, где его посылка. Вызов функций дает модели способ запросить такой поиск. Ваше приложение выполняет поиск и возвращает результат. Обработчик может использовать локальную библиотеку, базу данных или удаленный API.
Чтобы выполнить этот запрос через MCP, приложение использует клиент для обнаружения инструментов сервера с помощью tools/list и их вызова через tools/call. Приложение по‑прежнему решает, какие обнаруженные инструменты выставлять своей модели.

Рисунок 1. MCP обеспечивает общий интерфейс между AI‑приложениями и внешними системами. Источник: официальный вводный материал MCP. © 2024–2025 Anthropic, PBC и участники. Воспроизведено без изменений по лицензии MIT; полные атрибуции и уведомление о разрешении см. в оригинале.
MCP и вызов функций: ключевые отличия
Последняя строка — ключ к совместному использованию. Модель может запросить get_order_status, в то время как приложение выбирает, вызывать ли собственный обработчик или MCP‑сервер. Определение функции, видимое модели, может оставаться прежним.
Выберите интеграцию под нужды вашего приложения
Мы рекомендуем начинать с прямой интеграции, когда вы владеете несколькими инструментами, используемыми одним приложением. Нужно меньше разворачивать, а путь от запроса модели до вашего бизнес‑кода легко проследить.
MCP становится полезным, когда совместимый сервер уже предоставляет нужные возможности или когда нескольким AI‑приложениям требуется общий интерфейс инструментов. Один и тот же инструмент заказа может обслуживать ассистента поддержки и внутреннее операционное приложение, при этом каждый хост контролирует, как его пользователи получают к нему доступ.
Диспетчер может поддерживать и внутренние функции, и MCP‑сервисы, направляя каждую операцию к ее реализации. Библиотеки и REST API также могут быть общими; выбирайте MCP, когда его стандартные интерфейсы обнаружения и вызова полезны для приложений‑потребителей инструментов.
Постройте полный цикл работы инструмента через CometAPI
Мы используем одну операцию только для чтения — get_order_status, и один вымышленный заказ — ORD-104. Сначала пример вызывает операцию напрямую, затем запускает ее через локальный MCP‑сервер. В обоих режимах для инференса модели используется Responses API CometAPI.

Рисунок 2. Один агентский цикл может поддерживать прямое и MCP‑выполнение. Оригинальная иллюстрация, основанная на архитектуре MCP и схеме вызова функций CometAPI.
Шаг 1. Подготовьте проект
Используйте Python 3.11 или новее и ключ API CometAPI. Этот пошаговый пример проверен с Python 3.12.14 и mcp==2.3.0. Следуйте краткому руководству CometAPI, чтобы создать ключ. В оболочке macOS или Linux создайте проект и активируйте виртуальную среду:
mkdir mcp-agent-demo
cd mcp-agent-demo
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "mcp==2.3.0"
export COMETAPI_KEY="YOUR_COMETAPI_KEY"
Замените заполнитель вашим ключом и не публикуйте его в исходном коде. В этом walkthrough зафиксирована версия MCP SDK, используемая в примере; официальное руководство по Python‑серверу ориентировано на API 2.x. Две команды агента ниже отправляют запросы в CometAPI и расходуют кредит API.
Шаг 2. Определите поиск заказа
Сохраните это как orders.py. Фиксированные данные делают walkthrough самодостаточным; позже вы можете заменить поиск своей службой.
"""Данные о заказах только для чтения, вымышленные; никогда не подключается к реальной системе заказов."""
import re
def get_order_status(order_id: str) -> dict[str, str]:
"""Ищет вымышленный заказ; ORD-104 — единственная доступная запись."""
if not isinstance(order_id, str) or not re.fullmatch(r"ORD-[0-9]{3}", order_id):
raise ValueError("order_id должен иметь форму ORD-104")
if order_id != "ORD-104":
raise ValueError("Заказ не найден в этой демонстрации")
return {"order_id": order_id, "status": "shipped", "source": "fictional_demo"}
Вызов get_order_status("ORD-104") возвращает такой фрагмент данных:
{"order_id": "ORD-104", "status": "shipped", "source": "fictional_demo"}
Шаг 3. Добавьте цикл агента
Сохраните следующий полный скрипт как agent.py. Он регистрирует функцию, отправляет вопрос в CometAPI, выполняет запрошенные вызовы и возвращает каждый результат, используя исходный call_id.
Определение, видимое для модели, использует поле parameters Responses API CometAPI. MCP предоставляет inputSchema; этот пример сверяет обнаруженный контракт со схемой из одной строки order_id перед тем, как выставить проверенную функцию.
"""Преднамеренно узкий мост direct/MCP, не общий адаптер MCP."""
import argparse
import asyncio
import json
import os
from pathlib import Path
import sys
from urllib.request import Request, urlopen
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
from orders import get_order_status
NAME = "get_order_status"
SCHEMA = {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"], "additionalProperties": False}
TOOL = {"type": "function", "name": NAME, "description": "Поиск вымышленного заказа ORD-104.",
"parameters": SCHEMA, "strict": True}
def validate_arguments(arguments):
# Точная проверка для SCHEMA выше; намеренно не движок JSON Schema.
if (not isinstance(arguments, dict) or set(arguments) != {"order_id"}
or not isinstance(arguments["order_id"], str)):
raise ValueError("Ожидался ровно один строковый аргумент: order_id")
def validate_catalog(tools):
if len(tools) != 1 or tools[0].name != NAME:
raise RuntimeError("Неожиданный каталог инструментов MCP")
schema = tools[0].input_schema
if (schema.get("type") != "object" or schema.get("required") != ["order_id"]
or set(schema.get("properties", {})) != {"order_id"}
or schema["properties"]["order_id"].get("type") != "string"):
raise RuntimeError("Схема MCP не соответствует этому проверенному демонстрационному инструменту")
async def dispatch(name, arguments, client=None):
if name != NAME:
raise ValueError("Инструмент не в списке разрешенных")
try:
validate_arguments(arguments)
if client is None:
return {"is_error": False, "data": get_order_status(**arguments)}
except ValueError as error:
return {"is_error": True, "error": str(error)}
result = await client.call_tool(name, arguments)
if getattr(result, "result_type", "complete") != "complete":
raise RuntimeError("Результаты MCP, требующие ввода, вне рамок этой демонстрации")
if any(block.type != "text" for block in result.content):
raise RuntimeError("Нетекстовый контент MCP вне рамок этой демонстрации")
return {"is_error": bool(result.is_error),
"content": [block.text for block in result.content],
"structured_content": result.structured_content}
async def run_agent(send, execute, question, max_rounds=4):
history = [{"role": "user", "content": question}]
seen_call_ids = set()
for _ in range(max_rounds):
response = send({"model": "gpt-6-astra", "input": list(history),
"tools": [TOOL], "reasoning": {"effort": "low"}})
if response.get("status") != "completed" or response.get("error"):
raise RuntimeError("Ответ модели не удался или не завершился")
output = response["output"]
# Документированный ручной повтор CometAPI: сохраняйте ВСЕ элементы, опустите верхнеуровневые id.
history.extend({key: value for key, value in item.items() if key != "id"}
for item in output)
blocks = [block for item in output if item["type"] == "message"
for block in item.get("content", [])]
if any(block["type"] == "refusal" for block in blocks):
raise RuntimeError("Модель отказалась от запроса")
calls = [item for item in output if item["type"] == "function_call"]
if not calls:
text = "\n".join(block["text"] for block in blocks
if block["type"] == "output_text")
if not text:
raise RuntimeError("Итоговый текст не возвращен")
return text
for call in calls:
call_id = call["call_id"]
if not isinstance(call_id, str) or not call_id or call_id in seen_call_ids:
raise ValueError("Отсутствует или дублируется function call_id")
seen_call_ids.add(call_id)
try:
arguments = json.loads(call["arguments"])
except (ValueError, TypeError):
result = {"is_error": True, "error": "Аргументы должны быть текстом JSON"}
else:
result = await execute(call["name"], arguments)
print(json.dumps({"tool": call["name"], "arguments": call["arguments"],
"result": result}), file=sys.stderr)
history.append({"type": "function_call_output", "call_id": call_id,
"output": json.dumps(result)})
raise RuntimeError("Цикл инструмента достиг лимита раундов")
def send_live(payload):
key = os.environ["COMETAPI_KEY"] # В живом режиме только читается ключ вызывающего.
request = Request("https://api.cometapi.com/v1/responses",
data=json.dumps(payload).encode(),
headers={"Authorization": f"Bearer {key}",
"Content-Type": "application/json"})
with urlopen(request, timeout=45) as response:
return json.load(response)
def server_parameters():
return StdioServerParameters(command=sys.executable,
args=[str(Path(__file__).with_name("server.py"))])
async def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mode", choices=["direct", "mcp"], required=True)
parser.add_argument("--question", default="Используйте инструмент, чтобы проверить заказ ORD-104.")
args = parser.parse_args()
if args.mode == "direct":
answer = await run_agent(send_live, dispatch, args.question)
else:
async with Client(stdio_client(server_parameters())) as client:
discovered = await client.list_tools()
validate_catalog(discovered.tools)
async def execute(name, arguments):
return await dispatch(name, arguments, client)
answer = await run_agent(send_live, execute, args.question)
print(answer)
if __name__ == "__main__":
asyncio.run(main())
Цикл сохраняет исходный вопрос и каждый возвращенный элемент ответа. Следуя формату беседы CometAPI Responses, он удаляет верхнеуровневые поля id у элементов вывода перед повторным проигрыванием и добавляет function_call_output для каждого вызова. Сохранение остальных элементов также сохраняет любые элементы рассуждений, необходимые для следующего хода.
Шаг 4. Запустите прямой путь
Выполните:
python agent.py --mode direct
Приложение описывает get_order_status модели. Когда модель запрашивает его, dispatch() проверяет имя и аргументы, затем вызывает функцию из orders.py. Результат возвращается в беседу, и модель может превратить его в ответ.
Используйте трассировку инструмента, напечатанную в вашем терминале, чтобы убедиться, что поиск использовал ORD-104 и вернул status: shipped. Итоговый ответ должен это отражать; формулировка может различаться. Теперь у вас есть полный цикл вызова функций — от вопроса пользователя до операции, выполненной приложением, и последующего ответа модели.
Следующий шаг сохраняет модель и цикл беседы. Он переносит выполнение за MCP‑сервер и позволяет приложению обнаруживать и вызывать инструмент через клиент.
Шаг 5. Выставьте поиск как инструмент MCP
Сохраните это как server.py рядом с двумя другими файлами:
"""Один инструмент MCP только для чтения, обслуживаемый по stdio. Без логирования в stdout."""
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from orders import get_order_status as lookup_order
server = MCPServer("fictional-orders")
@server.tool(structured_output=True)
def get_order_status(order_id: str) -> dict[str, str]:
"""Ищет вымышленный заказ ORD-104; в этой демонстрации нет реальных данных клиентов."""
try:
return lookup_order(order_id)
except ValueError as error:
raise ToolError(str(error)) from error
if __name__ == "__main__":
server.run(transport="stdio")
SDK строит определение инструмента из сигнатуры функции и ее docstring. Сервер использует stdio, где клиент запускает его как подпроцесс и обменивается сообщениями протокола через стандартные потоки. Не ведите логи приложения в стандартный вывод сервера.
Шаг 6. Запустите путь MCP
Выполните:
python agent.py --mode mcp
Ветвь mcp в main() запускает server.py, вызывает list_tools() и проверяет контракт инструмента заказа. Когда модель запрашивает поиск, тот же диспетчер теперь использует client.call_tool(). Сервер запускает функцию, а приложение возвращает ее результат модели.
Сравните оба запуска по одним и тем же критериям: правильный инструмент, ORD-104 как аргумент, успешный результат, содержащий shipped, и итоговый ответ, согласующийся с результатом. Путь выполнения изменился; задача и функция, видимая моделью, остались прежними.
Все три исходных файла приведены выше. Пакет примера также содержит их и офлайн‑проверки для удобного переиспользования. Эти проверки упражняют реальный локальный MCP‑сервер со скриптованными ответами модели; живой вывод модели отделен от этой локальной верификации.
Адаптируйте пример под свои инструменты
Замените фиксированные данные заказа на минимальную операцию, которая действительно нужна вашему приложению. Для вопроса о доставке верните идентификатор заказа и полезные поля статуса. Компактный результат дает модели меньше лишнего материала для интерпретации и упрощает проверку ответа.
Сохраняйте описание инструмента и контракт аргументов конкретными. Этот пример принимает ровно одну строку, а хост проверяет имя запрошенного инструмента перед диспетчеризацией. Рекомендации по безопасности MCP предписывают серверу валидировать входные данные и внедрять контроль доступа. Для реального сервиса заказов определяйте личность клиента из доверенного контекста приложения и обеспечивайте доступ к заказам этого клиента внутри сервиса. Идентификатор заказа, переданный моделью, не может установить права владения.
Когда инструмент работает, оценивайте завершенные задачи, сквозное время и фактическую стоимость. Добавляйте MCP там, где его интерфейс сервиса помогает приложениям, которым нужен инструмент, и сохраняйте прямые обработчики там, где ими проще управлять.
Вопросы и ответы
Заменяет ли MCP вызов функций?
Они обслуживают разные интерфейсы: MCP соединяет клиент приложения с сервером. В этом примере модель запрашивает функцию, а приложение удовлетворяет этот запрос через MCP‑клиент. Оба участвуют в одном и том же агентском цикле.
Может ли инструмент MCP вызывать существующий REST API?
Да. Замените поиск внутри инструмента MCP вызовом вашего API. MCP‑сервер публикует контракт инструмента и обрабатывает вызовы инструментов; ваш существующий сервис сохраняет свое бизнес‑поведение.
Когда стоит использовать удаленный MCP‑сервер?
Используйте удаленный сервис, когда клиентам нужно обращаться к отдельно развернутому серверу, а не запускать локальный процесс. MCP определяет Streamable HTTP для такого соединения и stdio для подпроцесса, запускаемого клиентом. Настройте авторизацию удаленного сервера для предполагаемых вызывающих сторон.
Заключение
Для одного приложения замените вымышленную запись в orders.py одним запросом только для чтения из вашего сервиса и начните с прямого режима.
Если вам нужен существующий MCP‑сервис или общий интерфейс инструментов, используйте путь MCP и сохраняйте те же проверки: правильная операция, правильная запись и ответ, подтвержденный возвращенными данными.
