An MCP server for logging amateur-radio QSOs to N3FJP logging software over its TCP API.
n3fjp-mcp demonstrates solid fundamentals: all 9 tools have descriptions (avg 180 chars, within baseline 194), input schemas with types, and thoughtful permission modeling via tool annotations (readOnlyHint on 3 tools). However, several tools combine multiple operations under a single 'operation' enum parameter (log, fields, database, bandmode, notifications), violating the single-responsibility principle. Parameter descriptions are present but often generic ('Field name' without format/constraint details). Output schemas are not explicitly documented, responses are inferred from code. Error handling exists but lacks recovery guidance (e.g., 'N3FJP returned CMD_NOT_FOUND' does not suggest next steps). The escape-hatch tool (n3fjp_call) is a code smell indicating incomplete abstraction. Naming is verb-forward (status, query, log) but operation-based tools obscure intent.
Band, mode, and frequency control (when rig interface is OFF). operations: - set_band (band): set the band entry box. - set_mode (mode): set the mode entry box. - change_freq (frequency): set the frequency entry box (Hz as int or float).
Direct database operations: add, edit, delete, or query individual QSO records. operations: - add_direct (fields={...}): insert a new QSO record. `fields` is a dict of friendly field names → values (same as the `log` tool's exchange dict). - delete (qso_id): delete one QSO by its record ID. - search (query): search the log by call, date, band, mode, or other criteria. Returns matching QSO records. - list (limit, offset): list QSO records (paginated). - entity (qso_id): fetch one QSO record by ID. - sql (statement): execute raw SQL (SELECT only, unless N3FJP_ALLOW_DB_WIPE is on). Whole-database DELETE/UPDATE/DROP/TRUNCATE are blocked by default.
Host + network diagnostics for troubleshooting connectivity. Does NOT connect to N3FJP. Reports the resolved N3FJP_HOST/PORT, this process's Python and hostname, and the host's network interfaces — so you can tell whether the process can even see the target's network (e.g. when running host-side vs. sandboxed).
Read entry-box (text field) values. operations: - read (field=name): read one box. `field` may be a friendly name (call, rst_rcvd, class, section, name, …) or a raw control id (TXTENTRY…). - visible: every field the current contest shows, with values. - all: every field (regardless of visibility), with values. Derived fields (country_worked, cq_zone, itu_zone, continent, prefix) are readable here but are computed by N3FJP from the call — never write them.
Multi-operation tools (log, fields, database, bandmode, notifications) combine multiple responsibilities under a single 'operation' enum. This violates single-responsibility principle and forces LLMs to reason about which operation to invoke rather than selecting a single, focused tool.
Output schemas are not explicitly documented in tool definitions. Responses are inferred from code (e.g., status returns a dict with 'connected', 'program', 'version' fields). LLMs cannot plan downstream calls without knowing what fields to expect.
Error handling lacks recovery guidance. Example: 'N3FJP returned CMD_NOT_FOUND' tells the LLM the command failed but not what to do next (e.g., 'Try query operation "program" to check API version, or use n3fjp_call for newer commands').
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | D | 57 | 2026-07-28+ | v2 |
Logbook entry: set fields, run Call-tab, and ENTER a QSO. The headline tool. operations: - log_qso: the automatic-logging flow. Provide `call` (and usually `exchange`, a dict of friendly field names → values, e.g. {"class": "2A", "section": "CT"}). Sets the call, runs CALLTAB (dupe check + lookups) unless calltab=false, sets the exchange, then ENTER. Returns the number of records added and any dupe response. - set (field, value): write one entry box. - set_many (fields={...}): write several entry boxes. - calltab: run CALLTAB (after setting the call). - enter: log the current form. Returns records_added plus a `logged` boolean derived from the QSO-count delta / ENTEREVENT — trust `logged`, not records_added, which can report 0 on success in networked mode. Do not retry when logged=true (creates duplicates). - clear: clear the entry form. - focus (field): move focus to a box (e.g. to auto-fill default RST). Tip: `contest` (e.g. "field_day", "cq_wpx") lets the tool warn if the exchange is missing a field that contest needs.
Escape hatch: send a raw N3FJP command and get the response. For commands not yet wrapped by the tool suite, or to reach newer N3FJP builds that added commands after this server was released. Provide the raw command ID (e.g. "PROGRAM", "READBMF", "ENTER") and optional fields dict. Returns the parsed response block(s).
Drain async notifications from N3FJP (CALLTABEVENT, ENTEREVENT, etc.). operations: - drain: return all buffered notifications and clear the buffer. - enable: subscribe to notifications (default on). - disable: unsubscribe from notifications.
Read-only program queries. operations: program (name/version/API version), qso_count, next_serial, log_path, settings_path, shared_path, qso_rate (contest stats), band_mode_freq (current band/mode/frequency).
Snapshot: program, version, API version, QSO count, band/mode/frequency, log path. A quick health check that also confirms the connection and which N3FJP program/contest is loaded.
Parameter descriptions are generic and lack format/constraint details. Example: 'Field name' (for fields.field param) does not specify whether it accepts friendly names (call, rst_rcvd) or raw control IDs (TXTENTRY…), forcing LLMs to guess or re-read the tool description.
Escape-hatch tool (n3fjp_call) exists, signaling incomplete abstraction. This tool accepts raw N3FJP command IDs and optional fields dict, allowing LLMs to bypass the structured tool interface. This increases hallucination risk and makes auditing harder.