agentleFS
dbclsdbcls/togomcp/CLAUDE.md

togomcp

CLAUDE.md21 starsChanged 2 months agoMITView on GitHub

FastMCP server exposing the RDF Portal (SPARQL — most databases; seven run their own endpoints, see endpoints.csv) plus selected REST APIs (NCBI E-utilities, UniProt, ChEMBL, PDB, PubChem, Reactome, Rhea, MeSH, TogoID, TogoVar) to AI assistants. The server is assembled in main.py: a root FastMCP instance (mcp, defined in server.py) mounts four sub-servers as prefixes (plus kegg, stdio-only and opt-in): A REST wrapper becomes a mounted sub-server (not a flat api_tools.py search_* tool) when the external API is a cohesive multi-endpoint…

  • Reads credentials
  • Sends data out
  • Commits and pushes

The file

Files
CLAUDE.md

TogoMCP

FastMCP server exposing the RDF Portal (SPARQL — most databases; seven run their own endpoints, see endpoints.csv) plus selected REST APIs (NCBI E-utilities, UniProt, ChEMBL, PDB, PubChem, Reactome, Rhea, MeSH, TogoID, TogoVar) to AI assistants.

Architecture

The server is assembled in main.py: a root FastMCP instance (mcp, defined in server.py) mounts four sub-servers as prefixes (plus kegg, stdio-only and opt-in):

  • togoid_mcp from togoid.py — mounted as togoid
  • ncbi_mcp from ncbi_tools.py — mounted as ncbi
  • togovar_mcp from togovar.py — mounted as togovar
  • pubcasefinder_mcp from pubcasefinder.py — mounted as pubcasefinder

A REST wrapper becomes a mounted sub-server (not a flat api_tools.py search_* tool) when the external API is a cohesive multi-endpoint surface with no SPARQL counterpart in RDF Portal — the flat wrappers are keyword-search front doors to a SPARQL database (their error hint points back to run_sparql), so a DB without a SPARQL endpoint doesn't belong there. TogoVar (human genome variation: gnomAD/ToMMo/JGA/BBJ frequencies, ClinVar+MGeND significance) fits the sub-server shape, like TogoID and NCBI. PubCaseFinder is the other valid route in: it DOES have an RDF counterpart (database="pubcasefinder"), but the two tools wrap what the RDF cannot give — a computed phenotype-similarity ranking and a text-mined case-report index — so they are not keyword front doors either. Wrap only what SPARQL cannot reproduce; the rest of its REST API is SPARQList over the same RDF.

Tools registered directly on the root mcp live in rdf_portal.py (SPARQL, MIE files, endpoint resolution) and api_tools.py (REST search wrappers).

Entry points (from pyproject.toml):

  • togo-mcp-server — HTTP transport on 0.0.0.0:8000
  • togo-mcp-local — stdio transport (Claude Desktop)

Data layout

