Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions plugins/Presisitence/plant-public/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions plugins/Presisitence/plant-public/README.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions plugins/Presisitence/plant-public/bio_toolkit/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
43 changes: 43 additions & 0 deletions plugins/Presisitence/plant-public/bio_toolkit/atted.py
Original file line number Diff line number Diff line change
@@ -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),
}
101 changes: 101 additions & 0 deletions plugins/Presisitence/plant-public/bio_toolkit/bar.py
Original file line number Diff line number Diff line change
@@ -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}
162 changes: 162 additions & 0 deletions plugins/Presisitence/plant-public/bio_toolkit/cli.py
Original file line number Diff line number Diff line change
@@ -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())
Loading