A person pulling a sheet from a machine with a CALL button, gauge, and Python chip, next to a transparent box of MCP, Analysis, and Reuse modules fed by an API pipe.

MCP vs API Calls: Which Should You Use for Python LLM Apps?

by Valerio Maggio Updated Reading time estimate 28m intermediate ai api best-practices

Anthropic open-sourced the Model Context Protocol (MCP) in November 2024 to fix a scaling problem: every new tool a model reached for needed its own integration, built on direct API calls. Building that integration today involves a decision usually framed as MCP vs API calls.

One year later, more than 10,000 public MCP servers were running. ChatGPT, Gemini, Cursor, and Microsoft Copilot had all added support, and Anthropic had donated the protocol to the Linux Foundation. So it’s tempting to see direct API calls as the old way to connect a tool and MCP as what comes next.

With direct API calls, you describe the tool to the model provider’s API yourself, and your own function-calling code runs that tool whenever the model requests it. With MCP, you put that same tool behind a small standard server that any MCP-aware client can discover and call. To see the difference up close, you’ll build one small tool that looks up a package on PyPI and connect it to an app both ways.

MCP isn’t a replacement for direct API calls, though. An MCP server typically reaches its own backends the same way, through ordinary HTTP requests. MCP adds a layer on top of those requests instead of replacing them. So the question isn’t which technology is newer. It’s whether you’ll reuse your tools beyond the app you’re writing today.

If you’ve ever integrated the same tool into several apps and then kept those copies in sync by hand, then MCP targets exactly that problem. You write the tool once, and every app reuses it. If you’re shipping one app and want nothing extra to run, then the direct API call you already know is hard to beat. Here’s a quick reference to help you choose the right approach:

Use Case Pick MCP Pick Direct API Calls
You’ll reuse the same tools across several apps or model providers ✅ —
You’re shipping one app and want zero extra infrastructure — ✅

The comparison sections below give the reasoning behind each row.

Take the Quiz: Test your knowledge with our interactive “MCP vs API Calls: Which Should You Use for Python LLM Apps?” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

MCP vs API Calls: Which Should You Use for Python LLM Apps?

Test your understanding of MCP vs API calls in Python LLM apps, from tool schemas and MCP servers to when each approach pays off.

Metrics Comparison: MCP vs API Calls

MCP and direct API calls give you the same capability, so what separates them is structure and cost. A single tool call takes about the same time in both, so the comparison comes down to what each approach installs, runs, and repeats as your project grows.

This comparison weighs the two approaches along four axes:

  • Setup cost: Both approaches need a client library to reach the model. MCP also needs a package that implements the protocol, plus a separate server process to run. Direct API calls need neither, because the tool runs as a plain function inside your app.
  • Code per tool: Each tool needs a schema, the JSON description of its name and parameters. MCP takes a one-line registration and hands the schema back to any client that asks, while a direct API call takes a schema you write by hand.
  • Reach: An MCP server carries a tool to other model providers and to clients you didn’t write, while a direct API call hard-codes the tool inside one app.
  • Maturity: MCP arrived in late 2024, and its specification still moves quickly, while function calling has been part of the major model APIs since 2023.

Both approaches also need a dispatch loop, the code that runs the tool the model asks for and sends the result back.

The costs add up differently. You pay MCP’s setup cost once: a dependency, a server, and a registration. The server does remain one more process to run, though. The direct API call’s costs are smaller but return with every new app that needs the tool. So the more apps you build, the more MCP’s upfront cost pays for itself.

Feature Comparison 1: Integration Effort and Reusability

The first difference between the two approaches is the work of integrating a tool and how much of that work you can reuse when the next app needs the same tool.

You’ll write one Python function, get_package_info(), and integrate this function into a Python LLM app two ways: once with direct API calls and once with MCP. The tool stays identical across both approaches, so the only difference is its integration in the app.

Two terms are worth pinning down first. This tutorial uses the model as shorthand for the LLM that your app sends questions to, and it uses function and tool interchangeably, because a function that the model can call is what both approaches refer to as a tool.

The examples use Python 3.14 with anthropic 1.4.0 and mcp 2.1.1, and all the code runs on Python 3.10 and later. To follow along, you should be comfortable writing Python functions and working with dictionaries and JSON. The MCP client uses async and await, so a quick look at Python’s asyncio will help if those are new to you.

