rowan-mcp 1.0.1__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 +2 -2
  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.1.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +49 -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 -137
  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.1.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
rowan_mcp/__init__.py CHANGED
@@ -5,8 +5,8 @@ This package provides MCP (Model Context Protocol) server functionality
5
5
  for integrating with Rowan's computational chemistry platform.
6
6
  """
7
7
 
8
- __version__ = "1.0.0"
9
- __author__ = "Rowan MCP Team"
8
+ __version__ = "1.0.2"
9
+ __author__ = "Kat Yenko"
10
10
  __description__ = "MCP server for Rowan computational chemistry platform"
11
11
 
12
12
  from .server import main
rowan_mcp/__main__.py CHANGED
@@ -2,13 +2,11 @@
2
2
  Main entry point for Rowan MCP Server when run as a module.
3
3
 
4
4
  Usage:
5
- python -m src # STDIO mode (default)
6
- python -m src --stdio # STDIO mode
7
- python -m src --http # HTTP mode
8
- python -m src --help # Show help
5
+ python -m rowan_mcp # HTTP/SSE mode
6
+ python -m rowan_mcp --help # Show help
9
7
  """
10
8
 
11
9
  if __name__ == "__main__":
12
- # All modes now handled by the unified server
10
+ # HTTP transport only
13
11
  from .server import main
14
12
  main()
