phenoforge.mcp

The thin MCP server. Tool registration only — every tool here delegates directly to a phenoforge.engine function (AGENTS.md’s thin-server rule).

phenoforge.mcp.server

Thin MCP server exposing the engine’s lookup, expansion, and retrieval.

Tool registration only — no business logic (AGENTS.md). Every tool here delegates directly to a phenoforge.engine function; if a change requires new logic rather than a new call, it belongs in engine, not here.

Transport is flag-controlled: stdio by default, matching what Claude Desktop and other MCP clients expect today with no hosting step, but switching to HTTP later only requires a different –transport value, not a rewrite.

phenoforge.mcp.server.configure(db_path, *, library_dir=<object object>, index_path=<object object>, dense_embed_fn=<object object>)[source]

Point the server at a different vocabulary database, phenotype library, and/or dense index.

Resets any already-open connection and cached retrievers so the next tool call rebuilds them against the new path(s). Intended for tests; the console entry point never needs to call this.

Parameters:
  • db_path (Path) – Path to a DuckDB database built by scripts/load_vocab.py.

  • library_dir (Path | None) – Path to a directory of fetched cohort JSON files built by scripts/fetch_phenotype_library.py. Omit to leave unchanged; pass None explicitly to reset to the module default.

  • index_path (Path | None) – Path to a LanceDB index built by scripts/build_index.py. Omit to leave unchanged; pass None explicitly to reset to the module default.

  • dense_embed_fn (Callable[[list[str]], list[list[float]]] | None) – Embedding function for the dense retriever. Tests pass a fake embedder here so they never download BioLORD-2023. Omit to leave unchanged; pass None explicitly to reset to the real default embedder.

Return type:

None

phenoforge.mcp.server.lookup_concept(concept_code)

Look up a single ICD-10-CM concept by its exact billing code.

Use this when the caller already has a specific code, e.g. "E11.21", and wants its name and OMOP metadata. Not a search tool — for free-text clinical terms use search_concepts instead.

Parameters:

concept_code (str) – An exact ICD-10-CM code, e.g. "E11.21".

Returns:

The matching concept, or None if no ICD-10-CM concept has that code.

Return type:

Concept | None

phenoforge.mcp.server.expand_hierarchy(seed_code)

Expand an ICD-10-CM code to every code beneath it in the billing hierarchy.

For example, expanding "E11" (Type 2 diabetes mellitus) returns all of its more specific subtypes, such as "E11.21" (with diabetic nephropathy). Every returned concept is tagged generated provenance: it is a structural consequence of the vocabulary hierarchy, not a clinically validated inclusion, and should be treated as ungrounded until a human confirms it belongs in the target population.

Parameters:

seed_code (str) – An exact ICD-10-CM code to expand from, e.g. "E11".

Returns:

All descendant concepts, or an empty set if seed_code does not exist or has no descendants.

Return type:

ConceptSet

phenoforge.mcp.server.search_concepts(query, k=10)

Search ICD-10-CM concept names for a free-text clinical term or phrase.

Combines lexical (BM25) and semantic (embedding) matching, fused by reciprocal rank, so paraphrased or loosely-worded descriptions (e.g. “sugar disease” for diabetes) are found even without shared exact wording — always prefer this over guessing at exact terminology yourself. Every returned concept is tagged generated provenance and should be treated as ungrounded until a human confirms it.

Parameters:
  • query (str) – Free-text search string, e.g. "diabetic nephropathy".

  • k (int) – Maximum number of results to return.

Returns:

Fused, deduplicated results ordered by combined relevance. If nothing matches, concepts is empty and unmappable explains why.

Return type:

ConceptSet

phenoforge.mcp.server.find_curated_definition(query)

Search the OHDSI Phenotype Library for a validated cohort definition.

Use this FIRST for any population description that plausibly matches a peer-reviewed phenotype (e.g. “type 2 diabetes”, “diabetic ketoacidosis”) — a match here is curated provenance, safe to use as-is citing the cohort id. Only fall back to search_concepts/expand_hierarchy if nothing matches; those return generated provenance requiring human confirmation. This demo bundles a small, hand-picked set of cohorts spanning several disease areas, not the full ~1,100-cohort library — a miss here does not mean no curated definition exists.

Parameters:

query (str) – Free-text population description.

Returns:

The best-matching cohort’s resolved ICD-10-CM concepts tagged curated, or an empty set with an explanatory unmappable entry if nothing in the bundled set matches or the library has not been fetched yet.

Return type:

ConceptSet

phenoforge.mcp.server.explain_inclusion(concept_code, query=None, seed_code=None)

Explain why a specific code would be included in a result, via which path.

Nothing from a prior search_concepts/find_curated_definition/ expand_hierarchy call is kept server-side, so this re-runs whichever of those a caller would have made and reports whether concept_code actually comes back — pass query to check the curated and free-text search paths, seed_code to check the hierarchy-expansion path, or both to check everything at once. Useful for auditing a generated candidate before confirming it, or for double-checking why a curated code is in a cohort.

Parameters:
  • concept_code (str) – An exact ICD-10-CM code to explain, e.g. "E11.21".

  • query (str | None) – Free-text population description or clinical term, to check the same paths find_curated_definition/search_concepts would use.

  • seed_code (str | None) – An exact ICD-10-CM code, to check the same hierarchy path expand_hierarchy would use.

Returns:

Every path that actually produced concept_code; empty paths if none did, including if the code doesn’t exist.

Return type:

InclusionExplanation

phenoforge.mcp.server.main()[source]

Entry point for the phenoforge-mcp console script.

Raises:

FileNotFoundError – If data/vocab.duckdb has not been built yet via scripts/load_vocab.py.

Return type:

None