The Tool Integration Mess You’ve Been Living In
Here’s the thing: every LLM client—Claude, ChatGPT, Cursor, Continue—reinvented the wheel when it came to giving AI models access to tools. Your local Ollama setup couldn’t talk to the same tool protocol as Claude Desktop. A tool built for one client wouldn’t work with another. You had JSON schema nightmares, custom transport layers, and a fragmented ecosystem.
Then Anthropic said “no, we’re standardizing this” and gave us the Model Context Protocol (MCP).
MCP is a wire protocol. It’s boring on purpose. It sits between your LLM client and your tools—letting Claude, ChatGPT, Cursor, Cline, and a dozen other AI agents all speak the same language to the same servers. You write an MCP server once. It works everywhere.
By mid-2026, MCP servers have become the standard way to extend LLM applications. They’re in home labs, in enterprises, in open source projects that let Claude or GPT automate your infrastructure. The spec is open. The tooling is solid. If you’re an AI tinkerer—someone running local models, scripting your home lab, building agents—MCP is where you live now.
Let’s build one.
What MCP Actually Does (The Architecture)
MCP is transport-agnostic, but let’s demystify it first. Here’s the chain:
- Host — Your LLM client. Claude Desktop, ChatGPT, Cursor, Continue. Whatever is calling the shots.
- Client — The MCP client library. The host’s way of talking to servers. Built-in to Claude Desktop, your IDE, or your Python app.
- Server — Your tool. Exposes resources, tools, and prompts via MCP. Could be a Python script, a Rust binary, a Node.js service.
- Transport — How they talk. Two main flavors:
- Stdio — Processes talking over stdin/stdout. Lightweight. Local only. No network overhead. Easiest to debug.
- HTTP/SSE — Server listens on a port. Client connects via HTTP. Can be remote. More moving parts.
An MCP server announces three things:
- Resources — Static files, config snippets, or structured data the model can read. “Here’s the contents of
/etc/nginx/nginx.conf.” The model reads it; can’t change it. - Tools — Functions the model can call. “Run
df -hand return disk usage.” The model decides when to call it. It sees the schema, the model decides. - Prompts — Pre-written instructions or templates. “Here’s my standard ‘analyze system health’ routine.” Anthropic and other hosts can surface these in UI.
The model gets the schema, decides what to call, and the server handles the business logic. Simple.
Why MCP Wins
Before MCP, you had chaos:
- ChatGPT wanted OpenAI function schema
- Claude had its own tool format (Anthropic moved to MCP in late 2025)
- Ollama had no standard at all
- Continue (in VS Code) had a different shape again
- Cline (in Cursor) had yet another format
You’d write a tool. Six months later, you’d rewrite it for a new client. Your “run a shell command” tool worked in Cursor but not in Claude Desktop. Your Ansible inventory fetcher worked in Claude but needed adaptation for Continue.
MCP fixes this. One schema. One transport. Write once, use everywhere.
Also honestly: it’s the way LLM companies finally agreed to not lock you into their ecosystem. MCP is open. Anthropic, OpenAI, and community projects all support it. You’re not betting your automation on one company’s whim.
Claude Desktop: Where MCP Lives
Claude Desktop (Anthropic’s desktop app for Mac/Windows/Linux) is the flagship MCP host. You drop server configs into ~/Library/Application Support/Claude/claude_desktop_config.json (or the Linux/Windows equivalent), restart Claude, and boom—your servers are available in every conversation.
That config looks like this:
{ "mcpServers": { "filesearch": { "command": "python3", "args": ["/path/to/my_server.py"], "disabled": false }, "home_lab": { "command": "uv", "args": ["run", "--with", "mcp", "home_lab_server.py"], "disabled": false } }}Each server is a subprocess. Claude talks to it over stdio. When you ask Claude “what tools do you have?”, it asks each server “hey, what can you do?”, and the servers respond with their schema.
It’s elegant. It’s boring. It works.
Build Your First MCP Server (50 Lines of Python)
Let’s make something real: a server that exposes two things:
- A tool called
disk_usagethat runsdf -hand returns disk stats - A resource that reads your
/etc/fstab(or a mock config) and returns it
Here’s the full server:
import subprocessimport jsonfrom mcp.server import Serverfrom mcp.types import Tool, TextContent, Resourcefrom mcp.server.stdio import stdio_server
app = Server("disk-tools")
@app.tool()def disk_usage(): """Get disk usage for all mounted filesystems.""" result = subprocess.run( ["df", "-h"], capture_output=True, text=True, check=True ) return [TextContent(type="text", text=result.stdout)]
@app.resource("file:///etc/fstab")def get_fstab(): """Read the fstab file (or mock config).""" # In a real setup, read the actual /etc/fstab # For demo, return a mock config config = """# Filesystem mounts/dev/sda1 / ext4 defaults 0 1/dev/sda2 /home ext4 defaults 0 2/dev/sdb1 /mnt/data ext4 nofail 0 0""" return Resource( uri="file:///etc/fstab", name="fstab", mimeType="text/plain", contents=[TextContent(type="text", text=config)] )
if __name__ == "__main__": stdio_server(app)Wait, that’s not quite right for the latest mcp package. Let me give you the real, working version using the current SDK:
import subprocessimport sysimport jsonfrom mcp.server.models import InitializationOptionsfrom mcp.server import Serverfrom mcp.types import Tool, TextContent, Resource
server = Server("disk-tools")
@server.list_tools()async def list_tools(): return [ Tool( name="disk_usage", description="Get disk usage for all mounted filesystems (runs df -h)", inputSchema={ "type": "object", "properties": {}, "required": [] } ) ]
@server.call_tool()async def call_tool(name: str, arguments: dict): if name == "disk_usage": result = subprocess.run( ["df", "-h"], capture_output=True, text=True, check=True ) return [TextContent(type="text", text=result.stdout)] else: raise ValueError(f"Unknown tool: {name}")
@server.list_resources()async def list_resources(): return [ Resource( uri="file:///etc/fstab", name="fstab", description="System filesystem mounts", mimeType="text/plain" ) ]
@server.read_resource()async def read_resource(uri: str): if uri == "file:///etc/fstab": config = """# Example fstab/dev/sda1 / ext4 defaults 0 1/dev/sda2 /home ext4 defaults 0 2/dev/sdb1 /mnt/data ext4 nofail 0 0""" return config else: raise ValueError(f"Unknown resource: {uri}")
async def main(): from mcp.server.stdio import stdio_server async with stdio_server(server) as (read_stream, write_stream): await server.run(read_stream, write_stream, InitializationOptions())
if __name__ == "__main__": import asyncio asyncio.run(main())Install the SDK:
pip install mcpThen drop that script at ~/.local/bin/disk-tools.py or wherever, and add it to your Claude Desktop config:
{ "mcpServers": { "disk-tools": { "command": "python3", "args": ["/home/you/.local/bin/disk-tools.py"], "disabled": false } }}Restart Claude Desktop. In a new conversation, ask “what tools do you have?” Claude will see disk_usage and the fstab resource. Ask “what’s my disk usage?” and Claude will call the tool.
# What Claude sees when it calls the tool:Filesystem Size Used Avail Use% Mounted on/dev/sda1 100G 32G 68G 32% //dev/sda2 500G 200G 300G 40% /home/dev/sdb1 2.0T 1.5T 500G 75% /mnt/dataBoom. You just extended Claude with a system command. And now any MCP host—Continue, Cline, another agent—can use that same server.
Where This Fits in 2026
MCP servers are everywhere now. Here’s the ecosystem:
- Claude Desktop — The original flagship. You run servers locally.
- Continue (VS Code, JetBrains) — Full MCP support. Chain tools in your IDE.
- Cursor — MCP in beta; shipping widely by summer 2026.
- Cline (Cursor agent) — Native MCP integration.
- Anthropic’s Claude API — MCP support for programmatic access (using the tool-use API).
- OpenAI, xAI, others — Implementing MCP support. It’s becoming the standard wire format.
- Open source — Tons of pre-built servers. GitHub Search, web fetch, git operations, shell commands, Docker, Ansible, you name it.
If you’re building agents, you’re probably using MCP. If you’re integrating LLMs into your home lab or infrastructure, MCP is the sane way to do it.
And here’s what’s wild: security-conscious orgs love MCP because it’s explicit. You see exactly what servers are registered. You see exactly what tools they expose. No mysterious API keys buried in prompt injection attacks. No hidden capability escalation.
The Security Model: It Can Do Anything Your Process Can
Here’s the caveat that needs to be loud: an MCP server runs as your user and can do anything your user can do.
If you register a server that runs as kingpin, and that server exposes a tool called run_command, then Claude can ask for run_command("rm -rf /home/kingpin") and the server will run it. There’s no jail. There’s no sandbox. There’s no “are you sure?” prompt in MCP.
This is intentional. MCP is a protocol. Security is a host responsibility. Claude Desktop doesn’t let you ask it to delete your files—Claude’s frontend has rules. But the MCP protocol itself has no guardrails. The responsibility is on you to:
- Only register servers you trust
- Only expose tools that are safe to be called (not
run_arbitrary_shell_command) - Never register a server that reads
.aws/credentialsas a resource and exposes it
You wouldn’t run random scripts from the internet with bash <(curl ...). Same deal with MCP servers.
Stdio vs HTTP: When to Use Each
Stdio (default):
- Server is a subprocess
- Communicates over stdin/stdout
- Works great for Claude Desktop
- Easy debugging (just run the script, see output)
- Local only
- Low latency
- No port conflicts
HTTP/SSE (advanced):
- Server listens on a port (e.g.,
localhost:3000) - Client connects via HTTP
- Can be remote or local
- Useful if your server needs to run long-term separately
- Harder to debug (need to check logs, use curl to test)
- More complex protocol handling
- Can serve multiple clients
For a home lab or personal automation? Use stdio. It’s simpler, more reliable, and doesn’t require managing a daemon. For a shared service (multiple users, cloud deployment)? HTTP makes sense.
Gotchas and “Don’t Do This”
1. Don’t expose run_command as a catch-all
This:
# DANGEROUS@server.call_tool()async def call_tool(name: str, arguments: dict): if name == "run_command": cmd = arguments.get("command") result = subprocess.run(cmd, shell=True, capture_output=True) return [TextContent(type="text", text=result.stdout)]Is a one-way ticket to giving Claude root access to your machine. Bad idea.
Better:
@server.call_tool()async def call_tool(name: str, arguments: dict): if name == "check_service_status": service = arguments.get("service") if service not in ALLOWED_SERVICES: raise ValueError(f"Service not allowed: {service}") result = subprocess.run(["systemctl", "status", service], ...)Whitelist. Always.
2. Stdio servers don’t handle timeouts well
If your tool takes 30 seconds to run, Claude will hang. You need to handle that in your tool logic (add timeouts, run async, whatever). MCP itself has no timeout mechanism for stdio.
3. Resources are read-only
Don’t design your MCP server expecting the model to write back to resources. Resources are immutable from MCP’s perspective. If you need two-way sync, expose it as a tool.
4. Discovery and registration is manual
There’s no MCP server registry. No pip install mcp-slack and “boom, it’s available.” You have to know where your servers are, find them on GitHub, or build them yourself. This is actually good for security (no surprise dependencies), but it means the ecosystem is a bit fragmented.
5. Test your schema
The tool schema is JSON schema. If it’s wrong, Claude won’t call your tool. Test it. Ask Claude “what arguments does this tool take?” If it’s wrong, Claude will tell you, and you’ll feel silly.
Real-World Example: Home Lab Integration
Let’s say you’ve got a Proxmox cluster, a few VMs, and you’re tired of SSHing in to check things. You could build an MCP server that:
- Exposes a tool to list running VMs (calls
qm listor hits the Proxmox API) - Exposes a tool to check disk usage on the Proxmox host
- Exposes a resource that returns your Ansible inventory
- Exposes a tool that runs a read-only Ansible ad-hoc command
# Pseudocode@server.call_tool()async def call_tool(name: str, arguments: dict): if name == "list_vms": vms = proxmox_api.nodes.localhost.qemu.get() return [TextContent(type="text", text=json.dumps(vms))]
elif name == "run_ansible": playbook = arguments.get("playbook") if not playbook.startswith("check_"): # Only allow read-only playbooks raise ValueError("Only read-only playbooks allowed") result = subprocess.run(["ansible-playbook", f"playbooks/{playbook}.yml"], ...) return [TextContent(type="text", text=result.stdout)]Now Claude can ask “what VMs are down?” and it’ll call list_vms, parse the output, and tell you. Or “check the disk usage on all hosts” and it’ll run an Ansible playbook.
You’ve just turned your LLM into an infrastructure agent. And it works in Claude Desktop, Continue, Cursor, all of them, because they all speak MCP.
Where to Find Pre-Built Servers
- Anthropic’s SDK examples — https://github.com/anthropics/mcp-servers — GitHub search, file I/O, SQLite, lots of examples
- awesome-mcp — Community list on GitHub (search “awesome mcp”)
- GitHub trending — Sort by language, look for
mcp-server-*repos - Your own code — The best server is the one you build for your own use case
The Next Step
If you’ve got a home lab, an Ansible playbook you run weekly, a config file you reference a lot, or a tool you’d love to have in Claude—MCP is the move.
Build a server. Test it with Claude Desktop. Add it to your config. Ask Claude to do something. Watch it work.
Honestly, the hardest part is picking what to build first. The protocol itself? It’s solid. It’s straightforward. And it’s the direction the entire LLM ecosystem is moving.
Welcome to 2026. Your AI agents are wired into your infrastructure now.