📑 Table des matières

MCP, Function Calling, Tool Use : le guide complet

Agents IA 🟡 Intermédiaire ⏱️ 14 min de lecture 📅 2026-02-24

MCP, Function Calling, Tool Use: The Complete Guide

Large Language Models (LLMs) are powerful, but they have a fundamental limitation: they can only generate text. For an AI agent to truly take action — search the web, query a database, send an email — it needs a mechanism to call external tools. That's exactly what MCP (Model Context Protocol), Function Calling (OpenAI), and Tool Use (Anthropic) provide.

In this guide, we break down each approach, compare their architectures, and give you ready-to-use code examples to integrate tools into your AI agents.

🧠 Why LLMs Need Tools

An LLM on its own cannot:

  • Access real-time data (weather, stock prices, news)
  • Perform precise calculations (complex arithmetic, conversions)
  • Interact with external systems (APIs, databases, files)
  • Execute code or system commands

Without a tool-calling mechanism, an LLM is doomed to hallucinate answers to these questions. Tool-calling protocols solve this by allowing the model to declare that it wants to use a tool, then receive the result to formulate its final response.

The general flow is always the same:

  1. The user asks a question
  2. The model identifies that a tool is needed
  3. The model produces a structured call (tool name + parameters)
  4. The host system executes the tool
  5. The result is sent back to the model
  6. The model formulates its final response

What changes between approaches is how steps 3 through 5 are implemented.

🔧 Function Calling (OpenAI)

How It Works

Function Calling is the approach introduced by OpenAI in June 2023. The idea is simple: you describe available functions in your system prompt as JSON schemas, and the model can decide to call one instead of responding directly.

Architecture

User → OpenAI API (with function definitions)
                    ↓
            Model chooses a function
                    ↓
            Returns structured JSON
                    ↓
        Your code executes the function
                    ↓
        Result sent back to the model
                    ↓
            Final response to the user

Function Definition

Each function is described with a JSON Schema:

{
  "name": "get_weather",
  "description": "Retrieves the current weather for a given city",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "The city name (e.g., Paris, London)"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "The temperature unit"
      }
    },
    "required": ["city"]
  }
}

Full Python Example

import openai
import json

client = openai.OpenAI()

# Define available functions
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Retrieves the weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["city"]
            }
        }
    }
]

# Actual function (your logic)
def get_weather(city, unit="celsius"):
    # In production: call a weather API
    return {"city": city, "temperature": 18, "unit": unit, "condition": "cloudy"}

# Call the model
messages = [{"role": "user", "content": "What's the weather like in Paris?"}]
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

# Check if the model wants to call a function
message = response.choices[0].message
if message.tool_calls:
    for tool_call in message.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)

        # Execute the function
        result = get_weather(**func_args)

        # Send the result back to the model
        messages.append(message)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result)
        })

    # Get the final response
    final = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )
    print(final.choices[0].message.content)

Calling Modes

OpenAI offers three modes via the tool_choice parameter:

Mode Behavior
"auto" The model decides on its own whether to call a function
"none" The model cannot call any function
"required" The model must call at least one function
{"type": "function", "function": {"name": "X"}} Forces a specific function call

Parallel Calls

Since late 2023, GPT-4 and GPT-3.5-turbo can trigger multiple function calls in parallel within a single response. For example, if the user asks "weather in Paris and London," the model will produce two simultaneous tool_calls.

# The model can return multiple tool_calls
for tool_call in message.tool_calls:
    # Process each call independently
    ...

You can disable this behavior with parallel_tool_calls: false.

Strengths and Limitations

Advantages:
- Simple, well-documented syntax
- Native support for parallel calls
- Large ecosystem of examples
- Compatible with all recent GPT models

Limitations:
- Proprietary to OpenAI (though the format has been adopted by others)
- Function definitions consume tokens
- No official open standard

🛠️ Tool Use (Anthropic)

How It Works

Anthropic introduced Tool Use for Claude in April 2024. The concept is similar to OpenAI's Function Calling, but with some notable architectural differences, particularly in message format and result handling.

Architecture

The architecture is similar to OpenAI's, but the message format differs:

User  Anthropic API (with tool definitions)
                    
            Claude chooses a tool
                    
            Returns a tool_use block in the content
                    
        Your code executes the tool
                    
        Result sent back via a tool_result message
                    
            Final response to the user

Tool Definition

Anthropic also uses JSON Schema but with a slightly different structure:

{
  "name": "get_weather",
  "description": "Retrieves the current weather for a given city. Returns temperature and conditions.",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "The city name"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "The desired temperature unit"
      }
    },
    "required": ["city"]
  }
}

The key difference: input_schema instead of parameters.

Full Python Example

import anthropic
import json

client = anthropic.Anthropic()

