Construire avec CometAPIBesoin d'une seule API pour plusieurs modèles ?Testez le workflow dans Playground, puis créez une clé API lorsque le modèle et le coût correspondent à votre cas d'usage.
MCP vs appels de fonctions : comment connecter des outils aux agents d’IA - CometAPI
MCP vs appels de fonctions : comment connecter des outils aux agents d’IA
Comparaison entre MCP et l’appel de fonctions
- Appel de fonctions (function calling):
- Intégration étroite: les fonctions vivent dans le même runtime que le modèle et sont appelées via des “tool calls” propriétaires.
- Découverte limitée: pas de protocole standard pour lister, négocier ou versionner des outils; dépend des conventions de l’implémentation.
- Couplage fort: sécurité, permissions, et contexte sont gérés au cas par cas; difficile à partager entre modèles/clients.
- Transport implicite: pas de canal standard pour la communication; souvent spécifique à un fournisseur.
- MCP (Model Context Protocol):
- Standardisation: protocole normalisé pour exposer des outils, ressources et prompts avec schémas, capacités et négociation.
- Découplage: serveur d’outils séparé, réutilisable par plusieurs clients et modèles; meilleure observabilité et gouvernance.
- Découverte et schémas: outils listés, descriptions et schémas d’entrées/sorties publiés; validation systématique.
- Transports variés: prise en charge de stdio, sockets, etc., avec sessions, permissions et flux structurés.
Code Python: boucle d’outil complète CometAPI, exécution directe et via MCP
comet_api.py
```python
# comet_api.py
# API Comet simulée pour la recherche de commandes.
# Dans un cas réel, remplacez la logique par des appels HTTP (requests) vers votre service Comet.
from __future__ import annotations
import datetime
import random
from typing import Dict, Any, List
class CometAPI:
def __init__(self, api_key: str, base_url: str = "https://api.comet.example"):
# Stocke les paramètres d’accès (dans la vraie vie, vérifiez la clé, gérez la rotation, etc.)
self.api_key = api_key
self.base_url = base_url
def get_order(self, order_id: str) -> Dict[str, Any]:
# Simule une recherche de commande. Remplacez par un appel réseau réel.
rng = random.Random(hash(order_id) & 0xFFFFFFFF)
status = rng.choice(["processing", "fulfilled", "cancelled"])
items: List[Dict[str, Any]] = [
{"sku": "SKU-001", "name": "Widget A", "qty": 2, "unit_price": 19.99},
{"sku": "SKU-045", "name": "Gadget B", "qty": 1, "unit_price": 49.9},
]
total = sum(i["qty"] * i["unit_price"] for i in items)
return {
"id": order_id,
"status": status,
"currency": "USD",
"total": round(total, 2),
"items": items,
"customer": {
"id": "CUST-1827",
"name": "Jane Doe",
"email": "jane.doe@example.com",
},
"created_at": datetime.datetime.utcnow().isoformat() + "Z",
"source": "CometAPI",
}
```
direct_lookup.py
```python
# direct_lookup.py
# Exécute la recherche de commande directement via CometAPI (sans MCP).
import os
import json
from comet_api import CometAPI
def lookup_order_direct(order_id: str) -> dict:
api_key = os.environ.get("COMET_API_KEY", "dev-key")
api = CometAPI(api_key=api_key)
return api.get_order(order_id)
if __name__ == "__main__":
order_id = os.environ.get("ORDER_ID", "ORDER-12345")
result = lookup_order_direct(order_id)
print(json.dumps(result, indent=2, ensure_ascii=False))
```
mcp_server.py
```python
# mcp_server.py
# Serveur MCP qui expose l’outil "lookup_order" via CometAPI.
# Nécessite la bibliothèque Python MCP. Adaptez l’import selon votre environnement MCP.
import os
import json
import asyncio
from typing import Any, Dict
# Bibliothèques MCP (les noms peuvent varier selon la version)
try:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent
except ImportError:
raise ImportError("La bibliothèque MCP Python est requise (par ex. 'mcp').")
from comet_api import CometAPI
server = Server("comet-mcp-server")
# Définition de l’outil avec schéma d’entrée JSON
@server.tool(
name="lookup_order",
description="Recherche une commande par identifiant dans CometAPI.",
input_schema={
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
)
async def lookup_order(params: Dict[str, Any]) -> list[TextContent]:
order_id = params["order_id"]
api_key = os.environ.get("COMET_API_KEY", "dev-key")
api = CometAPI(api_key=api_key)
data = api.get_order(order_id)
# Retourne le contenu texte (MCP supporte contenus structurés; ici, JSON sérialisé)
return [TextContent(type="text", text=json.dumps(data))]
async def main():
# Démarre le serveur MCP sur stdio
async with stdio_server() as (read, write):
await server.run(read, write)
if __name__ == "__main__":
asyncio.run(main())
```
mcp_client.py
```python
# mcp_client.py
# Client MCP qui lance le serveur en sous-processus et appelle l’outil "lookup_order".
import os
import json
import asyncio
try:
from mcp.client import Client
from mcp.client.stdio import stdio_client
except ImportError:
raise ImportError("La bibliothèque MCP Python est requise (par ex. 'mcp').")
async def run_via_mcp(order_id: str):
# Lance le serveur MCP local (mcp_server.py) via stdio et appelle l’outil
async with stdio_client(command=["python", "mcp_server.py"]) as (read, write):
client = Client(read, write)
await client.initialize() # négociation de capacités
# Liste les outils disponibles (facultatif)
tools = await client.list_tools()
# Appel de l’outil avec paramètres
result = await client.call_tool("lookup_order", {"order_id": order_id})
# Agrège le texte des contenus retournés
texts = []
for content in result.contents:
if getattr(content, "type", None) == "text":
texts.append(content.text)
return "\n".join(texts)
if __name__ == "__main__":
order_id = os.environ.get("ORDER_ID", "ORDER-12345")
output = asyncio.run(run_via_mcp(order_id))
# Le serveur renvoie du JSON sérialisé en texte
parsed = json.loads(output)
print(json.dumps(parsed, indent=2, ensure_ascii=False))
```
Notes d’exécution (même recherche de commande, deux chemins)
- Exécution directe:
- python direct_lookup.py
- Exécution via MCP:
- python mcp_client.py
Structure des résultats
- Les deux chemins renvoient un objet JSON avec:
- id, status, currency, total, items (sku, name, qty, unit_price), customer (id, name, email), created_at, source
- Le parcours “direct” appelle CometAPI directement.
- Le parcours “MCP” appelle l’outil standardisé “lookup_order” servi par mcp_server.py via un client MCP, ce qui permet la découverte d’outil, la validation de schéma et le découplage du runtime.
Deon GoodwinÉquipe de recherche sur les modèles IA et API
L’appel de fonctions donne au modèle un moyen de demander un outil. MCP fournit à une application une façon standard de découvrir et d’appeler des outils sur un serveur. Utilisez une intégration directe des fonctions pour un petit ensemble d’outils que votre application possède. Ajoutez MCP lorsque vous souhaitez consommer un service compatible existant ou exposer des outils via une interface partagée. Ci-dessous, vous allez connecter la même recherche de commande de deux manières via CometAPI, en utilisant une boucle d’agent complète.
Points clés
Commencez par un dispatch direct lorsque quelques outils servent une seule application ; utilisez MCP lorsqu’un service d’outils compatible ou une interface d’outils partagée facilite la maintenance de l’intégration.
Les deux voies exigent une boucle complète : décrire l’outil, recevoir un appel, l’exécuter, renvoyer le résultat et laisser le modèle répondre.
Revu pour la clarté, l'attribution des sources et la terminologie API actuelle.
Utilisez ce résumé pour une réponse rapide, puis lisez le guide pour le contexte, les exemples et les détails d'implémentation.
Vous pouvez conserver la même fonction côté modèle tout en changeant son exécution d’un gestionnaire direct vers un client MCP.
Gardez des contrats d’outils étroits et vérifiez les résultats au regard de la tâche que vous voulez que l’agent accomplisse.
Comment l’appel de fonctions et MCP s’articulent
Un agent d’assistance peut expliquer un retard d’expédition, mais il a besoin d’accéder au système de commandes pour indiquer à un client où se trouve son colis. L’appel de fonctions donne au modèle un moyen de demander cette recherche. Votre application exécute la recherche et renvoie le résultat. Le gestionnaire peut utiliser une bibliothèque locale, une base de données ou une API distante.
Pour satisfaire cette demande via MCP, l’application utilise un client pour découvrir les outils du serveur avec tools/list et les invoquer avec tools/call. L’application décide toujours quels outils découverts exposer à son modèle.
Let the model request a named operation with structured arguments
Standardize how compatible applications discover and invoke server tools
Tool definitions
Definitions supplied in the model API’s format
Server-provided definitions, including an inputSchema
Execution location
The handler the application dispatches to, which may call a local or remote service
The MCP server’s tool implementation, which may call a backend service
Separate server required?
No server is required by the function-calling interface itself
An MCP server is required, but a client can launch it as a local stdio subprocess; a separately deployed remote service is optional
Typical use
A few application-owned tools with direct dispatch
Existing compatible services or a tool interface reused across applications
Can they work together?
The application can route a model’s function request through MCP
The MCP client fulfills that request with tools/call and returns the result to the application
La dernière ligne est la clé pour construire avec les deux. Un modèle peut demander get_order_status, tandis que l’application choisit d’appeler son propre gestionnaire ou un serveur MCP. La définition de fonction côté modèle peut rester inchangée.
Choisissez l’intégration dont votre application a besoin
Nous recommandons de commencer par une intégration directe lorsque vous possédez quelques outils utilisés par une seule application. Il y a moins à déployer, et le chemin entre la demande du modèle et votre code métier est facile à suivre.
MCP devient utile lorsqu’un serveur compatible fournit déjà la capacité, ou lorsque plusieurs applications d’IA ont besoin d’une interface d’outils commune. Le même outil de commande pourrait servir un assistant d’assistance et une application d’opérations interne, chaque hôte contrôlant la façon dont ses utilisateurs y accèdent.
Un répartiteur peut prendre en charge à la fois des fonctions internes et des services MCP, en routant chaque opération vers son implémentation. Les bibliothèques et les API REST peuvent également être partagées ; choisissez MCP lorsque son interface standard de découverte et d’invocation est utile aux applications qui consomment les outils.
Construisez une boucle d’outil complète via CometAPI
Nous utiliserons une opération en lecture seule, get_order_status, et une commande fictive, ORD-104. L’exemple appelle d’abord l’opération directement, puis l’exécute via un serveur MCP local. Les deux modes utilisent l’API Responses de CometAPI pour l’inférence du modèle.
Figure 2. Une seule boucle d’agent peut prendre en charge une exécution directe et via MCP. Illustration originale, basée sur l’architecture MCP et le flux d’appel de fonctions CometAPI.
Étape 1 Configurer le projet
Utilisez Python 3.11 ou une version ultérieure et une clé API CometAPI. Ce tutoriel a été testé avec Python 3.12.14 et mcp==2.3.0. Suivez le guide de démarrage rapide CometAPI pour créer une clé. Dans un shell macOS ou Linux, créez un projet et activez un environnement virtuel :
Remplacez l’espace réservé par votre clé et gardez-la hors du code source public. Ce tutoriel fixe la version du SDK MCP utilisée par l’exemple ; le guide officiel du serveur Python cible l’API 2.x. Les deux commandes d’agent ci-dessous envoient des requêtes à CometAPI et consomment du crédit API.
Étape 2 Définir la recherche de commande
Enregistrez ceci sous orders.py. Des données fixes rendent le tutoriel autonome ; plus tard, vous pouvez remplacer la recherche par votre propre service.
"""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"}
Appeler get_order_status("ORD-104") renvoie ce jeu de données :
Enregistrez le script complet suivant sous agent.py. Il enregistre la fonction, envoie la question à CometAPI, exécute les appels demandés et renvoie chaque résultat en utilisant le call_id d’origine.
La définition côté modèle utilise le champ parameters de l’API Responses de CometAPI. MCP fournit un inputSchema ; cet exemple vérifie le contrat découvert par rapport au schéma order_id (chaîne unique) avant d’exposer la fonction relue.
"""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())
La boucle conserve la question d’origine et chaque élément de réponse renvoyé. En suivant le format de conversation CometAPI Responses, elle supprime les champs id de niveau supérieur des éléments de sortie avant relecture et ajoute un function_call_output pour chaque appel. Conserver le reste des éléments renvoyés préserve également tout élément de raisonnement nécessaire au tour suivant.
Étape 4 Exécuter la voie directe
Exécutez :
python agent.py --mode direct
L’application décrit get_order_status au modèle. Lorsque le modèle le demande, dispatch() vérifie le nom et les arguments, puis appelle la fonction dans orders.py. Le résultat retourne dans la conversation, et le modèle peut le transformer en réponse.
Utilisez la trace d’outil affichée dans votre terminal pour vérifier que la recherche a utilisé ORD-104 et a renvoyé status: shipped. La réponse finale devrait refléter ce résultat ; sa formulation peut varier. Vous disposez maintenant de la boucle complète d’appel de fonctions, de la question de l’utilisateur à une opération exécutée par l’application et une réponse du modèle en suivi.
L’étape suivante conserve le modèle et la boucle de conversation en place. Elle déplace l’exécution derrière un serveur MCP et permet à l’application de découvrir et d’invoquer l’outil via un client.
Étape 5 Exposer la recherche comme outil MCP
Enregistrez ceci sous server.py, à côté des deux autres fichiers :
"""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")
La branche mcp dans main() démarre server.py, appelle list_tools() et vérifie le contrat de l’outil de commande. Lorsque le modèle demande la recherche, le même répartiteur utilise désormais client.call_tool(). Le serveur exécute la fonction, et l’application renvoie son résultat au modèle.
Comparez les deux exécutions selon les mêmes critères : le bon outil, ORD-104 comme argument, un résultat réussi contenant shipped, et une réponse finale cohérente avec ce résultat. Le chemin d’exécution a changé ; la tâche et la fonction côté modèle sont restées les mêmes.
Les trois fichiers source sont inclus ci-dessus. Le paquet d’exemple les contient également ainsi que les contrôles hors ligne pour une réutilisation pratique. Ces contrôles exercent un véritable serveur MCP local avec des réponses de modèle scriptées ; la sortie du modèle en direct est distincte de cette vérification locale.
Adaptez l’exemple à vos propres outils
Remplacez les données de commande fixes par la plus petite opération dont votre application a réellement besoin. Pour une question d’expédition, renvoyez un ID de commande et des champs de statut utiles. Un résultat compact donne au modèle moins de matière non pertinente à interpréter et facilite la vérification de la réponse.
Gardez la description de l’outil et le contrat d’arguments spécifiques. Cet exemple accepte exactement une chaîne, et l’hôte vérifie le nom de l’outil demandé avant le dispatch. Les recommandations de sécurité MCP demandent aux serveurs de valider les entrées et de mettre en œuvre des contrôles d’accès. Pour un service de commande réel, dérivez l’identité du client à partir du contexte d’application fiable et imposez l’accès à la commande de ce client à l’intérieur du service. Un ID de commande fourni par le modèle ne peut pas établir la propriété.
Une fois l’outil opérationnel, évaluez les tâches effectuées, le temps de bout en bout et le coût réel. Ajoutez MCP lorsque son interface de service aide les applications qui ont besoin de l’outil, et conservez les gestionnaires directs lorsqu’ils sont plus simples à exploiter.
FAQ
MCP remplace-t-il l’appel de fonctions ?
Ils gèrent des interfaces différentes : MCP connecte le client de l’application à un serveur. Dans cet exemple, le modèle demande une fonction et l’application satisfait cette demande via un client MCP. Les deux participent à la même boucle d’agent.
Un outil MCP peut-il appeler une API REST existante ?
Oui. Remplacez la recherche à l’intérieur de l’outil MCP par un appel à votre API. Le serveur MCP publie le contrat de l’outil et gère les appels d’outils ; votre service existant continue de fournir son comportement métier.
Quand faut-il utiliser un serveur MCP distant ?
Utilisez un service distant lorsque les clients doivent atteindre un serveur déployé séparément plutôt que lancer un processus local. MCP définit Streamable HTTP pour cette connexion et stdio pour un sous-processus lancé par le client. Configurez l’autorisation du serveur distant pour ses appelants prévus.
Conclusion
Pour une seule application, remplacez l’enregistrement fictif dans orders.py par une requête en lecture seule de votre service et commencez par le mode direct.
Si vous avez besoin d’un service MCP existant ou d’une interface d’outils partagée, utilisez la voie MCP et conservez les mêmes vérifications : la bonne opération, le bon enregistrement et une réponse étayée par les données renvoyées.