guohanghui commited on
Commit
4bc01de
·
verified ·
1 Parent(s): 154a8b6

Update biopython/mcp_output/mcp_plugin/mcp_service.py

Browse files
biopython/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -36,6 +36,10 @@ mcp = FastMCP("biopython_gene_species_identification")
36
  # Configure Entrez email (required by NCBI)
37
  Entrez.email = os.getenv("BIOPYTHON_ENTREZ_EMAIL", "biopython-mcp@huggingface.co")
38
 
 
 
 
 
39
 
40
  # ==============================================
41
  # 基础工具 - Basic Utilities
@@ -141,7 +145,215 @@ def calculate_gc_content(payload: dict):
141
 
142
 
143
  # ==============================================
144
- # BLAST 搜索工具 - BLAST Search Tools
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  # ==============================================
146
 
147
  @mcp.tool(name="blast_search", description="Search NCBI database using BLAST to find similar sequences")
@@ -467,6 +679,174 @@ def predict_species(payload: dict):
467
  # 完整流程工具 - Complete Workflow Tool
468
  # ==============================================
469
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  @mcp.tool(name="identify_gene_species_complete",
471
  description="Complete workflow: input unknown gene sequence, identify species through BLAST search and analysis")
472
  def identify_gene_species_complete(payload: dict):
 
36
  # Configure Entrez email (required by NCBI)
37
  Entrez.email = os.getenv("BIOPYTHON_ENTREZ_EMAIL", "biopython-mcp@huggingface.co")
38
 
39
+ # Global storage for async BLAST jobs
40
+ blast_jobs: Dict[str, Dict[str, Any]] = {}
41
+ blast_lock = threading.Lock()
42
+
43
 
44
  # ==============================================
45
  # 基础工具 - Basic Utilities
 
145
 
146
 
147
  # ==============================================
