rowan-mcp 1.0.2__py3-none-any.whl → 2.0.1__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 (57) hide show
  1. rowan_mcp/__init__.py +1 -1
  2. rowan_mcp/__main__.py +3 -5
  3. rowan_mcp/functions/admet.py +0 -5
  4. rowan_mcp/functions/bde.py +1 -8
  5. rowan_mcp/functions/conformers.py +1 -4
  6. rowan_mcp/functions/descriptors.py +1 -4
  7. rowan_mcp/functions/docking.py +6 -56
  8. rowan_mcp/functions/electronic_properties.py +1 -4
  9. rowan_mcp/functions/folder_management.py +1 -8
  10. rowan_mcp/functions/fukui.py +1 -4
  11. rowan_mcp/functions/hydrogen_bond_basicity.py +1 -8
  12. rowan_mcp/functions/multistage_opt.py +1 -4
  13. rowan_mcp/functions/pka.py +1 -8
  14. rowan_mcp/functions/redox_potential.py +2 -5
  15. rowan_mcp/functions/system_management.py +1 -8
  16. rowan_mcp/functions/tautomers.py +1 -4
  17. rowan_mcp/functions_v2/BENCHMARK.md +86 -0
  18. rowan_mcp/functions_v2/molecule_lookup.py +232 -0
  19. rowan_mcp/functions_v2/protein_management.py +141 -0
  20. rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
  21. rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
  22. rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
  23. rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
  24. rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
  25. rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
  26. rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
  27. rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
  28. rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
  29. rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
  30. rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
  31. rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
  32. rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
  33. rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
  34. rowan_mcp/server.py +109 -144
  35. rowan_mcp/tests/basic_calculation_from_json.py +0 -0
  36. rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
  37. rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
  38. rowan_mcp/tests/bde.py +37 -0
  39. rowan_mcp/tests/benchmark_queries.md +120 -0
  40. rowan_mcp/tests/cofolding_screen.py +131 -0
  41. rowan_mcp/tests/conformer_dependent_redox.py +37 -0
  42. rowan_mcp/tests/conformers.py +31 -0
  43. rowan_mcp/tests/data.json +189 -0
  44. rowan_mcp/tests/docking_screen.py +157 -0
  45. rowan_mcp/tests/irc.py +24 -0
  46. rowan_mcp/tests/macropka.py +13 -0
  47. rowan_mcp/tests/multistage_opt.py +13 -0
  48. rowan_mcp/tests/optimization.py +21 -0
  49. rowan_mcp/tests/phenol_pka.py +36 -0
  50. rowan_mcp/tests/pka.py +36 -0
  51. rowan_mcp/tests/protein_cofolding.py +17 -0
  52. rowan_mcp/tests/scan.py +28 -0
  53. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/METADATA +38 -45
  54. rowan_mcp-2.0.1.dist-info/RECORD +69 -0
  55. rowan_mcp-1.0.2.dist-info/RECORD +0 -34
  56. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/WHEEL +0 -0
  57. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,244 @@
