DEV Community

Edem Khadiev
Edem Khadiev

Posted on

How to Build a MCP Server in Python for Linux Network Diagnostics

LLMs can explain Linux networking concepts well enough, but out of the box they have no visibility into a live machine. They cannot see your network interfaces, tell you if a WireGuard tunnel has an MTU mismatch or check if an adapter is dropping packets.

Anthropic's Model Context Protocol (MCP) is a practical way to bridge that. It gives you a standard interface for exposing local or remote tools to clients like Claude Desktop, Cursor, or your own agents.

In this post, we will build an async Python MCP server—alongside a FastAPI REST endpoint—that reads live Linux network interface metrics (eth0, wlan0, tun0, wg0), structures the output with Pydantic v2, and flags a couple of common network misconfigurations.


Prerequisites

  • Python 3.12+ and uv installed
  • A Linux environment (Ubuntu, Debian, or WSL2)

Step 1: Bootstrap the Project with uv

Create a new project directory and install the mcp SDK along with psutil, pydantic, and fastapi:

mkdir netpulse-mcp && cd netpulse-mcp
uv init --python 3.12
uv add "mcp[cli]" fastapi "uvicorn[standard]" pydantic psutil

Enter fullscreen mode Exit fullscreen mode

Step 2: Define Typed Telemetry Schemas with Pydantic v2

If you pipe raw shell output like ifconfig directly into an LLM, you waste context tokens and risk the model misreading the text. Returning a strict Pydantic model is much cleaner because both REST clients and MCP tool calls get a predictable JSON structure.

Create main.py and set up the models:

from typing import Dict, List, Optional
import psutil
from fastapi import FastAPI
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("NetPulse Linux Network Diagnostics")
app = FastAPI(title="NetPulse Diagnostics API", version="0.1.0")

class InterfaceStats(BaseModel):
    name: str
    is_up: bool
    mtu: int
    bytes_sent: int
    bytes_recv: int
    packets_dropped: int = Field(description="Total inbound and outbound dropped packets")
    anomaly_detected: bool = False
    diagnostic_note: Optional[str] = None

class SystemNetworkReport(BaseModel):
    hostname_interfaces: List[InterfaceStats]
    healthy: bool

Enter fullscreen mode Exit fullscreen mode

Step 3: Implement the Linux Interface Inspector

Now we can add the diagnostic logic to main.py. We will use psutil.net_if_stats() and psutil.net_io_counters() to read the kernel interface counters and run two basic checks:

  1. Flag any interface with more than 100 total packet drops (dropin + dropout).
  2. Flag virtual tunnel interfaces (tun*, wg*) with an MTU above 1420, since that often causes packet fragmentation over encapsulated links.
async def collect_interface_diagnostics() -> SystemNetworkReport:
    stats = psutil.net_if_stats()
    io_counters = psutil.net_io_counters(pernic=True)
    results: List[InterfaceStats] = []
    system_healthy = True

    for iface, iface_stat in stats.items():
        io = io_counters.get(iface)
        if not io:
            continue

        dropped = io.dropin + io.dropout
        anomaly = False
        note = "OK"

        if dropped > 100:
            anomaly = True
            system_healthy = False
            note = f"High packet drop count ({dropped}). Check ring buffers (ethtool -g) or qdisc."
        elif iface.startswith(("tun", "wg")) and iface_stat.mtu > 1420:
            anomaly = True
            note = f"Suboptimal tunnel MTU ({iface_stat.mtu}). Consider clamping to 1420 or 1280 for IPv6."

        results.append(
            InterfaceStats(
                name=iface,
                is_up=iface_stat.isup,
                mtu=iface_stat.mtu,
                bytes_sent=io.bytes_sent,
                bytes_recv=io.bytes_recv,
                packets_dropped=dropped,
                anomaly_detected=anomaly,
                diagnostic_note=note,
            )
        )

    return SystemNetworkReport(hostname_interfaces=results, healthy=system_healthy)

Enter fullscreen mode Exit fullscreen mode

Step 4: Expose the Function to MCP and FastAPI

FastMCP uses your Python type hints and docstring to build the JSON-RPC tool definition automatically when you add the @mcp.tool() decorator. We can also wire the exact same function up to a FastAPI route:

@mcp.tool()
async def diagnose_linux_network() -> Dict:
    """Inspects all Linux network interfaces (including TUN/TAP and WireGuard), MTU settings, and packet drops."""
    report = await collect_interface_diagnostics()
    return report.model_dump()

@app.get("/api/v1/diagnostics", response_model=SystemNetworkReport)
async def get_diagnostics():
    return await collect_interface_diagnostics()

if __name__ == "__main__":
    mcp.run()

Enter fullscreen mode Exit fullscreen mode

Step 5: Test the Server Locally

To test the tool interactively, run the MCP Inspector:

uv run mcp dev main.py

Enter fullscreen mode Exit fullscreen mode

Or start uvicorn and hit the FastAPI endpoint with curl:

uv run uvicorn main:app --port 8000 &
curl -s http://localhost:8000/api/v1/diagnostics | python3 -m json.tool

Enter fullscreen mode Exit fullscreen mode

Exposing read-only telemetry like this gives an AI assistant enough context to spot network issues on a host without giving it raw shell access.

Top comments (0)