It’s a good idea to install both packages in a dedicated virtual environment, which keeps them from interfering with your other projects. You can create the virtual environment and install the packages with pip:

Language: Shell
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install anthropic==1.4.0 mcp==2.1.1

This tutorial uses the Anthropic client as an example, and that client requires an API key to work. Create a key in the Claude Platform, and then export it as an environment variable in your shell session so that the anthropic SDK can use it:

Language: Shell
(.venv) $ export ANTHROPIC_API_KEY="your-api-key-here"

With the environment ready, create a file named tool.py and add the function that both approaches share:

Language: Python Filename: tool.py
 1import json
 2import urllib.request
 3
 4def get_package_info(package_name: str) -> dict:
 5    """
 6    Look up a package on PyPI
 7    and return its latest version and summary.
 8    """
 9    url = f"https://pypi.org/pypi/{package_name}/json"
10    with urllib.request.urlopen(url) as response:
11        data = json.load(response)
12    info = data["info"]
13    return {
14        "name": info["name"],
15        "version": info["version"],
16        "summary": info["summary"],
17    }

The get_package_info() function gives your Python LLM app the capability to fetch live data from PyPI.

The function does three things:

  1. Takes a package_name argument and declares a dictionary return type (line 4)
  2. Calls the real PyPI JSON endpoint at https://pypi.org/pypi/<name>/json with urlopen() (lines 9 to 11)
  3. Trims the large response down to three fields (lines 13 to 17): the package name, its latest version, and summary, the natural language description of the package that the model uses for reasoning

Pay attention to the package_name: str annotation (line 4) and the docstring (lines 5 to 8), because they carry more weight than they appear to. When you promote this function to a tool, MCP reads both to describe the tool to the model, so a missing annotation or a vague docstring leaves the model guessing.

Everything past this point is scaffolding, and the scaffolding is where direct API calls and MCP part ways. Here’s how the two approaches compare:

Direct API calls and MCP running the same model loop and reaching the same PyPI JSON API.
Direct API Calls and MCP Side by Side

The two apps run the same loop. In both, the model asks for a tool with a tool_use block and gets a tool_result back. What changes is where the tool lives and where its schema comes from. For direct API calls, get_package_info() sits inside your app, and you write the schema by hand. When you use MCP, the function moves out to a server, and the server hands the schema to the client.

Under MCP, two named calls cross the boundary between your app and the server. list_tools() asks the server which tools it offers and gets their schemas back. call_tool() runs one of those tools and gets the result back. Direct API calls need neither, because the schema is already written in your app and running the tool is an ordinary function call.

Both paths still end at the same PyPI JSON API, since get_package_info() reaches PyPI through an ordinary HTTP request whether you call it directly or place it behind a server. MCP wraps that request rather than replacing it.

With Direct API Calls

To call get_package_info() directly, you keep the function inside your app and describe it to the model yourself. In the same folder as tool.py, create a file named client_api.py, starting with the schema that declares the tool:

Language: Python Filename: client_api.py
 1import json
 2
 3import anthropic
 4from anthropic.types import Message, ToolUseBlock
 5
 6from tool import get_package_info
 7
 8MODEL = "claude-sonnet-5"
 9QUESTION = (
10    "What is the latest version of the Jinja2 package? "
11    "Answer in one short plain-text sentence."
12)
13
14TOOLS = [
15    {
16        "name": "get_package_info",
17        "description": (
18            "Look up a package on PyPI and return its "
19            "latest version and summary."
20        ),
21        "input_schema": {
22            "type": "object",
23            "properties": {
24                "package_name": {
25                    "type": "string",
26                    "description": "The package name on PyPI.",
27                },
28            },
29            "required": ["package_name"],
30        },
31    },
32]

MODEL names the model that answers, and QUESTION holds the question you’ll ask (lines 8 to 12). Keeping both at the top means you can point the example at a different model or ask something else without touching any other code. The MCP client later in this tutorial defines the same two constants with the same values, so both approaches ask exactly the same thing of exactly the same model.

The TOOLS list (lines 14 to 32) is the JSON schema you maintain. The list names the tool (line 16), describes what it does (lines 17 to 20), and declares the package_name parameter (lines 24 to 27). Every one of those details already exists in tool.py.

Next, you’ll add ask_model(), which sends a question to the model along with the tool:

