guohanghui commited on
Commit
448dd94
·
verified ·
1 Parent(s): 2173366

Update biopython/mcp_output/mcp_plugin/mcp_service.py

Browse files
biopython/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,213 +1,474 @@
1
  import os
2
  import sys
 
3
 
 
4
  source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
5
- sys.path.insert(0, source_path)
 
6
 
7
  from fastmcp import FastMCP
8
- from Bio.SeqIO import parse, read, write
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from Bio.Seq import Seq
10
- from Bio.SeqRecord import SeqRecord
11
- from Bio.Blast.NCBIWWW import qblast
12
- from Bio.Entrez import efetch, esearch, email
13
- import Bio.Entrez
14
-
15
- # 设置 NCBI Entrez email(必需)
16
- Bio.Entrez.email = "biopython-mcp@huggingface.co"
17
-
18
- mcp = FastMCP("biopython_service")
19
-
20
- def seqrecord_to_dict(record):
21
- """Convert a SeqRecord object to a serializable dictionary."""
22
- return {
23
- "id": str(record.id),
24
- "name": str(record.name),
25
- "description": str(record.description),
26
- "sequence": str(record.seq),
27
- "length": len(record.seq),
28
- "annotations": dict(record.annotations) if record.annotations else {},
29
- "features": [
30
- {
31
- "type": f.type,
32
- "location": str(f.location),
33
- "qualifiers": dict(f.qualifiers)
34
- }
35
- for f in record.features
36
- ] if record.features else []
37
- }
38
-
39
- def dict_to_seqrecord(seq_dict):
40
- """Convert a dictionary back to a SeqRecord object."""
41
- if isinstance(seq_dict, SeqRecord):
42
- return seq_dict
43
-
44
- # 从字典创建 SeqRecord
45
- record = SeqRecord(
46
- Seq(seq_dict.get("sequence", "")),
47
- id=seq_dict.get("id", ""),
48
- name=seq_dict.get("name", ""),
49
- description=seq_dict.get("description", "")
50
- )
51
-
52
- # 添加注释
53
- if "annotations" in seq_dict:
54
- record.annotations.update(seq_dict["annotations"])
55
-
56
- return record
57
-
58
- @mcp.tool(name="seqio_parse", description="Parse sequence data from a file.")
59
- def seqio_parse(file_path: str, format: str) -> dict:
60
- """
61
- Parses sequence data from a file.
62
-
63
- Parameters:
64
- - file_path: Path to the sequence file.
65
- - format: Format of the sequence file (e.g., 'fasta').
66
-
67
- Returns:
68
- - A dictionary with success status and parsed sequences or error message.
69
- """
70
- try:
71
- sequences = list(parse(file_path, format))
72
- # Convert SeqRecord objects to serializable dictionaries
73
- result = [seqrecord_to_dict(seq) for seq in sequences]
74
  return {"success": True, "result": result, "error": None}
75
  except Exception as e:
76
  return {"success": False, "result": None, "error": str(e)}
77
 
78
- @mcp.tool(name="seqio_read", description="Read a single sequence from a file.")
79
- def seqio_read(file_path: str, format: str) -> dict:
80
- """
81
- Reads a single sequence from a file.
82
 
83
- Parameters:
84
- - file_path: Path to the sequence file.
85
- - format: Format of the sequence file (e.g., 'fasta').
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- Returns:
88
- - A dictionary with success status and the sequence or error message.
89
- """
90
  try:
91
- sequence = read(file_path, format)
92
- # Convert SeqRecord object to serializable dictionary
93
- result = seqrecord_to_dict(sequence)
94
  return {"success": True, "result": result, "error": None}
95
  except Exception as e:
96
  return {"success": False, "result": None, "error": str(e)}
97
 
