guohanghui commited on
Commit
b665acb
·
verified ·
1 Parent(s): 66e65a8

Update biotite/mcp_output/mcp_plugin/mcp_service.py

Browse files
biotite/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,911 +1,181 @@
1
- import os
2
- import sys
3
- from typing import List, Optional, Tuple
4
-
5
- # Add the local source directory to sys.path
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
- import numpy as np
12
 
13
- # Create the FastMCP service application
14
  mcp = FastMCP("biotite_service")
15
 
16
-
17
- # =============================================================================
18
- # Sequence Tools
19
- # =============================================================================
20
-
21
- @mcp.tool(name="align_sequences", description="Align biological sequences using Biotite.")
22
- def align_sequences(seq1: str, seq2: str) -> dict:
23
- """
24
- Align two biological sequences using optimal alignment algorithm.
25
-
26
- Parameters:
27
- - seq1: The first sequence to align (DNA, RNA, or protein).
28
- - seq2: The second sequence to align (DNA, RNA, or protein).
29
-
30
- Returns:
31
- A dictionary containing the alignment result with score and aligned sequences.
32
- """
33
- try:
34
- from biotite.sequence import NucleotideSequence, ProteinSequence
35
- from biotite.sequence.align import align_optimal, SubstitutionMatrix
36
-
37
- # Try to detect sequence type and create appropriate sequence objects
38
- seq1_upper = seq1.upper()
39
- seq2_upper = seq2.upper()
40
-
41
- # Check if sequences are nucleotide or protein
42
- nucleotide_chars = set("ACGTURYSWKMBDHVN")
43
- is_nucleotide = all(c in nucleotide_chars for c in seq1_upper if c not in "-. ")
44
-
45
- if is_nucleotide:
46
- sequence1 = NucleotideSequence(seq1_upper.replace("U", "T"))
47
- sequence2 = NucleotideSequence(seq2_upper.replace("U", "T"))
48
- matrix = SubstitutionMatrix.std_nucleotide_matrix()
49
- else:
50
- sequence1 = ProteinSequence(seq1_upper)
51
- sequence2 = ProteinSequence(seq2_upper)
52
- matrix = SubstitutionMatrix.std_protein_matrix()
53
-
54
- # Perform alignment
55
- alignments = align_optimal(sequence1, sequence2, matrix)
56
- alignment = alignments[0]
57
-
58
- # Format output
59
- aligned_seq1, aligned_seq2 = alignment.get_gapped_sequences()
60
-
61
- return {
62
- "success": True,
63
- "result": {
64
- "aligned_seq1": str(aligned_seq1),
65
- "aligned_seq2": str(aligned_seq2),
66
- "score": int(alignment.score),
67
- "alignment_length": len(alignment.trace),
68
- "sequence_type": "nucleotide" if is_nucleotide else "protein"
69
- },
70
- "error": None
71
- }
72
- except Exception as e:
73
- return {"success": False, "result": None, "error": str(e)}
74
-
75
-
76
- @mcp.tool(name="get_sequence_identity", description="Calculate sequence identity between two aligned sequences.")
77
- def get_sequence_identity(seq1: str, seq2: str) -> dict:
78
- """
79
- Calculate the sequence identity between two sequences after alignment.
80
-
81
- Parameters:
82
- - seq1: The first sequence.
83
- - seq2: The second sequence.
84
-
85
- Returns:
86
- A dictionary containing identity percentage and match statistics.
87
- """
88
- try:
89
- from biotite.sequence import NucleotideSequence, ProteinSequence
90
- from biotite.sequence.align import align_optimal, SubstitutionMatrix, get_sequence_identity as calc_identity
91
-
92
- seq1_upper = seq1.upper()
93
- seq2_upper = seq2.upper()
94
-
95
- nucleotide_chars = set("ACGTURYSWKMBDHVN")
96
- is_nucleotide = all(c in nucleotide_chars for c in seq1_upper if c not in "-. ")
97
-
98
- if is_nucleotide:
99
- sequence1 = NucleotideSequence(seq1_upper.replace("U", "T"))
100
- sequence2 = NucleotideSequence(seq2_upper.replace("U", "T"))
101
- matrix = SubstitutionMatrix.std_nucleotide_matrix()
102
- else:
103
- sequence1 = ProteinSequence(seq1_upper)
104
- sequence2 = ProteinSequence(seq2_upper)
105
- matrix = SubstitutionMatrix.std_protein_matrix()
106
-
107
- alignments = align_optimal(sequence1, sequence2, matrix)
108
- alignment = alignments[0]
109
- identity = calc_identity(alignment)
110
-
111
- return {
112
- "success": True,
113
- "result": {
114
- "identity": float(identity),
115
- "identity_percent": float(identity * 100),
116
- "alignment_score": int(alignment.score)
117
- },
118
- "error": None
119
- }
120
- except Exception as e:
121
- return {"success": False, "result": None, "error": str(e)}
122
-
123
-
124
- @mcp.tool(name="create_nucleotide_sequence", description="Create a nucleotide sequence object and get its properties.")
125
- def create_nucleotide_sequence(sequence: str) -> dict:
126
- """
127
- Create a nucleotide sequence and return its properties.
128
-
129
- Parameters:
130
- - sequence: DNA or RNA sequence string (e.g., "ATGCGATCGA").
131
-
132
- Returns:
133
- A dictionary containing sequence length, GC content, and complement.
134
- """
135
- try:
136
- from biotite.sequence import NucleotideSequence
137
-
138
- # Convert U to T for DNA compatibility
139
- seq_upper = sequence.upper().replace("U", "T")
140
- nuc_seq = NucleotideSequence(seq_upper)
141
-
142
- # Calculate GC content
143
- gc_count = str(nuc_seq).count("G") + str(nuc_seq).count("C")
144
- gc_content = gc_count / len(nuc_seq) if len(nuc_seq) > 0 else 0
145
-
146
- # Get complement
147
- complement = nuc_seq.complement()
148
-
149
- return {
150
- "success": True,
151
- "result": {
152
- "sequence": str(nuc_seq),
153
- "length": len(nuc_seq),
154
- "gc_content": float(gc_content),
155
- "gc_percent": float(gc_content * 100),
156
- "complement": str(complement),
157
- "reverse_complement": str(complement[::-1])
158
- },
159
- "error": None
160
- }
161
- except Exception as e:
162
- return {"success": False, "result": None, "error": str(e)}
163
-
164
-
165
- @mcp.tool(name="create_protein_sequence", description="Create a protein sequence object and get its properties.")
166
- def create_protein_sequence(sequence: str) -> dict:
167
- """
168
- Create a protein sequence and return its properties.
169
-
170
- Parameters:
171
- - sequence: Amino acid sequence string (e.g., "MKVLWAALLV").
172
-
173
- Returns:
174
- A dictionary containing sequence length and amino acid composition.
175
- """
176
- try:
177
- from biotite.sequence import ProteinSequence
178
-
179
- prot_seq = ProteinSequence(sequence.upper())
180
-
181
- # Calculate amino acid composition
182
- aa_composition = {}
183
- seq_str = str(prot_seq)
184
- for aa in set(seq_str):
185
- aa_composition[aa] = seq_str.count(aa)
186
-
187
- return {
188
- "success": True,
189
- "result": {
190
- "sequence": str(prot_seq),
191
- "length": len(prot_seq),
192
- "amino_acid_composition": aa_composition,
193
- "unique_amino_acids": len(aa_composition)
194
- },
195
- "error": None
196
- }
197
- except Exception as e:
198
- return {"success": False, "result": None, "error": str(e)}
199
-
200
-
201
- @mcp.tool(name="translate_dna", description="Translate a DNA sequence into a protein sequence.")
202
- def translate_dna(dna_sequence: str, codon_table: str = "Standard") -> dict:
203
- """
204
- Translate a DNA sequence to protein using specified codon table.
205
-
206
- Parameters:
207
- - dna_sequence: The DNA sequence to translate.
208
- - codon_table: The codon table to use (default: "Standard").
209
-
210
- Returns:
211
- A dictionary containing the translated protein sequence.
212
- """
213
- try:
214
- from biotite.sequence import NucleotideSequence, ProteinSequence
215
- from biotite.sequence.codon import CodonTable
216
-
217
- dna_seq = NucleotideSequence(dna_sequence.upper().replace("U", "T"))
218
- table = CodonTable.load(codon_table)
219
-
220
- # Translate
221
- protein_seq, _ = table.translate(dna_seq)
222
-
223
- return {
224
- "success": True,
225
- "result": {
226
- "dna_sequence": str(dna_seq),
227
- "protein_sequence": str(protein_seq),
228
- "dna_length": len(dna_seq),
229
- "protein_length": len(protein_seq),
230
- "codon_table": codon_table
231
- },
232
- "error": None
233
- }
234
- except Exception as e:
235
- return {"success": False, "result": None, "error": str(e)}
236
-
237
-
238
- # =============================================================================
239
- # Structure Tools
240
- # =============================================================================
241
-
242
- @mcp.tool(name="analyze_atoms", description="Analyze atoms in a molecular structure.")
243
- def analyze_atoms(structure_file: str) -> dict:
244
- """
245
- Load and analyze atoms in a given molecular structure file.
246
-
247
- Parameters:
248
- - structure_file: Path to the molecular structure file (PDB, mmCIF, etc.).
249
-
250
- Returns:
251
- A dictionary containing structural information including atom count,
252
- chains, residues, and element composition.
253
- """
254
- try:
255
- from biotite.structure.io import load_structure
256
- from biotite.structure import get_chains, get_residues
257
-
258
- atoms = load_structure(structure_file)
259
-
260
- # Get basic statistics
261
- chains = get_chains(atoms)
262
- residues = get_residues(atoms)
263
-
264
- # Element composition
265
- element_counts = {}
266
- for elem in atoms.element:
267
- element_counts[elem] = element_counts.get(elem, 0) + 1
268
-
269
- return {
270
- "success": True,
271
- "result": {
272
- "atom_count": atoms.array_length(),
273
- "chain_ids": list(set(chains[1])),
274
- "chain_count": len(set(chains[1])),
275
- "residue_count": len(residues[0]),
276
- "element_composition": element_counts,
277
- "has_bonds": atoms.bonds is not None
278
- },
279
- "error": None
280
- }
281
- except Exception as e:
282
- return {"success": False, "result": None, "error": str(e)}
283
-
284
-
285
- @mcp.tool(name="calculate_rmsd", description="Calculate RMSD between two structures.")
286
- def calculate_rmsd(structure_file1: str, structure_file2: str) -> dict:
287
- """
288
- Calculate Root Mean Square Deviation between two structures.
289
-
290
- Parameters:
291
- - structure_file1: Path to the first structure file.
292
- - structure_file2: Path to the second structure file.
293
-
294
- Returns:
295
- A dictionary containing RMSD value in Angstroms.
296
- """
297
- try:
298
- from biotite.structure.io import load_structure
299
- from biotite.structure import superimpose, rmsd
300
-
301
- atoms1 = load_structure(structure_file1)
302
- atoms2 = load_structure(structure_file2)
303
-
304
- # Superimpose structures
305
- atoms2_superimposed, transformation = superimpose(atoms1, atoms2)
306
-
307
- # Calculate RMSD
308
- rmsd_value = rmsd(atoms1, atoms2_superimposed)
309
-
310
- return {
311
- "success": True,
312
- "result": {
313
- "rmsd": float(rmsd_value),
314
- "rmsd_unit": "Angstrom",
315
- "atom_count": atoms1.array_length()
316
- },
317
- "error": None
318
- }
319
- except Exception as e:
320
- return {"success": False, "result": None, "error": str(e)}
321
-
322
-
323
- @mcp.tool(name="superimpose_structures", description="Superimpose one structure onto another.")
324
- def superimpose_structures(fixed_file: str, mobile_file: str, output_file: str) -> dict:
325
- """
326
- Superimpose one structure onto another and save the result.
327
-
328
- Parameters:
329
- - fixed_file: Path to the fixed (reference) structure file.
330
- - mobile_file: Path to the mobile structure file.
331
- - output_file: Path for the output superimposed structure.
332
-
333
- Returns:
334
- A dictionary containing RMSD before and after superimposition.
335
- """
336
- try:
337
- from biotite.structure.io import load_structure, save_structure
338
- from biotite.structure import superimpose, rmsd
339
-
340
- fixed = load_structure(fixed_file)
341
- mobile = load_structure(mobile_file)
342
-
343
- # Calculate RMSD before superimposition
344
- rmsd_before = rmsd(fixed, mobile)
345
-
346
- # Superimpose
347
- mobile_superimposed, transformation = superimpose(fixed, mobile)
348
-
349
- # Calculate RMSD after superimposition
350
- rmsd_after = rmsd(fixed, mobile_superimposed)
351
-
352
- # Save result
353
- save_structure(output_file, mobile_superimposed)
354
-
355
- return {
356
- "success": True,
357
- "result": {
358
- "rmsd_before": float(rmsd_before),
359
- "rmsd_after": float(rmsd_after),
360
- "improvement": float(rmsd_before - rmsd_after),
361
- "output_file": output_file
362
- },
363
- "error": None
364
- }
365
- except Exception as e:
366
- return {"success": False, "result": None, "error": str(e)}
367
-
368
-
369
- @mcp.tool(name="calculate_distance", description="Calculate distance between two atoms by index.")
370
- def calculate_distance(structure_file: str, atom_index1: int, atom_index2: int) -> dict:
371
  """
372
- Calculate the distance between two atoms in a structure.
373
-
374
- Parameters:
375
- - structure_file: Path to the structure file.
376
- - atom_index1: Index of the first atom (0-based).
377
- - atom_index2: Index of the second atom (0-based).
378
-
379
- Returns:
380
- A dictionary containing the distance in Angstroms.
381
- """
382
- try:
383
- from biotite.structure.io import load_structure
384
- from biotite.structure import distance
385
-
386
- atoms = load_structure(structure_file)
387
-
388
- # Get atom information
389
- atom1_info = {
390
- "index": atom_index1,
391
- "element": atoms.element[atom_index1],
392
- "atom_name": atoms.atom_name[atom_index1],
393
- "res_name": atoms.res_name[atom_index1],
394
- "res_id": int(atoms.res_id[atom_index1])
395
- }
396
- atom2_info = {
397
- "index": atom_index2,
398
- "element": atoms.element[atom_index2],
399
- "atom_name": atoms.atom_name[atom_index2],
400
- "res_name": atoms.res_name[atom_index2],
401
- "res_id": int(atoms.res_id[atom_index2])
402
- }
403
-
404
- # Calculate distance
405
- dist = distance(atoms[atom_index1], atoms[atom_index2])
406
-
407
- return {
408
- "success": True,
409
- "result": {
410
- "distance": float(dist),
411
- "distance_unit": "Angstrom",
412
- "atom1": atom1_info,
413
- "atom2": atom2_info
414
- },
415
- "error": None
416
- }
417
- except Exception as e:
418
- return {"success": False, "result": None, "error": str(e)}
419
 