# Define tools
tools = [
    {
        "name": "get_weather",
        "description": "Retrieves the weather for a given city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    }
]

def get_weather(city, unit="celsius"):
    return {"city": city, "temperature": 18, "unit": unit, "condition": "cloudy"}

# First call
messages = [{"role": "user", "content": "What's the weather like in Paris?"}]
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

# Check the stop_reason
if response.stop_reason == "tool_use":
    # Extract the tool_use block
    tool_block = next(b for b in response.content if b.type == "tool_use")

    # Execute the tool
    result = get_weather(**tool_block.input)

    # Build the complete conversation
    messages.append({"role": "assistant", "content": response.content})
    messages.append({
        "role": "user",
        "content": [
            {
                "type": "tool_result",
                "tool_use_id": tool_block.id,
                "content": json.dumps(result)
            }
        ]
    })

    # Get the final response
    final = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        tools=tools,
        messages=messages
    )
    print(final.content[0].text)

Tool Usage Control

Anthropic offers a similar tool_choice parameter:

Mode Behavior
{"type": "auto"} Claude decides on its own
{"type": "any"} Claude must use a tool (any tool)
{"type": "tool", "name": "X"} Forces use of a specific tool

Anthropic-Specific Features

"Thinking" before the tool call: Claude can include text (reasoning) before the tool call in the same message. This provides transparency into its thought process.

Error results: You can signal that a tool has failed:

{
    "type": "tool_result",
    "tool_use_id": "toolu_abc123",
    "is_error": True,
    "content": "Error: city not found"
}

Claude will adapt its response accordingly, potentially reformulating its request or informing the user.

Strengths and Limitations

Advantages:
- Visible reasoning before tool calls
- Native error handling with is_error
- Very detailed tool descriptions for better selection
- Streaming support with tool_use blocks

Limitations:
- Slightly more verbose format
- Smaller example ecosystem (but growing rapidly)
- Tool results must be in a user message (unique architecture)

🌐 MCP (Model Context Protocol)

How It Works

MCP is an open protocol created by Anthropic and released in late 2024. Unlike Function Calling and Tool Use, which are API mechanisms specific to each provider, MCP aims to be a universal standard for connecting LLMs to data sources and tools.

The analogy often used: MCP is to LLMs what USB is to peripherals. One protocol to connect everything.

Architecture

MCP introduces a client-server architecture:

┌─────────────────────────────────┐
│          Host Application        │
│  (Claude Desktop, IDE, Agent)    │
│                                  │
│  ┌──────────┐  ┌──────────┐     │
│  │ MCP      │  │ MCP      │     │
│  │ Client#1 │  │ Client#2 │     │
│  └────┬─────┘  └────┬─────┘     │
└───────┼──────────────┼───────────┘
        │              │
   ┌────▼─────┐  ┌────▼─────┐
   │ MCP      │  │ MCP      │
   │ Server   │  │ Server   │
   │ (Files)  │  │ (GitHub) │
   └──────────┘  └──────────┘

Components:

  • MCP Host: The application (Claude Desktop, an IDE, your agent)
  • MCP Client: Maintains a 1:1 connection with a server
  • MCP Server: Exposes tools, resources, and prompts

The Three MCP Primitives

MCP isn't limited to tools. It exposes three types of capabilities:

Primitive Description Example
Tools Functions callable by the LLM Search the web, execute SQL
Resources Queryable data (read-only) File contents, query results
Prompts Reusable prompt templates Summary template, analysis template

Python MCP Server Example

Using the official Python SDK:

from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("weather-server")

@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> dict:
    """Retrieves the current weather for a given city.

    Args:
        city: The city name (e.g., Paris, London)
        unit: The temperature unit (celsius or fahrenheit)
    """
    # In production: call a real API
    return {
        "city": city,
        "temperature": 18,
        "unit": unit,
        "condition": "cloudy"
    }

@mcp.resource("weather://current/{city}")
def current_weather(city: str) -> str:
    """Resource: current weather for a city."""
    data = get_weather(city)
    return f"{data['city']}: {data['temperature']}°C, {data['condition']}"

@mcp.prompt()
def weather_report(city: str) -> str:
    """Generates a prompt for a detailed weather report."""
    return f"Generate a comprehensive weather report for {city} including a 3-day forecast."

# Start the server
if __name__ == "__main__":
    mcp.run()

TypeScript MCP Server Example

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-server",
  version: "1.0.0"
});

server.tool(
  "get_weather",
  "Retrieves the weather for a city",
  {
    city: z.string().describe("City name"),
    unit: z.enum(["celsius", "fahrenheit"]).optional()
  },
  async ({ city, unit = "celsius" }) => {
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          city,
          temperature: 18,
          unit,
          condition: "cloudy"
        })
      }]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Transports

MCP supports two transport modes:

Transport Use Case Communication
stdio Local servers The client launches the server as a child process
SSE (Server-Sent Events) Remote servers HTTP communication, ideal for cloud services

Configuration in Claude Desktop

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/path/to/weather_server.py"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxx"
      }
    }
  }
}

MCP Server Ecosystem

