TL;DR
Wywoływanie funkcji daje modelowi sposób na poproszenie o użycie narzędzia. MCP daje aplikacji standardowy sposób odkrywania i wywoływania narzędzi na serwerze. Użyj bezpośredniej integracji funkcji dla małego zestawu narzędzi należących do Twojej aplikacji. Dodaj MCP, gdy chcesz korzystać z istniejącej kompatybilnej usługi lub wystawić narzędzia przez wspólny interfejs. Poniżej połączysz to samo wyszukiwanie zamówienia na oba sposoby przez CometAPI, używając jednej kompletnej pętli agenta.
Najważniejsze wnioski
- Zacznij od bezpośredniego wywołania, gdy kilka narzędzi obsługuje jedną aplikację; użyj MCP, gdy kompatybilna usługa narzędziowa lub wspólny interfejs ułatwiają utrzymanie integracji.
- Obie ścieżki wymagają kompletnej pętli: opisz narzędzie, odbierz wywołanie, wykonaj je, zwróć wynik i pozwól modelowi odpowiedzieć.
- Możesz zachować tę samą funkcję wystawioną modelowi, zmieniając jedynie wykonanie z bezpośredniego handlera na klienta MCP.
- Utrzymuj wąskie kontrakty narzędzi i sprawdzaj wyniki względem zadania, które agent ma wykonać.
Jak wywoływanie funkcji i MCP się uzupełniają
Agent wsparcia może wyjaśnić opóźnienie w dostawie, ale potrzebuje dostępu do systemu zamówień, by powiedzieć klientowi, gdzie jest jego paczka. Wywoływanie funkcji daje modelowi sposób na poproszenie o takie wyszukiwanie. Twoja aplikacja wykonuje wyszukiwanie i zwraca wynik. Handler może użyć lokalnej biblioteki, bazy danych lub zdalnego API.
Aby zrealizować to żądanie przez MCP, aplikacja używa klienta do odkrywania narzędzi serwera za pomocą tools/list i wywoływania ich przez tools/call. Aplikacja wciąż decyduje, które odkryte narzędzia udostępnia swojemu modelowi.

Rysunek 1. MCP zapewnia wspólny interfejs między aplikacjami AI a systemami zewnętrznymi. Źródło: oficjalne wprowadzenie do MCP. © 2024–2025 Anthropic, PBC i współtwórcy. Reprodukowano bez zmian na licencji MIT; pełne informacje o autorach i zgodzie w nocie licencyjnej.
MCP kontra wywoływanie funkcji: najważniejsze różnice
Ostatni wiersz jest kluczowy dla łączenia obu podejść. Model może zażądać get_order_status, a aplikacja decyduje, czy wywołać własny handler, czy serwer MCP. Definicja funkcji widoczna dla modelu może pozostać taka sama.
Wybierz integrację, której potrzebuje Twoja aplikacja
Zalecamy zacząć od bezpośredniej integracji, gdy posiadasz kilka narzędzi używanych przez jedną aplikację. Jest mniej do wdrożenia, a ścieżka od żądania modelu do Twojego kodu biznesowego jest łatwa do prześledzenia.
MCP staje się użyteczne, gdy kompatybilny serwer już dostarcza daną funkcję lub gdy kilka aplikacji AI potrzebuje wspólnego interfejsu narzędziowego. To samo narzędzie zamówień może obsługiwać asystenta wsparcia i wewnętrzną aplikację operacyjną, z tym że każde środowisko kontroluje, jak jego użytkownicy uzyskują do niego dostęp.
Dyspozytor może obsługiwać zarówno wewnętrzne funkcje, jak i usługi MCP, kierując każde działanie do odpowiedniej implementacji. Biblioteki i REST API również można współdzielić; wybierz MCP wtedy, gdy jego standardowy interfejs odkrywania i wywoływania narzędzi jest użyteczny dla aplikacji, które je konsumują.
Zbuduj kompletną pętlę narzędzia przez CometAPI
Użyjemy jednej operacji tylko do odczytu, get_order_status, i jednego fikcyjnego zamówienia, ORD-104. Przykład najpierw wywołuje operację bezpośrednio, a następnie uruchamia ją przez lokalny serwer MCP. Oba tryby używają Responses API CometAPI do inferencji modelu.