420
-
421
- @mcp.tool(name="calculate_angle", description="Calculate angle between three atoms.")
422
- def calculate_angle(structure_file: str, atom_index1: int, atom_index2: int, atom_index3: int) -> dict:
423
- """
424
- Calculate the angle formed by three atoms.
425
-
426
- Parameters:
427
- - structure_file: Path to the structure file.
428
- - atom_index1: Index of the first atom (0-based).
429
- - atom_index2: Index of the central atom (0-based).
430
- - atom_index3: Index of the third atom (0-based).
431
-
432
- Returns:
433
- A dictionary containing the angle in degrees and radians.
434
  """
435
  try:
436
- from biotite.structure.io import load_structure
437
- from biotite.structure import angle
438
-
439
- atoms = load_structure(structure_file)
440
-
441
- # Calculate angle
442
- ang = angle(atoms[atom_index1], atoms[atom_index2], atoms[atom_index3])
443
-
444
  return {
445
  "success": True,
446
- "result": {
447
- "angle_radians": float(ang),
448
- "angle_degrees": float(np.degrees(ang)),
449
- "atom_indices": [atom_index1, atom_index2, atom_index3]
450
- },
451
- "error": None
452
  }
453
  except Exception as e:
454
- return {"success": False, "result": None, "error": str(e)}
455
 
