From 2e3634eff4553873b88acdec73af33d6a7ffec00 Mon Sep 17 00:00:00 2001 From: Presisitence Date: Wed, 19 Aug 2026 22:05:50 +0800 Subject: [PATCH] Add plugin plant-public Public plant-pathology MCP for MiniMax Code: UniProt, NCBI, InterPro, PDB, AlphaFold, Ensembl Plants, and Sol Genomics. --- plugins/Presisitence/plant-public/LICENSE | 21 ++ plugins/Presisitence/plant-public/README.md | 47 +++++ .../plant-public/bio_toolkit/__init__.py | 13 ++ .../plant-public/bio_toolkit/atted.py | 43 ++++ .../plant-public/bio_toolkit/bar.py | 101 ++++++++++ .../plant-public/bio_toolkit/cli.py | 162 +++++++++++++++ .../plant-public/bio_toolkit/http.py | 98 ++++++++++ .../plant-public/bio_toolkit/interpro.py | 68 +++++++ .../plant-public/bio_toolkit/ncbi.py | 65 ++++++ .../plant-public/bio_toolkit/plant.py | 185 ++++++++++++++++++ .../plant-public/bio_toolkit/stringdb.py | 51 +++++ .../plant-public/bio_toolkit/structure.py | 91 +++++++++ .../plant-public/bio_toolkit/uniprot.py | 83 ++++++++ plugins/Presisitence/plant-public/mcp.json | 11 ++ plugins/Presisitence/plant-public/plugin.json | 22 +++ .../Presisitence/plant-public/pyproject.toml | 21 ++ plugins/Presisitence/plant-public/server.py | 185 ++++++++++++++++++ .../plant-public/skills/plant-public/SKILL.md | 24 +++ 18 files changed, 1291 insertions(+) create mode 100644 plugins/Presisitence/plant-public/LICENSE create mode 100644 plugins/Presisitence/plant-public/README.md create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/__init__.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/atted.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/bar.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/cli.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/http.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/interpro.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/ncbi.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/plant.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/stringdb.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/structure.py create mode 100644 plugins/Presisitence/plant-public/bio_toolkit/uniprot.py create mode 100644 plugins/Presisitence/plant-public/mcp.json create mode 100644 plugins/Presisitence/plant-public/plugin.json create mode 100644 plugins/Presisitence/plant-public/pyproject.toml create mode 100644 plugins/Presisitence/plant-public/server.py create mode 100644 plugins/Presisitence/plant-public/skills/plant-public/SKILL.md diff --git a/plugins/Presisitence/plant-public/LICENSE b/plugins/Presisitence/plant-public/LICENSE new file mode 100644 index 0000000..9637979 --- /dev/null +++ b/plugins/Presisitence/plant-public/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Presisitence + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/Presisitence/plant-public/README.md b/plugins/Presisitence/plant-public/README.md new file mode 100644 index 0000000..7e767fa --- /dev/null +++ b/plugins/Presisitence/plant-public/README.md @@ -0,0 +1,47 @@ +# plant-public + +Public plant-pathology bioinformatics for MiniMax Code: UniProt, NCBI, InterProScan, PDB, +AlphaFold DB, Ensembl Plants, Sol Genomics (Solanaceae), and Arabidopsis BAR / ATTED / STRING. + +This Plugin ships a stdio MCP server. It does not bundle any local genomes or RNA-seq matrices. +All sequence and annotation data come from public APIs. Web-only tools (SignalP, PlantCARE, …) +return submission guidance rather than invented results. + +## Try it + +```text +Look up tomato WRKY transcription factors in UniProt and NCBI. Then get InterProScan domains +for one protein and fetch its AlphaFold model if a UniProt accession exists. +``` + +```text +用 Ensembl Plants 查 solanum_lycopersicum 的 NPR1 同源基因,再给出 Sol Genomics BLAST 该怎么提交。 +``` + +Expected result: the agent calls `uniprot_search` / `ncbi_search` / `ensembl_plants_gene` (or +`plant_resource_guide("solgenomics")` for the SGN web path), returns accessions, FASTA or domain +tables from the live APIs, and marks any web-only step as a guide rather than fake data. + +## Requirements + +- Python 3.10+ and [uv](https://docs.astral.sh/uv/) on PATH (`mcp.json` starts the server with `uv run server.py`). +- Optional `NCBI_API_KEY` environment variable to raise NCBI rate limits. No key is required. +- Windows, macOS, and Linux. + +## Data and network + +The MCP server contacts public scholarly APIs only: + +- `rest.uniprot.org` +- `eutils.ncbi.nlm.nih.gov` +- `www.ebi.ac.uk` (InterProScan) +- `data.rcsb.org` / RCSB PDB search +- `alphafold.ebi.ac.uk` +- `rest.ensembl.org` (Ensembl Plants) +- `bar.utoronto.ca`, `atted.jp`, `string-db.org` (Arabidopsis) + +No telemetry. No credentials in the package. Optional `NCBI_API_KEY` stays in the user's environment. + +## License + +MIT. See [LICENSE](LICENSE). Upstream databases have their own terms; respect rate limits. diff --git a/plugins/Presisitence/plant-public/bio_toolkit/__init__.py b/plugins/Presisitence/plant-public/bio_toolkit/__init__.py new file mode 100644 index 0000000..519ba1c --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/__init__.py @@ -0,0 +1,13 @@ +"""植物抗病生物信息学公共接口工具集(不含本地基因组/转录组)。 + +- uniprot : 蛋白序列与功能注释 (UniProt REST) +- ncbi : 序列/基因/文献检索 (NCBI E-utilities) +- interpro : 结构域/家族注释 (EBI InterProScan REST) +- structure : 蛋白结构 (RCSB PDB + AlphaFold DB) +- plant : 植物垂直资源指引 + Ensembl Plants / Sol Genomics +- bar : 拟南芥 BAR/ThaleMine +- atted : 拟南芥 ATTED-II 共表达 +- stringdb : 拟南芥 STRING 蛋白互作 + 功能富集 (species=3702) +""" + +__version__ = "0.1.0" diff --git a/plugins/Presisitence/plant-public/bio_toolkit/atted.py b/plugins/Presisitence/plant-public/bio_toolkit/atted.py new file mode 100644 index 0000000..aa52f8b --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/atted.py @@ -0,0 +1,43 @@ +"""ATTED-II (atted.jp) 共表达数据库 API v5 封装。 + +拟南芥共表达网络:给定基因返回共表达基因及 z 分数(找共调控基因/候选上游 TF)。 +公开 REST API(GET,JSON)。 +""" +from __future__ import annotations + +from typing import Any + +from .http import get_json + +ATTED_API5 = "https://atted.jp/api5/" +# 平台→当前数据库 id(版本随 ATTED 更新,可用 db 参数覆盖;截至 2026-07)。 +# 注意:ATTED 只认带版本号的库 id,裸 'Ath-r' 会被静默当成统一库。 +_PLATFORM_DB = {"u": None, "r": "Ath-r.c5-0", "m": "Ath-m.c8-0"} + + +def coexpression( + gene_id: str, + top_n: int = 100, + cutoff: float | None = None, + platform: str = "u", + db: str | None = None, +) -> dict[str, Any]: + """拟南芥共表达基因(AGI + z 分数)。gene_id: AGI(如 At3g26440) 或 Entrez ID。 + platform: u/m/r(统一/微阵列/RNA-seq);db 显式指定完整 ATTED 库 id(如 Ath-r.c5-0)时优先于 platform。""" + pf = platform.lower() + if db is None and pf not in _PLATFORM_DB: + return {"error": f"platform 须为 u/m/r,收到 {platform!r}", "platforms": list(_PLATFORM_DB)} + resolved_db = db if db is not None else _PLATFORM_DB.get(pf) + params: dict[str, Any] = {"gene": gene_id.strip(), "topN": top_n} + if cutoff is not None: + params["value"] = cutoff + if resolved_db: + params["db"] = resolved_db + return { + "gene_id": gene_id.strip(), + "platform": pf if db is None else None, + "db": resolved_db, + "top_n": top_n, + "cutoff": cutoff, + "result": get_json(ATTED_API5, params=params), + } diff --git a/plugins/Presisitence/plant-public/bio_toolkit/bar.py b/plugins/Presisitence/plant-public/bio_toolkit/bar.py new file mode 100644 index 0000000..571434c --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/bar.py @@ -0,0 +1,101 @@ +"""BAR (Bio-Analytic Resource, bar.utoronto.ca) API 封装。 + +拟南芥模式参考数据:基因信息、ThaleMine 功能注释/GeneRIF/文献、 +eFP 表达数值与 eFP 图像(含 Biotic Stress 生物胁迫)。 +全部为公开 REST API(GET,JSON)。绝不编造返回数据:失败时返回错误与可选项。 +""" +from __future__ import annotations + +from typing import Any + +from .http import BioHTTPError, get, get_json + +BAR_API = "https://bar.utoronto.ca/api" + + +def _unwrap(resp: Any) -> Any: + """BAR 多数端点包一层 {"wasSuccessful":true,"data":...};成功则取 data,否则原样返回。""" + if isinstance(resp, dict) and resp.get("wasSuccessful") and "data" in resp: + return resp["data"] + return resp + + +def _intermine_rows(resp: Any) -> list[dict[str, Any]]: + """把 ThaleMine(InterMine) 的 {views/columnHeaders, results:[[...]]} 转成按列名归键的字典列表。""" + if not isinstance(resp, dict): + return [] + cols = resp.get("views") or resp.get("columnHeaders") or [] + out: list[dict[str, Any]] = [] + for row in resp.get("results") or []: + out.append({c: v for c, v in zip(cols, row)} if isinstance(row, list) else row) + return out + + +def gene_info(gene_id: str, species: str = "arabidopsis") -> dict[str, Any]: + """BAR 基因信息(位点/链向/别名/注释)。gene_id 如 AT3G26440。""" + gid = gene_id.strip() + resp = get_json(f"{BAR_API}/gene_information/single_gene_query/{species}/{gid}") + return {"gene_id": gid, "species": species, "data": _unwrap(resp)} + + +def gene_function(gene_id: str) -> dict[str, Any]: + """ThaleMine 拟南芥基因功能注释 + GeneRIF。gene_id 为 AGI(如 At3g26440)。""" + gid = gene_id.strip() + info = get_json(f"{BAR_API}/thalemine/gene_information/{gid}") + rifs = get_json(f"{BAR_API}/thalemine/gene_rifs/{gid}") + return {"gene_id": gid, "gene_information": _intermine_rows(info), "gene_rifs": _intermine_rows(rifs)} + + +def publications(gene_id: str) -> dict[str, Any]: + """ThaleMine 拟南芥基因相关文献。gene_id 为 AGI。""" + gid = gene_id.strip() + pubs = get_json(f"{BAR_API}/thalemine/publications/{gid}") + return {"gene_id": gid, "publications": _intermine_rows(pubs)} + + +def efp_views(species: str = "arabidopsis", view: str | None = None) -> dict[str, Any]: + """eFP 发现工具。view 缺省:列出全部 view→database 映射; + 给定 view(如 Biotic_Stress):返回该视图对照/处理样本分组(病原实验设计)。""" + if view: + resp = get_json(f"{BAR_API}/microarray_gene_expression/{species}/{view}/samples") + return _unwrap(resp) # 已含 {species, view, groups} + resp = get_json(f"{BAR_API}/microarray_gene_expression/{species}/databases") + return _unwrap(resp) # 已含 {species, databases} + + +def efp_expression(gene_id: str, database: str = "klepikova", species: str = "arabidopsis") -> dict[str, Any]: + """取基因在指定 eFP database 的表达数值(RNA-seq 类库如 klepikova 可用)。 + 注:微阵列库(atgenexp_*/生物胁迫)此端点暂不支持数值——生物胁迫请用 efp_image 取图、efp_views 取实验设计。""" + gid = gene_id.strip() + url = f"{BAR_API}/gene_expression/expression/{database}/{gid}" + try: + resp = get_json(url) + except BioHTTPError as exc: + return { + "gene_id": gid, + "database": database, + "error": f"该 database 在表达值端点不可用({exc})。RNA-seq 库(如 klepikova)可用;" + "微阵列库/生物胁迫请用 bar_efp_image 取图或 bar_efp_views 查实验设计。", + } + return {"gene_id": gid, "database": database, "species": species, "data": _unwrap(resp)} + + +def efp_image_url( + gene_id: str, + view: str = "Biotic_Stress", + mode: str = "Absolute", + species: str = "arabidopsis", + check: bool = False, +) -> dict[str, Any]: + """拟南芥基因 eFP 图像 URL(含 Biotic_Stress 生物胁迫;默认只返回 URL,不下载/不落盘)。 + view 如 Biotic_Stress/Abiotic_Stress/Developmental_Map;mode Absolute/Relative/Compare。 + check=True 时做一次状态探测(会请求但不保存)。""" + gid = gene_id.strip() + url = f"{BAR_API}/efp_image/efp_{species}/{view}/{mode}/{gid}" + reachable: bool | None = None + if check: + try: + reachable = get(url).status_code == 200 + except BioHTTPError: + reachable = False + return {"gene_id": gid, "view": view, "mode": mode, "url": url, "reachable": reachable} diff --git a/plugins/Presisitence/plant-public/bio_toolkit/cli.py b/plugins/Presisitence/plant-public/bio_toolkit/cli.py new file mode 100644 index 0000000..2b83603 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/cli.py @@ -0,0 +1,162 @@ +"""独立命令行入口:不启动 MCP 也能直接调度公共数据库。 + +用法示例: + uv run plant-cli uniprot summary P0DP23 + uv run plant-cli ncbi search protein "Solanum lycopersicum[Organism] AND WRKY[Gene]" + uv run plant-cli alphafold P0DP23 + uv run plant-cli plant list + uv run plant-cli plant guide solgenomics +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +from . import ( + atted, + bar, + interpro, + ncbi, + plant, + stringdb, + structure, + uniprot, +) + + +def _out(obj: Any) -> None: + if isinstance(obj, str): + print(obj) + else: + print(json.dumps(obj, ensure_ascii=False, indent=2)) + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="plant-cli", description="植物抗病生信公共接口 CLI") + sub = p.add_subparsers(dest="cmd", required=True) + + up = sub.add_parser("uniprot", help="UniProt") + up.add_argument("action", choices=["summary", "fasta", "search"]) + up.add_argument("value") + up.add_argument("--size", type=int, default=25) + + nc = sub.add_parser("ncbi", help="NCBI E-utilities") + nc.add_argument("action", choices=["search", "fetch", "search-fetch"]) + nc.add_argument("db") + nc.add_argument("value") + nc.add_argument("--rettype", default="fasta") + nc.add_argument("--retmax", type=int, default=10) + + af = sub.add_parser("alphafold", help="AlphaFold DB") + af.add_argument("uniprot") + + pdb = sub.add_parser("pdb", help="RCSB PDB 条目") + pdb.add_argument("pdb_id") + + ip = sub.add_parser("interpro", help="InterProScan") + ip.add_argument("action", choices=["run", "status", "result"]) + ip.add_argument("arg1", help="run: fasta文件; status/result: job_id") + ip.add_argument("email_or_type", nargs="?", default="tsv") + + pl = sub.add_parser("plant", help="植物垂直资源") + pl.add_argument("action", choices=["list", "guide", "ensembl", "taxid"]) + pl.add_argument("arg1", nargs="?") + pl.add_argument("arg2", nargs="?") + + ba = sub.add_parser("bar", help="BAR/ThaleMine 拟南芥资源") + ba.add_argument("action", choices=["info", "function", "pubs", "views", "expr", "image"]) + ba.add_argument("arg1", nargs="?", help="基因ID (AGI);views 时为 view 名(可选)") + ba.add_argument("arg2", nargs="?", help="expr: database;image: view") + + at = sub.add_parser("atted", help="ATTED-II 拟南芥共表达") + at.add_argument("gene", help="基因ID (AGI)") + at.add_argument("--top", type=int, default=100) + at.add_argument("--platform", default="u", help="u/m/r") + + st = sub.add_parser("string", help="STRING 拟南芥互作/富集 (species=3702)") + st.add_argument("action", choices=["partners", "enrichment"]) + st.add_argument("genes") + st.add_argument("--score", type=int, default=400) + return p + + +def main(argv: list[str] | None = None) -> int: + try: + sys.stdout.reconfigure(encoding="utf-8") + except (AttributeError, ValueError): + pass + args = build_parser().parse_args(argv) + + if args.cmd == "uniprot": + if args.action == "summary": + _out(uniprot.summarize(args.value)) + elif args.action == "fasta": + _out(uniprot.get_fasta(args.value)) + else: + _out(uniprot.search(args.value, size=args.size)) + + elif args.cmd == "ncbi": + if args.action == "search": + _out(ncbi.esearch(args.db, args.value, retmax=args.retmax)) + elif args.action == "fetch": + _out(ncbi.efetch(args.db, args.value, rettype=args.rettype)) + else: + _out(ncbi.search_and_fetch(args.db, args.value, rettype=args.rettype, retmax=args.retmax)) + + elif args.cmd == "alphafold": + _out(structure.alphafold_summary(args.uniprot)) + + elif args.cmd == "pdb": + _out(structure.pdb_entry(args.pdb_id)) + + elif args.cmd == "interpro": + if args.action == "run": + seq = Path(args.arg1).read_text(encoding="utf-8") if Path(args.arg1).exists() else args.arg1 + _out(interpro.run_and_wait(seq, args.email_or_type)) + elif args.action == "status": + _out({"job_id": args.arg1, "status": interpro.status(args.arg1)}) + else: + _out(interpro.result(args.arg1, args.email_or_type)) + + elif args.cmd == "plant": + if args.action == "list": + _out(plant.list_resources()) + elif args.action == "guide": + _out(plant.get_resource(args.arg1 or "")) + elif args.action == "ensembl": + _out(plant.ensembl_lookup_symbol(args.arg1, args.arg2)) + else: + key = (args.arg1 or "").lower() + _out({"species": key, "taxid": plant.TAXIDS.get(key), "known": list(plant.TAXIDS)}) + + elif args.cmd == "bar": + if args.action == "info": + _out(bar.gene_info(args.arg1)) + elif args.action == "function": + _out(bar.gene_function(args.arg1)) + elif args.action == "pubs": + _out(bar.publications(args.arg1)) + elif args.action == "views": + _out(bar.efp_views(view=args.arg1 or None)) + elif args.action == "expr": + _out(bar.efp_expression(args.arg1, database=args.arg2 or "klepikova")) + else: + _out(bar.efp_image_url(args.arg1, view=args.arg2 or "Biotic_Stress")) + + elif args.cmd == "atted": + _out(atted.coexpression(args.gene, top_n=args.top, platform=args.platform)) + + elif args.cmd == "string": + if args.action == "partners": + _out(stringdb.interaction_partners(args.genes, required_score=args.score)) + else: + _out(stringdb.enrichment(args.genes)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/Presisitence/plant-public/bio_toolkit/http.py b/plugins/Presisitence/plant-public/bio_toolkit/http.py new file mode 100644 index 0000000..ed14985 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/http.py @@ -0,0 +1,98 @@ +"""共享 HTTP 工具层:统一超时、重试、错误处理与 User-Agent。 + +所有对外 API 请求都经过这里,保证行为一致、可复现。 +""" +from __future__ import annotations + +import time +from typing import Any + +import httpx + +# 礼貌性 User-Agent:部分数据库要求标识来源 +USER_AGENT = "plant-public-mcp/0.1 (plant disease resistance research; contact: researcher)" + +DEFAULT_TIMEOUT = 30.0 +MAX_RETRIES = 3 +RETRY_BACKOFF = 2.0 # 秒,指数退避基数 + + +class BioHTTPError(RuntimeError): + """统一的对外请求异常。""" + + +def _client(timeout: float, follow_redirects: bool = True) -> httpx.Client: + return httpx.Client( + timeout=timeout, + follow_redirects=follow_redirects, + headers={"User-Agent": USER_AGENT}, + ) + + +def get( + url: str, + *, + params: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + timeout: float = DEFAULT_TIMEOUT, + accept: str | None = None, +) -> httpx.Response: + """带重试的 GET。对 429/5xx 自动退避重试。""" + h = dict(headers or {}) + if accept: + h["Accept"] = accept + last_exc: Exception | None = None + for attempt in range(1, MAX_RETRIES + 1): + try: + with _client(timeout) as c: + resp = c.get(url, params=params, headers=h) + if resp.status_code in (429, 500, 502, 503, 504): + raise BioHTTPError(f"HTTP {resp.status_code} from {url}") + resp.raise_for_status() + return resp + except (httpx.HTTPError, BioHTTPError) as exc: # noqa: PERF203 + last_exc = exc + if attempt < MAX_RETRIES: + time.sleep(RETRY_BACKOFF * attempt) + else: + raise BioHTTPError(f"GET failed after {MAX_RETRIES} tries: {url} -> {exc}") from exc + raise BioHTTPError(str(last_exc)) # pragma: no cover + + +def post( + url: str, + *, + data: dict[str, Any] | None = None, + json: Any | None = None, + headers: dict[str, str] | None = None, + timeout: float = DEFAULT_TIMEOUT, + accept: str | None = None, +) -> httpx.Response: + """带重试的 POST。""" + h = dict(headers or {}) + if accept: + h["Accept"] = accept + last_exc: Exception | None = None + for attempt in range(1, MAX_RETRIES + 1): + try: + with _client(timeout) as c: + resp = c.post(url, data=data, json=json, headers=h) + if resp.status_code in (429, 500, 502, 503, 504): + raise BioHTTPError(f"HTTP {resp.status_code} from {url}") + resp.raise_for_status() + return resp + except (httpx.HTTPError, BioHTTPError) as exc: + last_exc = exc + if attempt < MAX_RETRIES: + time.sleep(RETRY_BACKOFF * attempt) + else: + raise BioHTTPError(f"POST failed after {MAX_RETRIES} tries: {url} -> {exc}") from exc + raise BioHTTPError(str(last_exc)) # pragma: no cover + + +def get_json(url: str, **kw: Any) -> Any: + return get(url, accept="application/json", **kw).json() + + +def get_text(url: str, **kw: Any) -> str: + return get(url, **kw).text diff --git a/plugins/Presisitence/plant-public/bio_toolkit/interpro.py b/plugins/Presisitence/plant-public/bio_toolkit/interpro.py new file mode 100644 index 0000000..b3c0198 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/interpro.py @@ -0,0 +1,68 @@ +"""EBI InterProScan 结构域/家族注释 (REST API)。 + +文档: https://www.ebi.ac.uk/jdispatcher/docs/webservices/ +这是异步作业:submit -> poll status -> fetch result。 +抗病基因家族鉴定关键工具 (NLR/NB-ARC、LRR、Pkinase、WRKY 等结构域)。 +""" +from __future__ import annotations + +import time +from typing import Any + +from .http import get_text, post + +BASE = "https://www.ebi.ac.uk/Tools/services/rest/iprscan5" + + +def submit(sequence: str, email: str, *, title: str = "plant-bio-mcp") -> str: + """提交序列,返回 job id。sequence 为蛋白 FASTA 或纯序列。email 必填 (EBI 要求)。""" + data = { + "email": email, + "title": title, + "sequence": sequence, + "goterms": "true", + "pathways": "true", + } + resp = post(f"{BASE}/run", data=data) + return resp.text.strip() + + +def status(job_id: str) -> str: + """查询作业状态: RUNNING / FINISHED / ERROR / NOT_FOUND 等。""" + return get_text(f"{BASE}/status/{job_id}").strip() + + +def result_types(job_id: str) -> str: + return get_text(f"{BASE}/resulttypes/{job_id}") + + +def result(job_id: str, result_type: str = "json") -> str: + """获取结果。result_type: json / tsv / gff / xml / svg 等。""" + return get_text(f"{BASE}/result/{job_id}/{result_type}") + + +def run_and_wait( + sequence: str, + email: str, + *, + result_type: str = "tsv", + poll_interval: float = 15.0, + max_wait: float = 900.0, +) -> dict[str, Any]: + """提交并阻塞等待完成,返回结果文本。 + + InterProScan 通常需 1-5 分钟。max_wait 默认 15 分钟。 + """ + job_id = submit(sequence, email) + waited = 0.0 + while waited < max_wait: + st = status(job_id) + if st == "FINISHED": + return {"job_id": job_id, "status": st, "result_type": result_type, + "result": result(job_id, result_type)} + if st in ("ERROR", "FAILURE", "NOT_FOUND"): + return {"job_id": job_id, "status": st, "result": ""} + time.sleep(poll_interval) + waited += poll_interval + return {"job_id": job_id, "status": "TIMEOUT", "result": "", + "note": f"超过 {max_wait}s 未完成,可稍后用 job_id 查询"} diff --git a/plugins/Presisitence/plant-public/bio_toolkit/ncbi.py b/plugins/Presisitence/plant-public/bio_toolkit/ncbi.py new file mode 100644 index 0000000..4902109 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/ncbi.py @@ -0,0 +1,65 @@ +"""NCBI 数据库访问 (E-utilities)。 + +文档: https://www.ncbi.nlm.nih.gov/books/NBK25501/ +无需 API key(可选设置 NCBI_API_KEY 环境变量提高频率上限)。 +遵守 NCBI 频率限制:无 key 时 <=3 请求/秒。 +""" +from __future__ import annotations + +import os +from typing import Any + +from .http import get_json, get_text + +EUTILS = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils" + + +def _common() -> dict[str, str]: + p = {"tool": "plant-public-mcp", "email": "researcher@example.org"} + key = os.environ.get("NCBI_API_KEY") + if key: + p["api_key"] = key + return p + + +def esearch(db: str, term: str, retmax: int = 20) -> dict[str, Any]: + """检索数据库,返回匹配的 ID 列表。 + + db 常用: nuccore(核酸), protein(蛋白), gene(基因), pubmed(文献), assembly(基因组) + term 例: "Solanum lycopersicum[Organism] AND WRKY[Gene]" + """ + params = {**_common(), "db": db, "term": term, "retmax": str(retmax), "retmode": "json"} + data = get_json(f"{EUTILS}/esearch.fcgi", params=params) + result = data.get("esearchresult", {}) + return { + "count": result.get("count"), + "ids": result.get("idlist", []), + "query": term, + "db": db, + } + + +def esummary(db: str, ids: list[str] | str) -> list[dict[str, Any]]: + """获取 ID 的摘要信息。""" + id_str = ids if isinstance(ids, str) else ",".join(ids) + params = {**_common(), "db": db, "id": id_str, "retmode": "json"} + data = get_json(f"{EUTILS}/esummary.fcgi", params=params) + result = data.get("result", {}) + uids = result.get("uids", []) + return [result[u] for u in uids if u in result] + + +def efetch(db: str, ids: list[str] | str, rettype: str = "fasta", retmode: str = "text") -> str: + """下载序列/记录。rettype 例: fasta / gb / gp。返回文本。""" + id_str = ids if isinstance(ids, str) else ",".join(ids) + params = {**_common(), "db": db, "id": id_str, "rettype": rettype, "retmode": retmode} + return get_text(f"{EUTILS}/efetch.fcgi", params=params) + + +def search_and_fetch(db: str, term: str, rettype: str = "fasta", retmax: int = 5) -> dict[str, Any]: + """一步检索并抓取序列,返回 ID 与文本内容。""" + s = esearch(db, term, retmax=retmax) + if not s["ids"]: + return {"ids": [], "records": "", "count": s["count"], "query": term} + records = efetch(db, s["ids"], rettype=rettype) + return {"ids": s["ids"], "records": records, "count": s["count"], "query": term} diff --git a/plugins/Presisitence/plant-public/bio_toolkit/plant.py b/plugins/Presisitence/plant-public/bio_toolkit/plant.py new file mode 100644 index 0000000..0c16358 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/plant.py @@ -0,0 +1,185 @@ +"""植物垂直资源模块。 + +分两类: +1) 有可编程接口的:Ensembl Plants REST(覆盖多种植物基因/序列,含茄科)。 +2) 仅网页交互、无开放 API 的:SignalP / TMHMM / WoLF PSORT / PlantCARE / PlantTFDB / + scPlantDB / SolGenomics / TAIR。 + 对第 2 类提供结构化调度指引(提交步骤、输入格式、参数建议、结果解读), + 并标注可用 browser-use 浏览器自动化代为提交。 + +设计原则:绝不编造这些库的返回数据;无 API 者只给操作路径,由上层用浏览器或人工完成。 +本发布包不含物种专库本地数据;茄科公共入口走 Sol Genomics / Ensembl Plants。 +""" +from __future__ import annotations + +from typing import Any + +from .http import get_json + +ENSEMBL_PLANTS = "https://rest.ensembl.org" + +# 抗病研究常用物种 NCBI taxid(便于构造精确检索) +TAXIDS = { + "solanum_lycopersicum": 4081, # 番茄 + "solanum_tuberosum": 4113, # 马铃薯 + "nicotiana_benthamiana": 4100, # 本氏烟 + "arabidopsis_thaliana": 3702, # 拟南芥 + "oryza_sativa": 4530, # 水稻 +} + +RESOURCES: dict[str, dict[str, Any]] = { + "signalp6": { + "name": "SignalP 6.0", + "url": "https://services.healthtech.dtu.dk/services/SignalP-6.0/", + "capability": "信号肽预测,判断是否分泌蛋白(效应子/PR 蛋白)", + "access": "web-only", + "input": "蛋白 FASTA(可批量,单次上限见网站)", + "params": "Organism 选 Eukarya;Output 选 Long / summary", + "interpret": "Sec/SPI 概率高且有 cleavage site 提示为分泌型;结合 TMHMM 排除跨膜假阳性", + "automation": "browser-use 可上传 FASTA、提交并下载结果表", + }, + "tmhmm2": { + "name": "TMHMM 2.0", + "url": "https://services.healthtech.dtu.dk/services/TMHMM-2.0/", + "capability": "跨膜螺旋预测(受体类抗病蛋白 RLK/RLP 定位)", + "access": "web-only", + "input": "蛋白 FASTA", + "params": "Output format 选 one line per protein 便于批量解析", + "interpret": "PredHel>=1 表示含跨膜区;单跨膜+胞外 LRR 提示 RLK/RLP", + "automation": "browser-use 可代为提交", + }, + "wolfpsort": { + "name": "WoLF PSORT", + "url": "https://wolfpsort.hgc.jp/", + "capability": "蛋白亚细胞定位预测", + "access": "web-only", + "input": "蛋白 FASTA", + "params": "Organism type 选 Plant", + "interpret": "输出各定位打分;抗病信号蛋白关注 nucl/cyto/plas 分布", + "automation": "browser-use 可代为提交", + }, + "interproscan_web": { + "name": "InterProScan (网页版)", + "url": "https://www.ebi.ac.uk/interpro/search/sequence/", + "capability": "结构域/家族注释", + "access": "api-available", + "note": "本工具集已提供 REST 封装 (bio_toolkit.interpro),优先用 API", + }, + "plantcare": { + "name": "PlantCARE", + "url": "https://bioinformatics.psb.ugent.be/webtools/plantcare/html/", + "capability": "启动子顺式作用元件分析(防御/胁迫元件 W-box、TC-rich、TCA 等)", + "access": "web-only", + "input": "启动子 DNA 序列(一般取 ATG 上游 1500-2000 bp)FASTA", + "params": "提交 Search for CARE", + "interpret": "关注防御相关元件:W-box(WRKY 结合)、TC-rich(防御/胁迫)、TCA(水杨酸)、" + "ABRE(ABA)、MYB/MYC(茉莉酸/干旱)", + "automation": "browser-use 可粘贴序列提交并抓取元件表", + }, + "planttfdb": { + "name": "PlantTFDB", + "url": "https://planttfdb.gao-lab.org/", + "capability": "植物转录因子鉴定与家族分类(WRKY/MYB/bHLH/NAC 等)", + "access": "web-only", + "input": "蛋白序列(TF prediction)或直接按物种/家族浏览", + "params": "TF Prediction 上传序列;或 Browse 选物种(如 Solanum lycopersicum)", + "interpret": "返回 TF 家族归属;抗病常见 WRKY、MYB、NAC、ERF", + "automation": "browser-use 可代为提交序列预测", + }, + "solgenomics": { + "name": "SolGenomics (SGN)", + "url": "https://solgenomics.net/", + "capability": "茄科基因组、注释、标记、BLAST(番茄、马铃薯、本氏烟等)", + "access": "web-blast", + "input": "核酸/蛋白序列做 BLAST,或按基因 ID 检索", + "params": "选择目标基因组数据库(如 Solanum lycopersicum ITAG)", + "interpret": "获取茄科同源基因位点与注释", + "automation": "browser-use 可代为 BLAST;亦可用 NCBI 模块做等价检索", + }, + "tair": { + "name": "TAIR", + "url": "https://www.arabidopsis.org/", + "capability": "拟南芥基因/功能/突变体(抗病机制模式参考)", + "access": "web-partial-login", + "input": "AGI 基因 ID (如 AT1G64280=NPR1) 或基因名", + "params": "部分内容需登录/配额", + "interpret": "查抗病通路基因功能;机制参考可映射到作物同源基因", + "automation": "程序化优先:BAR/ThaleMine 已封装为 REST 工具(bar_gene_function/bar_gene_info/bar_publications);UniProt/NCBI 亦覆盖拟南芥", + }, + "scplantdb": { + "name": "scPlantDB", + "url": "https://biobigdata.nju.edu.cn/scplantdb/marker", + "capability": "植物单细胞图谱与 marker 基因(含番茄等)", + "access": "web-only", + "input": "物种 + 细胞类型 / marker 基因", + "interpret": "获取细胞类型特异 marker,用于单细胞注释", + "automation": "browser-use 可代为按物种(solanum_lycopersicum)检索 marker", + }, + "colabfold": { + "name": "ColabFold / AlphaFold2", + "url": "https://colab.research.google.com/github/sokrypton/ColabFold/blob/main/AlphaFold2.ipynb", + "capability": "从序列现算三维结构(AlphaFold DB 无收录时使用)", + "access": "notebook", + "input": "蛋白序列(单体或复合物用 : 分隔)", + "params": "MSA mode: mmseqs2;num_recycles 视需要提高", + "interpret": "pLDDT>70 较可信,>90 高可信;结合 PAE 判断域间相对定位", + "note": "若 AlphaFold DB 已有该 UniProt 结构,优先用 structure.alphafold_summary 直接取", + }, + "bar_efp": { + "name": "BAR eFP Browser / ePlant", + "url": "https://bar.utoronto.ca/", + "capability": "拟南芥 eFP 表达(含 Biotic Stress 生物胁迫)数值与图像", + "access": "api-available", + "note": "已封装:bar_efp_views / bar_efp_expression / bar_efp_image(BAR REST API)", + }, + "thalemine": { + "name": "ThaleMine / Araport11 (InterMine, 经 BAR 代理)", + "url": "https://bar.utoronto.ca/thalemine/", + "capability": "拟南芥基因功能/GeneRIF/文献(抗病机制模式参考)", + "access": "api-available", + "note": "已封装:bar_gene_function / bar_publications / bar_gene_info", + }, + "atted": { + "name": "ATTED-II", + "url": "https://atted.jp/", + "capability": "拟南芥共表达网络(找共调控基因/候选上游 TF)", + "access": "api-available", + "note": "已封装:atted_coexpression(API v5)", + }, + "string": { + "name": "STRING", + "url": "https://string-db.org/", + "capability": "拟南芥(3702) 蛋白互作网络 + 功能富集", + "access": "api-available", + "note": "已封装:string_interactions / string_enrichment", + }, +} + + +def list_resources() -> list[dict[str, str]]: + """列出全部植物垂直资源及访问方式,便于选择调度路径。""" + return [ + {"key": k, "name": v["name"], "capability": v["capability"], "access": v["access"]} + for k, v in RESOURCES.items() + ] + + +def get_resource(key: str) -> dict[str, Any]: + """获取某资源的详细调度指引(提交步骤/输入/参数/解读/自动化路径)。""" + r = RESOURCES.get(key.lower()) + if not r: + return {"error": f"未知资源 '{key}'", "available": list(RESOURCES.keys())} + return {"key": key.lower(), **r} + + +def ensembl_lookup_symbol(species: str, symbol: str) -> dict[str, Any]: + """按基因名在 Ensembl Plants 查基因。species 例: solanum_lycopersicum。""" + url = f"{ENSEMBL_PLANTS}/lookup/symbol/{species}/{symbol}" + return get_json(url, params={"content-type": "application/json", "expand": "1"}) + + +def ensembl_sequence(ensembl_id: str, seq_type: str = "protein") -> str: + """按 Ensembl ID 取序列。seq_type: protein / cds / genomic。""" + from .http import get_text + url = f"{ENSEMBL_PLANTS}/sequence/id/{ensembl_id}" + return get_text(url, params={"type": seq_type}, accept="text/x-fasta") diff --git a/plugins/Presisitence/plant-public/bio_toolkit/stringdb.py b/plugins/Presisitence/plant-public/bio_toolkit/stringdb.py new file mode 100644 index 0000000..519a526 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/stringdb.py @@ -0,0 +1,51 @@ +"""STRING (string-db.org) 蛋白互作与功能富集 API 封装。 + +拟南芥 (species=3702) 蛋白-蛋白互作网络与功能富集(GO/KEGG/Pfam/InterPro)。 +公开 REST API(GET,JSON)。STRING 可直接解析 AGI(如 AT3G26440)。 +""" +from __future__ import annotations + +from typing import Any + +from .http import get_json + +STRING_API = "https://string-db.org/api" +ARABIDOPSIS = 3702 +CALLER = "plant-bio-mcp" + + +def _ids(genes: str | list[str]) -> str: + """把单个/多个基因标识符组装成 STRING 约定格式(多标识符用 %0d 分隔)。""" + if isinstance(genes, str): + parts = [g for g in genes.replace(",", " ").split() if g] + else: + parts = [str(g).strip() for g in genes if str(g).strip()] + return "\r".join(parts) + + +def interaction_partners( + gene_id: str, + species: int = ARABIDOPSIS, + required_score: int = 400, + limit: int = 50, +) -> dict[str, Any]: + """某拟南芥蛋白的 STRING 互作伙伴。required_score 为 0-1000 阈值(400≈中等);返回项 score 为 0-1。""" + params = { + "identifiers": gene_id.strip(), + "species": species, + "required_score": required_score, + "limit": limit, + "caller_identity": CALLER, + } + return { + "gene_id": gene_id.strip(), + "species": species, + "required_score": required_score, + "partners": get_json(f"{STRING_API}/json/interaction_partners", params=params), + } + + +def enrichment(genes: str | list[str], species: int = ARABIDOPSIS) -> dict[str, Any]: + """对拟南芥基因集做功能富集(GO/KEGG/Pfam/InterPro/UniProt Keywords)。genes 支持单个或多个(逗号/空格分隔)。""" + params = {"identifiers": _ids(genes), "species": species, "caller_identity": CALLER} + return {"species": species, "enrichment": get_json(f"{STRING_API}/json/enrichment", params=params)} diff --git a/plugins/Presisitence/plant-public/bio_toolkit/structure.py b/plugins/Presisitence/plant-public/bio_toolkit/structure.py new file mode 100644 index 0000000..deb26c0 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/structure.py @@ -0,0 +1,91 @@ +"""蛋白结构访问:RCSB PDB + AlphaFold Protein Structure Database。 + +- RCSB PDB Data/Search API: https://data.rcsb.org / https://search.rcsb.org +- AlphaFold DB API: https://alphafold.ebi.ac.uk/api/prediction/{uniprot} +均无需 API key。 +""" +from __future__ import annotations + +from typing import Any + +from .http import get_json, get_text, post + +RCSB_DATA = "https://data.rcsb.org/rest/v1/core" +RCSB_SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" # 搜索端点 (Search API v2) +RCSB_FILES = "https://files.rcsb.org/download" +AFDB = "https://alphafold.ebi.ac.uk/api/prediction" + + +# ---------- RCSB PDB ---------- +def pdb_entry(pdb_id: str) -> dict[str, Any]: + """获取 PDB 条目元数据 (标题、方法、分辨率等)。""" + e = get_json(f"{RCSB_DATA}/entry/{pdb_id.upper()}") + info = e.get("struct", {}) + exptl = e.get("exptl", [{}]) + cell = e.get("rcsb_entry_info", {}) + return { + "pdb_id": pdb_id.upper(), + "title": info.get("title"), + "method": [x.get("method") for x in exptl], + "resolution": cell.get("resolution_combined"), + "polymer_entities": cell.get("polymer_entity_count_protein"), + "source": f"https://www.rcsb.org/structure/{pdb_id.upper()}", + } + + +def pdb_search_by_sequence(sequence: str, *, identity_cutoff: float = 0.3, limit: int = 10) -> list[dict[str, Any]]: + """按序列相似性搜索 PDB (mmseqs2 后端),用于给预测结构找实验结构模板。""" + query = { + "query": { + "type": "terminal", + "service": "sequence", + "parameters": { + "evalue_cutoff": 1, + "identity_cutoff": identity_cutoff, + "sequence_type": "protein", + "value": sequence, + }, + }, + "request_options": {"paginate": {"start": 0, "rows": limit}}, + "return_type": "polymer_entity", + } + resp = post(RCSB_SEARCH, json=query, accept="application/json") + if resp.status_code == 204 or not resp.text.strip(): + return [] + data = resp.json() + hits = [] + for item in data.get("result_set", []): + hits.append({"identifier": item.get("identifier"), "score": item.get("score")}) + return hits + + +def download_pdb(pdb_id: str, fmt: str = "pdb") -> str: + """下载结构坐标文件文本。fmt: pdb / cif。""" + ext = "pdb" if fmt == "pdb" else "cif" + return get_text(f"{RCSB_FILES}/{pdb_id.upper()}.{ext}") + + +# ---------- AlphaFold DB ---------- +def alphafold_prediction(uniprot_acc: str) -> list[dict[str, Any]]: + """获取某 UniProt 登录号的 AlphaFold 预测结构元数据 (含模型/PAE 下载链接)。""" + return get_json(f"{AFDB}/{uniprot_acc}") + + +def alphafold_summary(uniprot_acc: str) -> dict[str, Any]: + """精简 AlphaFold 结果,给出模型与置信度文件下载链接。""" + preds = alphafold_prediction(uniprot_acc) + if not preds: + return {"uniprot": uniprot_acc, "found": False} + p = preds[0] + return { + "uniprot": uniprot_acc, + "found": True, + "organism": p.get("organismScientificName"), + "gene": p.get("gene"), + "uniprot_description": p.get("uniprotDescription"), + "model_pdb_url": p.get("pdbUrl"), + "model_cif_url": p.get("cifUrl"), + "pae_image_url": p.get("paeImageUrl"), + "pae_doc_url": p.get("paeDocUrl"), + "source": f"https://alphafold.ebi.ac.uk/entry/{uniprot_acc}", + } diff --git a/plugins/Presisitence/plant-public/bio_toolkit/uniprot.py b/plugins/Presisitence/plant-public/bio_toolkit/uniprot.py new file mode 100644 index 0000000..1a6cd19 --- /dev/null +++ b/plugins/Presisitence/plant-public/bio_toolkit/uniprot.py @@ -0,0 +1,83 @@ +"""UniProt 蛋白数据库访问 (REST API)。 + +文档: https://www.uniprot.org/help/api_queries +无需 API key。 +""" +from __future__ import annotations + +from typing import Any + +from .http import get_json, get_text + +BASE = "https://rest.uniprot.org/uniprotkb" + + +def get_entry(accession: str) -> dict[str, Any]: + """按登录号获取完整条目 (JSON)。例: P0DP23。""" + return get_json(f"{BASE}/{accession}.json") + + +def get_fasta(accession: str) -> str: + """获取 FASTA 序列(保留原始头信息与 ID)。""" + return get_text(f"{BASE}/{accession}.fasta") + + +def search( + query: str, + *, + fields: str = "accession,id,protein_name,organism_name,length,gene_names", + size: int = 25, +) -> list[dict[str, Any]]: + """检索 UniProtKB。 + + query 支持 UniProt 查询语法,例如: + - "gene:WRKY33 AND organism_id:4081" (番茄 Solanum lycopersicum taxid=4081) + - "kinase AND reviewed:true" + """ + params = {"query": query, "fields": fields, "size": str(size), "format": "json"} + data = get_json(f"{BASE}/search", params=params) + return data.get("results", []) + + +def summarize(accession: str) -> dict[str, Any]: + """把冗长的 UniProt JSON 精简为抗病研究关注的关键字段。""" + e = get_entry(accession) + protein = ( + e.get("proteinDescription", {}) + .get("recommendedName", {}) + .get("fullName", {}) + .get("value") + ) + organism = e.get("organism", {}).get("scientificName") + seq = e.get("sequence", {}) + genes = [g.get("geneName", {}).get("value") for g in e.get("genes", []) if g.get("geneName")] + + # 结构域 / 特征 + domains: list[str] = [] + for f in e.get("features", []): + if f.get("type") in ("Domain", "Transmembrane", "Signal", "Region"): + desc = f.get("description") or f.get("type") + loc = f.get("location", {}) + start = loc.get("start", {}).get("value") + end = loc.get("end", {}).get("value") + domains.append(f"{f['type']}: {desc} [{start}-{end}]") + + # 关键词 / GO + keywords = [k.get("name") for k in e.get("keywords", [])] + go_terms = [] + for ref in e.get("uniProtKBCrossReferences", []): + if ref.get("database") == "GO": + props = {p["key"]: p["value"] for p in ref.get("properties", [])} + go_terms.append(f"{ref.get('id')} {props.get('GoTerm', '')}") + + return { + "accession": accession, + "protein_name": protein, + "gene_names": genes, + "organism": organism, + "length": seq.get("length"), + "features": domains[:30], + "keywords": keywords, + "go_terms": go_terms[:30], + "source": f"{BASE}/{accession}", + } diff --git a/plugins/Presisitence/plant-public/mcp.json b/plugins/Presisitence/plant-public/mcp.json new file mode 100644 index 0000000..dfedd62 --- /dev/null +++ b/plugins/Presisitence/plant-public/mcp.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "plant-public": { + "type": "stdio", + "command": "uv", + "args": ["run", "server.py"], + "cwd": "${PLUGIN_ROOT}" + } + } +} diff --git a/plugins/Presisitence/plant-public/plugin.json b/plugins/Presisitence/plant-public/plugin.json new file mode 100644 index 0000000..b8bae26 --- /dev/null +++ b/plugins/Presisitence/plant-public/plugin.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "plant-public", + "version": "0.1.0", + "description": "Public plant-pathology bioinformatics APIs: UniProt, NCBI, InterProScan, PDB, AlphaFold, Ensembl Plants, Sol Genomics, and Arabidopsis BAR/ATTED/STRING.", + "author": { + "name": "Presisitence", + "url": "https://github.com/Presisitence" + }, + "homepage": "https://github.com/Presisitence/plant-public-mcp", + "repository": "https://github.com/Presisitence/plant-public-mcp", + "license": "MIT", + "keywords": [ + "minimax-code", + "plugin", + "mcp", + "plant", + "uniprot", + "ncbi", + "solanaceae" + ] +} diff --git a/plugins/Presisitence/plant-public/pyproject.toml b/plugins/Presisitence/plant-public/pyproject.toml new file mode 100644 index 0000000..9870bcc --- /dev/null +++ b/plugins/Presisitence/plant-public/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "plant-public-mcp" +version = "0.1.0" +description = "植物抗病生物信息学公共接口 MCP:UniProt/NCBI/InterPro/PDB/AlphaFold + Ensembl Plants / Sol Genomics 等公开库" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +dependencies = [ + "httpx>=0.27", + "mcp[cli]>=1.2.0", +] + +[project.scripts] +plant-cli = "bio_toolkit.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["bio_toolkit"] diff --git a/plugins/Presisitence/plant-public/server.py b/plugins/Presisitence/plant-public/server.py new file mode 100644 index 0000000..3d2f5a5 --- /dev/null +++ b/plugins/Presisitence/plant-public/server.py @@ -0,0 +1,185 @@ +"""植物抗病生物信息学 MCP Server(公共接口版)。 + +仅暴露公开数据库 API 与网页调度指引。不含本地基因组、本地 RNA-seq、物种专库映射表。 + +运行: + uv run --directory server.py +""" +from __future__ import annotations + +from typing import Any + +from mcp.server.fastmcp import FastMCP + +from bio_toolkit import ( + atted, + bar, + interpro, + ncbi, + plant, + stringdb, + structure, + uniprot, +) + +mcp = FastMCP("plant-public") + + +@mcp.tool() +def uniprot_summary(accession: str) -> dict[str, Any]: + """获取 UniProt 蛋白的精简注释(名称/基因/物种/结构域/GO/关键词)。accession 如 P0DP23。""" + return uniprot.summarize(accession) + + +@mcp.tool() +def uniprot_fasta(accession: str) -> str: + """获取 UniProt 蛋白的 FASTA 序列。""" + return uniprot.get_fasta(accession) + + +@mcp.tool() +def uniprot_search(query: str, size: int = 25) -> list[dict[str, Any]]: + """UniProt 检索。query 用其查询语法,如 'gene:WRKY33 AND organism_id:4081'(番茄)。""" + return uniprot.search(query, size=size) + + +@mcp.tool() +def ncbi_search(db: str, term: str, retmax: int = 20) -> dict[str, Any]: + """NCBI 检索。db: nuccore/protein/gene/pubmed/assembly;term 如 'Solanum lycopersicum[Organism] AND WRKY[Gene]'。""" + return ncbi.esearch(db, term, retmax=retmax) + + +@mcp.tool() +def ncbi_fetch(db: str, ids: str, rettype: str = "fasta") -> str: + """按 ID 从 NCBI 下载记录。ids 逗号分隔;rettype: fasta/gb/gp。""" + return ncbi.efetch(db, ids, rettype=rettype) + + +@mcp.tool() +def ncbi_search_fetch(db: str, term: str, rettype: str = "fasta", retmax: int = 5) -> dict[str, Any]: + """NCBI 一步检索并抓取序列。""" + return ncbi.search_and_fetch(db, term, rettype=rettype, retmax=retmax) + + +@mcp.tool() +def interproscan_run(sequence: str, email: str, result_type: str = "tsv") -> dict[str, Any]: + """提交蛋白序列到 EBI InterProScan 并等待结果(结构域/家族/GO)。email 必填。用于 NLR/LRR/激酶等抗病结构域鉴定。""" + return interpro.run_and_wait(sequence, email, result_type=result_type) + + +@mcp.tool() +def interproscan_submit(sequence: str, email: str) -> str: + """仅提交 InterProScan 作业,返回 job_id(不等待)。""" + return interpro.submit(sequence, email) + + +@mcp.tool() +def interproscan_result(job_id: str, result_type: str = "tsv") -> dict[str, Any]: + """按 job_id 查询 InterProScan 状态并在完成时取结果。""" + st = interpro.status(job_id) + if st != "FINISHED": + return {"job_id": job_id, "status": st} + return {"job_id": job_id, "status": st, "result": interpro.result(job_id, result_type)} + + +@mcp.tool() +def pdb_entry(pdb_id: str) -> dict[str, Any]: + """获取 RCSB PDB 条目元数据(标题/方法/分辨率)。""" + return structure.pdb_entry(pdb_id) + + +@mcp.tool() +def pdb_search_by_sequence(sequence: str, identity_cutoff: float = 0.3, limit: int = 10) -> list[dict[str, Any]]: + """按序列相似性搜索 PDB 实验结构(找同源模板)。""" + return structure.pdb_search_by_sequence(sequence, identity_cutoff=identity_cutoff, limit=limit) + + +@mcp.tool() +def alphafold_summary(uniprot_acc: str) -> dict[str, Any]: + """获取某 UniProt 的 AlphaFold 预测结构(含模型/PAE 下载链接)。优先于现算 ColabFold。""" + return structure.alphafold_summary(uniprot_acc) + + +@mcp.tool() +def plant_list_resources() -> list[dict[str, str]]: + """列出植物垂直资源及访问方式(api/web-only/notebook)。茄科公共入口为 Sol Genomics 与 Ensembl Plants。""" + return plant.list_resources() + + +@mcp.tool() +def plant_resource_guide(key: str) -> dict[str, Any]: + """获取某植物资源的调度指引。key 如 signalp6/tmhmm2/wolfpsort/plantcare/planttfdb/solgenomics/scplantdb/colabfold。""" + return plant.get_resource(key) + + +@mcp.tool() +def ensembl_plants_gene(species: str, symbol: str) -> dict[str, Any]: + """在 Ensembl Plants 按基因名查基因。species 如 solanum_lycopersicum。""" + return plant.ensembl_lookup_symbol(species, symbol) + + +@mcp.tool() +def taxid(species: str) -> dict[str, Any]: + """查询常用植物物种的 NCBI taxid。species 如 solanum_lycopersicum。""" + tid = plant.TAXIDS.get(species.lower()) + return {"species": species, "taxid": tid, "known": list(plant.TAXIDS.keys())} + + +@mcp.tool() +def bar_gene_info(gene_id: str, species: str = "arabidopsis") -> dict[str, Any]: + """BAR 拟南芥基因信息(位点/链向/别名/注释)。gene_id 如 AT3G26440。""" + return bar.gene_info(gene_id, species=species) + + +@mcp.tool() +def bar_gene_function(gene_id: str) -> dict[str, Any]: + """ThaleMine 拟南芥基因功能注释 + GeneRIF。gene_id 为 AGI。""" + return bar.gene_function(gene_id) + + +@mcp.tool() +def bar_publications(gene_id: str) -> dict[str, Any]: + """ThaleMine 拟南芥基因相关文献。gene_id 为 AGI。""" + return bar.publications(gene_id) + + +@mcp.tool() +def bar_efp_views(species: str = "arabidopsis", view: str = "") -> dict[str, Any]: + """eFP 发现:view 缺省列出全部 view→database 映射;给定 view(如 Biotic_Stress)返回该视图对照/处理样本分组。""" + return bar.efp_views(species=species, view=view or None) + + +@mcp.tool() +def bar_efp_expression(gene_id: str, database: str = "klepikova", species: str = "arabidopsis") -> dict[str, Any]: + """取拟南芥基因在指定 eFP database 的表达数值。""" + return bar.efp_expression(gene_id, database=database, species=species) + + +@mcp.tool() +def bar_efp_image(gene_id: str, view: str = "Biotic_Stress", mode: str = "Absolute", + species: str = "arabidopsis") -> dict[str, Any]: + """返回拟南芥基因 eFP 图像 URL(只返回 URL,不下载)。""" + return bar.efp_image_url(gene_id, view=view, mode=mode, species=species) + + +@mcp.tool() +def atted_coexpression(gene_id: str, top_n: int = 100, cutoff: float | None = None, + platform: str = "u", db: str | None = None) -> dict[str, Any]: + """拟南芥共表达基因(ATTED-II v5)。platform: u/m/r。""" + return atted.coexpression(gene_id, top_n=top_n, cutoff=cutoff, platform=platform, db=db) + + +@mcp.tool() +def string_interactions(gene_id: str, required_score: int = 400, limit: int = 50) -> dict[str, Any]: + """拟南芥蛋白的 STRING 互作伙伴(species=3702)。""" + return stringdb.interaction_partners(gene_id, required_score=required_score, limit=limit) + + +@mcp.tool() +def string_enrichment(genes: str) -> dict[str, Any]: + """对拟南芥基因集做 STRING 功能富集。genes 逗号/空格分隔多个。""" + return stringdb.enrichment(genes) + + +if __name__ == "__main__": + mcp.run() diff --git a/plugins/Presisitence/plant-public/skills/plant-public/SKILL.md b/plugins/Presisitence/plant-public/skills/plant-public/SKILL.md new file mode 100644 index 0000000..43e3eab --- /dev/null +++ b/plugins/Presisitence/plant-public/skills/plant-public/SKILL.md @@ -0,0 +1,24 @@ +--- +name: plant-public +description: Use when the user needs plant or Solanaceae gene/protein lookup via public APIs (UniProt, NCBI, InterPro, PDB, AlphaFold, Ensembl Plants, Sol Genomics, Arabidopsis BAR/ATTED/STRING), domain annotation, or structure summaries. Do not invent sequences; call the plant-public MCP tools. +--- + +# plant-public + +Call the `plant-public` MCP tools. Do not reimplement UniProt/NCBI/InterPro in ad-hoc scripts. + +## Routing + +- Protein annotation / FASTA → `uniprot_summary`, `uniprot_fasta`, `uniprot_search` +- Nucleotide/gene/literature IDs → `ncbi_search`, `ncbi_fetch`, `ncbi_search_fetch` +- Domains (NLR/LRR/kinase) → `interproscan_run` (needs the user's email) +- Experimental or predicted structure → `pdb_entry`, `pdb_search_by_sequence`, `alphafold_summary` +- Plant gene by species + symbol → `ensembl_plants_gene` (example species: `solanum_lycopersicum`) +- Solanaceae BLAST / genome browser (no open API) → `plant_resource_guide` with key `solgenomics` +- Arabidopsis function / biotic-stress eFP / coexpression / PPI → `bar_*`, `atted_coexpression`, `string_interactions` + +Web-only resources (`signalp6`, `plantcare`, `planttfdb`, …) return submission steps only. Never fabricate those outputs. + +## Honesty + +If an API fails, report the error. Missing full text or a web-only tool is a source gap, not a negative biological result.