98
- @mcp.tool(name="seqio_write", description="Write sequences to a file.")
99
- def seqio_write(sequences, file_path: str, format: str) -> dict:
100
- """
101
- Writes sequences to a file.
102
-
103
- Parameters:
104
- - sequences: List of sequences to write (can be SeqRecord objects or dicts).
105
- - file_path: Path to the output file.
106
- - format: Format of the output file (e.g., 'fasta').
107
-
108
- Returns:
109
- - A dictionary with success status and number of records written or error message.
110
- """
111
- try:
112
- # 如果 sequences 是列表,将每个元素转换为 SeqRecord
113
- if isinstance(sequences, list):
114
- records = []
115
- for seq in sequences:
116
- if isinstance(seq, dict):
117
- records.append(dict_to_seqrecord(seq))
118
- else:
119
- records.append(seq)
120
- else:
121
- # 如果是单个对象
122
- if isinstance(sequences, dict):
123
- records = [dict_to_seqrecord(sequences)]
124
  else:
125
- records = [sequences]
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- count = write(records, file_path, format)
128
- return {"success": True, "result": count, "error": None}
129
  except Exception as e:
130
  return {"success": False, "result": None, "error": str(e)}
131
 
132
- @mcp.tool(name="blast_qblast", description="Run a BLAST query using NCBI's BLAST service.")
133
- def blast_qblast(program: str, database: str, sequence: str) -> dict:
134
- """
135
- Runs a BLAST query using NCBI's BLAST service.
 
 
 
136
 
137
- Parameters:
138
- - program: BLAST program to use (e.g., 'blastn').
139
- - database: Database to search against (e.g., 'nt').
140
- - sequence: Sequence to search.
 
 
 
141
 
142
- Returns:
143
- - A dictionary with success status and BLAST result or error message.
144
- """
145
  try:
