bioinformatics-mcp-server 0.1.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.
@@ -0,0 +1,44 @@
1
+ .env
2
+ keys.env
3
+ .mcp.json
4
+ analytics.json
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ .venv/
9
+ venv/
10
+ *.log
11
+ *.egg-info/
12
+ dist/
13
+ build/
14
+ .claude/
15
+
16
+ # Andere MCP-Projekte (haben eigene Git-Repos)
17
+ Agriculture_MCP/
18
+ Analytics_MCP/
19
+ Aviation_MCP/
20
+ Company_MCP/
21
+ Cybersecurity_MCP/
22
+ Directory_MCP/
23
+ Germany_MCP/
24
+ Health_MCP/
25
+ Memory_MCP/
26
+ Payment_Gateway_MCP/
27
+ Reputation_MCP/
28
+ Space_MCP/
29
+ Weather_MCP/
30
+ Workflow_MCP/
31
+ Agent_Commerce_MCP/
32
+ Agent_Identity_MCP/
33
+ Business_Bridge_MCP/
34
+ github_profile/
35
+ API_Converter_MCP/
36
+ Agent_Coordination_MCP/
37
+ Agent_Interface_Standard/
38
+ Agent_Validator_MCP/
39
+ Feedback_MCP/
40
+ Hive_Mind_MCP/
41
+ MCP_Hub/
42
+ Prompt_Library_MCP/
43
+ Political_Finance_MCP/
44
+ Supply_Chain_MCP/
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: bioinformatics-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP-Server für Bioinformatik-Daten — Gene, Proteine, PubMed-Literatur, Varianten und Sequenzen via NCBI & UniProt
5
+ Project-URL: Homepage, https://github.com/AiAgentKarl/bioinformatics-mcp-server
6
+ Project-URL: Repository, https://github.com/AiAgentKarl/bioinformatics-mcp-server
7
+ Project-URL: Issues, https://github.com/AiAgentKarl/bioinformatics-mcp-server/issues
8
+ Author: AiAgentKarl
9
+ License-Expression: MIT
10
+ Keywords: ai-agent,bioinformatics,genes,genomics,life-science,mcp,ncbi,proteins,pubmed,uniprot
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: mcp[cli]>=1.0.0
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # bioinformatics-mcp-server
25
+
26
+ MCP-Server für Bioinformatik- und Life-Science-Daten. Gibt AI-Agents Zugriff auf NCBI Gene, UniProt, PubMed und ClinVar — komplett kostenlos ohne API-Key.
27
+
28
+ ## Tools
29
+
30
+ | Tool | Beschreibung |
31
+ |------|-------------|
32
+ | `search_genes` | Gen-Suche in NCBI Gene Database (Symbol, Name, Funktion) |
33
+ | `get_gene_details` | Detailinfos zu einem Gen via NCBI Gene ID |
34
+ | `search_clinvar_variants` | Klinische Varianten/Mutationen via NCBI ClinVar |
35
+ | `get_taxonomy` | Taxonomische Klassifikation via NCBI Taxonomy |
36
+ | `search_proteins` | Protein-Suche in UniProt |
37
+ | `get_protein_details` | Protein-Details via UniProt Accession |
38
+ | `get_ncbi_sequence` | DNA/Protein-Sequenzen aus NCBI (FASTA/GenBank) |
39
+ | `search_pubmed` | Literatur-Suche in PubMed |
40
+ | `get_pubmed_abstract` | Vollständiger Abstract eines PubMed-Artikels |
41
+ | `get_pubmed_citations` | Artikel die eine Publikation zitieren |
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install bioinformatics-mcp-server
47
+ ```
48
+
49
+ ## Konfiguration (Claude Desktop)
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "bioinformatics": {
55
+ "command": "bioinformatics-server"
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Beispiele
62
+
63
+ ```
64
+ # Gen-Suche
65
+ search_genes("BRCA1", organism="human")
66
+ search_genes("p53", organism="mouse")
67
+
68
+ # Klinische Varianten
69
+ search_clinvar_variants("CFTR", significance="pathogenic")
70
+
71
+ # Protein-Lookup
72
+ search_proteins("insulin receptor", organism="human")
73
+ get_protein_details("P06213")
74
+
75
+ # DNA-Sequenz abrufen
76
+ get_ncbi_sequence("NM_007294", database="nucleotide")
77
+
78
+ # Literatur
79
+ search_pubmed("CRISPR gene therapy cancer 2025", max_results=5)
80
+ get_pubmed_abstract("33461210")
81
+ ```
82
+
83
+ ## APIs
84
+
85
+ - **NCBI E-utilities** — Gene, ClinVar, Taxonomy, PubMed, Sequenzen (kostenlos, 3 req/s)
86
+ - **UniProt REST API** — Proteine, Sequenzen, Funktionen (kostenlos, unbegrenzt)
87
+
88
+ Kein API-Key erforderlich.
89
+
90
+ ## Lizenz
91
+
92
+ MIT
@@ -0,0 +1,69 @@
1
+ # bioinformatics-mcp-server
2
+
3
+ MCP-Server für Bioinformatik- und Life-Science-Daten. Gibt AI-Agents Zugriff auf NCBI Gene, UniProt, PubMed und ClinVar — komplett kostenlos ohne API-Key.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Beschreibung |
8
+ |------|-------------|
9
+ | `search_genes` | Gen-Suche in NCBI Gene Database (Symbol, Name, Funktion) |
10
+ | `get_gene_details` | Detailinfos zu einem Gen via NCBI Gene ID |
11
+ | `search_clinvar_variants` | Klinische Varianten/Mutationen via NCBI ClinVar |
12
+ | `get_taxonomy` | Taxonomische Klassifikation via NCBI Taxonomy |
13
+ | `search_proteins` | Protein-Suche in UniProt |
14
+ | `get_protein_details` | Protein-Details via UniProt Accession |
15
+ | `get_ncbi_sequence` | DNA/Protein-Sequenzen aus NCBI (FASTA/GenBank) |
16
+ | `search_pubmed` | Literatur-Suche in PubMed |
17
+ | `get_pubmed_abstract` | Vollständiger Abstract eines PubMed-Artikels |
18
+ | `get_pubmed_citations` | Artikel die eine Publikation zitieren |
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install bioinformatics-mcp-server
24
+ ```
25
+
26
+ ## Konfiguration (Claude Desktop)
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "bioinformatics": {
32
+ "command": "bioinformatics-server"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Beispiele
39
+
40
+ ```
41
+ # Gen-Suche
42
+ search_genes("BRCA1", organism="human")
43
+ search_genes("p53", organism="mouse")
44
+
45
+ # Klinische Varianten
46
+ search_clinvar_variants("CFTR", significance="pathogenic")
47
+
48
+ # Protein-Lookup
49
+ search_proteins("insulin receptor", organism="human")
50
+ get_protein_details("P06213")
51
+
52
+ # DNA-Sequenz abrufen
53
+ get_ncbi_sequence("NM_007294", database="nucleotide")
54
+
55
+ # Literatur
56
+ search_pubmed("CRISPR gene therapy cancer 2025", max_results=5)
57
+ get_pubmed_abstract("33461210")
58
+ ```
59
+
60
+ ## APIs
61
+
62
+ - **NCBI E-utilities** — Gene, ClinVar, Taxonomy, PubMed, Sequenzen (kostenlos, 3 req/s)
63
+ - **UniProt REST API** — Proteine, Sequenzen, Funktionen (kostenlos, unbegrenzt)
64
+
65
+ Kein API-Key erforderlich.
66
+
67
+ ## Lizenz
68
+
69
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bioinformatics-mcp-server"
7
+ version = "0.1.0"
8
+ description = "MCP-Server für Bioinformatik-Daten — Gene, Proteine, PubMed-Literatur, Varianten und Sequenzen via NCBI & UniProt"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "AiAgentKarl" }]
13
+ keywords = ["mcp", "bioinformatics", "genomics", "ncbi", "uniprot", "pubmed", "genes", "proteins", "ai-agent", "life-science"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+ dependencies = [
24
+ "mcp[cli]>=1.0.0",
25
+ "httpx>=0.27.0",
26
+ "python-dotenv>=1.0.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/AiAgentKarl/bioinformatics-mcp-server"
31
+ Repository = "https://github.com/AiAgentKarl/bioinformatics-mcp-server"
32
+ Issues = "https://github.com/AiAgentKarl/bioinformatics-mcp-server/issues"
33
+
34
+ [project.scripts]
35
+ bioinformatics-server = "src.server:main"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src"]
@@ -0,0 +1,17 @@
1
+ name: bioinformatics-mcp-server
2
+ description: MCP-Server for bioinformatics data — genes, proteins, PubMed literature, clinical variants and sequences via NCBI & UniProt (free, no API key required)
3
+ license: MIT
4
+ homepage: https://github.com/AiAgentKarl/bioinformatics-mcp-server
5
+
6
+ startCommand:
7
+ type: stdio
8
+ configSchema:
9
+ type: object
10
+ properties: {}
11
+ required: []
12
+ commandFunction: |-
13
+ (config) => ({
14
+ command: "bioinformatics-server",
15
+ args: [],
16
+ env: {}
17
+ })
File without changes
@@ -0,0 +1,45 @@
1
+ """Bioinformatics MCP Server — Genomik- und Life-Science-Daten für AI-Agents.
2
+
3
+ Bietet:
4
+ - Gen-Suche und -Details via NCBI Gene Database
5
+ - Protein-Lookup via UniProt REST API
6
+ - Wissenschaftliche Literatur via PubMed (NCBI)
7
+ - Klinische Varianten via NCBI ClinVar
8
+ - DNA/Protein-Sequenzen via NCBI Nucleotide
9
+ - Taxonomie-Lookup via NCBI Taxonomy
10
+ Alle Daten kostenlos, kein API-Key erforderlich.
11
+ """
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+ from src.tools.genes import register_gene_tools
16
+ from src.tools.proteins import register_protein_tools
17
+ from src.tools.literature import register_literature_tools
18
+
19
+ # FastMCP Server erstellen
20
+ mcp = FastMCP(
21
+ "Bioinformatics MCP Server",
22
+ instructions=(
23
+ "Gibt AI-Agents Zugriff auf bioinformatische Datenbanken: "
24
+ "NCBI Gene (Gen-Suche, Funktion, Orthologe), "
25
+ "UniProt (Protein-Struktur, Funktion, Sequenzen), "
26
+ "PubMed (wissenschaftliche Literatur, Abstracts), "
27
+ "NCBI ClinVar (klinische Varianten, Pathogenität), "
28
+ "NCBI Taxonomy (Spezies-Klassifikation, Phylogenie). "
29
+ "Alle Endpunkte kostenlos ohne API-Key via NCBI E-utilities und UniProt REST API."
30
+ ),
31
+ )
32
+
33
+ # Tool-Gruppen registrieren
34
+ register_gene_tools(mcp)
35
+ register_protein_tools(mcp)
36
+ register_literature_tools(mcp)
37
+
38
+
39
+ def main():
40
+ """Server starten."""
41
+ mcp.run(transport="stdio")
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
File without changes
@@ -0,0 +1,272 @@
1
+ """Gen-Tools — Suche und Details via NCBI Gene Database und ClinVar."""
2
+
3
+ import httpx
4
+ from typing import Any
5
+
6
+
7
+ # NCBI E-utilities Basis-URL
8
+ NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
9
+ # Tool- und Email-Parameter für NCBI (höflicher Zugriff)
10
+ NCBI_PARAMS = {"tool": "bioinformatics-mcp-server", "email": "agent@aiagentkarl.com"}
11
+
12
+
13
+ async def _ncbi_get(endpoint: str, params: dict) -> dict[str, Any]:
14
+ """NCBI E-utilities API Anfrage."""
15
+ try:
16
+ merged = {**NCBI_PARAMS, **params, "retmode": "json"}
17
+ async with httpx.AsyncClient(timeout=20.0) as client:
18
+ response = await client.get(f"{NCBI_BASE}/{endpoint}", params=merged)
19
+ response.raise_for_status()
20
+ return response.json()
21
+ except Exception as e:
22
+ return {"error": str(e)}
23
+
24
+
25
+ async def _ncbi_fetch_text(endpoint: str, params: dict) -> str:
26
+ """NCBI E-utilities API Anfrage mit Text-Rückgabe (für Sequenzen)."""
27
+ try:
28
+ merged = {**NCBI_PARAMS, **params}
29
+ async with httpx.AsyncClient(timeout=20.0) as client:
30
+ response = await client.get(f"{NCBI_BASE}/{endpoint}", params=merged)
31
+ response.raise_for_status()
32
+ return response.text
33
+ except Exception as e:
34
+ return f"Fehler: {e}"
35
+
36
+
37
+ def register_gene_tools(mcp) -> None:
38
+ """Registriert alle Gen- und Varianten-Tools."""
39
+
40
+ @mcp.tool()
41
+ async def search_genes(
42
+ query: str,
43
+ organism: str = "human",
44
+ max_results: int = 10,
45
+ ) -> dict:
46
+ """Sucht nach Genen in der NCBI Gene Datenbank.
47
+
48
+ Args:
49
+ query: Suchbegriff (z.B. "BRCA1", "TP53", "insulin receptor")
50
+ organism: Organismus (z.B. "human", "mouse", "rat"). Standard: human
51
+ max_results: Maximale Ergebnisanzahl (1-20). Standard: 10
52
+ """
53
+ max_results = min(max(1, max_results), 20)
54
+ # Suchanfrage mit Organismus-Filter
55
+ search_term = f"{query}[Gene Name] AND {organism}[Organism]"
56
+
57
+ # Schritt 1: IDs suchen
58
+ search_data = await _ncbi_get(
59
+ "esearch.fcgi",
60
+ {"db": "gene", "term": search_term, "retmax": max_results},
61
+ )
62
+ if "error" in search_data:
63
+ return {"fehler": search_data["error"]}
64
+
65
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
66
+ if not ids:
67
+ # Breiteren Fallback versuchen
68
+ search_data = await _ncbi_get(
69
+ "esearch.fcgi",
70
+ {"db": "gene", "term": f"{query} AND {organism}[Organism]", "retmax": max_results},
71
+ )
72
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
73
+
74
+ if not ids:
75
+ return {"fehler": f"Keine Gene für '{query}' in '{organism}' gefunden"}
76
+
77
+ # Schritt 2: Zusammenfassungen abrufen
78
+ summary_data = await _ncbi_get(
79
+ "esummary.fcgi",
80
+ {"db": "gene", "id": ",".join(ids)},
81
+ )
82
+ if "error" in summary_data:
83
+ return {"fehler": summary_data["error"]}
84
+
85
+ result_map = summary_data.get("result", {})
86
+ gene_list = []
87
+ for gene_id in ids:
88
+ g = result_map.get(gene_id, {})
89
+ if not g or gene_id == "uids":
90
+ continue
91
+ gene_list.append({
92
+ "gen_id": gene_id,
93
+ "name": g.get("name", ""),
94
+ "vollstaendiger_name": g.get("description", ""),
95
+ "organismus": g.get("organism", {}).get("scientificname", ""),
96
+ "chromosom": g.get("chromosome", ""),
97
+ "position": g.get("maplocation", ""),
98
+ "zusammenfassung": (g.get("summary") or "")[:400],
99
+ "ncbi_url": f"https://www.ncbi.nlm.nih.gov/gene/{gene_id}",
100
+ })
101
+
102
+ return {
103
+ "suchbegriff": query,
104
+ "organismus": organism,
105
+ "gefunden": len(gene_list),
106
+ "gene": gene_list,
107
+ }
108
+
109
+ @mcp.tool()
110
+ async def get_gene_details(gene_id: str) -> dict:
111
+ """Ruft detaillierte Informationen zu einem Gen via NCBI Gene ID ab.
112
+
113
+ Args:
114
+ gene_id: NCBI Gene ID (z.B. "672" für BRCA1, "7157" für TP53)
115
+ """
116
+ data = await _ncbi_get("esummary.fcgi", {"db": "gene", "id": gene_id})
117
+ if "error" in data:
118
+ return {"fehler": data["error"]}
119
+
120
+ result = data.get("result", {}).get(gene_id, {})
121
+ if not result:
122
+ return {"fehler": f"Gen mit ID '{gene_id}' nicht gefunden"}
123
+
124
+ # Verwandte Gene (Gene Neighbors) aus LocationHistList
125
+ orthologe = []
126
+ for orth in result.get("orthologs", [])[:5]:
127
+ orthologe.append({
128
+ "gen_id": orth.get("geneids", [""])[0],
129
+ "organismus": orth.get("scientificname", ""),
130
+ })
131
+
132
+ return {
133
+ "gen_id": gene_id,
134
+ "symbol": result.get("name", ""),
135
+ "vollstaendiger_name": result.get("description", ""),
136
+ "organismus": result.get("organism", {}).get("scientificname", ""),
137
+ "chromosom": result.get("chromosome", ""),
138
+ "position": result.get("maplocation", ""),
139
+ "typ": result.get("genetype", ""),
140
+ "zusammenfassung": result.get("summary", ""),
141
+ "aliases": result.get("otheraliases", ""),
142
+ "andere_bezeichnungen": result.get("otherdesignations", ""),
143
+ "ncbi_url": f"https://www.ncbi.nlm.nih.gov/gene/{gene_id}",
144
+ }
145
+
146
+ @mcp.tool()
147
+ async def search_clinvar_variants(
148
+ gene_symbol: str,
149
+ significance: str = "pathogenic",
150
+ max_results: int = 10,
151
+ ) -> dict:
152
+ """Sucht klinische Varianten (Mutationen) für ein Gen via NCBI ClinVar.
153
+
154
+ Args:
155
+ gene_symbol: Gen-Symbol (z.B. "BRCA1", "TP53", "CFTR")
156
+ significance: Klinische Signifikanz — "pathogenic", "benign", "uncertain" oder "all". Standard: pathogenic
157
+ max_results: Maximale Ergebnisanzahl (1-20). Standard: 10
158
+ """
159
+ max_results = min(max(1, max_results), 20)
160
+
161
+ # Suchanfrage aufbauen
162
+ if significance == "all":
163
+ term = f"{gene_symbol}[gene]"
164
+ else:
165
+ sig_map = {
166
+ "pathogenic": "Pathogenic[ClinSig]",
167
+ "benign": "Benign[ClinSig]",
168
+ "uncertain": "Uncertain significance[ClinSig]",
169
+ }
170
+ sig_filter = sig_map.get(significance, "Pathogenic[ClinSig]")
171
+ term = f"{gene_symbol}[gene] AND {sig_filter}"
172
+
173
+ # IDs suchen
174
+ search_data = await _ncbi_get(
175
+ "esearch.fcgi",
176
+ {"db": "clinvar", "term": term, "retmax": max_results},
177
+ )
178
+ if "error" in search_data:
179
+ return {"fehler": search_data["error"]}
180
+
181
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
182
+ if not ids:
183
+ return {
184
+ "fehler": f"Keine Varianten für '{gene_symbol}' mit Signifikanz '{significance}' gefunden",
185
+ "tipp": "Versuche significance='all' für alle Varianten",
186
+ }
187
+
188
+ # Zusammenfassungen abrufen
189
+ summary_data = await _ncbi_get(
190
+ "esummary.fcgi",
191
+ {"db": "clinvar", "id": ",".join(ids)},
192
+ )
193
+ if "error" in summary_data:
194
+ return {"fehler": summary_data["error"]}
195
+
196
+ result_map = summary_data.get("result", {})
197
+ varianten = []
198
+ for var_id in ids:
199
+ v = result_map.get(var_id, {})
200
+ if not v or var_id == "uids":
201
+ continue
202
+
203
+ # Signifikanz aus clinical_significance
204
+ clin_sig = v.get("clinical_significance", {})
205
+ sig_label = clin_sig.get("description", "")
206
+
207
+ varianten.append({
208
+ "varianten_id": var_id,
209
+ "name": v.get("title", ""),
210
+ "gen": gene_symbol,
211
+ "typ": v.get("obj_type", ""),
212
+ "klinische_signifikanz": sig_label,
213
+ "review_status": clin_sig.get("review_status", ""),
214
+ "bedingung": v.get("trait_set", [{}])[0].get("trait_name", "") if v.get("trait_set") else "",
215
+ "letzte_bewertung": clin_sig.get("last_evaluated", ""),
216
+ "clinvar_url": f"https://www.ncbi.nlm.nih.gov/clinvar/variation/{var_id}/",
217
+ })
218
+
219
+ return {
220
+ "gen": gene_symbol,
221
+ "signifikanz_filter": significance,
222
+ "gefunden": len(varianten),
223
+ "varianten": varianten,
224
+ }
225
+
226
+ @mcp.tool()
227
+ async def get_taxonomy(
228
+ taxon: str,
229
+ ) -> dict:
230
+ """Ruft taxonomische Klassifikation einer Spezies via NCBI Taxonomy ab.
231
+
232
+ Args:
233
+ taxon: Artname oder Taxonomie-ID (z.B. "Homo sapiens", "9606", "Mus musculus")
234
+ """
235
+ # Prüfen ob ID oder Name
236
+ if taxon.isdigit():
237
+ tax_id = taxon
238
+ else:
239
+ # Name zu ID auflösen
240
+ search_data = await _ncbi_get(
241
+ "esearch.fcgi",
242
+ {"db": "taxonomy", "term": taxon},
243
+ )
244
+ if "error" in search_data:
245
+ return {"fehler": search_data["error"]}
246
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
247
+ if not ids:
248
+ return {"fehler": f"Taxonomie für '{taxon}' nicht gefunden"}
249
+ tax_id = ids[0]
250
+
251
+ # Details abrufen
252
+ summary_data = await _ncbi_get(
253
+ "esummary.fcgi",
254
+ {"db": "taxonomy", "id": tax_id},
255
+ )
256
+ if "error" in summary_data:
257
+ return {"fehler": summary_data["error"]}
258
+
259
+ t = summary_data.get("result", {}).get(tax_id, {})
260
+ if not t:
261
+ return {"fehler": f"Taxonomie-ID '{tax_id}' nicht gefunden"}
262
+
263
+ return {
264
+ "taxonomie_id": tax_id,
265
+ "wissenschaftlicher_name": t.get("scientificname", ""),
266
+ "gemeiner_name": t.get("commonname", ""),
267
+ "rang": t.get("rank", ""),
268
+ "abteilung": t.get("division", ""),
269
+ "genetischer_code": t.get("gencodeId", ""),
270
+ "mitochondrialer_code": t.get("mgcodeId", ""),
271
+ "ncbi_url": f"https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id={tax_id}",
272
+ }
@@ -0,0 +1,223 @@
1
+ """Literatur-Tools — Wissenschaftliche Artikel via PubMed (NCBI E-utilities)."""
2
+
3
+ import httpx
4
+ from typing import Any
5
+
6
+
7
+ # NCBI E-utilities Basis-URL
8
+ NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
9
+ NCBI_PARAMS = {"tool": "bioinformatics-mcp-server", "email": "agent@aiagentkarl.com"}
10
+
11
+
12
+ async def _ncbi_get(endpoint: str, params: dict) -> dict[str, Any]:
13
+ """NCBI E-utilities API Anfrage."""
14
+ try:
15
+ merged = {**NCBI_PARAMS, **params, "retmode": "json"}
16
+ async with httpx.AsyncClient(timeout=20.0) as client:
17
+ response = await client.get(f"{NCBI_BASE}/{endpoint}", params=merged)
18
+ response.raise_for_status()
19
+ return response.json()
20
+ except Exception as e:
21
+ return {"error": str(e)}
22
+
23
+
24
+ def register_literature_tools(mcp) -> None:
25
+ """Registriert alle Literatur-Tools."""
26
+
27
+ @mcp.tool()
28
+ async def search_pubmed(
29
+ query: str,
30
+ max_results: int = 10,
31
+ sort: str = "relevance",
32
+ years_back: int = 0,
33
+ ) -> dict:
34
+ """Sucht wissenschaftliche Artikel in PubMed.
35
+
36
+ Args:
37
+ query: Suchbegriff (z.B. "BRCA1 breast cancer therapy", "CRISPR gene editing")
38
+ max_results: Maximale Ergebnisanzahl (1-20). Standard: 10
39
+ sort: Sortierung — "relevance" oder "date". Standard: relevance
40
+ years_back: Nur Artikel der letzten N Jahre (0 = alle). Standard: 0
41
+ """
42
+ max_results = min(max(1, max_results), 20)
43
+
44
+ # Datumsfilter
45
+ search_term = query
46
+ if years_back > 0:
47
+ from datetime import datetime
48
+ aktuelles_jahr = datetime.now().year
49
+ von_jahr = aktuelles_jahr - years_back
50
+ search_term = f"{query} AND ({von_jahr}:{aktuelles_jahr}[pdat])"
51
+
52
+ sort_param = "relevance" if sort == "relevance" else "pub+date"
53
+
54
+ # Schritt 1: Artikel-IDs suchen
55
+ search_data = await _ncbi_get(
56
+ "esearch.fcgi",
57
+ {
58
+ "db": "pubmed",
59
+ "term": search_term,
60
+ "retmax": max_results,
61
+ "sort": sort_param,
62
+ "usehistory": "y",
63
+ },
64
+ )
65
+ if "error" in search_data:
66
+ return {"fehler": search_data["error"]}
67
+
68
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
69
+ count = search_data.get("esearchresult", {}).get("count", "0")
70
+
71
+ if not ids:
72
+ return {
73
+ "fehler": f"Keine Artikel für '{query}' in PubMed gefunden",
74
+ "tipp": "Versuche weniger spezifische Suchbegriffe oder Englisch",
75
+ }
76
+
77
+ # Schritt 2: Zusammenfassungen abrufen
78
+ summary_data = await _ncbi_get(
79
+ "esummary.fcgi",
80
+ {"db": "pubmed", "id": ",".join(ids)},
81
+ )
82
+ if "error" in summary_data:
83
+ return {"fehler": summary_data["error"]}
84
+
85
+ result_map = summary_data.get("result", {})
86
+ artikel = []
87
+
88
+ for pmid in ids:
89
+ a = result_map.get(pmid, {})
90
+ if not a or pmid == "uids":
91
+ continue
92
+
93
+ # Autoren extrahieren
94
+ autoren = []
95
+ for author in a.get("authors", [])[:5]:
96
+ autoren.append(author.get("name", ""))
97
+
98
+ artikel.append({
99
+ "pmid": pmid,
100
+ "titel": a.get("title", ""),
101
+ "autoren": autoren,
102
+ "journal": a.get("fulljournalname", a.get("source", "")),
103
+ "erscheinungsjahr": a.get("pubdate", "")[:4],
104
+ "doi": a.get("elocationid", ""),
105
+ "abstract_url": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/",
106
+ })
107
+
108
+ return {
109
+ "suchbegriff": query,
110
+ "gesamt_treffer": int(count),
111
+ "angezeigt": len(artikel),
112
+ "sortierung": sort,
113
+ "artikel": artikel,
114
+ }
115
+
116
+ @mcp.tool()
117
+ async def get_pubmed_abstract(pmid: str) -> dict:
118
+ """Ruft den vollständigen Abstract eines PubMed-Artikels ab.
119
+
120
+ Args:
121
+ pmid: PubMed ID (z.B. "33461210", "36257307")
122
+ """
123
+ # Abstract via efetch abrufen
124
+ try:
125
+ merged = {**NCBI_PARAMS, "db": "pubmed", "id": pmid, "rettype": "abstract", "retmode": "text"}
126
+ async with httpx.AsyncClient(timeout=20.0) as client:
127
+ response = await client.get(f"{NCBI_BASE}/efetch.fcgi", params=merged)
128
+ response.raise_for_status()
129
+ abstract_text = response.text
130
+ except Exception as e:
131
+ return {"fehler": str(e)}
132
+
133
+ # Zusätzliche Metadaten
134
+ summary_data = await _ncbi_get("esummary.fcgi", {"db": "pubmed", "id": pmid})
135
+ meta = summary_data.get("result", {}).get(pmid, {})
136
+
137
+ autoren = []
138
+ for author in meta.get("authors", []):
139
+ autoren.append(author.get("name", ""))
140
+
141
+ return {
142
+ "pmid": pmid,
143
+ "titel": meta.get("title", ""),
144
+ "autoren": autoren,
145
+ "journal": meta.get("fulljournalname", meta.get("source", "")),
146
+ "erscheinungsdatum": meta.get("pubdate", ""),
147
+ "doi": meta.get("elocationid", ""),
148
+ "abstract": abstract_text.strip(),
149
+ "pubmed_url": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/",
150
+ }
151
+
152
+ @mcp.tool()
153
+ async def get_pubmed_citations(
154
+ pmid: str,
155
+ max_results: int = 10,
156
+ ) -> dict:
157
+ """Ruft Artikel ab, die einen bestimmten PubMed-Artikel zitieren.
158
+
159
+ Args:
160
+ pmid: PubMed ID des zitierten Artikels (z.B. "33461210")
161
+ max_results: Maximale Ergebnisanzahl (1-20). Standard: 10
162
+ """
163
+ max_results = min(max(1, max_results), 20)
164
+
165
+ # elink: Artikel finden die diesen zitieren
166
+ try:
167
+ params = {
168
+ **NCBI_PARAMS,
169
+ "dbfrom": "pubmed",
170
+ "db": "pubmed",
171
+ "id": pmid,
172
+ "linkname": "pubmed_pubmed_citedin",
173
+ "retmode": "json",
174
+ }
175
+ async with httpx.AsyncClient(timeout=20.0) as client:
176
+ response = await client.get(f"{NCBI_BASE}/elink.fcgi", params=params)
177
+ response.raise_for_status()
178
+ link_data = response.json()
179
+ except Exception as e:
180
+ return {"fehler": str(e)}
181
+
182
+ # IDs aus linksets extrahieren
183
+ citing_ids = []
184
+ for linkset in link_data.get("linksets", []):
185
+ for lsd in linkset.get("linksetdbs", []):
186
+ if lsd.get("linkname") == "pubmed_pubmed_citedin":
187
+ citing_ids = lsd.get("links", [])[:max_results]
188
+ break
189
+
190
+ if not citing_ids:
191
+ return {
192
+ "pmid": pmid,
193
+ "zitiert_von": 0,
194
+ "artikel": [],
195
+ "hinweis": "Keine Zitationen in PubMed gefunden (nur indexierte Artikel werden erfasst)",
196
+ }
197
+
198
+ # Zusammenfassungen der zitierenden Artikel
199
+ id_str = ",".join(str(i) for i in citing_ids)
200
+ summary_data = await _ncbi_get("esummary.fcgi", {"db": "pubmed", "id": id_str})
201
+ result_map = summary_data.get("result", {})
202
+
203
+ artikel = []
204
+ for cid in citing_ids:
205
+ cid_str = str(cid)
206
+ a = result_map.get(cid_str, {})
207
+ if not a or cid_str == "uids":
208
+ continue
209
+ autoren = [auth.get("name", "") for auth in a.get("authors", [])[:3]]
210
+ artikel.append({
211
+ "pmid": cid_str,
212
+ "titel": a.get("title", ""),
213
+ "autoren": autoren,
214
+ "journal": a.get("fulljournalname", a.get("source", "")),
215
+ "erscheinungsjahr": a.get("pubdate", "")[:4],
216
+ "url": f"https://pubmed.ncbi.nlm.nih.gov/{cid_str}/",
217
+ })
218
+
219
+ return {
220
+ "original_pmid": pmid,
221
+ "gefundene_zitationen": len(artikel),
222
+ "artikel": artikel,
223
+ }
@@ -0,0 +1,242 @@
1
+ """Protein-Tools — Lookup und Sequenzen via UniProt REST API."""
2
+
3
+ import httpx
4
+ from typing import Any
5
+
6
+
7
+ # UniProt REST API Basis-URL
8
+ UNIPROT_BASE = "https://rest.uniprot.org/uniprotkb"
9
+ # NCBI E-utilities für Sequenzen
10
+ NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
11
+ NCBI_PARAMS = {"tool": "bioinformatics-mcp-server", "email": "agent@aiagentkarl.com"}
12
+
13
+
14
+ async def _uniprot_get(endpoint: str, params: dict = None) -> dict[str, Any]:
15
+ """UniProt REST API Anfrage."""
16
+ try:
17
+ async with httpx.AsyncClient(
18
+ timeout=20.0,
19
+ headers={"Accept": "application/json"},
20
+ ) as client:
21
+ response = await client.get(
22
+ f"{UNIPROT_BASE}/{endpoint}",
23
+ params=params or {},
24
+ )
25
+ response.raise_for_status()
26
+ return response.json()
27
+ except Exception as e:
28
+ return {"error": str(e)}
29
+
30
+
31
+ async def _ncbi_fetch_text(endpoint: str, params: dict) -> str:
32
+ """NCBI E-utilities Text-Anfrage für Sequenzen."""
33
+ try:
34
+ merged = {**NCBI_PARAMS, **params}
35
+ async with httpx.AsyncClient(timeout=20.0) as client:
36
+ response = await client.get(f"{NCBI_BASE}/{endpoint}", params=merged)
37
+ response.raise_for_status()
38
+ return response.text
39
+ except Exception as e:
40
+ return f"Fehler: {e}"
41
+
42
+
43
+ def register_protein_tools(mcp) -> None:
44
+ """Registriert alle Protein-Tools."""
45
+
46
+ @mcp.tool()
47
+ async def search_proteins(
48
+ query: str,
49
+ organism: str = "human",
50
+ max_results: int = 10,
51
+ ) -> dict:
52
+ """Sucht nach Proteinen in der UniProt-Datenbank.
53
+
54
+ Args:
55
+ query: Suchbegriff (z.B. "BRCA1", "insulin receptor", "p53 tumor suppressor")
56
+ organism: Organismus (z.B. "human", "mouse", "E. coli"). Standard: human
57
+ max_results: Maximale Ergebnisanzahl (1-25). Standard: 10
58
+ """
59
+ max_results = min(max(1, max_results), 25)
60
+
61
+ # UniProt Suchanfrage
62
+ search_query = f"({query}) AND (organism_name:{organism})"
63
+ data = await _uniprot_get(
64
+ "search",
65
+ {
66
+ "query": search_query,
67
+ "size": max_results,
68
+ "fields": "accession,id,protein_name,gene_names,organism_name,length,reviewed,function",
69
+ },
70
+ )
71
+ if "error" in data:
72
+ return {"fehler": data["error"]}
73
+
74
+ results = data.get("results", [])
75
+ if not results:
76
+ return {"fehler": f"Keine Proteine für '{query}' in '{organism}' gefunden"}
77
+
78
+ proteine = []
79
+ for p in results:
80
+ # Proteinname extrahieren
81
+ prot_names = p.get("proteinDescription", {})
82
+ rec_name = prot_names.get("recommendedName", {})
83
+ full_name = rec_name.get("fullName", {}).get("value", "")
84
+ if not full_name:
85
+ sub_names = prot_names.get("submittedNames", [])
86
+ if sub_names:
87
+ full_name = sub_names[0].get("fullName", {}).get("value", "")
88
+
89
+ # Gen-Namen
90
+ gene_names = []
91
+ for gn in p.get("genes", []):
92
+ gn_val = gn.get("geneName", {}).get("value", "")
93
+ if gn_val:
94
+ gene_names.append(gn_val)
95
+
96
+ # Funktion (erstes Kommentar)
97
+ funktion = ""
98
+ for kommentar in p.get("comments", []):
99
+ if kommentar.get("commentType") == "FUNCTION":
100
+ texts = kommentar.get("texts", [])
101
+ if texts:
102
+ funktion = texts[0].get("value", "")[:300]
103
+ break
104
+
105
+ proteine.append({
106
+ "accession": p.get("primaryAccession", ""),
107
+ "entry_name": p.get("uniProtkbId", ""),
108
+ "protein_name": full_name,
109
+ "gene_namen": gene_names,
110
+ "organismus": p.get("organism", {}).get("scientificName", ""),
111
+ "laenge": p.get("sequence", {}).get("length", 0),
112
+ "reviewed": p.get("entryType", "") == "UniProtKB reviewed (Swiss-Prot)",
113
+ "funktion": funktion,
114
+ "uniprot_url": f"https://www.uniprot.org/uniprotkb/{p.get('primaryAccession', '')}",
115
+ })
116
+
117
+ return {
118
+ "suchbegriff": query,
119
+ "organismus": organism,
120
+ "gefunden": len(proteine),
121
+ "proteine": proteine,
122
+ }
123
+
124
+ @mcp.tool()
125
+ async def get_protein_details(accession: str) -> dict:
126
+ """Ruft detaillierte Informationen zu einem Protein via UniProt Accession ab.
127
+
128
+ Args:
129
+ accession: UniProt Accession-Nummer (z.B. "P38398" für BRCA1, "P04637" für TP53)
130
+ """
131
+ data = await _uniprot_get(accession)
132
+ if "error" in data:
133
+ return {"fehler": data["error"]}
134
+
135
+ # Protein-Name
136
+ prot_names = data.get("proteinDescription", {})
137
+ rec_name = prot_names.get("recommendedName", {})
138
+ full_name = rec_name.get("fullName", {}).get("value", "")
139
+ kurz_name = rec_name.get("shortNames", [{}])[0].get("value", "") if rec_name.get("shortNames") else ""
140
+
141
+ # Gen-Namen
142
+ gene_namen = []
143
+ for gn in data.get("genes", []):
144
+ gn_val = gn.get("geneName", {}).get("value", "")
145
+ if gn_val:
146
+ gene_namen.append(gn_val)
147
+
148
+ # Funktion
149
+ funktion = ""
150
+ subunit = ""
151
+ for kommentar in data.get("comments", []):
152
+ ct = kommentar.get("commentType", "")
153
+ texts = kommentar.get("texts", [])
154
+ if ct == "FUNCTION" and texts and not funktion:
155
+ funktion = texts[0].get("value", "")[:500]
156
+ elif ct == "SUBUNIT" and texts and not subunit:
157
+ subunit = texts[0].get("value", "")[:300]
158
+
159
+ # Keywords
160
+ keywords = [kw.get("name", "") for kw in data.get("keywords", [])[:10]]
161
+
162
+ # Sequenz-Info
163
+ seq = data.get("sequence", {})
164
+
165
+ return {
166
+ "accession": accession,
167
+ "entry_name": data.get("uniProtkbId", ""),
168
+ "protein_name": full_name,
169
+ "kurzname": kurz_name,
170
+ "gene_namen": gene_namen,
171
+ "organismus": data.get("organism", {}).get("scientificName", ""),
172
+ "taxonomie_id": data.get("organism", {}).get("taxonId", ""),
173
+ "laenge_aminosaeuren": seq.get("length", 0),
174
+ "molekulargewicht": seq.get("molWeight", 0),
175
+ "funktion": funktion,
176
+ "untereinheit": subunit,
177
+ "keywords": keywords,
178
+ "reviewed": data.get("entryType", "") == "UniProtKB reviewed (Swiss-Prot)",
179
+ "sequenz": (seq.get("value", "") or "")[:200] + ("..." if len(seq.get("value", "") or "") > 200 else ""),
180
+ "uniprot_url": f"https://www.uniprot.org/uniprotkb/{accession}",
181
+ }
182
+
183
+ @mcp.tool()
184
+ async def get_ncbi_sequence(
185
+ accession: str,
186
+ database: str = "nucleotide",
187
+ sequence_type: str = "fasta",
188
+ ) -> dict:
189
+ """Ruft eine DNA- oder Protein-Sequenz aus NCBI ab.
190
+
191
+ Args:
192
+ accession: NCBI Accession-Nummer (z.B. "NM_007294" für BRCA1 mRNA, "NP_009225" für BRCA1 Protein)
193
+ database: Datenbank — "nucleotide" (DNA/RNA) oder "protein". Standard: nucleotide
194
+ sequence_type: Format — "fasta" (Sequenz) oder "gb" (GenBank-Eintrag). Standard: fasta
195
+ """
196
+ valid_dbs = ("nucleotide", "protein")
197
+ if database not in valid_dbs:
198
+ return {"fehler": f"Ungültige Datenbank. Erlaubt: {', '.join(valid_dbs)}"}
199
+
200
+ valid_types = ("fasta", "gb")
201
+ if sequence_type not in valid_types:
202
+ return {"fehler": f"Ungültiger Sequenztyp. Erlaubt: {', '.join(valid_types)}"}
203
+
204
+ # Zuerst ID suchen
205
+ params = {**NCBI_PARAMS, "retmode": "json"}
206
+ try:
207
+ async with httpx.AsyncClient(timeout=20.0) as client:
208
+ search_resp = await client.get(
209
+ f"{NCBI_BASE}/esearch.fcgi",
210
+ params={**params, "db": database, "term": accession},
211
+ )
212
+ search_resp.raise_for_status()
213
+ search_data = search_resp.json()
214
+ except Exception as e:
215
+ return {"fehler": str(e)}
216
+
217
+ ids = search_data.get("esearchresult", {}).get("idlist", [])
218
+ if not ids:
219
+ return {"fehler": f"Sequenz '{accession}' in '{database}' nicht gefunden"}
220
+
221
+ # Sequenz abrufen
222
+ ret_type = "fasta" if sequence_type == "fasta" else "gb"
223
+ sequenz = await _ncbi_fetch_text(
224
+ "efetch.fcgi",
225
+ {"db": database, "id": ids[0], "rettype": ret_type, "retmode": "text"},
226
+ )
227
+
228
+ # FASTA kürzen für Ausgabe
229
+ zeilen = sequenz.strip().split("\n")
230
+ header = zeilen[0] if zeilen else ""
231
+ seq_daten = "".join(zeilen[1:]) if len(zeilen) > 1 else ""
232
+
233
+ return {
234
+ "accession": accession,
235
+ "datenbank": database,
236
+ "format": sequence_type,
237
+ "ncbi_id": ids[0],
238
+ "header": header,
239
+ "sequenz_laenge": len(seq_daten),
240
+ "sequenz_vorschau": seq_daten[:300] + ("..." if len(seq_daten) > 300 else ""),
241
+ "vollstaendige_sequenz": seq_daten if len(seq_daten) <= 2000 else seq_daten[:2000] + f"... [{len(seq_daten) - 2000} weitere Zeichen]",
242
+ }