@@ -0,0 +1,86 @@
1
+ # Rowan MCP Benchmark Suite
2
+
3
+ ## Overview
4
+ Systematic evaluation of the Rowan MCP server's ability to handle chemistry workflows through natural language queries.
5
+
6
+ ## Evaluation Tiers
7
+
8
+ ### Tier 1: Single Tool Calls
9
+ **Tests**: Basic tool invocation and parameter passing
10
+ **Characteristics**:
11
+ - Single workflow submission
12
+ - Explicit parameters
13
+ - No dependencies
14
+ - Direct SMILES or common molecule names
15
+
16
+ **Example Queries**:
17
+ - "Calculate the pKa of phenol"
18
+ - "Optimize water geometry with GFN2-xTB"
19
+ - "Find conformers of ethanol"
20
+
21
+ ### Tier 2: Parameter Interpretation
22
+ **Tests**: Natural language to parameter mapping, molecule name resolution
23
+ **Characteristics**:
24
+ - Requires interpreting descriptive terms into API parameters
25
+ - Mode selection (rapid/careful/meticulous)
26
+ - Element specification by name vs atomic number
27
+ - Common name to SMILES conversion
28
+
29
+ **Example Queries**:
30
+ - "Calculate the oxidation potential of caffeine using careful mode"
31
+ - "Find the pKa of aspirin, only considering oxygen atoms"
32
+ - "Dock ibuprofen to CDK2 without optimization"
33
+
34
+ ### Tier 3: Batch Operations
35
+ **Tests**: Multiple independent calculations, result organization
36
+ **Characteristics**:
37
+ - Multiple molecules or methods
38
+ - Parallel workflow submission
39
+ - Result comparison/aggregation
40
+ - Folder organization
41
+
42
+ **Example Queries**:
43
+ - "Calculate pKa for phenol, p-nitrophenol, and p-chlorophenol"
44
+ - "Optimize butane with GFN2-xTB, UMA, and R2SCAN-3c methods"
45
+ - "Screen 5 molecules for docking against CDK2"
46
+
47
+ ### Tier 4: Workflow Chaining
48
+ **Tests**: Sequential dependent calculations, data extraction from results
49
+ **Characteristics**:
50
+ - Output from one workflow feeds into next
51
+ - Requires waiting for completion
52
+ - UUID and result extraction
53
+ - Proper async handling
54
+
55
+ **Example Queries**:
56
+ - "Find conformers of benzophenone, then calculate redox potential for top 3"
57
+ - "Optimize this transition state, then run IRC from the result"
58
+ - "Calculate pKa, then run conformer search at the predicted pKa value"
59
+
60
+ ### Tier 5: Conditional Logic
61
+ **Tests**: Decision-making based on results, complex multi-step analysis
62
+ **Characteristics**:
63
+ - Conditional branching based on results
64
+ - Threshold-based decisions
65
+ - Error handling and retries
66
+ - Statistical analysis of results
67
+
68
+ **Example Queries**:
69
+ - "Screen molecules for docking, only run detailed analysis if score < -8.0"
70
+ - "Calculate conformer energies, identify outliers (>2 kcal/mol from lowest), recalculate outliers with meticulous mode"
71
+ - "Find pKa sites, if any are between 6-8, run pH-dependent calculations at those values"
72
+
73
+ ## Scoring Criteria
74
+
75
+ ### Per Query
76
+ - **Success**: Workflow submitted correctly (1 point)
77
+ - **Parameters**: All parameters correctly mapped (1 point)
78
+ - **Completion**: Workflow completes without error (1 point)
79
+ - **Chaining**: Dependencies handled correctly (1 point, Tier 4-5 only)
80
+ - **Logic**: Conditional logic executed correctly (1 point, Tier 5 only)
81
+
82
+ ### Overall Metrics
83
+ - Success rate per tier
84
+ - Average time to completion
85
+ - Error recovery rate
86
+ - Parameter accuracy rate
@@ -0,0 +1,232 @@
1
+ """
2
+ Molecule name to SMILES converter using Chemical Identifier Resolver (CIR).
3
+ Enables natural language molecule input for Rowan workflows.
4
+ """
5
+
6
+ from typing import List, Dict, Annotated
7
+ from urllib.request import urlopen
8
+ from urllib.parse import quote
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def molecule_lookup(
15
+ molecule_name: Annotated[str, "Common name, IUPAC name, or CAS number of molecule (e.g., 'aspirin', 'caffeine', '50-78-2')"],
16
+ fallback_to_input: Annotated[bool, "If lookup fails, return the input string assuming it might be SMILES"] = False
17
+ ) -> str:
18
+ """Convert molecule names to SMILES using Chemical Identifier Resolver (CIR).
19
+
20
+ Args:
21
+ molecule_name: Common name, IUPAC name, or CAS number of molecule (e.g., 'aspirin', 'caffeine', '50-78-2')
22
+ fallback_to_input: If lookup fails, return the input string assuming it might be SMILES
23
+
24
+ This tool enables natural language input for molecules by converting common names,
25
+ IUPAC names, CAS numbers, and other identifiers to SMILES strings that can be
26
+ used with Rowan workflows.
27
+
28
+ Supported Input Types:
29
+ - Common names: 'aspirin', 'caffeine', 'benzene', 'glucose'
30
+ - IUPAC names: '2-acetoxybenzoic acid', '1,3,7-trimethylpurine-2,6-dione'
31
+ - CAS numbers: '50-78-2' (aspirin), '58-08-2' (caffeine)
32
+ - InChI strings
33
+ - Already valid SMILES (will be validated)
34
+
35
+ Returns:
36
+ SMILES string if successful, error message if not found
37
+
38
+ Examples:
39
+ # Common drug name
40
+ result = molecule_lookup("aspirin")
41
+ # Returns: "CC(=O)Oc1ccccc1C(=O)O"
42
+
43
+ # IUPAC name
44
+ result = molecule_lookup("2-acetoxybenzoic acid")
45
+ # Returns: "CC(=O)Oc1ccccc1C(=O)O"
46
+
47
+ # CAS number
48
+ result = molecule_lookup("50-78-2")
49
+ # Returns: "CC(=O)Oc1ccccc1C(=O)O"
50
+
51
+ # Complex molecule
52
+ result = molecule_lookup("paracetamol")
53
+ # Returns: "CC(=O)Nc1ccc(O)cc1"
54
+ """
55
+ try:
56
+ # Clean input
57
+ molecule_name = molecule_name.strip()
58
+
59
+ # Check if already SMILES-like (contains typical SMILES characters)
60
+ smiles_chars = {'=', '#', '(', ')', '[', ']', '@', '+', '-'}
61
+ if any(char in molecule_name for char in smiles_chars):
62
+ logger.info(f"Input '{molecule_name}' appears to be SMILES, returning as-is")
63
+ return molecule_name
64
+
65
+ # Query CIR service
66
+ logger.info(f"Looking up molecule: {molecule_name}")
67
+ url = f'http://cactus.nci.nih.gov/chemical/structure/{quote(molecule_name)}/smiles'
68
+
69
+ response = urlopen(url, timeout=10)
70
+ smiles = response.read().decode('utf8').strip()
71
+
72
+ # CIR may return multiple SMILES for some queries, take the first one
73
+ if '\n' in smiles:
74
+ smiles = smiles.split('\n')[0]
75
+
76
+ logger.info(f"Successfully converted '{molecule_name}' to SMILES: {smiles}")
77
+ return smiles
78
+
79
+ except Exception as e:
80
+ logger.warning(f"Failed to lookup '{molecule_name}': {e}")
81
+
82
+ if fallback_to_input:
83
+ logger.info(f"Returning original input as fallback: {molecule_name}")
84
+ return molecule_name
85
+ else:
86
+ return f"Could not find SMILES for '{molecule_name}'. Please check the name or provide a valid SMILES string."
87
+
88
+
89
+ def batch_molecule_lookup(
90
+ molecule_names: Annotated[List[str], "List of molecule names to convert to SMILES"],
91
+ skip_failures: Annotated[bool, "Skip molecules that fail lookup instead of stopping"] = True
92
+ ) -> Dict[str, str]:
93
+ """Convert multiple molecule names to SMILES in batch.
94
+
95
+ Args:
96
+ molecule_names: List of molecule names to convert to SMILES
97
+ skip_failures: Skip molecules that fail lookup instead of stopping
98
+
99
+ Useful for preparing multiple molecules for workflows or screening.
100
+
101
+ Returns:
102
+ Dictionary mapping input names to SMILES strings (or error messages)
103
+
104
+ Examples:
105
+ # Drug screening set
106
+ result = batch_molecule_lookup([
107
+ "aspirin",
108
+ "ibuprofen",
109
+ "paracetamol",
110
+ "caffeine"
111
+ ])
112
+ # Returns: {
113
+ # "aspirin": "CC(=O)Oc1ccccc1C(=O)O",
114
+ # "ibuprofen": "CC(C)Cc1ccc(C(C)C(=O)O)cc1",
115
+ # "paracetamol": "CC(=O)Nc1ccc(O)cc1",
116
+ # "caffeine": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
117
+ # }
118
+
119
+ # Mixed input types
120
+ result = batch_molecule_lookup([
121
+ "benzene", # Common name
122
+ "50-78-2", # CAS number
123
+ "ethanoic acid" # IUPAC name
124
+ ])
125
+ """
126
+ results = {}
127
+
128
+ for name in molecule_names:
129
+ try:
130
+ smiles = molecule_lookup(name, fallback_to_input=False)
131
+ results[name] = smiles
132
+ except Exception as e:
133
+ error_msg = f"Lookup failed: {str(e)}"
134
+ if skip_failures:
135
+ logger.warning(f"Skipping {name}: {error_msg}")
136
+ results[name] = error_msg
137
+ else:
138
+ raise ValueError(f"Failed to lookup '{name}': {error_msg}")
139
+
140
+ return results
141
+
142
+
143
+ def validate_smiles(
144
+ smiles: Annotated[str, "SMILES string to validate"]
145
+ ) -> Dict[str, any]:
146
+ """Validate a SMILES string and return basic molecular properties.
147
+
148
+ Args:
149
+ smiles: SMILES string to validate
150
+
151
+ Uses RDKit to validate SMILES and extract basic properties.
152
+
153
+ Returns:
154
+ Dictionary with validation status and properties if valid
155
+
156
+ Examples:
157
+ result = validate_smiles("CC(=O)O")
158
+ # Returns: {
159
+ # "valid": True,
160
+ # "canonical_smiles": "CC(=O)O",
161
+ # "molecular_formula": "C2H4O2",
162
+ # "molecular_weight": 60.05
163
+ # }
164
+ """
165
+ try:
166
+ from rdkit import Chem
167
+ from rdkit.Chem import Descriptors
168
+
169
+ mol = Chem.MolFromSmiles(smiles)
170
+
171
+ if mol is None:
172
+ return {
173
+ "valid": False,
174
+ "error": "Invalid SMILES string"
175
+ }
176
+
177
+ return {
178
+ "valid": True,
179
+ "canonical_smiles": Chem.MolToSmiles(mol),
180
+ "molecular_formula": Chem.rdMolDescriptors.CalcMolFormula(mol),
181
+ "molecular_weight": round(Descriptors.MolWt(mol), 2),
182
+ "num_atoms": mol.GetNumAtoms(),
183
+ "num_bonds": mol.GetNumBonds()
184
+ }
185
+
186
+ except ImportError:
187
+ return {
188
+ "valid": "unknown",
189
+ "error": "RDKit not available for validation"
190
+ }
191
+ except Exception as e:
192
+ return {
193
+ "valid": False,
194
+ "error": str(e)
195
+ }
196
+
197
+
198
+ # Common molecules reference (for documentation)
199
+ COMMON_MOLECULES = {
200
+ # Drugs
201
+ "aspirin": "CC(=O)Oc1ccccc1C(=O)O",
202
+ "paracetamol": "CC(=O)Nc1ccc(O)cc1",
203
+ "acetaminophen": "CC(=O)Nc1ccc(O)cc1", # Same as paracetamol
204
+ "ibuprofen": "CC(C)Cc1ccc(C(C)C(=O)O)cc1",
205
+ "caffeine": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C",
206
+ "penicillin": "CC1(C)SC2C(NC(=O)Cc3ccccc3)C(=O)N2C1C(=O)O",
207
+
208
+ # Solvents
209
+ "water": "O",
210
+ "ethanol": "CCO",
211
+ "methanol": "CO",
212
+ "acetone": "CC(=O)C",
213
+ "dmso": "CS(=O)C",
214
+ "chloroform": "C(Cl)(Cl)Cl",
215
+ "benzene": "c1ccccc1",
216
+ "toluene": "Cc1ccccc1",
217
+
218
+ # Organic compounds
219
+ "glucose": "C(C1C(C(C(C(O1)O)O)O)O)O",
220
+ "acetic acid": "CC(=O)O",
221
+ "ethanoic acid": "CC(=O)O", # IUPAC for acetic acid
222
+ "phenol": "Oc1ccccc1",
223
+ "aniline": "Nc1ccccc1",
224
+ "naphthalene": "c1ccc2c(c1)cccc2",
225
+
226
+ # Amino acids
227
+ "glycine": "C(C(=O)O)N",
228
+ "alanine": "CC(C(=O)O)N",
229
+ "valine": "CC(C)C(C(=O)O)N",
230
+ "leucine": "CC(C)CC(C(=O)O)N",
231
+ "lysine": "C(CCN)CC(C(=O)O)N",
232
+ }
@@ -0,0 +1,141 @@
1
+ """
2
+ Rowan v2 API: Protein Management
3
+ Tools for creating, retrieving, and managing protein structures.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Annotated
7
+ import rowan
8
+
9
+
10
+ def create_protein_from_pdb_id(
11
+ name: Annotated[str, "Name for the protein"],
12
+ code: Annotated[str, "PDB ID code (e.g., '1HCK')"]
13
+ ) -> Dict[str, Any]:
14
+ """Create a protein from a PDB ID.
15
+
16
+ Args:
17
+ name: Name for the protein
18
+ code: PDB ID code (e.g., '1HCK')
19
+
20
+ Returns:
21
+ Dictionary containing protein information
22
+ """
23
+ protein = rowan.create_protein_from_pdb_id(name=name, code=code)
24
+
25
+ return {
26
+ "uuid": protein.uuid,
27
+ "name": protein.name,
28
+ "sanitized": protein.sanitized,
29
+ "created_at": str(protein.created_at) if protein.created_at else None
30
+ }
31
+
32
+
33
+ def retrieve_protein(
34
+ uuid: Annotated[str, "UUID of the protein to retrieve"]
35
+ ) -> Dict[str, Any]:
36
+ """Retrieve a protein by UUID.
37
+
38
+ Args:
39
+ uuid: UUID of the protein to retrieve
40
+
41
+ Returns:
42
+ Dictionary containing the protein data
43
+ """
44
+ protein = rowan.retrieve_protein(uuid)
45
+
46
+ return {
47
+ "uuid": protein.uuid,
48
+ "name": protein.name,
49
+ "sanitized": protein.sanitized,
50
+ "created_at": str(protein.created_at) if protein.created_at else None
51
+ }
52
+
53
+
54
+ def list_proteins(
55
+ page: Annotated[int, "Page number (0-indexed)"] = 0,
56
+ size: Annotated[int, "Number per page"] = 20
57
+ ) -> List[Dict[str, Any]]:
58
+ """List proteins.
59
+
60
+ Args:
61
+ page: Page number (0-indexed)
62
+ size: Number per page
63
+
64
+ Returns:
65
+ List of protein dictionaries
66
+ """
67
+ proteins = rowan.list_proteins(page=page, size=size)
68
+
69
+ return [
70
+ {
71
+ "uuid": p.uuid,
72
+ "name": p.name,
73
+ "sanitized": p.sanitized,
74
+ "created_at": str(p.created_at) if p.created_at else None
75
+ }
76
+ for p in proteins
77
+ ]
78
+
79
+
80
+ def upload_protein(
81
+ name: Annotated[str, "Name for the protein"],
82
+ file_path: Annotated[str, "Path to PDB file"]
83
+ ) -> Dict[str, Any]:
84
+ """Upload a protein from a PDB file.
85
+
86
+ Args:
87
+ name: Name for the protein
88
+ file_path: Path to PDB file
89
+
90
+ Returns:
91
+ Dictionary containing protein information
92
+ """
93
+ from pathlib import Path
94
+ protein = rowan.upload_protein(name=name, file_path=Path(file_path))
95
+
96
+ return {
97
+ "uuid": protein.uuid,
98
+ "name": protein.name,
99
+ "sanitized": protein.sanitized,
100
+ "created_at": str(protein.created_at) if protein.created_at else None
101
+ }
102
+
103
+
104
+ def delete_protein(
105
+ uuid: Annotated[str, "UUID of the protein to delete"]
106
+ ) -> Dict[str, str]:
107
+ """Delete a protein.
108
+
109
+ Args:
110
+ uuid: UUID of the protein to delete
111
+
112
+ Returns:
113
+ Dictionary with confirmation message
114
+ """
115
+ protein = rowan.retrieve_protein(uuid)
116
+ protein.delete()
117
+
118
+ return {
119
+ "message": f"Protein {uuid} deleted",
120
+ "uuid": uuid
121
+ }
122
+
123
+
124
+ def sanitize_protein(
125
+ uuid: Annotated[str, "UUID of the protein to sanitize"]
126
+ ) -> Dict[str, Any]:
127
+ """Sanitize a protein for docking.
128
+
129
+ Args:
130
+ uuid: UUID of the protein to sanitize
131
+
132
+ Returns:
133
+ Dictionary with sanitization status
134
+ """
135
+ protein = rowan.retrieve_protein(uuid)
136
+ protein.sanitize()
137
+
138
+ return {
139
+ "uuid": uuid,
140
+ "message": f"Protein {uuid} sanitized"
141
+ }
@@ -0,0 +1,195 @@
1
+ """
2
+ Rowan v2 API: Basic Calculation Workflow
3
+ Submit basic quantum chemistry calculations with various methods and tasks.
4
+ """
5
+
6
+ # Simplified imports - no complex typing needed
7
+ from typing import Annotated
8
+ import rowan
9
+ import stjames
10
+ import json
11
+
12
+
13
+ def submit_basic_calculation_workflow(
14
+ initial_molecule: Annotated[str, "SMILES string or molecule JSON for quantum chemistry calculation"],
15
+ method: Annotated[str, "Computational method (e.g., 'gfn2-xtb', 'uma_m_omol', 'b3lyp-d3bj')"] = "uma_m_omol",
16
+ tasks: Annotated[str, "JSON array or comma-separated list of tasks (e.g., '[\"optimize\"]', 'optimize, frequencies')"] = "",
17
+ mode: Annotated[str, "Calculation mode: 'rapid', 'careful', 'meticulous', or 'auto'"] = "auto",
18
+ engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
19
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Basic Calculation Workflow",
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
+ """Submit a basic calculation workflow using Rowan v2 API.
24
+
25
+ Performs fundamental quantum chemistry calculations with configurable methods
26
+ and computational tasks. Returns a workflow object for tracking progress.
27
+
28
+ Examples:
29
+ # Simple water optimization with GFN2-xTB
30
+ result = submit_basic_calculation_workflow(
31
+ initial_molecule="O",
32
+ method="gfn2-xtb",
33
+ tasks=["optimize"],
34
+ engine="xtb",
35
+ name="Water Optimization"
36
+ )
37
+
38
+ # Butane optimization from SMILES
39
+ result = submit_basic_calculation_workflow(
40
+ initial_molecule="CCCC",
41
+ method="gfn2-xtb",
42
+ tasks=["optimize"],
43
+ mode="rapid",
44
+ engine="xtb",
45
+ name="Butane Optimization"
46
+ )
47
+
48
+ # Using a molecule dict (from data.json)
49
+ molecule_dict = {
50
+ "smiles": "CCCC",
51
+ "charge": 0,
52
+ "multiplicity": 1,
53
+ "atoms": [...] # atomic positions
54
+ }
55
+ result = submit_basic_calculation_workflow(
56
+ initial_molecule=molecule_dict,
57
+ method="gfn2-xtb",
58
+ tasks=["optimize"],
59
+ engine="xtb"
60
+ )
61
+ """
62
+
63
+ # Parse tasks parameter - handle string input
64
+ parsed_tasks = None
65
+ if tasks: # If not empty string
66
+ tasks = tasks.strip()
67
+ if tasks.startswith('[') and tasks.endswith(']'):
68
+ # JSON array format like '["optimize"]'
69
+ try:
70
+ parsed_tasks = json.loads(tasks)
71
+ except (json.JSONDecodeError, ValueError):
72
+ # Failed to parse as JSON, try as comma-separated
73
+ tasks = tasks.strip('[]').replace('"', '').replace("'", "")
74
+ parsed_tasks = [t.strip() for t in tasks.split(',') if t.strip()]
75
+ elif ',' in tasks:
76
+ # Comma-separated format like 'optimize, frequencies'
77
+ parsed_tasks = [t.strip() for t in tasks.split(',') if t.strip()]
78
+ else:
79
+ # Single task as string like 'optimize'
80
+ parsed_tasks = [tasks]
81
+
82
+
83
+ try:
84
+ # Handle initial_molecule parameter - could be JSON string, SMILES, or dict
85
+ if isinstance(initial_molecule, str):
86
+ # Check if it's a JSON string (starts with { or [)
87
+ initial_molecule_str = initial_molecule.strip()
88
+ if (initial_molecule_str.startswith('{') and initial_molecule_str.endswith('}')) or \
89
+ (initial_molecule_str.startswith('[') and initial_molecule_str.endswith(']')):
90
+ try:
91
+ # Parse the JSON string to dict
92
+ initial_molecule = json.loads(initial_molecule_str)
93
+
94
+ # Now handle as dict (fall through to dict handling below)
95
+ if isinstance(initial_molecule, dict) and 'smiles' in initial_molecule:
96
+ smiles = initial_molecule.get('smiles')
97
+ if smiles:
98
+ try:
99
+ initial_molecule = stjames.Molecule.from_smiles(smiles)
100
+ except Exception as e:
101
+ initial_molecule = smiles
102
+ except (json.JSONDecodeError, ValueError) as e:
103
+ # Not valid JSON, treat as SMILES string
104
+ try:
105
+ initial_molecule = stjames.Molecule.from_smiles(initial_molecule)
106
+ except Exception as e:
107
+ pass
108
+ else:
109
+ # Regular SMILES string
110
+ try:
111
+ initial_molecule = stjames.Molecule.from_smiles(initial_molecule)
112
+ except Exception as e:
113
+ pass
114
+ elif isinstance(initial_molecule, dict) and 'smiles' in initial_molecule:
115
+ # If we have a dict with SMILES, extract and use just the SMILES
116
+ smiles = initial_molecule.get('smiles')
117
+ if smiles:
118
+ try:
119
+ initial_molecule = stjames.Molecule.from_smiles(smiles)
120
+ except Exception as e:
121
+ initial_molecule = smiles
122
+
123
+ # Convert to appropriate format
124
+ if hasattr(initial_molecule, 'model_dump'):
125
+ initial_molecule_dict = initial_molecule.model_dump()
126
+ elif isinstance(initial_molecule, dict):
127
+ initial_molecule_dict = initial_molecule
128
+ else:
129
+ # Try to convert to StJamesMolecule if it's a string
130
+ try:
131
+ mol = stjames.Molecule.from_smiles(str(initial_molecule))
132
+ initial_molecule_dict = mol.model_dump()
133
+ except:
134
+ # If that fails, pass as-is
135
+ initial_molecule_dict = initial_molecule
136
+
137
+ # Convert method string to Method object to get the correct name
138
+ if isinstance(method, str):
139
+ # Handle common method name variations
140
+ method_map = {
141
+ 'gfn2_xtb': 'gfn2-xtb',
142
+ 'gfn1_xtb': 'gfn1-xtb',
143
+ 'gfn0_xtb': 'gfn0-xtb',
144
+ 'r2scan_3c': 'r2scan-3c',
145
+ 'wb97x_d3': 'wb97x-d3',
146
+ 'wb97m_d3bj': 'wb97m-d3bj',
147
+ 'b3lyp_d3bj': 'b3lyp-d3bj',
148
+ 'uma_m_omol': 'uma_m_omol', # This one stays the same
149
+ }
150
+ method = method_map.get(method, method)
151
+
152
+ try:
153
+ method_obj = stjames.Method(method)
154
+ method_name = method_obj.name
155
+ except:
156
+ # If Method conversion fails, use the string as-is
157
+ method_name = method
158
+ else:
159
+ method_name = method
160
+
161
+ # Use parsed tasks or default
162
+ final_tasks = parsed_tasks if parsed_tasks else ["optimize"]
163
+
164
+ # Build workflow_data following the official API structure
165
+ workflow_data = {
166
+ "settings": {
167
+ "method": method_name,
168
+ "tasks": final_tasks,
169
+ "mode": mode,
170
+ },
171
+ "engine": engine,
172
+ }
173
+
174
+ # Build the API request
175
+ data = {
176
+ "name": name,
177
+ "folder_uuid": folder_uuid if folder_uuid else None,
178
+ "workflow_type": "basic_calculation",
179
+ "workflow_data": workflow_data,
180
+ "initial_molecule": initial_molecule_dict,
181
+ "max_credits": max_credits if max_credits > 0 else None,
182
+ }
183
+
184
+ # Submit directly to API
185
+ from rowan.utils import api_client
186
+ from rowan import Workflow
187
+
188
+ with api_client() as client:
189
+ response = client.post("/workflow", json=data)
190
+ response.raise_for_status()
191
+ return Workflow(**response.json())
192
+
193
+ except Exception as e:
194
+ # Re-raise the exception so MCP can handle it
195
+ raise