goae
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
goae-lookup
How to give an LLM agent access to reference data that is far too large for its context window — a worked example using the German medical fee schedule (GOÄ).
The code is a complete, dependency-free tool (CLI, MCP server, batch pipeline) for looking up GOÄ fee codes. The ideas behind it apply to any large reference dataset: price lists, product catalogs, code systems (ICD-10, customs tariffs), regulations, configuration references.
The problem
Many agent tasks need lookups in a dataset that does not fit into the prompt: "Find the right code for each of these 200 items." The usual approaches break down:
Put everything into the prompt. The GOÄ alone is 550 KB of text, about 137,000 tokens. It does not fit into a 32K or 64K window.
Generic RAG over text chunks. Each query returns several pages of text (~1,000+ tokens), of which a few lines are relevant. In a multi-step task every result stays in the conversation. After a few dozen lookups the window is full, and well before that the model starts losing track of what it has already found.
A larger context window only postpones this. The fix is to change what the agent gets back.
Related MCP server: Medical Billing MCP
The pattern
Turn the source into structured data once. Parse it into records (here: code, text, points, section, notes, rules) in SQLite. A lookup becomes a query, not "read five pages and pick out the answer".
Answer every question in a few lines. Compact line format instead of JSON, long texts truncated, top 5 results. A search result here is ~90 tokens.
Disclose details progressively. A cheap
searchfirst, thengetfor the one record that matters, thenrules/checkonly if needed.Compute in code, not in the model. Fees and exclusion rules are evaluated by the tool. The model never does arithmetic or parses legal text.
Keep state in files, not in the conversation. For batch work, run a loop outside the agent: one small, independent LLM call per item, result written to a file immediately.
Keep the tool schema small. Tool definitions are sent with every request. Four tools with terse descriptions cost 245 tokens here instead of 583.
Measure. Define example tasks with known answers and measure tokens and correctness for every design change (see How the design was found).
Results for the GOÄ example
40 example tasks with known answers: find a code from a plain-language description, compute a fee, check whether two codes may be billed together, look up a rule.
Approach | Tokens per search | All 40 tasks in one conversation | Test tasks answered correctly¹ |
Whole fee schedule in the prompt | ~137,000 | does not fit | – |
Generic RAG, top-5 text chunks | ~1,150 | ~49,000 | 5/17 |
Straightforward tool (verbose, JSON, 10 results) | ~680 | ~22,000 | 13/17 |
This repository | ~90 | ~3,700 | 12/17, 17/17 with 6 added synonyms |
¹ 17 of the 40 tasks are test tasks: they were not used while the tool was being improved, so they show how well it handles questions it was not tuned for. 12/17 is that result. Only in a final step were 6 synonyms added for the failed ones, which gives 17/17 (see How the design was found).
Token counts are estimates: number of characters divided by four. Results for every single task are in CONTEXT_STATS.md.
Quick start
Requirements: Python 3.10+ with SQLite FTS5 (part of standard CPython builds). No other dependencies.
git clone https://github.com/markusrt/goae-lookup && cd goae-lookup
python -m goae search "Blutabnahme aus der Vene" # builds data/goae.db on first run (~1 s)1012|74P|Blutentnahme beim Fetus
250|40P|Blutentnahme mittels Spritze, Kanüle oder Katheter aus der Vene
284|90P|Eigenbluteinspritzung - einschließlich Blutentnahme -
1013|178P|Blutentnahme beim Fetus - einschließlich pH-Messung(en) im Blut -
251|60P|Blutentnahme mittels Spritze oder Kanüle aus der Arteriepython -m goae get 250 # full text, points, fees, own rules
python -m goae check 1 3 250 # may these codes be billed together?
python -m goae rules 5 # general rules that mention a code
python -m goae price 250 --factor 2.0$ python -m goae get 250
Nr. 250 · C.II Nichtgebietsbezogene Sonderleistungen
Blutentnahme mittels Spritze, Kanüle oder Katheter aus der Vene
40 Punkte · 1,0x 2,33 € · 1,8x 4,20 € (Regel) · 2,5x 5,83 € (max)
(+3 weitere Bestimmungen nennen Nr. 250 → rules)
$ python -m goae check 1 3 250
✗ 3 nicht neben 1, 250: Die Leistung nach Nummer 3 (Dauer mindestens 10 Minuten) ist nur berechnungsfähig als einzige Leistung oder im Zusammenhang mit einer Untersuchung nach Nummer 5, 6, 7, 8, 800 oder 801. … [Nr. 3]Optionally pip install -e . provides a goae command.
Using it from an agent
The same operations are available in three ways, so it works with any agent.
Shell — for any agent that can run commands. Add the snippet from
examples/AGENTS.md to your AGENTS.md, CLAUDE.md or system
prompt.
MCP — stdio server, standard library only. Tools: goae_search, goae_get,
goae_check, goae_rules.
opencode (opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"goae": {
"type": "local",
"command": ["python3", "-m", "goae", "mcp"],
"environment": { "PYTHONPATH": "/path/to/goae-lookup" },
"enabled": true
}
}
}Claude Code, Claude Desktop and most other clients (.mcp.json):
{
"mcpServers": {
"goae": {
"command": "python3",
"args": ["-m", "goae", "mcp"],
"env": { "PYTHONPATH": "/path/to/goae-lookup" }
}
}
}Python — from goae import db; print(db.search("Hausbesuch")).
Processing many items: a loop outside the conversation
Compact results make the context fill up more slowly. But if one conversation handles all items, it still grows with every item, because each result stays in it:
system prompt + tool definitions
item 1 → tool call → result 1 ┐
item 2 → tool call → result 2 │ every result stays in the conversation;
… │ with enough items the window overflows
item N → tool call → result N ┘For batch work, move the loop into a script. For each item it runs the lookup itself, asks the model one short question and writes the answer to the output file. Every LLM call starts without any chat history, and the file holds the progress:
for each row in input.csv:
candidates = search(row) # database, no LLM, ~90 tokens
prompt = row + candidates # ~140 tokens, the same for every row
answer = LLM(prompt) # fresh call, no history
write answer to output.csv # the file is the memoryOne row, end to end. For the input row Ruhe-EKG this is the complete prompt the
model receives:
Wähle die passende GOÄ-Ziffer für die Leistung.
Leistung: Ruhe-EKG
Kandidaten (Ziffer|Punkte|Text):
651|253P|Elektrokardiographische Untersuchung in Ruhe - auch gegebenenfalls nach…
650|152P|Elektrokardiographische Untersuchung zur Feststellung einer Rhythmusstörung…
653|253P|Elektrokardiographische Untersuchung auf telemetrischem Wege
659|400P|Elektrokardiographische Untersuchung über mindestens 18 Stunden (Langzeit-EKG)…
5422|1000P|Szintigraphische Untersuchung des Myokards mit myokardaffinen Tracern in Ruhe…
Antworte nur mit JSON: {"code": "<Ziffer oder leer>", "confidence": <0..1>}The model answers, for example, {"code": "651", "confidence": 0.9}, and the row in the
output file gets three new columns:
leistung | … | goae | goae_confidence | goae_candidates |
Ruhe-EKG | … | 651 | 0.90 | 651 650 653 659 5422 |
An answer that is not among the candidates, or has a confidence below
--min-confidence, leaves goae empty for a person to decide. An interrupted run
continues with the rows that are not done yet.
# candidates only, no LLM: a person picks from goae_candidates (e.g. in a spreadsheet)
python -m goae fill examples/leistungen.csv
# fully automatic with a local model behind any OpenAI-compatible endpoint
# (Ollama, llama.cpp server, LM Studio, vLLM)
python -m goae fill examples/leistungen.csv --llm http://localhost:11434/v1 --model qwen2.5:7b-instruct
# fully automatic with any CLI that reads a prompt on stdin (see below)
python -m goae fill examples/leistungen.csv --llm-cmd "claude -p --model haiku"
# options: column with the description, number of candidates, output file
python -m goae fill my.csv --col Leistungsbeschreibung -k 5 -o my.done.csvNo API? Use the agent CLI you already have as the LLM
--llm-cmd runs a command line tool that reads a prompt on stdin, once per row. Each call
is a new process, so every row starts with an empty conversation:
python -m goae fill examples/leistungen.csv --llm-cmd claude
python -m goae fill examples/leistungen.csv --llm-cmd "ollama run qwen2.5:7b-instruct"claude is a preset for Claude Code that uses your normal subscription, no API key.
The preset matters: a plain claude -p call carries Claude Code's complete system
prompt, tool definitions, MCP servers and skills into every call. The preset replaces
the system prompt and switches the rest off (--system-prompt … --tools "" --strict-mcp-config --disallowedTools "mcp__*" --no-chrome --disable-slash-commands --no-session-persistence,
model haiku). Measured with Claude Code 2.1.321 on the example table:
Input tokens per row | 8 rows | |
plain | ~42,000 | ~340,000 |
| ~2,000 | ~16,000, 8/8 correct, 18 s |
For comparison, an agent session is not free per step either: every turn re-sends the whole conversation, including the agent's own system prompt (~30,000 tokens in Claude Code), mostly as cheaper cached input. The per-row preset calls stay small and constant.
Letting an agent work through the table
If an agent should do the work inside its own session, two commands keep that session
small. next shows a batch of open rows with three short candidates each; set saves a
whole batch of answers and immediately prints the next batch. One tool call handles ten
rows, and the agent never has to open the file:
$ python -m goae next examples/leistungen.csv
8 open; answer with: set leistungen.csv ROW=CODE … (? = nothing fits)
#1 Blutabnahme aus der Vene
1012|74P|Blutentnahme beim Fetus
250|40P|Blutentnahme mittels Spritze, Kanüle oder Katheter aus der…
284|90P|Eigenbluteinspritzung - einschließlich Blutentnahme -
#2 Ruhe-EKG
651|253P|Elektrokardiographische Untersuchung in Ruhe - auch…
…
$ python -m goae set examples/leistungen.csv 1=250 2=651 3=375 4=3 5=417 6=3550 7=50 8=70
saved 1=250 2=651 3=375 4=3 5=417 6=3550 7=50 8=70
done: 8 rows → leistungen.goae.csvA prompt for the agent:
Find the GOÄ codes for examples/leistungen.csv. Work in batches:
1. Run `python -m goae next examples/leistungen.csv`. It lists up to 10 open rows
(#row and description), each with three candidate codes.
2. For each row pick the matching candidate. Only if none fits, run
`python -m goae search "<medical term>"` for that row; use ? if still nothing fits.
3. Save the whole batch with one command, e.g.
`python -m goae set examples/leistungen.csv 1=250 2=651 3=?`
It saves the answers and prints the next batch. Repeat 2–3 until it prints "done".
Rules: do not open, print or edit the CSV files yourself. Do not repeat rows or results
in your messages, and keep your reasoning short. At the end, report only the output file
and the rows marked ?.What to expect: a batch of 10 rows is about 530 tokens of tool output. The agent's own reasoning and tool-call messages usually cost more than that, which is why batching matters: it pays this overhead once per ten rows instead of once per row.
Two more things keep the session small:
Results go to a separate file (
leistungen.goae.csv); the input is never changed. Some agents re-insert a file into the conversation whenever a file they have read changes on disk (Claude Code does this as a "file updated" note with a diff). Writing to a file the agent has never opened avoids that.Delegate the loop to a subagent if your agent supports it (Claude Code, opencode): start the prompt above with "Use a subagent for this and report only its summary." The subagent's context is discarded afterwards; your main conversation only receives the summary.
No MCP configuration is needed for any of this: an agent that can run shell commands
uses the CLI. MCP (.mcp.json, opencode.json) is only needed for agents without shell
access, or if you prefer tool calls to shell commands; next and set are CLI-only.
How the design was found
The defaults in this repository were not designed up front. They are the result of an experiment that you can rerun and reuse for your own data:
Write tasks with known answers in
bench/tasks.json, split into two groups:23 tuning tasks (
devin the file). While improving the tool you may look at these, e.g. when a search fails, find out why and fix it.17 test tasks (
holdoutin the file). These are only run, never used to decide on a change. If a change helps the tuning tasks but not the test tasks, it was probably tailored to those specific questions and will not help real users.
Answer every task with the tools and measure three things: the size of the tool definitions, the size of all tool results, and whether the correct answer is contained in them.
Change one thing, measure again, keep what helps.
Each step in the table is one such change. Tokens is the average size of a search result; the last two columns count correctly answered tasks.
Step | Change | Tokens | Tuning | Test |
it1 | Straightforward tool: JSON, full texts and notes, 10 results | 678 | 17/23 | 13/17 |
it2 | Line format | 171 | 17/23 | 13/17 |
it3 | Texts truncated to 80 characters, 5 results — cheap, but loses hits | 62 | 15/23 | 11/17 |
it4 | Hybrid ranking (word search + character trigrams for German compounds) | 88 | 19/23 | 12/17 |
it5 | Synonym file for plain-language terms, built from failed tuning tasks | 88 | 23/23 | 12/17 |
it6 | Terse tool schema, 4 instead of 5 tools (583 → 245 tokens per request) | 88 | 23/23 | 12/17 |
it8 | 6 synonyms for the failed test tasks (from here on the test tasks are no longer unseen) | 89 | 23/23 | 17/17 |
The lesson: output format and size bring the largest savings, better ranking wins back the hits that truncation costs, and the remaining misses are vocabulary gaps.
Where the synonyms come from. Users write "Blutabnahme", "EKG" or "MRT"; the official
text says "Blutentnahme", "Elektrokardiographische Untersuchung" and
"Magnetresonanztomographie". No ranking method bridges that gap reliably, so
data/synonyms.tsv maps everyday terms to the official wording and
the search adds that wording to the query. Every entry was added by hand after a search
failed: look at the task, look up the official text of the expected code, add one line.
Ten entries came from failed tuning tasks (step it5) and one from the example table in
examples/. Six more were added after test tasks failed (step it8, marked separately in
the file; "CT" was added along with "MRT"). In a real deployment the best source
is the searches your users run that find nothing; a domain expert or an LLM can propose
candidates, but each entry should be checked against a failing example.
python bench/run.py # all steps → CONTEXT_STATS.md
python bench/run.py --only final # current defaults, lists failed tasks
python bench/run.py --log "what I changed" # also record the run in the historyCONTEXT_STATS.md has all numbers, including the two baselines (whole document, generic RAG) and a per-task breakdown.
Applying the pattern to your own data
The GOÄ-specific parts are small. To adapt the repository to another dataset:
Replace | With |
| a parser for your source that produces records ( |
| the operations your tasks need; keep |
| tool names and one-line descriptions |
| the vocabulary of your users |
| 20–40 tasks with known answers from your domain |
Then run the benchmark and iterate. Search, MCP server, batch pipeline and benchmark work unchanged.
Example: briefing a coding agent. The following prompt applies the pattern to a completely different dataset — a parts catalog — and can be handed to any coding agent (Claude Code, opencode, Codex, …) together with a link to this repository:
Build a context-minimal lookup tool for our electronic parts catalog. Use the pattern
and project structure of https://github.com/markusrt/goae-lookup (read its README first).
Data
- catalog.csv (~400,000 rows, 300 MB): part_number, manufacturer, description,
category, package, attributes (JSON: resistance, tolerance, voltage, …),
price_breaks (JSON: quantity → unit price), stock, lifecycle (active/NRND/EOL)
- The file is far too large for any context window. Never put it, or large parts of
it, into a prompt.
Questions the tool must answer
1. Which parts match a free-text description from a bill of materials,
e.g. "10k resistor 0603 1%" or "100nF 50V X7R 0805"?
2. What does part X cost at quantity N (use the price breaks; compute in code)?
3. Is part X still active, and if not, which in-stock parts have the same category,
package and key attributes?
4. Fill a BOM (bom.csv, one line per item) with a part number per line.
Requirements
- Parse the catalog once into SQLite with full-text search. Normalise units in
descriptions and attributes (10k = 10 kΩ = 10000, 100nF = 0.1 µF).
- Operations: search, get, price, alternatives. Every answer at most ~10 short lines;
search returns the top 5 as "part|key attributes|price@1|stock".
- Offer them as a CLI and as an MCP server with short tool descriptions.
- BOM filling runs as a script loop: one small LLM call per line (line + 5 candidates),
result written to the output file immediately, resumable, codes outside the
candidate list rejected.
- Write 40 tasks with known answers (25 tuning, 15 test) and a benchmark that reports
tokens per answer and correctness. Iterate on output format, ranking and synonyms
("cap" → capacitor, "res" → resistor) until results are small and correct, using only
the tuning tasks to decide on changes. Report the test results honestly.
- Write the results of each iteration to CONTEXT_STATS.md.Building the database from another source
By default the database is built from the official GOÄ text. As an example of a
different source, goae/sources/abrechnungsstelle.py builds it from the website
https://abrechnungsstelle.com/goae/ziffern/, which adds analogue codes and an exclusion
list per code. The website's content is not part of this repository; it is downloaded
when you build:
python -m goae build --source abrechnungsstelle # → data/goae-abrechnungsstelle.db
python -m goae --db data/goae-abrechnungsstelle.db search "Ruhe-EKG"
export GOAE_DB=data/goae-abrechnungsstelle.db # use it for the MCP server / agentsDownloads 14 pages once (respects robots.txt, one request every 3 s), caches the HTML in
data/cache/and writes a separate database. Both are git-ignored.--refreshdownloads again.Takes service text, points, notes and exclusion codes per entry. The exclusion lists are converted into rule sentences, so
checkuses them as well. Sections, fee factors and general rules still come from the official text.Tested offline against a hand-written HTML fixture (
tests/fixtures/). If the website changes its layout,python -m goae.sources.abrechnungsstelle --debug 1-498shows what the parser sees.Check the website's terms before using the data beyond personal purposes.
Data and limitations
Default source: the official GOÄ text (BGBl. I 1996, last amended 21.10.2019) from the Markdown mirror of gesetze-im-internet.de, bundestag/gesetze (
g/goae_1982/index.md, Unlicense). German statutes are official works without copyright (§ 5 UrhG). A copy is indata/source/.The official text has no analogue codes (BÄK Analogbewertungen), no commentary and no case law, and the planned new GOÄ is not included.
checkevaluates explicit exclusion rules ("nicht neben", "nur … im Zusammenhang mit"). Frequency limits ("im Behandlungsfall nur einmal") are shown bygetandrules, but not enforced.This is a technical example, not billing or legal advice.
Layout
goae/ingest.py parse the official text → SQLite with full-text indexes
goae/db.py search / get / price / check / rules, all returning short text
goae/tools.py tool definitions (terse set, plus the verbose set used as a baseline)
goae/mcp_server.py MCP server over stdio, no dependencies
goae/fill.py batch pipeline (optional OpenAI-compatible LLM), next/set for agents
goae/sources/ optional adapters for other sources (abrechnungsstelle.com)
goae/__main__.py CLI
bench/ tasks, benchmark, results, history
data/ source text and synonyms; databases and cache are generated
tests/ unittest suite (python -m unittest discover -s tests), HTML fixtureLicense
Code: MIT. GOÄ text: public domain (see above).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Offline US medical code lookup and crosswalk — ICD-10-CM/PCS, HCPCS Level II, RxNorm. Keyless.
ICD-10-GM, OPS, DRG, ZE, PEPP, DKR, SEG-4, ATC lookup for German coding, 2010-2027
WHO ICD-10/ICD-11 diagnosis codes. Lookup, search, chapters via official WHO API.
DME billing and medical coding: code lookup, DMEPOS fee schedules, Medicare prior-auth checks
Related MCP Servers
- AlicenseAqualityBmaintenanceHealthcare billing AI for agents — 12 tools for ICD-10/CPT/HCPCS code lookup (80K+ codes), prior auth prediction, medical NER, claims validation, HIPAA compliance auditing, and provider/drug enrichment. Pay-per-call via credits or USDC.32187 npm3MIT
- AlicenseAqualityDmaintenanceEnables AI assistants to look up medical billing codes, denial reasons, and payer rules for faster claim resolution.66MIT
- AlicenseNot gradedqualityBmaintenanceEnables agents to search, look up, validate, and cross-translate Brazilian health terminology codes (CID-10, CIAP-2, TUSS, SIGTAP, ANVISA, CBO, LOINC, UCUM) returned as FHIR concepts, with structured handling of key, access, quota, network, and server errors.Apache 2.0

KeepMD EOB Decoderofficial
AlicenseNot gradedqualityBmaintenanceEnables AI assistants to look up ICD-10-CM and HCPCS codes, explain insurance and billing terms, verify EOB arithmetic, and access guides for reading medical bills and explanations of benefits.MIT