Architecture¶
How QSO-Graph servers work together — two foundation packages, nine MCP servers, 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"]
hamqth["hamqth-mcp"]
pota["pota-mcp"]
sota["sota-mcp"]
solar["solar-mcp"]
wspr["wspr-mcp"]
subgraph Auth["qso-graph-auth"]
persona["PersonaManager → OS Keyring"]
end
subgraph ADIF["adif-mcp"]
spec["ADIF 3.1.6 Spec (186 fields, 26 enums)"]
valid["Validation Engine"]
geo["Geospatial (distance, heading)"]
end
eqsl & qrz & lotw & hamqth --> Auth
end
Servers -->|"HTTPS only"| Services
subgraph Services["External Services"]
svc["eQSL.cc · QRZ.com · LoTW · HamQTH<br/>POTA · SOTA · NOAA SWPC · WSPR Network"]
end
Foundation: qso-graph-auth + adif-mcp¶
Two foundation packages provide the shared capabilities that other servers depend on.
qso-graph-auth — Credential Management¶
Named identities (personas) 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 qso-graph-auth which reads them from the keyring at runtime. Credentials never exist in config files, environment variables, or MCP protocol messages.
pip install qso-graph-auth
qso-auth persona add --name ki7mt --callsign KI7MT --start 2020-01-01
qso-auth creds set ki7mt eqsl
adif-mcp — ADIF 3.1.6 Spec Engine¶
The complete ADIF 3.1.6 spec bundled as JSON:
- 186 fields with data types, valid ranges, and descriptions
- 26 enumerations with 4,427+ records (Mode, Band, DXCC, Country, Contest_ID, etc.)
- 28 data types (Number, String, Date, GridSquare, etc.)
Plus validation, parsing, and geospatial tools. See adif-mcp for the full 8-tool reference.
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 26 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 qso-graph-auth (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 |
| 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 ──── qso-auth creds set ──── OS Keyring (encrypted)
│
MCP Server ──── qso-graph-auth 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 qso-graph-auth for credential management. Public servers (POTA, SOTA, IOTA, Solar, WSPR) have no dependency on qso-graph-auth.
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 or eQSL.
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.