Hero image for Build Log: Writing a Custom System Info MCP Server for Hermes

Build Log: Writing a Custom System Info MCP Server for Hermes

Build a stdio MCP server from scratch in Python, register it with Hermes Agent, and call it from a live session — real code, real output.

TLDR: If you need a tool Hermes doesn’t ship with, the fastest path isn’t writing a native Hermes plugin — it’s building a small MCP server. In ~120 lines of Python you get a full stdio MCP server that Hermes discovers, connects to, and uses in its conversation loop. This build log walks the entire process: protocol implementation, registration with hermes mcp add, and a live tool call from the agent.

Why Build a Custom MCP Server?

Hermes ships with ~35 built-in tools across 20+ toolsets — web search, file I/O, code execution, terminal, image gen, and more. But no toolset covers every possible need. When you want to expose a new capability to the agent, you have three options:

ApproachLines of CodeLifecycleEffort
Native Hermes plugin200-500Need to add to tools/ + toolsets.pyModerate
MCP server (stdio)~120Self-contained process, no Hermes source changesLow
MCP server (HTTP)~150Separate service, needs a port/URLLow

The MCP route wins for one-off or domain-specific tools. No Hermes source changes, no reinstall, no PR. The server is a standalone process that communicates over stdin/stdout using a well-defined JSON-RPC protocol.

Architecture

The MCP protocol uses JSON-RPC 2.0 over stdio, as specified in the Model Context Protocol specification. Here’s the flow:

Hermes Agent                Custom MCP Server
    │                              │
    │── initialize (JSON-RPC) ────→│
    │←──── capabilities ───────────│
    │                              │
    │── tools/list ───────────────→│
    │←──── tool schemas ───────────│
    │                              │
    │── tools/call (get_system) ──→│
    │←──── tool result ────────────│

The Hermes MCP client handles discovery, lifecycle, and message framing — documented in the Hermes MCP guide. Your server just needs to respond to three JSON-RPC methods: initialize, tools/list, and tools/call.

Implementation

The server I built exposes a single tool — get_system_info — that returns CPU model, load averages, memory usage, disk usage, and uptime. Here’s the core protocol handler:

import json, os, platform, shutil, sys, time

def handle_request(request):
    method = request.get("method")

    if method == "initialize":
        return {
            "jsonrpc": "2.0",
            "id": request["id"],
            "result": {
                "protocolVersion": "2025-03-26",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "system-info", "version": "1.0.0"}
            }
        }
    elif method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": request["id"],
            "result": {
                "tools": [{
                    "name": "get_system_info",
                    "description": "Get system info: CPU, memory, disk usage, uptime, and OS",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "paths": {
                                "type": "string",
                                "description": "Comma-separated mount points"
                            }
                        }
                    }
                }]
            }
        }
    elif method == "tools/call":
        tool_name = request["params"]["name"]
        if tool_name == "get_system_info":
            result = get_system_info()  # returns JSON string
            return {
                "jsonrpc": "2.0",
                "id": request["id"],
                "result": {
                    "content": [{"type": "text", "text": result}]
                }
            }

The protocol layer is ~50 lines. The actual tool logic (parsing /proc/cpuinfo, /proc/meminfo, calling shutil.disk_usage, os.getloadavg) is another ~70 lines of straightforward Python.

Three methods is the minimum viable surface. You only need initialize (handshake), tools/list (schema discovery per the MCP tool specification), and tools/call (execution). The notifications/initialized method can be silently ignored since it’s a fire-and-forget signal from the client.

Full source: mcp-servers/system-info/server.py

Registration and Verification

Testing the server directly against the JSON-RPC protocol:

$ cd ~/hermes-tutorials/mcp-servers/system-info
$ python3 test_server.py

=== INITIALIZE ===
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "system-info", "version": "1.0.0" }
  }
}

=== TOOLS LIST ===
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [{ "name": "get_system_info", ... }]
  }
}