456
-
457
- @mcp.tool(name="calculate_dihedral", description="Calculate dihedral angle between four atoms.")
458
- def calculate_dihedral(structure_file: str, atom_index1: int, atom_index2: int,
459
- atom_index3: int, atom_index4: int) -> dict:
460
- """
461
- Calculate the dihedral angle formed by four atoms.
462
-
463
- Parameters:
464
- - structure_file: Path to the structure file.
465
- - atom_index1-4: Indices of the four atoms (0-based).
466
-
467
- Returns:
468
- A dictionary containing the dihedral angle in degrees and radians.
469
  """
470
- try:
471
- from biotite.structure.io import load_structure
472
- from biotite.structure import dihedral
473
-
474
- atoms = load_structure(structure_file)
475
-
476
- # Calculate dihedral
477
- dih = dihedral(atoms[atom_index1], atoms[atom_index2],
478
- atoms[atom_index3], atoms[atom_index4])
479
-
480
- return {
481
- "success": True,
482
- "result": {
483
- "dihedral_radians": float(dih),
484
- "dihedral_degrees": float(np.degrees(dih)),
485
- "atom_indices": [atom_index1, atom_index2, atom_index3, atom_index4]
486
- },
487
- "error": None
488
- }
489
- except Exception as e:
490
- return {"success": False, "result": None, "error": str(e)}
491
 
 
 
492
 
493
- @mcp.tool(name="calculate_centroid", description="Calculate the centroid of a structure.")
494
- def calculate_centroid(structure_file: str) -> dict:
495
- """
496
- Calculate the geometric centroid of a structure.
497
-
498
- Parameters:
499
- - structure_file: Path to the structure file.
500
-
501
- Returns:
502
- A dictionary containing the centroid coordinates (x, y, z).
503
  """
504
  try:
505
- from biotite.structure.io import load_structure
506
- from biotite.structure import centroid
507
-
508
- atoms = load_structure(structure_file)
509
- center = centroid(atoms)
510
-
511
- return {
512
- "success": True,
513
- "result": {
514
- "centroid": {
515
- "x": float(center[0]),
516
- "y": float(center[1]),
517
- "z": float(center[2])
518
- },
519
- "unit": "Angstrom"
520
- },
521
- "error": None
522
  }
523
- except Exception as e:
524
- return {"success": False, "result": None, "error": str(e)}
525
-
526
-
527
- @mcp.tool(name="calculate_sasa", description="Calculate Solvent Accessible Surface Area.")
528
- def calculate_sasa(structure_file: str, probe_radius: float = 1.4) -> dict:
529
- """
530
- Calculate the Solvent Accessible Surface Area (SASA) of a structure.
531
-
532
- Parameters:
533
- - structure_file: Path to the structure file.
534
- - probe_radius: Radius of the solvent probe in Angstroms (default: 1.4).
535
-
536
- Returns:
537
- A dictionary containing total SASA and per-atom SASA values.
538
- """
539
- try:
540
- from biotite.structure.io import load_structure
541
- from biotite.structure import sasa
542
-
543
- atoms = load_structure(structure_file)
544
- atom_sasa = sasa(atoms, probe_radius=probe_radius)
545
-
546
- # Filter out NaN values
547
- valid_sasa = atom_sasa[~np.isnan(atom_sasa)]
548
- total_sasa = np.sum(valid_sasa)
549
-
550
  return {
551
  "success": True,
552
- "result": {
553
- "total_sasa": float(total_sasa),
554
- "sasa_unit": "Angstrom^2",
555
- "probe_radius": probe_radius,
556
- "atoms_calculated": len(valid_sasa),
557
- "mean_atom_sasa": float(np.mean(valid_sasa)) if len(valid_sasa) > 0 else 0
558
- },
559
- "error": None
560
  }
561
  except Exception as e:
562
- return {"success": False, "result": None, "error": str(e)}
563
 
564
-
565
- @mcp.tool(name="find_hydrogen_bonds", description="Find hydrogen bonds in a structure.")
566
- def find_hydrogen_bonds(structure_file: str, cutoff_dist: float = 2.5,
567
- cutoff_angle: float = 120.0) -> dict:
568
- """
569
- Find hydrogen bonds in a molecular structure.
570
-
571
- Parameters:
572
- - structure_file: Path to the structure file.
573
- - cutoff_dist: Maximum H-A distance in Angstroms (default: 2.5).
574
- - cutoff_angle: Minimum D-H-A angle in degrees (default: 120).
575
-
576
- Returns:
577
- A dictionary containing hydrogen bond triplets (Donor, H, Acceptor).
578
  """
579
- try:
580
- from biotite.structure.io import load_structure
581
- from biotite.structure import hbond
582
-
583
- atoms = load_structure(structure_file)
584
-
585
- triplets = hbond(atoms, cutoff_dist=cutoff_dist, cutoff_angle=cutoff_angle)
586
-
587
- # Format results
588
- hbond_list = []
589
- for i in range(len(triplets)):
590
- d_idx, h_idx, a_idx = triplets[i]
591
- hbond_list.append({
592
- "donor_index": int(d_idx),
593
- "hydrogen_index": int(h_idx),
594
- "acceptor_index": int(a_idx),
595
- "donor_element": atoms.element[d_idx],
596
- "acceptor_element": atoms.element[a_idx]
597
- })
598
-
599
- return {
600
- "success": True,
601
- "result": {
602
- "hydrogen_bond_count": len(hbond_list),
603
- "hydrogen_bonds": hbond_list[:50], # Limit output
604
- "cutoff_distance": cutoff_dist,
605
- "cutoff_angle": cutoff_angle
606
- },
607
- "error": None
608
- }
609
- except Exception as e:
610
- return {"success": False, "result": None, "error": str(e)}
611
 
 
 
612
 
613
- @mcp.tool(name="annotate_secondary_structure", description="Annotate secondary structure elements.")
614
- def annotate_secondary_structure(structure_file: str) -> dict:
615
- """
616
- Annotate secondary structure elements (alpha helix, beta strand, coil) in a protein.
617
-
618
- Parameters:
619
- - structure_file: Path to the structure file.
620
-
621
- Returns:
622
- A dictionary containing SSE annotations for each residue.
623
  """
624
  try:
625
- from biotite.structure.io import load_structure
626
- from biotite.structure import annotate_sse, get_residues
627
-
628
- atoms = load_structure(structure_file)
629
- sse = annotate_sse(atoms)
630
-
631
- # Count SSE types
632
- sse_counts = {
633
- "alpha_helix": int(np.sum(sse == 'a')),
634
- "beta_strand": int(np.sum(sse == 'b')),
635
- "coil": int(np.sum(sse == 'c')),
636
- "other": int(np.sum(sse == ''))
637
- }
638
-
639
- # Get residue info
640
- residues = get_residues(atoms)
641
-
642
  return {
643
  "success": True,
644
- "result": {
645
- "sse_sequence": ''.join(sse),
646
- "sse_counts": sse_counts,
647
- "residue_count": len(sse),
648
- "helix_percentage": float(sse_counts["alpha_helix"] / len(sse) * 100) if len(sse) > 0 else 0,
649
- "strand_percentage": float(sse_counts["beta_strand"] / len(sse) * 100) if len(sse) > 0 else 0
650
- },
651
- "error": None
652
  }
653
  except Exception as e:
654
- return {"success": False, "result": None, "error": str(e)}
655
 
656
-
657
- @mcp.tool(name="filter_amino_acids", description="Filter and extract amino acid residues from a structure.")
658
- def filter_amino_acids(structure_file: str) -> dict:
659
- """
660
- Filter a structure to keep only amino acid residues.
661
-
662
- Parameters:
663
- - structure_file: Path to the structure file.
664
-
665
- Returns:
666
- A dictionary containing information about filtered amino acids.
667
  """
668
- try:
669
- from biotite.structure.io import load_structure
670
- from biotite.structure import filter_amino_acids as filter_aa, get_residues
671
-
672
- atoms = load_structure(structure_file)
673
- aa_mask = filter_aa(atoms)
674
- aa_atoms = atoms[aa_mask]
675
-
676
- # Get unique residue names
677
- unique_res = set(aa_atoms.res_name)
678
-
679
- return {
680
- "success": True,
681
- "result": {
682
- "original_atom_count": atoms.array_length(),
683
- "amino_acid_atom_count": aa_atoms.array_length(),
684
- "unique_residues": list(unique_res),
685
- "residue_types_count": len(unique_res)
686
- },
687
- "error": None
688
- }
689
- except Exception as e:
690
- return {"success": False, "result": None, "error": str(e)}
691
 
 
 
 
692
 
693
- @mcp.tool(name="transform_rotate", description="Rotate a structure around an axis.")
694
- def transform_rotate(structure_file: str, axis: List[float], angle_degrees: float,
695
- output_file: str) -> dict:
696
- """
697
- Rotate a structure around a specified axis.
698
-
699
- Parameters:
700
- - structure_file: Path to the input structure file.
701
- - axis: Rotation axis as [x, y, z] vector.
702
- - angle_degrees: Rotation angle in degrees.
703
- - output_file: Path for the output rotated structure.
704
-
705
- Returns:
706
- A dictionary confirming the rotation operation.
707
  """
708
  try:
709
- from biotite.structure.io import load_structure, save_structure
710
- from biotite.structure import rotate
711
-
712
- atoms = load_structure(structure_file)
713
-
714
- # Rotate
715
- rotated = rotate(atoms, axis, np.radians(angle_degrees))
716
-
717
- # Save
718
- save_structure(output_file, rotated)
719
 
 
 
 
720
  return {
721
  "success": True,
722
- "result": {
723
- "rotation_axis": axis,
724
- "rotation_angle_degrees": angle_degrees,
725
- "output_file": output_file
726
- },
727
- "error": None
728
  }
729
  except Exception as e:
730
- return {"success": False, "result": None, "error": str(e)}
731
-
732
 
733
- @mcp.tool(name="transform_translate", description="Translate a structure by a vector.")
734
- def transform_translate(structure_file: str, translation: List[float],
735
- output_file: str) -> dict:
736
  """
737
- Translate a structure by a specified vector.
738
-
739
- Parameters:
740
- - structure_file: Path to the input structure file.
741
- - translation: Translation vector as [x, y, z] in Angstroms.
742
- - output_file: Path for the output translated structure.
743
-
744
- Returns:
745
- A dictionary confirming the translation operation.
746
- """
747
- try:
748
- from biotite.structure.io import load_structure, save_structure
749
- from biotite.structure import translate
750
-
751
- atoms = load_structure(structure_file)
752
-
753
- # Translate
754
- translated = translate(atoms, translation)
755
-
756
- # Save
757
- save_structure(output_file, translated)
758
-
759
- return {
760
- "success": True,
761
- "result": {
762
- "translation_vector": translation,
763
- "output_file": output_file
764
- },
765
- "error": None
766
- }
767
- except Exception as e:
768
- return {"success": False, "result": None, "error": str(e)}
769
 
 
 
 
770
 
771
- @mcp.tool(name="get_backbone_angles", description="Calculate backbone dihedral angles (phi, psi, omega).")
772
- def get_backbone_angles(structure_file: str) -> dict:
773
- """
774
- Calculate backbone dihedral angles (phi, psi, omega) for protein residues.
775
-
776
- Parameters:
777
- - structure_file: Path to the structure file.
778
-
779
- Returns:
780
- A dictionary containing backbone dihedral angles.
781
- """
782
- try:
783
- from biotite.structure.io import load_structure
784
- from biotite.structure import dihedral_backbone
785
-
786
- atoms = load_structure(structure_file)
787
- phi, psi, omega = dihedral_backbone(atoms)
788
-
789
- # Convert to degrees and handle NaN
790
- phi_deg = np.degrees(phi)
791
- psi_deg = np.degrees(psi)
792
- omega_deg = np.degrees(omega)
793
-
794
- # Calculate Ramachandran statistics
795
- valid_phi = phi_deg[~np.isnan(phi_deg)]
796
- valid_psi = psi_deg[~np.isnan(psi_deg)]
797
-
798
- return {
799
- "success": True,
800
- "result": {
801
- "residue_count": len(phi),
802
- "phi_mean": float(np.nanmean(phi_deg)) if len(valid_phi) > 0 else None,
803
- "psi_mean": float(np.nanmean(psi_deg)) if len(valid_psi) > 0 else None,
804
- "phi_range": [float(np.nanmin(phi_deg)), float(np.nanmax(phi_deg))] if len(valid_phi) > 0 else None,
805
- "psi_range": [float(np.nanmin(psi_deg)), float(np.nanmax(psi_deg))] if len(valid_psi) > 0 else None,
806
- "unit": "degrees"
807
- },
808
- "error": None
809
- }
810
- except Exception as e:
811
- return {"success": False, "result": None, "error": str(e)}
812
-
813
-
814
- @mcp.tool(name="extract_chain", description="Extract a specific chain from a structure.")
815
- def extract_chain(structure_file: str, chain_id: str, output_file: str) -> dict:
816
- """
817
- Extract a specific chain from a multi-chain structure.
818
-
819
- Parameters:
820
- - structure_file: Path to the input structure file.
821
- - chain_id: The chain ID to extract (e.g., "A", "B").
822
- - output_file: Path for the output structure file.
823
-
824
- Returns:
825
- A dictionary containing extracted chain information.
826
  """
827
  try:
828
- from biotite.structure.io import load_structure, save_structure
829
-
830
- atoms = load_structure(structure_file)
831
-
832
- # Filter by chain
833
- chain_mask = atoms.chain_id == chain_id
834
- chain_atoms = atoms[chain_mask]
835
-
836
- if chain_atoms.array_length() == 0:
 
837
  return {
838
  "success": False,
839
- "result": None,
840
- "error": f"Chain '{chain_id}' not found in structure"
841
  }
842
-
843
- # Save
844
- save_structure(output_file, chain_atoms)
845
-
846
  return {
847
  "success": True,
848
- "result": {
849
- "chain_id": chain_id,
850
- "extracted_atom_count": chain_atoms.array_length(),
851
- "original_atom_count": atoms.array_length(),
852
- "output_file": output_file
853
- },
854
- "error": None
855
  }
856
  except Exception as e:
857
- return {"success": False, "result": None, "error": str(e)}
858
-
859
 
860
- @mcp.tool(name="get_structure_sequence", description="Extract the amino acid sequence from a protein structure.")
861
- def get_structure_sequence(structure_file: str) -> dict:
862
  """
863
- Extract the amino acid sequence from a protein structure file.
864
-
865
- Parameters:
866
- - structure_file: Path to the structure file.
867
-
868
- Returns:
869
- A dictionary containing the extracted sequence(s) for each chain.
 
870
  """
871
  try:
872
- from biotite.structure.io import load_structure
873
- from biotite.structure import get_chains
874
- from biotite.structure.sequence import to_sequence
875
-
876
- atoms = load_structure(structure_file)
877
-
878
- # Get chains
879
- chain_starts, chain_ids = get_chains(atoms)
880
- unique_chains = list(set(chain_ids))
881
-
882
- sequences = {}
883
- for chain_id in unique_chains:
884
- chain_mask = atoms.chain_id == chain_id
885
- chain_atoms = atoms[chain_mask]
886
- try:
887
- seq = to_sequence(chain_atoms)
888
- sequences[chain_id] = str(seq)
889
- except:
890
- sequences[chain_id] = "Unable to extract sequence"
891
 
 
 
 
892
  return {
893
  "success": True,
894
- "result": {
895
- "chain_sequences": sequences,
896
- "chain_count": len(unique_chains)
897
- },
898
- "error": None
899
  }
900
  except Exception as e:
901
- return {"success": False, "result": None, "error": str(e)}
902
-
903
 
 
904
  def create_app() -> FastMCP:
905
  """
906
- Create and return the FastMCP application instance.
907
-
908
- Returns:
909
- The FastMCP instance for the service.
910
  """
911
  return mcp
 
 
 
 
 
 
 
 
 
 
1
  from fastmcp import FastMCP
 
2
 
3
+ # 创建 FastMCP 服务应用
4
  mcp = FastMCP("biotite_service")
5
 
6
+ @mcp.tool(name="list_available_modules", description="列出 Biotite 中的所有模块")
7
+ def list_available_modules() -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
+ 列出 Biotite 中的所有模块。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ 返回:
12
+ - dict: 包含成功状态和模块列表的字典。
 
 
 
 
 
 
 
 
 
 
 
 
13
  """
14
  try:
15
+ modules = [
16
+ "sequence",
17
+ "structure",
18
+ "application",
19
+ "database",
20
+ "interface",
21
+ ]
 
22
  return {
23
  "success": True,
24
+ "modules": modules,
25
+ "count": len(modules)
 
 
 
 
26
  }
27
  except Exception as e:
28
+ return {"success": False, "error": str(e)}
29
 
30
+ @mcp.tool(name="get_module_info", description="获取 Biotite 模块的详细信息")
31
+ def get_module_info(module_name: str) -> dict:
 
 
 
 
 
 
 
 
 
 
 
32
  """
33
+ 获取 Biotite 模块的详细信息。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ 参数:
36
+ - module_name: 模块名称(例如 'sequence')
37
 
38
+ 返回:
39
+ - dict: 包含模块信息的字典。
 
 
 
 
 
 
 
 
40
  """
41
  try:
42
+ module_info = {
43
+ "sequence": "提供序列操作和分析功能,包括对齐、注释和可视化。",
44
+ "structure": "提供分子结构的操作和分析功能,包括超位移和比较。",
45
+ "application": "提供与外部生物信息学工具的接口,例如 BLAST、DSSP 等。",
46
+ "database": "支持从生物数据库中搜索和获取数据。",
47
+ "interface": "提供与其他生物信息学库(如 RDKit)的接口。"
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
+ if module_name not in module_info:
50
+ return {
51
+ "success": False,
52
+ "error": f"模块 '{module_name}' 不存在。请使用 list_available_modules 查看可用模块。"
53
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return {
55
  "success": True,
56
+ "module_name": module_name,
57
+ "description": module_info[module_name]
 
 
 
 
 
 
58
  }
59
  except Exception as e:
60
+ return {"success": False, "error": str(e)}
61
 
62
+ @mcp.tool(name="analyze_sequence", description="分析生物序列")
63
+ def analyze_sequence(sequence: str) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
64
  """
65
+ 分析给定的生物序列。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ 参数:
68
+ - sequence: 生物序列字符串
69
 
70
+ 返回:
71
+ - dict: 包含分析结果的字典。
 
 
 
 
 
 
 
 
72
  """
73
  try:
74
+ from biotite.sequence import NucleotideSequence
75
+ seq = NucleotideSequence(sequence)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  return {
77
  "success": True,
78
+ "length": len(seq),
79
+ "alphabet": str(seq.alphabet),
80
+ "sequence": str(seq)
 
 
 
 
 
81
  }
82
  except Exception as e:
83
+ return {"success": False, "error": str(e)}
84
 
85
+ @mcp.tool(name="calculate_rmsd", description="计算两种分子结构的 RMSD")
86
+ def calculate_rmsd(reference: list, subject: list) -> dict:
 
 
 
 
 
 
 
 
 
87
  """
88
+ 计算两种分子结构的 RMSD(均方根偏差)。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ 参数:
91
+ - reference: 参考分子结构的坐标列表
92
+ - subject: 待比较分子结构的坐标列表
93
 
94
+ 返回:
95
+ - dict: 包含 RMSD 值的字典。
 
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
  try:
98
+ from biotite.structure import rmsd
99
+ import numpy as np
 
 
 
 
 
 
 
 
100
 
101
+ reference_array = np.array(reference)
102
+ subject_array = np.array(subject)
103
+ rmsd_value = rmsd(reference_array, subject_array)
104
  return {
105
  "success": True,
106
+ "rmsd": rmsd_value
 
 
 
 
 
107
  }
108
  except Exception as e:
109
+ return {"success": False, "error": str(e)}
 
110
 
111
+ @mcp.tool(name="fetch_biological_data", description="从生物数据库中获取数据")
112
+ def fetch_biological_data(database: str, query: str) -> dict:
 
113
  """
114
+ 从指定的生物数据库中获取数据。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ 参数:
117
+ - database: 数据库名称(例如 'NCBI', 'RCSB', 'UniProt')
118
+ - query: 查询字符串
119
 
120
+ 返回:
121
+ - dict: 包含查询结果的字典。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  """
123
  try:
124
+ if database.lower() == "ncbi":
125
+ from biotite.database.entrez import fetch
126
+ data = fetch(query, "fasta", "nucleotide", "test.fasta")
127
+ elif database.lower() == "rcsb":
128
+ from biotite.database.rcsb import fetch
129
+ data = fetch(query, "pdb")
130
+ elif database.lower() == "uniprot":
131
+ from biotite.database.uniprot import fetch
132
+ data = fetch(query, "fasta")
133
+ else:
134
  return {
135
  "success": False,
136
+ "error": f"数据库 '{database}' 不受支持。支持的数据库包括: NCBI, RCSB, UniProt。"
 
137
  }
 
 
 
 
138
  return {
139
  "success": True,
140
+ "database": database,
141
+ "query": query,
142
+ "data": data
 
 
 
 
143
  }
144
  except Exception as e:
145
+ return {"success": False, "error": str(e)}
 
146
 
147
+ @mcp.tool(name="superimpose_structures", description="对齐两种分子结构")
148
+ def superimpose_structures(fixed: list, mobile: list) -> dict:
149
  """
150
+ 对齐两种分子结构。
151
+
152
+ 参数:
153
+ - fixed: 固定分子结构的坐标列表
154
+ - mobile: 待对齐的分子结构的坐标列表
155
+
156
+ 返回:
157
+ - dict: 包含对齐结果的字典。
158
  """
159
  try:
160
+ from biotite.structure import superimpose
161
+ import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ fixed_array = np.array(fixed)
164
+ mobile_array = np.array(mobile)
165
+ transformation = superimpose(fixed_array, mobile_array)
166
  return {
167
  "success": True,
168
+ "transformation": transformation.tolist()
 
 
 
 
169
  }
170
  except Exception as e:
171
+ return {"success": False, "error": str(e)}
 
172
 
173
+ # 创建 FastMCP 应用实例
174
  def create_app() -> FastMCP:
175
  """
176
+ 创建并返回 FastMCP 应用实例。
177
+
178
+ 返回:
179
+ - FastMCP: FastMCP 应用实例。
180
  """
181
  return mcp