If you've been building software for any length of time, you've probably connected to a REST API. If you've worked with ChatGPT or Claude, you may have used function calling. And now there's MCP. So what's the difference, and why does it matter?
This lesson breaks down the three dominant approaches to connecting AI with external systems. By the end, you'll understand not just how they differ, but when each approach is the right choice.
The Three Approaches at a Glance
Before we dive deep, here's a high-level comparison:
| Feature | REST APIs | Function Calling | MCP ||----------------------|--------------------|----------------------|------------------------|| Who calls it? | Your app code | The AI model | The AI model || Discovery | Read docs manually | Hardcoded in prompt | Dynamic at runtime || Protocol | HTTP + JSON | Provider-specific | JSON-RPC 2.0 || Standardized? | Loosely (OpenAPI) | No (per-provider) | Yes (open spec) || Stateful? | Usually no | Per-conversation | Yes (session-based) || Bidirectional? | Request/Response | Request/Response | Full bidirectional || Multi-provider? | N/A | Lock-in | Any MCP-compatible AI |
Let's unpack each approach in detail.
REST APIs: The Traditional Backbone
REST (Representational State Transfer) APIs have been the standard for system-to-system communication since the mid-2000s. They use HTTP methods (GET, POST, PUT, DELETE) with JSON payloads.
How REST Works with AI
When you build an AI application using REST APIs, the flow looks like this:
Concept Card
Python
import requestsimport openai# Step 1: User asks "What's the weather in Tokyo?"user_question = "What's the weather in Tokyo?"# Step 2: Your app code decides to call a weather API# YOU write this logic - the AI doesn't choose thisresponse = requests.get( "https://api.weatherapi.com/v1/current.json", params={"key": "YOUR_API_KEY", "q": "Tokyo"})weather_data = response.json()# Step 3: You manually inject the result into the AI promptcompletion = openai.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": f"Current weather data: {weather_data}"}, {"role": "user", "content": user_question} ])
The Problem with REST + AI
Notice what happened: you had to write the glue code. You decided when to call the API, which API to call, how to parse the response, and how to inject it into the prompt. The AI model had zero involvement in that decision.
This creates several problems:
Rigid routing logic - You must anticipate every possible user intent and map it to the right API call
No AI autonomy - The model can't decide it needs additional data mid-reasoning
Maintenance burden - Every new API requires new glue code, new parsing logic, new error handling
No discovery - The AI doesn't know what tools are available
Tip
Use MCP vs REST APIs vs Function Calling in a low-risk branch or scratch project first. That keeps the lesson concrete without making your first attempt carry production pressure.
Python
# This is what REST + AI integration looks like at scale# It becomes a massive if/else treeif "weather" in user_message: data = call_weather_api(extract_city(user_message))elif "stock" in user_message: data = call_stock_api(extract_ticker(user_message))elif "email" in user_message: data = call_email_api(extract_email_params(user_message))elif "calendar" in user_message: data = call_calendar_api(extract_date(user_message))# ... this never ends
Warning
REST APIs are designed for application-to-application communication. They were never designed for AI-to-application communication. Forcing them into that role creates brittle, hard-to-maintain systems.
Function Calling: Letting the AI Choose
Function calling (also called "tool use") was a breakthrough. Instead of writing routing logic yourself, you describe available functions to the AI model, and it decides which ones to call.
How Function Calling Works
Python
import openai# Define tools the AI can usetools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a city", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City name" } }, "required": ["city"] } } }]# The AI decides whether and how to call the functionresponse = openai.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": "What's the weather in Tokyo?"}], tools=tools)# If the AI chose to call a function:tool_call = response.choices[0].message.tool_calls[0]# tool_call.function.name == "get_weather"# tool_call.function.arguments == '{"city": "Tokyo"}'# YOU still execute it and return the resultweather = get_weather(city="Tokyo") # Your implementation# Send the result back to the AIfollow_up = openai.chat.completions.create( model="gpt-4", messages=[ {"role": "user", "content": "What's the weather in Tokyo?"}, response.choices[0].message, {"role": "tool", "tool_call_id": tool_call.id, "content": str(weather)} ], tools=tools)
What Function Calling Got Right
This was a genuine step forward:
AI decides which function to call (and whether to call one at all)
Structured output - The AI returns well-formed JSON matching your schema
Multi-step reasoning - The AI can chain multiple function calls together
The Problems with Function Calling
But function calling has significant limitations:
Problem 1: PROVIDER LOCK-IN=================================OpenAI function calling format: {"type": "function", "function": {"name": "...", ...}}Anthropic tool use format: {"name": "...", "input_schema": {...}}Google Gemini format: {"function_declarations": [{"name": "...", ...}]}Each provider uses a different format. Your tool definitionsare NOT portable. Switch providers? Rewrite everything.Problem 2: STATIC DEFINITIONS=================================Tools are defined at the START of a conversation.They are literally embedded in the prompt. You cannot: - Add new tools mid-conversation - Discover tools dynamically - Share tools between applicationsProblem 3: NO STANDARD EXECUTION LAYER=================================The AI says "call get_weather with city=Tokyo"But YOU must implement get_weather()And YOU must wire the result backThere is no standard for HOW tools are executed
Info
Function calling gave AI the ability to choose which tools to use, but it did not standardize how tools are defined, discovered, or executed. Every app reimplements the execution layer from scratch.
MCP: The Complete Solution
MCP takes the best idea from function calling (let the AI choose tools) and wraps it in a complete, standardized protocol. It solves every problem we have discussed.
How MCP Works
Python
# Server side - define a tool ONCEfrom mcp.server.fastmcp import FastMCPmcp = FastMCP("Weather Service")@mcp.tool()async def get_weather(city: str) -> str: """Get current weather for a city. Args: city: The city name to look up weather for """ # Your actual implementation async with httpx.AsyncClient() as client: resp = await client.get( f"https://api.weatherapi.com/v1/current.json", params={"key": API_KEY, "q": city} ) data = resp.json() return f"{data['current']['temp_c']}C, {data['current']['condition']['text']}"# Any MCP-compatible AI can discover and use this tool.
What Makes MCP Fundamentally Different
Here is the key insight: MCP is not just function calling with a different syntax. It is a fundamentally different architecture.
Add One MCP vs REST APIs vs Function Calling Connection
Choose one external system that would genuinely improve this lesson's workflow.
Configure an MCP server or inspect the one your team already uses.
Ask Claude to call exactly one tool from that server and verify the result is useful.
FUNCTION CALLING: App defines tools -> Embeds in prompt -> AI chooses -> App executes (Everything lives in YOUR application)MCP: Server exposes tools -> Client discovers them -> AI chooses -> Server executes (Tools live in independent servers, reusable by ANY client)
Let me break down the five critical differences:
1. Dynamic Discovery
JSON
// With MCP, the AI discovers tools at RUNTIME// Client sends:{"jsonrpc": "2.0", "method": "tools/list", "id": 1}// Server responds with all available tools:{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "get_weather", "description": "Get current weather for a city", "inputSchema": { "type": "object", "properties": { "city": {"type": "string", "description": "City name"} }, "required": ["city"] } } ] }}
No hardcoding tool definitions in prompts. The AI asks "what can you do?" and the server answers.
2. Provider Independence
MCP tool definition works with: - Claude Desktop - Cursor IDE - VS Code Copilot - Windsurf - Any MCP-compatible hostFunction calling tool definition works with: - Only the specific provider you wrote it for
Write once, connect everywhere. That is the MCP promise.
3. Beyond Just Tools
Function calling gives you tools (functions the AI can call). MCP gives you three primitive types:
Python
# TOOLS - Actions the AI can take (like function calling)@mcp.tool()def search_database(query: str) -> str: """Search the product database.""" return db.search(query)# RESOURCES - Data the AI can read (like a file system)@mcp.resource("config://settings")def get_settings() -> str: """Application settings the AI can reference.""" return json.dumps(app_settings)# PROMPTS - Reusable prompt templates@mcp.prompt()def analyze_data(dataset: str) -> str: """Template for data analysis tasks.""" return f"Analyze this dataset thoroughly: {dataset}"
Resources and prompts have no equivalent in function calling. They enable entirely new interaction patterns.
4. Stateful Sessions
REST API: Each request is independent (stateless)Function Call: State lives in conversation context onlyMCP: Persistent session with the serverMCP servers can maintain state across multiple tool calls: Call 1: connect_to_database("production") -> Session established Call 2: run_query("SELECT * FROM users") -> Uses same connection Call 3: run_query("SELECT * FROM orders") -> Still same connection
5. Bidirectional Communication
MCP is not just request/response. Servers can send notifications to clients:
JSON
// Server notifies client that available tools have changed{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed"}// Server can request the AI to sample (generate text){ "jsonrpc": "2.0", "method": "sampling/createMessage", "params": { "messages": [{"role": "user", "content": "Summarize this data..."}], "maxTokens": 500 }}
No other approach offers this level of interaction.
Real-World Comparison: Building a Code Review Bot
Let us see how each approach handles the same real-world task: building an AI that reviews pull requests on GitHub.
Quick Check
What is the main benefit of using MCP vs REST APIs vs Function Calling well in Claude Code?
REST API Approach
Python
# You write ALL the orchestration logicdef review_pr(pr_number): # Fetch PR details pr = requests.get( f"https://api.github.com/repos/owner/repo/pulls/{pr_number}" ).json() # Fetch the diff diff = requests.get(pr["diff_url"]).text # Fetch related files for context files = requests.get(f"{pr['url']}/files").json() # Fetch previous review comments comments = requests.get(f"{pr['url']}/comments").json() # Manually construct the prompt with all this context prompt = f"""Review this PR: Title: {pr['title']} Diff: {diff} Files changed: {json.dumps(files)} Previous comments: {json.dumps(comments)} """ # Send to AI review = call_ai(prompt) # Post the review back (more REST calls) requests.post(f"{pr['url']}/reviews", json={"body": review})
Lines of glue code: 50+. The AI has no autonomy.
Function Calling Approach
Python
# Better - AI decides what info it needstools = [ make_tool("get_pr_details", "Get PR info", {"pr_number": "integer"}), make_tool("get_pr_diff", "Get the diff", {"pr_number": "integer"}), make_tool("get_file_content", "Read a file", {"path": "string"}), make_tool("post_review", "Post review", {"pr_number": "integer", "body": "string"}),]# Still need to implement each function yourself# Still locked to one AI provider# Tool definitions are static
Lines of glue code: 30. Better, but still provider-locked.
MCP Approach
JSON
// In Claude Desktop config, just add:{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "your-token" } } }}// Claude now has full GitHub access.// It discovers and uses all GitHub tools automatically.// No glue code. No custom implementations.
0
Lines of Glue Code
With MCP, the server handles discovery, execution, and results -- your application needs zero custom integration code
Lines of glue code: 0. The MCP server handles everything.
Tip
The GitHub MCP server is a real, production-ready server maintained by the community. When you connect it to Claude Desktop, the AI automatically discovers tools like create_pull_request, search_repositories, get_file_contents, and dozens more.
When to Use What
Each approach has its place. Here is a decision framework:
Use REST APIs When:
Building traditional app-to-app integrations
The AI does not need to make decisions about which APIs to call
You are building a simple, linear pipeline (fetch data then process then display)
You need maximum control over every API call
Use Function Calling When:
You are locked into a single AI provider
You have a small, fixed set of tools (fewer than 10)
You do not need tool reusability across projects
You are building a quick prototype
Use MCP When:
You want tools that work with any AI application
You need dynamic tool discovery
You are building tools that multiple teams or projects will share
You want bidirectional communication
You need stateful, session-based interactions
You are building production-grade AI integrations
Choosing Your Integration Approach
Do
Start with MCP for new AI integrations -- it is portable, discoverable, and future-proof
Don't
Use REST glue code for AI tool access when MCP can handle discovery and execution automatically
Decision flowchart:Does the AI need to choose which actions to take? No -> Use REST APIs Yes -> Do you need portability across AI providers? No -> Function calling may suffice Yes -> Use MCP
The Convergence
Here is what is fascinating: the industry is converging on MCP. OpenAI added MCP support in March 2025. Google ADK supports MCP. Microsoft Copilot Studio supports MCP. Even function calling is evolving toward MCP compatibility.
This is not about one approach winning. It is about the industry recognizing that a standard protocol for AI-tool communication is essential, and MCP is becoming that standard.
Try This Now
Take one of your existing projects that uses REST APIs or function calling. Map out all the integration points. Then sketch how you would redesign it with MCP servers. How many lines of glue code would disappear? Which parts become reusable across projects? This exercise will cement the difference in your mind.
Key Takeaways
REST APIs require you to write all routing and glue code; the AI has no autonomy in choosing which APIs to call
Function calling lets the AI choose tools but locks you into a specific provider format with no standardization
MCP combines AI-driven tool selection with a standardized protocol, dynamic discovery, and stateful sessions
MCP goes beyond tools by adding resources (data) and prompts (templates) as first-class primitives
The industry is converging on MCP, with OpenAI, Google, and Microsoft all adding support
Use REST for simple pipelines, function calling for quick prototypes, and MCP for production-grade AI integrations