=== TOOL CALL ===
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\n  \"hostname\": \"cachyos-x8664\",\n  \"os\": \"Linux 6.18.28-1-cachyos-lts\",\n  \"uptime\": \"28d 10h 21m\",\n  \"cpu\": { \"model\": \"Intel(R) N100\", \"cores\": 5, ... },\n  \"memory\": { \"total\": \"11734.7 MB\", \"used\": \"6474.9 MB\", ... },\n  \"disk\": { \"/\": { \"total_gb\": 234.5, \"used_gb\": 50.3, ... } }\n}"
    }]
  }
}

With the server verified, I registered it with Hermes:

$ echo "y" | hermes mcp add system-info \
  --command python3 \
  --args /home/techgeek/hermes-tutorials/mcp-servers/system-info/server.py

 Connected! Found 1 tool(s) from 'system-info':
    get_system_info    Get system info: CPU, memory, disk usage, uptime, and OS
 Saved 'system-info' to ~/.hermes/config.yaml (1/1 tools enabled)

The hermes mcp add command connected, discovered the tool schema, and asked to enable it — matching the registration flow documented in the Hermes MCP setup guide. The echo "y" pipes the confirmation. After registration, it showed up in the MCP server list:

$ hermes mcp list

MCP Servers:

  Name             Transport                      Tools        Status
  ──────────────── ────────────────────────────── ──────────── ──────────
  system-info      python3 /home/techgeek/he...   all enabled
  ...

Live Tool Call

The moment of truth — asking Hermes to use the custom tool in a live session:

$ hermes chat -q "Call get_system_info from the system-info MCP server \
  and show me the CPU model and memory usage"

Response from the agent:

CPU: Intel(R) N100 (5 cores) — load 1.93 / 0.67 / 0.38 (1m/5m/15m)
Memory: 7,042 MB used of 11,735 MB total (60%) — 4,693 MB available

The agent discovered the MCP tool automatically, called it, and incorporated the result into its response. The tool was treated exactly like a native Hermes tool — no special flags, no extra config.

Config Persistence

What gets written to ~/.hermes/config.yaml:

mcp_servers:
  system-info:
    command: python3
    args:
      - /home/techgeek/hermes-tutorials/mcp-servers/system-info/server.py
    enabled: true

The server will load on every Hermes session until removed with hermes mcp remove system-info. The args field is an array — each element is passed as a separate argument to the command, unlike shell-quoted strings.

What I Learned

The MCP spec is refreshingly minimal. Three JSON-RPC methods is all you need for a working tool server. The inputSchema follows JSON Schema, which Hermes uses to generate tool-call arguments automatically. My initial implementation passed arguments as a single quoted string; switching to inputSchema with typed properties was a one-line change that made the tool discoverable without looking at source.

Stdio lifecycle is cleaner than expected. Hermes launches the process on demand, reads JSON lines from stdout, and sends requests via stdin — the MCP transport specification defines this contract. No port management, no health checks, no restart logic. If the process crashes, Hermes restarts it on the next tool call.

hermes mcp add validates before saving. If the server doesn’t respond to initialize, the command fails immediately — no config pollution. I hit this once when I had a syntax error in the JSON-RPC response handler.

The tool shows up in hermes mcp list immediately after registration. No session restart needed, though the current session won’t see new tools until /reload-mcp is issued.

When to Build vs. When to Extend

  • Build an MCP server when the capability is self-contained (data lookup, system info, calculation, API wrapper) — ~120 lines of Python, zero coupling to Hermes internals.
  • Write a native Hermes plugin when the tool needs deep integration (accessing Hermes session state, modifying memory, intercepting tool calls).
  • Use an existing MCP server when someone already built it — a growing ecosystem of MCP servers covers GitHub, filesystem, databases, browsers, Stripe, and dozens more.

Resources

  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
  • NiteAgent — AI agent development, frameworks, and production patterns

Cross-links automatically generated from Hermes Tutorials.

From The Network

AI Tools

ToolBrain

In-depth AI tool reviews, comparisons, and guides

AI Agents

NiteAgent

AI agent frameworks, orchestration, and production patterns

Engineering

CodeIntel

Code intelligence, testing patterns, and production engineering

No-Code

NoCode Insider

No-code workflows, automation tools, and visual development

Smart Home

Smart Home Field Guide

Smart home devices, automation, and IoT reviews

/* deployment 1779804339 */