1
+ """
2
+ Rowan v2 API: Docking Workflow
3
+ Perform molecular docking simulations for drug discovery.
4
+ """
5
+
6
+ from typing import Dict, Any, Union, List, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+ from stjames.pdb import PDB, read_pdb
11
+
12
+ def submit_docking_workflow(
13
+ protein: Annotated[str, "Protein UUID or PDB content/path for docking target"],
14
+ pocket: Annotated[str, "JSON string defining binding pocket coordinates or 'auto' for automatic detection"],
15
+ initial_molecule: Annotated[str, "SMILES string of the ligand molecule to dock"],
16
+ do_csearch: Annotated[bool, "Whether to perform conformer search before docking"] = True,
17
+ do_optimization: Annotated[bool, "Whether to optimize docked poses"] = True,
18
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Docking Workflow",
19
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
20
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0,
21
+ blocking: Annotated[bool, "Whether to wait for workflow completion before returning"] = False
22
+ ):
23
+ """Submits a Docking workflow to the API.
24
+
25
+ Args:
26
+ protein: Protein for docking. Can be: 1) PDB ID string (e.g., '1HCK'), 2) Protein UUID string, 3) JSON string dict with 'pdb_id' and optional 'name'
27
+ pocket: Binding pocket as JSON string "[[x1,y1,z1], [x2,y2,z2]]" defining box corners for docking site
28
+ initial_molecule: SMILES string representing the ligand
29
+ do_csearch: Whether to perform conformational search on the ligand before docking
30
+ do_optimization: Whether to optimize the ligand geometry before docking
31
+ name: Workflow name for identification and tracking
32
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
33
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
34
+ blocking: Whether to wait for workflow completion before returning
35
+
36
+ Automatically handles protein creation from PDB ID and sanitization if needed.
37
+
38
+ Returns:
39
+ Workflow object representing the submitted workflow
40
+
41
+ Examples:
42
+ # Example 1: Using PDB ID directly
43
+ result = submit_docking_workflow(
44
+ protein="1HCK", # PDB ID
45
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
46
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1",
47
+ name="CDK2 Docking"
48
+ )
49
+
50
+ # Example 2: Using dict with PDB ID and custom name
51
+ result = submit_docking_workflow(
52
+ protein='{"pdb_id": "1HCK", "name": "My CDK2 Protein"}',
53
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
54
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
55
+ )
56
+
57
+ # Example 3: Using existing protein UUID
58
+ result = submit_docking_workflow(
59
+ protein="abc123-def456-...", # Protein UUID
60
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
61
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
62
+ )
63
+ """
64
+ import logging
65
+ logger = logging.getLogger(__name__)
66
+
67
+ # Handle protein parameter
68
+ protein_obj = None
69
+
70
+ # Try to parse protein as JSON first
71
+ try:
72
+ protein_dict = json.loads(protein)
73
+ if isinstance(protein_dict, dict):
74
+ protein = protein_dict
75
+ except (json.JSONDecodeError, ValueError):
76
+ # Not JSON, keep as string
77
+ pass
78
+
79
+ # Check if protein is a PDB ID or dict with PDB ID
80
+ if isinstance(protein, str):
81
+ # Check if it's a UUID (36 chars with dashes) or PDB ID (4 chars)
82
+ if len(protein) == 36 and '-' in protein:
83
+ # It's a UUID, retrieve the protein
84
+ logger.info(f"Using existing protein UUID: {protein}")
85
+ protein_obj = rowan.retrieve_protein(protein)
86
+ elif len(protein) <= 6: # PDB IDs are typically 4 characters
87
+ # It's a PDB ID, create protein from it
88
+ logger.info(f"Creating protein from PDB ID: {protein}")
89
+
90
+ # Get or create a project (REQUIRED for v2.1.1)
91
+ project_uuid = None
92
+ try:
93
+ # Try to get default project
94
+ project = rowan.default_project()
95
+ project_uuid = project.uuid
96
+ logger.info(f"Using default project: {project_uuid}")
97
+ except Exception as e:
98
+ logger.info(f"Could not get default project: {e}")
99
+ try:
100
+ # List existing projects and use the first one
101
+ projects = rowan.list_projects(size=1)
102
+ if projects:
103
+ project_uuid = projects[0].uuid
104
+ logger.info(f"Using existing project: {project_uuid}")
105
+ else:
106
+ # Create a new project if none exist
107
+ new_project = rowan.create_project(name="Docking Project")
108
+ project_uuid = new_project.uuid
109
+ logger.info(f"Created new project: {project_uuid}")
110
+ except Exception as e2:
111
+ logger.error(f"Failed to get/create project: {e2}")
112
+ raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
113
+
114
+ # Create protein with REQUIRED project_uuid
115
+ protein_obj = rowan.create_protein_from_pdb_id(
116
+ name=f"Protein from {protein}",
117
+ code=protein,
118
+ project_uuid=project_uuid
119
+ )
120
+ logger.info(f"Created protein with UUID: {protein_obj.uuid}")
121
+
122
+ # Sanitize the protein for docking
123
+ logger.info("Sanitizing protein for docking...")
124
+ try:
125
+ protein_obj.sanitize()
126
+
127
+ # Wait for sanitization to complete
128
+ import time
129
+ max_wait = 30
130
+ start_time = time.time()
131
+ while time.time() - start_time < max_wait:
132
+ time.sleep(2)
133
+ protein_obj.refresh()
134
+ if protein_obj.sanitized and protein_obj.sanitized != 0:
135
+ logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
136
+ break
137
+ else:
138
+ logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
139
+ except Exception as e:
140
+ logger.warning(f"Sanitization failed: {e}")
141
+ logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
142
+ else:
143
+ raise ValueError(f"Invalid protein parameter: {protein}. Expected PDB ID (4 chars) or UUID (36 chars)")
144
+
145
+ elif isinstance(protein, dict):
146
+ # Dict with PDB ID and optional name
147
+ pdb_id = protein.get('pdb_id')
148
+ protein_name = protein.get('name', f"Protein from {pdb_id}")
149
+
150
+ if not pdb_id:
151
+ raise ValueError("Dict protein parameter must include 'pdb_id' key")
152
+
153
+ logger.info(f"Creating protein '{protein_name}' from PDB ID: {pdb_id}")
154
+
155
+ # Get or create a project (REQUIRED for v2.1.1)
156
+ project_uuid = None
157
+ try:
158
+ # Try to get default project
159
+ project = rowan.default_project()
160
+ project_uuid = project.uuid
161
+ logger.info(f"Using default project: {project_uuid}")
162
+ except Exception as e:
163
+ logger.info(f"Could not get default project: {e}")
164
+ try:
165
+ # List existing projects and use the first one
166
+ projects = rowan.list_projects(size=1)
167
+ if projects:
168
+ project_uuid = projects[0].uuid
169
+ logger.info(f"Using existing project: {project_uuid}")
170
+ else:
171
+ # Create a new project if none exist
172
+ new_project = rowan.create_project(name="Docking Project")
173
+ project_uuid = new_project.uuid
174
+ logger.info(f"Created new project: {project_uuid}")
175
+ except Exception as e2:
176
+ logger.error(f"Failed to get/create project: {e2}")
177
+ raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
178
+
179
+ # Create protein with REQUIRED project_uuid
180
+ protein_obj = rowan.create_protein_from_pdb_id(
181
+ name=protein_name,
182
+ code=pdb_id,
183
+ project_uuid=project_uuid
184
+ )
185
+ logger.info(f"Created protein with UUID: {protein_obj.uuid}")
186
+
187
+ # Sanitize the protein
188
+ logger.info("Sanitizing protein for docking...")
189
+ try:
190
+ protein_obj.sanitize()
191
+
192
+ # Wait for sanitization
193
+ import time
194
+ max_wait = 30
195
+ start_time = time.time()
196
+ while time.time() - start_time < max_wait:
197
+ time.sleep(2)
198
+ protein_obj.refresh()
199
+ if protein_obj.sanitized and protein_obj.sanitized != 0:
200
+ logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
201
+ break
202
+ else:
203
+ logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
204
+ except Exception as e:
205
+ logger.warning(f"Sanitization failed: {e}")
206
+ logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
207
+
208
+ else:
209
+ # Assume it's already a protein object
210
+ protein_obj = protein
211
+
212
+ # Parse pocket parameter (always a string in simplified version)
213
+ try:
214
+ pocket = json.loads(pocket)
215
+ except (json.JSONDecodeError, ValueError) as e:
216
+ raise ValueError(f"Invalid pocket format: {pocket}. Expected JSON string like \"[[x1,y1,z1], [x2,y2,z2]]\"")
217
+
218
+ # Ensure pocket is a list of lists
219
+ if not isinstance(pocket, list) or len(pocket) != 2:
220
+ raise ValueError(f"Pocket must be a list with exactly 2 coordinate lists")
221
+
222
+ # Ensure each element is a list of floats
223
+ pocket = [list(coord) for coord in pocket]
224
+
225
+ # Submit the workflow
226
+ logger.info(f"Submitting docking workflow: {name}")
227
+ workflow = rowan.submit_docking_workflow(
228
+ protein=protein_obj,
229
+ pocket=pocket,
230
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
231
+ do_csearch=do_csearch,
232
+ do_optimization=do_optimization,
233
+ name=name,
234
+ folder_uuid=folder_uuid if folder_uuid else None,
235
+ max_credits=max_credits if max_credits > 0 else None
236
+ )
237
+
238
+ logger.info(f"Docking workflow submitted with UUID: {workflow.uuid}")
239
+
240
+ # If blocking, wait for completion
241
+ if blocking:
242
+ workflow.wait_for_result()
243
+
244
+ return workflow
@@ -0,0 +1,114 @@
1
+ """
2
+ Rowan v2 API: Fukui Workflow
3
+ Calculate Fukui indices for reactivity analysis.
4
+ """
5
+
6
+ from typing import Dict, Any, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+
11
+ def submit_fukui_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string of the molecule to calculate Fukui indices for"],
13
+ optimization_method: Annotated[str, "Method for geometry optimization (e.g., 'gfn2_xtb', 'uma_m_omol')"] = "gfn2_xtb",
14
+ fukui_method: Annotated[str, "Method for Fukui indices calculation (e.g., 'gfn1_xtb', 'gfn2_xtb')"] = "gfn1_xtb",
15
+ solvent_settings: Annotated[str, "JSON string for solvent settings. Empty string for vacuum"] = "",
16
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Fukui 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 Fukui indices calculation workflow using Rowan v2 API.
21
+
22
+ Args:
23
+ initial_molecule: SMILES string for Fukui analysis
24
+ optimization_method: Method for geometry optimization. Options: 'gfn2_xtb', 'r2scan_3c', 'aimnet2_wb97md3'
25
+ fukui_method: Method for Fukui calculation. Options: 'gfn1_xtb', 'gfn2_xtb'
26
+ solvent_settings: Solvent configuration JSON string, e.g., '{"solvent": "water", "model": "alpb"}'. Empty for gas phase.
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 Fukui indices to predict molecular reactivity at different sites.
32
+ Fukui indices indicate susceptibility to nucleophilic/electrophilic attack.
33
+
34
+ Returns:
35
+ Workflow object representing the submitted workflow
36
+
37
+ Example:
38
+ # Basic Fukui indices
39
+ result = submit_fukui_workflow(
40
+ initial_molecule="CC(=O)O"
41
+ )
42
+
43
+ # With solvent and advanced methods
44
+ result = submit_fukui_workflow(
45
+ initial_molecule="c1ccccc1N",
46
+ optimization_method="r2scan_3c",
47
+ fukui_method="gfn2_xtb",
48
+ solvent_settings='{"solvent": "water", "model": "alpb"}'
49
+ )
50
+ """
51
+ # Parse solvent_settings if provided
52
+ parsed_solvent_settings = None
53
+ if solvent_settings:
54
+ try:
55
+ parsed_solvent_settings = json.loads(solvent_settings)
56
+ except (json.JSONDecodeError, ValueError):
57
+ pass
58
+
59
+ try:
60
+ # Convert initial_molecule to StJamesMolecule
61
+ molecule = stjames.Molecule.from_smiles(initial_molecule)
62
+ initial_molecule_dict = molecule.model_dump()
63
+
64
+ # Create Settings objects
65
+ optimization_settings = stjames.Settings(method=optimization_method)
66
+ fukui_settings = stjames.Settings(method=fukui_method, solvent_settings=parsed_solvent_settings)
67
+
68
+ # Serialize to dicts
69
+ opt_settings_dict = optimization_settings.model_dump(mode="json")
70
+ fukui_settings_dict = fukui_settings.model_dump(mode="json")
71
+
72
+ # Fix soscf boolean to string enum conversion for optimization settings
73
+ if 'scf_settings' in opt_settings_dict and 'soscf' in opt_settings_dict['scf_settings']:
74
+ soscf_val = opt_settings_dict['scf_settings']['soscf']
75
+ if isinstance(soscf_val, bool):
76
+ if soscf_val is False:
77
+ opt_settings_dict['scf_settings']['soscf'] = 'never'
78
+ elif soscf_val is True:
79
+ opt_settings_dict['scf_settings']['soscf'] = 'always'
80
+
81
+ # Fix soscf boolean to string enum conversion for fukui settings
82
+ if 'scf_settings' in fukui_settings_dict and 'soscf' in fukui_settings_dict['scf_settings']:
83
+ soscf_val = fukui_settings_dict['scf_settings']['soscf']
84
+ if isinstance(soscf_val, bool):
85
+ if soscf_val is False:
86
+ fukui_settings_dict['scf_settings']['soscf'] = 'never'
87
+ elif soscf_val is True:
88
+ fukui_settings_dict['scf_settings']['soscf'] = 'always'
89
+
90
+ workflow_data = {
91
+ "opt_settings": opt_settings_dict,
92
+ "opt_engine": stjames.Method(optimization_method).default_engine(),
93
+ "fukui_settings": fukui_settings_dict,
94
+ "fukui_engine": stjames.Method(fukui_method).default_engine(),
95
+ }
96
+
97
+ # Build the API request payload
98
+ data = {
99
+ "name": name,
100
+ "folder_uuid": folder_uuid if folder_uuid else None,
101
+ "workflow_type": "fukui",
102
+ "workflow_data": workflow_data,
103
+ "initial_molecule": initial_molecule_dict,
104
+ "max_credits": max_credits if max_credits > 0 else None,
105
+ }
106
+
107
+ # Submit to API
108
+ with rowan.api_client() as client:
109
+ response = client.post("/workflow", json=data)
110
+ response.raise_for_status()
111
+ return rowan.Workflow(**response.json())
112
+
113
+ except Exception as e:
114
+ raise e
@@ -0,0 +1,58 @@
1
+ """
2
+ Rowan v2 API: IRC Workflow
3
+ Perform Intrinsic Reaction Coordinate calculations to trace reaction paths.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+ def submit_irc_workflow(
11
+ initial_molecule: Annotated[str, "SMILES string for IRC calculation"],
12
+ method: Annotated[str, "Computational method for IRC (e.g., 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c')"] = "uma_m_omol",
13
+ engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
14
+ preopt: Annotated[bool, "Whether to pre-optimize the transition state before IRC step"] = True,
15
+ step_size: Annotated[float, "Step size for IRC path tracing in Bohr (typically 0.03-0.1)"] = 0.05,
16
+ max_irc_steps: Annotated[int, "Maximum number of IRC steps in each direction from TS"] = 30,
17
+ name: Annotated[str, "Workflow name for identification and tracking"] = "IRC 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
+ """Submits an Intrinsic Reaction Coordinate (IRC) workflow to the API.
22
+
23
+ Args:
24
+ initial_molecule: SMILES string for IRC calculation
25
+ method: Computational method for IRC. Options: 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c'
26
+ engine: Computational engine. Options: 'omol25', 'xtb', 'psi4'
27
+ preopt: Whether to pre-optimize the transition state before IRC
28
+ step_size: Step size for IRC path tracing in Bohr (typically 0.03-0.1)
29
+ max_irc_steps: Maximum number of IRC steps in each direction from TS
30
+ name: Workflow name for identification and tracking
31
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
32
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
33
+
34
+ Returns:
35
+ Workflow object representing the submitted IRC workflow
36
+
37
+ Example:
38
+ # IRC from SMILES
39
+ result = submit_irc_workflow(
40
+ initial_molecule="N=C([O-])[OH2+]", # Transition state SMILES
41
+ name="HNCO + H₂O - IRC",
42
+ preopt=True, # Pre-optimize TS
43
+ method="gfn2_xtb",
44
+ engine="xtb"
45
+ )
46
+ """
47
+
48
+ return rowan.submit_irc_workflow(
49
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
50
+ method=method,
51
+ engine=engine,
52
+ preopt=preopt,
53
+ step_size=step_size,
54
+ max_irc_steps=max_irc_steps,
55
+ name=name,
56
+ folder_uuid=folder_uuid if folder_uuid else None,
57
+ max_credits=max_credits if max_credits > 0 else None
58
+ )
@@ -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
+ )