Rysunek 2. Jedna pętla agenta może obsługiwać wykonanie bezpośrednie i przez MCP. Oryginalna ilustracja, oparta na architekturze MCP i przebiegu wywoływania funkcji w CometAPI.
Krok 1 Skonfiguruj projekt
Użyj Pythona 3.11 lub nowszego oraz klucza API CometAPI. Ten przewodnik testowano z Pythonem 3.12.14 i mcp==2.3.0. Postępuj zgodnie z CometAPI quickstart, aby utworzyć klucz. W powłoce macOS lub Linux utwórz projekt i aktywuj wirtualne środowisko:
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"
Zastąp placeholder własnym kluczem i nie umieszczaj go w publicznym kodzie źródłowym. Ten przewodnik przypina wersję SDK MCP używaną w przykładzie; oficjalny przewodnik serwera Pythona dotyczy API w wersji 2.x. Dwa polecenia agenta poniżej wysyłają żądania do CometAPI i zużywają kredyty API.
Krok 2 Zdefiniuj wyszukiwanie zamówienia
Zapisz to jako orders.py. Stałe dane utrzymują przewodnik w pełni samodzielnym; później możesz zastąpić wyszukiwanie własną usługą.
"""Read-only, invented order data; never connects to a real order system."""
import re
def get_order_status(order_id: str) -> dict[str, str]:
"""Look up an invented order; ORD-104 is the only available record."""
if not isinstance(order_id, str) or not re.fullmatch(r"ORD-[0-9]{3}", order_id):
raise ValueError("order_id must have the form ORD-104")
if order_id != "ORD-104":
raise ValueError("Order not found in this demo")
return {"order_id": order_id, "status": "shipped", "source": "fictional_demo"}
Wywołanie get_order_status("ORD-104") zwraca taki fixture:
{"order_id": "ORD-104", "status": "shipped", "source": "fictional_demo"}
Krok 3 Dodaj pętlę agenta
Zapisz poniższy kompletny skrypt jako agent.py. Rejestruje funkcję, wysyła pytanie do CometAPI, wykonuje żądane wywołania i zwraca każdy wynik, używając oryginalnego call_id.
Definicja widoczna dla modelu używa pola parameters Responses API CometAPI. MCP dostarcza inputSchema; ten przykład sprawdza odkryty kontrakt względem schematu z jednym polem typu string order_id, zanim wystawi przejrzaną funkcję.
"""A deliberately narrow direct/MCP bridge, not a general MCP adapter."""
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": "Look up fictional order ORD-104.",
"parameters": SCHEMA, "strict": True}
def validate_arguments(arguments):
# Exact validation for SCHEMA above; deliberately not a JSON Schema engine.
if (not isinstance(arguments, dict) or set(arguments) != {"order_id"}
or not isinstance(arguments["order_id"], str)):
raise ValueError("Expected exactly one string argument: order_id")
def validate_catalog(tools):
if len(tools) != 1 or tools[0].name != NAME:
raise RuntimeError("Unexpected MCP tool catalog")
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 schema does not match this reviewed demo tool")
async def dispatch(name, arguments, client=None):
if name != NAME:
raise ValueError("Tool is not allowlisted")
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("Input-required MCP results are outside this demo")
if any(block.type != "text" for block in result.content):
raise RuntimeError("Non-text MCP content is outside this demo")
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("Model response failed or did not complete")
output = response["output"]
# CometAPI's documented manual replay: keep ALL items, omit top-level ids.
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("Model refused the request")
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("No final text returned")
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("Missing or duplicate function call_id")
seen_call_ids.add(call_id)
try:
arguments = json.loads(call["arguments"])
except (ValueError, TypeError):
result = {"is_error": True, "error": "Arguments must be JSON text"}
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("Tool loop reached its round limit")
def send_live(payload):
key = os.environ["COMETAPI_KEY"] # Only live mode reads the caller's 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="Use the tool to check order 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())
Pętla zachowuje oryginalne pytanie i każdy zwrócony element odpowiedzi. Zgodnie z CometAPI Responses formatem konwersacji usuwa pola id na najwyższym poziomie z elementów wyjściowych przed odtworzeniem i dołącza function_call_output dla każdego wywołania. Zachowanie pozostałych zwróconych elementów utrzymuje także ewentualne elementy rozumowania potrzebne w następnej turze.
Krok 4 Uruchom ścieżkę bezpośrednią
Uruchom:
python agent.py --mode direct
Aplikacja opisuje get_order_status dla modelu. Gdy model o to poprosi, dispatch() sprawdza nazwę i argumenty, a następnie wywołuje funkcję w orders.py. Wynik trafia z powrotem do konwersacji i model może zamienić go w odpowiedź.
Użyj śladu narzędzia wypisanego w terminalu, aby sprawdzić, że wyszukiwanie użyło ORD-104 i zwróciło status: shipped. Końcowa odpowiedź powinna odzwierciedlać ten wynik; jej brzmienie może się różnić. Masz teraz kompletną pętlę wywoływania funkcji: od pytania użytkownika, przez operację wykonaną przez aplikację, po odpowiedź modelu.
Następny krok utrzymuje model i pętlę konwersacji bez zmian. Przenosi wykonanie za serwer MCP i pozwala aplikacji odkrywać i wywoływać narzędzie przez klienta.
Krok 5 Wystaw wyszukiwanie jako narzędzie MCP
Zapisz to jako server.py, obok pozostałych dwóch plików:
"""One read-only MCP tool, served over stdio. No stdout logging."""
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]:
"""Look up fictional order ORD-104; this demo has no real customer data."""
try:
return lookup_order(order_id)
except ValueError as error:
raise ToolError(str(error)) from error
if __name__ == "__main__":
server.run(transport="stdio")
SDK buduje definicję narzędzia na podstawie sygnatury funkcji i jej docstringa. Serwer używa stdio, gdzie klient uruchamia go jako podproces i wymienia komunikaty protokołu przez standardowe strumienie. Nie wypisuj logów aplikacji na standardowe wyjście serwera.
Krok 6 Uruchom ścieżkę MCP
Uruchom:
python agent.py --mode mcp
Gałąź mcp w main() uruchamia server.py, wywołuje list_tools() i sprawdza kontrakt narzędzia zamówień. Gdy model poprosi o wyszukiwanie, ten sam dyspozytor używa teraz client.call_tool(). Serwer uruchamia funkcję, a aplikacja zwraca jej wynik do modelu.
Porównaj oba przebiegi pod tymi samymi kryteriami: właściwe narzędzie, ORD-104 jako argument, udany wynik zawierający shipped oraz końcowa odpowiedź spójna z tym wynikiem. Ścieżka wykonania się zmieniła; zadanie i funkcja widoczna dla modelu pozostały takie same.
Wszystkie trzy pliki źródłowe są zamieszczone powyżej. Pakiet przykładowy również je zawiera oraz testy offline do wygodnego ponownego użycia. Te testy sprawdzają rzeczywisty lokalny serwer MCP przy użyciu skryptowanych odpowiedzi modelu; wyjście modelu na żywo jest oddzielone od tej lokalnej weryfikacji.
Dostosuj przykład do swoich narzędzi
Zastąp stałe dane zamówienia najmniejszą operacją, której Twoja aplikacja faktycznie potrzebuje. Dla pytania o wysyłkę zwróć identyfikator zamówienia i użyteczne pola statusu. Zwarty wynik daje modelowi mniej nieistotnego materiału do interpretacji i ułatwia sprawdzenie odpowiedzi.
Utrzymuj opis narzędzia i kontrakt argumentów możliwie precyzyjne. Ten przykład przyjmuje dokładnie jeden napis i host sprawdza nazwę żądanego narzędzia przed dyspozycją. Wytyczne bezpieczeństwa MCP zalecają, aby serwery weryfikowały dane wejściowe i wdrażały kontrolę dostępu. Dla prawdziwej usługi zamówień tożsamość klienta wywiedź z zaufanego kontekstu aplikacji i egzekwuj dostęp do zamówienia tego klienta wewnątrz usługi. Identyfikator zamówienia dostarczony przez model nie może potwierdzić prawa własności.
Gdy narzędzie działa, oceniaj ukończone zadania, czas end-to-end i rzeczywisty koszt. Dodaj MCP tam, gdzie jego interfejs serwisowy pomaga aplikacjom, które potrzebują narzędzia, a bezpośrednie handlery zachowaj tam, gdzie prościej je obsługiwać.
FAQ
Czy MCP zastępuje wywoływanie funkcji?
Obsługują różne interfejsy: MCP łączy klienta aplikacji z serwerem. W tym przykładzie model prosi o funkcję, a aplikacja realizuje to żądanie przez klienta MCP. Oba uczestniczą w tej samej pętli agenta.
Czy narzędzie MCP może wywoływać istniejące REST API?
Tak. Zastąp wyszukiwanie wewnątrz narzędzia MCP wywołaniem Twojego API. Serwer MCP publikuje kontrakt narzędzia i obsługuje wywołania narzędzi; Twoja istniejąca usługa nadal dostarcza swoją logikę biznesową.
Kiedy używać zdalnego serwera MCP?
Użyj zdalnej usługi, gdy klienci muszą połączyć się z oddzielnie wdrożonym serwerem zamiast uruchamiać proces lokalny. MCP definiuje Streamable HTTP dla tego połączenia oraz stdio dla podprocesu uruchamianego przez klienta. Skonfiguruj autoryzację zdalnego serwera odpowiednio do zamierzonych wywołujących.
Podsumowanie
Dla pojedynczej aplikacji zastąp fikcyjny rekord w orders.py jednym zapytaniem tylko do odczytu z Twojej usługi i zacznij od trybu bezpośredniego.
Jeśli potrzebujesz istniejącej usługi MCP lub wspólnego interfejsu narzędzi, użyj ścieżki MCP i zachowaj te same kontrole: właściwa operacja, właściwy rekord i odpowiedź poparta zwróconymi danymi.
