A pure foundational model, no matter how advanced, lives in a bubble. It doesn't know what time it is, it can't query your database inventory, and it can't deploy code. For an agent to interact with the world, it needs tools.
The core idea: Robotic arms for digital brains
In the corporate world, they'll talk to you about "microservices orchestration through expanded cognitive capabilities." In code, this translates to giving the LLM access to specific functions of your system.
Imagine the LLM is a brilliant brain stuck in a jar. MCP (Model Context Protocol) is the standard API that plugs robotic arms into that jar. Previously, each model provider had their own proprietary format for calling external functions. Now, MCP standardizes how a model discovers and executes tools, regardless of whether you're using OpenAI, Anthropic, or a local model.
The problem with custom-made tools
I've dealt with dozens of implementations where programmers create gigantic wrappers for every single API endpoint just so the LLM can understand it. That scales terribly. If you change the API contract, the agent breaks and starts hallucinating parameters.
Adopting MCP forces you to think of your tools as independent servers. The agent acts as an MCP client, asks what tools are available on the server, and then requests to execute one of them by passing a standardized JSON.
Practical Example: A basic MCP server
Let's see how to spin up a minimal MCP server in Python using the official SDK. This server exposes a tool to read the contents of a local file.
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.types as types
import mcp.server.stdio
import os
# We initialize the server with an identifying name
server = Server("mi-servidor-local")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
Tells the agent what tools we have available.
"""
return [
types.Tool(
name="leer_archivo",
description="Lee el contenido de un archivo de texto en disco.",
inputSchema={
"type": "object",
"properties": {
"ruta": {"type": "string", "description": "Ruta absoluta al archivo"}
},
"required": ["ruta"]
}
)
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Executes the tool requested by the agent.
"""
if name == "leer_archivo":
ruta = arguments.get("ruta")
if not ruta or not os.path.exists(ruta):
return [types.TextContent(type="text", text="Error: Archivo no encontrado.")]
try:
with open(ruta, 'r', encoding='utf-8') as f:
contenido = f.read()
return [types.TextContent(type="text", text=contenido)]
except Exception as e:
return [types.TextContent(type="text", text=f"Error leyendo el archivo: {str(e)}")]
raise ValueError(f"Herramienta desconocida: {name}")
async def main():
# The server communicates with the agent via standard input/output (stdio)
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="mi-servidor-local",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
)
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
This approach isolates permissions. You can run this script in a restricted environment where it only has read access to a specific folder, without worrying about the agent itself trying to do something weird with the main machine's operating system. This is how you secure real deployments.