Language: Python Filename: client_api.py
33# ...
34def ask_model(
35    client: anthropic.Anthropic,
36    messages: list[dict],
37    tools: list[dict],
38    model: str = MODEL,
39) -> Message:
40    return client.messages.create(
41        model=model,
42        max_tokens=2048,
43        tools=tools,
44        messages=messages,
45    )

ask_model() wraps a single call to the Claude API. It takes the conversation so far, stored in messages, and returns a Message, which is the model’s whole turn in that conversation. A turn can hold more than prose, so the content of a Message is a list of blocks rather than a single string.

To handle the model’s tool requests, you’ll implement run_tool(), which runs the function the model asked for, and tool_result(), which packages the output in the format the model expects:

Language: Python Filename: client_api.py
46# ...
47def run_tool(block: ToolUseBlock) -> str:
48    result = get_package_info(**block.input)
49    return json.dumps(result)
50
51def tool_result(block: ToolUseBlock, output: str) -> dict:
52    return {
53        "type": "tool_result",
54        "tool_use_id": block.id,
55        "content": output,
56    }

ToolUseBlock is one of the content blocks that a Message can hold. The model adds a ToolUseBlock when it wants a tool run, and that block carries the tool name, the arguments the model chose, and an ID.

run_tool() (lines 47 to 49) calls get_package_info() in your own process with those arguments. tool_result() (lines 51 to 56) wraps the output in the tool_result block that the model expects back, echoing the same ID so the model can match your result to the request that produced it.

Finally, you’ll write main() to drive the conversation:

Language: Python Filename: client_api.py
57# ...
58def main() -> None:
59    client = anthropic.Anthropic()
60    messages = [{"role": "user", "content": QUESTION}]
61    response = ask_model(client, messages, TOOLS)
62
63    tool_uses = [b for b in response.content if b.type == "tool_use"]
64    if tool_uses:
65        messages.append({"role": "assistant", "content": response.content})
66        results = [tool_result(b, run_tool(b)) for b in tool_uses]
67        messages.append({"role": "user", "content": results})
68
69    final = ask_model(client, messages, TOOLS)
70    print(next(b.text for b in final.content if b.type == "text"))
71
72if __name__ == "__main__":
73    main()

main() asks the model the question (line 61), then collects every tool_use block the response contains (line 63). It appends the model’s turn once (line 65), runs each tool, and sends all the results back in a single user message (lines 66 and 67). The second call to ask_model() (line 69) sends that updated conversation back, and the model turns the raw data into an answer.

This loop simplifies the dispatch in two ways. run_tool() calls get_package_info() directly instead of mapping the name in block.name to a matching function, because TOOLS declares one tool. The loop also makes a single round trip, while a real app keeps calling the model until it stops asking for tools.

Running the script prints something like:

Language: Shell
(.venv) $ python client_api.py
The latest version of the Jinja2 package is 3.1.6.

This approach works, and all the integration code sits in client_api.py and runs in a single process. Two pieces of that file are yours to maintain. The schema restates what tool.py already declares, and the mapping from tool name to function is yours to write. MCP removes both the schema and the mapping.

With an MCP Server

The MCP approach introduces a server between your app and the tool. The server holds get_package_info(), derives the schema from the function, and looks up tools by name. Any MCP client can then discover and call the tool without knowing how the function works.

The Server

MCPServer, which ships with the mcp SDK, implements that server for you. In the same folder as tool.py, create a file named server.py that imports get_package_info() and registers the function as a tool:

Language: Python Filename: server.py
 1from mcp.server import MCPServer
 2from tool import get_package_info
 3
 4mcp = MCPServer("pypi-tools")
 5mcp.add_tool(get_package_info)
 6
 7if __name__ == "__main__":
 8    mcp.run()

MCPServer("pypi-tools") (line 4) creates a named server, and mcp.add_tool() (line 5) registers get_package_info() as a callable tool. That single registration replaces the TOOLS constant you wrote in client_api.py. MCPServer derives the schema from the function itself, taking the tool name from the function name, the description from the docstring, and the parameter and its type from the package_name: str annotation.

Finally, mcp.run() (line 8) starts the server over standard I/O (stdio) when the file runs, but you won’t launch server.py yourself. The client spawns the server as a subprocess and talks to it over that channel.

The Client

Create a file named client.py alongside tool.py and server.py. The MCP client API is asynchronous, so its calls run inside an asyncio event loop. Start with the imports and the function that asks the server which tools it offers:

