ghh1125 commited on
Commit
f970651
·
verified ·
1 Parent(s): acd5e23

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
 
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
11
 
12
- COPY --chown=user . /app
13
  ENV MCP_TRANSPORT=http
14
  ENV MCP_PORT=7860
15
 
16
  EXPOSE 7860
17
 
 
 
18
  CMD ["python", "biopython/mcp_output/start_mcp.py"]
 
1
+ FROM python:3.11-slim
2
 
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ RUN useradd -m -u 1000 appuser
7
 
8
  WORKDIR /app
9
 
10
+ COPY requirements.txt /app/requirements.txt
11
+ RUN pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY biopython /app/biopython
14
+ COPY app.py /app/app.py
15
 
 
16
  ENV MCP_TRANSPORT=http
17
  ENV MCP_PORT=7860
18
 
19
  EXPOSE 7860
20
 
21
+ USER appuser
22
+
23
  CMD ["python", "biopython/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,58 @@
1
  ---
2
- title: Biopython
3
- emoji: 🐨
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: biopython MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # biopython MCP Service
12
+
13
+ This deployment package exposes core Biopython sequence-analysis capabilities as an MCP service using FastMCP.
14
+
15
+ ## Available Tools
16
+
17
+ - `health_check`
18
+ - `parse_fasta_text`
19
+ - `translate_dna`
20
+ - `reverse_complement`
21
+ - `global_align`
22
+ - `compute_gc_fraction`
23
+ - `find_motif_positions`
24
+
25
+ ## Local stdio (Claude Desktop / CLI)
26
+
27
+ ```bash
28
+ cd biopython/mcp_output
29
+ MCP_TRANSPORT=stdio python start_mcp.py
30
+ ```
31
+
32
+ You can also run the local stdio entry directly:
33
+
34
+ ```bash
35
+ cd biopython/mcp_output/mcp_plugin
36
+ python main.py
37
+ ```
38
+
39
+ ## HTTP MCP (Docker / HF Spaces)
40
+
41
+ This deployment uses FastMCP HTTP transport, with endpoint exposed at `/mcp`.
42
+
43
+ Local run:
44
+
45
+ ```bash
46
+ MCP_TRANSPORT=http MCP_PORT=7860 python biopython/mcp_output/start_mcp.py
47
+ ```
48
+
49
+ Docker run:
50
+
51
+ ```bash
52
+ ./run_docker.sh
53
+ ```
54
+
55
+ Then connect MCP clients to:
56
+
57
+ - `http://localhost:7860/mcp`
58
+ - `https://<your-space-host>/mcp`
app.py CHANGED
@@ -1,45 +1,82 @@
1
- from fastapi import FastAPI
 
 
 
2
  import os
 
3
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "biopython", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
7
 
8
- app = FastAPI(
9
- title="Biopython MCP Service",
10
- description="Auto-generated MCP service for biopython",
11
- version="1.0.0"
12
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Biopython MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
 
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "biopython MCP"}
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
  try:
30
- from mcp_service import create_app
 
31
  mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
36
- "description": tool_func.__doc__ or "No description available"
37
- })
38
- return {"tools": tools}
39
- except Exception as e:
40
- return {"error": f"Failed to load tools: {str(e)}"}
41
-
42
- if __name__ == "__main__":
43
- import uvicorn
44
- port = int(os.environ.get("PORT", 7860))
45
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ """Supplementary FastAPI app for local inspection (not MCP runtime entrypoint)."""
2
+
3
+ from __future__ import annotations
4
+
5
  import os
6
+ import importlib
7
  import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ try:
12
+ FastAPI = getattr(importlib.import_module("fastapi"), "FastAPI", None)
13
+ except Exception:
14
+ FastAPI = None
15
+
16
+ PLUGIN_DIR = Path(__file__).resolve().parent / "biopython" / "mcp_output" / "mcp_plugin"
17
+ plugin_dir_str = str(PLUGIN_DIR)
18
+ if plugin_dir_str not in sys.path:
19
+ sys.path.insert(0, plugin_dir_str)
20
+
21
+ if FastAPI is not None:
22
+ app = FastAPI(title="biopython-mcp-info", version="1.0.0")
23
+ else:
24
+ class _FallbackApp:
25
+ def get(self, *_args: Any, **_kwargs: Any):
26
+ def decorator(func):
27
+ return func
28
+
29
+ return decorator
30
+
31
+ app = _FallbackApp()
32
+ PORT = int(os.getenv("PORT", "7860"))
33
 
 
 
34
 
35
+ def _extract_tools(app_obj: Any) -> list[dict[str, str]]:
36
+ tools_attr = getattr(app_obj, "tools", None)
37
+ if tools_attr is None:
38
+ return []
39
+
40
+ if isinstance(tools_attr, dict):
41
+ items = tools_attr.values()
42
+ else:
43
+ items = tools_attr
44
+
45
+ tools: list[dict[str, str]] = []
46
+ for item in items:
47
+ if isinstance(item, dict):
48
+ name = str(item.get("name", ""))
49
+ description = str(item.get("description", ""))
50
+ else:
51
+ name = str(getattr(item, "name", getattr(item, "__name__", "")))
52
+ description = str(getattr(item, "description", ""))
53
+ if name:
54
+ tools.append({"name": name, "description": description})
55
+ return tools
56
+
57
 
58
  @app.get("/")
59
+ def root() -> dict[str, Any]:
60
  return {
61
+ "service": "biopython-mcp-deployment",
62
+ "mcp_transport": os.getenv("MCP_TRANSPORT", "stdio"),
63
+ "mcp_port": os.getenv("MCP_PORT", "8000"),
64
+ "info_port": PORT,
65
+ "note": "This FastAPI app is supplementary and does not run the MCP server.",
66
  }
67
 
68
+
69
  @app.get("/health")
70
+ def health() -> dict[str, str]:
71
+ return {"status": "healthy"}
72
+
73
 
74
  @app.get("/tools")
75
+ def tools() -> dict[str, Any]:
76
  try:
77
+ create_app = getattr(importlib.import_module("mcp_service"), "create_app")
78
+
79
  mcp_app = create_app()
80
+ return {"tools": _extract_tools(mcp_app)}
81
+ except Exception as exc:
82
+ return {"tools": [], "error": str(exc)}
 
 
 
 
 
 
 
 
 
 
 
