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.
- rowan_mcp/__init__.py +2 -2
- rowan_mcp/__main__.py +3 -5
- rowan_mcp/functions_v2/BENCHMARK.md +86 -0
- rowan_mcp/functions_v2/molecule_lookup.py +232 -0
- rowan_mcp/functions_v2/protein_management.py +141 -0
- rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
- rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
- rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
- rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
- rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
- rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
- rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
- rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
- rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
- rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
- rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
- rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
- rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
- rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
- rowan_mcp/server.py +109 -144
- rowan_mcp/tests/basic_calculation_from_json.py +0 -0
- rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
- rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
- rowan_mcp/tests/bde.py +37 -0
- rowan_mcp/tests/benchmark_queries.md +120 -0
- rowan_mcp/tests/cofolding_screen.py +131 -0
- rowan_mcp/tests/conformer_dependent_redox.py +37 -0
- rowan_mcp/tests/conformers.py +31 -0
- rowan_mcp/tests/data.json +189 -0
- rowan_mcp/tests/docking_screen.py +157 -0
- rowan_mcp/tests/irc.py +24 -0
- rowan_mcp/tests/macropka.py +13 -0
- rowan_mcp/tests/multistage_opt.py +13 -0
- rowan_mcp/tests/optimization.py +21 -0
- rowan_mcp/tests/phenol_pka.py +36 -0
- rowan_mcp/tests/pka.py +36 -0
- rowan_mcp/tests/protein_cofolding.py +17 -0
- rowan_mcp/tests/scan.py +28 -0
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +49 -33
- rowan_mcp-2.0.0.dist-info/RECORD +42 -0
- rowan_mcp/functions/admet.py +0 -94
- rowan_mcp/functions/bde.py +0 -113
- rowan_mcp/functions/calculation_retrieve.py +0 -89
- rowan_mcp/functions/conformers.py +0 -80
- rowan_mcp/functions/descriptors.py +0 -92
- rowan_mcp/functions/docking.py +0 -340
- rowan_mcp/functions/docking_enhanced.py +0 -174
- rowan_mcp/functions/electronic_properties.py +0 -205
- rowan_mcp/functions/folder_management.py +0 -137
- rowan_mcp/functions/fukui.py +0 -219
- rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
- rowan_mcp/functions/irc.py +0 -125
- rowan_mcp/functions/macropka.py +0 -120
- rowan_mcp/functions/molecular_converter.py +0 -423
- rowan_mcp/functions/molecular_dynamics.py +0 -191
- rowan_mcp/functions/molecule_lookup.py +0 -57
- rowan_mcp/functions/multistage_opt.py +0 -171
- rowan_mcp/functions/pdb_handler.py +0 -200
- rowan_mcp/functions/pka.py +0 -137
- rowan_mcp/functions/redox_potential.py +0 -352
- rowan_mcp/functions/scan.py +0 -536
- rowan_mcp/functions/scan_analyzer.py +0 -347
- rowan_mcp/functions/solubility.py +0 -277
- rowan_mcp/functions/spin_states.py +0 -747
- rowan_mcp/functions/system_management.py +0 -368
- rowan_mcp/functions/tautomers.py +0 -91
- rowan_mcp/functions/workflow_management.py +0 -422
- rowan_mcp-1.0.1.dist-info/RECORD +0 -34
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
- {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.
|
|
9
|
-
__author__ = "
|
|
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
|
|
6
|
-
python -m
|
|
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
|
-
#
|
|
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
|