Language: Python Filename: client.py
 1import asyncio
 2import sys
 3
 4import anthropic
 5from anthropic.types import Message, ToolUseBlock
 6from mcp import Client, StdioServerParameters
 7
 8MODEL = "claude-sonnet-5"
 9QUESTION = (
10    "What is the latest version of the Jinja2 package? "
11    "Answer in one short plain-text sentence."
12)
13
14async def discover_tools(client: Client) -> list[dict]:
15    discovered = await client.list_tools()
16    return [
17        {
18            "name": tool.name,
19            "description": tool.description,
20            "input_schema": tool.input_schema,
21        }
22        for tool in discovered.tools
23    ]

discover_tools() is where MCP shows its advantage. The call to client.list_tools() (line 15) asks the server what it offers, and the server hands back the schema it derived from get_package_info(). The comprehension (lines 16 to 23) reshapes that schema into the format the model expects.

discover_tools() returns a list[dict], the same type as the TOOLS constant in client_api.py, except that this time you didn’t have to write the schema yourself.

The next three functions should look familiar, because you already wrote them in client_api.py:

Language: Python Filename: client.py
24# ...
25def ask_model(
26    client: anthropic.Anthropic,
27    messages: list[dict],
28    tools: list[dict],
29    model: str = MODEL,
30) -> Message:
31    return client.messages.create(
32        model=model,
33        max_tokens=2048,
34        tools=tools,
35        messages=messages,
36    )
37
38async def run_tool(client: Client, block: ToolUseBlock) -> str:
39    result = await client.call_tool(block.name, block.input)
40    return result.content[0].text
41
42def tool_result(block: ToolUseBlock, output: str) -> dict:
43    return {
44        "type": "tool_result",
45        "tool_use_id": block.id,
46        "content": output,
47    }

ask_model() (lines 25 to 36) and tool_result() (lines 42 to 47) are character for character the same as in client_api.py. The conversation with the model has the same structure as before.

run_tool() (lines 38 to 40) is the one function that changes. It’s a coroutine function here, declared with async def, because client.call_tool() has to be awaited inside the event loop. It still returns a str, but it takes a client as well, since the server runs the tool instead of your own process.

Notice that client.call_tool() passes block.name along. The server holds the mapping and looks up the tool by name, so this loop keeps working when the server offers more tools. The mapping you’d have to write in client_api.py never appears here.

The Client in Action

Finish client.py with main(), which now manages its MCP connection with an asynchronous context manager:

Language: Python Filename: client.py
48# ...
49async def main(server: StdioServerParameters) -> None:
50    async with Client(server) as mcp:
51        tools = await discover_tools(mcp)
52
53        client = anthropic.Anthropic()
54        messages = [{"role": "user", "content": QUESTION}]
55        response = ask_model(client, messages, tools)
56
57        tool_uses = [b for b in response.content if b.type == "tool_use"]
58        if tool_uses:
59            messages.append({"role": "assistant", "content": response.content})
60            results = [
61                tool_result(b, await run_tool(mcp, b)) for b in tool_uses
62            ]
63            messages.append({"role": "user", "content": results})
64
65        final = ask_model(client, messages, tools)
66        print(next(b.text for b in final.content if b.type == "text"))
67
68if __name__ == "__main__":
69    server = StdioServerParameters(
70        command=sys.executable,
71        args=["server.py"],
72    )
73    asyncio.run(main(server))

The Client(server) context manager (line 50) launches the server, opens a channel to it, and negotiates the connection, all on entry. It releases those resources automatically when main() finishes.

Past that setup, the body of main() (lines 51 to 66) is the same conversation loop you built in client_api.py. It asks the model, spots a tool_use block, runs the tool, and sends the result back for a final answer. The only difference is that tools now comes from the server instead of being hard-coded in your app.

StdioServerParameters (line 69) names the command to run and the arguments to pass it, which is how the client knows to launch server.py.

The command is sys.executable, the absolute path to the interpreter running the client, rather than a plain "python" string. That points the server subprocess at the same interpreter, so it finds mcp in your active virtual environment. Most examples, including the official docs, show a bare python here, which assumes your shell resolves it to the right environment.

Running the client prints the same kind of answer as before:

Language: Shell
(.venv) $ python client.py
The latest version of the Jinja2 package is 3.1.6.