biopython/mcp_output/README_MCP.md CHANGED
@@ -1,119 +1,99 @@
1
- # Biopython MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This service wraps core Biopython APIs as MCP (Model Context Protocol) tools for sequence, alignment, structure, phylogeny, search-result parsing, and NCBI Entrez retrieval workflows.
6
-
7
- Primary goals:
8
- - Provide stable, developer-friendly bioinformatics operations via MCP (Model Context Protocol)
9
- - Expose high-value Biopython modules (`SeqIO`, `AlignIO`, `Align`, `PDB`, `Phylo`, `SearchIO`, `Entrez`)
10
- - Enable format conversion, parsing, indexing, and basic analysis in a tool-callable interface
11
-
12
- ---
13
-
14
- ## 2) Installation Method
15
-
16
- ### Requirements
17
- - Python 3.x
18
- - `biopython`
19
- - `numpy` (required by Biopython)
20
-
21
- Optional (feature-dependent):
22
- - `reportlab` (graphics/genome diagrams)
23
- - `matplotlib` (phylo plotting)
24
- - `networkx` or `igraph` (some phylo integrations)
25
- - `mmtf-python` (MMTF support)
26
- - DB drivers for BioSQL: `mysqlclient` / `mysql-connector-python` / `psycopg2` / `sqlite3`
27
- - External binaries for some modules: DSSP, NACCESS, PAML, BLAST tools
28
-
29
- ### Install
30
- pip install biopython numpy
31
-
32
- Optional extras (as needed):
33
- pip install reportlab matplotlib networkx mmtf-python
34
-
35
- ---
36
-
37
- ## 3) Quick Start
38
-
39
- ### Minimal usage flow
40
- 1. Start your MCP (Model Context Protocol) host runtime
41
- 2. Register this Biopython service
42
- 3. Call tools such as sequence parse/read/write or alignment/structure parsers
43
-
44
- ### Typical tool calls (conceptual)
45
- - Parse FASTA/GenBank records using `SeqIO.parse` / `SeqIO.read`
46
- - Convert sequence formats using `SeqIO.convert`
47
- - Parse alignments using `AlignIO.parse` or modern `Bio.Align.parse`
48
- - Load substitution matrices via `Bio.Align.substitution_matrices.load`
49
- - Parse PDB/mmCIF structures with `PDBParser` / `MMCIFParser`
50
- - Read/convert phylogenetic trees via `Phylo.read` / `Phylo.convert`
51
- - Parse BLAST/HMMER outputs via `SearchIO.parse`
52
- - Query NCBI with `Entrez.esearch` + `Entrez.efetch`
53
-
54
- ---
55
-
56
- ## 4) Available Tools and Endpoints List
57
-
58
- Recommended MCP (Model Context Protocol) endpoints for this service:
59
-
60
- - `seqio_parse`
61
- - Parse multi-record sequence files (FASTA/GenBank/EMBL/FASTQ/etc.)
62
- - `seqio_read`
63
- - Read exactly one sequence record
64
- - `seqio_write`
65
- - Write sequence records to target format
66
- - `seqio_convert`
67
- - Convert sequence files between supported formats
68
- - `seqio_index` / `seqio_index_db`
69
- - Build lightweight/random-access sequence indexes
70
-
71
- - `alignio_parse` / `alignio_read` / `alignio_write` / `alignio_convert`
72
- - Multiple sequence alignment I/O and conversion
73
-
74
- - `align_parse` / `align_read` / `align_write`
75
- - Modern `Bio.Align` alignment APIs
76
- - `pairwise_align`
77
- - Pairwise alignment via `PairwiseAligner`
78
- - `load_substitution_matrix`
79
- - Load scoring matrices (e.g., BLOSUM/PAM)
80
-
81
- - `pdb_parse`
82
- - Parse PDB files
83
- - `mmcif_parse`
84
- - Parse mmCIF files
85
- - `pdb_write`
86
- - Export structures
87
- - `structure_superimpose` / `neighbor_search`
88
- - Structural comparison and proximity queries
89
-
90
- - `phylo_read` / `phylo_parse` / `phylo_write` / `phylo_convert`
91
- - Phylogenetic tree I/O workflows
92
-
93
- - `searchio_parse` / `searchio_read` / `searchio_index` / `searchio_convert`
94
- - Search tool output parsing (BLAST/HMMER/Infernal/Exonerate)
95
-
96
- - `entrez_esearch` / `entrez_efetch` / `entrez_esummary` / `entrez_elink`
97
- - NCBI E-utilities access (network dependent)
98
-
99
- ---
100
-
101
- ## 5) Common Issues and Notes
102
-
103
- - No first-class package CLI entry points are defined; Biopython is primarily a library.
104
- - Some capabilities require optional dependencies or external binaries.
105
- - Entrez usage requires internet and proper NCBI etiquette (`Entrez.email`, rate-limit awareness, API key if needed).
106
- - Large files (BAM-like, big alignments, massive GenBank) should use streaming/indexing to avoid memory pressure.
107
- - Format strictness varies; malformed biological files may parse partially or fail.
108
- - BioSQL features need database-specific drivers and schema setup.
109
- - Use pinned versions in production for reproducibility.
110
-
111
- ---
112
-
113
- ## 6) Reference Links / Documentation
114
-
115
- - Biopython repository: https://github.com/biopython/biopython
116
- - Biopython docs: https://biopython.org/wiki/Documentation
117
- - API docs: https://biopython.org/docs/latest/api/
118
- - Tutorial & Cookbook: https://biopython.org/DIST/docs/tutorial/Tutorial.html
119
- - NCBI Entrez E-utilities: https://www.ncbi.nlm.nih.gov/books/NBK25501/
 
1
+ # Biopython MCP Plugin
2
+
3
+ This MCP plugin exposes core Biopython sequence-analysis capabilities through FastMCP.
4
+
5
+ ## Exposed Tools
6
+
7
+ ### 1) `health_check`
8
+ - **Parameters**: none
9
+ - **Returns**: dependency availability and adapter/module loading health.
10
+ - **Example**:
11
+ ```json
12
+ {"name":"health_check","arguments":{}}
13
+ ```
14
+
15
+ ### 2) `parse_fasta_text`
16
+ - **Parameters**:
17
+ - `fasta_text: str` - FASTA content
18
+ - `max_records: int = 50` - max records to return
19
+ - **Returns**: parsed records with id, description, length, and sequence.
20
+ - **Example**:
21
+ ```json
22
+ {"name":"parse_fasta_text","arguments":{"fasta_text":">seq1\nATGC\n","max_records":10}}
23
+ ```
24
+
25
+ ### 3) `translate_dna`
26
+ - **Parameters**:
27
+ - `sequence: str`
28
+ - `to_stop: bool = false`
29
+ - `table: int = 1`
30
+ - **Returns**: translated protein sequence.
31
+ - **Example**:
32
+ ```json
33
+ {"name":"translate_dna","arguments":{"sequence":"ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG"}}
34
+ ```
35
+
36
+ ### 4) `reverse_complement`
37
+ - **Parameters**:
38
+ - `sequence: str`
39
+ - **Returns**: reverse-complement sequence.
40
+ - **Example**:
41
+ ```json
42
+ {"name":"reverse_complement","arguments":{"sequence":"ATGCCT"}}
43
+ ```
44
+
45
+ ### 5) `global_align`
46
+ - **Parameters**:
47
+ - `seq_a: str`
48
+ - `seq_b: str`
49
+ - `match_score: float = 1.0`
50
+ - `mismatch_score: float = -1.0`
51
+ - `open_gap_score: float = -0.5`
52
+ - `extend_gap_score: float = -0.1`
53
+ - `max_alignments: int = 5`
54
+ - **Returns**: alignment list with score/start/end.
55
+ - **Example**:
56
+ ```json
57
+ {"name":"global_align","arguments":{"seq_a":"ACCGT","seq_b":"ACG"}}
58
+ ```
59
+
60
+ ### 6) `compute_gc_fraction`
61
+ - **Parameters**:
62
+ - `sequence: str`
63
+ - **Returns**: GC fraction in [0,1].
64
+ - **Example**:
65
+ ```json
66
+ {"name":"compute_gc_fraction","arguments":{"sequence":"ATGCGCGT"}}
67
+ ```
68
+
69
+ ### 7) `find_motif_positions`
70
+ - **Parameters**:
71
+ - `sequence: str`
72
+ - `motif: str`
73
+ - **Returns**: motif and all start positions.
74
+ - **Example**:
75
+ ```json
76
+ {"name":"find_motif_positions","arguments":{"sequence":"ATGATGAT","motif":"ATG"}}
77
+ ```
78
+
79
+ ## Run Locally (stdio)
80
+
81
+ From `mcp_output/`:
82
+
83
+ ```bash
84
+ python start_mcp.py
85
+ ```
86
+
87
+ Or explicit:
88
+
89
+ ```bash
90
+ MCP_TRANSPORT=stdio python start_mcp.py
91
+ ```
92
+
93
+ ## Run via HTTP
94
+
95
+ ```bash
96
+ MCP_TRANSPORT=http MCP_PORT=8000 python start_mcp.py
97
+ ```
98
+
99
+ MCP endpoint will be served by FastMCP at `/mcp`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
biopython/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,432 +1,199 @@
1
- import os
2
- import sys
3
- from typing import Any, Dict, Optional, Tuple
4
-
5
- source_path = os.path.join(
6
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
7
- "source",
8
- )
9
- sys.path.insert(0, source_path)
10
 
 
11
 