Bundled under togo_mcp/data/ and shipped in the wheel via package-data:

  • mie/*.yaml — one MIE (Metadata-Interoperability-Exchange) file per supported RDF database
  • resources/endpoints.csv — SPARQL endpoint registry consumed by load_sparql_endpoints
  • resources/usage_guide_v7/ — the Usage Guide, tiered. Top-level *.md parts are assembled (sorted, plus the generated Workflows list) into the core that TogoMCP_Usage_Guide returns; references/*.md are the on-demand sections, served by get_workflow(name="usage-guide", path="references/<file>"); local_only/ holds parts gated on tools that are not on every transport. The core must stay small enough to arrive inline (Claude Code saves a result past 50,000 characters or 25,000 tokens to a file — the 57 KB v6 guide reached no Sonnet 5.5 session whole); tests/test_usage_guide_size.py enforces 40,000. The core must also stand alone: a client with a tool list older than get_workflow cannot fetch a reference file. 02b_database_catalog.md (compact) and references/database-catalog.md (full) are GENERATED by scripts/generate_usage_guide_catalog.py — never hand-edit them. The directory name carries the guide version.
  • docs/ — developer docs (MIE spec, examples)
  • skills/public/<name>/ — Agent Skills served by get_workflow and as skill:// resources (registry: skills.py; loaded at startup, a malformed skill fails the boot). .claude/skills/<name> for these are symlinks into here. Developer skills (mie-generator, qa-generator, intro-page-updater) stay real directories in .claude/skills/ and must never be put under public/.

Parameter conventions

These conventions were deliberately normalized across tools; preserve them when adding new tools:

  • database is the canonical parameter name for RDF-database selection. dbname is deprecated — do not add it to new tools. Only get_MIE_file in rdf_portal.py still accepts dbname/db as legacy aliases (its database has a default, so alias-only calls resolve). run_sparql/get_graph_list dropped these aliases: database is schema-required there, so alias-only calls were already rejected by validation and the fallback was dead code.
  • database vs endpoint_name: database takes a single RDF database key (e.g. uniprot, chembl); endpoint_name takes an endpoint group (e.g. ebi, sib). Combining a valid database with a valid endpoint_name is the supported cross-DB/federated pattern (see the next bullet) — what fails is an invalid database/endpoint_name value, which returns a deterministic error telling the caller not to retry. Do not add fallback behavior for the invalid case.
  • database is required on run_sparql/get_graph_list. Its params are keyword-only (*,) so the schema required array carries database — many LLMs omit fields that aren't marked required and only fail at runtime, so this is deliberate; prefer a hard required over a oneOf/anyOf soft constraint. Endpoint-selection is still overridable: pass a member database plus endpoint_name/endpoint_url for cross-DB/federated queries (priority endpoint_url > endpoint_name > database). A consequence: endpoint-only calls are rejected by schema validation — always supply database. (These two tools no longer define dbname/db at all, so passing either now errors as an unexpected argument rather than being silently ignored.)
  • ids accepts str | list[str] across NCBI and TogoID tools. Normalize with _normalize_ids (ncbi_tools.py) or _ids_to_csv (togoid.py).
  • Search-tool query aliases: the flat search_* wrappers in api_tools.py accept query/search/term/keyword/keywords/search_term/name, resolved via _resolve_query_alias. This does not cover the mounted sub-server tools — the TogoVar togovar_search_* tools take query directly (no alias set), and the NCBI tools accept the raw E-utilities names instead: db/term/retmax/retstart on esearch, db/id on esummary/efetch.

Return-shape and error conventions

Two intentional, different error conventions coexist — preserve each module's:

  • REST-wrapper tools in api_tools.py (search_uniprot/pdb/reactome/rhea_entity, search_mesh_descriptor, ChEMBL, PubChem): raise ValueError for bad parameters (caught early, tells the caller not to retry) but degrade gracefully on upstream/HTTP failure by returning a payload carrying an error key plus a "fall back to SPARQL" hint. Never raise on a transient HTTP error here. (The list_databases/find_databases/list_categories discovery trio that also lived here was retired at the v3 MIE release — the database catalog moved to a static, generated Usage-Guide section (02b_database_catalog.md); the full database= roster is on the run_sparql/get_MIE_file schema.)
  • TogoID tools in togoid.py: raise on HTTP error via raise_for_status_with_body (with a client_error_hint). This is the module's consistent convention; FastMCP surfaces the message to the caller. Don't convert only some togoid tools to the return-JSON style — keep the module uniform.
  • PubCaseFinder tools in pubcasefinder.py: same raise convention as TogoVar. Two things specific to it: the ranking uses the web app's undocumented /pcf_get_ranking_by_hpo_id because the documented /api/pcf_get_ranked_list returns every candidate with full descriptions (~10 MB, ~11 s per call; it also 404'd during the 2026-09 outage) — read the module docstring before "fixing" the path; and DBCLS limits API use to 10/min, 100/h, 1,000/day, which on the HTTP deployment is ONE budget shared by every user — hence the process-wide sliding-window limiter and the caches. Unknown HPO/MONDO IDs come back upstream as a silent [], so the wrapper validates them.
  • TogoVar tools in togovar.py: same convention as TogoID — raise ValueError on bad params AND on HTTP error (via raise_for_status_with_body); never return an {"error": ...} payload. No "fall back to SPARQL" hint (TogoVar has no RDF Portal endpoint). Keep the module uniform. The variant-query DSL is assembled by the pure helper _build_variant_query (unit-tested without HTTP); the search_variant tool exposes flat filters, not raw nested JSON. Note two live-API quirks the wrapper handles: the client pins Accept: application/json (else the API 501s), and a frequency dataset is an object {"name": ...} (a bare string 500s). Each list row's id is the tgv ID (surfaced as tgv_id), but it is null for most variants, and since the 2026-10 reload those are in SPARQL anyway — so variant_iri is emitted for every row (it was gated on tgv_id while SPARQL was a 2.8× smaller subset). rs/clinvar cross-links come from each row's external_links and genes from genes; the same reload renamed both (from external_link/symbols) and emptied those fields silently, so the wrapper reads either spelling.

List-style result tools return a JSON string of a bare array (not a Python list): empty and non-empty then share one wire shape. Returning a bare list makes FastMCP double-represent it (text array + wrapped {"result": ...}), so empty vs non-empty diverge for clients. dict returns (ChEMBL, search_reactome_entity, get_sparql_endpoints) and NCBI list[TextContent] returns are exempt — they aren't wrapped. search_reactome_entity uses the ChEMBL envelope {total_count, has_more, results} on success and {"error": ...} on failure (it validates+normalizes species/types case-insensitively against vendored vocabularies and raises on an unknown value — the server-side filter is case-sensitive and silently ignores a mis-cased value; limit is a true overall cap, not per-type; summation is opt-in).

Deployment

compose.yaml is NOT the production path. Production and test run on the DBCLS host (vs94) via scripts/deploy.sh — rootless podman (podman run -p, network mode slirp4netns), test-before-prod, promoting the exact tested image without rebuilding. Compose is for local/dev only. deploy.sh is DBCLS-internal and deliberately absent from README.md.

This distinction is written down because assuming Compose was the deployment cost two releases in a row (2026-07-29):

  • forwarded_allow_ips was defaulted to Docker's 172.16.0.0/12, read off compose.yaml. Under rootless podman + slirp4netns the rootlesskit port handler SNATs every inbound connection to the container's own address 10.0.2.100, so the value never matched and X-Forwarded-Proto kept being discarded.
  • deploy.sh forwards a fixed list of env vars (TOGOMCP_PERSERVICE_VARS / TOGOMCP_SHARED_VARS). A knob added to compose.yaml and .env.example but not to that list is inert in production. Add new env vars in all three places.

Both failures were silent, and both passed a green test suite, because the tests asserted the assumption (the Docker range) rather than the environment. Two checks read what a client actually receives and cut through this class of bug — prefer them over local assertions whenever a change has to survive the deploy boundary:

# version actually being served (no session needed; /health returns only "OK")
curl -sS -X POST https://togomcp.rdfportal.org/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'

For tool metadata, capture Mcp-Session-Id from that response, POST notifications/initialized, then tools/list with the session header — that is how the 2.1.0 readOnlyHint annotations were confirmed to reach clients rather than merely to build.

Versioning

The version in pyproject.toml is read at runtime via importlib.metadata and reported in serverInfo.version (the deployment tell — a stale build shows the old number). Bump it on every dev → main release, and sync uv.lock in the same commit.

A dev → main PR that bumps the version is a release. All four steps below are required — do not do only the first:

  1. Bump pyproject.toml + sync uv.lock in the same commit.
  2. Add a dated CHANGELOG.md section (## [x.y.z] - YYYY-MM-DD) and move anything under [Unreleased] that ships in it; add the compare link at the foot of the file. CI enforces the heading (changelog.yml) but not its quality — write for someone diffing two versions, and say why a change matters, not just what moved. If the release has a user-facing highlight (new database/tool, a capability or format change a user would notice), add a <!-- whatsnew: YYYY-MM-DD | one sentence --> marker under the heading and run python scripts/generate_whatsnew.py to refresh the intro page's "What's New" (drift-guarded by whatsnew.yml + tests/test_whatsnew_in_sync.py).
  3. Merge via PR to main (every release since #23 has; gh pr create --base main --head dev).
  4. Tag the MERGE commit — git tag vX.Y.Z <merge-sha> && git push origin vX.Y.Z. Lightweight tags on the merge, not the bump: that is what every existing tag points at. Not every merge is a release — a docs-only merge (e.g. #157) carries no bump and gets no tag.

Steps 2 and 4 exist as rules because they were the ones nobody wrote down: the changelog fell 8 releases and 303 commits behind, and tagging stopped dead after v1.0.1 — while the bump rule, which was written here, was followed every time. Both were reconstructed on 2026-07-17; don't let them drift again.

The "public contract" of this server is the tool surface as a client sees it — tool names, parameters, and return shapes — NOT any importable Python API. Apply semver against that surface, using the agent-pragmatic policy (our dominant client is an LLM that re-reads the tool schema every session and adapts, so a return-shape change is less catastrophic than for a compiled client):

  • MAJOR (x.0.0) — a change an agent genuinely can't recover from: remove or rename a tool, remove a parameter, or make an optional parameter required.
  • MINOR (1.x.0) — new capability, old calls still work: add a tool/database/optional parameter, add a field to a return object, widen accepted input (new alias, case-insensitivity). Return-shape changes ride here too (e.g. bare-list → {total_count, has_more, results}) — that's why the Reactome (1.1→1.2) and Rhea (1.2→1.3) overhauls were minor despite reshaping returns. Strict semver would call those MAJOR; we don't, deliberately, because agents adapt. (Caveat: a purely programmatic client hitting the endpoint can break on a reshape — weigh that if such consumers appear.)
  • PATCH (1.3.x) — behavior fixed, contract unchanged: a bug fix that makes a tool return what it already promised (the chebi:CHEBI: 500, the empty-query guard), docstring/description fixes, a silently-relaxed filter now honored, internal refactors/tests.

Clients cache the tool list; assume months of lag. ChatGPT connectors work from a frozen snapshot of the tool list (taken at Scan Tools / publish time) and never refetch; updating it is plan-specific and mostly an admin task (OpenAI help article, 2026-08) — the 2026-07-27→31 production log carried 28 calls to tools that no longer exist, all from openai-mcp, one cohort still using names retired three months earlier. Two consequences:

  • A rename or removal is MAJOR because the client can't recover, not just the agent. Budget a long tail of calls on the old name, and prefer a deprecation alias over a clean break where the traffic matters. The alias mechanism is _StaleToolNames in server.py: add the old name to _RENAMED_TOOLS (served under the new name with a refresh-your-connector notice, never listed in tools/list) or to _RETIRED_TOOLS (still an error, but one that says what replaced it). Only rename-with-compatible-arguments belongs in _RENAMED_TOOLS.
  • When you ADD a tool, repoint the canaries in the stale-tool-list row of usage_guide_v7/04_reference.md at it. That row is how a stale client finds out it is stale, and a canary only detects caches older than itself — an out-of-date canary silently under-detects. tests/test_usage_guide_canaries.py enforces that the canaries exist (a removed one would fire the row for every healthy client) but deliberately cannot check that they are the newest tools. That part is this checklist's job.
  • Prompts are cached the same way. After the 2.29.0 deploy, Claude Desktop did not show the new record_for_paper prompt until the connector was disconnected and reconnected. A server restart ends the session, the client reconnects on its own, and it keeps the prompt list it already had. That list was empty here, since 2.29.0 introduced the server's first prompt. A prompts/list_changed notification cannot help, because the new prompt exists only after the restart. When a prompt is the user-side trigger for something (as record_for_paper is on Claude Desktop, where tool descriptions and server instructions don't reach the model), its user-facing docs must say "reconnect if it isn't in the menu".
  • Never remove an output schema from a tool that has one. Clients validate each result against the schema they cached at tools/list time, so a stale client rejects every call ("has an output schema but did not return structured content") while the server and a fresh client look perfectly healthy. 2.23.0 did this to TogoMCP_Usage_Guide (output_schema=None, to make a saved result pageable) and a claude.ai connector with a cached 2.21 list could no longer call the guide; 2.23.3 restored it. The reverse is harmless (structured content sent to a client expecting none is ignored), so a schema can be added but not taken away. tests/test_output_schema_stability.py pins the set. A return-shape change is MINOR only while the declared schema still validates the old client's expectations.

Reach differs by kind of change, so weigh it when choosing how to ship a capability: a new database reaches every client immediately (the catalog ships inside TogoMCP_Usage_Guide at query time, and database is a free-form string validated server-side), while a new tool is invisible to any client that has not re-scanned. Where reach matters, prefer a new database or a parameter on an existing tool over a new tool.

Testing

uv run pytest              # all tests
uv run pytest tests/test_server.py -v

tests/test_api_tools.py has a pre-existing failure caused by FastMCP wrapping tool functions as FunctionTool objects (not directly callable from tests). Don't block on it unless explicitly asked.

Running locally

export NCBI_API_KEY="..."   # required for NCBI tools (has a default fallback but rate-limited)
uv sync
uv run togo-mcp-local        # stdio
uv run togo-mcp-server       # HTTP :8000

Editing MIE files

MIE YAMLs in togo_mcp/data/mie/ drive the get_MIE_file tool — the primary schema/example resource LLMs read before writing SPARQL. The corpus is the v3 format; the format spec is MIE_v3_spec.md (it supersedes the retired v2 MIE_file_specs.md). The authoring tooling that implements it is the mie-generator skill under .claude/skills/. When adding a database, also add an entry to resources/endpoints.csv. A served MIE must stay under the inline limit: tests/test_mie_size.py fails a file whose get_MIE_file response exceeds 47,000 characters (Claude Code saves anything past 50,000 to a file, and the model then reads a fraction of it). The served size is not the file size: check: blocks are stripped and a banner is added, but YAML comments ARE served. When a file is over, cut restatement and authoring history, never a verified example.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.