You only ever run client.py. It launches server.py as a subprocess and talks to it over stdio, which is what StdioServerParameters sets up. That’s why you never start the server by hand.

This client is intentionally minimal. For a fuller one with an interactive chat loop, work through Real Python’s Build a Python MCP Client tutorial.

Both approaches took a similar amount of code, and much of it is literally the same code. ask_model() and tool_result() are identical in both files, main() runs the same conversation loop in each, and both approaches call the same get_package_info() from tool.py. Integration effort isn’t what separates the two approaches. The difference lies in what the code contains.

You can see this in the conversation itself. Toggle between the two approaches in the widget below and watch the messages list. Either way, it holds the same three messages in the same order. The only difference is formatting: the MCP tool result arrives as indented JSON, while the direct path sends compact JSON. The real differences sit outside the conversation, in where the tool list comes from and which process runs the function:

Interactive diagram — enable JavaScript to view.

The model sees essentially the same conversation either way, so the difference lies in the code around it. In client_api.py, you wrote the schema yourself, in the exact shape the anthropic SDK expects, and hard-coded this schema into the app. Point that app at another provider, and you rewrite the schema. With MCP, the server derives the schema from the function, and the protocol carries it to the client, so your app and the tool stay decoupled. The price of that is a server to run.

The choice is a question of scale, then. If get_package_info() only ever serves this one app, the schema you wrote costs you nothing more. If other apps need the same tool, then each one writes that schema again, while a single MCP server publishes it once for every client that connects.

Each client still reshapes what the server sends into the format its own model expects, which is what discover_tools() does. That reshaping is the same few lines whether the server offers one tool or twenty. The handwritten schema in client_api.py has no such property, because every new tool means another entry you write yourself.

Feature Comparison 2: Standardization and Interoperability

Every app that has called get_package_info() so far is one you wrote, in Python, against a model you chose. So far, reuse has stayed inside your own project. The harder test is an app you didn’t write, in a language that isn’t Python, talking to a model you didn’t choose.

With Direct API Calls

The TOOLS schema you wrote in client_api.py targets Anthropic’s tool call format. Moving to another model provider means reshaping that schema and the dispatch loop around it. Tool discovery is limited to what client_api.py hard-codes, so the model can’t ask which tools exist at runtime.

Extracting the schema and the dispatch loop into a shared Python module would help the other apps you write, but it wouldn’t reach any further. An app in another language, or a desktop client you didn’t write, still can’t call get_package_info(), because the tool is a Python function inside your app, with no interface for anything outside your app.

With an MCP Server

Rewriting the same integration for each new app or provider is exactly what MCP exists to remove. Anthropic introduced the protocol to replace one-off, custom integrations with a single standard.

MCP is one protocol that many clients already speak, so server.py works with apps you didn’t write and with model providers you haven’t chosen yet. The protocol is language-agnostic, so a client written in another language, such as TypeScript, can call get_package_info() without knowing that Python is involved. Any MCP client can also list a server’s tools at runtime, which is how a new app discovers the tool without you touching its code.

What stays provider-specific is the client side. In client.py, everything that talks to the model follows Anthropic’s format: ask_model(), the reshaping in discover_tools(), and the tool_use and tool_result blocks that run_tool(), tool_result(), and main() handle. Switching providers means rewriting that half of the client, while server.py stays the same. That rewrite is the same size whether the server offers one tool or twenty.

Because every request passes through the server, that server also becomes the natural place to enforce a security boundary, such as checking who’s allowed to call which tool.

Feature Comparison 3: Overhead, Control, and Maturity

The two approaches also differ in what they cost you to run, in how much control you keep over a tool call, and in how settled the technology underneath them is.

One thing stays the same either way. The HTTP request that get_package_info() sends to PyPI doesn’t disappear under MCP, because the server makes that same request on your app’s behalf:

An MCP server wrapping a direct API call to the PyPI JSON API.
An MCP Server Around a Direct API Call

The HTTP request to PyPI sits inside the MCP server rather than beside it. Your app reaches the tool through MCP, and the tool still reaches PyPI through an ordinary HTTP request, so MCP adds a layer around that request instead of replacing it. That’s why the skills you practice in client_api.py keep paying off whichever approach you choose, and why APIs aren’t becoming obsolete for Python LLM apps.

With Direct API Calls

