rowan-mcp 1.0.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rowan-mcp might be problematic. Click here for more details.

Files changed (70) hide show
  1. rowan_mcp/__init__.py +1 -1
  2. rowan_mcp/__main__.py +3 -5
  3. rowan_mcp/functions_v2/BENCHMARK.md +86 -0
  4. rowan_mcp/functions_v2/molecule_lookup.py +232 -0
  5. rowan_mcp/functions_v2/protein_management.py +141 -0
  6. rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
  7. rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
  8. rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
  9. rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
  10. rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
  11. rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
  12. rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
  13. rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
  14. rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
  15. rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
  16. rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
  17. rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
  18. rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
  19. rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
  20. rowan_mcp/server.py +109 -144
  21. rowan_mcp/tests/basic_calculation_from_json.py +0 -0
  22. rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
  23. rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
  24. rowan_mcp/tests/bde.py +37 -0
  25. rowan_mcp/tests/benchmark_queries.md +120 -0
  26. rowan_mcp/tests/cofolding_screen.py +131 -0
  27. rowan_mcp/tests/conformer_dependent_redox.py +37 -0
  28. rowan_mcp/tests/conformers.py +31 -0
  29. rowan_mcp/tests/data.json +189 -0
  30. rowan_mcp/tests/docking_screen.py +157 -0
  31. rowan_mcp/tests/irc.py +24 -0
  32. rowan_mcp/tests/macropka.py +13 -0
  33. rowan_mcp/tests/multistage_opt.py +13 -0
  34. rowan_mcp/tests/optimization.py +21 -0
  35. rowan_mcp/tests/phenol_pka.py +36 -0
  36. rowan_mcp/tests/pka.py +36 -0
  37. rowan_mcp/tests/protein_cofolding.py +17 -0
  38. rowan_mcp/tests/scan.py +28 -0
  39. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +41 -33
  40. rowan_mcp-2.0.0.dist-info/RECORD +42 -0
  41. rowan_mcp/functions/admet.py +0 -94
  42. rowan_mcp/functions/bde.py +0 -113
  43. rowan_mcp/functions/calculation_retrieve.py +0 -89
  44. rowan_mcp/functions/conformers.py +0 -80
  45. rowan_mcp/functions/descriptors.py +0 -92
  46. rowan_mcp/functions/docking.py +0 -340
  47. rowan_mcp/functions/docking_enhanced.py +0 -174
  48. rowan_mcp/functions/electronic_properties.py +0 -205
  49. rowan_mcp/functions/folder_management.py +0 -137
  50. rowan_mcp/functions/fukui.py +0 -219
  51. rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
  52. rowan_mcp/functions/irc.py +0 -125
  53. rowan_mcp/functions/macropka.py +0 -120
  54. rowan_mcp/functions/molecular_converter.py +0 -423
  55. rowan_mcp/functions/molecular_dynamics.py +0 -191
  56. rowan_mcp/functions/molecule_lookup.py +0 -57
  57. rowan_mcp/functions/multistage_opt.py +0 -171
  58. rowan_mcp/functions/pdb_handler.py +0 -200
  59. rowan_mcp/functions/pka.py +0 -88
  60. rowan_mcp/functions/redox_potential.py +0 -352
  61. rowan_mcp/functions/scan.py +0 -536
  62. rowan_mcp/functions/scan_analyzer.py +0 -347
  63. rowan_mcp/functions/solubility.py +0 -277
  64. rowan_mcp/functions/spin_states.py +0 -747
  65. rowan_mcp/functions/system_management.py +0 -368
  66. rowan_mcp/functions/tautomers.py +0 -91
  67. rowan_mcp/functions/workflow_management.py +0 -422
  68. rowan_mcp-1.0.2.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,99 @@
