deepdna-mcp 0.1.1__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.
@@ -0,0 +1,54 @@
1
+ # Publishes to PyPI with trusted publishing (no token): PyPI trusts this workflow,
2
+ # in this repository, under the "pypi" environment. Runs only when a v* tag is pushed.
3
+ name: Publish to PyPI
4
+
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
17
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install -e . pytest
21
+ - run: pytest -q
22
+
23
+ build:
24
+ needs: test
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
28
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
29
+ with:
30
+ python-version: "3.12"
31
+ - name: Tag, package, server.json and README must name the same version
32
+ run: |
33
+ version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
34
+ test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != version $version"; exit 1; }
35
+ python -c "import json,sys; s=json.load(open('server.json')); sys.exit(0 if s['version']==s['packages'][0]['version']=='$version' else 'server.json version != $version')"
36
+ grep -q "deepdna-mcp@$version" README.md || { echo "README does not install $version"; exit 1; }
37
+ - run: pip install build==1.2.2.post1 && python -m build
38
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment: pypi
47
+ permissions:
48
+ id-token: write
49
+ steps:
50
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepDNA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: deepdna-mcp
3
+ Version: 0.1.1
4
+ Summary: Local MCP server that reads a raw DNA file on your computer and reports what it shows at the variants DeepDNA curates, with cited records. Nothing from the file is sent to deepdna.ai.
5
+ Project-URL: Homepage, https://deepdna.ai/docs/#mcp
6
+ Project-URL: Repository, https://github.com/alfonsoguv/deepdna-mcp
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: 23andme,ai-agents,dna,mcp,pharmacogenomics
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: mcp<3,>=2.3
13
+ Description-Content-Type: text/markdown
14
+
15
+ # DeepDNA MCP server
16
+
17
+ A remote [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI agents cited pharmacogenomic and gene-variant lookups, so the model quotes CPIC and DPWG guidance instead of recalling it from memory. This repository also holds a [local package](#read-a-raw-dna-file-on-your-own-computer) that reads a raw DNA file on the user's own computer; nothing from the file is sent to DeepDNA.
18
+
19
+ ```text
20
+ https://deepdna.ai/mcp
21
+ ```
22
+
23
+ Streamable HTTP, stateless, no API key, no account. Listed in the official MCP registry as `ai.deepdna/knowledge`.
24
+
25
+ > Educational information only, not medical advice. The server reports what curated sources and guidelines say about a gene, a variant or a drug–gene pair. It does not diagnose, prescribe or recommend a dose.
26
+
27
+ ## Tools
28
+
29
+ | Tool | What it returns |
30
+ |---|---|
31
+ | `list_genes` | The genes in the knowledge base, with a one-line summary and category for each |
32
+ | `lookup_gene` | Everything curated about one gene: function, key variants with rsIDs, phenotypes, drug interactions, what a consumer genotyping array can and cannot tell you, and sources |
33
+ | `lookup_variant` | One variant by dbSNP rsID: gene, star-allele or common name, alleles, effect, frequency note, sources, and a `reading` block (plus-strand GRCh37 alleles, `countable`, `strand_ambiguous`). Takes the rsID only, never a genotype |
34
+ | `lookup_drug_gene` | Guidance for a gene, optionally for one drug: guideline (CPIC or DPWG where one exists), summary, phenotype groups and sources |
35
+ | `check_medications` | Splits a list of generic drug names into drugs with a curated record (gene, and a `basis` field: CPIC or DPWG guideline, drug label, or primary literature) and drugs not covered. Needs no genotype |
36
+ | `request_beta_access` | The only tool that writes: registers the user's interest in the private beta (email and optional use case). The agent must ask the user first and set `user_consent: true`. See the [privacy policy](https://deepdna.ai/privacy/) |
37
+
38
+ Two prompts, `medication_review` and `explain_variant`, package the common questions for clients that show MCP prompts.
39
+
40
+ Coverage today is 12 genes: CYP2C19, CYP2C9, CYP2D6, SLCO1B1, VKORC1, UGT1A1, G6PD, MTHFR, COMT, HFE, APOE and BRCA1, with 51 curated drug–gene pairs across 11 of them: 40 cite a CPIC or DPWG guideline, the rest a drug label or the primary literature; the `basis` field in `check_medications` says which (`guideline` is null, or "drug label", when no guideline exists). It is a small, reviewed set, not a mirror of PharmGKB/ClinPGx. Every record lists its sources. The `level` field (the guideline's evidence grade) is `null` for every pair today.
41
+
42
+ ## Connect
43
+
44
+ **Claude Code**
45
+
46
+ ```bash
47
+ claude mcp add --transport http deepdna https://deepdna.ai/mcp
48
+ ```
49
+
50
+ **Cursor** (`~/.cursor/mcp.json`) and other clients that use an `mcpServers` file:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "deepdna": { "type": "http", "url": "https://deepdna.ai/mcp" }
56
+ }
57
+ }
58
+ ```
59
+
60
+ **VS Code** (`.vscode/mcp.json`):
61
+
62
+ ```json
63
+ {
64
+ "servers": {
65
+ "deepdna": { "type": "http", "url": "https://deepdna.ai/mcp" }
66
+ }
67
+ }
68
+ ```
69
+
70
+ **Claude Desktop and claude.ai:** add `https://deepdna.ai/mcp` as a custom connector.
71
+
72
+ ## Try it without a client
73
+
74
+ ```bash
75
+ curl -s https://deepdna.ai/mcp \
76
+ -H "Content-Type: application/json" \
77
+ -H "Accept: application/json, text/event-stream" \
78
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lookup_drug_gene","arguments":{"gene":"CYP2C19","drug":"clopidogrel"}}}'
79
+ ```
80
+
81
+ The answer carries the CPIC recommendation for clopidogrel by CYP2C19 metabolizer group, the sources it was written from, and the disclaimer. The same data is available as plain REST, documented in the [API docs](https://deepdna.ai/docs/) and the [OpenAPI spec](https://deepdna.ai/openapi.json):
82
+
83
+ ```text
84
+ GET https://deepdna.ai/api/v1/pgx/CYP2C19/clopidogrel
85
+ GET https://deepdna.ai/api/v1/variants/rs4244285
86
+ ```
87
+
88
+ ## Read a raw DNA file on your own computer
89
+
90
+ The remote server takes no genotype: its tools have no field for one. For a raw DNA file there is a local package in this repository. It reads the file on the user's computer and reports what it shows at each position DeepDNA curates, with the cited records. Nothing from the file is sent to deepdna.ai: the package downloads the same public records for every user, and `lookup_drug_gene` sends only the gene and drug names. What `read_dna_file` returns (the letters and counts at the curated positions) becomes part of the conversation with the AI model your client uses, under that provider's terms.
91
+
92
+ **Claude Code** (needs [uv](https://docs.astral.sh/uv/); the package is on [PyPI](https://pypi.org/project/deepdna-mcp/)):
93
+
94
+ ```bash
95
+ claude mcp add deepdna-local -- uvx deepdna-mcp@0.1.1
96
+ ```
97
+
98
+ Clients that use an `mcpServers` file (if the client cannot find `uvx`, put its full path, from `which uvx`, in `command`):
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "deepdna-local": { "command": "uvx", "args": ["deepdna-mcp@0.1.1"] }
104
+ }
105
+ }
106
+ ```
107
+
108
+ | Tool | What it returns |
109
+ |---|---|
110
+ | `read_dna_file` | What a 23andMe, AncestryDNA or MyHeritage/FamilyTreeDNA export (.txt, .csv or .zip) shows at each curated position |
111
+ | `lookup_drug_gene` | Guidance for a gene, optionally for one drug, from the public API. Sends only the gene and drug names |
112
+
113
+ - Each position gets one of four states: `read`, `no_call`, `not_in_file`, or `unexpected_letters` (letters that fit neither the record nor its complement). A missing or unreadable position is unknown, never normal.
114
+ - For positions the record marks as `countable` (no phasing or other variants are needed to read them), it gives how many copies of the variant allele the file shows, with the record's text and sources.
115
+ - `genes` limits the answer to what the user asked about. APOE, BRCA1 and HFE are disease-risk genes: they are read only when named in `genes`, so the user chooses what to learn.
116
+ - `sex` is needed only for some chromosome X positions (G6PD).
117
+ - It checks the file's strand first: at least three positions that are not A/T or C/G must agree, and none disagree. A/T and C/G positions, whose two letters look the same on either strand, are counted only on a confirmed strand.
118
+
119
+ A count at one position is not a metabolizer status, a diplotype, a diagnosis or a dose. The tools tell the model never to suggest starting, stopping or changing a medication: that stays with the prescriber. VCF files are not supported.
120
+
121
+ Try it with: *"My 23andMe file is at /Users/me/Downloads/genome.txt. What does it show at the CYP2C19 positions DeepDNA curates, and what do the sources say about clopidogrel?"* (on Windows, a path such as `C:\Users\me\Downloads\genome.txt`).
122
+
123
+ ## What it is not
124
+
125
+ - **The remote server does not read DNA files.** Parsing on DeepDNA's servers is specified in the API but is in private beta and not deployed. A remote lookup carries a gene symbol, an rsID or a drug name. Its tools have no genotype field, and an undeclared argument is refused and not used. An agent that reads a raw DNA file itself compares the user's genotype with the `reading` block, outside DeepDNA. The `reading` block gives the reference and variant alleles on the plus strand of GRCh37, as 23andMe raw data reports it.
126
+ - **Neither server calls phenotypes or doses.** Turning a genotype into a metabolizer status needs phasing and the other variants in the gene, which a single lookup or a chip file does not give. Tools such as PharmCAT do that from a VCF.
127
+ - **Remote usage is counted, people are not.** Each request to the remote server is counted (method, tool, the lookup label described below, whether there was an answer, the client's name and version, its user-agent, a country code). IP addresses, request bodies and medication lists are not stored. A value typed into a declared field is recorded only as a short lookup label: an rsID in the form rs123, a gene from a fixed list of known pharmacogenes, or a drug DeepDNA covers; anything else, whatever it is, is recorded as "(other)". Please don't type a personal genetic result into these fields. The one exception is `request_beta_access`: it keeps the email and use case the user agrees to send, the signup metadata listed in the [privacy policy](https://deepdna.ai/privacy/) (time, source, country code and the client's name), and a hashed IP for one hour to limit abuse. The local package's downloads are ordinary requests to the public API, with the user-agent `deepdna-mcp-local/<version>`.
128
+ - **A consumer chip is not a clinical test.** Arrays are reliable for common variants and much less so for rare ones; anything that could affect a health decision needs a validated test and a clinician.
129
+
130
+ ## Links
131
+
132
+ - Docs: <https://deepdna.ai/docs/#mcp>
133
+ - Walkthrough with real outputs: <https://deepdna.ai/blog/pharmacogenomics-mcp-server/>
134
+ - Worked example in Python, where your own code goes from a raw DNA file to CPIC phenotypes (neither server does this step): <https://deepdna.ai/blog/pharmacogenomics-raw-dna-python/>
135
+ - Terms: <https://deepdna.ai/terms/> · Privacy: <https://deepdna.ai/privacy/>
136
+
137
+ This repository holds the documentation and the local package; the remote server is hosted by DeepDNA. Questions and bug reports go in the issues; please don't post a raw DNA file, genotypes or medications there.
138
+
139
+ <sub>Written with AI assistance.</sub>
140
+
141
+ <!-- mcp-name: ai.deepdna/local -->
@@ -0,0 +1,127 @@
1
+ # DeepDNA MCP server
2
+
3
+ A remote [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI agents cited pharmacogenomic and gene-variant lookups, so the model quotes CPIC and DPWG guidance instead of recalling it from memory. This repository also holds a [local package](#read-a-raw-dna-file-on-your-own-computer) that reads a raw DNA file on the user's own computer; nothing from the file is sent to DeepDNA.
4
+
5
+ ```text
6
+ https://deepdna.ai/mcp
7
+ ```
8
+
9
+ Streamable HTTP, stateless, no API key, no account. Listed in the official MCP registry as `ai.deepdna/knowledge`.
10
+
11
+ > Educational information only, not medical advice. The server reports what curated sources and guidelines say about a gene, a variant or a drug–gene pair. It does not diagnose, prescribe or recommend a dose.
12
+
13
+ ## Tools
14
+
15
+ | Tool | What it returns |
16
+ |---|---|
17
+ | `list_genes` | The genes in the knowledge base, with a one-line summary and category for each |
18
+ | `lookup_gene` | Everything curated about one gene: function, key variants with rsIDs, phenotypes, drug interactions, what a consumer genotyping array can and cannot tell you, and sources |
19
+ | `lookup_variant` | One variant by dbSNP rsID: gene, star-allele or common name, alleles, effect, frequency note, sources, and a `reading` block (plus-strand GRCh37 alleles, `countable`, `strand_ambiguous`). Takes the rsID only, never a genotype |
20
+ | `lookup_drug_gene` | Guidance for a gene, optionally for one drug: guideline (CPIC or DPWG where one exists), summary, phenotype groups and sources |
21
+ | `check_medications` | Splits a list of generic drug names into drugs with a curated record (gene, and a `basis` field: CPIC or DPWG guideline, drug label, or primary literature) and drugs not covered. Needs no genotype |
22
+ | `request_beta_access` | The only tool that writes: registers the user's interest in the private beta (email and optional use case). The agent must ask the user first and set `user_consent: true`. See the [privacy policy](https://deepdna.ai/privacy/) |
23
+
24
+ Two prompts, `medication_review` and `explain_variant`, package the common questions for clients that show MCP prompts.
25
+
26
+ Coverage today is 12 genes: CYP2C19, CYP2C9, CYP2D6, SLCO1B1, VKORC1, UGT1A1, G6PD, MTHFR, COMT, HFE, APOE and BRCA1, with 51 curated drug–gene pairs across 11 of them: 40 cite a CPIC or DPWG guideline, the rest a drug label or the primary literature; the `basis` field in `check_medications` says which (`guideline` is null, or "drug label", when no guideline exists). It is a small, reviewed set, not a mirror of PharmGKB/ClinPGx. Every record lists its sources. The `level` field (the guideline's evidence grade) is `null` for every pair today.
27
+
28
+ ## Connect
29
+
30
+ **Claude Code**
31
+
32
+ ```bash
33
+ claude mcp add --transport http deepdna https://deepdna.ai/mcp
34
+ ```
35
+
36
+ **Cursor** (`~/.cursor/mcp.json`) and other clients that use an `mcpServers` file:
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "deepdna": { "type": "http", "url": "https://deepdna.ai/mcp" }
42
+ }
43
+ }
44
+ ```
45
+
46
+ **VS Code** (`.vscode/mcp.json`):
47
+
48
+ ```json
49
+ {
50
+ "servers": {
51
+ "deepdna": { "type": "http", "url": "https://deepdna.ai/mcp" }
52
+ }
53
+ }
54
+ ```
55
+
56
+ **Claude Desktop and claude.ai:** add `https://deepdna.ai/mcp` as a custom connector.
57
+
58
+ ## Try it without a client
59
+
60
+ ```bash
61
+ curl -s https://deepdna.ai/mcp \
62
+ -H "Content-Type: application/json" \
63
+ -H "Accept: application/json, text/event-stream" \
64
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lookup_drug_gene","arguments":{"gene":"CYP2C19","drug":"clopidogrel"}}}'
65
+ ```
66
+
67
+ The answer carries the CPIC recommendation for clopidogrel by CYP2C19 metabolizer group, the sources it was written from, and the disclaimer. The same data is available as plain REST, documented in the [API docs](https://deepdna.ai/docs/) and the [OpenAPI spec](https://deepdna.ai/openapi.json):
68
+
69
+ ```text
70
+ GET https://deepdna.ai/api/v1/pgx/CYP2C19/clopidogrel
71
+ GET https://deepdna.ai/api/v1/variants/rs4244285
72
+ ```
73
+
74
+ ## Read a raw DNA file on your own computer
75
+
76
+ The remote server takes no genotype: its tools have no field for one. For a raw DNA file there is a local package in this repository. It reads the file on the user's computer and reports what it shows at each position DeepDNA curates, with the cited records. Nothing from the file is sent to deepdna.ai: the package downloads the same public records for every user, and `lookup_drug_gene` sends only the gene and drug names. What `read_dna_file` returns (the letters and counts at the curated positions) becomes part of the conversation with the AI model your client uses, under that provider's terms.
77
+
78
+ **Claude Code** (needs [uv](https://docs.astral.sh/uv/); the package is on [PyPI](https://pypi.org/project/deepdna-mcp/)):
79
+
80
+ ```bash
81
+ claude mcp add deepdna-local -- uvx deepdna-mcp@0.1.1
82
+ ```
83
+
84
+ Clients that use an `mcpServers` file (if the client cannot find `uvx`, put its full path, from `which uvx`, in `command`):
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "deepdna-local": { "command": "uvx", "args": ["deepdna-mcp@0.1.1"] }
90
+ }
91
+ }
92
+ ```
93
+
94
+ | Tool | What it returns |
95
+ |---|---|
96
+ | `read_dna_file` | What a 23andMe, AncestryDNA or MyHeritage/FamilyTreeDNA export (.txt, .csv or .zip) shows at each curated position |
97
+ | `lookup_drug_gene` | Guidance for a gene, optionally for one drug, from the public API. Sends only the gene and drug names |
98
+
99
+ - Each position gets one of four states: `read`, `no_call`, `not_in_file`, or `unexpected_letters` (letters that fit neither the record nor its complement). A missing or unreadable position is unknown, never normal.
100
+ - For positions the record marks as `countable` (no phasing or other variants are needed to read them), it gives how many copies of the variant allele the file shows, with the record's text and sources.
101
+ - `genes` limits the answer to what the user asked about. APOE, BRCA1 and HFE are disease-risk genes: they are read only when named in `genes`, so the user chooses what to learn.
102
+ - `sex` is needed only for some chromosome X positions (G6PD).
103
+ - It checks the file's strand first: at least three positions that are not A/T or C/G must agree, and none disagree. A/T and C/G positions, whose two letters look the same on either strand, are counted only on a confirmed strand.
104
+
105
+ A count at one position is not a metabolizer status, a diplotype, a diagnosis or a dose. The tools tell the model never to suggest starting, stopping or changing a medication: that stays with the prescriber. VCF files are not supported.
106
+
107
+ Try it with: *"My 23andMe file is at /Users/me/Downloads/genome.txt. What does it show at the CYP2C19 positions DeepDNA curates, and what do the sources say about clopidogrel?"* (on Windows, a path such as `C:\Users\me\Downloads\genome.txt`).
108
+
109
+ ## What it is not
110
+
111
+ - **The remote server does not read DNA files.** Parsing on DeepDNA's servers is specified in the API but is in private beta and not deployed. A remote lookup carries a gene symbol, an rsID or a drug name. Its tools have no genotype field, and an undeclared argument is refused and not used. An agent that reads a raw DNA file itself compares the user's genotype with the `reading` block, outside DeepDNA. The `reading` block gives the reference and variant alleles on the plus strand of GRCh37, as 23andMe raw data reports it.
112
+ - **Neither server calls phenotypes or doses.** Turning a genotype into a metabolizer status needs phasing and the other variants in the gene, which a single lookup or a chip file does not give. Tools such as PharmCAT do that from a VCF.
113
+ - **Remote usage is counted, people are not.** Each request to the remote server is counted (method, tool, the lookup label described below, whether there was an answer, the client's name and version, its user-agent, a country code). IP addresses, request bodies and medication lists are not stored. A value typed into a declared field is recorded only as a short lookup label: an rsID in the form rs123, a gene from a fixed list of known pharmacogenes, or a drug DeepDNA covers; anything else, whatever it is, is recorded as "(other)". Please don't type a personal genetic result into these fields. The one exception is `request_beta_access`: it keeps the email and use case the user agrees to send, the signup metadata listed in the [privacy policy](https://deepdna.ai/privacy/) (time, source, country code and the client's name), and a hashed IP for one hour to limit abuse. The local package's downloads are ordinary requests to the public API, with the user-agent `deepdna-mcp-local/<version>`.
114
+ - **A consumer chip is not a clinical test.** Arrays are reliable for common variants and much less so for rare ones; anything that could affect a health decision needs a validated test and a clinician.
115
+
116
+ ## Links
117
+
118
+ - Docs: <https://deepdna.ai/docs/#mcp>
119
+ - Walkthrough with real outputs: <https://deepdna.ai/blog/pharmacogenomics-mcp-server/>
120
+ - Worked example in Python, where your own code goes from a raw DNA file to CPIC phenotypes (neither server does this step): <https://deepdna.ai/blog/pharmacogenomics-raw-dna-python/>
121
+ - Terms: <https://deepdna.ai/terms/> · Privacy: <https://deepdna.ai/privacy/>
122
+
123
+ This repository holds the documentation and the local package; the remote server is hosted by DeepDNA. Questions and bug reports go in the issues; please don't post a raw DNA file, genotypes or medications there.
124
+
125
+ <sub>Written with AI assistance.</sub>
126
+
127
+ <!-- mcp-name: ai.deepdna/local -->
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "deepdna-mcp"
3
+ version = "0.1.1"
4
+ description = "Local MCP server that reads a raw DNA file on your computer and reports what it shows at the variants DeepDNA curates, with cited records. Nothing from the file is sent to deepdna.ai."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ dependencies = ["mcp>=2.3,<3", "httpx>=0.27"]
10
+ keywords = ["mcp", "pharmacogenomics", "23andme", "dna", "ai-agents"]
11
+
12
+ [project.urls]
13
+ Homepage = "https://deepdna.ai/docs/#mcp"
14
+ Repository = "https://github.com/alfonsoguv/deepdna-mcp"
15
+
16
+ [project.scripts]
17
+ deepdna-mcp = "deepdna_mcp.server:main"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/deepdna_mcp"]
25
+
26
+ [tool.pytest.ini_options]
27
+ testpaths = ["tests"]
28
+ pythonpath = ["src"]
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "ai.deepdna/local",
4
+ "title": "DeepDNA local",
5
+ "description": "Reads a raw DNA file on your computer and reports the variants DeepDNA curates, with sources.",
6
+ "version": "0.1.1",
7
+ "websiteUrl": "https://github.com/alfonsoguv/deepdna-mcp",
8
+ "repository": {
9
+ "url": "https://github.com/alfonsoguv/deepdna-mcp",
10
+ "source": "github"
11
+ },
12
+ "packages": [
13
+ {
14
+ "registryType": "pypi",
15
+ "identifier": "deepdna-mcp",
16
+ "version": "0.1.1",
17
+ "runtimeHint": "uvx",
18
+ "transport": {
19
+ "type": "stdio"
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,3 @@
1
+ """DeepDNA local MCP server: reads a raw DNA file on this computer and looks up cited records by rsID."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,72 @@
1
+ """DeepDNA's public knowledge API, read-only.
2
+
3
+ The requests are the same for every user: the gene list, each gene's record and
4
+ each curated variant's record. Nothing about the user's file is ever sent.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from functools import lru_cache
11
+ from typing import Any
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ from . import __version__
17
+
18
+ API = "https://deepdna.ai/api/v1"
19
+ USER_AGENT = f"deepdna-mcp-local/{__version__} (+https://github.com/alfonsoguv/deepdna-mcp)"
20
+
21
+
22
+ class KnowledgeUnavailable(RuntimeError):
23
+ """deepdna.ai could not be reached or answered with an error."""
24
+
25
+
26
+ def _get(client: httpx.Client, path: str) -> dict[str, Any]:
27
+ try:
28
+ r = client.get(API + path)
29
+ except httpx.HTTPError as e:
30
+ raise KnowledgeUnavailable("Could not reach deepdna.ai to download the public records. "
31
+ "Check the internet connection and try again.") from e
32
+ if r.status_code == 404:
33
+ return {}
34
+ if r.status_code != 200:
35
+ raise KnowledgeUnavailable(f"deepdna.ai answered {r.status_code} for {path}.")
36
+ return r.json()
37
+
38
+
39
+ def _client() -> httpx.Client:
40
+ return httpx.Client(timeout=20, headers={"User-Agent": USER_AGENT, "Accept": "application/json"})
41
+
42
+
43
+ @lru_cache(maxsize=1)
44
+ def curated_genes() -> list[str]:
45
+ """The gene symbols DeepDNA curates. Fetched once per session."""
46
+ with _client() as client:
47
+ return [g["symbol"] for g in _get(client, "/genes").get("genes", [])]
48
+
49
+
50
+ @lru_cache(maxsize=1)
51
+ def curated_variants() -> tuple[list[dict[str, Any]], str]:
52
+ """Every curated variant with its reading block, and the API's disclaimer. Fetched once per session."""
53
+ genes = curated_genes()
54
+ with _client() as client:
55
+ with ThreadPoolExecutor(max_workers=6) as pool:
56
+ gene_records = list(pool.map(lambda s: _get(client, f"/genes/{quote(s)}"), genes))
57
+ rsids = sorted({k["rsid"] for g in gene_records for k in g.get("gene", {}).get("key_variants", []) if k.get("rsid")})
58
+ records = list(pool.map(lambda rs: _get(client, f"/variants/{quote(rs)}"), rsids))
59
+ variants = [r["variant"] for r in records if r.get("variant", {}).get("reading")]
60
+ disclaimer = next((r["meta"]["disclaimer"] for r in records if r.get("meta", {}).get("disclaimer")), "")
61
+ if not variants:
62
+ raise KnowledgeUnavailable("deepdna.ai returned no curated variants.")
63
+ return variants, disclaimer
64
+
65
+
66
+ def drug_gene(gene: str, drug: str | None) -> dict[str, Any]:
67
+ """CPIC or DPWG guidance for a gene, optionally for one drug, as the public API returns it."""
68
+ if not any(c.isalnum() for c in gene) or (drug is not None and not any(c.isalnum() for c in drug)):
69
+ return {} # ".." would otherwise be resolved as a path segment
70
+ path = f"/pgx/{quote(gene.strip().upper(), safe='')}" + (f"/{quote(drug.strip().lower(), safe='')}" if drug else "")
71
+ with _client() as client:
72
+ return _get(client, path)
@@ -0,0 +1,122 @@
1
+ """Read a consumer raw DNA file on this computer.
2
+
3
+ Nothing in this module touches the network or writes to disk. The file's text is
4
+ held in memory only while it is parsed; after that only the rsIDs asked for are kept.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import io
11
+ import os
12
+ import zipfile
13
+ from dataclasses import dataclass
14
+
15
+ MAX_BYTES = 300 * 1024 * 1024
16
+ NO_CALLS = {"", "-", "--", "0", "00", "??", "NC"}
17
+
18
+
19
+ class UnsupportedFile(ValueError):
20
+ """The file is not a raw DNA export this package can read."""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Call:
25
+ chromosome: str
26
+ genotype: str # letters as written in the file ("AG", "A", "DI"); "" when the chip could not read it
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class RawFile:
31
+ source: str
32
+ data_lines: int
33
+ calls: dict[str, Call] # only the rsIDs that were asked for
34
+
35
+
36
+ def _chromosome(value: str) -> str:
37
+ c = value.strip().upper().removeprefix("CHR")
38
+ return {"23": "X", "24": "Y", "25": "X", "26": "MT", "M": "MT", "XY": "X"}.get(c, c)
39
+
40
+
41
+ def _genotype(value: str) -> str:
42
+ g = value.strip().strip('"').upper().replace("/", "").replace(" ", "")
43
+ return "" if g in NO_CALLS else g
44
+
45
+
46
+ TOO_LARGE = "The file is larger than 300 MB; a raw DNA export is usually under 30 MB."
47
+
48
+
49
+ def _text(path: str) -> str:
50
+ path = os.path.abspath(os.path.expanduser(path))
51
+ if not os.path.isfile(path):
52
+ raise FileNotFoundError(f"No file at {path}. Pass the full path to the file, for example /Users/name/Downloads/genome.txt.")
53
+ if os.path.getsize(path) > MAX_BYTES:
54
+ raise UnsupportedFile(TOO_LARGE)
55
+ if zipfile.is_zipfile(path):
56
+ with zipfile.ZipFile(path) as z:
57
+ names = [n for n in z.namelist() if n.lower().endswith((".txt", ".csv")) and not n.startswith("__MACOSX/")]
58
+ if not names:
59
+ raise UnsupportedFile("The zip file has no .txt or .csv inside.")
60
+ if z.getinfo(names[0]).file_size > MAX_BYTES:
61
+ raise UnsupportedFile(TOO_LARGE)
62
+ with z.open(names[0]) as f:
63
+ data = f.read(MAX_BYTES + 1) # the declared size can lie
64
+ if len(data) > MAX_BYTES:
65
+ raise UnsupportedFile(TOO_LARGE)
66
+ return data.decode("utf-8-sig", errors="replace")
67
+ with open(path, encoding="utf-8-sig", errors="replace") as f:
68
+ return f.read()
69
+
70
+
71
+ def read_raw_file(path: str, wanted: set[str]) -> RawFile:
72
+ """Parse a 23andMe, AncestryDNA or MyHeritage/FamilyTreeDNA export, keeping only the rsIDs in `wanted`."""
73
+ text = _text(path)
74
+ if text.lstrip().startswith("##fileformat=VCF"):
75
+ raise UnsupportedFile("VCF files are not supported: this package reads consumer raw data exports (23andMe, AncestryDNA, MyHeritage or FamilyTreeDNA).")
76
+ lines = text.splitlines()
77
+ first = next((l for l in lines if l.strip() and not l.startswith("#")), "")
78
+ header = first.replace('"', "").replace(" ", "").lower()
79
+ wanted = {w.lower() for w in wanted}
80
+ calls: dict[str, Call] = {}
81
+ data_lines = 0
82
+
83
+ if header.startswith("rsid,chromosome,position,result"):
84
+ source = "MyHeritage or FamilyTreeDNA (CSV)"
85
+ rows = csv.reader(io.StringIO("\n".join(l for l in lines if l.strip() and not l.startswith("#"))))
86
+ next(rows, None)
87
+ for row in rows:
88
+ if len(row) < 4:
89
+ continue
90
+ data_lines += 1
91
+ rsid = row[0].strip().lower()
92
+ if rsid in wanted:
93
+ calls[rsid] = Call(_chromosome(row[1]), _genotype(row[3]))
94
+ elif header.startswith("rsid\tchromosome\tposition\tallele1\tallele2"):
95
+ source = "AncestryDNA"
96
+ for line in lines[lines.index(first) + 1:]:
97
+ parts = line.split("\t")
98
+ if len(parts) < 5 or line.startswith("#"):
99
+ continue
100
+ data_lines += 1
101
+ rsid = parts[0].strip().lower()
102
+ if rsid in wanted:
103
+ a1, a2 = parts[3].strip(), parts[4].strip()
104
+ calls[rsid] = Call(_chromosome(parts[1]), "" if "0" in (a1, a2) else _genotype(a1 + a2))
105
+ elif len(first.split("\t")) == 4:
106
+ source = "23andMe" if "23andme" in text[:4000].lower() else "four-column raw data (rsid, chromosome, position, genotype)"
107
+ for line in lines:
108
+ if line.startswith("#"):
109
+ continue
110
+ parts = line.split("\t")
111
+ if len(parts) != 4:
112
+ continue
113
+ data_lines += 1
114
+ rsid = parts[0].strip().lower()
115
+ if rsid in wanted:
116
+ calls[rsid] = Call(_chromosome(parts[1]), _genotype(parts[3]))
117
+ else:
118
+ raise UnsupportedFile("Unrecognized file. Supported: 23andMe (.txt), AncestryDNA (.txt), MyHeritage or FamilyTreeDNA (.csv), or a .zip with one of them.")
119
+
120
+ if data_lines == 0:
121
+ raise UnsupportedFile("The file has no genotype rows.")
122
+ return RawFile(source=source, data_lines=data_lines, calls=calls)