148
+ # 异步 BLAST 搜索工具 - Async BLAST Tools
149
+ # ==============================================
150
+
151
+ def _run_blast_async(job_id: str, sequence: str, database: str, program: str,
152
+ hitlist_size: int, expect: float):
153
+ """后台运行 BLAST 搜索"""
154
+ try:
155
+ print(f"🔄 [Job {job_id}] Starting BLAST search in background...")
156
+
157
+ with blast_lock:
158
+ blast_jobs[job_id]["status"] = "running"
159
+ blast_jobs[job_id]["message"] = "BLAST search in progress..."
160
+
161
+ # 执行 BLAST
162
+ handle = NCBIWWW.qblast(
163
+ program=program,
164
+ database=database,
165
+ sequence=sequence,
166
+ hitlist_size=hitlist_size,
167
+ expect=expect,
168
+ format_type="XML"
169
+ )
170
+
171
+ blast_record = NCBIXML.read(handle)
172
+ handle.close()
173
+
174
+ # 处理结果
175
+ hits = []
176
+ for idx, alignment in enumerate(blast_record.alignments[:hitlist_size]):
177
+ if not alignment.hsps:
178
+ continue
179
+
180
+ hsp = alignment.hsps[0]
181
+ align_len = int(hsp.align_length)
182
+ identities = int(hsp.identities)
183
+ identity_pct = (identities / align_len * 100.0) if align_len else 0
184
+
185
+ hits.append({
186
+ "rank": idx + 1,
187
+ "accession": alignment.accession,
188
+ "hit_id": alignment.hit_id,
189
+ "hit_def": alignment.hit_def,
190
+ "length": alignment.length,
191
+ "bit_score": float(hsp.bits),
192
+ "evalue": float(hsp.expect),
193
+ "identity_pct": round(identity_pct, 2),
194
+ "identities": identities,
195
+ "align_length": align_len
196
+ })
197
+
198
+ result = {
199
+ "query_length": len(sequence),
200
+ "database": database,
201
+ "program": program,
202
+ "total_hits": len(hits),
203
+ "hits": hits
204
+ }
205
+
206
+ with blast_lock:
207
+ blast_jobs[job_id]["status"] = "completed"
208
+ blast_jobs[job_id]["result"] = result
209
+ blast_jobs[job_id]["message"] = f"Completed: {len(hits)} hits found"
210
+ blast_jobs[job_id]["completed_at"] = time.time()
211
+
212
+ print(f"✅ [Job {job_id}] BLAST completed: {len(hits)} hits")
213
+
214
+ except Exception as e:
215
+ with blast_lock:
216
+ blast_jobs[job_id]["status"] = "failed"
217
+ blast_jobs[job_id]["error"] = str(e)
218
+ blast_jobs[job_id]["message"] = f"Failed: {str(e)}"
219
+
220
+ print(f"❌ [Job {job_id}] BLAST failed: {e}")
221
+
222
+
223
+ @mcp.tool(name="blast_search_async", description="Start BLAST search asynchronously (returns job_id immediately)")
224
+ def blast_search_async(payload: dict):
225
+ """
226
+ 异步启动 BLAST 搜索,立即返回 job_id
227
+ Required fields: sequence
228
+ Optional fields: database (refseq_rna/nt), program (blastn), hitlist_size (20), expect (1e-10)
229
+ """
230
+ try:
231
+ sequence = str(payload.get("sequence", "")).strip()
232
+ if not sequence:
233
+ return {"success": False, "result": None, "error": "sequence is required"}
234
+
235
+ sequence = "".join(sequence.split()).upper()
236
+
237
+ database = payload.get("database", "refseq_rna")
238
+ program = payload.get("program", "blastn")
239
+ hitlist_size = int(payload.get("hitlist_size", 20))
240
+ expect = float(payload.get("expect", 1e-10))
241
+
242
+ # 生成唯一 job_id
243
+ job_id = f"blast_{int(time.time() * 1000)}"
244
+
245
+ # 初始化 job 状态
246
+ with blast_lock:
247
+ blast_jobs[job_id] = {
248
+ "status": "pending",
249
+ "created_at": time.time(),
250
+ "sequence_length": len(sequence),
251
+ "database": database,
252
+ "message": "Job created, starting soon..."
253
+ }
254
+
255
+ # 在后台线程中运行 BLAST
256
+ thread = threading.Thread(
257
+ target=_run_blast_async,
258
+ args=(job_id, sequence, database, program, hitlist_size, expect),
259
+ daemon=True
260
+ )
261
+ thread.start()
262
+
263
+ print(f"🚀 BLAST job {job_id} started asynchronously")
264
+
265
+ return {
266
+ "success": True,
267
+ "result": {
268
+ "job_id": job_id,
269
+ "status": "pending",
270
+ "message": "BLAST search started. Use blast_check_status to check progress.",
271
+ "estimated_time": "1-15 minutes depending on database"
272
+ },
273
+ "error": None
274
+ }
275
+
276
+ except Exception as e:
277
+ return {"success": False, "result": None, "error": str(e)}
278
+
279
+
280
+ @mcp.tool(name="blast_check_status", description="Check status of async BLAST job")
281
+ def blast_check_status(payload: dict):
282
+ """
283
+ 检查异步 BLAST 任务的状态
284
+ Required fields: job_id
285
+ """
286
+ try:
287
+ job_id = payload.get("job_id", "")
288
+ if not job_id:
289
+ return {"success": False, "result": None, "error": "job_id is required"}
290
+
291
+ with blast_lock:
292
+ if job_id not in blast_jobs:
293
+ return {
294
+ "success": False,
295
+ "result": None,
296
+ "error": f"Job {job_id} not found"
297
+ }
298
+
299
+ job = blast_jobs[job_id].copy()
300
+
301
+ # 计算运行时间
302
+ elapsed = time.time() - job["created_at"]
303
+ job["elapsed_seconds"] = round(elapsed, 1)
304
+
305
+ return {
306
+ "success": True,
307
+ "result": job,
308
+ "error": None
309
+ }
310
+
311
+ except Exception as e:
312
+ return {"success": False, "result": None, "error": str(e)}
313
+
314
+
315
+ @mcp.tool(name="blast_get_result", description="Get result of completed BLAST job")
316
+ def blast_get_result(payload: dict):
317
+ """
318
+ 获取已完成的 BLAST 任务结果
319
+ Required fields: job_id
320
+ """
321
+ try:
322
+ job_id = payload.get("job_id", "")
323
+ if not job_id:
324
+ return {"success": False, "result": None, "error": "job_id is required"}
325
+
326
+ with blast_lock:
327
+ if job_id not in blast_jobs:
328
+ return {
329
+ "success": False,
330
+ "result": None,
331
+ "error": f"Job {job_id} not found"
332
+ }
333
+
334
+ job = blast_jobs[job_id]
335
+
336
+ if job["status"] != "completed":
337
+ return {
338
+ "success": False,
339
+ "result": None,
340
+ "error": f"Job {job_id} is not completed yet. Status: {job['status']}"
341
+ }
342
+
343
+ result = job.get("result")
344
+
345
+ return {
346
+ "success": True,
347
+ "result": result,
348
+ "error": None
349
+ }
350
+
351
+ except Exception as e:
352
+ return {"success": False, "result": None, "error": str(e)}
353
+
354
+
355
+ # ==============================================
356
+ # BLAST 搜索工具 - BLAST Search Tools (同步版本,保留用于小序列)
357
  # ==============================================