client_api.py runs in one process. A call to get_package_info() is an ordinary Python function call, so a traceback points straight at your own code, and the dispatch loop is yours to change whenever the model does something you didn’t expect. Nothing sits between your app and the tool.

With an MCP Server

server.py is a second process that you launch, monitor, and debug. Every call to get_package_info() now crosses the stdio channel instead of running inside your app, so when client.py fails, the cause could be the tool, the server, or the transport between them.

MCP’s overhead isn’t always worth paying:

  • Prototypes and one-off scripts: A single-file script doesn’t need a separate server process, so with MCP you pay the setup cost before you see any benefit.
  • Tight debugging loops: While you’re still changing the tool many times an hour, one process and one traceback are quicker to work through than two processes with a channel in between.
  • Protocol stability: The MCP specification is still young and moves quickly, so patterns can shift between releases.

Decision Table: MCP vs API Calls

This table maps common situations to the approach that fits, so you can find your own use case:

If You… Choose MCP Choose Direct API Calls
Reuse the same tools across several apps or clients ✅ —
Need the same tools to work across model providers ✅ —
Want runtime tool discovery and swappable tools ✅ —
Ship one app and want zero extra infrastructure — ✅
Need maximum control and minimal dependencies — ✅
Want the leanest path for a quick prototype — ✅

The top three rows describe tools that will be reused beyond a single app. The bottom three describe a single app that values leanness and minimal setup. If your situation matches rows on both sides, then let reuse decide: start with direct API calls, and move the tool behind an MCP server once a second app needs it.

Conclusion

You’ve integrated one Python function, get_package_info(), into an LLM app two ways: once with direct API calls and once through an MCP server. Both apps ran the same conversation loop and gave the same answer. What changed was where the tool lived and where its schema came from.

In this tutorial, you’ve:

  • Integrated a PyPI lookup tool with direct API calls and a handwritten JSON schema
  • Registered the same function on an MCP server that derives the schema for you
  • Built an MCP client that discovers and runs tools with list_tools() and call_tool()
  • Compared both approaches on reusability, interoperability, and operational overhead

Direct API calls win on leanness and control, while MCP wins on reuse and standardization. The deciding question is how far beyond a single app your tools need to reach. Whichever approach you choose, your API skills still count, because an MCP server typically reaches its backends through ordinary web APIs.

A good next step is to swap get_package_info() for a tool from your own project and integrate it the way that fits your situation. To go deeper on the pieces around this choice, a few Real Python tutorials pick up nearby threads:

You can find the complete source code for both approaches in the supporting materials linked below:

Frequently Asked Questions

Now that you’ve compared MCP and direct API calls, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.

MCP is the Model Context Protocol, a standard way for a Python LLM app to discover and call tools over a small server. A direct API call keeps that same tool inside your app, integrated through your own function-calling code. MCP moves the tool behind a shared protocol, while direct API calls keep everything in the same process.

No. An MCP server typically reaches its own backends through ordinary HTTP requests, so MCP rides on top of APIs rather than replacing them. Direct API calls stay lean and battle-tested, so they aren’t becoming obsolete, and the skills you build calling them keep paying off whichever approach you pick.

Reach for MCP when the same tools will be reused across several apps, clients, or model providers. The write-once server saves you from re-implementing the tool schema and the mapping from tool names to functions in every app. For a single app with no reuse in sight, direct API calls stay the leaner choice.

Yes. MCP is one protocol that many clients already speak, so a single server works across providers without a rewrite. Direct API calls, by contrast, use a provider-specific tool call format that you reshape whenever you switch providers.

The overhead is operational rather than per call. A single tool call takes about the same time either way, but an MCP server is a second process to launch, monitor, and debug, and a failure could come from the tool, the server, or the transport between them.

Take the Quiz: Test your knowledge with our interactive “MCP vs API Calls: Which Should You Use for Python LLM Apps?” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

MCP vs API Calls: Which Should You Use for Python LLM Apps?

Test your understanding of MCP vs API calls in Python LLM apps, from tool schemas and MCP servers to when each approach pays off.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Dictionary merging in Python 3.5+

About Valerio Maggio

Valerio is a Data Scientist and Developer Advocate who's been loitering around the PyCon community since 2009. Unapologetic nerd. Catch him at Magic: The Gathering tables as "lotus_vale"

» More about Valerio

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!