1
+ """
2
+ Rowan v2 API: MacropKa Workflow
3
+ Calculate macroscopic pKa values across a pH range.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+
9
+ def submit_macropka_workflow(
10
+ initial_smiles: Annotated[str, "SMILES string of the molecule for macropKa calculation"],
11
+ min_pH: Annotated[int, "Minimum pH value for the calculation range"] = 0,
12
+ max_pH: Annotated[int, "Maximum pH value for the calculation range"] = 14,
13
+ min_charge: Annotated[int, "Minimum molecular charge to consider"] = -2,
14
+ max_charge: Annotated[int, "Maximum molecular charge to consider"] = 2,
15
+ compute_solvation_energy: Annotated[bool, "Whether to compute solvation energy corrections"] = True,
16
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Macropka Workflow",
17
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
18
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
19
+ ):
20
+ """Submit a MacropKa workflow using Rowan v2 API.
21
+
22
+ Args:
23
+ initial_smiles: SMILES string of the molecule for macropKa calculation
24
+ min_pH: Minimum pH value for the calculation range
25
+ max_pH: Maximum pH value for the calculation range
26
+ min_charge: Minimum molecular charge to consider
27
+ max_charge: Maximum molecular charge to consider
28
+ compute_solvation_energy: Whether to compute solvation energy for each species
29
+ name: Workflow name for identification and tracking
30
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
31
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
32
+
33
+ Calculates macroscopic pKa values across a pH range, determining the
34
+ protonation states and their populations at different pH values.
35
+
36
+ The workflow will:
37
+ 1. Identify all ionizable sites in the molecule
38
+ 2. Calculate microscopic pKa values for each site
39
+ 3. Determine macroscopic pKa values and species populations
40
+ 4. Optionally compute solvation energies
41
+
42
+ Returns:
43
+ Workflow object representing the submitted workflow
44
+
45
+ Examples:
46
+ # Simple molecule macropKa
47
+ result = submit_macropka_workflow(
48
+ initial_smiles="CC(=O)O", # Acetic acid
49
+ min_pH=0,
50
+ max_pH=14
51
+ )
52
+
53
+ # Complex molecule with custom charge range
54
+ result = submit_macropka_workflow(
55
+ initial_smiles="CC(C)CC(C(=O)O)N", # Leucine
56
+ min_pH=0,
57
+ max_pH=14,
58
+ min_charge=-3,
59
+ max_charge=3,
60
+ compute_solvation_energy=True
61
+ )
62
+ """
63
+
64
+ try:
65
+ # Build workflow_data
66
+ workflow_data = {
67
+ "min_pH": min_pH,
68
+ "max_pH": max_pH,
69
+ "min_charge": min_charge,
70
+ "max_charge": max_charge,
71
+ "compute_solvation_energy": compute_solvation_energy,
72
+ }
73
+
74
+ # Build the API request
75
+ data = {
76
+ "name": name,
77
+ "folder_uuid": folder_uuid,
78
+ "workflow_type": "macropka",
79
+ "workflow_data": workflow_data,
80
+ "initial_smiles": initial_smiles,
81
+ "max_credits": max_credits,
82
+ }
83
+
84
+ # Submit to API using rowan module
85
+ return rowan.submit_macropka_workflow(
86
+ initial_smiles=initial_smiles,
87
+ min_pH=min_pH,
88
+ max_pH=max_pH,
89
+ min_charge=min_charge,
90
+ max_charge=max_charge,
91
+ compute_solvation_energy=compute_solvation_energy,
92
+ name=name,
93
+ folder_uuid=folder_uuid if folder_uuid else None,
94
+ max_credits=max_credits if max_credits > 0 else None
95
+ )
96
+
97
+ except Exception as e:
98
+ # Re-raise the exception so MCP can handle it
99
+ raise e
@@ -0,0 +1,72 @@
1
+ """
2
+ Rowan v2 API: pKa Workflow
3
+ Predict acid dissociation constants for ionizable groups in molecules.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Annotated
7
+ import rowan
8
+ import json
9
+ import stjames
10
+
11
+ def submit_pka_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string of the molecule to calculate pKa"],
13
+ pka_range: Annotated[List[float], "pKa range [min, max] to search (e.g., [2, 12])"] = [2, 12],
14
+ deprotonate_elements: Annotated[str, "Comma-separated elements for deprotonation (e.g., 'N,O,S'). Empty for auto-detect"] = "",
15
+ protonate_elements: Annotated[str, "Comma-separated elements for protonation (e.g., 'N,O'). Empty for auto-detect"] = "",
16
+ mode: Annotated[str, "Calculation mode: 'rapid', 'careful', or 'meticulous'"] = "careful",
17
+ name: Annotated[str, "Workflow name for identification and tracking"] = "pKa Workflow",
18
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
19
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
20
+ ):
21
+ """Submit a pKa prediction workflow using Rowan v2 API.
22
+
23
+ Args:
24
+ initial_molecule: The molecule to calculate the pKa of. SMILES string.
25
+ pka_range: pKa range [min, max] to search, e.g., [2, 12]
26
+ deprotonate_elements: Atomic numbers to consider for deprotonation, e.g., "[7, 8, 16]" for N, O, S. Empty string uses defaults.
27
+ protonate_elements: Atomic numbers to consider for protonation, e.g., "[7, 8]" for N, O. Empty string uses defaults.
28
+ mode: Calculation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
29
+ name: Workflow name for identification and tracking
30
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
31
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
32
+
33
+ Returns:
34
+ Workflow object representing the submitted workflow
35
+
36
+ Examples:
37
+ # Phenol pKa
38
+ result = submit_pka_workflow(
39
+ initial_molecule="Oc1ccccc1",
40
+ name="pKa phenol",
41
+ deprotonate_elements="[8]" # Only consider oxygen
42
+ )
43
+ """
44
+
45
+ # Handle JSON string inputs for element lists
46
+ parsed_deprotonate_elements = None
47
+ if deprotonate_elements:
48
+ try:
49
+ parsed_deprotonate_elements = json.loads(deprotonate_elements)
50
+ except json.JSONDecodeError:
51
+ pass # Keep as None if not valid JSON
52
+
53
+ parsed_protonate_elements = None
54
+ if protonate_elements:
55
+ try:
56
+ parsed_protonate_elements = json.loads(protonate_elements)
57
+ except json.JSONDecodeError:
58
+ pass # Keep as None if not valid JSON
59
+
60
+ # Convert List[float] to Tuple[float, float] for Rowan SDK compatibility
61
+ pka_range_tuple = tuple(pka_range) if len(pka_range) == 2 else (pka_range[0], pka_range[1] if len(pka_range) > 1 else pka_range[0])
62
+
63
+ return rowan.submit_pka_workflow(
64
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
65
+ pka_range=pka_range_tuple,
66
+ deprotonate_elements=parsed_deprotonate_elements,
67
+ protonate_elements=parsed_protonate_elements,
68
+ mode=mode,
69
+ name=name,
70
+ folder_uuid=folder_uuid if folder_uuid else None,
71
+ max_credits=max_credits if max_credits > 0 else None
72
+ )
@@ -0,0 +1,88 @@
1
+ """
2
+ Rowan v2 API: Protein Cofolding Workflow
3
+ Simulate protein-protein interactions and cofolding.
4
+ """
5
+
6
+ from typing import List, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+
11
+
12
+ def submit_protein_cofolding_workflow(
13
+ initial_protein_sequences: Annotated[str, "JSON string list of protein sequences for cofolding (e.g., '[\"MKLLV...\", \"MAHQR...\"]')"],
14
+ initial_smiles_list: Annotated[str, "JSON string list of SMILES for ligands to include in cofolding (e.g., '[\"CCO\", \"CC(=O)O\"]'). Empty for protein-only"] = "",
15
+ ligand_binding_affinity_index: Annotated[str, "JSON string mapping ligand indices to protein binding sites. Empty for automatic detection"] = "",
16
+ use_msa_server: Annotated[bool, "Whether to use multiple sequence alignment server for better structure prediction"] = True,
17
+ use_potentials: Annotated[bool, "Whether to include additional potentials in the calculation"] = False,
18
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Cofolding Workflow",
19
+ model: Annotated[str, "Structure prediction model to use (e.g., 'boltz_2', 'alphafold3')"] = "boltz_2",
20
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
21
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
22
+ ):
23
+ """Submits a protein cofolding workflow to the API.
24
+
25
+ Args:
26
+ initial_protein_sequences: JSON string list of protein sequences (amino acid strings) to cofold
27
+ initial_smiles_list: JSON string list of ligand SMILES strings to include in cofolding. Empty string for protein-only
28
+ ligand_binding_affinity_index: Index of ligand in initial_smiles_list for binding affinity calculation (e.g., "0"). Empty string skips affinity
29
+ use_msa_server: Whether to use MSA (Multiple Sequence Alignment) server for improved accuracy
30
+ use_potentials: Whether to use statistical potentials in the calculation
31
+ name: Workflow name for identification and tracking
32
+ model: Cofolding model to use (e.g., 'boltz_2', 'alphafold3')
33
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
34
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
35
+
36
+ Returns:
37
+ Workflow object representing the submitted workflow
38
+
39
+ Example:
40
+ # Protein-ligand cofolding with CDK2 kinase (from test)
41
+ result = submit_protein_cofolding_workflow(
42
+ initial_protein_sequences='["MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVPSTAIREISLLKELNHPNIVKLLDVIHTENKLYLVFEFLHQDLKKFMDASALTGIPLPLIKSYLFQLLQGLAFCHSHRVLHRDLKPQNLLINTEGAIKLADFGLARAFGVPVRTYTHEVVTLWYRAPEILLGCKYYSTAVDIWSLGCIFAEMVTRRALFPGDSEIDQLFRIFRTLGTPDEVVWPGVTSMPDYKPSFPKWARQDFSKVVPPLDEDGRSLLSQMLHYDPNKRISAKAALAHPFFQDVTKPVPHLRL"]',
43
+ initial_smiles_list='["CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"]',
44
+ ligand_binding_affinity_index="0",
45
+ name="Cofolding CDK2 with ligand"
46
+ )
47
+ """
48
+ # Parse initial_protein_sequences (always a string in simplified version)
49
+ try:
50
+ initial_protein_sequences = json.loads(initial_protein_sequences)
51
+ except (json.JSONDecodeError, ValueError):
52
+ # Try to parse as comma-separated
53
+ if ',' in initial_protein_sequences:
54
+ initial_protein_sequences = [s.strip() for s in initial_protein_sequences.split(',') if s.strip()]
55
+ else:
56
+ initial_protein_sequences = [initial_protein_sequences.strip()]
57
+
58
+ # Parse initial_smiles_list if provided
59
+ parsed_initial_smiles_list = None
60
+ if initial_smiles_list:
61
+ try:
62
+ parsed_initial_smiles_list = json.loads(initial_smiles_list)
63
+ except (json.JSONDecodeError, ValueError):
64
+ # Try to parse as comma-separated
65
+ if ',' in initial_smiles_list:
66
+ parsed_initial_smiles_list = [s.strip() for s in initial_smiles_list.split(',') if s.strip()]
67
+ else:
68
+ parsed_initial_smiles_list = [initial_smiles_list.strip()]
69
+
70
+ # Parse ligand_binding_affinity_index if provided
71
+ parsed_ligand_index = None
72
+ if ligand_binding_affinity_index:
73
+ try:
74
+ parsed_ligand_index = int(ligand_binding_affinity_index)
75
+ except (ValueError, TypeError):
76
+ raise ValueError(f"Invalid ligand_binding_affinity_index: '{ligand_binding_affinity_index}' must be an integer")
77
+
78
+ return rowan.submit_protein_cofolding_workflow(
79
+ initial_protein_sequences=initial_protein_sequences,
80
+ initial_smiles_list=parsed_initial_smiles_list,
81
+ ligand_binding_affinity_index=parsed_ligand_index,
82
+ use_msa_server=use_msa_server,
83
+ use_potentials=use_potentials,
84
+ name=name,
85
+ model=model,
86
+ folder_uuid=folder_uuid if folder_uuid else None,
87
+ max_credits=max_credits if max_credits > 0 else None
88
+ )
@@ -0,0 +1,55 @@
1
+ """
2
+ Rowan v2 API: Redox Potential Workflow
3
+ Calculate reduction and oxidation potentials for molecules.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+
11
+ def submit_redox_potential_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string for redox potential calculation"],
13
+ reduction: Annotated[bool, "Whether to calculate reduction potential (gaining electron)"] = False,
14
+ oxidization: Annotated[bool, "Whether to calculate oxidation potential (losing electron)"] = True,
15
+ mode: Annotated[str, "Calculation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)"] = "rapid",
16
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Redox Potential Workflow",
17
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
18
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
19
+ ):
20
+ """Submit a redox potential calculation workflow using Rowan v2 API.
21
+
22
+ Args:
23
+ initial_molecule: SMILES string for redox potential calculation
24
+ reduction: Whether to calculate reduction potential (gaining electron)
25
+ oxidization: Whether to calculate oxidation potential (losing electron)
26
+ mode: Calculation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
27
+ name: Workflow name for identification and tracking
28
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
29
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
30
+
31
+ Calculates reduction and/or oxidation potentials for a molecule using
32
+ quantum chemistry methods.
33
+
34
+ Returns:
35
+ Workflow object representing the submitted workflow
36
+
37
+ Example:
38
+ # Simple redox potential from SMILES
39
+ result = submit_redox_potential_workflow(
40
+ initial_molecule="Cc1ccccc1", # Toluene
41
+ reduction=True,
42
+ oxidization=True,
43
+ name="Toluene Redox Potential"
44
+ )
45
+ """
46
+
47
+ return rowan.submit_redox_potential_workflow(
48
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
49
+ reduction=reduction,
50
+ oxidization=oxidization,
51
+ mode=mode,
52
+ name=name,
53
+ folder_uuid=folder_uuid if folder_uuid else None,
54
+ max_credits=max_credits if max_credits > 0 else None
55
+ )
@@ -0,0 +1,82 @@
1
+ """
2
+ Rowan v2 API: Scan Workflow
3
+ Perform potential energy surface scans along molecular coordinates.
4
+ """
5
+
6
+ from typing import Dict, Any, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+
11
+ def submit_scan_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string to scan"],
13
+ scan_settings: Annotated[str, "JSON string of scan parameters: '{\"type\": \"dihedral\"/\"bond\"/\"angle\", \"atoms\": [1-indexed], \"start\": value, \"stop\": value, \"num\": points}'"] = "",
14
+ calculation_engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
15
+ calculation_method: Annotated[str, "Computational method (e.g., 'uma_m_omol', 'gfn2-xtb', 'b3lyp-d3bj')"] = "uma_m_omol",
16
+ wavefront_propagation: Annotated[bool, "Whether to use wavefront propagation for scan"] = True,
17
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Scan Workflow",
18
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
19
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
20
+ ):
21
+ """Submit a potential energy surface scan workflow using Rowan v2 API.
22
+
23
+ Args:
24
+ initial_molecule: SMILES string to scan
25
+ scan_settings: JSON string of scan parameters: '{"type": "dihedral"/"bond"/"angle", "atoms": [1-indexed], "start": value, "stop": value, "num": points}'
26
+ calculation_engine: Computational engine: 'omol25', 'xtb', or 'psi4'
27
+ calculation_method: Calculation method (depends on engine): 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c'
28
+ wavefront_propagation: Use previous scan point geometries as starting points for faster convergence
29
+ name: Workflow name for identification and tracking
30
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
31
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
32
+
33
+ Performs systematic scans along specified molecular coordinates (bonds, angles,
34
+ or dihedrals) to map the potential energy surface.
35
+
36
+ Returns:
37
+ Workflow object representing the submitted workflow
38
+
39
+ Example:
40
+ # Water angle scan
41
+ result = submit_scan_workflow(
42
+ initial_molecule="O", # Water SMILES
43
+ name="Water Angle scan",
44
+ scan_settings='{"type": "angle", "atoms": [2, 1, 3], "start": 100, "stop": 110, "num": 5}',
45
+ calculation_method="GFN2-xTB",
46
+ calculation_engine="xtb"
47
+ )
48
+ """
49
+ # Parse scan_settings if provided
50
+ parsed_scan_settings = None
51
+ if scan_settings:
52
+ try:
53
+ parsed_scan_settings = json.loads(scan_settings)
54
+ except (json.JSONDecodeError, ValueError) as e:
55
+ raise ValueError(f"Invalid scan_settings format: {e}")
56
+
57
+ # Validate and convert scan_settings
58
+ if parsed_scan_settings is not None:
59
+ # Convert step to num if step is provided instead of num
60
+ if 'step' in parsed_scan_settings and 'num' not in parsed_scan_settings:
61
+ start = parsed_scan_settings.get('start', 0)
62
+ stop = parsed_scan_settings.get('stop', 360)
63
+ step = parsed_scan_settings['step']
64
+ parsed_scan_settings['num'] = int((stop - start) / step) + 1
65
+ del parsed_scan_settings['step'] # Remove step as API doesn't accept it
66
+
67
+ # Validate required fields
68
+ required_fields = ['type', 'atoms', 'start', 'stop', 'num']
69
+ missing_fields = [field for field in required_fields if field not in parsed_scan_settings]
70
+ if missing_fields:
71
+ raise ValueError(f"Missing required fields in scan_settings: {missing_fields}")
72
+
73
+ return rowan.submit_scan_workflow(
74
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
75
+ scan_settings=parsed_scan_settings,
76
+ calculation_engine=calculation_engine,
77
+ calculation_method=calculation_method,
78
+ wavefront_propagation=wavefront_propagation,
79
+ name=name,
80
+ folder_uuid=folder_uuid if folder_uuid else None,
81
+ max_credits=max_credits if max_credits > 0 else None
82
+ )
@@ -0,0 +1,157 @@
1
+ """
2
+ Rowan v2 API: Solubility Workflow
3
+ Predict molecular solubility in various solvents at different temperatures.
4
+ """
5
+
6
+ from typing import List, Annotated
7
+ import rowan
8
+ import json
9
+
10
+
11
+ def submit_solubility_workflow(
12
+ initial_smiles: Annotated[str, "SMILES string of the molecule for solubility prediction"],
13
+ solvents: Annotated[str, "JSON string list of solvents as SMILES or names (e.g., '[\"water\", \"ethanol\", \"CCO\"]'). Empty string uses defaults"] = "",
14
+ temperatures: Annotated[str, "JSON string list of temperatures in Kelvin (e.g., '[298.15, 310.15]'). Empty string uses default range"] = "",
15
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Solubility Workflow",
16
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
17
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
18
+ ):
19
+ """Submit a solubility prediction workflow using Rowan v2 API.
20
+
21
+ Args:
22
+ initial_smiles: SMILES string of the molecule for solubility prediction
23
+ solvents: JSON string list of solvents as SMILES or names (e.g., '["water", "ethanol", "CCO"]'). Empty string uses defaults
24
+ temperatures: JSON string list of temperatures in Kelvin (e.g., '[298.15, 310.15]'). Empty string uses default range
25
+ name: Workflow name for identification and tracking
26
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
27
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
28
+
29
+ Predicts solubility (log S) of a molecule in multiple solvents at various temperatures
30
+ using machine learning models.
31
+
32
+ Returns:
33
+ Workflow object representing the submitted workflow
34
+
35
+ Example:
36
+ # Basic solubility prediction
37
+ result = submit_solubility_workflow(
38
+ initial_smiles="CC(=O)Nc1ccc(O)cc1",
39
+ solvents='["water", "ethanol"]',
40
+ temperatures='[298.15, 310.15]'
41
+ )
42
+
43
+ # With SMILES solvents
44
+ result = submit_solubility_workflow(
45
+ initial_smiles="CC(=O)O",
46
+ solvents='["O", "CCO", "CCCCCC"]',
47
+ temperatures='[273.15, 298.15, 323.15]'
48
+ )
49
+ """
50
+
51
+ # Parse solvents parameter - handle string input
52
+ parsed_solvents = None
53
+ if solvents:
54
+ # Handle various string formats
55
+ solvents = solvents.strip()
56
+ if solvents.startswith('[') and solvents.endswith(']'):
57
+ # JSON array format like '["water", "ethanol"]'
58
+ try:
59
+ parsed_solvents = json.loads(solvents)
60
+ except (json.JSONDecodeError, ValueError):
61
+ # Failed to parse as JSON, try as comma-separated
62
+ solvents = solvents.strip('[]').replace('"', '').replace("'", "")
63
+ parsed_solvents = [s.strip() for s in solvents.split(',') if s.strip()]
64
+ elif ',' in solvents:
65
+ # Comma-separated format like 'water, ethanol'
66
+ parsed_solvents = [s.strip() for s in solvents.split(',') if s.strip()]
67
+ else:
68
+ # Single solvent as string like 'water'
69
+ parsed_solvents = [solvents]
70
+
71
+ # Convert solvent names to SMILES if needed
72
+ solvent_name_to_smiles = {
73
+ 'water': 'O',
74
+ 'ethanol': 'CCO',
75
+ 'dmso': 'CS(=O)C',
76
+ 'acetone': 'CC(=O)C',
77
+ 'methanol': 'CO',
78
+ 'chloroform': 'C(Cl)(Cl)Cl',
79
+ 'dichloromethane': 'C(Cl)Cl',
80
+ 'toluene': 'Cc1ccccc1',
81
+ 'benzene': 'c1ccccc1',
82
+ 'hexane': 'CCCCCC',
83
+ 'ether': 'CCOCC',
84
+ 'diethyl ether': 'CCOCC',
85
+ 'thf': 'C1CCOC1',
86
+ 'tetrahydrofuran': 'C1CCOC1',
87
+ 'dioxane': 'C1COCCO1',
88
+ 'acetonitrile': 'CC#N',
89
+ 'pyridine': 'c1ccncc1'
90
+ }
91
+
92
+ converted_solvents = []
93
+ for solvent in parsed_solvents:
94
+ solvent_lower = solvent.lower().strip()
95
+ if solvent_lower in solvent_name_to_smiles:
96
+ converted_solvents.append(solvent_name_to_smiles[solvent_lower])
97
+ else:
98
+ # Assume it's already a SMILES string or use as-is
99
+ converted_solvents.append(solvent)
100
+ parsed_solvents = converted_solvents
101
+
102
+ # Validate the final solvent SMILES to catch issues early
103
+ try:
104
+ from rdkit import Chem
105
+ for i, solvent_smiles in enumerate(parsed_solvents):
106
+ mol = Chem.MolFromSmiles(solvent_smiles)
107
+ if mol is None:
108
+ raise ValueError(f"Invalid solvent SMILES: '{solvent_smiles}' at position {i}")
109
+ except ImportError:
110
+ # RDKit not available for validation, proceed anyway
111
+ pass
112
+
113
+ # Parse temperatures parameter - handle string input
114
+ parsed_temperatures = None
115
+ if temperatures:
116
+ # Handle various string formats
117
+ temperatures = temperatures.strip()
118
+ if temperatures.startswith('[') and temperatures.endswith(']'):
119
+ # JSON array format like '[298.15, 310.15]'
120
+ try:
121
+ parsed_temperatures = json.loads(temperatures)
122
+ except (json.JSONDecodeError, ValueError):
123
+ # Failed to parse as JSON, try as comma-separated
124
+ temperatures = temperatures.strip('[]').replace('"', '').replace("'", "")
125
+ parsed_temperatures = [float(t.strip()) for t in temperatures.split(',') if t.strip()]
126
+ elif ',' in temperatures:
127
+ # Comma-separated format like '298.15, 310.15'
128
+ parsed_temperatures = [float(t.strip()) for t in temperatures.split(',') if t.strip()]
129
+ else:
130
+ # Single temperature as string like '298.15'
131
+ parsed_temperatures = [float(temperatures)]
132
+
133
+ # Validate the main SMILES string early to catch issues
134
+ try:
135
+ from rdkit import Chem
136
+ mol = Chem.MolFromSmiles(initial_smiles)
137
+ if mol is None:
138
+ raise ValueError(f"Invalid initial SMILES: '{initial_smiles}'")
139
+ except ImportError:
140
+ # RDKit not available for validation, proceed anyway
141
+ pass
142
+
143
+ try:
144
+ result = rowan.submit_solubility_workflow(
145
+ initial_smiles=initial_smiles,
146
+ solvents=parsed_solvents,
147
+ temperatures=parsed_temperatures,
148
+ name=name,
149
+ folder_uuid=folder_uuid if folder_uuid else None,
150
+ max_credits=max_credits if max_credits > 0 else None
151
+ )
152
+
153
+ return result
154
+
155
+ except Exception as e:
156
+ # Re-raise the exception so MCP can handle it
157
+ raise
@@ -0,0 +1,51 @@
1
+ """
2
+ Rowan v2 API: Tautomer Search Workflow
3
+ Search for tautomeric forms of molecules.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+ def submit_tautomer_search_workflow(
11
+ initial_molecule: Annotated[str, "SMILES string to search for tautomers"],
12
+ mode: Annotated[str, "Search mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)"] = "careful",
13
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Tautomer Search Workflow",
14
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
15
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
16
+ ):
17
+ """Submit a tautomer search workflow using Rowan v2 API.
18
+
19
+ Args:
20
+ initial_molecule: SMILES string to search for tautomers
21
+ mode: Search mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
22
+ name: Workflow name for identification and tracking
23
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
24
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
25
+
26
+ Searches for different tautomeric forms of a molecule and evaluates their
27
+ relative stabilities. Tautomers are structural isomers that readily interconvert.
28
+
29
+ Returns:
30
+ Workflow object representing the submitted workflow
31
+
32
+ Example:
33
+ # Basic tautomer search
34
+ result = submit_tautomer_search_workflow(
35
+ initial_molecule="CC(=O)CC(=O)C"
36
+ )
37
+
38
+ # Meticulous search for complex molecule
39
+ result = submit_tautomer_search_workflow(
40
+ initial_molecule="c1ccc2c(c1)ncc(=O)[nH]2",
41
+ mode="meticulous"
42
+ )
43
+ """
44
+
45
+ return rowan.submit_tautomer_search_workflow(
46
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
47
+ mode=mode,
48
+ name=name,
49
+ folder_uuid=folder_uuid if folder_uuid else None,
50
+ max_credits=max_credits if max_credits > 0 else None
51
+ )