12
- class Adapter:
13
- """
14
- MCP Import-mode adapter for Biopython core I/O and alignment surfaces.
15
-
16
- This adapter targets the analyzed modules:
17
- - source.Bio.SeqIO -> Bio.SeqIO
18
- - source.Bio.AlignIO -> Bio.AlignIO
19
- - source.Bio.Align -> Bio.Align
20
 
21
- It provides:
22
- - Dedicated class instance factory methods for identified classes:
23
- PairwiseAligner, Alignment, Alignments
24
- - Dedicated function-call methods for identified functions:
25
- parse, read, write (for SeqIO / AlignIO / Align)
26
- - Unified dictionary response format with status field
27
- - Graceful fallback when import fails
28
- """
29
 
30
- # -------------------------------------------------------------------------
31
- # Initialization and module management
32
- # -------------------------------------------------------------------------
33
- def __init__(self) -> None:
34
- """
35
- Initialize adapter in import mode and attempt module imports.
36
 
37
- Attributes:
38
- mode (str): Always "import" for this adapter.
39
- available (bool): True if core imports succeeded.
40
- import_error (Optional[str]): Captured import error if unavailable.
41
- """
42
- self.mode = "import"
43
- self.available = False
44
- self.import_error: Optional[str] = None
45
 
46
- self._seqio = None
47
- self._alignio = None
48
- self._align = None
 
 
 
49
 
50
- self._import_modules()
 
 
 
51
 
52
- def _import_modules(self) -> None:
53
- """Attempt importing required modules and classes from repository source tree."""
54
  try:
55
- import Bio.SeqIO as seqio_module
56
- import Bio.AlignIO as alignio_module
57
- import Bio.Align as align_module
58
-
59
- self._seqio = seqio_module
60
- self._alignio = alignio_module
61
- self._align = align_module
62
- self.available = True
63
- self.import_error = None
64
  except Exception as exc:
65
- self.available = False
66
- self.import_error = (
67
- f"Import failed. Ensure repository source is present under '{source_path}' "
68
- f"and compatible dependencies are installed. Details: {exc}"
 
 
 
 
 
 
 
 
 
69
  )
70
 
71
- def _result(self, status: str, **kwargs: Any) -> Dict[str, Any]:
72
- """Create unified result dictionary."""
73
- payload = {"status": status, "mode": self.mode}
74
- payload.update(kwargs)
75
- return payload
76
-
77
- def _ensure_available(self) -> Tuple[bool, Optional[Dict[str, Any]]]:
78
- """Check adapter availability and return fallback error payload when unavailable."""
79
- if not self.available:
80
- return False, self._result(
81
- "error",
82
- message=self.import_error
83
- or "Adapter imports are unavailable. Verify source path and dependencies.",
84
- actionable_guidance=(
85
- "Confirm the 'source' directory exists, includes Bio package, and retry. "
86
- "If needed, install optional dependencies required by target format."
87
- ),
88
- )
89
- return True, None
90
-
91
- # -------------------------------------------------------------------------
92
- # Health and capability methods
93
- # -------------------------------------------------------------------------
94
- def health(self) -> Dict[str, Any]:
95
- """
96
- Return adapter health and import status.
97
- """
98
- if self.available:
99
- return self._result(
100
  "ok",
101
- available=True,
102
- modules=["Bio.SeqIO", "Bio.AlignIO", "Bio.Align"],
 
 
103
  )
104
- return self._result(
105
- "error",
106
- available=False,
107
- message=self.import_error,
108
- )
109
-
110
- # -------------------------------------------------------------------------
111
- # Class instance factory methods (identified classes from Bio.Align)
112
- # -------------------------------------------------------------------------
113
- def create_pairwise_aligner(self, **kwargs: Any) -> Dict[str, Any]:
114
- """
115
- Create a Bio.Align.PairwiseAligner instance.
116
 
117
- Parameters:
118
- **kwargs: Attributes to set on the newly created aligner, e.g.
119
- mode="local", match_score=2.0, mismatch_score=-1.0,
120
- open_gap_score=-0.5, extend_gap_score=-0.1
121
-
122
- Returns:
123
- dict: Unified status payload with aligner instance under 'instance' on success.
124
- """
125
- ok, err = self._ensure_available()
126
- if not ok:
127
- return err
128
- try:
129
- cls = getattr(self._align, "PairwiseAligner")
130
- obj = cls()
131
- for k, v in kwargs.items():
132
- setattr(obj, k, v)
133
- return self._result("ok", instance=obj, class_name="PairwiseAligner")
134
- except Exception as exc:
135
- return self._result(
136
- "error",
137
- message=f"Failed to create PairwiseAligner: {exc}",
138
- actionable_guidance="Validate aligner parameter names and values.",
139
  )
140
 
141
- def create_alignment(self, sequences: Any = None, coordinates: Any = None) -> Dict[str, Any]:
142
- """
143
- Create a Bio.Align.Alignment instance.
144
-
145
- Parameters:
146
- sequences: Sequence-like inputs expected by Bio.Align.Alignment.
147
- coordinates: Optional coordinates array-like object.
148
-
149
- Returns:
150
- dict: Unified status payload with alignment instance under 'instance' on success.
151
- """
152
- ok, err = self._ensure_available()
153
- if not ok:
154
- return err
155
- try:
156
- cls = getattr(self._align, "Alignment")
157
- if sequences is not None and coordinates is not None:
158
- obj = cls(sequences, coordinates)
159
- elif sequences is not None:
160
- obj = cls(sequences)
161
- else:
162
- obj = cls()
163
- return self._result("ok", instance=obj, class_name="Alignment")
164
- except Exception as exc:
165
- return self._result(
166
- "error",
167
- message=f"Failed to create Alignment: {exc}",
168
- actionable_guidance=(
169
- "Provide valid sequences and optional coordinates matching Bio.Align.Alignment expectations."
170
- ),
171
- )
172
 
173
- def create_alignments(self, iterable: Any = None) -> Dict[str, Any]:
174
- """
175
- Create a Bio.Align.Alignments instance.
 
 
 
 
 
 
 
176
 
177
- Parameters:
178
- iterable: Optional iterable of alignment objects.
 
 
 
 
 
 
 
179
 
180
- Returns:
181
- dict: Unified status payload with alignments instance under 'instance' on success.
182
- """
183
- ok, err = self._ensure_available()
184
- if not ok:
185
- return err
186
- try:
187
- cls = getattr(self._align, "Alignments")
188
- obj = cls(iterable) if iterable is not None else cls()
189
- return self._result("ok", instance=obj, class_name="Alignments")
190
- except Exception as exc:
191
- return self._result(
192
  "error",
193
- message=f"Failed to create Alignments: {exc}",
194
- actionable_guidance="Ensure iterable elements are valid alignment objects.",
195
  )
196
 