One of MCP's major advantages is its growing ecosystem of ready-to-use servers:

  • Files: Read/write local files
  • GitHub: Repos, issues, pull requests
  • Databases: PostgreSQL, SQLite, MongoDB
  • Web: Brave Search, Fetch
  • Productivity: Google Drive, Slack, Notion
  • Development: Docker, Kubernetes

Strengths and Limitations

Advantages:
- Open and interoperable standard
- Three primitives (tools, resources, prompts)
- Ecosystem of reusable servers
- Clear client/server separation
- Works with any LLM (not tied to a provider)

Limitations:
- More complex to set up than simple function calling
- Still young (evolving spec)
- Requires a runtime for servers
- Overhead for simple use cases

📊 Full Comparison Table

Criteria Function Calling (OpenAI) Tool Use (Anthropic) MCP
Type Proprietary API Proprietary API Open protocol
Creator OpenAI Anthropic Anthropic (open source)
Definition format JSON Schema (parameters) JSON Schema (input_schema) Decorators / SDK
Parallel calls ✅ Native ✅ Possible ✅ Via the client
Error handling Via message content Native is_error Via the protocol
Streaming
Multi-provider ❌ (format adopted by others)
Resources (data)
Reusable prompts
Setup complexity ⭐ Low ⭐ Low ⭐⭐⭐ Medium
Maturity ⭐⭐⭐ ⭐⭐ ⭐⭐
Documentation Excellent Very good Good (growing)

🔀 When to Use What?

Use Function Calling (OpenAI) if:

  • You're already using GPT models
  • Your use case is simple (a few functions)
  • You want the fastest possible setup
  • You don't need to share tools between projects

Use Tool Use (Anthropic) if:

  • You're using Claude as your main model
  • You need visible model reasoning
  • Fine-grained error handling matters
  • You want a model that excels at choosing among complex tools

Use MCP if:

  • You're building an agent that needs to work with multiple LLMs
  • You want a reusable tool ecosystem across projects
  • You need more than just tools (resources, prompts)
  • You're building a platform or agent framework
  • Standardization and interoperability are priorities

Combining Approaches

In practice, many developers combine these approaches. For example:

  • Use MCP to define and share tools
  • Translate MCP tools into Function Calling or Tool Use depending on the LLM being used
  • Frameworks like LangChain and OpenClaw handle this translation automatically
# Pseudo-code: an agent that uses MCP internally
# but communicates via Function Calling or Tool Use

class UniversalAgent:
    def __init__(self, mcp_servers, llm_provider):
        self.mcp_client = MCPClient(mcp_servers)
        self.llm = llm_provider  # "openai" or "anthropic"

    def get_tools_for_llm(self):
        mcp_tools = self.mcp_client.list_tools()
        if self.llm == "openai":
            return convert_to_function_calling(mcp_tools)
        elif self.llm == "anthropic":
            return convert_to_tool_use(mcp_tools)

    async def run(self, user_message):
        tools = self.get_tools_for_llm()
        response = await self.llm.complete(user_message, tools)
        # Handle tool calls via MCP
        ...

🚀 Common Best Practices

Regardless of the approach you choose, certain best practices always apply:

1. Clear and Precise Descriptions

The model chooses its tools based on descriptions. Be explicit:

# ❌ Bad
"description": "Search for stuff"

# ✅ Good
"description": "Searches articles in the database by keyword. Returns the 10 most recent results with title, date, and summary."

2. Validate Parameters

Never trust model-generated parameters without validation:

def execute_tool(name, params):
    # Always validate!
    if name == "delete_file":
        path = params.get("path", "")
        if ".." in path or path.startswith("/"):
            raise ValueError("Unauthorized path")
    # ...

3. Handle Errors Gracefully

Return clear error messages so the model can adapt:

try:
    result = execute_tool(name, params)
    return {"success": True, "data": result}
except Exception as e:
    return {"success": False, "error": str(e), "suggestion": "Check the parameters"}

4. Limit the Number of Tools

The more tools you expose, the more likely the model is to make mistakes. Keep a focused and relevant set for the task.

Number of Tools Recommendation
1-5 ✅ Ideal
6-15 ⚠️ Acceptable with good descriptions
15-30 ⚠️ Group into categories
30+ ❌ Too many — use a router or tool selector

5. Test with Edge Cases

Models can hallucinate parameters or call the wrong tool. Systematically test:

  • Ambiguous requests ("search for that" → which tool?)
  • Missing parameters
  • Empty results or errors
  • Call chains (tool A → tool B)

🔮 The Future of LLM-Tool Interactions

The ecosystem is evolving rapidly:

  • MCP is gaining adoption and could become the de facto standard
  • OpenAI and Google are working on their own tool protocols
  • Frameworks (LangChain, CrewAI, OpenClaw) abstract away these differences
  • The trend is toward interoperability: define a tool once, use it everywhere

What matters isn't choosing "the best" protocol, but understanding the underlying principles. The fundamental mechanism — the model declares it wants to use a tool, your code executes it, the result goes back to the model — remains the same everywhere.

Master this pattern, and you'll be able to build AI agents capable of interacting with any system.