146
- result_handle = qblast(program, database, sequence)
147
- return {"success": True, "result": result_handle.read(), "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  except Exception as e:
149
  return {"success": False, "result": None, "error": str(e)}
150
 
151
- @mcp.tool(name="entrez_efetch", description="Fetch data from NCBI's Entrez databases.")
152
- def entrez_efetch(db: str, id: str, rettype: str, retmode: str) -> dict:
153
- """
154
- Fetches data from NCBI's Entrez databases.
155
 
156
- Parameters:
157
- - db: Database to fetch from (e.g., 'nucleotide').
158
- - id: ID of the record to fetch.
159
- - rettype: Return type (e.g., 'gb').
160
- - retmode: Return mode (e.g., 'text').
161
 
162
- Returns:
163
- - A dictionary with success status and fetched data or error message.
164
- """
165
  try:
166
- handle = efetch(db=db, id=id, rettype=rettype, retmode=retmode)
167
- return {"success": True, "result": handle.read(), "error": None}
 
168
  except Exception as e:
169
  return {"success": False, "result": None, "error": str(e)}
170
 
171
- @mcp.tool(name="entrez_esearch", description="Search NCBI's Entrez databases.")
172
- def entrez_esearch(db: str, term: str) -> dict:
173
- """
174
- Searches NCBI's Entrez databases.
175
 
176
- Parameters:
177
- - db: Database to search (e.g., 'nucleotide', 'protein', 'gene', 'pubmed').
178
- - term: Search term. Use proper NCBI query syntax:
179
- - gene_name[GENE] - Search by gene name
180
- - organism[ORGN] - Search by organism
181
- - "Homo sapiens"[ORGN] - Species filter
182
- - "RefSeq"[Filter] - Only RefSeq records
 
 
183
 
184
- Returns:
185
- - A dictionary with success status and search results or error message.
186
-
187
- Examples:
188
- - "TP53[gene] AND human[organism]"
189
- - "BRCA1[gene]"
190
- - "insulin[protein name]"
191
- """
192
  try:
193
- # 确保 email 已设置
194
- if not Bio.Entrez.email:
195
- Bio.Entrez.email = "biopython-mcp@huggingface.co"
196
-
197
- # 使用 retmax 参数获取更多结果
198
- handle = esearch(db=db, term=term, retmax=100)
199
- result = handle.read()
200
- handle.close()
201
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  return {"success": True, "result": result, "error": None}
203
  except Exception as e:
204
  return {"success": False, "result": None, "error": str(e)}
205
 
206
- def create_app() -> FastMCP:
207
- """
208
- Creates and returns the FastMCP application instance.
209
 
210
- Returns:
211
- - FastMCP instance.
212
- """
213
- return mcp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
+ from io import StringIO
4
 
5
+ # Ensure Biopython source is importable when running the MCP service
6
  source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
7
+ if source_path not in sys.path:
8
+ sys.path.insert(0, source_path)
9
 
10
  from fastmcp import FastMCP
11
+
12
+ # Core Biopython utilities from setup.py
13
+ from setup import can_import as biopy_can_import
14
+ from setup import get_version as biopy_get_version
15
+
16
+ # Script entrypoints (aliased to avoid name shadowing)
17
+ from Scripts.query_pubmed import print_usage as query_pubmed_print_usage
18
+ from Scripts.update_ncbi_codon_table import line_wrap as codon_line_wrap
19
+ from Scripts.scop_pdb import main as scop_main
20
+ from Scripts.scop_pdb import open_pdb as scop_open_pdb
21
+ from Scripts.scop_pdb import usage as scop_usage
22
+ from Scripts.xbbtools.nextorf import MissingTable
23
+ from Scripts.xbbtools.nextorf import NextOrf
24
+ from Scripts.xbbtools.nextorf import help as nextorf_help
25
+ from Scripts.xbbtools.nextorf import makeTableX
26
+ from Scripts.xbbtools.xbb_utils import NotePad
27
+
28
+ # Biopython top-level version
29
+ from Bio import __version__ as bio_version
30
+ from Bio import Entrez, SeqIO, pairwise2, Phylo, AlignIO
31
+ from Bio.PDB import PDBParser
32
+ from Bio.Align import MultipleSeqAlignment
33
  from Bio.Seq import Seq
34
+ from Bio.SeqUtils import GC
35
+
36
+ mcp = FastMCP("biopython")
37
+
38
+ # Configure Entrez email (required by NCBI); allow user override via env
39
+ Entrez.email = os.getenv("BIOPYTHON_ENTREZ_EMAIL", "biopython-mcp@huggingface.co")
40
+
41
+
42
+ @mcp.tool(name="can_import", description="Check if a module can be imported (from setup.py)")
43
+ def can_import_tool(payload: dict):
44
+ try:
45
+ result = biopy_can_import(**payload)
46
+ return {"success": True, "result": str(result), "error": None}
47
+ except Exception as e:
48
+ return {"success": False, "result": None, "error": str(e)}
49
+
50
+ @mcp.tool(name="get_version", description="Biopython package version from setup.py")
51
+ def get_version_tool(payload: dict | None = None):
52
+ try:
53
+ result = biopy_get_version()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return {"success": True, "result": result, "error": None}
55
  except Exception as e:
56
  return {"success": False, "result": None, "error": str(e)}
57
 
 
 
 
 
58
 
59
+ @mcp.tool(name="bio_version", description="Return Bio.__version__ from the Biopython package")
60
+ def bio_version_tool(payload: dict | None = None):
61
+ try:
62
+ return {"success": True, "result": bio_version, "error": None}
63
+ except Exception as e:
64
+ return {"success": False, "result": None, "error": str(e)}
65
+
66
+ @mcp.tool(name="query_pubmed_print_usage", description="Print usage for query_pubmed script")
67
+ def print_usage_tool(payload: dict | None = None):
68
+ try:
69
+ result = query_pubmed_print_usage()
70
+ return {"success": True, "result": result, "error": None}
71
+ except Exception as e:
72
+ return {"success": False, "result": None, "error": str(e)}
73
+
74
+ @mcp.tool(name="line_wrap", description="Wrap long strings (from update_ncbi_codon_table.py)")
75
+ def line_wrap_tool(payload: dict):
76
+ try:
77
+ result = codon_line_wrap(**payload)
78
+ return {"success": True, "result": result, "error": None}
79
+ except Exception as e:
80
+ return {"success": False, "result": None, "error": str(e)}
81
+
82
+ @mcp.tool(name="scop_main", description="Extract SCOP domain PDB records (Scripts/scop_pdb.py main)")
83
+ def scop_main_tool(payload: dict | None = None):
84
+ try:
85
+ result = scop_main()
86
+ return {"success": True, "result": result, "error": None}
87
+ except Exception as e:
88
+ return {"success": False, "result": None, "error": str(e)}
89
+
90
+ @mcp.tool(name="scop_open_pdb", description="Download/open a PDB file (Scripts/scop_pdb.py open_pdb)")
91
+ def open_pdb_tool(payload: dict):
92
+ try:
93
+ result = scop_open_pdb(**payload)
94
+ return {"success": True, "result": str(result), "error": None}
95
+ except Exception as e:
96
+ return {"success": False, "result": None, "error": str(e)}
97
 
98
+ @mcp.tool(name="scop_usage", description="Print usage for scop_pdb.py")
99
+ def usage_tool(payload: dict | None = None):
 
100
  try:
101
+ result = scop_usage()
 
 
102
  return {"success": True, "result": result, "error": None}
103
  except Exception as e:
104
  return {"success": False, "result": None, "error": str(e)}
105
 
106
+ @mcp.tool(name="notepad", description="Instantiate Scripts/xbbtools/xbb_utils.NotePad")
107
+ def notepad_tool(*args, **kwargs):
108
+ """NotePad class"""
109
+ try:
110
+ if NotePad is None:
111
+ return {"success": False, "result": None, "error": "Class NotePad is not available, path may need adjustment"}
112
+
113
+ # MCP parameter type conversion
114
+ converted_args = []
115
+ converted_kwargs = kwargs.copy()
116
+
117
+ # Handle position argument type conversion
118
+ for arg in args:
119
+ if isinstance(arg, str):
120
+ # Try to convert to numeric type
121
+ try:
122
+ if '.' in arg:
123
+ converted_args.append(float(arg))
124
+ else:
125
+ converted_args.append(int(arg))
126
+ except ValueError:
127
+ converted_args.append(arg)
 
 
 
 
128
  else:
129
+ converted_args.append(arg)
130
+
131
+ # Handle keyword argument type conversion
132
+ for key, value in converted_kwargs.items():
133
+ if isinstance(value, str):
134
+ try:
135
+ if '.' in value:
136
+ converted_kwargs[key] = float(value)
137
+ else:
138
+ converted_kwargs[key] = int(value)
139
+ except ValueError:
140
+ pass
141
 
142
+ instance = NotePad(*converted_args, **converted_kwargs)
143
+ return {"success": True, "result": str(instance), "error": None}
144
  except Exception as e:
145
  return {"success": False, "result": None, "error": str(e)}
146
 
147
+ @mcp.tool(name="nextorf_help", description="Help text for NextOrf (Scripts/xbbtools/nextorf.py)")
148
+ def help_tool(payload: dict | None = None):
149
+ try:
150
+ result = nextorf_help()
151
+ return {"success": True, "result": result, "error": None}
152
+ except Exception as e:
153
+ return {"success": False, "result": None, "error": str(e)}
154
 
155
+ @mcp.tool(name="makeTableX", description="Generate NextOrf lookup table (Scripts/xbbtools/nextorf.py)")
156
+ def makeTableX_tool(payload: dict):
157
+ try:
158
+ result = makeTableX(**payload)
159
+ return {"success": True, "result": result, "error": None}
160
+ except Exception as e:
161
+ return {"success": False, "result": None, "error": str(e)}
162
 
163
+ @mcp.tool(name="missingtable", description="Instantiate MissingTable (Scripts/xbbtools/nextorf.py)")
164
+ def missingtable_tool(*args, **kwargs):
165
+ """MissingTable class"""
166
  try:
167
+ if MissingTable is None:
168
+ return {"success": False, "result": None, "error": "Class MissingTable is not available, path may need adjustment"}
169
+
170
+ # MCP parameter type conversion
171
+ converted_args = []
172
+ converted_kwargs = kwargs.copy()
173
+
174
+ # Handle position argument type conversion
175
+ for arg in args:
176
+ if isinstance(arg, str):
177
+ # Try to convert to numeric type
178
+ try:
179
+ if '.' in arg:
180
+ converted_args.append(float(arg))
181
+ else:
182
+ converted_args.append(int(arg))
183
+ except ValueError:
184
+ converted_args.append(arg)
185
+ else:
186
+ converted_args.append(arg)
187
+
188
+ # Handle keyword argument type conversion
189
+ for key, value in converted_kwargs.items():
190
+ if isinstance(value, str):
191
+ try:
192
+ if '.' in value:
193
+ converted_kwargs[key] = float(value)
194
+ else:
195
+ converted_kwargs[key] = int(value)
196
+ except ValueError:
197
+ pass
198
+
199
+ instance = MissingTable(*converted_args, **converted_kwargs)
200
+ return {"success": True, "result": str(instance), "error": None}
201
+ except Exception as e:
202
+ return {"success": False, "result": None, "error": str(e)}
203
+
204
+ @mcp.tool(name="nextorf", description="Instantiate NextOrf (Scripts/xbbtools/nextorf.py)")
205
+ def nextorf_tool(*args, **kwargs):
206
+ """NextOrf class"""
207
+ try:
208
+ if NextOrf is None:
209
+ return {"success": False, "result": None, "error": "Class NextOrf is not available, path may need adjustment"}
210
+
211
+ # MCP parameter type conversion
212
+ converted_args = []
213
+ converted_kwargs = kwargs.copy()
214
+
215
+ # Handle position argument type conversion
216
+ for arg in args:
217
+ if isinstance(arg, str):
218
+ # Try to convert to numeric type
219
+ try:
220
+ if '.' in arg:
221
+ converted_args.append(float(arg))
222
+ else:
223
+ converted_args.append(int(arg))
224
+ except ValueError:
225
+ converted_args.append(arg)
226
+ else:
227
+ converted_args.append(arg)
228
+
229
+ # Handle keyword argument type conversion
230
+ for key, value in converted_kwargs.items():
231
+ if isinstance(value, str):
232
+ try:
233
+ if '.' in value:
234
+ converted_kwargs[key] = float(value)
235
+ else:
236
+ converted_kwargs[key] = int(value)
237
+ except ValueError:
238
+ pass
239
+
240
+ instance = NextOrf(*converted_args, **converted_kwargs)
241
+ return {"success": True, "result": str(instance), "error": None}
242
  except Exception as e:
243
  return {"success": False, "result": None, "error": str(e)}
244
 
 
 
 
 
245
 
246
+ # -----------------------
247
+ # Core Biopython utilities
248
+ # -----------------------
 
 
249
 
250
+
251
+ @mcp.tool(name="seq_reverse_complement", description="Reverse-complement a DNA/RNA sequence string")
252
+ def seq_reverse_complement(payload: dict):
253
  try:
254
+ seq = payload.get("sequence", "")
255
+ result = str(Seq(seq).reverse_complement())
256
+ return {"success": True, "result": result, "error": None}
257
  except Exception as e:
258
  return {"success": False, "result": None, "error": str(e)}
259
 
 
 
 
 
260
 
261
+ @mcp.tool(name="seq_transcribe", description="Transcribe DNA to RNA (replace T->U)")
262
+ def seq_transcribe(payload: dict):
263
+ try:
264
+ seq = payload.get("sequence", "")
265
+ result = str(Seq(seq).transcribe())
266
+ return {"success": True, "result": result, "error": None}
267
+ except Exception as e:
268
+ return {"success": False, "result": None, "error": str(e)}
269
+
270
 
271
+ @mcp.tool(name="seq_translate", description="Translate DNA/RNA to protein; accepts optional table, to_stop")
272
+ def seq_translate(payload: dict):
 
 
 
 
 
 
273
  try:
274
+ seq = payload.get("sequence", "")
275
+ table = payload.get("table", 1)
276
+ to_stop = bool(payload.get("to_stop", False))
277
+ cds = bool(payload.get("cds", False))
278
+ result = str(Seq(seq).translate(table=table, to_stop=to_stop, cds=cds))
279
+ return {"success": True, "result": result, "error": None}
280
+ except Exception as e:
281
+ return {"success": False, "result": None, "error": str(e)}
282
+
283
+
284
+ @mcp.tool(name="gc_content", description="Compute GC percentage of a sequence")
285
+ def gc_content(payload: dict):
286
+ try:
287
+ seq = payload.get("sequence", "")
288
+ result = GC(seq)
289
+ return {"success": True, "result": result, "error": None}
290
+ except Exception as e:
291
+ return {"success": False, "result": None, "error": str(e)}
292
+
293
+
294
+ @mcp.tool(name="pairwise_align_globalxx", description="Global alignment (identity scoring) between two sequences")
295
+ def pairwise_align_globalxx(payload: dict):
296
+ try:
297
+ seq1 = payload.get("seq1", "")
298
+ seq2 = payload.get("seq2", "")
299
+ limit = int(payload.get("limit", 3))
300
+ aligns = pairwise2.align.globalxx(seq1, seq2)[:limit]
301
+ result = [
302
+ {
303
+ "seqA": a.seqA,
304
+ "seqB": a.seqB,
305
+ "score": a.score,
306
+ "start": a.start,
307
+ "end": a.end,
308
+ }
309
+ for a in aligns
310
+ ]
311
  return {"success": True, "result": result, "error": None}
312
  except Exception as e:
313
  return {"success": False, "result": None, "error": str(e)}
314
 
 
 
 
315
 
316
+ @mcp.tool(name="seqio_parse_string", description="Parse FASTA/GenBank data from string; returns list of records")
317
+ def seqio_parse_string(payload: dict):
318
+ try:
319
+ data = payload.get("data", "")
320
+ fmt = payload.get("format", "fasta")
321
+ handle = StringIO(data)
322
+ records = []
323
+ for rec in SeqIO.parse(handle, fmt):
324
+ records.append({"id": rec.id, "name": rec.name, "description": rec.description, "seq": str(rec.seq)})
325
+ return {"success": True, "result": records, "error": None}
326
+ except Exception as e:
327
+ return {"success": False, "result": None, "error": str(e)}
328
+
329
+
330
+ @mcp.tool(name="seqio_convert_file", description="Convert sequence file between formats (e.g., fasta->genbank)")
331
+ def seqio_convert_file(payload: dict):
332
+ try:
333
+ input_path = payload["input_path"]
334
+ input_format = payload.get("input_format", "fasta")
335
+ output_path = payload["output_path"]
336
+ output_format = payload.get("output_format", "genbank")
337
+ count = SeqIO.convert(input_path, input_format, output_path, output_format)
338
+ return {"success": True, "result": {"converted_records": count, "output_path": output_path}, "error": None}
339
+ except Exception as e:
340
+ return {"success": False, "result": None, "error": str(e)}
341
+
342
+
343
+ @mcp.tool(name="entrez_search", description="NCBI Entrez esearch; provide db, term; optional retmax")
344
+ def entrez_search(payload: dict):
345
+ try:
346
+ db = payload.get("db", "pubmed")
347
+ term = payload.get("term", "")
348
+ retmax = int(payload.get("retmax", 20))
349
+ handle = Entrez.esearch(db=db, term=term, retmax=retmax, usehistory="y")
350
+ rec = Entrez.read(handle)
351
+ handle.close()
352
+ return {
353
+ "success": True,
354
+ "result": {"ids": rec.get("IdList", []), "count": int(rec.get("Count", 0)), "webenv": rec.get("WebEnv"), "query_key": rec.get("QueryKey")},
355
+ "error": None,
356
+ }
357
+ except Exception as e:
358
+ return {"success": False, "result": None, "error": str(e)}
359
+
360
+
361
+ @mcp.tool(name="entrez_fetch", description="NCBI Entrez efetch; provide db, id(s), rettype, retmode")
362
+ def entrez_fetch(payload: dict):
363
+ try:
364
+ db = payload.get("db", "pubmed")
365
+ ids = payload.get("ids", [])
366
+ if isinstance(ids, list):
367
+ ids = ",".join(ids)
368
+ rettype = payload.get("rettype", "medline")
369
+ retmode = payload.get("retmode", "text")
370
+ handle = Entrez.efetch(db=db, id=ids, rettype=rettype, retmode=retmode)
371
+ data = handle.read()
372
+ handle.close()
373
+ return {"success": True, "result": data, "error": None}
374
+ except Exception as e:
375
+ return {"success": False, "result": None, "error": str(e)}
376
+
377
+
378
+ @mcp.tool(name="set_entrez_email", description="Override Entrez.email at runtime; required for NCBI")
379
+ def set_entrez_email(payload: dict):
380
+ try:
381
+ email = payload.get("email", "")
382
+ if not email:
383
+ return {"success": False, "result": None, "error": "email is required"}
384
+ Entrez.email = email
385
+ return {"success": True, "result": {"email": Entrez.email}, "error": None}
386
+ except Exception as e:
387
+ return {"success": False, "result": None, "error": str(e)}
388
+
389
+
390
+ @mcp.tool(name="phylo_parse_newick", description="Parse Newick string; return leaf names and counts")
391
+ def phylo_parse_newick(payload: dict):
392
+ try:
393
+ newick = payload.get("newick", "")
394
+ tree = Phylo.read(StringIO(newick), "newick")
395
+ leaves = [term.name for term in tree.get_terminals()]
396
+ return {"success": True, "result": {"leaf_count": len(leaves), "leaves": leaves}, "error": None}
397
+ except Exception as e:
398
+ return {"success": False, "result": None, "error": str(e)}
399
+
400
+
401
+ @mcp.tool(name="pdb_summary", description="Parse a PDB file; report chains, residues, atoms")
402
+ def pdb_summary(payload: dict):
403
+ try:
404
+ path = payload.get("path", "")
405
+ structure_id = payload.get("structure_id", "structure")
406
+ parser = PDBParser(QUIET=True)
407
+ structure = parser.get_structure(structure_id, path)
408
+ chains = []
409
+ for model in structure:
410
+ for chain in model:
411
+ atom_count = sum(1 for _ in chain.get_atoms())
412
+ res_count = sum(1 for _ in chain.get_residues())
413
+ chains.append({"id": chain.id, "residues": res_count, "atoms": atom_count})
414
+ return {"success": True, "result": {"chains": chains}, "error": None}
415
+ except Exception as e:
416
+ return {"success": False, "result": None, "error": str(e)}
417
+
418
+
419
+ @mcp.tool(name="msa_read_fasta", description="Read a FASTA alignment file; return length and sequences")
420
+ def msa_read_fasta(payload: dict):
421
+ try:
422
+ path = payload.get("path", "")
423
+ alignment = AlignIO.read(path, "fasta")
424
+ seqs = [{"id": rec.id, "seq": str(rec.seq)} for rec in alignment]
425
+ aln_len = alignment.get_alignment_length() if isinstance(alignment, MultipleSeqAlignment) else None
426
+ return {"success": True, "result": {"alignment_length": aln_len, "sequences": seqs}, "error": None}
427
+ except Exception as e:
428
+ return {"success": False, "result": None, "error": str(e)}
429
+
430
+
431
+ @mcp.tool(name="msa_consensus", description="Compute simple consensus from FASTA alignment file")
432
+ def msa_consensus(payload: dict):
433
+ try:
434
+ path = payload.get("path", "")
435
+ alignment = AlignIO.read(path, "fasta")
436
+ consensus = alignment.column_annotations.get("consensus") if hasattr(alignment, "column_annotations") else None
437
+ if consensus is None:
438
+ # naive majority rule per column
439
+ aln_len = alignment.get_alignment_length()
440
+ cols = []
441
+ for i in range(aln_len):
442
+ counts = {}
443
+ for rec in alignment:
444
+ c = rec.seq[i]
445
+ counts[c] = counts.get(c, 0) + 1
446
+ cols.append(max(counts, key=counts.get))
447
+ consensus = "".join(cols)
448
+ return {"success": True, "result": {"consensus": str(consensus)}, "error": None}
449
+ except Exception as e:
450
+ return {"success": False, "result": None, "error": str(e)}
451
+
452
+
453
+ @mcp.tool(name="fasta_filter_length", description="Filter FASTA sequences by minimum length; returns kept sequences")
454
+ def fasta_filter_length(payload: dict):
455
+ try:
456
+ data = payload.get("data", "")
457
+ min_len = int(payload.get("min_len", 1))
458
+ handle = StringIO(data)
459
+ kept = []
460
+ for rec in SeqIO.parse(handle, "fasta"):
461
+ if len(rec.seq) >= min_len:
462
+ kept.append({"id": rec.id, "seq": str(rec.seq), "len": len(rec.seq)})
463
+ return {"success": True, "result": {"kept": kept, "count": len(kept)}, "error": None}
464
+ except Exception as e:
465
+ return {"success": False, "result": None, "error": str(e)}
466
+
467
+
468
+
469
+ def create_app():
470
+ """Create and return FastMCP application instance"""
471
+ return mcp
472
+
473
+ if __name__ == "__main__":
474
+ mcp.run(transport="http", host="0.0.0.0", port=8000)