ardupilot-research-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ardupilot-research-mcplook up RC_OPTIONS and decode the bitmask value 0x2000"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ardupilot-research-mcp
A local MCP server that answers ArduPilot, ExpressLRS, EdgeTX, CRSF and MAVLink questions from a
pinned offline corpus, and hands back a citation you can check with git show.
It runs on your machine as a stdio process. There is no URL, no port, no API key, no daemon and no
database server. Your MCP client starts python -m ardupilot_research_mcp.server and talks to it
over stdio. At answer time the server reads one file, data/index.sqlite. The network is used once,
to build that index.
It never arms, flashes, erases, writes parameters, opens a serial port or talks to a vehicle. Answers
either carry a cite_id, a pinned permalink, an exact line range and a verbatim excerpt, or say
found:false.
python -m ardupilot_research_mcp.server # stdio MCP server, 14 tools
python -m pytest -q # 141 passedRepo: github.com/TOKGS/Drone_MCP. Package ardupilot-research-mcp v0.1.0, Python 3.10 or newer.
Bản tiếng Việt: README.vi.md.
What it is, and what it is not
It is a reference tool for someone writing or debugging ArduPilot code, wiring a flight controller,
decoding a CRSF frame, reading a hwdef.dat, checking an Intel HEX file, or asking why a parameter
no longer exists at a release tag. The corpus is the firmware source, wiki, MAVLink XML, ExpressLRS
source and docs, EdgeTX and the CRSF spec, each at a pinned ref.
It has no write path. No tool changes state, and an unanswered question returns found:false instead
of a guess.
You ask | You get |
| the declaring |
|
|
| msgid 0, fields, CRC extra, and a citation into |
| 16 channels of 11 bits, CRC8 status, microseconds from the spec macro |
| checksum status, record type, data bytes |
| a refusal with the reason it is dangerous and a safe path, never the recipe for turning the failsafe off |
|
|
Related MCP server: Synorive
Guarantees
Guarantee | Enforced by |
Read-only: all 14 tools carry |
|
Offline while answering: only |
|
Excerpts are real: chunk text is a byte slice of the pinned file, checked over all 86 262 chunks |
|
Pins, never branches: URLs point at a tag or a full commit SHA |
|
No invented names: a missing parameter, board or message returns |
|
Safety first: requests to disable or mask arm checks, failsafes or fences are refused with a safe path, and hardware procedures carry |
|
Excerpts are data: every citation is |
|
No silent failure: oversized input returns a |
|
Install
Requirements:
Python 3.10 or newer with
pip(tested on 3.11.9).About 3 GB free disk for the build: corpus 115 MB,
data/index.sqlite182 MB, and a 2.7 GB download cache. The cache is gitignored and can be deleted once the build succeeds, because the answering side needs only the index.Network once, for the build.
git clone https://github.com/TOKGS/Drone_MCP.git ardupilot-research-mcp
cd ardupilot-research-mcp
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -e .# Linux or WSL
python3 -m venv .venv && . .venv/bin/activate && pip install -e .The only dependencies are mcp==1.30.0 and pyyaml. pytest comes with the dev extra. The SDK is
pinned because the tool schema surface was verified against that exact version
(docs/research-notes.md).
Build the index
python -m research.fetch tags # resolve the newest Copter-X.Y.Z tag with git ls-remote, no beta or rc
python -m research.fetch download # download the pinned archives, no search quota
python -m research.extract # filtered corpus into data/corpus
python -m ardupilot_research_mcp.ingest.cli --all --rebuild-index
python -m research.build_params # @Param, @Bitmask and @Default into the params table
python -m research.build_mavlink # MAVLink XML into message and enum tables
python -m ardupilot_research_mcp.ingest.cli --verifyExpect about five minutes and a 182 MB data/index.sqlite: 7 356 documents become 86 262 chunks,
5 089 parameters, 397 MAVLink messages and 2 367 enum entries.
--resume is the default and skips files already indexed by hash. --rebuild-index recreates the
SQLite file.
Run it
python -m ardupilot_research_mcp.server # Ctrl+C to stop, nothing listens on a portBecause it speaks stdio, it prints nothing and waits for an MCP client. To try a tool without a client:
$env:PYTHONPATH = "src"
python -c "import sys; sys.path.insert(0,'src'); from ardupilot_research_mcp.tools.lookup_param import lookup_param; import json; print(json.dumps(lookup_param('RC_OPTIONS'), ensure_ascii=False, indent=1)[:1500])"Good first questions:
lookup_param("ARMING_SKIPCHK")
decode_bitmask("RC_OPTIONS", "0x2000")
decode_crsf_frame("C81816E0031FF8C0073EF0810F7CE0031FF8C0073EF0810F7CAD")
check_intelhex_line(":10010000214601360121470136007EFE09D2190140")
troubleshoot("what does FS_THR_ENABLE do, and which value means disabled?")The 14 tools
Each tool is a pure function in src/ardupilot_research_mcp/tools/<name>.py. A shared guard limits
calls to 60 per minute, rejects oversized input before SQLite is touched, caps the response at three
excerpts per source, 600 characters per excerpt and 24 KB total, and turns unexpected exceptions into
an internal error payload with an error_id.
Tool | Arguments | Returns |
|
| ranked documents with citations, |
|
| the chunk and its neighbours, with the same citation object |
|
|
|
|
| bits to names, or names to a value, cross-checked against |
|
| defining path, pinned permalink, citation |
|
| parsed |
|
| fields, CRC extra, enum values, citation into |
|
| evidence-backed triage, or a refusal with |
|
| a complete script from |
|
| frames, |
|
| start code, byte count, checksum status, record type |
|
| record counts by type, data bytes, address span, EOF presence |
| none | each source with pin, tier, license and document count |
| none | counts for sources, documents, chunks and params, index size, last ingest, offline note |
scope values: docs is the wiki plus ExpressLRS docs, code is firmware source, protocol is
MAVLink and CRSF, dev is the ArduPilot dev wiki, local is your own board files and never appears
in recall numbers, all is everything except local.
Citation contract
{
"cite_id": "ardupilot#1a2b3c4d5e6f7a8b", // <source_id>#<hash16>
"source_id": "ardupilot",
"repo": "ArduPilot/ardupilot",
"ref": "Copter-4.7.2", // tag or full commit SHA
"channel": "stable", // stable | snapshot | local
"unreleased": false,
"pinned": true,
"url": "https://github.com/ArduPilot/ardupilot/blob/Copter-4.7.2/libraries/RC_Channel/RC_Channels_VarInfo.h#L92-L97",
"path": "libraries/RC_Channel/RC_Channels_VarInfo.h",
"lines": [92, 97],
"excerpt": "…verbatim slice, 600 characters at most…",
"version": "Copter-4.7.2 (2026-10-20)",
"ingested_at": "2026-10-25T20:13:33+0700",
"tier": "T1",
"license": "GPL-3.0",
"untrusted": true
}cite_id comes from content: sha256(source_id | path | line_start | line_end | sha256(text))
truncated to 16 hex characters. Re-ingesting the same text yields the same id, and changing the text
changes the id.
get_chunk(cite_id) returns the text the citation promised. The blackbox run checked 20 of 20 with
field_diff = 0.
A tag pin gives channel:"stable". A commit pin gives channel:"snapshot" with unreleased:true.
The local overlay gives channel:"local", pinned:false and url:null.
Corpus and pins
sources.yaml holds repo, ref (a tag or a SHA, never a branch), tier, license and include globs.
research.fetch tags resolves the newest Copter-X.Y.Z tag with git ls-remote --tags --refs and
drops beta and rc, and no release number is hardcoded in src/
(tests/test_offline.py::test_no_hardcoded_release_version_outside_tests).
Source | Repo | Pin | Tier | License |
| ArduPilot/ardupilot |
| T1 | GPL-3.0 |
| ArduPilot/ardupilot_wiki |
| T1 | CC-BY-SA-3.0 |
| mavlink/mavlink |
| T1 | unstated |
| ExpressLRS/ExpressLRS |
| T2 | GPL-3.0 |
| ExpressLRS/ExpressLRS |
| T2 | GPL-3.0 |
| EdgeTX/edgetx |
| T2 | GPL-2.0 |
| CRSF spec mirror |
| T2 | CC-BY-SA-4.0 |
| your files |
| T3 | user-owned |
The index holds 7 sources, 7 356 documents and 86 262 chunks in 182.3 MB. list_sources gives the
per-source counts. Licenses were read from each archive's own license file
(docs/license-notes.md). The corpus is never committed: data/ and research/cache/ are
gitignored.
Parameters
lookup_param answers from the @Param, @Bitmask and @Default metadata in the pinned source.
5 089 parameter names from 2 070
@Paramblocks. Full names are resolved through the pinned@Groupand@Pathchain, so@Param: 1_PROTOCOLinAP_SerialManager.cppplus theSERIALprefix becomesSERIAL1_PROTOCOL. Instance families expand:RC1..RC16,SERIAL1..8,BATTandBATTA,INS,INS2,INS3,COMPASS2/3,OSD1..5. No row is a duplicate: zero rows share(path, line_start, prefix).default_rawis the verbatim@Default:argument, present on 4 703 of 5 089 rows. It becomes a number only when it is a literal, a bit expression, or exactly one in-corpus#define. That resolves 3 003 rows and leaves 1 700 unresolved, which is reported rather than guessed. Examples:FS_THR_ENABLEgives 1 fromFS_THR_ENABLED_ALWAYS_RTL,FS_THR_VALUEgives 975,RC_OPTIONSgives 32,ARMING_SKIPCHKgives 0.LOG_BITMASKkeepsdefault_raw=DEFAULT_LOG_BITMASKwithdefault_resolved:false.Every citation points at the parameter's own block. All 5 089 rows are machine-checked so the excerpt contains
@Param:<suffix>and the line range covers it, with zero violations (scripts/check_param_citations.py). Before this was fixed, 3 891 of 5 089 citations showed a neighbouring block.Renames are reported.
ARMING_CHECKreturnsfound:falsewithrenamed_hint{new:"ARMING_SKIPCHK", path:"ArduCopter/ReleaseNotes.txt"}.WPNAV_SPEEDreturnsfound:falsewith no hint, because the release notes do not record that rename.Usage coverage is measured against real files. Of 1 440 unique names used by
Tools/autotest/default_params/*.paramandTools/Frame_params/*.paramat the pin, 739 (51.3 %) are found. Most of the rest belong to other vehicles' frames, classified indocs/fix/A-names.txt.Unresolvable cases are listed in
docs/fix/A-unresolved.txt: 509 unresolved blocks and 142 unlinked blocks.
Safety policy
The server explains safety systems. It does not present disabling one as a fix.
The rules live in src/ardupilot_research_mcp/safety_rules.yaml as lists of disable_verbs,
safety_objects, safety_params, value_patterns, info_markers, hardware_markers and
dangerous_excerpt. Matching runs on folded text, so tắt failsafe, tat failsafe and turn the failsafe off hit the same rule.
A request is refused when a disable verb targets a safety object, or when a safety parameter is combined with a pattern that zeroes it, and that holds inside a question too. A safety object with only an information marker takes the informational branch, which explains what the check does and cites evidence.
A refusal returns dangerous:true, refused_as_fix:true, message, why_dangerous and
safe_path, where the safe path says to read the actual pre-arm message, fix that cause, and
reproduce in SITL first. Citations are limited to ones explaining the purpose of the check; excerpts
that carry the disabling recipe are filtered out, and when nothing safe remains the payload has
citations: [] and a citations_note.
Props off for bench tests. SITL before hardware. appears when the gate fired or when the topic is a
bench procedure, and not otherwise.
Measured on the current tree: 20 bypass phrasings (Vietnamese with and without diacritics, mixed, and English) give 0 false negatives, 10 benign information questions give 0 false positives, and the blackbox run2 E section passes 15 of 15. Run1 missed three.
It will not print ARMING_CHECK=0, ARMING_SKIPCHK≠0, "force arm", "disable the failsafe or fence",
or a bench step without the props-off line.
How retrieval works
The query is folded for case and diacritics and expanded with a Vietnamese to English glossary
matched per token, so bit matches the word and not the substring in bitcoin. Stopwords are
removed.
A corpus anchor gate follows. Content words that appear in fewer than three documents, and that are
not snake_case symbols present in the index, do not anchor a query, and an unanchored query returns
found:false with an out-of-scope note.
Candidates come from FTS5 over a distilled text field holding path words, the heading path and the de-markuped body, plus a literal identifier pass for exact symbol and parameter names, ranked by BM25.
Scoring is per document: the best chunk wins, with boosts for path and heading tokens, a bonus when
the heading carries every query token, and a penalty for simulator-only models under
libraries/SITL/.
Scope notes and confidence follow. Out-of-corpus product names such as PX4, version numbers no pin
carries, and weak overlap with the top three results each add a scope_notes entry and lower
confidence.
Only then does a chunk become a citation, with its content hash, pinned URL, line range and excerpt.
chunks.text stays byte-verbatim for citations, and chunks.distilled exists only for the FTS
index, which is why markup-heavy wiki pages match prose queries without corrupting the quote.
Configuration
Three environment variables, with defaults filled in by the checkout:
Variable | Default | Meaning |
| the checkout containing | where |
|
| where |
|
| where the quota ledger lives |
Because the index and the ledger resolve from these, you can keep isolated copies on one machine, for example one per worktree:
$env:ARM_REPO_ROOT = "D:\work\ardupilot-research-mcp"
$env:ARM_DATA_DIR = "D:\work\ardupilot-research-mcp\data"
$env:GOVERNOR_HOME = "D:\work\ardupilot-research-mcp\research"A missing index makes every tool return
{"found":false,"error":"index not built: run ardupilot-research-ingest --all"}.
Quota governance
Metered sources (Firecrawl, Exa, web search, the GitHub API) are spent only through
research/governor.py, in a reserve, call, record loop. The governor keeps a SQLite WAL ledger
(research/ledger.sqlite) with a file lock, a sliding window, 20 % headroom, a minimum interval
between calls, a per-provider budget, cache-first reuse, Retry-After handling, exponential backoff,
a circuit breaker, and a STATE.md written when the budget runs out.
python -m research.governor status
python -m research.governor reserve --provider firecrawl --endpoint scrape --query "https://…"
python -m research.governor record --id 42 --status ok --http-code 200 --result-sha abc123
python -m research.governor export-reportThe ledger for this index reads calls=88, cost_units=32.0, with firecrawl 0, exa 0 and web_search
0. The corpus came from git ls-remote and codeload archives, which spend no search units. A rebuild
from a warm cache spends nothing.
Register a client
The server command is always:
python -m ardupilot_research_mcp.serverIt needs PYTHONPATH=<repo>/src or an editable install. Replace
D:\Skill_tulan\ardupilot-research-mcp with your checkout path.
What has been verified outside the sandbox: in Claude Code 2.1.308 the server connects, the client
lists all 14 tools as mcp__ardupilot-research__*, and claude mcp list reports √ Connected for a
user-scope registration. The Claude Code model turn stops at "Not logged in · Please run /login",
which is client authentication and not the server.
In OpenCode 2.1.3 with the free model opencode/deepseek-v4.1-flash-free a full turn ran: the model
called tools["ardupilot-research"].lookup_param({name_or_pattern:"RC_OPTIONS"}), got
default: 32 with cite_id ardupilot#11fb2497e2a89d95, and answered 32. Transcript:
docs/blackbox/h/opencode_stream.jsonl; setup in docs/client-setup.md section 7.
Codex was not launched. Raw init JSON for both working clients is in docs/client-setup.md.
A project-scope .mcp.json needs one interactive approval. A headless -p session shows it as
pending with 0 tools, and that is what run1 recorded as status: failed. Setting
enableAllProjectMcpServers: true in .claude/settings.json or .claude/settings.local.json did not
change it on 2.1.308.
Claude Code
# user scope: Connected with no manual approval
claude mcp add --scope user ardupilot-research `
--env PYTHONPATH=D:\Skill_tulan\ardupilot-research-mcp\src `
--env ARM_REPO_ROOT=D:\Skill_tulan\ardupilot-research-mcp `
--env ARM_DATA_DIR=D:\Skill_tulan\ardupilot-research-mcp\data `
-- python -m ardupilot_research_mcp.server
claude mcp list # expect: ardupilot-research … √ Connected
# project scope: writes .mcp.json, then approve once in the TUI
claude mcp add --scope project ardupilot-research … # then run `claude` and approve
# or skip approval by passing the file explicitly:
claude -p "<question>" --mcp-config .\.mcp.json --strict-mcp-configThe project file is per-machine, so it is gitignored; the committed template is
.mcp.json.example. It looks like this:
{
"mcpServers": {
"ardupilot-research": {
"type": "stdio",
"command": "python",
"args": ["-m", "ardupilot_research_mcp.server"],
"env": {
"PYTHONPATH": "D:\\Skill_tulan\\ardupilot-research-mcp\\src",
"ARM_REPO_ROOT": "D:\\Skill_tulan\\ardupilot-research-mcp",
"ARM_DATA_DIR": "D:\\Skill_tulan\\ardupilot-research-mcp\\data"
}
}
}
}OpenCode
opencode.jsonc. Local servers take an mcp entry with type: "local" and a command array,
transcribed from sst/opencode, packages/web/src/content/docs/mcp-servers.mdx:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"ardupilot-research": {
"type": "local",
"command": ["python", "-m", "ardupilot_research_mcp.server"],
"enabled": true,
"environment": {
"PYTHONPATH": "D:\\Skill_tulan\\ardupilot-research-mcp\\src",
"ARM_DATA_DIR": "D:\\Skill_tulan\\ardupilot-research-mcp\\data"
}
}
}
}Codex
The key names below are unverified. Codex's config reference redirects to a login page, so nothing was guessed:
[mcp_servers.ardupilot-research]
command = "python"
args = ["-m", "ardupilot_research_mcp.server"]
env = { PYTHONPATH = 'D:\Skill_tulan\ardupilot-research-mcp\src', ARM_DATA_DIR = 'D:\Skill_tulan\ardupilot-research-mcp\data' }Proving it works
claude mcp list
# ardupilot-research: python -m ardupilot_research_mcp.server - √ Connected
claude -p "hi" --mcp-config .\.mcp.json --strict-mcp-config --output-format stream-json --verbose 2>&1 |
Select-Object -First 1
# …"mcp_servers":[{"name":"ardupilot-research","status":"connected"}],
# "tools":[… "mcp__ardupilot-research__lookup_param" … 14 of them]Then ask: "Look up RC_OPTIONS and tell me which bit enables the ELRS 420kbaud mode." A correct
answer cites libraries/RC_Channel/RC_Channels_VarInfo.h, lines 92 to 97, and names
bit 13: Use 420kbaud for ELRS protocol. A wrong answer has no cite_id, or points at AP_Arming.
If the client reports the server as failed, docs/client-setup.md section 4 walks through python on
PATH, PYTHONPATH, cwd, environment variables and the cmd /c wrapper.
Tests and gates
python -m pytest -q # 141 passed
python scripts\check_traceability.py # requirement to test to result, failing=0
python scripts\fidelity_scan.py # 86 262 chunks, 0 mismatch, PASS
python scripts\check_param_citations.py # 5 089 parameters, 0 violations
python scripts\check_readmes.py # both READMEs: no AI tells, numbers agree
python scripts\bench.py # search p50/p95 against a budget
python -m research.verify_citations # line ranges and pinned URLs, PASS
python -m research.audit_safety # the six required safety inputs
python -m research.eval_retrieval --set tests/golden.yaml --aggregate-only
python -m research.eval_retrieval --set tests/heldout_v3.yaml --aggregate-only
python scripts\mutation_check.py # mutation scoreLast measured on the merged tree with the index rebuilt from scratch:
Gate | Result |
| 141 passed, 1 skipped |
| 86 262 chunks, 0 mismatch, PASS |
| 5 089 checked, 0 violations |
| 22 requirements, failing=0 |
| PASS, 0 unpinned URLs |
safety corpus, 30 cases | 0 false negatives, 0 false positives |
golden set, dev and tunable | recall@5 1.000, P@1 0.767, MRR 0.859 |
held-out v3, blind and frozen | recall@5 0.933, P@1 0.833, MRR 0.868 |
mutation | 1.000, 14 scored mutants killed, 1 equivalent mutant excluded and flagged |
blackbox run2, 108 frozen cases | 92 pass, 16 fail, 0 regressions against run1 |
scoring determinism | 5 scripts run twice, byte-identical output |
| PASS |
| search p50 58.5 ms, p95 186 ms, budget 400 ms, PASS |
tests/conftest.py resets the in-process rate guard between tests. The guard still enforces 60 calls
per minute in production.
Both data/index.sqlite and data/corpus are gitignored, so a fresh clone adapts instead of failing.
With the index only, the one test that compares citations against source files skips, giving 133
passed and 1 skipped. With the corpus extracted as well, all 134 run.
Measured quality
Set | What it is | recall@5 | P@1 | MRR |
| 30 dev questions, tunable, not evidence | 1.000 | 0.767 | 0.859 |
| 30 blind questions with one defensible answer, SHA-256 frozen before the run | 0.933 | 0.833 | 0.868 |
| first frozen set, several expectations factually wrong | 0.433 | 0.300 | 0.334 |
| blind but ambiguous | 0.733 | 0.400 | 0.513 |
The fair instrument is v3 (docs/eval-heldout-v3.md). v1 and v2 are kept for honesty and are not used
for scoring. Hashes are in docs/heldout*.sha256, and the evaluator refuses to run if a set changed.
An independent tester froze 108 tool-level cases (tests/blackbox_cases.md, SHA-256
1b53e69…dcaf27), ran them over one stdio session, and wrote docs/blackbox/REPORT.md, which scored
80 pass and 28 fail. After the fix round in docs/FIX_REPORT.md, the same frozen cases ran once more
into docs/blackbox/run2/: 92 pass and 16 fail, with no case going from pass to fail and 12 fixed.
The 16 remaining splits into 6 tool defects and 10 cases whose frozen expectation was wrong (a hex
string missing a nibble, a parameter that does not exist at the tag, expectations that treat a
source_id as part of a path). The 108-row comparison with raw output is in
docs/blackbox/run2/comparison.md.
The five scoring scripts were also run twice on the same raw output and produced byte-identical
results, so the 92/16 verdict does not depend on run order or timing
(docs/blackbox/run2/determinism.md).
Reproduce the build
python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e .
python -m research.build_all --gates # fetch, extract, ingest, params, mavlink, then the gates
python -m research.eval_retrieval --set tests/heldout_v3.yaml --aggregate-only
python scripts\smoke_stdio.py # needs stdio pipes, so run it outside a sandbox
python -m research.build_all --prune-cache # optional: delete the downloaded archivesbuild_all runs each stage as its own process and stops at the first failure, so the error names the
stage. --dry-run prints the commands without running them, and --prune-cache removes the 2.7 GB
of archives once the index exists.
Layout
sources.yaml pinned corpus: repo, ref, tier, license, include and exclude globs
glossary.vi-en.yaml Vietnamese to English query terms, matched per token
scope.yaml out-of-corpus product names, weak-match threshold
pyproject.toml mcp==1.30.0 and pyyaml, console scripts
src/ardupilot_research_mcp/
server.py MCP wiring only: stdio, ToolAnnotations, 14 tools
config.py repo, data and ledger paths
db.py SQLite schema and FTS5 external-content index
query.py anchor gate, BM25 and identifier search, scope notes, ranking
cite.py cite_id, pinned URLs, 600-character excerpts
safety.py refuse and inform gate
safety_rules.yaml the rules themselves
guard.py rate limit and response cap
tools/<name>.py one pure read-only tool per module
decoders/ CRSF frames and Intel HEX, stdlib only
ingest/ the only package allowed to touch the network
research/ fetch, extract, build_params, build_mavlink, governor, audits, eval
scripts/ smoke, tool matrix, fidelity, checks, mutation, blackbox
examples/ 4 runnable scripts, each with its cite_ids
tests/ unit, protocol, safety, offline, golden, frozen held-out sets
docs/ design, licenses, eval reports, FIX_REPORT, client-setup, blackboxdata/ holds the corpus, the index and your local-project files. Both it and research/cache/ are
gitignored, so the repo ships the recipe rather than gigabytes of upstream source.
Troubleshooting
Symptom | Cause and fix |
|
|
| its library is outside the include list, or the name belongs to another vehicle; the payload lists real candidates and, when the release notes record it, a |
| the macro chain has no single resolution, so it is reported instead of guessed ( |
| the corpus does not use those words |
| input limits: query 2000, names 128, paths 1024, Intel HEX line 600, CRSF hex 4096 |
| a bug; the traceback is on stderr under that id, capture stderr and report it |
| the session blocks stdio pipes, run it outside the sandbox |
|
|
the client shows the server as failed with no reason |
|
a held-out score looks low | read |
Limitations and unverified claims
Verified on Claude Code 2.1.308 outside the sandbox: the server connects, the client lists 14 tools,
and claude mcp list reports √ Connected for a user-scope registration.
Still unverified:
Codex. It is not installed here and its stdio key names are unconfirmed.
A model-issued tool call inside Claude Code, which needs a logged-in session. OpenCode has one on record (see above); the Claude Code client was not logged in.
Known limitations:
Ranking misses 3 frozen blackbox cases (
C4,C10,C11). The right file is indexed but does not reach the top five for those phrasings.C1is fixed: the winning chunk is now the definition (0xD5), not the Command-frame paragraph (0xBA) that shared its words.Two parameters,
SERIAL1_PROTOCOLandLOG_BITMASK, are found but their default stays unresolved because the macro chain is not single-valued. A case that demands a numeric default for them fails.509 unresolved
@Paramblocks and 142 unlinked blocks are listed indocs/fix/A-unresolved.txt.A4_100Kreturns a schema validation error (isError, empty content) rather than a{found:false}payload, becausedecode_crsf_frameuses a pydanticmax_lengthwhile the other tools validate in the wrapper. Both are explicit errors, with different shapes.board_infofollows one level ofincludeinhwdef.dat.T2 issue and PR evidence and T3 forum evidence are not ingested. The source list is the seven pinned repos.
local-projectindexes your own files with no pin and no URL, and stays out of every recall number.heldout_lockedscores 0.433, below the 0.8 bar. That set is documented as unfair and is not used as evidence.The link-escape test (
tests/test_hardening.py) skips on accounts that cannot create directory symlinks, so junction traversal is unproven on this machine.
Licenses and provenance
The server code is MIT, in LICENSE (and declared in pyproject.toml).
The corpus is not redistributed. data/ and research/cache/ are gitignored, and returned excerpts
are capped at 600 characters, carry the source license and the pinned URL, so a quote stays traceable
to its upstream file.
Per source, read from each archive's own license file (docs/license-notes.md): ardupilot GPL-3.0,
ardupilot_wiki CC-BY-SA-3.0, ExpressLRS GPL-3.0, ExpressLRS Docs GPL-3.0, EdgeTX GPL-2.0, CRSF spec
CC-BY-SA-4.0, MAVLink XML unstated and kept with that label by explicit decision.
Binaries, images, fonts, archives, .hex and .apj files never enter the corpus, and every source's
LICENSE, COPYING or README is force-included so provenance travels with the text.
Status
v0.1.0, first complete pass, 2026-10-25.
14 tools over stdio, read-only and offline, 141 tests, 86 262 chunks with no fidelity mismatch.
Parameter table rebuilt from pinned metadata: 230 to 5 089 parameters, citation violations 3 891 to 0, defaults 0 to 4 703 raw with 3 003 resolved.
Safety gate rewritten as data with folded bilingual matching: false negatives went from 3 of 11 to 0 of 20, false positives stayed at 0 of 10, and refusal citations no longer carry the disabling recipe.
Input limits and internal-error shielding on every tool, a token-based glossary, a corpus anchor gate so out-of-corpus questions return
found:false, and a ranking penalty for simulator code.decode_crsf_framereportsframes_found, returnsall_crc_valid:nullwith zero frames, reportstruncated_tail_bytes, and converts microseconds with C-style truncating division, so tick 172 gives 988.Independent blackbox rerun over the frozen 108 cases: 92 pass, no regressions, with a deterministic scoring pass.
Client wiring proven in two real clients: Claude Code connects and lists 14 tools (user scope reports
√ Connected), and an OpenCode session with a free model issued alookup_paramcall and answered from the citation.Chunk selection inside a winning document now prefers whole-word matches and the chunk without a narrower heading, which turns C1 from a near-miss into a correct quote.
Mutation harness re-pointed at the current code, an equivalent mutant classified instead of scored, and the stopword filter is now covered by a test.
research.build_allgives the build one entry point,research.extractno longer fails on the local overlay, andscripts/bench.pyguards latency.CI runs the unit suite, the README gate and traceability on Windows and Ubuntu; a weekly job builds the index and runs the documented gates.
LICENSEadded, and.mcp.jsonis per-machine again (only.mcp.json.exampleis committed).
The defect ledger, decisions and costs are in docs/FIX_REPORT.md, with per-defect logs under
docs/fix/.
Next steps that would move the needle: rank the four remaining retrieval cases without tuning per question, resolve more macro defaults, ingest T2 and T3 evidence, and finish one logged-in client session so a model-issued tool call is on record.
This server cannot be deployed
Maintenance
Related MCP Connectors
Offline, keyless lookup of the US civil aircraft registry — decode N-numbers, search records.
- docs2mcpOAuthcom.docs2mcp
Query your own PDFs and documents from any MCP client. Every answer cites the page it came from.
- alloyOAuthai.usealloy
Connect Claude, Cursor, Codex, and other AI tools to your robotics mission data.
Docs Q&A: search 169 data and AI guides, fetch any page as markdown. Read-only, keyless.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables semantic search and question answering over a knowledge base using hybrid retrieval and grounded answers, all running offline with no API keys.4MIT
- AlicenseNot gradedqualityCmaintenanceEnables private, offline semantic search across local files (documents, images, videos) using OCR and vector search, and optionally performs web research with cited sources.AGPL 3.0
- FlicenseNot gradedqualityCmaintenanceEnables agents to search a local, fully offline personal document library (Markdown/txt/PDF/docx) via SQLite FTS5 trigram search with subject-terminology expansion, returning every hit with its domain, document title, section path, and page or offset provenance. It also exposes domain statistics, incremental ingestion, and document deprecation as tools, alongside a read-only, runtime-persona-scoped ask/coverage mode.-
- AlicenseNot gradedqualityBmaintenanceEnables engineers and AI assistants to search source-backed hardware design guidelines, retrieve detailed conditions and limitations, and trace citations to original sources, all through a read-only offline interface.AGPL 3.0