358
 
359
  @mcp.tool(name="blast_search", description="Search NCBI database using BLAST to find similar sequences")
 
679
  # 完整流程工具 - Complete Workflow Tool
680
  # ==============================================
681
 
682
+ @mcp.tool(name="identify_gene_species_async",
683
+ description="Complete workflow with async BLAST - returns job_id immediately, check status separately")
684
+ def identify_gene_species_async(payload: dict):
685
+ """
686
+ 异步版本的完整基因物种鉴定流程
687
+ Required fields: sequence
688
+ Optional fields: min_identity (70), max_hits (20), database (refseq_rna)
689
+
690
+ Returns job_id immediately. Use these tools to track progress:
691
+ 1. blast_check_status(job_id) - Check if BLAST is done
692
+ 2. continue_workflow_after_blast(job_id) - Complete remaining steps after BLAST finishes
693
+ """
694
+ try:
695
+ sequence = payload.get("sequence", "")
696
+ min_identity = float(payload.get("min_identity", 70.0))
697
+ max_hits = int(payload.get("max_hits", 20))
698
+ database = payload.get("database", "refseq_rna")
699
+
700
+ # Step 1: Validate sequence
701
+ print("📋 Step 1: Validating sequence...")
702
+ val_result = validate_sequence({"sequence": sequence})
703
+
704
+ if not val_result["success"]:
705
+ return {
706
+ "success": False,
707
+ "result": None,
708
+ "error": "Sequence validation failed"
709
+ }
710
+
711
+ validated_seq = val_result["result"]["sequence"]
712
+
713
+ # Step 2: Start async BLAST
714
+ print("🚀 Step 2: Starting async BLAST search...")
715
+ blast_async_result = blast_search_async({
716
+ "sequence": validated_seq,
717
+ "database": database,
718
+ "hitlist_size": max_hits
719
+ })
720
+
721
+ if not blast_async_result["success"]:
722
+ return blast_async_result
723
+
724
+ job_id = blast_async_result["result"]["job_id"]
725
+
726
+ # Store workflow parameters for later continuation
727
+ with blast_lock:
728
+ if job_id in blast_jobs:
729
+ blast_jobs[job_id]["workflow_params"] = {
730
+ "min_identity": min_identity,
731
+ "max_hits": max_hits,
732
+ "sequence_length": len(validated_seq)
733
+ }
734
+
735
+ return {
736
+ "success": True,
737
+ "result": {
738
+ "job_id": job_id,
739
+ "status": "blast_running",
740
+ "message": "BLAST search started. Use 'blast_check_status' to monitor progress, then 'continue_workflow_after_blast' to complete analysis.",
741
+ "next_steps": [
742
+ f"1. Check status: blast_check_status({{\"job_id\": \"{job_id}\"}})",
743
+ f"2. When completed: continue_workflow_after_blast({{\"job_id\": \"{job_id}\"}})"
744
+ ]
745
+ },
746
+ "error": None
747
+ }
748
+
749
+ except Exception as e:
750
+ return {"success": False, "result": None, "error": str(e)}
751
+
752
+
753
+ @mcp.tool(name="continue_workflow_after_blast",
754
+ description="Continue species identification workflow after BLAST completes")
755
+ def continue_workflow_after_blast(payload: dict):
756
+ """
757
+ 在 BLAST 完成后继续工作流程
758
+ Required fields: job_id
759
+ """
760
+ try:
761
+ job_id = payload.get("job_id", "")
762
+ if not job_id:
763
+ return {"success": False, "result": None, "error": "job_id is required"}
764
+
765
+ # Get BLAST result
766
+ blast_result_response = blast_get_result({"job_id": job_id})
767
+ if not blast_result_response["success"]:
768
+ return blast_result_response
769
+
770
+ blast_result = blast_result_response["result"]
771
+ hits = blast_result["hits"]
772
+
773
+ # Get workflow parameters
774
+ with blast_lock:
775
+ workflow_params = blast_jobs[job_id].get("workflow_params", {})
776
+
777
+ min_identity = workflow_params.get("min_identity", 70.0)
778
+
779
+ workflow_results = {"steps": []}
780
+
781
+ # Step 3: Filter hits
782
+ print("🔬 Step 3: Filtering BLAST hits...")
783
+ filter_result = filter_blast_hits({
784
+ "hits": hits,
785
+ "min_identity": min_identity,
786
+ "max_hits": 10
787
+ })
788
+ workflow_results["steps"].append({
789
+ "step": 3,
790
+ "name": "filter_blast_hits",
791
+ "result": filter_result
792
+ })
793
+
794
+ filtered_hits = filter_result["result"]["hits"]
795
+
796
+ # Step 4: Extract species
797
+ print("🌍 Step 4: Extracting species information...")
798
+ species_result = extract_species_from_blast({"hits": filtered_hits})
799
+ workflow_results["steps"].append({
800
+ "step": 4,
801
+ "name": "extract_species_from_blast",
802
+ "result": species_result
803
+ })
804
+
805
+ species_data = species_result["result"]["species_data"]
806
+
807
+ # Step 5: Aggregate scores
808
+ print("📊 Step 5: Aggregating species scores...")
809
+ agg_result = aggregate_species_scores({"species_data": species_data})
810
+ workflow_results["steps"].append({
811
+ "step": 5,
812
+ "name": "aggregate_species_scores",
813
+ "result": agg_result
814
+ })
815
+
816
+ species_rankings = agg_result["result"]["species_rankings"]
817
+
818
+ # Step 6: Predict species
819
+ print("🎯 Step 6: Predicting species...")
820
+ pred_result = predict_species({"species_rankings": species_rankings})
821
+ workflow_results["steps"].append({
822
+ "step": 6,
823
+ "name": "predict_species",
824
+ "result": pred_result
825
+ })
826
+
827
+ # Compile final result
828
+ final_result = {
829
+ "job_id": job_id,
830
+ "input_sequence_length": workflow_params.get("sequence_length"),
831
+ "total_blast_hits": len(hits),
832
+ "filtered_hits": len(filtered_hits),
833
+ "unique_species_found": len(species_rankings),
834
+ "prediction": pred_result["result"] if pred_result["success"] else None,
835
+ "workflow": workflow_results
836
+ }
837
+
838
+ print("✅ Complete workflow finished successfully!")
839
+
840
+ return {
841
+ "success": True,
842
+ "result": final_result,
843
+ "error": None
844
+ }
845
+
846
+ except Exception as e:
847
+ return {"success": False, "result": None, "error": str(e)}
848
+
849
+
850
  @mcp.tool(name="identify_gene_species_complete",
851
  description="Complete workflow: input unknown gene sequence, identify species through BLAST search and analysis")
852
  def identify_gene_species_complete(payload: dict):