197
- # -------------------------------------------------------------------------
198
- # SeqIO function wrappers: parse/read/write
199
- # -------------------------------------------------------------------------
200
- def seqio_parse(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
201
- """
202
- Call Bio.SeqIO.parse.
203
 
204
- Parameters:
205
- handle: File path, handle, or text stream.
206
- format (str): Sequence file format (e.g., 'fasta', 'genbank').
207
- **kwargs: Extra parser options forwarded to Bio.SeqIO.parse.
208
-
209
- Returns:
210
- dict: status='ok' with iterator under 'result' on success.
211
- """
212
- ok, err = self._ensure_available()
213
- if not ok:
214
- return err
215
- try:
216
- result = self._seqio.parse(handle, format, **kwargs)
217
- return self._result("ok", result=result, module="Bio.SeqIO", function="parse")
218
- except Exception as exc:
219
- return self._result(
220
  "error",
221
- message=f"Bio.SeqIO.parse failed: {exc}",
222
- actionable_guidance="Verify input handle/path and format string.",
223
  )
224
 
225
- def seqio_read(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
226
- """
227
- Call Bio.SeqIO.read.
228
-
229
- Parameters:
230
- handle: File path, handle, or text stream containing exactly one record.
231
- format (str): Sequence file format.
232
- **kwargs: Extra options forwarded to Bio.SeqIO.read.
233
-
234
- Returns:
235
- dict: status='ok' with record under 'result' on success.
236
- """
237
- ok, err = self._ensure_available()
238
- if not ok:
239
- return err
240
- try:
241
- result = self._seqio.read(handle, format, **kwargs)
242
- return self._result("ok", result=result, module="Bio.SeqIO", function="read")
243
- except Exception as exc:
244
- return self._result(
245
  "error",
246
- message=f"Bio.SeqIO.read failed: {exc}",
247
- actionable_guidance="Ensure exactly one record exists in input for read().",
 
248
  )
249
 
250
- def seqio_write(self, sequences: Any, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
251
- """
252
- Call Bio.SeqIO.write.
253
-
254
- Parameters:
255
- sequences: SeqRecord or iterable of SeqRecord objects.
256
- handle: Output file path or writable handle.
257
- format (str): Output format.
258
- **kwargs: Extra options forwarded to Bio.SeqIO.write.
259
-
260
- Returns:
261
- dict: status='ok' with count under 'result' on success.
262
- """
263
- ok, err = self._ensure_available()
264
- if not ok:
265
- return err
266
  try:
267
- result = self._seqio.write(sequences, handle, format, **kwargs)
268
- return self._result("ok", result=result, module="Bio.SeqIO", function="write")
269
  except Exception as exc:
270
- return self._result(
271
  "error",
272
- message=f"Bio.SeqIO.write failed: {exc}",
273
- actionable_guidance="Validate sequence objects, output handle, and format.",
 
274
  )
275
 
276
- # -------------------------------------------------------------------------
277
- # AlignIO function wrappers: parse/read/write
278
- # -------------------------------------------------------------------------
279
- def alignio_parse(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
280
- """
281
- Call Bio.AlignIO.parse.
282
-
283
- Parameters:
284
- handle: File path, handle, or text stream.
285
- format (str): Alignment format (e.g., 'clustal', 'stockholm', 'phylip').
286
- **kwargs: Extra parser options.
287
-
288
- Returns:
289
- dict: status='ok' with iterator under 'result' on success.
290
- """
291
- ok, err = self._ensure_available()
292
- if not ok:
293
- return err
294
- try:
295
- result = self._alignio.parse(handle, format, **kwargs)
296
- return self._result("ok", result=result, module="Bio.AlignIO", function="parse")
297
- except Exception as exc:
298
- return self._result(
299
  "error",
300
- message=f"Bio.AlignIO.parse failed: {exc}",
301
- actionable_guidance="Check alignment input source and specified format.",
302
  )
303
 
304
- def alignio_read(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
305
- """
306
- Call Bio.AlignIO.read.
307
-
308
- Parameters:
309
- handle: Input file path or handle containing one alignment.
310
- format (str): Alignment format.
311
- **kwargs: Extra options.
312
-
313
- Returns:
314
- dict: status='ok' with alignment object under 'result' on success.
315
- """
316
- ok, err = self._ensure_available()
317
- if not ok:
318
- return err
319
- try:
320
- result = self._alignio.read(handle, format, **kwargs)
321
- return self._result("ok", result=result, module="Bio.AlignIO", function="read")
322
- except Exception as exc:
323
- return self._result(
324
  "error",
325
- message=f"Bio.AlignIO.read failed: {exc}",
326
- actionable_guidance="Ensure input contains exactly one alignment.",
 
327
  )
328
 
329
- def alignio_write(self, alignments: Any, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
330
- """
331
- Call Bio.AlignIO.write.
332
-
333
- Parameters:
334
- alignments: Alignment object or iterable of alignments.
335
- handle: Output file path or writable handle.
336
- format (str): Alignment output format.
337
- **kwargs: Extra options.
338
-
339
- Returns:
340
- dict: status='ok' with write count under 'result' on success.
341
- """
342
- ok, err = self._ensure_available()
343
- if not ok:
344
- return err
345
  try:
346
- result = self._alignio.write(alignments, handle, format, **kwargs)
347
- return self._result("ok", result=result, module="Bio.AlignIO", function="write")
348
- except Exception as exc:
349
- return self._result(
350
- "error",
351
- message=f"Bio.AlignIO.write failed: {exc}",
352
- actionable_guidance="Validate alignment objects and output destination.",
353
- )
354
-
355
- # -------------------------------------------------------------------------
356
- # Align function wrappers: parse/read/write
357
- # -------------------------------------------------------------------------
358
- def align_parse(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
359
- """
360
- Call Bio.Align.parse.
361
-
362
- Parameters:
363
- handle: Alignment input source.
364
- format (str): Format supported by Bio.Align parser.
365
- **kwargs: Additional parser options.
366
-
367
- Returns:
368
- dict: status='ok' with iterator or alignment stream under 'result'.
369
- """
370
- ok, err = self._ensure_available()
371
- if not ok:
372
- return err
373
- try:
374
- result = self._align.parse(handle, format, **kwargs)
375
- return self._result("ok", result=result, module="Bio.Align", function="parse")
376
- except Exception as exc:
377
- return self._result(
378
- "error",
379
- message=f"Bio.Align.parse failed: {exc}",
380
- actionable_guidance="Verify format and input compatibility with Bio.Align.",
381
  )
382
-
383
- def align_read(self, handle: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
384
- """
385
- Call Bio.Align.read.
386
-
387
- Parameters:
388
- handle: Input source containing a single alignment item.
389
- format (str): Expected alignment format.
390
- **kwargs: Additional read options.
391
-
392
- Returns:
393
- dict: status='ok' with alignment object under 'result'.
394
- """
395
- ok, err = self._ensure_available()
396
- if not ok:
397
- return err
398
- try:
399
- result = self._align.read(handle, format, **kwargs)
400
- return self._result("ok", result=result, module="Bio.Align", function="read")
401
  except Exception as exc:
402
- return self._result(
403
  "error",
404
- message=f"Bio.Align.read failed: {exc}",
405
- actionable_guidance="Ensure a single valid alignment is present in the input.",
 
406
  )
407
-
408
- def align_write(self, alignments: Any, target: Any, format: str, **kwargs: Any) -> Dict[str, Any]:
409
- """
410
- Call Bio.Align.write.
411
-
412
- Parameters:
413
- alignments: Alignment or iterable of alignments.
414
- target: Output destination path or handle.
415
- format (str): Output format.
416
- **kwargs: Additional write options.
417
-
418
- Returns:
419
- dict: status='ok' with write result under 'result'.
420
- """
421
- ok, err = self._ensure_available()
422
- if not ok:
423
- return err
424
- try:
425
- result = self._align.write(alignments, target, format, **kwargs)
426
- return self._result("ok", result=result, module="Bio.Align", function="write")
427
- except Exception as exc:
428
- return self._result(
429
- "error",
430
- message=f"Bio.Align.write failed: {exc}",
431
- actionable_guidance="Check output target permissions and alignment object validity.",
432
- )
 
1
+ """Adapter layer for dynamic Biopython module loading and execution."""
 
 
 
 
 
 
 
 
2
 
3
+ from __future__ import annotations
4
 
5
+ import importlib
6
+ import inspect
7
+ import pkgutil
8
+ import sys
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Any
 
12
 
13
+ SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
14
+ if SOURCE_DIR.exists():
15
+ source_dir_str = str(SOURCE_DIR)
16
+ if source_dir_str not in sys.path:
17
+ sys.path.insert(0, source_dir_str)
 
 
 
18
 
 
 
 
 
 
 
19
 
20
+ class Adapter:
21
+ """Dynamically loads Biopython modules and provides introspection/execution helpers."""
 
 
 
 
 
 
22
 
23
+ def __init__(self, root_package: str = "Bio") -> None:
24
+ self.root_package = root_package
25
+ self.loaded_modules: dict[str, ModuleType] = {}
26
+ self.failed_modules: dict[str, str] = {}
27
+ self.mode = "active"
28
+ self._load_root_package()
29
 
30
+ def _status(self, status: str, **payload: Any) -> dict[str, Any]:
31
+ result: dict[str, Any] = {"status": status}
32
+ result.update(payload)
33
+ return result
34
 
35
+ def _load_root_package(self) -> None:
 
36
  try:
37
+ root = importlib.import_module(self.root_package)
38
+ self.loaded_modules[self.root_package] = root
39
+ self.mode = "active"
 
 
 
 
 
 
40
  except Exception as exc:
41
+ self.failed_modules[self.root_package] = str(exc)
42
+ self.mode = "blackbox"
43
+
44
+ def load_all_submodules(self) -> dict[str, Any]:
45
+ """Load all importable submodules under the root package."""
46
+ if self.root_package not in self.loaded_modules:
47
+ self.mode = "blackbox"
48
+ return self._status(
49
+ "fallback",
50
+ mode=self.mode,
51
+ loaded=0,
52
+ failed=len(self.failed_modules),
53
+ message="Root package unavailable; running in blackbox mode.",
54
  )
55
 
56
+ root = self.loaded_modules[self.root_package]
57
+ if not hasattr(root, "__path__"):
58
+ return self._status(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  "ok",
60
+ mode=self.mode,
61
+ loaded=len(self.loaded_modules),
62
+ failed=len(self.failed_modules),
63
+ message="Root package has no __path__; no submodules discovered.",
64
  )
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ for module_info in pkgutil.walk_packages(root.__path__, prefix=f"{self.root_package}."):
67
+ module_name = module_info.name
68
+ if module_name in self.loaded_modules or module_name in self.failed_modules:
69
+ continue
70
+ try:
71
+ module = importlib.import_module(module_name)
72
+ self.loaded_modules[module_name] = module
73
+ except Exception as exc:
74
+ self.failed_modules[module_name] = str(exc)
75
+
76
+ if not self.loaded_modules:
77
+ self.mode = "blackbox"
78
+ return self._status(
79
+ "fallback",
80
+ mode=self.mode,
81
+ loaded=0,
82
+ failed=len(self.failed_modules),
83
+ message="No modules loaded; running in blackbox mode.",
 
 
 
 
84
  )
85
 
86
+ return self._status(
87
+ "ok",
88
+ mode=self.mode,
89
+ loaded=len(self.loaded_modules),
90
+ failed=len(self.failed_modules),
91
+ message="Module scan complete.",
92
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ def health(self) -> dict[str, Any]:
95
+ """Return module loading health information."""
96
+ status = "ok" if self.loaded_modules else "fallback"
97
+ return self._status(
98
+ status,
99
+ mode=self.mode,
100
+ root_package=self.root_package,
101
+ loaded_count=len(self.loaded_modules),
102
+ failed_count=len(self.failed_modules),
103
+ )
104
 
105
+ def list_modules(self) -> dict[str, Any]:
106
+ """List loaded and failed module names."""
107
+ status = "ok" if self.loaded_modules else "fallback"
108
+ return self._status(
109
+ status,
110
+ mode=self.mode,
111
+ loaded_modules=sorted(self.loaded_modules.keys()),
112
+ failed_modules=sorted(self.failed_modules.keys()),
113
+ )
114
 
115
+ def list_symbols(self, module_name: str) -> dict[str, Any]:
116
+ """List public symbols from a loaded module."""
117
+ module = self.loaded_modules.get(module_name)
118
+ if module is None:
119
+ if module_name in self.failed_modules:
120
+ return self._status(
121
+ "error",
122
+ message="Module failed to load.",
123
+ module=module_name,
124
+ error=self.failed_modules[module_name],
125
+ )
126
+ return self._status(
127
  "error",
128
+ message="Module is not loaded.",
129
+ module=module_name,
130
  )
131
 
132
+ symbols = [name for name in dir(module) if not name.startswith("_")]
133
+ return self._status("ok", module=module_name, symbols=symbols)
 
 
 
 
134
 
135
+ def call_function(self, module_name: str, function_name: str, args: list[Any]) -> dict[str, Any]:
136
+ """Call a function by module and function name."""
137
+ module = self.loaded_modules.get(module_name)
138
+ if module is None:
139
+ return self._status(
 
 
 
 
 
 
 
 
 
 
 
140
  "error",
141
+ message="Module is not loaded.",
142
+ module=module_name,
143
  )
144
 
145
+ target = getattr(module, function_name, None)
146
+ if target is None or not callable(target):
147
+ return self._status(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  "error",
149
+ message="Function not found or not callable.",
150
+ module=module_name,
151
+ function=function_name,
152
  )
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  try:
155
+ value = target(*args)
156
+ return self._status("ok", module=module_name, function=function_name, result=value)
157
  except Exception as exc:
158
+ return self._status(
159
  "error",
160
+ module=module_name,
161
+ function=function_name,
162
+ error=str(exc),
163
  )
164
 
165
+ def create_instance(self, module_name: str, class_name: str, args: list[Any]) -> dict[str, Any]:
166
+ """Create an instance from a class in a loaded module."""
167
+ module = self.loaded_modules.get(module_name)
168
+ if module is None:
169
+ return self._status(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  "error",
171
+ message="Module is not loaded.",
172
+ module=module_name,
173
  )
174
 
175
+ cls = getattr(module, class_name, None)
176
+ if cls is None or not inspect.isclass(cls):
177
+ return self._status(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  "error",
179
+ message="Class not found.",
180
+ module=module_name,
181
+ class_name=class_name,
182
  )
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  try:
185
+ instance = cls(*args)
186
+ return self._status(
187
+ "ok",
188
+ module=module_name,
189
+ class_name=class_name,
190
+ instance_type=type(instance).__name__,
191
+ repr=repr(instance),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except Exception as exc:
194
+ return self._status(
195
  "error",
196
+ module=module_name,
197
+ class_name=class_name,
198
+ error=str(exc),
199
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
biopython/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,8 @@
1
- """
2
- MCP Service Auto-Wrapper - Auto-generated
3
- """
4
  from mcp_service import create_app
5
 
6
- def main():
7
- """Main entry point"""
8
- app = create_app()
9
- return app
10
 
11
  if __name__ == "__main__":
12
- app = main()
13
- app.run()
 
1
+ """Local stdio entry point for MCP plugin."""
2
+
 
3
  from mcp_service import create_app
4
 
 
 
 
 
5
 
6
  if __name__ == "__main__":
7
+ # Local stdio use only (Claude Desktop / CLI), not for web/Docker deployment.
8
+ create_app().run(transport="stdio")
biopython/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,281 +1,264 @@
1
- import os
2
- import sys
3
- from io import StringIO
4
- from typing import List, Optional, Dict, Any
5
-
6
- source_path = os.path.join(
7
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
8
- "source",
9
- )
10
- if source_path not in sys.path:
11
- sys.path.insert(0, source_path)
12
-
13
- from fastmcp import FastMCP
14
- from Bio import SeqIO, AlignIO, Align
15
- from Bio.Seq import Seq
16
- from Bio.SeqRecord import SeqRecord
17
 
 
18
 
19
- mcp = FastMCP("biopython_core_service")
20
-
21
-
22
- @mcp.tool(
23
- name="seqio_parse_text",
24
- description="Parse sequence records from text using Bio.SeqIO.parse.",
25
- )
26
- def seqio_parse_text(format_name: str, data: str) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  """
28
- Parse sequence records from in-memory text content.
 
29
 
30
- Parameters:
31
- - format_name: Sequence file format (e.g., 'fasta', 'genbank').
32
- - data: Raw text content containing one or more records.
33
-
34
- Returns:
35
- - dict with success/result/error.
36
- """
37
  try:
38
- handle = StringIO(data)
39
- records = list(SeqIO.parse(handle, format_name))
40
- result = [
41
- {
42
- "id": rec.id,
43
- "name": rec.name,
44
- "description": rec.description,
45
- "seq": str(rec.seq),
46
- "length": len(rec.seq),
47
- }
48
- for rec in records
49
- ]
50
- return {"success": True, "result": result, "error": None}
51
- except Exception as e:
52
- return {"success": False, "result": None, "error": str(e)}
53
-
54
-
55
- @mcp.tool(
56
- name="seqio_read_text",
57
- description="Read exactly one sequence record from text using Bio.SeqIO.read.",
58
- )
59
- def seqio_read_text(format_name: str, data: str) -> Dict[str, Any]:
 
 
 
 
 
 
 
60
  """
61
- Read a single sequence record from in-memory text content.
62
-
63
- Parameters:
64
- - format_name: Sequence file format.
65
- - data: Raw text content containing exactly one record.
66
 
67
- Returns:
68
- - dict with success/result/error.
69
- """
70
  try:
71
- handle = StringIO(data)
72
- rec = SeqIO.read(handle, format_name)
73
- result = {
74
- "id": rec.id,
75
- "name": rec.name,
76
- "description": rec.description,
77
- "seq": str(rec.seq),
78
- "length": len(rec.seq),
79
- }
80
- return {"success": True, "result": result, "error": None}
81
- except Exception as e:
82
- return {"success": False, "result": None, "error": str(e)}
83
-
84
-
85
- @mcp.tool(
86
- name="seqio_write_fasta",
87
- description="Write sequence records to FASTA text using Bio.SeqIO.write.",
88
- )
89
- def seqio_write_fasta(
90
- sequences: List[str],
91
- ids: Optional[List[str]] = None,
92
- descriptions: Optional[List[str]] = None,
93
- ) -> Dict[str, Any]:
94
- """
95
- Create FASTA text from provided sequences.
96
 
97
- Parameters:
98
- - sequences: List of sequence strings.
99
- - ids: Optional list of IDs matching sequence count.
100
- - descriptions: Optional list of descriptions matching sequence count.
101
 
102
- Returns:
103
- - dict with success/result/error.
104
- """
105
- try:
106
- if ids is not None and len(ids) != len(sequences):
107
- raise ValueError("Length of ids must match length of sequences")
108
- if descriptions is not None and len(descriptions) != len(sequences):
109
- raise ValueError("Length of descriptions must match length of sequences")
110
-
111
- records = []
112
- for i, seq_text in enumerate(sequences):
113
- rec_id = ids[i] if ids is not None else f"seq_{i+1}"
114
- rec_desc = descriptions[i] if descriptions is not None else rec_id
115
- records.append(SeqRecord(Seq(seq_text), id=rec_id, description=rec_desc))
116
-
117
- out = StringIO()
118
- SeqIO.write(records, out, "fasta")
119
- return {"success": True, "result": out.getvalue(), "error": None}
120
- except Exception as e:
121
- return {"success": False, "result": None, "error": str(e)}
122
-
123
-
124
- @mcp.tool(
125
- name="alignio_parse_text",
126
- description="Parse alignments from text using Bio.AlignIO.parse.",
127
- )
128
- def alignio_parse_text(format_name: str, data: str) -> Dict[str, Any]:
129
- """
130
- Parse multiple alignments from in-memory text content.
131
-
132
- Parameters:
133
- - format_name: Alignment format (e.g., 'clustal', 'stockholm', 'phylip').
134
- - data: Raw text content containing one or more alignments.
135
 
136
- Returns:
137
- - dict with success/result/error.
138
  """
 
 
 
139
  try:
140
- handle = StringIO(data)
141
- aligns = list(AlignIO.parse(handle, format_name))
142
- result = [
143
- {
144
- "num_sequences": len(aln),
145
- "alignment_length": aln.get_alignment_length(),
146
- "ids": [rec.id for rec in aln],
147
- }
148
- for aln in aligns
149
- ]
150
- return {"success": True, "result": result, "error": None}
151
- except Exception as e:
152
- return {"success": False, "result": None, "error": str(e)}
153
-
154
-
155
- @mcp.tool(
156
- name="alignio_read_text",
157
- description="Read exactly one alignment from text using Bio.AlignIO.read.",
158
- )
159
- def alignio_read_text(format_name: str, data: str) -> Dict[str, Any]:
160
- """
161
- Read a single alignment from in-memory text content.
162
 
163
- Parameters:
164
- - format_name: Alignment format.
165
- - data: Raw text content containing exactly one alignment.
166
 
167
- Returns:
168
- - dict with success/result/error.
169
- """
170
- try:
171
- handle = StringIO(data)
172
- aln = AlignIO.read(handle, format_name)
173
- result = {
174
- "num_sequences": len(aln),
175
- "alignment_length": aln.get_alignment_length(),
176
- "ids": [rec.id for rec in aln],
177
- "rows": [str(rec.seq) for rec in aln],
178
- }
179
- return {"success": True, "result": result, "error": None}
180
- except Exception as e:
181
- return {"success": False, "result": None, "error": str(e)}
182
-
183
-
184
- @mcp.tool(
185
- name="align_pairwise_global",
186
- description="Run pairwise global alignment using Bio.Align.PairwiseAligner.",
187
- )
188
- def align_pairwise_global(
189
- sequence_a: str,
190
- sequence_b: str,
191
  match_score: float = 1.0,
192
- mismatch_score: float = 0.0,
193
- open_gap_score: float = -1.0,
194
- extend_gap_score: float = -0.5,
195
- ) -> Dict[str, Any]:
196
- """
197
- Perform global pairwise alignment with configurable scoring.
198
-
199
- Parameters:
200
- - sequence_a: First sequence.
201
- - sequence_b: Second sequence.
202
- - match_score: Score for matching characters.
203
- - mismatch_score: Score for mismatching characters.
204
- - open_gap_score: Gap opening score.
205
- - extend_gap_score: Gap extension score.
206
-
207
- Returns:
208
- - dict with success/result/error.
209
  """
 
 
 
210
  try:
211
- aligner = Align.PairwiseAligner()
212
- aligner.mode = "global"
213
- aligner.match_score = match_score
214
- aligner.mismatch_score = mismatch_score
215
- aligner.open_gap_score = open_gap_score
216
- aligner.extend_gap_score = extend_gap_score
217
-
218
- alignments = aligner.align(sequence_a, sequence_b)
219
- best = alignments[0]
220
- result = {
221
- "score": float(best.score),
222
- "num_alignments": len(alignments),
223
- "alignment": str(best),
224
- }
225
- return {"success": True, "result": result, "error": None}
226
- except Exception as e:
227
- return {"success": False, "result": None, "error": str(e)}
228
-
229
-
230
- @mcp.tool(
231
- name="align_pairwise_local",
232
- description="Run pairwise local alignment using Bio.Align.PairwiseAligner.",
233
- )
234
- def align_pairwise_local(
235
- sequence_a: str,
236
- sequence_b: str,
237
- match_score: float = 1.0,
238
- mismatch_score: float = 0.0,
239
- open_gap_score: float = -1.0,
240
- extend_gap_score: float = -0.5,
241
- ) -> Dict[str, Any]:
242
  """
243
- Perform local pairwise alignment with configurable scoring.
244
-
245
- Parameters:
246
- - sequence_a: First sequence.
247
- - sequence_b: Second sequence.
248
- - match_score: Score for matching characters.
249
- - mismatch_score: Score for mismatching characters.
250
- - open_gap_score: Gap opening score.
251
- - extend_gap_score: Gap extension score.
252
-
253
- Returns:
254
- - dict with success/result/error.
 
 
 
 
 
 
255
  """
256
  try:
257
- aligner = Align.PairwiseAligner()
258
- aligner.mode = "local"
259
- aligner.match_score = match_score
260
- aligner.mismatch_score = mismatch_score
261
- aligner.open_gap_score = open_gap_score
262
- aligner.extend_gap_score = extend_gap_score
263
-
264
- alignments = aligner.align(sequence_a, sequence_b)
265
- best = alignments[0]
266
- result = {
267
- "score": float(best.score),
268
- "num_alignments": len(alignments),
269
- "alignment": str(best),
270
- }
271
- return {"success": True, "result": result, "error": None}
272
- except Exception as e:
273
- return {"success": False, "result": None, "error": str(e)}
274
-
275
-
276
- def create_app() -> FastMCP:
 
 
 
277
  return mcp
278
 
279
 
280
  if __name__ == "__main__":
281
- mcp.run()
 
1
+ """FastMCP service exposing core Biopython capabilities."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ from __future__ import annotations
4
 
5
+ import importlib
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ SOURCE_DIR = Path(__file__).resolve().parents[2] / "source"
11
+ if SOURCE_DIR.exists():
12
+ source_dir_str = str(SOURCE_DIR)
13
+ if source_dir_str not in sys.path:
14
+ sys.path.insert(0, source_dir_str)
15
+
16
+ try:
17
+ from fastmcp import FastMCP
18
+ except Exception:
19
+ FastMCP = None
20
+
21
+ try:
22
+ _bio_seq_module = importlib.import_module("Bio.Seq")
23
+ Seq = getattr(_bio_seq_module, "Seq", None)
24
+ except Exception:
25
+ Seq = None
26
+
27
+ try:
28
+ SeqIO = importlib.import_module("Bio.SeqIO")
29
+ except Exception:
30
+ SeqIO = None
31
+
32
+ try:
33
+ pairwise2 = importlib.import_module("Bio.pairwise2")
34
+ except Exception:
35
+ pairwise2 = None
36
+
37
+ try:
38
+ _sequtils_module = importlib.import_module("Bio.SeqUtils")
39
+ gc_fraction = getattr(_sequtils_module, "gc_fraction", None)
40
+ except Exception:
41
+ gc_fraction = None
42
+
43
+ try:
44
+ _sequtils_module2 = importlib.import_module("Bio.SeqUtils")
45
+ nt_search = getattr(_sequtils_module2, "nt_search", None)
46
+ except Exception:
47
+ nt_search = None
48
+
49
+ from adapter import Adapter
50
+
51
+
52
+ class _FallbackMCP:
53
+ def __init__(self) -> None:
54
+ self.tools: list[Any] = []
55
+
56
+ def tool(self, name: str, description: str):
57
+ def decorator(func):
58
+ func.name = name
59
+ func.description = description
60
+ self.tools.append(func)
61
+ return func
62
+
63
+ return decorator
64
+
65
+ def run(self, *_, **__):
66
+ raise RuntimeError("fastmcp is unavailable")
67
+
68
+
69
+ mcp = FastMCP("biopython-mcp-service") if FastMCP is not None else _FallbackMCP()
70
+ adapter = Adapter(root_package="Bio")
71
+ adapter.load_all_submodules()
72
+
73
+
74
+ def _response(success: bool, result: Any = None, error: str | None = None) -> dict[str, Any]:
75
+ return {"success": success, "result": result, "error": error}
76
+
77
+
78
+ @mcp.tool(name="health_check", description="Check MCP service and Biopython dependency health.")
79
+ def health_check() -> dict[str, Any]:
80
+ """Return runtime health details including dependency availability and adapter status."""
81
+ deps = {
82
+ "fastmcp": FastMCP is not None,
83
+ "Bio.Seq": Seq is not None,
84
+ "Bio.SeqIO": SeqIO is not None,
85
+ "Bio.pairwise2": pairwise2 is not None,
86
+ "Bio.SeqUtils.gc_fraction": gc_fraction is not None,
87
+ "Bio.SeqUtils.nt_search": nt_search is not None,
88
+ }
89
+ return _response(True, {"dependencies": deps, "adapter": adapter.health()}, None)
90
+
91
+
92
+ @mcp.tool(name="parse_fasta_text", description="Parse FASTA text and return sequence summaries.")
93
+ def parse_fasta_text(fasta_text: str, max_records: int = 50) -> dict[str, Any]:
94
+ """Parse FASTA-formatted text.
95
+
96
+ Args:
97
+ fasta_text: FASTA input content.
98
+ max_records: Maximum number of records to return.
99
  """
100
+ if SeqIO is None:
101
+ return _response(False, None, "Bio.SeqIO is unavailable")
102
 
 
 
 
 
 
 
 
103
  try:
104
+ from io import StringIO
105
+
106
+ handle = StringIO(fasta_text)
107
+ parsed = []
108
+ for idx, record in enumerate(SeqIO.parse(handle, "fasta")):
109
+ if idx >= max_records:
110
+ break
111
+ parsed.append(
112
+ {
113
+ "id": record.id,
114
+ "name": record.name,
115
+ "description": record.description,
116
+ "length": len(record.seq),
117
+ "sequence": str(record.seq),
118
+ }
119
+ )
120
+ return _response(True, {"count": len(parsed), "records": parsed}, None)
121
+ except Exception as exc:
122
+ return _response(False, None, str(exc))
123
+
124
+
125
+ @mcp.tool(name="translate_dna", description="Translate a DNA sequence into amino acids.")
126
+ def translate_dna(sequence: str, to_stop: bool = False, table: int = 1) -> dict[str, Any]:
127
+ """Translate a nucleotide sequence.
128
+
129
+ Args:
130
+ sequence: DNA sequence string.
131
+ to_stop: Whether to stop at first stop codon.
132
+ table: NCBI codon table number.
133
  """
134
+ if Seq is None:
135
+ return _response(False, None, "Bio.Seq is unavailable")
 
 
 
136
 
 
 
 
137
  try:
138
+ protein = str(Seq(sequence).translate(to_stop=to_stop, table=table))
139
+ return _response(True, {"protein": protein, "length": len(protein)}, None)
140
+ except Exception as exc:
141
+ return _response(False, None, str(exc))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
 
 
 
 
143
 
144
+ @mcp.tool(name="reverse_complement", description="Compute reverse complement for a nucleotide sequence.")
145
+ def reverse_complement(sequence: str) -> dict[str, Any]:
146
+ """Return reverse complement of a DNA/RNA-like sequence.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ Args:
149
+ sequence: Input nucleotide sequence.
150
  """
151
+ if Seq is None:
152
+ return _response(False, None, "Bio.Seq is unavailable")
153
+
154
  try:
155
+ rc = str(Seq(sequence).reverse_complement())
156
+ return _response(True, {"reverse_complement": rc}, None)
157
+ except Exception as exc:
158
+ return _response(False, None, str(exc))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
 
 
 
160
 
161
+ @mcp.tool(name="global_align", description="Run global pairwise alignment between two sequences.")
162
+ def global_align(
163
+ seq_a: str,
164
+ seq_b: str,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  match_score: float = 1.0,
166
+ mismatch_score: float = -1.0,
167
+ open_gap_score: float = -0.5,
168
+ extend_gap_score: float = -0.1,
169
+ max_alignments: int = 5,
170
+ ) -> dict[str, Any]:
171
+ """Compute global pairwise alignments.
172
+
173
+ Args:
174
+ seq_a: First sequence.
175
+ seq_b: Second sequence.
176
+ match_score: Match score.
177
+ mismatch_score: Mismatch score.
178
+ open_gap_score: Gap opening penalty.
179
+ extend_gap_score: Gap extension penalty.
180
+ max_alignments: Maximum alignments to include in output.
 
 
181
  """
182
+ if pairwise2 is None:
183
+ return _response(False, None, "Bio.pairwise2 is unavailable")
184
+
185
  try:
186
+ alignments = pairwise2.align.globalms(
187
+ seq_a,
188
+ seq_b,
189
+ match_score,
190
+ mismatch_score,
191
+ open_gap_score,
192
+ extend_gap_score,
193
+ )
194
+ items = []
195
+ for alignment in alignments[: max_alignments if max_alignments > 0 else 1]:
196
+ items.append(
197
+ {
198
+ "seqA": alignment.seqA,
199
+ "seqB": alignment.seqB,
200
+ "score": alignment.score,
201
+ "start": alignment.start,
202
+ "end": alignment.end,
203
+ }
204
+ )
205
+ return _response(True, {"count": len(items), "alignments": items}, None)
206
+ except Exception as exc:
207
+ return _response(False, None, str(exc))
208
+
209
+
210
+ @mcp.tool(name="compute_gc_fraction", description="Compute GC fraction for a nucleotide sequence.")
211
+ def compute_gc_fraction(sequence: str) -> dict[str, Any]:
212
+ """Compute the GC fraction.
213
+
214
+ Args:
215
+ sequence: Input nucleotide sequence.
 
216
  """
217
+ try:
218
+ if gc_fraction is not None:
219
+ value = float(gc_fraction(sequence))
220
+ else:
221
+ seq = sequence.upper()
222
+ value = ((seq.count("G") + seq.count("C")) / len(seq)) if seq else 0.0
223
+ return _response(True, {"gc_fraction": value}, None)
224
+ except Exception as exc:
225
+ return _response(False, None, str(exc))
226
+
227
+
228
+ @mcp.tool(name="find_motif_positions", description="Find motif positions in a nucleotide sequence.")
229
+ def find_motif_positions(sequence: str, motif: str) -> dict[str, Any]:
230
+ """Find positions where motif occurs in a sequence.
231
+
232
+ Args:
233
+ sequence: Sequence to search in.
234
+ motif: Nucleotide motif to search for.
235
  """
236
  try:
237
+ if nt_search is not None:
238
+ raw = nt_search(sequence.upper(), motif.upper())
239
+ pattern = raw[0] if raw else motif
240
+ positions = raw[1:] if len(raw) > 1 else []
241
+ else:
242
+ pattern = motif
243
+ positions = []
244
+ start = 0
245
+ upper_seq = sequence.upper()
246
+ upper_motif = motif.upper()
247
+ while True:
248
+ idx = upper_seq.find(upper_motif, start)
249
+ if idx < 0:
250
+ break
251
+ positions.append(idx)
252
+ start = idx + 1
253
+ return _response(True, {"motif": pattern, "positions": positions}, None)
254
+ except Exception as exc:
255
+ return _response(False, None, str(exc))
256
+
257
+
258
+ def create_app():
259
+ """Create and return the module-level FastMCP app instance."""
260
  return mcp
261
 
262
 
263
  if __name__ == "__main__":
264
+ mcp.run()
biopython/mcp_output/requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
  numpy
 
1
  fastmcp
2
+ biopython
 
 
3
  numpy
biopython/mcp_output/start_mcp.py CHANGED
@@ -1,30 +1,33 @@
 
 
 
1
 
2
- """
3
- MCP Service Startup Entry
4
- """
5
- import sys
6
  import os
 
 
7
 
8
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
17
  app = create_app()
18
- # Use environment variable to configure port, default 8000
19
- port = int(os.environ.get("MCP_PORT", "8000"))
20
-
21
- # Choose transport mode based on environment variable
22
- transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
  if transport == "http":
24
- app.run(transport="http", host="0.0.0.0", port=port)
25
- else:
26
- # Default to STDIO mode
27
- app.run()
 
 
 
 
28
 
29
  if __name__ == "__main__":
30
  main()
 
1
+ """Environment-aware launcher for MCP service."""
2
+
3
+ from __future__ import annotations
4
 
 
 
 
 
5
  import os
6
+ import sys
7
+ from pathlib import Path
8
 
9
+ PLUGIN_DIR = Path(__file__).resolve().parent / "mcp_plugin"
10
+ plugin_dir_str = str(PLUGIN_DIR)
11
+ if plugin_dir_str not in sys.path:
12
+ sys.path.insert(0, plugin_dir_str)
13
 
14
  from mcp_service import create_app
15
 
16
+
17
+ def main() -> None:
18
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() or "stdio"
19
+ port = int(os.getenv("MCP_PORT", "8000"))
20
  app = create_app()
21
+
 
 
 
 
22
  if transport == "http":
23
+ try:
24
+ app.run(transport="http", host="0.0.0.0", port=port)
25
+ except TypeError:
26
+ app.run(transport="http", port=port)
27
+ return
28
+
29
+ app.run(transport="stdio")
30
+
31
 
32
  if __name__ == "__main__":
33
  main()
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "biopython",
3
- "port": 7862,
4
- "timestamp": 1773379300
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
  numpy
 
 
 
1
  fastmcp
2
+ biopython
 
 
3
  numpy
4
+ fastapi
5
+ uvicorn
run_docker.ps1 CHANGED
@@ -1,26 +1,7 @@
1
- cd $PSScriptRoot
2
  $ErrorActionPreference = "Stop"
3
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "biopython" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7862/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "biopython-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
7
- $mcpPath = Join-Path $mcpDir "mcp.json"
8
- if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
9
- $config = @{}
10
- if (Test-Path $mcpPath) {
11
- try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
12
- }
13
- $serversOrdered = [ordered]@{}
14
- if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
15
- $existing = $config.mcpServers
16
- if ($existing -is [pscustomobject]) {
17
- foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
18
- } elseif ($existing -is [System.Collections.IDictionary]) {
19
- foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
20
- }
21
- }
22
- $serversOrdered[$entryName] = @{ url = $entryUrl }
23
- $config = @{ mcpServers = $serversOrdered }
24
- $config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
25
  docker build -t $imageName .
26
- docker run --rm -p 7862:7860 $imageName
 
 
1
  $ErrorActionPreference = "Stop"
2
+
3
+ $port = (Get-Content -Raw "port.json" | ConvertFrom-Json).port
4
+ $imageName = "biopython-mcp"
5
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  docker build -t $imageName .
7
+ docker run --rm -it -p "${port}:${port}" $imageName
run_docker.sh CHANGED
@@ -1,75 +1,8 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-biopython}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7862/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
9
- if command -v python3 >/dev/null 2>&1; then
10
- python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
11
- import json, os, sys
12
- path, name, url = sys.argv[1:4]
13
- cfg = {"mcpServers": {}}
14
- if os.path.exists(path):
15
- try:
16
- with open(path, "r", encoding="utf-8") as f:
17
- cfg = json.load(f)
18
- except Exception:
19
- cfg = {"mcpServers": {}}
20
- if not isinstance(cfg, dict):
21
- cfg = {"mcpServers": {}}
22
- servers = cfg.get("mcpServers")
23
- if not isinstance(servers, dict):
24
- servers = {}
25
- ordered = {}
26
- for k, v in servers.items():
27
- if k != name:
28
- ordered[k] = v
29
- ordered[name] = {"url": url}
30
- cfg = {"mcpServers": ordered}
31
- with open(path, "w", encoding="utf-8") as f:
32
- json.dump(cfg, f, indent=2, ensure_ascii=False)
33
- PY
34
- elif command -v python >/dev/null 2>&1; then
35
- python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
36
- import json, os, sys
37
- path, name, url = sys.argv[1:4]
38
- cfg = {"mcpServers": {}}
39
- if os.path.exists(path):
40
- try:
41
- with open(path, "r", encoding="utf-8") as f:
42
- cfg = json.load(f)
43
- except Exception:
44
- cfg = {"mcpServers": {}}
45
- if not isinstance(cfg, dict):
46
- cfg = {"mcpServers": {}}
47
- servers = cfg.get("mcpServers")
48
- if not isinstance(servers, dict):
49
- servers = {}
50
- ordered = {}
51
- for k, v in servers.items():
52
- if k != name:
53
- ordered[k] = v
54
- ordered[name] = {"url": url}
55
- cfg = {"mcpServers": ordered}
56
- with open(path, "w", encoding="utf-8") as f:
57
- json.dump(cfg, f, indent=2, ensure_ascii=False)
58
- PY
59
- elif command -v jq >/dev/null 2>&1; then
60
- name="${mcp_entry_name}"; url="${mcp_entry_url}"
61
- if [ -f "${mcp_path}" ]; then
62
- tmp="$(mktemp)"
63
- jq --arg name "$name" --arg url "$url" '
64
- .mcpServers = (.mcpServers // {})
65
- | .mcpServers as $s
66
- | ($s | with_entries(select(.key != $name))) as $base
67
- | .mcpServers = ($base + {($name): {"url": $url}})
68
- ' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
69
- else
70
- printf '{ "mcpServers": { "%s": { "url": "%s" } } }
71
- ' "$name" "$url" > "${mcp_path}"
72
- fi
73
- fi
74
- docker build -t biopython-mcp .
75
- docker run --rm -p 7862:7860 biopython-mcp
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
+ PORT=$(python3 -c 'import json; print(json.load(open("port.json", "r", encoding="utf-8"))["port"])')
5
+ IMAGE_NAME="biopython-mcp"
6
+
7
+ DOCKER_BUILDKIT=1 docker build -t "$IMAGE_NAME" .
8
+ docker run --rm -it -p "${PORT}:${PORT}" "$IMAGE_NAME"