rowan-mcp 0.1.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 (35) hide show
  1. rowan_mcp/__init__.py +14 -0
  2. rowan_mcp/__main__.py +14 -0
  3. rowan_mcp/functions/admet.py +94 -0
  4. rowan_mcp/functions/bde.py +113 -0
  5. rowan_mcp/functions/calculation_retrieve.py +89 -0
  6. rowan_mcp/functions/conformers.py +135 -0
  7. rowan_mcp/functions/descriptors.py +92 -0
  8. rowan_mcp/functions/docking.py +340 -0
  9. rowan_mcp/functions/docking_enhanced.py +174 -0
  10. rowan_mcp/functions/electronic_properties.py +263 -0
  11. rowan_mcp/functions/folder_management.py +137 -0
  12. rowan_mcp/functions/fukui.py +355 -0
  13. rowan_mcp/functions/hydrogen_bond_basicity.py +94 -0
  14. rowan_mcp/functions/irc.py +125 -0
  15. rowan_mcp/functions/macropka.py +195 -0
  16. rowan_mcp/functions/molecular_converter.py +423 -0
  17. rowan_mcp/functions/molecular_dynamics.py +191 -0
  18. rowan_mcp/functions/molecule_cache.db +0 -0
  19. rowan_mcp/functions/molecule_lookup.py +446 -0
  20. rowan_mcp/functions/multistage_opt.py +171 -0
  21. rowan_mcp/functions/pdb_handler.py +200 -0
  22. rowan_mcp/functions/pka.py +137 -0
  23. rowan_mcp/functions/redox_potential.py +352 -0
  24. rowan_mcp/functions/scan.py +536 -0
  25. rowan_mcp/functions/scan_analyzer.py +347 -0
  26. rowan_mcp/functions/solubility.py +277 -0
  27. rowan_mcp/functions/spin_states.py +747 -0
  28. rowan_mcp/functions/system_management.py +368 -0
  29. rowan_mcp/functions/tautomers.py +91 -0
  30. rowan_mcp/functions/workflow_management.py +422 -0
  31. rowan_mcp/server.py +169 -0
  32. rowan_mcp-0.1.0.dist-info/METADATA +216 -0
  33. rowan_mcp-0.1.0.dist-info/RECORD +35 -0
  34. rowan_mcp-0.1.0.dist-info/WHEEL +4 -0
  35. rowan_mcp-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,171 @@
