Skip to content

Architecture

How qso-graph servers work together — one foundation, ten packages, zero cloud dependencies.


System Overview

graph TD
    AI["<b>AI Assistant</b><br/>Claude · ChatGPT · Cursor · Gemini"]
    AI -->|"MCP Protocol (stdio)"| Servers

    subgraph Servers["MCP Servers (local)"]
        direction TB
        eqsl["eqsl-mcp"]
        qrz["qrz-mcp"]
        lotw["lotw-mcp"]
        clublog["clublog-mcp"]
        hamqth["hamqth-mcp"]
        pota["pota-mcp"]
        sota["sota-mcp"]
        solar["solar-mcp"]
        wspr["wspr-mcp"]

        subgraph Foundation["adif-mcp (foundation)"]
            persona["PersonaManager → OS Keyring"]
            spec["ADIF 3.1.6 Spec (186 fields, 25 enums)"]
            valid["Validation Engine"]
            geo["Geospatial (distance, heading)"]
        end

        eqsl & qrz & lotw & clublog & hamqth --> Foundation
    end

    Servers -->|"HTTPS only"| Services

    subgraph Services["External Services"]
        svc["eQSL.cc · QRZ.com · LoTW · Club Log · HamQTH<br/>POTA · SOTA · NOAA SWPC · WSPR Network"]
    end

Foundation: adif-mcp

adif-mcp provides three capabilities that all other servers depend on:

1. Persona Management

Named identities with credentials stored in the OS keyring. One persona serves all services:

Persona: "ki7mt"
  ├── eqsl    → password in OS keyring
  ├── lotw    → password in OS keyring
  ├── qrz     → password in OS keyring
  └── hamqth  → password in OS keyring

When a server needs credentials, it calls adif-mcp which reads them from the keyring at runtime. Credentials never exist in config files, environment variables, or MCP protocol messages.

2. ADIF 3.1.6 Specification

The complete ADIF 3.1.6 spec bundled as JSON:

  • 186 fields with data types, valid ranges, and descriptions
  • 25 enumerations with 4,427 records (Mode, Band, DXCC, Contest_ID, etc.)
  • 28 data types (Number, String, Date, GridSquare, etc.)

All servers share this spec for consistent parsing and validation.

3. Validation Engine

Record validation against the full spec:

  • Field name recognition (186 fields)
  • Data type checking (Number, Date, etc.)
  • Enum membership checking (43 enum-typed fields across 25 enumerations)
  • Compound format parsing (CreditList, multi-medium)
  • Conditional validation (Submode depends on Mode)
  • Import-only detection (warn, don't reject historical data)

Server Architecture

Each MCP server follows the same pattern:

MCP Client Request
Input Validation ──── Regex on all user strings
Rate Limiter ──────── Per-service throttle (prevents account bans)
Credential Lookup ─── OS keyring via adif-mcp (authenticated servers only)
API Call ──────────── HTTPS only, response parsed
Response Cache ────── In-memory TTL cache
Safe Return ───────── No credentials in results, errors, or logs

Common Properties

Property Value
Transport stdio (default) or --transport streamable-http for MCP Inspector
Framework FastMCP 3.x
Python 3.10+
License GPL-3.0-or-later
Mock mode <NAME>_MCP_MOCK=1 for testing without credentials

Rate Limiting

Each server implements rate limiting appropriate for its service:

Server Min Delay Max Rate Notes
eqsl-mcp 500ms Respectful crawl
qrz-mcp 500ms 35/min IP ban risk above 35/min
clublog-mcp 500ms 30/min API key rate limit
lotw-mcp 500ms Respectful crawl
hamqth-mcp 500ms XML session rate limit
pota-mcp 100ms Public API
sota-mcp 200ms Public API
solar-mcp 200ms NOAA public data
wspr-mcp 200ms Public API

Credential Flow

Credentials take one path and never deviate:

User ──── adif-mcp creds set ──── OS Keyring (encrypted)
MCP Server ──── adif-mcp read ─────────┘
    │                                (in-process, never serialized)
HTTPS Request ──── credential in Authorization header
Response ──── parsed, credential stripped
MCP Tool Result ──── data only, no credentials

What gets persisted: Nothing. Credentials exist in memory only during the API call. The OS keyring handles encryption at rest.

What the AI sees: Tool parameters (persona, callsign, band) and tool results (lookup data, QSO records). Never passwords, API keys, or session tokens.


Package Independence

Each server is a standalone pip install:

pip install eqsl-mcp    # just eqsl-mcp + its dependencies
pip install pota-mcp     # just pota-mcp, no auth needed

Servers don't depend on each other. You can install one or all ten.

Authenticated servers depend on adif-mcp for credential management. Public servers (POTA, SOTA, Solar, WSPR) have no dependency on adif-mcp.


MCP Client Configuration

All servers work with any MCP client. Example for Claude Desktop:

{
  "mcpServers": {
    "adif": {
      "command": "adif-mcp"
    },
    "eqsl": {
      "command": "eqsl-mcp"
    },
    "pota": {
      "command": "pota-mcp"
    }
  }
}

For Claude Code, add to ~/.claude/settings.json:

{
  "mcpServers": {
    "adif": { "command": "adif-mcp", "args": [] },
    "eqsl": { "command": "eqsl-mcp", "args": [] }
  }
}

See Getting Started for configuration for all 6 supported MCP clients.


Design Principles

Good Neighbor Policy

qso-graph servers wrap external APIs — they don't replicate them. Rate limiting is built in to prevent account bans. If a service goes down, the server fails gracefully.

Read-Only Security Model

No qso-graph server writes to external services. All operations are read-only: lookups, downloads, queries. No log uploads, no QSO submissions, no account modifications.

Validate Before Upload

adif-mcp's validation engine catches data defects at the source. A busted QSO is not a confirmation — and a rare DXpedition contact may be irreplaceable. Validate before uploading to LoTW, eQSL, or Club Log.

Pip Install and Go

Every server is one pip install away. No Docker, no containers, no config files (except MCP client config). Credentials go in the OS keyring, not YAML files.