From 5f0c1ebef1021fcb34e7215576e3b9512651a4d8 Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 2 Sep 2026 13:48:48 +0100 Subject: [PATCH 1/6] Tool: Add function to get package info from PyPI This function retrieves package information from PyPI, including the name, version, and summary. --- mcp-vs-api-calls/tool.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 mcp-vs-api-calls/tool.py diff --git a/mcp-vs-api-calls/tool.py b/mcp-vs-api-calls/tool.py new file mode 100644 index 0000000000..892b9dae11 --- /dev/null +++ b/mcp-vs-api-calls/tool.py @@ -0,0 +1,18 @@ +import json +import urllib.request + + +def get_package_info(package_name: str) -> dict: + """ + Look up a package on PyPI + and return its latest version and summary. + """ + url = f"https://pypi.org/pypi/{package_name}/json" + with urllib.request.urlopen(url) as response: + data = json.load(response) + info = data["info"] + return { + "name": info["name"], + "version": info["version"], + "summary": info["summary"], + } From 0e91746723e3cc465bcbc5c566990f954b422b67 Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 2 Sep 2026 13:50:18 +0100 Subject: [PATCH 2/6] MCP vs API Calls Python LLM Apps Materials Materials attached to the "MCP vs API Calls: Which Should You Use for Python LLM Apps?" tutorial --- mcp-vs-api-calls/client.py | 82 ++++++++++++++++++++++++++++++++++ mcp-vs-api-calls/client_api.py | 82 ++++++++++++++++++++++++++++++++++ mcp-vs-api-calls/server.py | 8 ++++ 3 files changed, 172 insertions(+) create mode 100644 mcp-vs-api-calls/client.py create mode 100644 mcp-vs-api-calls/client_api.py create mode 100644 mcp-vs-api-calls/server.py diff --git a/mcp-vs-api-calls/client.py b/mcp-vs-api-calls/client.py new file mode 100644 index 0000000000..0240e5e1d2 --- /dev/null +++ b/mcp-vs-api-calls/client.py @@ -0,0 +1,82 @@ +import asyncio +import sys + +import anthropic +from anthropic.types import Message, ToolUseBlock +from mcp import Client, StdioServerParameters + +MODEL = "claude-sonnet-5" +QUESTION = ( + "What is the latest version of the Jinja2 package? " + "Answer in one short plain-text sentence." +) + + +async def discover_tools(client: Client) -> list[dict]: + discovered = await client.list_tools() + return [ + { + "name": tool.name, + "description": tool.description, + "input_schema": tool.input_schema, + } + for tool in discovered.tools + ] + + +def ask_model( + client: anthropic.Anthropic, + messages: list[dict], + tools: list[dict], + model: str = MODEL, +) -> Message: + return client.messages.create( + model=model, + max_tokens=1024, + tools=tools, + messages=messages, + ) + + +async def run_tool(client: Client, block: ToolUseBlock) -> str: + result = await client.call_tool(block.name, block.input) + return result.content[0].text + + +def tool_result(block: ToolUseBlock, output: str) -> dict: + return { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }, + ], + } + + +async def main(server: StdioServerParameters) -> None: + async with Client(server) as mcp: + tools = await discover_tools(mcp) + + client = anthropic.Anthropic() + messages = [{"role": "user", "content": QUESTION}] + response = ask_model(client, messages, tools) + for block in response.content: + if block.type == "tool_use": + messages.append( + {"role": "assistant", "content": response.content} + ) + output = await run_tool(mcp, block) + messages.append(tool_result(block, output)) + final = ask_model(client, messages, tools) + print(final.content[0].text) + + +if __name__ == "__main__": + server = StdioServerParameters( + command=sys.executable, + args=["server.py"], + ) + asyncio.run(main(server)) diff --git a/mcp-vs-api-calls/client_api.py b/mcp-vs-api-calls/client_api.py new file mode 100644 index 0000000000..069b84a472 --- /dev/null +++ b/mcp-vs-api-calls/client_api.py @@ -0,0 +1,82 @@ +import json + +import anthropic +from anthropic.types import Message, ToolUseBlock + +from tool import get_package_info + +MODEL = "claude-sonnet-5" +QUESTION = ( + "What is the latest version of the Jinja2 package? " + "Answer in one short plain-text sentence." +) + +TOOLS = [ + { + "name": "get_package_info", + "description": ( + "Look up a package on PyPI and return its " + "latest version and summary." + ), + "input_schema": { + "type": "object", + "properties": { + "package_name": { + "type": "string", + "description": "The package name on PyPI.", + }, + }, + "required": ["package_name"], + }, + }, +] + + +def ask_model( + client: anthropic.Anthropic, + messages: list[dict], + tools: list[dict], + model: str = MODEL, +) -> Message: + return client.messages.create( + model=model, + max_tokens=1024, + tools=tools, + messages=messages, + ) + + +def run_tool(block: ToolUseBlock) -> str: + result = get_package_info(**block.input) + return json.dumps(result) + + +def tool_result(block: ToolUseBlock, output: str) -> dict: + return { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }, + ], + } + + +def main() -> None: + client = anthropic.Anthropic() + messages = [{"role": "user", "content": QUESTION}] + response = ask_model(client, messages, TOOLS) + for block in response.content: + if block.type == "tool_use": + messages.append( + {"role": "assistant", "content": response.content}, + ) + messages.append(tool_result(block, run_tool(block))) + final = ask_model(client, messages, TOOLS) + print(final.content[0].text) + + +if __name__ == "__main__": + main() diff --git a/mcp-vs-api-calls/server.py b/mcp-vs-api-calls/server.py new file mode 100644 index 0000000000..9fb5943127 --- /dev/null +++ b/mcp-vs-api-calls/server.py @@ -0,0 +1,8 @@ +from mcp.server import MCPServer +from tool import get_package_info + +mcp = MCPServer("pypi-tools") +mcp.add_tool(get_package_info) + +if __name__ == "__main__": + mcp.run() From 86d1992f0d2125d274b7ea1299fb7c757ba08c97 Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 2 Sep 2026 13:54:39 +0100 Subject: [PATCH 3/6] Create README.md for MCP vs API Calls tutorial Add README for MCP vs API Calls tutorial with setup instructions and example usage. --- mcp-vs-api-calls/README.md | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 mcp-vs-api-calls/README.md diff --git a/mcp-vs-api-calls/README.md b/mcp-vs-api-calls/README.md new file mode 100644 index 0000000000..7fc000e502 --- /dev/null +++ b/mcp-vs-api-calls/README.md @@ -0,0 +1,44 @@ +# MCP vs API Calls: Which Should You Use for Python LLM Apps? + +This folder contains the sample code for the Real Python tutorial [MCP vs API Calls: Which Should You Use for Python LLM Apps?](https://realpython.com/mcp-vs-api-calls/). + +## Files + +- `tool.py`: the PyPI lookup function that both approaches share, written with the standard library only. +- `client_api.py`: the direct API calls integration, which declares the tool schema by hand. +- `server.py`: the MCP server, which exposes the tool through the `mcp` SDK. +- `client.py`: the MCP client, which discovers and calls the tool through the server. + +## Setup + +Create a virtual environment and install the dependencies: + +```console +$ python -m venv tools-venv +$ source tools-venv/bin/activate +(tools-venv) $ python -m pip install anthropic "mcp>=2,<3" +``` + +The examples use the Anthropic client, which requires an API key. Create a key in the [Anthropic Console](https://console.anthropic.com/), scope it to a single workspace, and export it: + +```console +(tools-venv) $ export ANTHROPIC_API_KEY="your-api-key-here" +``` + +## Running the examples + +Run the direct API calls version: + +```console +(tools-venv) $ python client_api.py +The latest version of the Jinja2 package is 3.1.6. +``` + +Run the MCP version: + +```console +(tools-venv) $ python client.py +The latest version of the Jinja2 package is 3.1.6. +``` + +You only run `client.py`. It launches `server.py` as a subprocess and talks to it over standard I/O, so you never start the server yourself. From 8d03f9fe91e19482a4fb44d39afbe9c75bc6f42c Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 16 Sep 2026 07:31:11 +0100 Subject: [PATCH 4/6] Update installation command for Anthropic package --- mcp-vs-api-calls/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-vs-api-calls/README.md b/mcp-vs-api-calls/README.md index 7fc000e502..d4ba755b7e 100644 --- a/mcp-vs-api-calls/README.md +++ b/mcp-vs-api-calls/README.md @@ -16,7 +16,7 @@ Create a virtual environment and install the dependencies: ```console $ python -m venv tools-venv $ source tools-venv/bin/activate -(tools-venv) $ python -m pip install anthropic "mcp>=2,<3" +(tools-venv) $ python -m pip install "anthropic>=1,<2" "mcp>=2,<3" ``` The examples use the Anthropic client, which requires an API key. Create a key in the [Anthropic Console](https://console.anthropic.com/), scope it to a single workspace, and export it: From ebf7ba867dbb85bd58590ec289adc043e8a5840e Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 16 Sep 2026 07:31:41 +0100 Subject: [PATCH 5/6] Increase max_tokens and refactor tool_result handling --- mcp-vs-api-calls/client.py | 39 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/mcp-vs-api-calls/client.py b/mcp-vs-api-calls/client.py index 0240e5e1d2..b5f2fd6e33 100644 --- a/mcp-vs-api-calls/client.py +++ b/mcp-vs-api-calls/client.py @@ -32,7 +32,7 @@ def ask_model( ) -> Message: return client.messages.create( model=model, - max_tokens=1024, + max_tokens=2048, tools=tools, messages=messages, ) @@ -45,14 +45,9 @@ async def run_tool(client: Client, block: ToolUseBlock) -> str: def tool_result(block: ToolUseBlock, output: str) -> dict: return { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": block.id, - "content": output, - }, - ], + "type": "tool_result", + "tool_use_id": block.id, + "content": output, } @@ -63,15 +58,25 @@ async def main(server: StdioServerParameters) -> None: client = anthropic.Anthropic() messages = [{"role": "user", "content": QUESTION}] response = ask_model(client, messages, tools) - for block in response.content: - if block.type == "tool_use": - messages.append( - {"role": "assistant", "content": response.content} - ) - output = await run_tool(mcp, block) - messages.append(tool_result(block, output)) + + tool_uses = [b for b in response.content if b.type == "tool_use"] + if tool_uses: + messages.append({"role": "assistant", "content": response.content}) + results = [ + tool_result(b, await run_tool(mcp, b)) for b in tool_uses + ] + messages.append({"role": "user", "content": results}) + final = ask_model(client, messages, tools) - print(final.content[0].text) + print(next(b.text for b in final.content if b.type == "text")) + + +if __name__ == "__main__": + server = StdioServerParameters( + command=sys.executable, + args=["server.py"], + ) + asyncio.run(main(server)) if __name__ == "__main__": From 2916ea2bae1a309579271d022042d234eb87937e Mon Sep 17 00:00:00 2001 From: Valerio Maggio Date: Wed, 16 Sep 2026 07:31:59 +0100 Subject: [PATCH 6/6] Refactor client_api.py for improved token limit and structure Increased max_tokens from 1024 to 2048 and refactored tool_result function to simplify the return structure. Updated main function to handle tool results more efficiently. --- mcp-vs-api-calls/client_api.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/mcp-vs-api-calls/client_api.py b/mcp-vs-api-calls/client_api.py index 069b84a472..7346bb77db 100644 --- a/mcp-vs-api-calls/client_api.py +++ b/mcp-vs-api-calls/client_api.py @@ -40,7 +40,7 @@ def ask_model( ) -> Message: return client.messages.create( model=model, - max_tokens=1024, + max_tokens=2048, tools=tools, messages=messages, ) @@ -53,14 +53,9 @@ def run_tool(block: ToolUseBlock) -> str: def tool_result(block: ToolUseBlock, output: str) -> dict: return { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": block.id, - "content": output, - }, - ], + "type": "tool_result", + "tool_use_id": block.id, + "content": output, } @@ -68,10 +63,19 @@ def main() -> None: client = anthropic.Anthropic() messages = [{"role": "user", "content": QUESTION}] response = ask_model(client, messages, TOOLS) - for block in response.content: - if block.type == "tool_use": - messages.append( - {"role": "assistant", "content": response.content}, + + tool_uses = [b for b in response.content if b.type == "tool_use"] + if tool_uses: + messages.append({"role": "assistant", "content": response.content}) + results = [tool_result(b, run_tool(b)) for b in tool_uses] + messages.append({"role": "user", "content": results}) + + final = ask_model(client, messages, TOOLS) + print(next(b.text for b in final.content if b.type == "text")) + + +if __name__ == "__main__": + main() ) messages.append(tool_result(block, run_tool(block))) final = ask_model(client, messages, TOOLS)