Home / AI & GenAI

AI & GenAIHow-toIntermediate

Build a read-only MCP server so AI assistants can query your ops tools safely

Expose a few well-described, read-only operations tools (service health, recent errors, runbook search) to AI assistants through a Model Context Protocol server in Python, with guardrails.

Deepak Bhardwaj2 min read

Applies toModel Context Protocol (MCP) Python SDK; MCP-capable clients such as Claude Code, Claude Desktop, VS Code and other agent hosts

The Model Context Protocol (MCP) is an open standard for connecting AI assistants to tools and data. One MCP server for your ops tooling can serve every MCP-capable assistant your engineers use.

1. Start small and read-only

Good first tools for an ops team:

Tool Returns
service_health(service) Status, last deploy, open incidents
recent_errors(service, minutes) Top error messages with counts
search_runbooks(query) Titles and links of matching runbooks

No restarts, no deletes, no config changes in version one.

2. The server

pip install "mcp[cli]" httpx
# ops_mcp.py
from mcp.server.fastmcp import FastMCP
import httpx, os

mcp = FastMCP("ops-tools")
API = os.environ["OPS_API_URL"]
TOKEN = os.environ["OPS_API_TOKEN"]          # read-only token

@mcp.tool()
async def service_health(service: str) -> dict:
    """Current health of ONE service: status, last deployment time and open incidents.
    Use when the user asks whether a named service is up or degraded."""
    async with httpx.AsyncClient(timeout=10) as c:
        r = await c.get(f"{API}/services/{service}/health", headers={"Authorization": f"Bearer {TOKEN}"})
        r.raise_for_status()
        return r.json()

@mcp.tool()
async def recent_errors(service: str, minutes: int = 30) -> list[dict]:
    """Top 20 distinct error messages for a service in the last N minutes (max 240), with counts."""
    minutes = max(1, min(minutes, 240))
    async with httpx.AsyncClient(timeout=20) as c:
        r = await c.get(f"{API}/services/{service}/errors", params={"minutes": minutes, "limit": 20},
                        headers={"Authorization": f"Bearer {TOKEN}"})
        r.raise_for_status()
        return r.json()

if __name__ == "__main__":
    mcp.run()          # stdio transport by default

The docstrings become the tool descriptions the model reads. Write them like instructions: what it does, when to use it, limits.

3. Test it

mcp dev ops_mcp.py        # opens the MCP Inspector to call tools by hand

Then register it in your MCP client following that client's documentation (each one has a config file or command for adding a stdio server, passing the environment variables).

4. Guardrails

  • Read-only credentials for the backing APIs. The server can't do more than its token allows.
  • Clamp parameters (time ranges, limits) in code, as above.
  • Strip secrets and personal data from results before returning them.
  • Log every tool call with the user, arguments and result size.
  • For a shared remote server (HTTP transport), require authentication and authorise per user.

5. Adding write actions later

If you add actions such as "restart pod", make them narrow (one service, one action), require the client's human approval step, rate-limit them, and keep them in a separate server or toolset that only on-call engineers enable.

Tip

Treat tool output as untrusted input to the model: an error message or log line can contain text like "ignore previous instructions". See the prompt injection checklist before connecting tools that read customer-provided content.