Skip to content
Go back

MCP Servers: Tools for LLMs

By SumGuy 12 min read
MCP Servers: Tools for LLMs

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:

  1. Host — Your LLM client. Claude Desktop, ChatGPT, Cursor, Continue. Whatever is calling the shots.
  2. Client — The MCP client library. The host’s way of talking to servers. Built-in to Claude Desktop, your IDE, or your Python app.
  3. Server — Your tool. Exposes resources, tools, and prompts via MCP. Could be a Python script, a Rust binary, a Node.js service.
  4. 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:

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:

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:

claude_desktop_config.json
{
"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:

  1. A tool called disk_usage that runs df -h and returns disk stats
  2. A resource that reads your /etc/fstab (or a mock config) and returns it

Here’s the full server:

server.py
import subprocess
import json
from mcp.server import Server
from mcp.types import Tool, TextContent, Resource
from 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:

server.py
import subprocess
import sys
import json
from mcp.server.models import InitializationOptions
from mcp.server import Server
from 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:

Terminal window
pip install mcp

Then drop that script at ~/.local/bin/disk-tools.py or wherever, and add it to your Claude Desktop config:

claude_desktop_config.json
{
"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.

Terminal window
# 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/data

Boom. 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:

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:

  1. Only register servers you trust
  2. Only expose tools that are safe to be called (not run_arbitrary_shell_command)
  3. Never register a server that reads .aws/credentials as 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):

HTTP/SSE (advanced):

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:

  1. Exposes a tool to list running VMs (calls qm list or hits the Proxmox API)
  2. Exposes a tool to check disk usage on the Proxmox host
  3. Exposes a resource that returns your Ansible inventory
  4. 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


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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Caddy vs Traefik

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts