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

Update biotite/mcp_output/mcp_plugin/mcp_service.py

Browse files
biotite/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import sys
 
3
 
4
  # Add the local source directory to sys.path
5
  source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
@@ -7,50 +8,899 @@ if source_path not in sys.path:
7
  sys.path.insert(0, source_path)
8
 
9
  from fastmcp import FastMCP
10
- from biotite.sequence.align import alignment
11
- from biotite.structure import atoms
12
 
13
  # Create the FastMCP service application
14
  mcp = FastMCP("biotite_service")
15
 
 
 
 
 
 
16
  @mcp.tool(name="align_sequences", description="Align biological sequences using Biotite.")
17
  def align_sequences(seq1: str, seq2: str) -> dict:
18
  """
19
- Align two biological sequences.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  Parameters:
22
- - seq1: The first sequence to align.
23
- - seq2: The second sequence to align.
24
 
25
  Returns:
26
- A dictionary containing the alignment result.
27
  """
28
  try:
29
- # Perform sequence alignment
30
- result = alignment.align(seq1, seq2)
31
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  except Exception as e:
33
  return {"success": False, "result": None, "error": str(e)}
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  @mcp.tool(name="analyze_atoms", description="Analyze atoms in a molecular structure.")
36
  def analyze_atoms(structure_file: str) -> dict:
37
  """
38
- Analyze atoms in a given molecular structure file.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  Parameters:
41
- - structure_file: Path to the molecular structure file.
42
 
43
  Returns:
44
- A dictionary containing the atom analysis result.
45
  """
46
  try:
47
- # Load and analyze atoms
48
- atom_array = atoms.load_structure(structure_file)
49
- analysis_result = atoms.analyze(atom_array)
50
- return {"success": True, "result": analysis_result, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  except Exception as e:
52
  return {"success": False, "result": None, "error": str(e)}
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def create_app() -> FastMCP:
55
  """
56
  Create and return the FastMCP application instance.
 
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")
 
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.