SEUyishu commited on
Commit
35cea78
·
verified ·
1 Parent(s): d96f8c7

Update mcp_service.py

Browse files
Files changed (1) hide show
  1. mcp_service.py +351 -27
mcp_service.py CHANGED
@@ -18,12 +18,16 @@ Spaces or any other environment that supports SSE connections.
18
 
19
  from __future__ import annotations
20
 
 
21
  import json
22
  import logging
23
  import os
 
 
24
  import uuid
25
  from dataclasses import dataclass, field
26
  from functools import lru_cache
 
27
  from typing import Any, Dict, Iterable, List, Optional, Tuple
28
 
29
  import numpy as np
@@ -127,14 +131,41 @@ def _structure_to_payload(structure: Structure, include_formats: Optional[List[s
127
 
128
 
129
  @lru_cache(maxsize=4)
130
- def _get_potential(model_name: str = "MP-2021.2.8-EFS") -> Potential:
131
- """Load and cache a Potential for a given pre-trained model name."""
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- logger.info("Loading potential '%s'", model_name)
134
- model = M3GNet.load(model_name)
135
  return Potential(model)
136
 
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  def _structure_to_atoms(structure: Structure) -> Atoms:
139
  """Utility to convert a pymatgen Structure into an ASE Atoms object."""
140
 
@@ -165,6 +196,18 @@ def _serialize_relaxation(observer: TrajectoryObserver) -> List[Dict[str, Any]]:
165
  return frames
166
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  @mcp.tool()
169
  def list_available_models() -> Dict[str, Any]:
170
  """Return metadata about bundled and downloadable pre-trained models."""
@@ -182,9 +225,96 @@ def list_available_models() -> Dict[str, Any]:
182
  }
183
 
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  @mcp.tool()
186
  def describe_model(model_name: str = "MP-2021.2.8-EFS") -> Dict[str, Any]:
187
- """Return configuration details for a loaded model."""
 
 
 
 
 
188
 
189
  potential = _get_potential(model_name)
190
  config = potential.model.get_config()
@@ -203,7 +333,14 @@ def predict_properties(
203
  include_forces: bool = True,
204
  include_stresses: bool = True,
205
  ) -> Dict[str, Any]:
206
- """Compute energy, forces, and stresses for a single structure."""
 
 
 
 
 
 
 
207
 
208
  target = _decode_structure(structure)
209
  potential = _get_potential(model_name)
@@ -235,7 +372,15 @@ def batch_predict_properties(
235
  include_stresses: bool = False,
236
  batch_size: int = 16,
237
  ) -> Dict[str, Any]:
238
- """Compute energies (and optionally forces/stresses) for multiple structures."""
 
 
 
 
 
 
 
 
239
 
240
  if not structures:
241
  raise ValueError("structures list is empty")
@@ -280,6 +425,57 @@ def batch_predict_properties(
280
  }
281
 
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  @mcp.tool()
284
  def relax_structure(
285
  structure: Dict[str, Any],
@@ -291,7 +487,18 @@ def relax_structure(
291
  interval: int = 1,
292
  include_formats: Optional[List[str]] = None,
293
  ) -> Dict[str, Any]:
294
- """Run a structural relaxation and return the relaxed structure and trajectory."""
 
 
 
 
 
 
 
 
 
 
 
295
 
296
  include_formats = include_formats or ["cif"]
297
  target = _decode_structure(structure)
@@ -335,7 +542,18 @@ def run_molecular_dynamics(
335
  log_interval: int = 10,
336
  stress_weight: float = 1 / 160.21766208,
337
  ) -> Dict[str, Any]:
338
- """Run a short molecular dynamics simulation and return sampled frames."""
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  target = _decode_structure(structure)
341
  atoms = _structure_to_atoms(target)
@@ -773,6 +991,7 @@ def load_custom_model(model_path: str) -> Dict[str, Any]:
773
  def get_training_code_template(
774
  task_type: str = "potential",
775
  include_example_data: bool = True,
 
776
  ) -> Dict[str, Any]:
777
  """
778
  Get Python code template for training M3GNet models locally.
@@ -788,7 +1007,46 @@ def get_training_code_template(
788
  """
789
 
790
  if task_type == "potential":
791
- code = '''"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  M3GNet Potential Training Script
793
  ================================
794
  Train an interatomic potential with energies, forces, and stresses.
@@ -899,7 +1157,40 @@ md = MolecularDynamics(
899
  md.run(steps=1000)
900
  '''
901
  else: # property
902
- code = '''"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  M3GNet Property Prediction Training Script
904
  ==========================================
905
  Train a model to predict scalar material properties.
@@ -973,17 +1264,25 @@ predictions = model.predict_structures(new_structures)
973
  print(predictions)
974
  '''
975
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  return {
977
  "success": True,
978
  "task_type": task_type,
979
  "code": code,
980
- "instructions": [
981
- "1. Install dependencies: pip install m3gnet pymatgen tensorflow",
982
- "2. Prepare your training data (structures + labels)",
983
- "3. Copy the code template and modify data loading section",
984
- "4. Run the script: python train_m3gnet.py",
985
- "5. The trained model will be saved to the specified directory",
986
- ],
987
  "tips": [
988
  "Use fit_per_element_offset=True for better accuracy on formation energies",
989
  "Adjust batch_size based on your GPU memory",
@@ -996,6 +1295,7 @@ print(predictions)
996
  @mcp.tool()
997
  def get_inference_code_template(
998
  task_type: str = "relaxation",
 
999
  ) -> Dict[str, Any]:
1000
  """
1001
  Get Python code template for running M3GNet inference locally.
@@ -1149,22 +1449,30 @@ m3g relax --infile struct1.cif struct2.cif struct3.cif --suffix _relaxed
1149
  "error": f"Unknown task_type: {task_type}",
1150
  "available_types": list(templates.keys()),
1151
  }
1152
-
 
 
 
 
 
 
 
 
 
 
 
 
1153
  return {
1154
  "success": True,
1155
  "task_type": task_type,
1156
  "code": templates[task_type],
1157
- "instructions": [
1158
- "1. Install m3gnet: pip install m3gnet",
1159
- "2. Copy the code template",
1160
- "3. Modify the structure loading section for your data",
1161
- "4. Run the script",
1162
- ],
1163
  }
1164
 
1165
 
1166
  @mcp.tool()
1167
- def get_graph_conversion_code() -> Dict[str, Any]:
1168
  """
1169
  Get code template for converting structures to M3GNet graph format.
1170
 
@@ -1209,10 +1517,23 @@ graph_list = tf_graph.as_list() # List format for model.call()
1209
  # n_triple_bonds, triple_bond_lengths, theta]
1210
  '''
1211
 
 
 
 
 
 
 
 
 
 
 
 
1212
  return {
1213
  "success": True,
1214
  "code": code,
 
1215
  "description": "Template for converting structures to M3GNet graph format",
 
1216
  }
1217
 
1218
 
@@ -1232,7 +1553,7 @@ def evaluate_model(
1232
  structures: List of structure payloads
1233
  true_energies: Ground truth energies in eV
1234
  true_forces: Optional ground truth forces in eV/Å
1235
- model_name: Model to evaluate
1236
 
1237
  Returns:
1238
  Evaluation metrics (MAE, RMSE for energy and forces)
@@ -1288,9 +1609,12 @@ def evaluate_model(
1288
  __all__ = [
1289
  "mcp",
1290
  "list_available_models",
 
1291
  "describe_model",
 
1292
  "predict_properties",
1293
  "batch_predict_properties",
 
1294
  "relax_structure",
1295
  "run_molecular_dynamics",
1296
  "convert_structure_format",
 
18
 
19
  from __future__ import annotations
20
 
21
+ import importlib
22
  import json
23
  import logging
24
  import os
25
+ import pkgutil
26
+ import textwrap
27
  import uuid
28
  from dataclasses import dataclass, field
29
  from functools import lru_cache
30
+ from pathlib import Path
31
  from typing import Any, Dict, Iterable, List, Optional, Tuple
32
 
33
  import numpy as np
 
131
 
132
 
133
  @lru_cache(maxsize=4)
134
+ def _get_potential(model_reference: str = "MP-2021.2.8-EFS") -> Potential:
135
+ """Load and cache a Potential from either a named checkpoint or a path."""
136
+
137
+ if not model_reference:
138
+ raise ValueError("model_reference must be a non-empty string")
139
+
140
+ resolved_path = Path(model_reference).expanduser().resolve() if os.path.exists(model_reference) else None
141
+
142
+ if resolved_path and resolved_path.exists():
143
+ logger.info("Loading potential from directory '%s'", resolved_path)
144
+ model = M3GNet.from_dir(str(resolved_path))
145
+ else:
146
+ logger.info("Loading potential '%s'", model_reference)
147
+ model = M3GNet.load(model_reference)
148
 
 
 
149
  return Potential(model)
150
 
151
 
152
+ @lru_cache(maxsize=4)
153
+ def _get_property_model(model_reference: str) -> M3GNet:
154
+ """Load and cache an M3GNet model for scalar property prediction."""
155
+
156
+ if not model_reference:
157
+ raise ValueError("model_reference must be provided")
158
+
159
+ resolved_path = Path(model_reference).expanduser().resolve() if os.path.exists(model_reference) else None
160
+
161
+ if resolved_path and resolved_path.exists():
162
+ logger.info("Loading property model from directory '%s'", resolved_path)
163
+ return M3GNet.from_dir(str(resolved_path))
164
+
165
+ logger.info("Loading property model '%s'", model_reference)
166
+ return M3GNet.load(model_reference)
167
+
168
+
169
  def _structure_to_atoms(structure: Structure) -> Atoms:
170
  """Utility to convert a pymatgen Structure into an ASE Atoms object."""
171
 
 
196
  return frames
197
 
198
 
199
+ def _maybe_write_script(output_path: Optional[str], code: str) -> Optional[str]:
200
+ """Write generated script text to disk when an output path is provided."""
201
+
202
+ if not output_path:
203
+ return None
204
+
205
+ target_path = Path(output_path).expanduser().resolve()
206
+ target_path.parent.mkdir(parents=True, exist_ok=True)
207
+ target_path.write_text(code, encoding="utf-8")
208
+ return str(target_path)
209
+
210
+
211
  @mcp.tool()
212
  def list_available_models() -> Dict[str, Any]:
213
  """Return metadata about bundled and downloadable pre-trained models."""
 
225
  }
226
 
227
 
228
+ @mcp.tool()
229
+ def list_library_components() -> Dict[str, Any]:
230
+ """Enumerate key modules, submodules, and public symbols in the m3gnet package."""
231
+
232
+ base_modules = [
233
+ "m3gnet",
234
+ "m3gnet.models",
235
+ "m3gnet.trainers",
236
+ "m3gnet.graph",
237
+ "m3gnet.layers",
238
+ "m3gnet.utils",
239
+ "m3gnet.callbacks",
240
+ "m3gnet.config",
241
+ "m3gnet.type",
242
+ "m3gnet.cli",
243
+ ]
244
+
245
+ overview: Dict[str, Any] = {}
246
+
247
+ for module_name in base_modules:
248
+ try:
249
+ module = importlib.import_module(module_name)
250
+ except Exception as exc: # noqa: BLE001
251
+ overview[module_name] = {"error": str(exc)}
252
+ continue
253
+
254
+ public_symbols = getattr(module, "__all__", None)
255
+ if public_symbols is None:
256
+ public_symbols = [name for name in dir(module) if not name.startswith("_")]
257
+
258
+ submodules: List[str] = []
259
+ module_path = getattr(module, "__path__", None)
260
+ if module_path:
261
+ submodules = sorted(
262
+ f"{module_name}.{info.name}" for info in pkgutil.iter_modules(module_path)
263
+ )
264
+
265
+ overview[module_name] = {
266
+ "public_symbols": sorted(public_symbols),
267
+ "submodules": submodules,
268
+ "doc": textwrap.shorten((module.__doc__ or "").strip(), width=120, placeholder="..."),
269
+ }
270
+
271
+ return {"success": True, "overview": overview}
272
+
273
+
274
+ @mcp.tool()
275
+ def get_component_documentation(target: str) -> Dict[str, Any]:
276
+ """Return docstrings and metadata for a given m3gnet component.
277
+
278
+ The *target* parameter accepts "module" or "module:attribute" syntax.
279
+ Examples: "m3gnet.models", "m3gnet.layers._basis:RadialBasisLayer".
280
+ """
281
+
282
+ if not target or not target.strip():
283
+ raise ValueError("target must be a non-empty string")
284
+
285
+ module_name = target
286
+ attr_name: Optional[str] = None
287
+
288
+ if ":" in target:
289
+ module_name, attr_name = target.split(":", 1)
290
+ module_name = module_name.strip()
291
+ attr_name = attr_name.strip() if attr_name else None
292
+
293
+ module = importlib.import_module(module_name)
294
+ obj = getattr(module, attr_name) if attr_name else module
295
+
296
+ doc = textwrap.dedent(obj.__doc__ or "").strip() or "No documentation available."
297
+
298
+ metadata = {
299
+ "module": module_name,
300
+ "object_type": type(obj).__name__,
301
+ "has_attributes": bool(getattr(obj, "__dict__", {})) if attr_name else False,
302
+ }
303
+
304
+ if not attr_name and hasattr(module, "__all__"):
305
+ metadata["exported_names"] = list(module.__all__)
306
+
307
+ return {"success": True, "doc": doc, "metadata": metadata}
308
+
309
+
310
  @mcp.tool()
311
  def describe_model(model_name: str = "MP-2021.2.8-EFS") -> Dict[str, Any]:
312
+ """Return configuration details for a loaded model or custom checkpoint.
313
+
314
+ Args:
315
+ model_name: Pre-trained identifier (e.g. "MP-2021.2.8-EFS") or path to a
316
+ directory containing an exported M3GNet model.
317
+ """
318
 
319
  potential = _get_potential(model_name)
320
  config = potential.model.get_config()
 
333
  include_forces: bool = True,
334
  include_stresses: bool = True,
335
  ) -> Dict[str, Any]:
336
+ """Compute energy, forces, and stresses for a single structure.
337
+
338
+ Args:
339
+ structure: Serialized structure payload.
340
+ model_name: Pre-trained model name or custom checkpoint directory.
341
+ include_forces: Whether to include force components in the response.
342
+ include_stresses: Whether to include the Voigt stress tensor.
343
+ """
344
 
345
  target = _decode_structure(structure)
346
  potential = _get_potential(model_name)
 
372
  include_stresses: bool = False,
373
  batch_size: int = 16,
374
  ) -> Dict[str, Any]:
375
+ """Compute energies (and optionally forces/stresses) for multiple structures.
376
+
377
+ Args:
378
+ structures: Sequence of serialized structures.
379
+ model_name: Pre-trained model name or path to a custom checkpoint.
380
+ include_forces: If True, include atomic forces for each structure.
381
+ include_stresses: If True, include stress tensors when available.
382
+ batch_size: Batch size used for batched predictions.
383
+ """
384
 
385
  if not structures:
386
  raise ValueError("structures list is empty")
 
425
  }
426
 
427
 
428
+ @mcp.tool()
429
+ def predict_scalar_property(
430
+ structures: List[Dict[str, Any]],
431
+ model_name: str,
432
+ batch_size: int = 32,
433
+ return_numpy: bool = False,
434
+ ) -> Dict[str, Any]:
435
+ """Predict scalar material properties using a property model or custom checkpoint.
436
+
437
+ Args:
438
+ structures: List of serialized structure payloads.
439
+ model_name: Pre-trained property model identifier or path to saved model.
440
+ batch_size: Batch size for batched inference.
441
+ return_numpy: If True, include the full numpy array (as nested lists) in the response.
442
+ """
443
+
444
+ if not structures:
445
+ raise ValueError("structures list is empty")
446
+
447
+ decoded = [_decode_structure(item) for item in structures]
448
+ model = _get_property_model(model_name)
449
+
450
+ predictions = model.predict_structures(decoded, batch_size=batch_size)
451
+ flat_values = np.asarray(predictions).reshape(-1)
452
+
453
+ results = [
454
+ {
455
+ "structure_index": idx,
456
+ "formula": struct.composition.reduced_formula,
457
+ "num_sites": struct.num_sites,
458
+ "value": float(flat_values[idx]),
459
+ }
460
+ for idx, struct in enumerate(decoded)
461
+ ]
462
+
463
+ response: Dict[str, Any] = {
464
+ "success": True,
465
+ "model_name": model_name,
466
+ "batch_size": batch_size,
467
+ "predictions": results,
468
+ }
469
+
470
+ if return_numpy:
471
+ response["raw_array"] = flat_values.tolist()
472
+
473
+ if len(results) == 1:
474
+ response["value"] = results[0]["value"]
475
+
476
+ return response
477
+
478
+
479
  @mcp.tool()
480
  def relax_structure(
481
  structure: Dict[str, Any],
 
487
  interval: int = 1,
488
  include_formats: Optional[List[str]] = None,
489
  ) -> Dict[str, Any]:
490
+ """Run a structural relaxation and return the relaxed structure and trajectory.
491
+
492
+ Args:
493
+ structure: Serialized structure payload for relaxation.
494
+ model_name: Pre-trained identifier or custom checkpoint path.
495
+ fmax: Force convergence threshold in eV/Å.
496
+ steps: Maximum optimizer steps.
497
+ relax_cell: Whether to relax lattice parameters.
498
+ optimizer: Optimizer name supported by :class:`Relaxer`.
499
+ interval: Interval (in steps) for recording trajectory frames.
500
+ include_formats: Extra serialization formats to add to the response.
501
+ """
502
 
503
  include_formats = include_formats or ["cif"]
504
  target = _decode_structure(structure)
 
542
  log_interval: int = 10,
543
  stress_weight: float = 1 / 160.21766208,
544
  ) -> Dict[str, Any]:
545
+ """Run a short molecular dynamics simulation and return sampled frames.
546
+
547
+ Args:
548
+ structure: Serialized structure payload.
549
+ model_name: Pre-trained identifier or custom checkpoint path.
550
+ ensemble: Statistical ensemble name ("nvt", "npt", etc.).
551
+ temperature: Target temperature in Kelvin.
552
+ timestep_fs: Time step in femtoseconds.
553
+ steps: Number of MD steps to simulate.
554
+ log_interval: Interval between recorded frames.
555
+ stress_weight: Coupling factor for stress control.
556
+ """
557
 
558
  target = _decode_structure(structure)
559
  atoms = _structure_to_atoms(target)
 
991
  def get_training_code_template(
992
  task_type: str = "potential",
993
  include_example_data: bool = True,
994
+ output_path: Optional[str] = None,
995
  ) -> Dict[str, Any]:
996
  """
997
  Get Python code template for training M3GNet models locally.
 
1007
  """
1008
 
1009
  if task_type == "potential":
1010
+ if not include_example_data:
1011
+ code = textwrap.dedent(
1012
+ """
1013
+ \"\"\"Minimal M3GNet potential training skeleton.\"\"\"
1014
+
1015
+ import tensorflow as tf
1016
+ from m3gnet.models import M3GNet, Potential
1017
+ from m3gnet.trainers import PotentialTrainer
1018
+
1019
+
1020
+ def train_potential(structures, energies, forces, **kwargs):
1021
+ model = M3GNet(is_intensive=False)
1022
+ potential = Potential(model=model)
1023
+ optimizer = tf.keras.optimizers.Adam(kwargs.get("learning_rate", 1e-3))
1024
+ trainer = PotentialTrainer(potential=potential, optimizer=optimizer)
1025
+ trainer.train(
1026
+ structures,
1027
+ energies,
1028
+ forces,
1029
+ stresses=kwargs.get("stresses"),
1030
+ validation_graphs_or_structures=kwargs.get("val_structures"),
1031
+ val_energies=kwargs.get("val_energies"),
1032
+ val_forces=kwargs.get("val_forces"),
1033
+ val_stresses=kwargs.get("val_stresses"),
1034
+ batch_size=kwargs.get("batch_size", 16),
1035
+ epochs=kwargs.get("epochs", 200),
1036
+ force_loss_ratio=kwargs.get("force_loss_ratio", 1.0),
1037
+ stress_loss_ratio=kwargs.get("stress_loss_ratio", 0.1),
1038
+ fit_per_element_offset=kwargs.get("fit_per_element_offset", True),
1039
+ verbose=kwargs.get("verbose", 1),
1040
+ )
1041
+ return potential
1042
+
1043
+
1044
+ if __name__ == "__main__":
1045
+ raise SystemExit("Replace this stub with your data loading pipeline and call train_potential(...).")
1046
+ """
1047
+ )
1048
+ else:
1049
+ code = '''"""
1050
  M3GNet Potential Training Script
1051
  ================================
1052
  Train an interatomic potential with energies, forces, and stresses.
 
1157
  md.run(steps=1000)
1158
  '''
1159
  else: # property
1160
+ if not include_example_data:
1161
+ code = textwrap.dedent(
1162
+ """
1163
+ \"\"\"Minimal M3GNet property model training skeleton.\"\"\"
1164
+
1165
+ import tensorflow as tf
1166
+ from m3gnet.models import M3GNet
1167
+ from m3gnet.trainers import Trainer
1168
+
1169
+
1170
+ def train_property_model(structures, targets, **kwargs):
1171
+ model = M3GNet(is_intensive=True)
1172
+ optimizer = tf.keras.optimizers.Adam(kwargs.get("learning_rate", 1e-3))
1173
+ trainer = Trainer(model=model, optimizer=optimizer)
1174
+ trainer.train(
1175
+ structures,
1176
+ targets,
1177
+ validation_graphs_or_structures=kwargs.get("val_structures"),
1178
+ validation_targets=kwargs.get("val_targets"),
1179
+ batch_size=kwargs.get("batch_size", 32),
1180
+ epochs=kwargs.get("epochs", 300),
1181
+ early_stop_patience=kwargs.get("early_stop_patience", 100),
1182
+ fit_per_element_offset=kwargs.get("fit_per_element_offset", True),
1183
+ verbose=kwargs.get("verbose", 1),
1184
+ )
1185
+ return model
1186
+
1187
+
1188
+ if __name__ == "__main__":
1189
+ raise SystemExit("Provide training data and call train_property_model(...)")
1190
+ """
1191
+ )
1192
+ else:
1193
+ code = '''"""
1194
  M3GNet Property Prediction Training Script
1195
  ==========================================
1196
  Train a model to predict scalar material properties.
 
1264
  print(predictions)
1265
  '''
1266
 
1267
+ script_path = _maybe_write_script(output_path, code)
1268
+
1269
+ instructions = [
1270
+ "1. Install dependencies: pip install m3gnet pymatgen tensorflow",
1271
+ "2. Prepare your training data (structures + labels)",
1272
+ "3. Copy the code template and modify data loading section",
1273
+ "4. Run the script: python train_m3gnet.py",
1274
+ "5. The trained model will be saved to the specified directory",
1275
+ ]
1276
+
1277
+ if script_path:
1278
+ instructions.insert(0, f"Script written to {script_path}")
1279
+
1280
  return {
1281
  "success": True,
1282
  "task_type": task_type,
1283
  "code": code,
1284
+ "output_path": script_path,
1285
+ "instructions": instructions,
 
 
 
 
 
1286
  "tips": [
1287
  "Use fit_per_element_offset=True for better accuracy on formation energies",
1288
  "Adjust batch_size based on your GPU memory",
 
1295
  @mcp.tool()
1296
  def get_inference_code_template(
1297
  task_type: str = "relaxation",
1298
+ output_path: Optional[str] = None,
1299
  ) -> Dict[str, Any]:
1300
  """
1301
  Get Python code template for running M3GNet inference locally.
 
1449
  "error": f"Unknown task_type: {task_type}",
1450
  "available_types": list(templates.keys()),
1451
  }
1452
+
1453
+ script_path = _maybe_write_script(output_path, templates[task_type])
1454
+
1455
+ instructions = [
1456
+ "1. Install m3gnet: pip install m3gnet",
1457
+ "2. Copy the code template",
1458
+ "3. Modify the structure loading section for your data",
1459
+ "4. Run the script",
1460
+ ]
1461
+
1462
+ if script_path:
1463
+ instructions.insert(0, f"Script written to {script_path}")
1464
+
1465
  return {
1466
  "success": True,
1467
  "task_type": task_type,
1468
  "code": templates[task_type],
1469
+ "output_path": script_path,
1470
+ "instructions": instructions,
 
 
 
 
1471
  }
1472
 
1473
 
1474
  @mcp.tool()
1475
+ def get_graph_conversion_code(output_path: Optional[str] = None) -> Dict[str, Any]:
1476
  """
1477
  Get code template for converting structures to M3GNet graph format.
1478
 
 
1517
  # n_triple_bonds, triple_bond_lengths, theta]
1518
  '''
1519
 
1520
+ script_path = _maybe_write_script(output_path, code)
1521
+
1522
+ instructions = [
1523
+ "1. Install m3gnet and pymatgen",
1524
+ "2. Place your structure file alongside the script",
1525
+ "3. Run the script to print graph statistics",
1526
+ ]
1527
+
1528
+ if script_path:
1529
+ instructions.insert(0, f"Script written to {script_path}")
1530
+
1531
  return {
1532
  "success": True,
1533
  "code": code,
1534
+ "output_path": script_path,
1535
  "description": "Template for converting structures to M3GNet graph format",
1536
+ "instructions": instructions,
1537
  }
1538
 
1539
 
 
1553
  structures: List of structure payloads
1554
  true_energies: Ground truth energies in eV
1555
  true_forces: Optional ground truth forces in eV/Å
1556
+ model_name: Pre-trained identifier or path to custom checkpoint
1557
 
1558
  Returns:
1559
  Evaluation metrics (MAE, RMSE for energy and forces)
 
1609
  __all__ = [
1610
  "mcp",
1611
  "list_available_models",
1612
+ "list_library_components",
1613
  "describe_model",
1614
+ "get_component_documentation",
1615
  "predict_properties",
1616
  "batch_predict_properties",
1617
+ "predict_scalar_property",
1618
  "relax_structure",
1619
  "run_molecular_dynamics",
1620
  "convert_structure_format",