1
+ """
2
+ Rowan multistage optimization function for geometry optimization.
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import time
8
+ from typing import Optional, List, Dict, Any
9
+
10
+ try:
11
+ import rowan
12
+ except ImportError:
13
+ rowan = None
14
+
15
+ # Setup logging
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Setup API key
19
+ api_key = os.getenv("ROWAN_API_KEY")
20
+ if rowan and api_key:
21
+ rowan.api_key = api_key
22
+
23
+ def log_rowan_api_call(workflow_type: str, **kwargs):
24
+ """Log Rowan API calls with detailed parameters."""
25
+
26
+ # Special handling for long-running calculations
27
+ if workflow_type in ["multistage_opt", "conformer_search"]:
28
+ ping_interval = kwargs.get('ping_interval', 5)
29
+ blocking = kwargs.get('blocking', True)
30
+ if blocking:
31
+ if workflow_type == "multistage_opt":
32
+ logger.info(f" Multi-stage optimization may take several minutes...")
33
+ else:
34
+ logger.info(f" Conformer search may take several minutes...")
35
+ logger.info(f" Progress will be checked every {ping_interval} seconds")
36
+ else:
37
+ logger.info(f" {workflow_type.replace('_', ' ').title()} submitted without waiting")
38
+
39
+ try:
40
+ start_time = time.time()
41
+ result = rowan.compute(workflow_type=workflow_type, **kwargs)
42
+ api_time = time.time() - start_time
43
+
44
+ if isinstance(result, dict) and 'uuid' in result:
45
+ job_status = result.get('status', result.get('object_status', 'Unknown'))
46
+ status_names = {0: "Queued", 1: "Running", 2: "Completed", 3: "Failed", 4: "Stopped", 5: "Awaiting Queue"}
47
+ status_text = status_names.get(job_status, f"Unknown ({job_status})")
48
+
49
+ return result
50
+
51
+ except Exception as e:
52
+ api_time = time.time() - start_time
53
+ raise e
54
+
55
+ def rowan_multistage_opt(
56
+ name: str,
57
+ molecule: str,
58
+ mode: str = "rapid",
59
+ solvent: Optional[str] = None,
60
+ xtb_preopt: bool = False,
61
+ constraints: Optional[List[Dict[str, Any]]] = None,
62
+ transition_state: bool = False,
63
+ frequencies: bool = False,
64
+ folder_uuid: Optional[str] = None,
65
+ blocking: bool = True,
66
+ ping_interval: int = 30
67
+ ) -> str:
68
+ """Run multi-level geometry optimization using stjames MultiStageOptWorkflow.
69
+
70
+ Performs hierarchical optimization using multiple levels of theory based on mode. There are only four valid modes:
71
+
72
+ **RAPID** (default): GFN2-xTB optimization → r²SCAN-3c single point
73
+ **RECKLESS**: GFN-FF optimization → GFN2-xTB single point
74
+ **CAREFUL**: GFN2-xTB preopt → r²SCAN-3c optimization → ωB97X-3c single point
75
+ **METICULOUS**: GFN2-xTB preopt → r²SCAN-3c opt → ωB97X-3c opt → ωB97M-D3BJ/def2-TZVPPD single point
76
+
77
+ This follows the exact stjames MultiStageOptWorkflow implementation for:
78
+ - High accuracy final structures
79
+ - Efficient computational cost
80
+ - Reliable convergence across chemical space
81
+
82
+ Args:
83
+ name: Name for the calculation
84
+ molecule: Molecule SMILES string
85
+ mode: Optimization mode - "rapid", "reckless", "careful", or "meticulous" (default: "rapid")
86
+ solvent: Solvent for single point calculation (e.g., "water", "dmso", "acetone")
87
+ xtb_preopt: Whether to include xTB pre-optimization step (default: False)
88
+ constraints: List of optimization constraints (default: None)
89
+ transition_state: Whether this is a transition state optimization (default: False)
90
+ frequencies: Whether to calculate vibrational frequencies (default: False)
91
+ folder_uuid: Optional folder UUID for organization
92
+ blocking: Whether to wait for completion (default: True)
93
+ ping_interval: Check status interval in seconds (default: 30)
94
+
95
+ Returns:
96
+ Comprehensive optimization results following MultiStageOptWorkflow format
97
+
98
+ Example:
99
+ # Basic optimization
100
+ result = rowan_multistage_opt("aspirin_opt", "CC(=O)Oc1ccccc1C(=O)O")
101
+
102
+ # With solvent and frequency analysis
103
+ result = rowan_multistage_opt(
104
+ "aspirin_water",
105
+ "CC(=O)Oc1ccccc1C(=O)O",
106
+ mode="careful",
107
+ solvent="water",
108
+ frequencies=True
109
+ )
110
+ """
111
+
112
+ # Validate mode parameter - only allow Rowan's supported modes
113
+ valid_modes = ["rapid", "reckless", "careful", "meticulous"]
114
+ if mode.lower() not in valid_modes:
115
+ error_msg = f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}"
116
+ logger.error(f"Mode validation failed: {error_msg}")
117
+ return f"Error: {error_msg}"
118
+
119
+ # Prepare parameters following stjames MultiStageOptWorkflow structure
120
+ params = {
121
+ "name": name,
122
+ "molecule": molecule,
123
+ "initial_molecule": molecule, # Required by MultiStageOptWorkflow
124
+ "mode": mode.lower(),
125
+ "folder_uuid": folder_uuid,
126
+ "blocking": blocking,
127
+ "ping_interval": ping_interval
128
+ }
129
+
130
+ # Add optional parameters if specified
131
+ if solvent:
132
+ params["solvent"] = solvent
133
+
134
+ if xtb_preopt:
135
+ params["xtb_preopt"] = xtb_preopt
136
+
137
+ if constraints:
138
+ params["constraints"] = constraints
139
+
140
+ if transition_state:
141
+ params["transition_state"] = transition_state
142
+
143
+ if frequencies:
144
+ params["frequencies"] = frequencies
145
+
146
+ # Submit to Rowan using multistage_opt workflow
147
+ result = log_rowan_api_call(
148
+ workflow_type="multistage_opt",
149
+ **params
150
+ )
151
+ return str(result)
152
+
153
+ def test_rowan_multistage_opt():
154
+ """Test the rowan_multistage_opt function with stjames parameters."""
155
+ try:
156
+ # Test with stjames-compatible parameters
157
+ result = rowan_multistage_opt(
158
+ name="test_stjames_opt",
159
+ molecule="CCO",
160
+ mode="rapid",
161
+ blocking=False
162
+ )
163
+ print("✅ Multistage optimization test successful!")
164
+ print(f"Result length: {len(result)} characters")
165
+ return True
166
+ except Exception as e:
167
+ print(f"Multistage optimization test failed: {e}")
168
+ return False
169
+
170
+ if __name__ == "__main__":
171
+ test_rowan_multistage_opt()
@@ -0,0 +1,200 @@
1
+ """
2
+ PDB handler for Rowan docking - attempts to handle PDB input intelligently
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import requests
8
+ from typing import Optional, Dict, Any, Tuple
9
+ import rowan
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def parse_pdb_content(pdb_content: str) -> Dict[str, Any]:
14
+ """
15
+ Parse PDB content into a structured format that might work with Rowan.
16
+
17
+ This attempts to extract key information from PDB files that the
18
+ Rowan API might accept.
19
+ """
20
+ lines = pdb_content.split('\n')
21
+
22
+ # Extract basic information
23
+ header = ""
24
+ title = ""
25
+ atoms = []
26
+
27
+ for line in lines:
28
+ if line.startswith("HEADER"):
29
+ header = line[10:50].strip()
30
+ elif line.startswith("TITLE"):
31
+ title += line[10:].strip() + " "
32
+ elif line.startswith("ATOM") or line.startswith("HETATM"):
33
+ atoms.append(line)
34
+
35
+ # Try to create a structure that might work with Rowan's PDB format
36
+ pdb_data = {
37
+ "description": {
38
+ "title": title.strip() or header,
39
+ "classification": header
40
+ },
41
+ "experiment": {
42
+ "technique": "X-RAY DIFFRACTION" # Default assumption
43
+ },
44
+ "geometry": {},
45
+ "contents": pdb_content, # Keep full content
46
+ "name": f"pdb_upload_{len(pdb_content)}"
47
+ }
48
+
49
+ return pdb_data
50
+
51
+ def attempt_pdb_upload(pdb_id: str, pdb_content: str, folder_uuid: Optional[str] = None) -> Tuple[Optional[str], str]:
52
+ """
53
+ Attempt to upload PDB content to Rowan.
54
+
55
+ Returns:
56
+ Tuple of (uuid or None, status message)
57
+ """
58
+ try:
59
+ # First, try the Protein.create method if it exists
60
+ if hasattr(rowan, 'Protein') and hasattr(rowan.Protein, 'create'):
61
+ logger.info(f"Attempting to create protein from PDB {pdb_id}")
62
+ result = rowan.Protein.create(
63
+ name=f"{pdb_id}_upload",
64
+ pdb_content=pdb_content,
65
+ folder_uuid=folder_uuid
66
+ )
67
+ if result and 'uuid' in result:
68
+ return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
69
+
70
+ # Try alternative upload methods
71
+ if hasattr(rowan, 'upload_pdb'):
72
+ result = rowan.upload_pdb(pdb_content, name=pdb_id)
73
+ if result and 'uuid' in result:
74
+ return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
75
+
76
+ # Try to create a file upload
77
+ if hasattr(rowan, 'File') and hasattr(rowan.File, 'create'):
78
+ result = rowan.File.create(
79
+ name=f"{pdb_id}.pdb",
80
+ content=pdb_content,
81
+ file_type="pdb",
82
+ folder_uuid=folder_uuid
83
+ )
84
+ if result and 'uuid' in result:
85
+ return result['uuid'], f"Uploaded PDB {pdb_id} as file"
86
+
87
+ except Exception as e:
88
+ logger.error(f"Failed to upload PDB {pdb_id}: {str(e)}")
89
+
90
+ return None, f"Cannot upload PDB through API - manual upload required"
91
+
92
+ def handle_pdb_input(pdb_input: str, folder_uuid: Optional[str] = None) -> Dict[str, Any]:
93
+ """
94
+ Handle PDB input (either PDB ID or UUID) and return information for docking.
95
+
96
+ Args:
97
+ pdb_input: Either a PDB ID (like "4Z18") or a UUID
98
+ folder_uuid: Optional folder for uploads
99
+
100
+ Returns:
101
+ Dictionary with:
102
+ - success: bool
103
+ - uuid: str (if successful)
104
+ - message: str (status/error message)
105
+ - pdb_id: str (if it was a PDB ID)
106
+ - instructions: str (what to do next)
107
+ """
108
+ # Check if it's a UUID (36 chars with dashes)
109
+ if len(pdb_input) == 36 and pdb_input.count('-') == 4:
110
+ return {
111
+ "success": True,
112
+ "uuid": pdb_input,
113
+ "message": "Using provided protein UUID",
114
+ "instructions": None
115
+ }
116
+
117
+ # Assume it's a PDB ID
118
+ pdb_id = pdb_input.upper()
119
+
120
+ # Fetch PDB content
121
+ try:
122
+ url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
123
+ response = requests.get(url, timeout=30)
124
+ response.raise_for_status()
125
+ pdb_content = response.text
126
+ logger.info(f"Successfully fetched PDB {pdb_id} ({len(pdb_content)} characters)")
127
+ except Exception as e:
128
+ return {
129
+ "success": False,
130
+ "uuid": None,
131
+ "message": f"Failed to fetch PDB {pdb_id}: {str(e)}",
132
+ "pdb_id": pdb_id,
133
+ "instructions": "Please verify the PDB ID is correct"
134
+ }
135
+
136
+ # Attempt to upload
137
+ uuid, upload_status = attempt_pdb_upload(pdb_id, pdb_content, folder_uuid)
138
+
139
+ if uuid:
140
+ return {
141
+ "success": True,
142
+ "uuid": uuid,
143
+ "message": upload_status,
144
+ "pdb_id": pdb_id,
145
+ "instructions": None
146
+ }
147
+
148
+ # Upload failed - provide instructions
149
+ instructions = f"""
150
+ To use PDB {pdb_id} for docking:
151
+
152
+ 1. Download the PDB file:
153
+ https://files.rcsb.org/download/{pdb_id}.pdb
154
+
155
+ 2. Upload to your Rowan account:
156
+ https://labs.rowansci.com
157
+
158
+ 3. Get the protein UUID from your account
159
+
160
+ 4. Use the UUID in your docking call:
161
+ rowan_docking(
162
+ target_uuid="your-protein-uuid-here",
163
+ ...
164
+ )
165
+ """
166
+
167
+ return {
168
+ "success": False,
169
+ "uuid": None,
170
+ "message": upload_status,
171
+ "pdb_id": pdb_id,
172
+ "instructions": instructions
173
+ }
174
+
175
+ def enhance_docking_with_pdb_handler(
176
+ target_input: Optional[str] = None,
177
+ target_uuid: Optional[str] = None,
178
+ **kwargs
179
+ ) -> Tuple[Optional[str], str]:
180
+ """
181
+ Enhanced target handling for docking that accepts PDB IDs.
182
+
183
+ Returns:
184
+ Tuple of (target_uuid or None, message)
185
+ """
186
+ # If UUID already provided, use it
187
+ if target_uuid:
188
+ return target_uuid, "Using provided target UUID"
189
+
190
+ # If target_input provided, try to handle it
191
+ if target_input:
192
+ result = handle_pdb_input(target_input, kwargs.get('folder_uuid'))
193
+
194
+ if result['success']:
195
+ return result['uuid'], result['message']
196
+ else:
197
+ # Return None but with helpful message
198
+ return None, result['instructions'] or result['message']
199
+
200
+ return None, "No protein target specified"
@@ -0,0 +1,137 @@
1
+ """
2
+ Calculate pKa values for molecules using Rowan API.
3
+ """
4
+
5
+ import os
6
+ import rowan
7
+ from typing import Optional
8
+
9
+ # Set up logging
10
+ import logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Configure rowan API key
14
+ if not hasattr(rowan, 'api_key') or not rowan.api_key:
15
+ api_key = os.getenv("ROWAN_API_KEY")
16
+ if api_key:
17
+ rowan.api_key = api_key
18
+ logger.info("🔑 Rowan API key configured")
19
+ else:
20
+ logger.error("No ROWAN_API_KEY found in environment")
21
+
22
+ def log_rowan_api_call(workflow_type: str, **kwargs):
23
+ """Log Rowan API calls and let Rowan handle its own errors."""
24
+
25
+ # Simple logging for calculations
26
+ logger.info(f" Starting {workflow_type.replace('_', ' ')}...")
27
+
28
+ # Let Rowan handle everything - no custom error handling
29
+ return rowan.compute(workflow_type=workflow_type, **kwargs)
30
+
31
+ def rowan_pka(
32
+ name: str,
33
+ molecule: str,
34
+ folder_uuid: Optional[str] = None,
35
+ blocking: bool = True,
36
+ ping_interval: int = 5
37
+ ) -> str:
38
+ """Calculate pKa values for molecules.
39
+
40
+ Args:
41
+ name: Name for the calculation
42
+ molecule: Molecule SMILES string or common name
43
+ folder_uuid: UUID of folder to organize calculation in
44
+ blocking: Whether to wait for completion (default: True)
45
+ ping_interval: How often to check status in seconds (default: 5)
46
+
47
+ Returns:
48
+ pKa calculation results
49
+ """
50
+ try:
51
+ result = log_rowan_api_call(
52
+ workflow_type="pka",
53
+ name=name,
54
+ molecule=molecule,
55
+ folder_uuid=folder_uuid,
56
+ blocking=blocking,
57
+ ping_interval=ping_interval
58
+ )
59
+
60
+ # Format results based on whether we waited or not
61
+ if blocking:
62
+ # We waited for completion - format actual results
63
+ status = result.get('status', result.get('object_status', 'Unknown'))
64
+
65
+ if status == 2: # Completed successfully
66
+ formatted = f" pKa calculation for '{name}' completed successfully!\n\n"
67
+ elif status == 3: # Failed
68
+ formatted = f" pKa calculation for '{name}' failed!\n\n"
69
+ else:
70
+ formatted = f" pKa calculation for '{name}' finished with status {status}\n\n"
71
+
72
+ formatted += f" Molecule: {molecule}\n"
73
+ formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
74
+ formatted += f" Status: {status}\n"
75
+
76
+ # Try to extract pKa results
77
+ if isinstance(result, dict) and 'object_data' in result and result['object_data']:
78
+ data = result['object_data']
79
+
80
+ # Extract pKa values
81
+ if 'strongest_acid' in data:
82
+ if data['strongest_acid'] is not None:
83
+ formatted += f" Strongest Acid pKa: {data['strongest_acid']:.2f}\n"
84
+ else:
85
+ formatted += f" Strongest Acid pKa: N/A (no acidic sites found)\n"
86
+
87
+ if 'strongest_base' in data:
88
+ if data['strongest_base'] is not None:
89
+ formatted += f" Strongest Base pKa: {data['strongest_base']:.2f}\n"
90
+ else:
91
+ formatted += f" Strongest Base pKa: N/A (no basic sites found)\n"
92
+ if 'pka_values' in data and isinstance(data['pka_values'], list):
93
+ formatted += f" All pKa values: {', '.join([f'{val:.2f}' for val in data['pka_values']])}\n"
94
+
95
+ # Additional properties if available
96
+ if 'ionizable_sites' in data:
97
+ formatted += f" Ionizable sites found: {data['ionizable_sites']}\n"
98
+
99
+ # Basic guidance
100
+ if status == 2:
101
+ formatted += f"\n Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for detailed data\n"
102
+ else:
103
+ # Non-blocking mode - just submission confirmation
104
+ formatted = f" pKa calculation for '{name}' submitted!\n\n"
105
+ formatted += f" Molecule: {molecule}\n"
106
+ formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
107
+ formatted += f" Status: {result.get('status', 'Submitted')}\n"
108
+
109
+ return formatted
110
+
111
+ except Exception as e:
112
+ error_response = {
113
+ "error": f"pKa calculation failed: {str(e)}",
114
+ "name": name,
115
+ "molecule": molecule
116
+ }
117
+ return str(error_response)
118
+
119
+
120
+ def test_rowan_pka():
121
+ """Test the rowan_pka function."""
122
+ try:
123
+ # Test with minimal parameters
124
+ result = rowan_pka(
125
+ name="test_pka_water",
126
+ molecule="O"
127
+ )
128
+ print("✅ pKa test successful!")
129
+ print(f"Result: {result}")
130
+ return True
131
+ except Exception as e:
132
+ print(f"pKa test failed: {e}")
133
+ return False
134
+
135
+
136
+ if __name__ == "__main__":
137
+ test_rowan_pka()