igvf-catalog-mcp 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. igvf_catalog_mcp-1.3.0/.github/workflows/release_to_pypi.yml +35 -0
  2. igvf_catalog_mcp-1.3.0/.github/workflows/test.yml +29 -0
  3. igvf_catalog_mcp-1.3.0/.gitignore +207 -0
  4. igvf_catalog_mcp-1.3.0/CHANGELOG.md +91 -0
  5. igvf_catalog_mcp-1.3.0/EVALUATION.md +27 -0
  6. igvf_catalog_mcp-1.3.0/IMPLEMENTATION_GUIDE.md +370 -0
  7. igvf_catalog_mcp-1.3.0/LICENSE +21 -0
  8. igvf_catalog_mcp-1.3.0/PKG-INFO +175 -0
  9. igvf_catalog_mcp-1.3.0/QUICKSTART.md +145 -0
  10. igvf_catalog_mcp-1.3.0/README.md +157 -0
  11. igvf_catalog_mcp-1.3.0/evaluation/IMPLEMENTATION_GUIDE.md +305 -0
  12. igvf_catalog_mcp-1.3.0/evaluation/README.md +73 -0
  13. igvf_catalog_mcp-1.3.0/evaluation/comparison.md +239 -0
  14. igvf_catalog_mcp-1.3.0/evaluation/llm_query_answers.md +129 -0
  15. igvf_catalog_mcp-1.3.0/evaluation/llm_query_client.py +129 -0
  16. igvf_catalog_mcp-1.3.0/evaluation/mcp_answers.md +129 -0
  17. igvf_catalog_mcp-1.3.0/plans/igvf_catalog_mcp_server_569b6211.plan.md +396 -0
  18. igvf_catalog_mcp-1.3.0/plans/improve_mcp_edge_tools_501efc50.plan.md +177 -0
  19. igvf_catalog_mcp-1.3.0/pyproject.toml +52 -0
  20. igvf_catalog_mcp-1.3.0/skills/disease-genes/SKILL.md +78 -0
  21. igvf_catalog_mcp-1.3.0/skills/dissect-locus/SKILL.md +65 -0
  22. igvf_catalog_mcp-1.3.0/skills/gene-dossier/SKILL.md +58 -0
  23. igvf_catalog_mcp-1.3.0/skills/ld-compare/SKILL.md +65 -0
  24. igvf_catalog_mcp-1.3.0/skills/regulatory-landscape/SKILL.md +63 -0
  25. igvf_catalog_mcp-1.3.0/skills/variant-report/SKILL.md +56 -0
  26. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/__init__.py +6 -0
  27. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/resources/__init__.py +1 -0
  28. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/resources/schemas.py +213 -0
  29. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/server.py +88 -0
  30. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/__init__.py +1 -0
  31. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/api_client.py +167 -0
  32. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/edge_config.py +894 -0
  33. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/formatter.py +84 -0
  34. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/id_parser.py +492 -0
  35. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/services/region_parser.py +274 -0
  36. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/__init__.py +1 -0
  37. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/find_associations.py +558 -0
  38. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/find_ld.py +181 -0
  39. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/get_entity.py +145 -0
  40. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/list_sources.py +213 -0
  41. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/resolve_id.py +379 -0
  42. igvf_catalog_mcp-1.3.0/src/igvf_catalog_mcp/tools/search_region.py +188 -0
  43. igvf_catalog_mcp-1.3.0/tests/__init__.py +1 -0
  44. igvf_catalog_mcp-1.3.0/tests/conftest.py +70 -0
  45. igvf_catalog_mcp-1.3.0/tests/fakes.py +59 -0
  46. igvf_catalog_mcp-1.3.0/tests/test_api_client.py +140 -0
  47. igvf_catalog_mcp-1.3.0/tests/test_contract.py +174 -0
  48. igvf_catalog_mcp-1.3.0/tests/test_edge_config.py +406 -0
  49. igvf_catalog_mcp-1.3.0/tests/test_formatter.py +32 -0
  50. igvf_catalog_mcp-1.3.0/tests/test_id_parser.py +667 -0
  51. igvf_catalog_mcp-1.3.0/tests/test_region_parser.py +342 -0
  52. igvf_catalog_mcp-1.3.0/tests/test_tools/__init__.py +1 -0
  53. igvf_catalog_mcp-1.3.0/tests/test_tools/test_tool_execution.py +2169 -0
  54. igvf_catalog_mcp-1.3.0/tests/test_tools/test_tool_names.py +33 -0
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ # The package version always equals the IGVF Catalog version it was released for.
4
+ # Publishing uses PyPI trusted publishing (environment "pypi"), so no API token is stored here.
5
+ on:
6
+ release:
7
+ types: [published]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ publish-to-pypi:
12
+ runs-on: ubuntu-latest
13
+ environment:
14
+ name: pypi
15
+ url: https://pypi.org/project/igvf-catalog-mcp/
16
+ permissions:
17
+ id-token: write
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: '3.11'
24
+ # PyPI rejects packages whose dependencies are direct (git) references.
25
+ - name: Check the async client is pinned
26
+ run: |
27
+ if grep -q '"igvf-catalog-async-client @' pyproject.toml; then
28
+ echo "::error::pyproject.toml still depends on the async client from git; pin igvf-catalog-async-client==X.Y.Z first"
29
+ exit 1
30
+ fi
31
+ - name: Build
32
+ run: |
33
+ python -m pip install --upgrade pip build
34
+ python -m build
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ python-version: ['3.10', '3.11', '3.12', '3.13']
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - name: Install
18
+ run: |
19
+ python -m pip install --upgrade pip
20
+ pip install -e '.[dev]'
21
+ # dev tracks the in-progress catalog API, so test against the client generated from igvf-catalog@dev
22
+ # instead of the released client pinned in pyproject.toml.
23
+ - name: Install development async client
24
+ if: github.ref_name == 'dev' || github.base_ref == 'dev'
25
+ run: pip install --force-reinstall "igvf-catalog-async-client @ git+https://github.com/IGVF-DACC/igvf-catalog-async-python-client@dev"
26
+ - name: Lint
27
+ run: ruff check
28
+ - name: Test
29
+ run: pytest
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Changed
6
+ - **API calls use the generated async client**: the hand-written httpx client is replaced by a thin wrapper (`services/api_client.py`) around `AsyncIgvfCatalogApi` from [`igvf-catalog-async-client`](https://github.com/IGVF-DACC/igvf-catalog-async-python-client). One client is shared by the server process and closed on shutdown. `IGVF_CATALOG_API_URL` still takes the host; `/api` is appended if missing.
7
+ - **`edge_config.py` names client methods**: each edge endpoint has a `client_method` (e.g. `genes_from_variants`) and a `path` without the `/api` prefix, so `_endpoint_path` and `list_sources` now show paths like `/variants/genes`. Verbose support is read from the generated method signature.
8
+ - **Gene symbols and pathways use the node endpoint parameter names**: `IDParser` returns `name` for gene symbols and `id` for Reactome pathways, which is what `/genes` and `/pathways` accept. `find_associations` maps them to the edge parameters (`gene_name`, …).
9
+ - **Versioning**: the version is read from `src/igvf_catalog_mcp/__init__.py` and follows the IGVF Catalog version (see README). Added the `igvf-catalog-mcp` console script, so the server runs with `uvx igvf-catalog-mcp`.
10
+ - **Dependencies**: removed `httpx` and `pydantic`; `mcp` is pinned below 2.0, whose server API this package does not support.
11
+ - **`get_entity` is paginated**: it accepts `limit` and `page` and returns `{"_pagination": …, "entity_type": …, "entities": [...]}` instead of a plain list, so name lookups that match many entities can be paged through. A limit above the entity type's maximum page size is reported as `limit_capped_to`.
12
+ - **Identifier resolution fetches every page**: resolving an rsID to its alleles (`resolve_id`, and `find_associations` for endpoints without `rsid`) or a UniProt accession to its UniProt names pages through all results instead of using a fixed limit of 100.
13
+ - **`find_associations` reports which entities a name stood for**: when `entity_id` is a name, the response has a top-level `resolved_entities: [{"id": …, "name": …}]`. `IDParser.identify` returns an `Identification` named tuple that also carries the exact and near matches of a gene symbol lookup.
14
+ - **`resolve_id` lists every protein match and picks a representative allele**: protein results have a `proteins` list (`protein_id`, `name`, `uniprot_ids`, `uniprot_names`, `MANE_Select`) and a `note` when an ID matches several proteins, like the `alleles` list of variants. For multi-allelic variants the top-level identifiers now describe the allele with the highest gnomAD allele frequency (e.g. the common C:T allele of rs58542926) instead of the first, the note says which, and each allele carries `gnomad_af_total`.
15
+ - **`find_associations` reports capped endpoints at the top level**: `_pagination.limit` still shows the requested limit, and `_pagination.limit_capped_to: {endpoint: cap}` lists the endpoints whose maximum page size is lower.
16
+
17
+ ### Fixed
18
+ - **Endpoint parameters and filter values re-verified against the catalog OpenAPI spec**:
19
+ - p-value filters send `neg_log10_pvalue` instead of `log10pvalue`. Previously the `p_value` filter was never applied.
20
+ - GO term annotations query `/gene-products/go-terms` (the old `/annotations/go-terms` returned 404).
21
+ - Gene symbol lookups in `get_entity` and `resolve_id` send `name`; previously the symbol was ignored and unrelated genes were returned. Pathway lookups send `id`.
22
+ - Gene edges accept `synonym` instead of `alias`; protein edges accept `uniprot_name` and `uniprot_full_name` instead of `full_name`; ontology terms are sent as `disease_id` (not `disease_name`) to `/diseases/genes`.
23
+ - Removed parameters the API does not have: `chr`/`position` (LD), `GENCODE_category` (variants/drugs), `disease_id`/`disease_name` (variants/diseases, now `assertion` and `pmid`), `files_fileset` (variants/coding-variants), `biosample_id`/`biosample_synonyms` (biosamples/genomic-elements), `biosample_*` (genomic-elements/genes, now `biosample_term`), and `log10pvalue` on variants/proteins and variants/genomic-elements.
24
+ - Source, method and label values match the API enums (e.g. `EBI` instead of `EBI eQTL Catalogue`, `spliceQTL` instead of `splice_QTL`), and are only sent to endpoints that accept them. `AMR` was removed from the LD ancestries, which the API does not support.
25
+ - Gene `regulatory` associations query `/genes/genomic-elements` for E2G predictions and CRISPR screens; `/genomic-elements/genes` only accepts a region, so this part of the query never returned results.
26
+ - Identifier types an endpoint does not accept (e.g. an rsID for `/variants/coding-variants`) are reported in `query_metadata` without calling the API.
27
+ - Fixed existing `ruff check` errors.
28
+ - **`find_associations` returned only the first endpoint's rows**: every endpoint was queried with `limit`, but the combined list was then cut to `limit`, so later endpoints (e.g. `genes_genomic_elements` for a gene's `regulatory` relationship) were never visible on any page. `limit` now applies per endpoint and every fetched row is returned.
29
+ - **Gene symbols detected as UniProt accessions**: any six-character alphanumeric ID (TCF7L2, PNPLA3, VKORC1, CYP2C9, …) was treated as a protein. Proteins are now detected with the official UniProt accession pattern.
30
+ - **Bare UniProt accessions did not resolve**: the API stores isoform IDs (`P49711-1`) as `protein_id`, so `P49711` found nothing. Accessions without an isoform suffix are now looked up through `dbxrefs` in `get_entity`, `resolve_id` and `find_associations`.
31
+ - **`resolve_id` showed one allele of a multi-allelic rsID**: only the first returned variant was used, so e.g. the common C:T allele of rs58542926 was missing. Variant results now include an `alleles` list (SPDI, HGVS, CA ID, ref, alt) with every matching variant, plus a `note` when there is more than one.
32
+ - **`relationship: "coding"` returned nothing for rsIDs**: `/variants/coding-variants` does not accept rsIDs, and the failure was only reported in `query_metadata` (also inside `relationship: "all"`). `find_associations` now resolves the rsID to the SPDI of each allele, queries such endpoints once per SPDI, and marks them with `resolved_from: "rsid"`. Bare UniProt accessions are resolved the same way to UniProt names for `/proteins/complexes`, which accepts `dbxrefs` only alongside `protein_id`, a name, `uniprot_name` or `uniprot_full_name`.
33
+ - **Errors were returned as normal results**: invalid input (bad regions, unknown IDs or relationships, relationships that do not apply to an entity type) and API failures came back as plain text with `isError: false`. Tools now raise `ToolError`, which the MCP server returns with `isError: true`. `find_associations` reports an error when every endpoint failed; failures of some endpoints stay in `query_metadata`. Empty results ("No gene found …") are still normal results.
34
+ - **`find_associations` capped `limit` silently**: endpoints with a lower maximum page size (e.g. 100 for `genes_genes` and `variants_phenotypes`) now report `limit_capped_to` in `query_metadata`, and the `limit` description points to `max_limit` in `list_sources`.
35
+ - **`get_entity` mostly ignored `hint`**: the hint was only used when the identifier also matched that type's ID pattern, so `CTCF` with hint `protein` returned the gene and `lupus` with hint `disease` searched genes. A gene, protein, disease, drug or complex hint now looks up other identifiers by name; an unknown hint, or a name with a `variant` or `transcript` hint, is reported as an error.
36
+ - **IDs in the wrong case returned "not found"**: IDs are matched case-insensitively but were sent as typed, so `RS7903146`, `ensg00000141510`, `mondo_0007915` or a lowercase SPDI found nothing. IDs are now sent in the case the catalog stores (`rs…`, `Orphanet_…`, `…:g.…`, otherwise uppercase); names are passed through. `Orphanet:586` is accepted like other ontology IDs. When `find_associations` resolves an rsID to no variants, the endpoint's `query_metadata` says so instead of reporting an empty result.
37
+ - **Gene symbols that are also valid UniProt accessions were treated as proteins**: e.g. P2RY12, B3GAT1, B3GNT2 or R3HDM1, so `find_associations` reported gene relationships as not applicable. Such IDs are now genes when a gene with exactly that symbol exists, and proteins otherwise. `find_associations` accepts the same `hint` as `get_entity` to choose the type explicitly or to start from a name (e.g. `lupus` with hint `disease`).
38
+ - **Ontology IDs outside a fixed list of prefixes were not recognised**: `HP_0033726`, `NCIT_C27153` or `PR_000001`, which the server itself returns, gave "Could not detect entity type", and with hint `disease` they were looked up as names. `PREFIX_LOCALID` or `PREFIX:LOCALID` is now an ontology term ID when the prefix is one of the 89 prefixes the catalog stores (`ONTOLOGY_PREFIXES`, collected from every page of `/ontology-terms`: the loaded ontologies and the terms they import, such as `PR`, `NCBITaxon`, `CVCL`, `PATO`, `AfPO` or `Oncotree`) in any case, and the local ID is letters and digits, in parts joined by `-` or `_` (`Oncotree_ACC`, `Oncotree_HEAD_NECK`, `PR_A6H6A9-1`). Every one of the catalog's 869,267 term IDs with these prefixes is detected; the catalog's other `term_id` values (`CHR_9606-chr1`, `lipidmaps:…`, HGNC report URLs, country names) are deliberately not. The local ID needs no digit: the prefix table keeps names such as `type_two` from looking like IDs, and no gene symbol has the form `<prefix>_<local ID>`. UniProt entry names (`<mnemonic>_<SPECIES>`) can have that form, as `HBA_HUMAN` has, so an identifier shaped like one whose species code is 1–5 letters is not an ontology ID unless its prefix is `Oncotree`, the only prefix with stored term IDs of that form (`Oncotree_ACC`). Without a hint, `HBA_HUMAN` therefore gives "Could not detect entity type", as `CTCF_HUMAN` does. The prefix table replaces the per-ontology patterns. Other strings, such as RefSeq accessions (`NM_000546`, `NP_000537`) or positions (`chrX:1000`), are not ontology IDs and still give "Could not detect entity type". The ID is normalised to the catalog's form: `_` separator, the prefix in its canonical case (`ncbitaxon_9606` → `NCBITaxon_9606`, `vario_0001` → `VariO_0001`, `orphanet:586` → `Orphanet_586`, `oncotree_acc` → `Oncotree_ACC`) and an uppercase local ID, as the catalog stores every local ID of these prefixes.
39
+ - **Indel HGVS and SPDI with an empty allele were not recognised**: only substitution HGVS (`g.123A>G`) and SPDIs with both alleles were accepted, so IDs the catalog itself returns, such as `NC_000019.10:g.19259535_19259538del` (`find_ld`) or `NC_000016.10:19999964:A:` (`search_region`), gave "Could not detect entity type". SPDIs may now have an empty reference or alternate allele, and genomic HGVS accepts `del`, `dup`, `ins…` and `delins…` at a position or range. They are normalised to the catalog's form: uppercase, with `g.` and `del`/`dup`/`ins`/`delins` lowercase.
40
+ - **A hint that did not fit the ID gave inconsistent results**: `ENST00000264010` with hint `protein` or `MONDO_0007915` with hint `gene` were looked up as names and returned "No … found with name=…", while `BRCA1` with hint `variant` was a tool error. An identifier that matches the ID pattern of a different type than the hint is now a tool error, e.g. `'ENST00000264010' is a transcript ID, not a protein name.` Strings shaped like gene symbols still count as names, so e.g. `P2RY12` or `CA1` with hint `gene` or `protein` keep working. Strings shaped like a UniProt entry name with a species code of 1–5 letters, such as `HBA_HUMAN`, are not ontology IDs unless the prefix is `Oncotree` (see above), so with hint `protein` they are looked up as protein names like `HBB_HUMAN`. Every ontology term ID gives the error whatever the length of its local ID, e.g. `Orphanet_586`, `CHEBI_10`, `Oncotree_ACC` or `HBA_9HIV1` (a species code with a digit is not read as a UniProt entry name).
41
+ - **Gene symbols matched fuzzily**: the catalog's gene name search is fuzzy, so `liver` returned gene LIVAR (and `find_associations liver regulatory` returned LIVAR's heart eQTLs), and `BRCA` returned BRCA1 and BRCA2. A gene symbol now only refers to genes whose symbol equals it case-insensitively, in `get_entity`, `resolve_id` and `find_associations`, with or without hint `gene`. The symbol is looked up across all pages and filtered; without an exact match the result is a normal "No gene found with symbol 'LIVER'. Did you mean: LIVAR?" listing the genes the search returned.
42
+ - **Gene symbols shaped like other IDs were treated as those IDs**: `RS1` was looked up as an rsID and `CA1`–`CA14` as ClinGen CA IDs, so `get_entity CA1` found no variant and `find_associations CA2 genetic` returned no rows. The rule for UniProt-shaped symbols now covers every ID that also fits the gene symbol pattern (rsIDs, CA IDs, UniProt accessions, PharmGKB and GWAS Catalog IDs), in any case: without a hint (or with hint `gene`) it is a gene if a gene with exactly that symbol exists. The check is the same single gene name lookup, and `find_associations` reuses its result. `find_ld` always reads its input as a variant.
43
+ - **Hyphenated gene symbols were not detected**: the gene symbol pattern allowed only letters and digits, up to 10 characters, so without a hint `HLA-A`, `HLA-DRB1`, `NKX2-1`, `MT-ND1` or `ZNF33A-AS1` gave "Could not detect entity type", and `resolve_id`, which takes no hint, could not resolve them at all. Gene symbols may now have hyphen-separated alphanumeric parts and up to 25 characters (the catalog's longest readthrough symbols). `CPX-9`, `R-HSA-164843` and `P49711-2` keep their types, and the check for a gene symbol shaped like another ID only applies to symbols without a hyphen of up to 10 characters (no longer ones exist in the catalog), so complex, pathway and long IDs cost no `/genes` lookup. A symbol that matches a gene is reported in the catalog's case (`c1orf112` → `C1orf112`).
44
+ - **Irregular gene symbols were not detected**: about 15 catalog symbols contain `_` or `.` or start with a digit (`C4B_2`, `GTF2H2C_2`, `Y_RNA`, `Metazoa_SRP`, `5S_rRNA`, `7SK`, `CTD-2194D22.4`, …), so without a hint they gave "Could not detect entity type", and `resolve_id` could not resolve them at all. `GENE_SYMBOL` is unchanged, as widening it would make unknown accessions such as `NM_000546` silent "No gene found" results again. Instead, an identifier that fits no ID pattern or known ontology prefix but has the form of an irregular symbol (`IRREGULAR_SYMBOL`: 2–25 letters, digits, `-`, `_` or `.`, starting with a letter or digit, with at least one letter) costs one `/genes?name=` lookup and is a gene only if a gene with exactly that symbol exists, reported in the catalog's case (`5s_rrna` → `5S_rRNA`); otherwise it is still "Could not detect entity type" (`NM_000546`, `NP_000537`). Identifiers of other forms (`chrX:1000`) cost no lookup. With hint `gene` the symbol is looked up as before.
45
+ - **Names in `find_associations` were sent to edge name parameters**: edge name matching is fuzzy for genes and case-sensitive for drugs and proteins, so `Warfarin` (hint `drug`) and `ctcf` (hint `protein`) returned no rows, and the API ignores `phenotype_name`. Names are now resolved through their node lookup (all pages) to the entities that match exactly, case-insensitively: genes by symbol, proteins by `name`, `uniprot_names` or `uniprot_full_names`, ontology terms by `name` or `synonyms` (so `lupus` finds MONDO_0004670), drugs and complexes by `name`. The candidates come from every node search for those fields, run concurrently and merged by ID: `name` and `synonyms` for ontology terms (so `SLE` finds MONDO_0007915 and `lupus` also finds EFO_0002690, which carries the lupus GWAS data), `name`, `uniprot_name` and `uniprot_full_name` for proteins (so `CTCF_HUMAN` finds the CTCF proteins), and `name` for the other types. Edges are queried once per matched ID (`gene_id`, `protein_id` or `query`, `phenotype_id`/`disease_id`, `drug_id`, `complex_id`) and never by name; such endpoints report `resolved_from: "name"` and the IDs in `params_used`. Without an exact match the result is a normal "No … found with name …" with near matches, and no edge is queried. `get_entity` with a non-gene hint still returns the API's name search results unchanged.
46
+ - **`limit` was silently multiplied when an identifier resolved to several IDs**: an endpoint queried once per resolved ID (a name matching several entities, or the alleles of an rsID) used the full `limit` for each, so `CTCF` with hint `protein`, `physical` and limit 3 returned 18 `proteins_proteins` rows while the descriptions promised at most `limit` per endpoint. The API takes one ID per request and `page` must page through every ID, so the limit stays per ID and this is now stated: the tool and `limit` descriptions say the limit applies to each data source and to each resolved ID; each resolved endpoint's `query_metadata` has `queried_ids`, and its `_pagination` adds `limit_applies_to: "each of the N queried IDs"` (`has_more` if any ID has more); the top-level `_pagination` lists `queried_ids: {endpoint: N}` for endpoints queried with several IDs. `results_returned` counts every row returned.
47
+ - **`resolve_id` omitted `term_id` for ontology terms**: it read `_id`, but ontology records carry `term_id`, so e.g. `mondo_0009861` returned only name, synonyms and source. It now reads `term_id`, falling back to `_id`.
48
+ - **`resolve_id` reported only the first of several genes sharing a symbol**: `CD99` returned ENSG00000002586 with no sign of its Y PAR copy ENSG00000002586_PAR_Y, and `HLA-DRB1` one of its 6 genes on alternate haplotypes. Gene results now have a `genes` list (`gene_id`, `name`, `chromosome`, `start`, `end`) and a `note` when the identifier matches several genes, like the `alleles` of variants and `proteins` of proteins. The top-level gene is chosen deterministically, preferring a gene on a primary chromosome (not an alternate haplotype, fix patch, scaffold or `_PAR_Y` copy), then one with an HGNC ID, then the lowest gene ID: 314 symbols have several genes on the primary assembly (e.g. `MATR3`: ENSG00000015479, with HGNC:6912, and ENSG00000280987, both on chr5). The note counts where the genes are and names the rule that decided, e.g. `MATR3 matches 2 genes, listed in genes: 2 on the primary assembly. The top-level identifiers describe ENSG00000015479, the only one on the primary assembly with an HGNC ID (…)`, or `1 on the primary assembly and 1 Y chromosome PAR copy` for `CD99`.
49
+ - **`resolve_id` returned poor protein identifiers**: `P04637` gave the first of 15 isoforms (`P04637-9`), `names: []` (a field proteins don't have) and all 1,144 dbxrefs. Every matching protein is now fetched; the top-level identifiers describe the MANE Select protein when there is one (the first of them if several are, as the note then says), otherwise the first, using the real fields (`name`, `uniprot_ids`, `uniprot_names`, `uniprot_full_names`, `MANE_Select`). The dbxrefs are replaced by `dbxrefs_count` and `uniprot_accession` (the UniProtKB accession from dbxrefs, or else the accession shared by `uniprot_ids`); `get_entity` still returns the full record.
50
+ - **Filters were dropped silently**: the schema's own example `source: 'EBI eQTL Catalogue'` is not a valid value and was ignored, and a filter that applies to only some endpoints of a relationship (e.g. `method: 'eQTL'` for a gene's `regulatory`) left the others unfiltered without saying so. Each endpoint's `query_metadata` now has `filters_not_applied`, mapping every user filter not applied to it to the reason (`not supported by this endpoint`, or `invalid value; valid: …`), and the response has a top-level `note` naming filters applied to no endpoint. Unsupported filters stay non-fatal, as other endpoints may support them. The filter examples in the schema are valid values, and the descriptions point to `list_sources` for the valid values per endpoint.
51
+ - **`method` and `label` filters were dropped on endpoints that support them**: `/genes/genes` accepts `method` (e.g. `COXPRESdb` or a BioGRID genetic interaction type) and `/proteins/proteins` accepts `method` (the interaction type, e.g. `direct interaction`) and `label` (the detection method, e.g. `two hybrid`), but the edge configuration listed no values for them, so the filters were reported as "not supported by this endpoint" and the rows came back unfiltered. Every endpoint was audited against the spec: where the client method accepts `source`, `method` or `label` and the spec has an enum, the configured values now equal it (`list_sources` shows them); where the spec has no enum the value is passed through instead of dropped. The spec contract test (`IGVF_CATALOG_OPENAPI_SPEC`) now checks that the values equal the enum rather than being a subset.
52
+ - **No score filter**: `score` is now a filter on `/genes/genomic-elements` and `/genomic-elements/genes`, e.g. for E2G prediction scores. Like `r2` and `d_prime`, it takes a number, which is a minimum (sent as `gte:<value>`), or an operator string `gt:`, `gte:`, `lt:` or `lte:` followed by a number (e.g. `gt:0.7` for a strict minimum), which is passed through. The input schema of these three filters accepts both forms (it declared a number only, so `score: "gt:0.9"` failed with "Input validation error"); other range values are reported in `filters_not_applied` as invalid instead of being sent to the API.
53
+ - **`get_entity` reported a `limit_capped_to` above the requested limit**: complexes and studies, whose lookup takes no `limit`, reported `limit_capped_to: 25` for a request of 5. A cap is now only reported when it is below the request; for these types `_pagination` has `page_size: 25` and the response a `note` that the page size is fixed. Rows are not trimmed, so `page` keeps working.
54
+ - **"Not found" messages named ontology terms inconsistently**: `get_entity` and `resolve_id` said "No ontology found with term_id=…" while name lookups said "No ontology term found …". Both now say "ontology term".
55
+ - **`find_ld` past the last page changed shape**: an empty page after page 0 returned no `_pagination` and claimed "No variants in LD found". It now has the normal shape with `ld_variants: []` and `_pagination`; the message is kept for an empty page 0.
56
+ - **A blank identifier with a hint searched the whole collection**: an empty or whitespace-only identifier was stripped and, with a name hint, looked up by name, which the API treats as no filter. `get_entity` with hint `gene` paged through every gene (about 140 pages), `find_associations` with hint `disease` did not return, and hint `protein` returned arbitrary proteins. A blank identifier is now a tool error (`The identifier is empty.`) in `get_entity`, `resolve_id`, `find_associations` and `find_ld`, with or without a hint, before any API call; `IDParser.lookup_name` also rejects a blank name. The identifier parameters declare `minLength: 1`.
57
+ - **`_PAR_Y` gene and transcript IDs were not recognised**: the catalog stores the Y chromosome PAR copies of genes and transcripts as `ENSG00000002586_PAR_Y` and `ENST00000611428_PAR_Y`, which `resolve_id CD99`, `get_entity` and transcription rows return, but as input they gave "Could not detect entity type", and with hint `gene` "No gene found with symbol …". ENSG and ENST IDs may now end in `_PAR_Y` (in any case; sent as the catalog stores it), so they work in `get_entity`, `resolve_id` and `find_associations` and are never read as gene symbols. Proteins have no `_PAR_Y` IDs in the catalog.
58
+ - **Versioned Ensembl gene and transcript IDs found nothing**: `ENSG00000141510.17` or `ENST00000264010.10` were accepted but sent with the version, while the catalog stores genes and transcripts unversioned, so `get_entity` and `resolve_id` reported "No gene found" and `find_associations` returned 0 rows without explanation. The version is now removed from ENSG and ENST IDs, also before a `_PAR_Y` suffix as GENCODE writes it (`ENSG00000002586.20_PAR_Y` → `ENSG00000002586_PAR_Y`). The queried ID is what `find_associations` reports as `entity_id` and what "No … found" messages show. Versioned ENSP IDs are still sent as typed, as the API accepts them with or without the version.
59
+ - **Timeouts were reported with an empty error text**: a request that hit the client timeout (30 s) raises an exception without a message, and `search_region` and `find_associations` used that message as the error, so a timed-out entity type showed `"error": ""` and an all-failed `find_associations` said `Error finding associations: genes_variants: ; genes_genomic_elements: `. `get_entity` and `find_ld` said only `TimeoutError: `. `format_error()` now describes a timeout without a message as `TimeoutError: the IGVF Catalog API did not respond within 30 s` and any other exception without a message by its type name, and `search_region` and `find_associations` use it for per-type and per-endpoint errors, e.g. `BadRequestException: (400)` followed by the API's error body.
60
+ - **`search_region` returned a normal result when every entity type failed**: e.g. when every query timed out, the result had `has_more: false` and only `{"error": …}` entries, which looked like an empty region. It is now a tool error listing each type's error, as in `find_associations`. When only some types fail, the result has a top-level `note` naming them, since `_pagination.has_more` covers only the other types.
61
+ - **`search_region` accepted chromosomes that do not exist**: the region parser accepted any one- or two-digit chromosome, so `chr23:1-500`, `chr0:1000-2000`, `99:1M-2M` or `chr22:1M-2M` with organism `Mus musculus` returned a normal, empty result that looked like a real empty region. `RegionParser.parse_region` now takes the organism (default `Homo sapiens`) and checks the chromosome against its chromosome set (`VALID_CHROMOSOMES`: chr1–chr22, chrX, chrY, chrM for human; chr1–chr19, chrX, chrY, chrM for mouse), so these are tool errors such as `Invalid chromosome 'chr23' for Homo sapiens. Valid: chr1-chr22, chrX, chrY, chrM`. Chromosomes are still accepted with or without `chr` and in any case.
62
+ - **`search_region` accepted positions beyond the end of the chromosome**: `chr1:1G-2G` was sent to the API with only a large-region warning, and positions such as 10^29 reached the API, which answered 400. The region parser now has the length of every chromosome in the assemblies the catalog uses (`CHROMOSOME_LENGTHS`, GRCh38 for human and GRCm39 for mouse, from the UCSC hg38 and mm39 `chrom.sizes` files, which match the NCBI assembly reports). Positions are 0-based and the end is exclusive, as in the catalog, so a region may end at the chromosome length (`chr1:1-248956422`, `chrM:1-16569`). A start at or beyond the chromosome length is a tool error (`Invalid region: start position '1G' is beyond the end of chr1 (248,956,422 bp in GRCh38; the largest start position is 248956421)`). An end beyond the chromosome length is clipped to it, since a region that runs past the end of a chromosome is a reasonable request: `chr1:248,900,000-249,000,000` searches `chr1:248900000-248956422`, and the result has a top-level `note` saying so (`The end position '249,000,000' is beyond the end of chr1 (248,956,422 bp in GRCh38), so the region was clipped to chr1:248900000-248956422.`). When some entity types also failed, the two notes are combined in `note`, as in `find_associations`. The `region` description states the assemblies, the coordinate convention and the clipping.
63
+ - **`search_region` results for regions over 10 Mb were not valid JSON**: the large-region warning was prepended to the JSON as a plain-text line (`Warning: Large region (19,999,999 bp). Results may be incomplete.`), so the result could not be parsed, while every other tool returns pure JSON or a plain not-found message. The warning is now a top-level `warning` field of the JSON (`"warning": "Large region (19,999,999 bp). Results may be incomplete."`). It is kept apart from `note`, which combines the clipping and partial-failure notes.
64
+ - **`find_associations` hid partial failures behind `_pagination.has_more`**: an endpoint that failed while others succeeded was reported only in `query_metadata`, and the top-level `has_more` silently ignored it. The result now has a top-level `note` naming the failed endpoints and saying that `_pagination.has_more` covers only the endpoints that succeeded, as in `search_region`. Endpoints skipped because they do not accept the identifier type are not counted as failures. When filters were also applied to no endpoint, both notes are combined in `note`.
65
+ - **`find_associations` called a filter applied to no endpoint when it was sent to an endpoint that failed**: a failed endpoint's `query_metadata` held only the error, so the note "Filters not applied to any endpoint" ignored it. With `score` (supported only by `genes_genomic_elements`) and that endpoint timing out, or `ancestry: XYZ` sent to `/variants/variant-ld`, which returned 400, the note named the filter and pointed to `filters_not_applied` reasons that did not exist. A failed endpoint now also reports the `params_used` and `filters_not_applied` of the request built for it, and the note covers every endpoint a request was built for, failed or not. An endpoint whose identifier could not be resolved (e.g. the rsID→SPDI lookup failed) had no request built; it is left out of the note, and its `query_metadata` says so in a `note`. The partial-failure note, the error when every endpoint failed and `results_returned` are unchanged.
66
+ - **`relationship: "all"` returned a normal empty result for entity types without relationships**: for a pathway (`R-HSA-164843`) or a study (`GCST005752`) no endpoint applies, and `find_associations` returned `num_associations: 0` with an empty `query_metadata`, while every specific relationship was a tool error ("Relationship type 'functional' not applicable to pathway entities."). It is now a tool error too: `No relationships are available for pathway entities.` A test runs every entity type and lookup parameter `IDParser` returns through every relationship and `all`, and checks that each combination queries endpoints or is a tool error. The only combinations without endpoints are `all` for pathways and studies; ENTREZ gene IDs are accepted by no edge endpoint and were already a tool error.
67
+ - **Decimal K/M/G positions were rounded down, and misplaced commas were dropped**: `search_region` computed `int(float(position) * multiplier)`, so `chrX:1M-1.001M` searched `chrX:1000000-1000999` and `1.001K` was 1000 (747 of 50,000 sampled `a.bbbM` values were 1 bp too low), a position that is not a whole number of base pairs such as `1.5` or `1.0000001M` was silently truncated, and commas were removed wherever they were, so `1,5M` (a decimal comma) became 15,000,000 and `1,00` became 100. Positions are now computed exactly with `decimal.Decimal`, at a precision that holds every digit of the position (under the default 28-digit precision, `1.0000000000000000000000000001M` was rounded to 1,000,000 and `123456789012345678901234567891` to `…567900`); one that is not a whole number of base pairs is a tool error (`Invalid position: '1.0000001M' is 1000000.1 bp, not a whole number of base pairs`), and commas are only accepted between groups of three digits (`1,000,000`), otherwise the position is a tool error (`Invalid position format: '1,5M'. …`).
68
+ - **The `p_value` filter did not match the requested maximum**: `p_value` is sent as the minimum `neg_log10_pvalue`, which was formatted to 2 decimals. The catalog stores `neg_log10_pvalue` at full precision, so the rounded threshold could exclude rows within the maximum or admit rows above it: e.g. `p_value: 0.0002` sent `gte:3.70`, which excluded p-values from 1.995e-4 to 2e-4 (rounding up happened for 5,376 of 10,800 sampled thresholds), and any rounding down admits p-values above the maximum. `-log10(p_value)` is now sent at full precision (`gte:3.6989700043360187`, the shortest form that reads back as the same float), so the filter admits exactly the rows with p-values up to the maximum, and the `p_value` description says so. Other numeric filters are sent unchanged (`gte:<number>`), and the API accepts the exponent notation Python uses for small numbers (`gte:1e-05`).
69
+
70
+ ### Added
71
+ - **Contract tests** (`tests/test_contract.py`) checking every method and parameter used by the server against the generated client, and offline tests running all six tools through `call_tool`.
72
+ - **GitHub Actions**: `test.yml` (ruff and pytest on Python 3.10–3.13) and `release_to_pypi.yml` (trusted publishing to PyPI on GitHub releases).
73
+ - **Pagination support for all multi-result tools**: `find_ld`, `find_associations`, and `search_region` now accept a `page` parameter (0-indexed) and return a `_pagination` metadata object in every response. The metadata includes `has_more`, `next_page`, and a natural-language `note` prompting agents to fetch subsequent pages when results are truncated.
74
+ - **`build_pagination_metadata` helper** in `formatter.py`: Shared utility that computes pagination signals using the heuristic "results_returned == limit implies more pages exist".
75
+
76
+ ### Fixed
77
+ - **API client page override bug**: `IGVFCatalogClient.find_associations()` previously overwrote the `page` parameter to 0 regardless of what the tool passed. Now respects the page value from params.
78
+ - **ID Parser gene symbol parameter**: Changed gene symbol detection to return `"gene_name"` instead of `"name"` as the parameter name. This ensures compatibility with edge endpoints (e.g., `/api/genes/variants`, `/api/variants/genes`) which expect `gene_name`, while maintaining compatibility with node endpoints (e.g., `/api/genes`) which accept both `name` and `gene_name`. This fix resolves issues where `find_associations` tool would fail when querying relationships for gene symbols like "TP53" or "BRCA1".
79
+
80
+ ### Changed
81
+ - **Tool descriptions** for `find_ld`, `find_associations`, and `search_region` now explicitly mention pagination support, making the capability visible to agents reading the tool schema.
82
+
83
+ ## [0.1.0] - Initial Release
84
+
85
+ ### Added
86
+ - Complete MCP server implementation with 5 tools
87
+ - Smart entity lookup with auto-detection for 30+ ID patterns
88
+ - Flexible genomic region parsing (supports M/K notation, commas, etc.)
89
+ - Async API client with httpx
90
+ - Comprehensive test suite
91
+ - MCP resources for schemas and guides
@@ -0,0 +1,27 @@
1
+ Find information about gene TP53.
2
+
3
+ Show all the variants in chromosome 16, position at 20000000?
4
+
5
+ What diseases the variant with SPDI NC_000012.12:102917129:T:C is associated with?
6
+
7
+ Find variants that are relevant to lupus disease.
8
+
9
+ Find transcripts from human protein CTCF?
10
+
11
+ Tell me about genetic elements overlapping with variant rs62513865?
12
+
13
+ What variants are active in liver tissue, consider eQTL data specifically.
14
+
15
+ Find the genomic elements that are associated with BRCA1 where score is larger than 0.70.
16
+
17
+ Find the top 5 (sorted by p_value in ascending order) eQTLs in ontology term Orphanet_101435.
18
+
19
+ Find the genomic elements that it is containing variant rs6994300.
20
+
21
+ Find the top 5 genes (sorted by z_score in ascending order) that are co-expressed with gene ENSG00000012048.
22
+
23
+ Which genes are linked to variant with SPDI NC_000020.11:3658947:A:G?
24
+
25
+ Is variant rs6994300 an eQTL?
26
+
27
+ Find 5 variants associated to gene BRCA1.