Conversation
Stage 1 of KEGG support: the pure logic layer. stdlib only, no network, no FastMCP — the counterpart of togovar._build_variant_query, so the part that carries the real complexity is unit-testable without HTTP (39 tests). Handing raw KGML to an LLM is close to useless: it is coordinate-heavy XML whose edges reference drawing-box ids rather than biological identifiers. What this turns it into — a SIGNED directed graph over resolved identifiers — is a shape RDF Portal cannot produce, since Reactome RDF has no equivalent of KGML's activation/inhibition relation subtypes. Eight ways a naive parser gets this wrong are handled. Six are derivable from the DTD; 7 and 8 only appeared when real maps were run through it (the synthetic fixture passes both), and neither shows up in edge counts — only in connectivity: 1 entry/@name is a space-separated LIST (a naive parse drops 23-76% of ids) 2 type="group" is a complex; relations point at the group, not its members 3 ECrel's compound @value is an ENTRY ID, not a compound accession 4 maplink / type="map" are pointers to other maps, not interactions 5 reaction/@id is the ENZYME's entry id; @name is the reaction. reversible means both directions 6 graphics/@type="line" entries are rendering artifacts, not molecules 7 KGML NEVER JOINS the enzyme layer (ECrel) to the compound layer (reaction), so a metabolic map parses disconnected by construction — bridged with explicit substrate->enzyme->product catalysis edges (ko00010: 2 components -> 1) 8 a map is a DRAWING: one molecule is drawn wherever needed, each drawing getting its own entry id (hsa05200: 54 duplicates, 49 -> 32 components once merged; merging never split a component across six real maps) Sign handling keeps MECHANISM and DIRECTION in separate fields, because inhibitory phosphorylation is routine: activation/expression = +1, inhibition/repression = -1, everything else 0. A relation carrying both activation and inhibition collapses to 0 rather than last-writer-wins. metabolic_gaps() is a RESULT, not a bug: an organism map keeps the reference layout and leaves the steps that organism lacks as bare ortholog boxes. Confirmed arithmetically, twice, counting entry boxes — ko00010 reactions 63 - hsa00010 34 = 29 = hsa00010's isolated ortholog boxes (63 - 35 = 28 for eco00010). Under the default duplicate-merge these are 25 and 23, i.e. distinct missing steps rather than boxes; both numbers are correct and the docstring now says which is which. The fixture is SYNTHETIC and contains no KEGG-derived content: the KEGG API is licensed to academic users at academic institutions, so no KEGG data is committed and no test touches rest.kegg.jp. scripts/kgml_probe.py validates against real maps but only fetches with an explicit --fetch (3 req/s, cached to a gitignored kgml_cache/), to be run from an institutional network. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 2: the tool surface. Six tools (kegg_find, kegg_get_entry,
kegg_pathway_graph, kegg_pathway_neighborhood, kegg_link, kegg_conv) wrapping
rest.kegg.jp, kept deliberately thin — all graph logic stays in kgml.py, where
it is tested without HTTP. Module conventions follow togovar.py: raise
ValueError on bad params AND on HTTP error, never an {"error": ...} payload, no
SPARQL-fallback hint (KEGG has no RDF Portal endpoint), JSON-string bare arrays
for list returns, READ_ONLY_TOOL on every tool.
STDIO ONLY, AND THE GATE IS STRUCTURAL. The KEGG API is licensed "for academic
use by academic users belonging to academic institutions", and providing a
service on top of KEGG needs a separate academic service-provider licence. The
public HTTP host cannot verify a caller's affiliation, so setup() mounts the
sub-server only under setup(local=True), reached from run_local(). This is
NOT an env flag on purpose: deploy.sh forwards env vars by a fixed list
(TOGOMCP_PERSERVICE_VARS / TOGOMCP_SHARED_VARS), and a knob missing from that
list is silently inert in production — that failure happened twice in one week
(2026-07-29), both times silently, both times with a green test suite. A
licensing boundary must not be one forgotten list entry away from opening.
tests/test_kegg.py asserts both halves of the gate against a fresh root server.
Raw KGML is never returned. kegg_pathway_graph ships the normalized graph plus
the two numbers that stop an agent over-reading it:
* signal_quality.signed_edge_fraction — how much of the map states a
direction of regulation at all. It swings from 0.98 (hsa04151) to 0.40
(hsa04010) to 0 (metabolic maps), because most KGML relations record only a
MECHANISM (phosphorylation, binding/association). net_sign 0 therefore means
UNKNOWN, never "no effect".
* component_count / largest_component — fragmentation is expected, not a
parse failure: hsa05200 wires its sub-modules through 22 cross-map
pointers, which are excluded because treating them as edges would fabricate
interactions, leaving ~32 pieces.
metabolic_gaps rides along on every map, flagged as a result rather than an
error. expand_members is refused above the edge cap rather than silently
returning a fraction of a combinatorial blow-up (one entry box is a whole
paralog family, so an edge becomes the product of member counts); an oversize
map degrades to its highest-degree core with an explicit `truncated` block,
while `stats` keeps describing the full map.
Transport details that are easy to get wrong: the 3 req/s cap is a PROCESS-wide
budget, so the limiter is one shared gate rather than per-tool (an LLM session
spreads dozens of calls across six tools); KGML is memoized in-process, so one
map costs one request however many times it is queried; KEGG answers "no match"
with an EMPTY HTTP 200 rather than a 404, which is handled explicitly; and
403/429 is surfaced as the licence/rate signal and never retried, since
retrying it is what gets an address blocked.
kegg_conv is the bridge to RDF Portal and returns prefix-stripped
source_id/target_id next to the KEGG-namespaced forms — KEGG ids do not resolve
in run_sparql, and an agent that does not know that will query `hsa:10458` and
get zero rows. The Usage Guide's Database Catalog now carries a KEGG section
saying so; because that file is generated from MIE discovery blocks and
byte-compared by a drift guard, the section lives in the generator. No MIE and
no endpoints.csv row: KEGG has no SPARQL endpoint, and the MIE format describes
a SPARQL schema.
No test touches rest.kegg.jp — HTTP is mocked with respx and every KGML input
is the synthetic fixture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds kegg_pathway_paths and kegg_pathway_cycles, the two graph queries kgml.py
already implemented but the tool surface did not reach. Feedback-loop detection
is much of the argument for carrying KEGG at all: a directed cycle IS a feedback
loop, and the product of its edge signs says whether it is negative
(self-limiting) or positive (self-reinforcing, switch-like) — a structural claim
about a pathway that no keyword search surfaces and that RDF Portal cannot
answer, since Reactome RDF has no equivalent of KGML's relation subtypes.
Both wrappers stay thin; the traps they close are in the tool layer because they
are about how a caller reads the result:
* find_paths() returns [] both when an endpoint matched NOTHING and when the
endpoints are fine but unconnected. Those demand opposite reactions, so the
tool resolves the endpoints itself and reports `unresolved` (a lookup
failure, biologically meaningless) separately from `no_path_note`.
* the `feedback` filter necessarily runs AFTER the `max_cycles` cap, so on a
dense map the cap can fill with cycles that the filter then removes, leaving
a zero that is not a real zero. `truncated` is emitted whenever the cap was
reached and says exactly this.
* on METABOLIC maps `max_length` must be raised: the catalysis bridge puts the
enzyme between substrate and product, so the hop count is about DOUBLE the
reaction count. Glucose -> pyruvate in glycolysis needs ~12 and returns
nothing at the default 6 (measured on ko00010) — documented on the tool.
Also fixes a real defect this exposed. find_paths projected each edge without
its reaction accession, so two DIFFERENT reactions joining the same pair of
metabolites produced byte-identical rows: a caller saw one path apparently
repeated, and the duplicates ate the max_paths budget. Measured on ko00010, rows
2 and 3 of glucose -> pyruvate were R00200 and R00199, two pyruvate-kinase
routes that had been indistinguishable. The accession now rides on every path
edge, which is also the useful annotation for a metabolic route.
resolve_seeds joins kgml.__all__ — it was already documented as part of the
public API and is what lets the tool layer tell the two empty results apart.
Enumeration cost was measured before choosing the bounds rather than guessed:
find_cycles is trivial on real maps (<=3ms at max_length=6 on hsa05200/ko00010,
helped by the nxt > start canonical pruning), while find_paths grows steeply
with max_length on dense metabolic maps (0.145s at length 8 on ko00010), which
is why max_length is capped at 12 and the docstring says to raise it
deliberately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Usage Guide is served by BOTH transports, so the KEGG material added with
the sub-server went out in full to HTTP clients that have no kegg_* tools: six
tools' worth of workflow, bridging and rate-limit instructions for things they
cannot call, led by a sentence asserting KEGG "is reachable through the kegg_*
tools" — false on the public server, and stated before the availability caveat
that followed two bullets later.
Split by audience instead of by wording:
* the catalog keeps a SHORT, transport-neutral note that is true either way —
KEGG is not an RDF Portal database, database="kegg" is invalid, and if you
see no kegg_* tool then KEGG is unavailable in this session, so answer from
reactome/rhea and do not report it as an error. Worth keeping for everyone:
without it an agent asked about KEGG invents database="kegg".
* the operating detail moves to usage_guide_v6/local_only/kegg.md and is
appended only when the tools are actually mounted.
The gate reads the LIVE tool registry rather than a flag or an env var, so the
guide cannot disagree with what the server really exposes — the same reasoning
that made the mount itself structural. The conditional parts sit in a
SUBDIRECTORY, which the guide's top-level sorted(glob("*.md")) cannot reach, so
they can never be served by accident; a test pins that too.
The first version of this gate was wrong in a way worth writing down: it assumed
mcp.get_tool() RAISES for an unknown tool. It returns None. The try/except
therefore never fired, the condition was always true, and every HTTP client
still received the stdio-only section — precisely the bug the change existed to
prevent, and invisible unless you diff the two transports' output. Now checked
explicitly, with the except kept for a future FastMCP that raises instead.
Verified end to end rather than by assertion alone: remote assembles 41,143
chars with no kegg_find/kegg_conv anywhere, stdio 43,299; removing the None
check makes two tests fail; and the new local_only/ directory does ship in the
wheel (checked against `uv build`, since a data file missing from package-data
would be silent in production).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… primitive
Acting on a user observation from real use: canonical feedback loops (p53-MDM2,
mTOR-S6K-IRS) come back as 0 cycles. Verified against the cached maps, and the
cause is more specific than "layout artifacts" — it is a MISSING EDGE in the
data, which no parser change can recover.
On hsa05200 both molecules are present and correctly merged (TP53 absorbs entry
317, MDM2 absorbs 318, so pitfall-8 merging IS working). MDM2 -| TP53 is drawn
with sign -1. TP53 -> MDM2 is simply not drawn: TP53's six outgoing edges all go
to downstream effectors (CDKN1A, GADD45G, BAK1, POLK, BAX, DDB2) and the
induction arm lives on hsa04115. A KEGG map is a DRAWING of one process, not a
complete interaction model, so the loop cannot close here at any depth.
Merging is load-bearing in the other direction and worth recording: hsa05200
gives 3 cycles merged and 0 unmerged, so duplicate drawings do destroy cycles —
that mechanism is real, it is just not what defeats p53/MDM2.
The measured picture across all six maps, which is what justifies the change:
map signed_frac cycles neg pos unsigned
hsa04151 0.98 0 0 0 0
hsa04010 0.40 4 0 0 4
hsa05200 0.80 3 0 0 3
hsa00010/ko/eco 0.00 500+ 0 0 500+
ZERO signed cycles anywhere — including a map that is 98% signed. So the tool's
headline promise (negative vs positive feedback) is essentially never delivered,
and its normal answer is "nothing", whose obvious reading is the wrong one.
* the docstring now leads with that, and points at kegg_pathway_paths as the
robust primitive: net_sign needs only a PATH, not a closed loop, so it
returns the MDM2 -| TP53 inhibition the cycle search cannot see.
* the payload carries an `interpretation` block, because empty is the RULE
here and a caller should not have to re-read a docstring to avoid reporting
"this pathway has no feedback".
* find_cycles marks reversible-reaction 2-cycles (A<->B is a cycle by
construction, not feedback: 82 of ko00010's 102 two-cycles) and the tool
drops them by default.
That last flag is documented as the small cleanup it is, not as a rescue. The
first draft of this commit claimed it cleaned up metabolic maps; measuring the
actual distribution showed it removes 69 of 5,001 cycles at depth 6, because
find_cycles returns mostly LONG cycles and the 82/102 figure holds only at
max_length=2. Cycle enumeration on a metabolic map is meaningless regardless —
no signed edges exist there — so the guidance is to not ask.
The guide gains the corresponding rule, plus the sharper framing of what KEGG
adds at all: edge SIGN and organism-specific ABSENCE. A question needing neither
is answerable from RDF Portal alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asked to justify KEGG's unique value, I checked the claim I had repeated in five
places instead of repeating it a sixth time. It is wrong.
Reactome RDF carries 61,819 signed BioPAX controls (57,350 ACTIVATION, 4,469
INHIBITION), verified against the live endpoint. "Reactome RDF has no equivalent
of KGML's activation/inhibition subtypes" was simply false, and it was load-
bearing: it is the sentence an agent would read when choosing between KEGG and
run_sparql.
The real difference is one of LEVEL, and it is narrower but defensible:
* Reactome's sign is on a REACTION — "does entity X promote this reaction?".
MDM2's repression of p53 is stored as ACTIVATION of "MDM2 ubiquitinates
TP53". Recovering "MDM2 inhibits TP53" requires knowing that ubiquitination
leads to degradation, i.e. domain inference the RDF does not supply.
* KGML's sign is on the NET EFFECT BETWEEN MOLECULES: MDM2 -| TP53, stated
outright.
The second claimed value survives untouched and is actually the stronger one:
Reactome holds 15 species, every one a eukaryote, so E. coli is absent from it
entirely. metabolic_gaps has no counterpart anywhere in RDF Portal — and, unlike
the signed-edge argument, it does not involve signs at all.
(Finding this needed the literal-typing trap first: biopax:controlType is
xsd:string-typed, so a plain-literal match returns zero rows silently.)
Corrected in kgml.py, kegg.py (three docstrings), the Usage Guide catalog and
the CHANGELOG. The guide now also tells an agent when NOT to reach for KEGG: if a
question needs neither the between-molecule net effect nor organism-specific
absence, reactome/rhea over SPARQL is the better route.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t as pathways
Second correction in a row, and the same methodological mistake: I asserted a
negative ("Reactome has no prokaryotes") from a query whose restrictions I did
not state. The query required both `a biopax:Pathway` and `biopax:name`, so it
returned 15 rows and I read that as the organism roster.
The graph actually names 64 organisms, ~20 of them bacteria, E. coli included.
What survives, checked properly this time: no prokaryote owns a single
biopax:Pathway. Exactly 15 organisms do and every one is a eukaryote. The
bacteria are present only as reference entities — E. coli is 101
ProteinReference + 1 RnaReference and zero reactions — i.e. pathogen molecules
participating in HUMAN infection pathways, hanging off the human namespace
(48887#BioSource*).
So "which glycolysis steps does E. coli lack" is still unanswerable in Reactome,
but for a different reason than I gave: not that the organism is missing, but
that no prokaryote has a pathway reconstruction to compare against. Corrected in
kgml.py and the Usage Guide.
Both wrong claims were found only because they were challenged. Neither would
have been caught by the test suite — they were assertions about an external
endpoint embedded in prose that ships to agents as tool documentation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ism-coverage scoping Revised togo_mcp/data/mie/reactome.yaml via the mie-generator skill, prompted by my own two wrong answers about Reactome earlier in this session. Both were avoidable from the endpoint, and the second one is a direct indictment of where the first fact was filed. THE REGULATION FACT WAS ALREADY IN THE FILE — AS PROSE, IN schema_delta: "Catalysis/regulation: bp:Catalysis (bp:controller -> bp:controlled) with bp:controlType exactly "ACTIVATION"|"INHIBITION"^^xsd:string" I read that file and still concluded Reactome had no signed regulation. This is the ablation result in miniature: the leave-one-in sweep found the query group alone recovers ~99% of the whole-MIE effect, while guardrail prose alone was not significant. A fact an agent must first translate into SPARQL is not the same asset as a query it can copy. So the finding moved to where it works. Worse, the prose was WRONG in a way a verified example cannot be. bp:Catalysis is 100% ACTIVATION (50,692/50,692) — BioPAX defines catalysis as inherently activating — and ALL 4,464 meaningful INHIBITIONs live in bp:Control. The line told a reader hunting inhibition to anchor on the one class that has none, which returns zero rows silently. Deleted (§4.2: a predicate an example demonstrates does not also live in schema_delta). Two new examples, both re-run live 2026-07-31: signed_regulation (13 rows) — MDM2's controls via UniProt anchor. Carries four measured traps: the Catalysis/Control split above; controller is a Complex 2,473x vs bare Protein 1,040x, so `?ctrl a bp:Protein` or a missing (bp:component|bp:memberPhysicalEntity)* path drops 54% of rows (13 -> 6, and the TP53 row is among the lost); ^^xsd:string on bp:db/bp:id; and bp:controlled polymorphism (BiochemicalReaction 4,455 + Degradation 9). Its `teaches` states the thing I actually got wrong: the sign is on a REACTION, so "MDM2 ubiquitinates TP53" is ACTIVATION while its effect on TP53 is repression. Reading the sign without the reaction inverts the conclusion. enum_pathway_organisms (15 rows) — which organisms actually own pathways. Defuses the second error: 57 bp:BioSource NAMES (64 IRIs) is not 57 queryable organisms. Exactly 15 own a bp:Pathway and all are eukaryotes. Bacteria ARE present (~20 taxa incl. E. coli) but only as reference entities in HUMAN infection pathways — E. coli is 101 ProteinReference + 1 RnaReference and zero reactions, minted under the human namespace. A prokaryote pathway query returns 0 rows with no error. entity_counts gains pathway_owning_organisms, signed_controls and inhibition_controls; discovery's "across 57 species" — the figure that seeded the second error — becomes "pathway reconstructions for 15 eukaryotic organisms". Validation: 11/11 examples live (check_mie_examples.py: 0 zero-row, 0 error), no `on:` YAML trap, every cited predicate confirmed against the endpoint, no benchmark leakage (MDM2/Q00987/p14ARF absent from benchmark/questions), catalog regenerated. examples byte share 70.0% -> 75.7% (+5.7 pts) — the revision bought queries, not guardrails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ually made
The KEGG section itself already existed (added with the sub-server), but it sat
120 lines below the two places a reader actually decides to enable KEGG, so it
was easy to miss:
* Configuration / Claude Desktop — the config block tells you to run
`togo-mcp-local`, which IS the command that mounts the kegg_* tools. Now
carries a short note (academic users at academic institutions, 3 req/s) with
a link to the full section.
* License — said "MIT License" and stopped. True of the code, but a reader can
reasonably infer the data access is unencumbered too. Now states that MIT
covers this code only, that each API carries its own terms, and names KEGG as
the case that needs an academic service-provider licence to redistribute —
which is why the hosted server does not expose it.
Also lists kegg.py and kgml.py in the directory tree, which had gone stale when
they were added.
Both new cross-links verified to resolve against the real GitHub heading anchor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…users can run local
KEGG was mounted for anyone running togo-mcp-local. The licence covers academic
users at academic institutions, so that put an API call a user may not be
entitled to make on the path of least resistance — and an LLM will use any tool
it can see, so the user need not even realise KEGG was called. Now it needs an
explicit opt-in, and a non-academic user just leaves the variable unset and loses
nothing else.
http + TOGOMCP_ENABLE_KEGG=1 -> 0 kegg tools
stdio + unset -> 0 kegg tools
stdio + TOGOMCP_ENABLE_KEGG=1 -> 8 kegg tools
The two gates answer different questions and compose with AND. The TRANSPORT gate
stays structural and is not configurable: the public host cannot verify a
caller's affiliation, so no environment setting may put KEGG on the HTTP surface
(pinned by a test that tries "1"/"true"/"YES"/"on" against local=False). The
OPT-IN records eligibility, which only the person running the process can judge.
This reverses an explicit prohibition in the KEGG handoff ("❌ env フラグでゲート
する"), so the reasoning is worth recording. That rule exists because deploy.sh
forwards env vars by a FIXED LIST and a knob missing from it is silently inert in
production — twice in one week, both with a green suite. But that hazard is
entirely about a knob whose ABSENCE leaves a boundary OPEN. This one is inverted:
* FAIL-CLOSED — absent, empty, or misspelled ("ture") all mean OFF, so a
forwarding miss DISABLES KEGG. Parametrised over falsy and malformed values.
* ANDed BEHIND the transport gate — the variable cannot affect the HTTP path,
so deploy.sh never enters the picture at all.
Deliberately NOT added to compose.yaml, .env.example, or the deploy.sh forwarding
lists: listing a variable that cannot do anything on the HTTP path would imply it
can. The handoff doc now records both the reversal and the two conditions that
must not be broken.
The Usage Guide needed no gate change — it already keys off the LIVE tool
registry, so the KEGG section follows the mount automatically. Its shared note
now states the opt-in and tells the agent NOT to prompt the user to enable it:
eligibility is the user's to assert, not the assistant's to sell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tool-search
A deferred-tool client ranks by DESCRIPTION and loads only the top few, so a tool
that does not rank is effectively uncallable. Five KEGG descriptions each named
`run_sparql` — good boundary documentation, but it put five decoys on exactly the
query someone types to FIND run_sparql. Reported symptom: searching `run_sparql`
returned only KEGG tools, and reaching the real one took seven searches.
Now exactly one names it: kegg_conv, the RDF bridge, where the precise tool name
IS the payload ("KEGG ids do not resolve there — convert first"). The other four
say "not RDF-resolvable" / "any downstream RDF query" / "an RDF query", which
carries the same instruction without the colliding token. Also drops the repeated
"SPARQL endpoint" / "RDF Portal" boilerplate: 5 -> 1 for `run_sparql`, and 0
elsewhere for the other two.
SCOPE, HONESTLY: I could not reproduce the symptom before changing anything.
Searching `run_sparql` now returns the real tool at ranks 1-3 and no KEGG tool at
all; `get_MIE_file` likewise. The reason is that the three registered servers all
run builds WITHOUT KEGG, so the decoys are not in the searchable set — the report
matches the window between 66c2150 (KEGG mounted on stdio) and 08fd91b (opt-in,
default off), which already removes the collision for anyone who has not opted
in. The change is therefore forward-looking: it protects the academic user who
DOES opt in, and it is cheap. But it has no measured before/after, and I am not
claiming one.
NOT DONE, with reasons rather than silence:
* eager/non-deferred core tools — deferral is decided by the client, not by the
MCP server; there is no annotation this repo can set to demand it.
* a tool-search regression test — the ranking layer is client-side and this
repo cannot query it. Added a LEXICAL guard instead (only kegg_conv may name
run_sparql), which pins the intent without pretending to test the search.
* get_MIE_file description — already opens with the spelled-out "Metadata
Interoperability Exchange" and names ShEx/schema/SPARQL, so the proposed
P2 wording is already in place; changing it would be churn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverses the exception I made one commit ago. That commit deliberately kept the `run_sparql` token in kegg_conv, on the argument that the bridge tool needs the precise name, and pinned it with a test asserting exactly `== ["kegg_conv"]`. It was a decision, not an omission — and testing showed it was the wrong one: kegg_conv then remained the SOLE KEGG tool surfacing across all three probe queries, i.e. the entire residual collision was that one exception. The argument does not survive the evidence. What a caller must learn from kegg_conv is "convert before any RDF query", and that survives periphrasis intact; the literal tool name was never the payload. So the exposed description now carries zero of `run_sparql` / `SPARQL` / `RDF Portal` / `reactome`, matching the other seven. What is deliberately NOT stripped: uniprot / chebi / pubchem / ncbi-geneid. Those are the namespaces kegg_conv actually converts between — its API vocabulary, not decoy text. Removing them to shave token overlap would leave the tool undocumented, which is a worse failure than a search collision. A second test now guards that floor, so the rule above cannot be over-applied later. Final audit over the exposed descriptions of all 8 KEGG tools: run_sparql 0, SPARQL 0, RDF Portal 0, reactome 2 (both in the signed-regulation comparison, where naming Reactome is the point). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump + uv.lock in the same commit, and promote the accumulated [Unreleased] section to a dated [2.4.0] heading with its compare link. MINOR under the agent-pragmatic policy: this adds a tool group (8 KEGG tools) and new MIE examples; nothing is removed or renamed. Note the release is NOT only the stdio-gated KEGG work — togo_mcp/data/mie/ reactome.yaml and the generated Usage Guide catalog ship to every hosted-server user, which is why the version has to move rather than riding along unbumped. No `whatsnew:` marker: KEGG is stdio + opt-in and so invisible to the hosted audience the intro page addresses, and the Reactome MIE work is a correctness fix rather than a user-facing highlight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 2.4.0. MINOR under the agent-pragmatic policy: adds a tool group and new MIE examples, removes/renames nothing.
KEGG — a new tool group, opt-in and stdio-only
Eight tools (
kegg_find,kegg_get_entry,kegg_pathway_graph,kegg_pathway_neighborhood,kegg_pathway_paths,kegg_pathway_cycles,kegg_link,kegg_conv) mounted only when both the local stdio server is used andTOGOMCP_ENABLE_KEGG=1.The two gates answer different questions and compose with AND:
1/true/YES/onagainstlocal=False.deploy.shforwarding miss disables rather than enables. Deliberately not added tocompose.yaml/.env.example/ the deploy forwarding lists.What it actually adds
togo_mcp/kgml.pyturns KGML into a signed directed graph — pure, stdlib-only, 40 tests, no network. Eight documented traps are handled; six come from the DTD, and two were found only by running real maps (both invisible in edge counts, visible only in connectivity): KGML never joins the enzyme layer to the compound layer, and one molecule is drawn many times with different entry ids.metabolic_gapsreports the steps an organism lacks — confirmed arithmetically againstko00010/hsa00010/eco00010.Reactome MIE — this part ships to hosted-server users
Two new verified examples, and a deleted line that was actively wrong:
signed_regulation— Reactome carries 61,819 signed BioPAX controls.bp:Catalysisis 100% ACTIVATION (50,692/50,692); all 4,464 meaningful INHIBITIONs arebp:Control. The oldschema_deltaprose pointed inhibition queries atbp:Catalysis, which returns zero rows silently. The controller is a Complex 2,473× vs a bare Protein 1,040×, so omitting(bp:component|bp:memberPhysicalEntity)*drops ~54% of rows.enum_pathway_organisms— 57 BioSource names is not 57 queryable organisms. Exactly 15 own a pathway, all eukaryotes. Bacteria are present but only as reference entities inside human infection pathways (E. coli: 101 ProteinReference, zero reactions), so a prokaryote pathway query returns 0 rows with no error.11/11 examples re-run live;
examplesbyte share 70.0% → 75.7%.Also
database="kegg".kegg_pathway_cyclesno longer oversells: measured across six real maps it finds zero signed cycles, because a KEGG map is a drawing and canonical loops routinely have an arm on another map. The payload now says so, and points atkegg_pathway_pathsas the robust primitive.run_sparql, which was crowding the real tool out of client tool-search results.Verification
rest.kegg.jp(respx-mocked, synthetic KGML fixture).serverInfo.versionreports 2.4.0.whatsnew:marker — KEGG is invisible to the hosted audience the intro page addresses.After merge: tag the merge commit —
git tag v2.4.0 <merge-sha> && git push origin v2.4.0.🤖 Generated with Claude Code