guohanghui commited on
Commit
341bd2a
·
verified ·
1 Parent(s): b665acb

Update biotite/mcp_output/mcp_plugin/mcp_service.py

Browse files
biotite/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,181 +1,911 @@
 
 
 
 
 
 
 
 
 
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
 
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