rowan-mcp 0.1.0__py3-none-any.whl → 1.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.
- rowan_mcp/__init__.py +1 -1
- rowan_mcp/functions/conformers.py +3 -58
- rowan_mcp/functions/electronic_properties.py +2 -60
- rowan_mcp/functions/fukui.py +36 -172
- rowan_mcp/functions/macropka.py +4 -79
- rowan_mcp/functions/molecule_lookup.py +40 -429
- {rowan_mcp-0.1.0.dist-info → rowan_mcp-1.0.1.dist-info}/METADATA +69 -58
- {rowan_mcp-0.1.0.dist-info → rowan_mcp-1.0.1.dist-info}/RECORD +10 -11
- rowan_mcp/functions/molecule_cache.db +0 -0
- {rowan_mcp-0.1.0.dist-info → rowan_mcp-1.0.1.dist-info}/WHEEL +0 -0
- {rowan_mcp-0.1.0.dist-info → rowan_mcp-1.0.1.dist-info}/entry_points.txt +0 -0
rowan_mcp/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ 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__ = "
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
9
|
__author__ = "Rowan MCP Team"
|
|
10
10
|
__description__ = "MCP server for Rowan computational chemistry platform"
|
|
11
11
|
|
|
@@ -42,8 +42,8 @@ def rowan_conformers(
|
|
|
42
42
|
folder_uuid: Optional[str] = None,
|
|
43
43
|
blocking: bool = True,
|
|
44
44
|
ping_interval: int = 5
|
|
45
|
-
)
|
|
46
|
-
"""Generate and optimize molecular conformers using Rowan's conformer_search workflow.
|
|
45
|
+
):
|
|
46
|
+
"""Generate and optimize molecular conformers using Rowan's conformer_search workflow. Valid modes are "reckless", "rapid", "careful", and "meticulous", and default to using SMILES strings for the "molecule" parameter.
|
|
47
47
|
|
|
48
48
|
Args:
|
|
49
49
|
name: Name for the calculation
|
|
@@ -65,7 +65,7 @@ def rowan_conformers(
|
|
|
65
65
|
f"Invalid mode '{mode}'. Valid modes are: {', '.join(valid_modes)}"
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
return log_rowan_api_call(
|
|
69
69
|
workflow_type="conformer_search",
|
|
70
70
|
name=name,
|
|
71
71
|
molecule=molecule,
|
|
@@ -75,61 +75,6 @@ def rowan_conformers(
|
|
|
75
75
|
blocking=blocking,
|
|
76
76
|
ping_interval=ping_interval
|
|
77
77
|
)
|
|
78
|
-
|
|
79
|
-
# Format results based on whether we waited or not
|
|
80
|
-
if blocking:
|
|
81
|
-
# We waited for completion - format actual results
|
|
82
|
-
status = result.get('status', result.get('object_status', 'Unknown'))
|
|
83
|
-
|
|
84
|
-
if status == 2: # Completed successfully
|
|
85
|
-
formatted = f" Conformer search for '{name}' completed successfully!\n\n"
|
|
86
|
-
elif status == 3: # Failed
|
|
87
|
-
formatted = f" Conformer search for '{name}' failed!\n\n"
|
|
88
|
-
else:
|
|
89
|
-
formatted = f" Conformer search for '{name}' finished with status {status}\n\n"
|
|
90
|
-
|
|
91
|
-
formatted += f" Molecule: {molecule}\n"
|
|
92
|
-
formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
93
|
-
formatted += f" Status: {status}\n"
|
|
94
|
-
formatted += f" Mode: {mode.upper()}\n"
|
|
95
|
-
formatted += f" Max Conformers: {max_conformers}\n"
|
|
96
|
-
|
|
97
|
-
# Try to extract actual results
|
|
98
|
-
if isinstance(result, dict) and 'object_data' in result and result['object_data']:
|
|
99
|
-
data = result['object_data']
|
|
100
|
-
|
|
101
|
-
# Count conformers found
|
|
102
|
-
if 'conformers' in data:
|
|
103
|
-
conformer_count = len(data['conformers']) if isinstance(data['conformers'], list) else data.get('num_conformers', 'Unknown')
|
|
104
|
-
formatted += f" Generated Conformers: {conformer_count}\n"
|
|
105
|
-
|
|
106
|
-
# Energy information
|
|
107
|
-
if 'energies' in data and isinstance(data['energies'], list) and data['energies']:
|
|
108
|
-
energies = data['energies']
|
|
109
|
-
min_energy = min(energies)
|
|
110
|
-
max_energy = max(energies)
|
|
111
|
-
energy_range = max_energy - min_energy
|
|
112
|
-
formatted += f" Energy Range: {min_energy:.3f} to {max_energy:.3f} kcal/mol (Δ={energy_range:.3f})\n"
|
|
113
|
-
formatted += f" Lowest Energy Conformer: {min_energy:.3f} kcal/mol\n"
|
|
114
|
-
|
|
115
|
-
# Additional properties if available
|
|
116
|
-
if 'properties' in data:
|
|
117
|
-
props = data['properties']
|
|
118
|
-
formatted += f" Properties calculated: {', '.join(props.keys())}\n"
|
|
119
|
-
|
|
120
|
-
# Basic guidance
|
|
121
|
-
if status == 2:
|
|
122
|
-
formatted += f"\n Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for detailed data\n"
|
|
123
|
-
else:
|
|
124
|
-
# Non-blocking mode - just submission confirmation
|
|
125
|
-
formatted = f" Conformer search for '{name}' submitted!\n\n"
|
|
126
|
-
formatted += f" Molecule: {molecule}\n"
|
|
127
|
-
formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
128
|
-
formatted += f" Status: {result.get('status', 'Submitted')}\n"
|
|
129
|
-
formatted += f" Mode: {mode.upper()}\n"
|
|
130
|
-
formatted += f" Max Conformers: {max_conformers}\n"
|
|
131
|
-
|
|
132
|
-
return formatted
|
|
133
78
|
|
|
134
79
|
if __name__ == "__main__":
|
|
135
80
|
pass
|
|
@@ -186,68 +186,10 @@ def rowan_electronic_properties(
|
|
|
186
186
|
**electronic_params
|
|
187
187
|
)
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
if blocking:
|
|
191
|
-
status = result.get('status', result.get('object_status', 'Unknown'))
|
|
192
|
-
|
|
193
|
-
if status == 2: # Completed successfully
|
|
194
|
-
formatted = f"Electronic properties calculation for '{name}' completed successfully!\n\n"
|
|
195
|
-
elif status == 3: # Failed
|
|
196
|
-
formatted = f"Electronic properties calculation for '{name}' failed!\n\n"
|
|
197
|
-
else:
|
|
198
|
-
formatted = f"Electronic properties calculation for '{name}' submitted!\n\n"
|
|
199
|
-
|
|
200
|
-
formatted += f"Molecule: {molecule}\n"
|
|
201
|
-
formatted += f"Canonical SMILES: {canonical_smiles}\n"
|
|
202
|
-
formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
203
|
-
formatted += f"Status: {status}\n\n"
|
|
204
|
-
|
|
205
|
-
formatted += f"Molecule Lookup: Advanced PubChemPy + SQLite + RDKit system\n\n"
|
|
206
|
-
formatted += f"Calculation Settings:\n"
|
|
207
|
-
formatted += f"• Method: {method.upper()}\n"
|
|
208
|
-
formatted += f"• Basis Set: {basis_set}\n"
|
|
209
|
-
formatted += f"• Engine: {engine.upper()}\n"
|
|
210
|
-
formatted += f"• Charge: {charge}, Multiplicity: {multiplicity}\n\n"
|
|
211
|
-
|
|
212
|
-
formatted += f"Property Calculations:\n"
|
|
213
|
-
formatted += f"• Density Cube: {'Enabled' if compute_density_cube else 'Disabled'}\n"
|
|
214
|
-
formatted += f"• ESP Cube: {'Enabled' if compute_electrostatic_potential_cube else 'Disabled'}\n"
|
|
215
|
-
formatted += f"• Occupied MOs: {compute_num_occupied_orbitals}\n"
|
|
216
|
-
formatted += f"• Virtual MOs: {compute_num_virtual_orbitals}\n\n"
|
|
217
|
-
|
|
218
|
-
if status == 2:
|
|
219
|
-
formatted += f"Additional Analysis:\n"
|
|
220
|
-
formatted += f"• Use rowan_calculation_retrieve('{result.get('uuid')}') for full calculation details\n"
|
|
221
|
-
formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for workflow metadata\n"
|
|
222
|
-
|
|
223
|
-
elif status == 3:
|
|
224
|
-
formatted += f"Troubleshooting:\n"
|
|
225
|
-
formatted += f"• Try simpler method/basis: method='hf', basis_set='sto-3g'\n"
|
|
226
|
-
formatted += f"• Check molecular charge and multiplicity\n"
|
|
227
|
-
formatted += f"• Disable cube generation for faster calculations\n"
|
|
228
|
-
formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for error details\n"
|
|
229
|
-
else:
|
|
230
|
-
formatted += f"Next Steps:\n"
|
|
231
|
-
formatted += f"• Monitor status with rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}')\n"
|
|
232
|
-
formatted += f"• Electronic properties calculations may take several minutes\n"
|
|
233
|
-
|
|
234
|
-
return formatted
|
|
235
|
-
else:
|
|
236
|
-
return str(result)
|
|
189
|
+
return result
|
|
237
190
|
|
|
238
191
|
except Exception as e:
|
|
239
|
-
|
|
240
|
-
error_msg += f"Molecule: {molecule}\n"
|
|
241
|
-
error_msg += f"Canonical SMILES: {canonical_smiles}\n"
|
|
242
|
-
error_msg += f"Settings: {method}/{basis_set}/{engine}\n\n"
|
|
243
|
-
error_msg += f"Molecule Lookup: Advanced PubChemPy + SQLite + RDKit system\n\n"
|
|
244
|
-
error_msg += f"Common Issues:\n"
|
|
245
|
-
error_msg += f"• Invalid method/basis set combination\n"
|
|
246
|
-
error_msg += f"• Incorrect charge/multiplicity for molecule\n"
|
|
247
|
-
error_msg += f"• Engine compatibility issues\n"
|
|
248
|
-
error_msg += f"• Molecule not found in PubChem database\n"
|
|
249
|
-
error_msg += f"• Try with default parameters first\n"
|
|
250
|
-
return error_msg
|
|
192
|
+
return f"Electronic properties calculation failed: {str(e)}"
|
|
251
193
|
|
|
252
194
|
def test_electronic_properties():
|
|
253
195
|
"""Test the electronic properties function with advanced molecule lookup."""
|
rowan_mcp/functions/fukui.py
CHANGED
|
@@ -54,53 +54,33 @@ def log_rowan_api_call(workflow_type: str, **kwargs):
|
|
|
54
54
|
raise e
|
|
55
55
|
|
|
56
56
|
def lookup_molecule_smiles(molecule_name: str) -> str:
|
|
57
|
-
"""Look up canonical SMILES
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"aniline": "Nc1ccccc1",
|
|
65
|
-
"benzoic acid": "O=C(O)c1ccccc1",
|
|
66
|
-
"salicylic acid": "O=C(O)c1ccccc1O",
|
|
67
|
-
"aspirin": "CC(=O)Oc1ccccc1C(=O)O",
|
|
57
|
+
"""Look up canonical SMILES using the advanced molecule_lookup system.
|
|
58
|
+
|
|
59
|
+
Uses PubChemPy + SQLite caching + RDKit validation for scalable molecule lookup.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
# Import the advanced molecule lookup system
|
|
63
|
+
from .molecule_lookup import get_lookup_instance
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"acetone": "CC(=O)C",
|
|
72
|
-
"dmso": "CS(=O)C",
|
|
73
|
-
"dmf": "CN(C)C=O",
|
|
74
|
-
"thf": "C1CCOC1",
|
|
75
|
-
"dioxane": "C1COCCO1",
|
|
76
|
-
"chloroform": "ClC(Cl)Cl",
|
|
77
|
-
"dichloromethane": "ClCCl",
|
|
65
|
+
lookup = get_lookup_instance()
|
|
66
|
+
smiles, source, metadata = lookup.get_smiles(molecule_name)
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"ethylene": "C=C",
|
|
82
|
-
"acetylene": "C#C",
|
|
83
|
-
"formaldehyde": "C=O",
|
|
84
|
-
"ammonia": "N",
|
|
85
|
-
"hydrogen peroxide": "OO",
|
|
86
|
-
"carbon dioxide": "O=C=O",
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# Normalize the input (lowercase, strip whitespace)
|
|
90
|
-
normalized_name = molecule_name.lower().strip()
|
|
91
|
-
|
|
92
|
-
# Direct lookup
|
|
93
|
-
if normalized_name in MOLECULE_SMILES:
|
|
94
|
-
return MOLECULE_SMILES[normalized_name]
|
|
95
|
-
|
|
96
|
-
# Try partial matches for common variations
|
|
97
|
-
for name, smiles in MOLECULE_SMILES.items():
|
|
98
|
-
if normalized_name in name or name in normalized_name:
|
|
99
|
-
logger.info(f"SMILES Lookup (partial match): '{molecule_name}' → '{name}' → '{smiles}'")
|
|
68
|
+
if smiles:
|
|
69
|
+
logger.info(f"Molecule lookup successful: '{molecule_name}' → '{smiles}' (source: {source})")
|
|
100
70
|
return smiles
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
else:
|
|
72
|
+
logger.warning(f"Molecule lookup failed for '{molecule_name}': {metadata.get('error', 'Unknown error')}")
|
|
73
|
+
# Return original input as fallback (might be valid SMILES)
|
|
74
|
+
return molecule_name
|
|
75
|
+
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
logger.error(f"Could not import molecule_lookup: {e}")
|
|
78
|
+
# Fallback: return original input
|
|
79
|
+
return molecule_name
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Molecule lookup error for '{molecule_name}': {e}")
|
|
82
|
+
# Fallback: return original input
|
|
83
|
+
return molecule_name
|
|
104
84
|
|
|
105
85
|
def rowan_fukui(
|
|
106
86
|
name: str,
|
|
@@ -135,9 +115,12 @@ def rowan_fukui(
|
|
|
135
115
|
- Per-atom reactivity indices for site-specific analysis
|
|
136
116
|
- Global reactivity descriptors
|
|
137
117
|
|
|
118
|
+
**Molecule Lookup**: Uses advanced PubChemPy + SQLite caching + RDKit validation system
|
|
119
|
+
for robust molecule identification and SMILES canonicalization.
|
|
120
|
+
|
|
138
121
|
Args:
|
|
139
122
|
name: Name for the calculation
|
|
140
|
-
molecule: Molecule
|
|
123
|
+
molecule: Molecule name (e.g., "aspirin", "taxol") or SMILES string
|
|
141
124
|
optimize: Whether to optimize geometry before Fukui calculation (default: True)
|
|
142
125
|
opt_method: Method for optimization (default: None, uses engine default)
|
|
143
126
|
opt_basis_set: Basis set for optimization (default: None, uses engine default)
|
|
@@ -212,135 +195,16 @@ def rowan_fukui(
|
|
|
212
195
|
if fukui_engine:
|
|
213
196
|
fukui_params["fukui_engine"] = fukui_engine.lower()
|
|
214
197
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if blocking:
|
|
221
|
-
status = result.get('status', result.get('object_status', 'Unknown'))
|
|
222
|
-
|
|
223
|
-
if status == 2: # Completed successfully
|
|
224
|
-
formatted = f"Fukui analysis for '{name}' completed successfully!\n\n"
|
|
225
|
-
elif status == 3: # Failed
|
|
226
|
-
formatted = f"Fukui analysis for '{name}' failed!\n\n"
|
|
227
|
-
else:
|
|
228
|
-
formatted = f"Fukui analysis for '{name}' finished with status {status}\n\n"
|
|
229
|
-
|
|
230
|
-
formatted += f"Molecule: {molecule}\n"
|
|
231
|
-
formatted += f"SMILES: {canonical_smiles}\n"
|
|
232
|
-
formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
233
|
-
formatted += f"Status: {status}\n"
|
|
234
|
-
|
|
235
|
-
# Computational settings summary
|
|
236
|
-
formatted += f"\nComputational Settings:\n"
|
|
237
|
-
formatted += f" Optimization: {'Enabled' if optimize else 'Disabled'}\n"
|
|
238
|
-
if optimize:
|
|
239
|
-
opt_method_display = opt_settings.get('method', 'default') if opt_settings else 'default'
|
|
240
|
-
formatted += f" Opt Method: {opt_method_display.upper()}\n"
|
|
241
|
-
if opt_engine:
|
|
242
|
-
formatted += f" Opt Engine: {opt_engine.upper()}\n"
|
|
243
|
-
formatted += f" Fukui Method: {fukui_method.upper()}\n"
|
|
244
|
-
if fukui_engine:
|
|
245
|
-
formatted += f" Fukui Engine: {fukui_engine.upper()}\n"
|
|
246
|
-
formatted += f" Charge: {charge}, Multiplicity: {multiplicity}\n"
|
|
198
|
+
try:
|
|
199
|
+
result = log_rowan_api_call(
|
|
200
|
+
workflow_type="fukui",
|
|
201
|
+
**fukui_params
|
|
202
|
+
)
|
|
247
203
|
|
|
248
|
-
|
|
249
|
-
if isinstance(result, dict) and 'object_data' in result and result['object_data']:
|
|
250
|
-
data = result['object_data']
|
|
251
|
-
|
|
252
|
-
# Global electrophilicity index
|
|
253
|
-
if 'global_electrophilicity_index' in data and data['global_electrophilicity_index'] is not None:
|
|
254
|
-
gei = data['global_electrophilicity_index']
|
|
255
|
-
formatted += f"\nGlobal Electrophilicity Index: {gei:.4f}\n"
|
|
256
|
-
if gei > 1.5:
|
|
257
|
-
formatted += f" → Strong electrophile (highly reactive towards nucleophiles)\n"
|
|
258
|
-
elif gei > 0.8:
|
|
259
|
-
formatted += f" → Moderate electrophile\n"
|
|
260
|
-
else:
|
|
261
|
-
formatted += f" → Weak electrophile\n"
|
|
204
|
+
return result
|
|
262
205
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if 'fukui_positive' in data and data['fukui_positive']:
|
|
266
|
-
fukui_available.append("f(+)")
|
|
267
|
-
if 'fukui_negative' in data and data['fukui_negative']:
|
|
268
|
-
fukui_available.append("f(-)")
|
|
269
|
-
if 'fukui_zero' in data and data['fukui_zero']:
|
|
270
|
-
fukui_available.append("f(0)")
|
|
271
|
-
|
|
272
|
-
if fukui_available:
|
|
273
|
-
formatted += f"\nFukui Indices Available: {', '.join(fukui_available)}\n"
|
|
274
|
-
|
|
275
|
-
# Analyze most reactive sites
|
|
276
|
-
formatted += f"\nMost Reactive Sites:\n"
|
|
277
|
-
|
|
278
|
-
# f(+) - electrophilic attack sites
|
|
279
|
-
if 'fukui_positive' in data and data['fukui_positive']:
|
|
280
|
-
f_plus = data['fukui_positive']
|
|
281
|
-
if isinstance(f_plus, list) and len(f_plus) > 0:
|
|
282
|
-
# Find top 3 sites
|
|
283
|
-
indexed_values = [(i+1, val) for i, val in enumerate(f_plus) if val is not None]
|
|
284
|
-
top_f_plus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
|
|
285
|
-
formatted += f" f(+) Top Sites (electrophilic attack): "
|
|
286
|
-
formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_plus])}\n"
|
|
287
|
-
|
|
288
|
-
# f(-) - nucleophilic attack sites
|
|
289
|
-
if 'fukui_negative' in data and data['fukui_negative']:
|
|
290
|
-
f_minus = data['fukui_negative']
|
|
291
|
-
if isinstance(f_minus, list) and len(f_minus) > 0:
|
|
292
|
-
indexed_values = [(i+1, val) for i, val in enumerate(f_minus) if val is not None]
|
|
293
|
-
top_f_minus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
|
|
294
|
-
formatted += f" f(-) Top Sites (nucleophilic attack): "
|
|
295
|
-
formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_minus])}\n"
|
|
296
|
-
|
|
297
|
-
# f(0) - radical attack sites
|
|
298
|
-
if 'fukui_zero' in data and data['fukui_zero']:
|
|
299
|
-
f_zero = data['fukui_zero']
|
|
300
|
-
if isinstance(f_zero, list) and len(f_zero) > 0:
|
|
301
|
-
indexed_values = [(i+1, val) for i, val in enumerate(f_zero) if val is not None]
|
|
302
|
-
top_f_zero = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
|
|
303
|
-
formatted += f" f(0) Top Sites (radical attack): "
|
|
304
|
-
formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_zero])}\n"
|
|
305
|
-
|
|
306
|
-
# Status-specific guidance
|
|
307
|
-
formatted += f"\nNext Steps:\n"
|
|
308
|
-
if status == 2: # Completed
|
|
309
|
-
formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for full per-atom data\n"
|
|
310
|
-
formatted += f"• Higher Fukui values indicate more reactive sites\n"
|
|
311
|
-
formatted += f"• f(+) predicts where nucleophiles will attack\n"
|
|
312
|
-
formatted += f"• f(-) predicts where electrophiles will attack\n"
|
|
313
|
-
formatted += f"• f(0) predicts radical reaction sites\n"
|
|
314
|
-
elif status == 3: # Failed
|
|
315
|
-
formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for error details\n"
|
|
316
|
-
formatted += f"• Troubleshooting:\n"
|
|
317
|
-
formatted += f" - Try disabling optimization: optimize=False\n"
|
|
318
|
-
formatted += f" - Use faster Fukui method: fukui_method='gfn1_xtb'\n"
|
|
319
|
-
formatted += f" - Check if molecule SMILES is valid\n"
|
|
320
|
-
formatted += f" - Verify charge and multiplicity are correct\n"
|
|
321
|
-
elif status in [0, 1, 5]: # Running
|
|
322
|
-
formatted += f"• Check progress: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
|
|
323
|
-
if optimize:
|
|
324
|
-
formatted += f"• Two-step process: optimization → Fukui calculation\n"
|
|
325
|
-
formatted += f"• Fukui analysis may take 5-20 minutes depending on method and molecule size\n"
|
|
326
|
-
elif status == 4: # Stopped
|
|
327
|
-
formatted += f"• Check why stopped: rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}')\n"
|
|
328
|
-
formatted += f"• You can restart with same or modified parameters\n"
|
|
329
|
-
else: # Unknown
|
|
330
|
-
formatted += f"• Check status: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
|
|
331
|
-
|
|
332
|
-
return formatted
|
|
333
|
-
else:
|
|
334
|
-
formatted = f"Fukui analysis for '{name}' submitted!\n\n"
|
|
335
|
-
formatted += f"Molecule: {molecule}\n"
|
|
336
|
-
formatted += f"SMILES: {canonical_smiles}\n"
|
|
337
|
-
formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
338
|
-
formatted += f"Status: {result.get('status', 'Submitted')}\n"
|
|
339
|
-
formatted += f"Optimization: {'Enabled' if optimize else 'Disabled'}\n"
|
|
340
|
-
formatted += f"Fukui Method: {fukui_method.upper()}\n"
|
|
341
|
-
formatted += f"Charge: {charge}, Multiplicity: {multiplicity}\n"
|
|
342
|
-
formatted += f"\nUse rowan_workflow_management tools to check progress and retrieve results\n"
|
|
343
|
-
return formatted
|
|
206
|
+
except Exception as e:
|
|
207
|
+
return f"Fukui analysis failed: {str(e)}"
|
|
344
208
|
|
|
345
209
|
def test_fukui():
|
|
346
210
|
"""Test the fukui function."""
|
rowan_mcp/functions/macropka.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""MacropKa workflow function for MCP server."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
from typing import Optional, Union, List
|
|
7
6
|
|
|
@@ -61,11 +60,11 @@ def rowan_macropka(
|
|
|
61
60
|
try:
|
|
62
61
|
# Validate pH range
|
|
63
62
|
if min_pH >= max_pH:
|
|
64
|
-
return
|
|
63
|
+
return "Error: min_pH must be less than max_pH"
|
|
65
64
|
|
|
66
65
|
# Validate charge range
|
|
67
66
|
if min_charge >= max_charge:
|
|
68
|
-
return
|
|
67
|
+
return "Error: min_charge must be less than max_charge"
|
|
69
68
|
|
|
70
69
|
# Log the API call
|
|
71
70
|
log_rowan_api_call(
|
|
@@ -102,85 +101,11 @@ def rowan_macropka(
|
|
|
102
101
|
compute_solvation_energy=compute_solvation_energy
|
|
103
102
|
)
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
# Format completed results
|
|
107
|
-
status = result.get("status", "unknown")
|
|
108
|
-
uuid = result.get("uuid", "unknown")
|
|
109
|
-
|
|
110
|
-
if status == "success":
|
|
111
|
-
object_data = result.get("object_data", {})
|
|
112
|
-
|
|
113
|
-
# Extract key results
|
|
114
|
-
microstates = object_data.get("microstates", [])
|
|
115
|
-
pka_values = object_data.get("pKa_values", [])
|
|
116
|
-
isoelectric_point = object_data.get("isoelectric_point")
|
|
117
|
-
solvation_energy = object_data.get("solvation_energy")
|
|
118
|
-
kpuu_probability = object_data.get("kpuu_probability")
|
|
119
|
-
microstate_weights_by_pH = object_data.get("microstate_weights_by_pH", [])
|
|
120
|
-
logD_by_pH = object_data.get("logD_by_pH", [])
|
|
121
|
-
aqueous_solubility_by_pH = object_data.get("aqueous_solubility_by_pH", [])
|
|
122
|
-
|
|
123
|
-
formatted = f"✅ MacropKa calculation completed successfully!\n"
|
|
124
|
-
formatted += f"🔖 Workflow UUID: {uuid}\n"
|
|
125
|
-
formatted += f"📋 Status: {status}\n\n"
|
|
126
|
-
|
|
127
|
-
# Format pKa values
|
|
128
|
-
if pka_values:
|
|
129
|
-
formatted += "📊 pKa Values:\n"
|
|
130
|
-
for pka in pka_values:
|
|
131
|
-
formatted += f" • {pka.get('initial_charge', 'N/A')} → {pka.get('final_charge', 'N/A')}: pKa = {pka.get('pKa', 'N/A')}\n"
|
|
132
|
-
formatted += "\n"
|
|
133
|
-
|
|
134
|
-
# Format microstates
|
|
135
|
-
if microstates:
|
|
136
|
-
formatted += f"🔬 Microstates ({len(microstates)} found):\n"
|
|
137
|
-
for i, microstate in enumerate(microstates[:5]): # Show first 5
|
|
138
|
-
formatted += f" {i+1}. Charge: {microstate.get('charge', 'N/A')}, Energy: {microstate.get('energy', 'N/A')} kcal/mol\n"
|
|
139
|
-
if len(microstates) > 5:
|
|
140
|
-
formatted += f" ... and {len(microstates) - 5} more\n"
|
|
141
|
-
formatted += "\n"
|
|
142
|
-
|
|
143
|
-
# Add other properties
|
|
144
|
-
if isoelectric_point is not None:
|
|
145
|
-
formatted += f"⚡ Isoelectric Point: pH {isoelectric_point}\n"
|
|
146
|
-
|
|
147
|
-
if solvation_energy is not None:
|
|
148
|
-
formatted += f"💧 Solvation Energy: {solvation_energy} kcal/mol\n"
|
|
149
|
-
|
|
150
|
-
if kpuu_probability is not None:
|
|
151
|
-
formatted += f"🧠 Kpuu Probability (≥0.3): {kpuu_probability:.2%}\n"
|
|
152
|
-
|
|
153
|
-
# Show pH-dependent properties if available
|
|
154
|
-
if logD_by_pH:
|
|
155
|
-
formatted += f"\n📈 logD values available for {len(logD_by_pH)} pH points\n"
|
|
156
|
-
|
|
157
|
-
if aqueous_solubility_by_pH:
|
|
158
|
-
formatted += f"💧 Aqueous solubility values available for {len(aqueous_solubility_by_pH)} pH points\n"
|
|
159
|
-
|
|
160
|
-
if microstate_weights_by_pH:
|
|
161
|
-
formatted += f"⚖️ Microstate weights available for {len(microstate_weights_by_pH)} pH points\n"
|
|
162
|
-
|
|
163
|
-
return formatted
|
|
164
|
-
else:
|
|
165
|
-
# Handle failed calculation
|
|
166
|
-
return f"❌ MacropKa calculation failed\n🔖 UUID: {uuid}\n📋 Status: {status}\n💬 Check workflow details for more information"
|
|
167
|
-
else:
|
|
168
|
-
# Non-blocking mode - return submission confirmation
|
|
169
|
-
uuid = result.get("uuid", "unknown")
|
|
170
|
-
formatted = f"📋 MacropKa calculation submitted!\n"
|
|
171
|
-
formatted += f"🔖 Workflow UUID: {uuid}\n"
|
|
172
|
-
formatted += f"⏳ Status: Running...\n"
|
|
173
|
-
formatted += f"💡 Use rowan_workflow_management to check status\n"
|
|
174
|
-
formatted += f"\nCalculation parameters:\n"
|
|
175
|
-
formatted += f" • pH range: {min_pH} - {max_pH}\n"
|
|
176
|
-
formatted += f" • Charge range: {min_charge} to {max_charge}\n"
|
|
177
|
-
formatted += f" • Compute solvation energy: {compute_solvation_energy}\n"
|
|
178
|
-
formatted += f" • Compute aqueous solubility: {compute_aqueous_solubility}\n"
|
|
179
|
-
return formatted
|
|
104
|
+
return result
|
|
180
105
|
|
|
181
106
|
except Exception as e:
|
|
182
107
|
logger.error(f"Error in rowan_macropka: {str(e)}")
|
|
183
|
-
return
|
|
108
|
+
return f"MacropKa calculation failed: {str(e)}"
|
|
184
109
|
|
|
185
110
|
|
|
186
111
|
# Test function
|
|
@@ -1,446 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
"""
|
|
1
|
+
from urllib.request import urlopen
|
|
2
|
+
from urllib.parse import quote
|
|
4
3
|
|
|
5
|
-
import sqlite3
|
|
6
|
-
import logging
|
|
7
|
-
from datetime import datetime, timedelta
|
|
8
|
-
from typing import Optional, Tuple
|
|
9
|
-
import os
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Import dependencies with fallbacks
|
|
15
|
-
try:
|
|
16
|
-
import pubchempy as pcp
|
|
17
|
-
PUBCHEMPY_AVAILABLE = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
logger.warning("pubchempy not available - install with: pip install pubchempy")
|
|
20
|
-
PUBCHEMPY_AVAILABLE = False
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from rdkit import Chem
|
|
24
|
-
from rdkit.Chem import Descriptors
|
|
25
|
-
RDKIT_AVAILABLE = True
|
|
26
|
-
except ImportError:
|
|
27
|
-
logger.warning("rdkit not available - install with: pip install rdkit")
|
|
28
|
-
RDKIT_AVAILABLE = False
|
|
29
|
-
|
|
30
|
-
class MoleculeLookup:
|
|
31
|
-
"""Molecule lookup with PubChem API, SQLite caching, and RDKit validation."""
|
|
32
|
-
|
|
33
|
-
def __init__(self, cache_db: str = 'molecule_cache.db', cache_expiry_days: int = 30):
|
|
34
|
-
"""Initialize the molecule lookup system."""
|
|
35
|
-
self.cache_expiry_days = cache_expiry_days
|
|
36
|
-
|
|
37
|
-
# Create cache database
|
|
38
|
-
cache_path = os.path.join(os.path.dirname(__file__), cache_db)
|
|
39
|
-
self.conn = sqlite3.connect(cache_path, check_same_thread=False)
|
|
40
|
-
|
|
41
|
-
# Create tables if they don't exist
|
|
42
|
-
self.conn.execute('''
|
|
43
|
-
CREATE TABLE IF NOT EXISTS molecules (
|
|
44
|
-
identifier TEXT PRIMARY KEY,
|
|
45
|
-
smiles TEXT,
|
|
46
|
-
canonical_smiles TEXT,
|
|
47
|
-
name TEXT,
|
|
48
|
-
iupac_name TEXT,
|
|
49
|
-
formula TEXT,
|
|
50
|
-
molecular_weight REAL,
|
|
51
|
-
cid INTEGER,
|
|
52
|
-
retrieved_at TIMESTAMP,
|
|
53
|
-
source TEXT
|
|
54
|
-
)
|
|
55
|
-
''')
|
|
56
|
-
|
|
57
|
-
self.conn.execute('''
|
|
58
|
-
CREATE TABLE IF NOT EXISTS lookup_stats (
|
|
59
|
-
date TEXT PRIMARY KEY,
|
|
60
|
-
cache_hits INTEGER DEFAULT 0,
|
|
61
|
-
api_calls INTEGER DEFAULT 0,
|
|
62
|
-
failed_lookups INTEGER DEFAULT 0
|
|
63
|
-
)
|
|
64
|
-
''')
|
|
65
|
-
|
|
66
|
-
self.conn.commit()
|
|
67
|
-
logger.info("Molecule lookup cache initialized")
|
|
68
|
-
|
|
69
|
-
def validate_smiles(self, smiles: str) -> Optional[str]:
|
|
70
|
-
"""Validate and canonicalize SMILES using RDKit."""
|
|
71
|
-
if not RDKIT_AVAILABLE:
|
|
72
|
-
logger.warning("RDKit not available - returning SMILES as-is")
|
|
73
|
-
return smiles
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
mol = Chem.MolFromSmiles(smiles)
|
|
77
|
-
if mol is not None:
|
|
78
|
-
canonical = Chem.MolToSmiles(mol, canonical=True)
|
|
79
|
-
logger.debug(f"SMILES validated: {smiles} -> {canonical}")
|
|
80
|
-
return canonical
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.warning(f"SMILES validation failed for {smiles}: {e}")
|
|
83
|
-
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
def get_molecular_properties(self, smiles: str) -> dict:
|
|
87
|
-
"""Calculate molecular properties using RDKit."""
|
|
88
|
-
if not RDKIT_AVAILABLE:
|
|
89
|
-
return {}
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
mol = Chem.MolFromSmiles(smiles)
|
|
93
|
-
if mol is not None:
|
|
94
|
-
return {
|
|
95
|
-
'molecular_weight': round(Descriptors.MolWt(mol), 2),
|
|
96
|
-
'logp': round(Descriptors.MolLogP(mol), 2),
|
|
97
|
-
'hbd': Descriptors.NumHDonors(mol),
|
|
98
|
-
'hba': Descriptors.NumHAcceptors(mol),
|
|
99
|
-
'rotatable_bonds': Descriptors.NumRotatableBonds(mol),
|
|
100
|
-
'aromatic_rings': Descriptors.NumAromaticRings(mol)
|
|
101
|
-
}
|
|
102
|
-
except Exception as e:
|
|
103
|
-
logger.warning(f"Property calculation failed for {smiles}: {e}")
|
|
104
|
-
|
|
105
|
-
return {}
|
|
106
|
-
|
|
107
|
-
def _is_cache_valid(self, retrieved_at: str) -> bool:
|
|
108
|
-
"""Check if cache entry is still valid."""
|
|
109
|
-
try:
|
|
110
|
-
cache_time = datetime.fromisoformat(retrieved_at)
|
|
111
|
-
expiry_time = datetime.now() - timedelta(days=self.cache_expiry_days)
|
|
112
|
-
return cache_time > expiry_time
|
|
113
|
-
except:
|
|
114
|
-
return False
|
|
115
|
-
|
|
116
|
-
def _update_stats(self, stat_type: str):
|
|
117
|
-
"""Update lookup statistics."""
|
|
118
|
-
today = datetime.now().date().isoformat()
|
|
119
|
-
|
|
120
|
-
# Insert or update today's stats
|
|
121
|
-
self.conn.execute(f'''
|
|
122
|
-
INSERT OR IGNORE INTO lookup_stats (date, {stat_type}) VALUES (?, 1)
|
|
123
|
-
''', (today,))
|
|
124
|
-
|
|
125
|
-
self.conn.execute(f'''
|
|
126
|
-
UPDATE lookup_stats SET {stat_type} = {stat_type} + 1 WHERE date = ?
|
|
127
|
-
''', (today,))
|
|
128
|
-
|
|
129
|
-
self.conn.commit()
|
|
130
|
-
|
|
131
|
-
def get_smiles(self, identifier: str) -> Tuple[Optional[str], str, dict]:
|
|
132
|
-
"""Get canonical SMILES for a molecule identifier."""
|
|
133
|
-
identifier = identifier.strip()
|
|
134
|
-
identifier_lower = identifier.lower()
|
|
135
|
-
|
|
136
|
-
# 1. Check cache first
|
|
137
|
-
cursor = self.conn.execute('''
|
|
138
|
-
SELECT smiles, canonical_smiles, name, iupac_name, formula,
|
|
139
|
-
molecular_weight, cid, retrieved_at, source
|
|
140
|
-
FROM molecules WHERE identifier = ?
|
|
141
|
-
''', (identifier_lower,))
|
|
142
|
-
|
|
143
|
-
result = cursor.fetchone()
|
|
144
|
-
if result:
|
|
145
|
-
retrieved_at = result[7]
|
|
146
|
-
if self._is_cache_valid(retrieved_at):
|
|
147
|
-
self._update_stats('cache_hits')
|
|
148
|
-
logger.info(f"Cache hit for: {identifier}")
|
|
149
|
-
|
|
150
|
-
metadata = {
|
|
151
|
-
'name': result[2],
|
|
152
|
-
'iupac_name': result[3],
|
|
153
|
-
'formula': result[4],
|
|
154
|
-
'molecular_weight': result[5],
|
|
155
|
-
'cid': result[6],
|
|
156
|
-
'source': result[8],
|
|
157
|
-
'cached': True
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return result[1], result[8], metadata # Return canonical_smiles
|
|
161
|
-
|
|
162
|
-
# 2. Check if input is already a valid SMILES
|
|
163
|
-
validated_smiles = self.validate_smiles(identifier)
|
|
164
|
-
if validated_smiles and validated_smiles != identifier:
|
|
165
|
-
logger.info(f"Input was valid SMILES, canonicalized: {identifier} -> {validated_smiles}")
|
|
166
|
-
|
|
167
|
-
# Cache the result
|
|
168
|
-
metadata = {'source': 'input_smiles', 'cached': False}
|
|
169
|
-
properties = self.get_molecular_properties(validated_smiles)
|
|
170
|
-
metadata.update(properties)
|
|
171
|
-
|
|
172
|
-
self._cache_result(identifier_lower, identifier, validated_smiles,
|
|
173
|
-
"User Input SMILES", "", "",
|
|
174
|
-
properties.get('molecular_weight'), None, 'input_smiles')
|
|
175
|
-
|
|
176
|
-
return validated_smiles, 'input_smiles', metadata
|
|
177
|
-
|
|
178
|
-
# 3. Fetch from PubChem using PubChemPy
|
|
179
|
-
if not PUBCHEMPY_AVAILABLE:
|
|
180
|
-
logger.error("PubChemPy not available for API lookup")
|
|
181
|
-
self._update_stats('failed_lookups')
|
|
182
|
-
return None, 'error', {'error': 'PubChemPy not available'}
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
self._update_stats('api_calls')
|
|
186
|
-
logger.info(f"PubChem API lookup for: {identifier}")
|
|
187
|
-
|
|
188
|
-
# Try name lookup first
|
|
189
|
-
compounds = pcp.get_compounds(identifier, 'name')
|
|
190
|
-
|
|
191
|
-
# If name lookup fails, try as SMILES/InChI
|
|
192
|
-
if not compounds:
|
|
193
|
-
compounds = pcp.get_compounds(identifier, 'smiles')
|
|
194
|
-
|
|
195
|
-
if compounds:
|
|
196
|
-
compound = compounds[0]
|
|
197
|
-
|
|
198
|
-
# Validate the SMILES from PubChem
|
|
199
|
-
pubchem_smiles = compound.canonical_smiles
|
|
200
|
-
validated_smiles = self.validate_smiles(pubchem_smiles)
|
|
201
|
-
|
|
202
|
-
if validated_smiles:
|
|
203
|
-
# Get additional properties
|
|
204
|
-
properties = self.get_molecular_properties(validated_smiles)
|
|
205
|
-
|
|
206
|
-
# Cache the successful result
|
|
207
|
-
self._cache_result(
|
|
208
|
-
identifier_lower,
|
|
209
|
-
pubchem_smiles,
|
|
210
|
-
validated_smiles,
|
|
211
|
-
getattr(compound, 'iupac_name', '') or identifier,
|
|
212
|
-
getattr(compound, 'iupac_name', ''),
|
|
213
|
-
getattr(compound, 'molecular_formula', ''),
|
|
214
|
-
properties.get('molecular_weight') or getattr(compound, 'molecular_weight', None),
|
|
215
|
-
getattr(compound, 'cid', None),
|
|
216
|
-
'pubchem'
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
metadata = {
|
|
220
|
-
'name': identifier,
|
|
221
|
-
'iupac_name': getattr(compound, 'iupac_name', ''),
|
|
222
|
-
'formula': getattr(compound, 'molecular_formula', ''),
|
|
223
|
-
'molecular_weight': properties.get('molecular_weight') or getattr(compound, 'molecular_weight', None),
|
|
224
|
-
'cid': getattr(compound, 'cid', None),
|
|
225
|
-
'source': 'pubchem',
|
|
226
|
-
'cached': False
|
|
227
|
-
}
|
|
228
|
-
metadata.update(properties)
|
|
229
|
-
|
|
230
|
-
logger.info(f"PubChem lookup successful: {identifier} -> {validated_smiles}")
|
|
231
|
-
return validated_smiles, 'pubchem', metadata
|
|
232
|
-
|
|
233
|
-
except Exception as e:
|
|
234
|
-
logger.error(f"PubChem lookup failed for {identifier}: {e}")
|
|
235
|
-
self._update_stats('failed_lookups')
|
|
236
|
-
return None, 'error', {'error': str(e)}
|
|
237
|
-
|
|
238
|
-
# 4. No results found
|
|
239
|
-
logger.warning(f"No results found for: {identifier}")
|
|
240
|
-
self._update_stats('failed_lookups')
|
|
241
|
-
return None, 'not_found', {'error': 'No results found'}
|
|
5
|
+
def CIRconvert(ids):
|
|
6
|
+
"""
|
|
7
|
+
Convert molecule name/identifier to SMILES using Chemical Identifier Resolver.
|
|
242
8
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
molecular_weight: Optional[float], cid: Optional[int], source: str):
|
|
246
|
-
"""Cache a successful lookup result."""
|
|
247
|
-
try:
|
|
248
|
-
self.conn.execute('''
|
|
249
|
-
INSERT OR REPLACE INTO molecules
|
|
250
|
-
(identifier, smiles, canonical_smiles, name, iupac_name, formula,
|
|
251
|
-
molecular_weight, cid, retrieved_at, source)
|
|
252
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
253
|
-
''', (identifier, original_smiles, canonical_smiles, name, iupac_name,
|
|
254
|
-
formula, molecular_weight, cid, datetime.now().isoformat(), source))
|
|
255
|
-
|
|
256
|
-
self.conn.commit()
|
|
257
|
-
logger.debug(f"Cached result for: {identifier}")
|
|
258
|
-
except Exception as e:
|
|
259
|
-
logger.error(f"Failed to cache result: {e}")
|
|
9
|
+
Args:
|
|
10
|
+
ids (str): Molecule name or identifier (e.g., 'Aspirin', '3-Methylheptane', CAS numbers)
|
|
260
11
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
cache_stats = cursor.fetchone()
|
|
271
|
-
|
|
272
|
-
cursor = self.conn.execute('''
|
|
273
|
-
SELECT SUM(cache_hits) as total_hits,
|
|
274
|
-
SUM(api_calls) as total_calls,
|
|
275
|
-
SUM(failed_lookups) as total_failures
|
|
276
|
-
FROM lookup_stats
|
|
277
|
-
''')
|
|
278
|
-
|
|
279
|
-
usage_stats = cursor.fetchone()
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
'total_cached_molecules': cache_stats[0] or 0,
|
|
283
|
-
'pubchem_entries': cache_stats[1] or 0,
|
|
284
|
-
'smiles_entries': cache_stats[2] or 0,
|
|
285
|
-
'total_cache_hits': usage_stats[0] or 0,
|
|
286
|
-
'total_api_calls': usage_stats[1] or 0,
|
|
287
|
-
'total_failures': usage_stats[2] or 0
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
# Global instance
|
|
291
|
-
_lookup_instance = None
|
|
12
|
+
Returns:
|
|
13
|
+
str: SMILES string if found, 'Did not work' if failed
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
url = 'http://cactus.nci.nih.gov/chemical/structure/' + quote(ids) + '/smiles'
|
|
17
|
+
ans = urlopen(url).read().decode('utf8')
|
|
18
|
+
return ans
|
|
19
|
+
except:
|
|
20
|
+
return 'Did not work'
|
|
292
21
|
|
|
293
|
-
def get_lookup_instance():
|
|
294
|
-
"""Get or create the global MoleculeLookup instance."""
|
|
295
|
-
global _lookup_instance
|
|
296
|
-
if _lookup_instance is None:
|
|
297
|
-
_lookup_instance = MoleculeLookup()
|
|
298
|
-
return _lookup_instance
|
|
299
22
|
|
|
300
|
-
def rowan_molecule_lookup(molecule_name: str
|
|
301
|
-
"""
|
|
302
|
-
|
|
303
|
-
Features:
|
|
304
|
-
- PubChemPy integration for reliable API access
|
|
305
|
-
- SQLite caching for faster repeated lookups
|
|
306
|
-
- RDKit validation and canonicalization
|
|
307
|
-
- Comprehensive molecular properties
|
|
308
|
-
- Usage statistics and cache management
|
|
23
|
+
def rowan_molecule_lookup(molecule_name: str) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Convert a molecule name to SMILES using Chemical Identifier Resolver.
|
|
309
26
|
|
|
310
27
|
Args:
|
|
311
|
-
molecule_name: Name of the molecule (e.g.,
|
|
312
|
-
show_properties: Include molecular properties in output
|
|
28
|
+
molecule_name (str): Name of the molecule (e.g., 'aspirin', 'benzene')
|
|
313
29
|
|
|
314
30
|
Returns:
|
|
315
|
-
|
|
31
|
+
str: SMILES notation, or error message if not found
|
|
316
32
|
"""
|
|
33
|
+
smiles = CIRconvert(molecule_name)
|
|
317
34
|
|
|
318
|
-
if not
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
formatted += "• Molecular properties - MW, LogP, H-bond donors/acceptors\n\n"
|
|
328
|
-
|
|
329
|
-
formatted += "**Usage Examples:**\n"
|
|
330
|
-
formatted += "• rowan_molecule_lookup('aspirin') - Look up pharmaceuticals\n"
|
|
331
|
-
formatted += "• rowan_molecule_lookup('taxol') - Complex natural products\n"
|
|
332
|
-
formatted += "• rowan_molecule_lookup('remdesivir') - Modern drugs\n"
|
|
333
|
-
formatted += "• rowan_molecule_lookup('SMILES_STRING') - Validate existing SMILES\n\n"
|
|
334
|
-
|
|
335
|
-
formatted += "**Cache Statistics:**\n"
|
|
336
|
-
formatted += f"• Cached molecules: {stats['total_cached_molecules']}\n"
|
|
337
|
-
formatted += f"• Cache hits: {stats['total_cache_hits']}\n"
|
|
338
|
-
formatted += f"• API calls made: {stats['total_api_calls']}\n"
|
|
339
|
-
formatted += f"• Failed lookups: {stats['total_failures']}\n\n"
|
|
340
|
-
|
|
341
|
-
formatted += "**Dependencies Status:**\n"
|
|
342
|
-
formatted += f"• PubChemPy: {'✓ Available' if PUBCHEMPY_AVAILABLE else '✗ Missing (pip install pubchempy)'}\n"
|
|
343
|
-
formatted += f"• RDKit: {'✓ Available' if RDKIT_AVAILABLE else '✗ Missing (pip install rdkit)'}\n"
|
|
344
|
-
|
|
345
|
-
return formatted
|
|
35
|
+
if smiles == 'Did not work':
|
|
36
|
+
return f"{molecule_name}: Not found"
|
|
37
|
+
else:
|
|
38
|
+
return smiles.strip() # Remove any trailing newlines
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def batch_convert(identifiers):
|
|
42
|
+
"""
|
|
43
|
+
Convert multiple molecule identifiers to SMILES.
|
|
346
44
|
|
|
347
|
-
|
|
348
|
-
|
|
45
|
+
Args:
|
|
46
|
+
identifiers (list): List of molecule names/identifiers
|
|
349
47
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
formatted += "• Check internet connection for PubChem access\n"
|
|
355
|
-
formatted += "• Verify molecule name spelling\n"
|
|
356
|
-
formatted += "• Try alternative names or systematic names\n"
|
|
357
|
-
return formatted
|
|
48
|
+
Returns:
|
|
49
|
+
dict: Dictionary mapping identifiers to SMILES
|
|
50
|
+
"""
|
|
51
|
+
results = {}
|
|
358
52
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
formatted += "**Searched in:**\n"
|
|
362
|
-
formatted += "• PubChem database (via PubChemPy)\n"
|
|
363
|
-
formatted += "• Local SQLite cache\n\n"
|
|
364
|
-
formatted += "**Suggestions:**\n"
|
|
365
|
-
formatted += "• Check spelling of molecule name\n"
|
|
366
|
-
formatted += "• Try alternative names (e.g., 'acetaminophen' vs 'paracetamol')\n"
|
|
367
|
-
formatted += "• Try systematic IUPAC name\n"
|
|
368
|
-
formatted += "• Try CAS registry number\n"
|
|
369
|
-
formatted += "• If you have a SMILES string, it will be validated automatically\n"
|
|
370
|
-
return formatted
|
|
53
|
+
for ids in identifiers:
|
|
54
|
+
results[ids] = CIRconvert(ids)
|
|
371
55
|
|
|
372
|
-
|
|
373
|
-
source_names = {
|
|
374
|
-
'pubchem': 'PubChem Database (via PubChemPy)',
|
|
375
|
-
'input_smiles': 'Input SMILES Validation (RDKit)',
|
|
376
|
-
'cache': 'Local Cache'
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
formatted = f"**SMILES lookup successful!** {'(Cached)' if metadata.get('cached') else ''}\n\n"
|
|
380
|
-
formatted += f"**Molecule:** {molecule_name}\n"
|
|
381
|
-
formatted += f"**Canonical SMILES:** {smiles}\n"
|
|
382
|
-
formatted += f"**Source:** {source_names.get(source, source)}\n\n"
|
|
383
|
-
|
|
384
|
-
# Add molecular information if available
|
|
385
|
-
if metadata.get('name') and metadata['name'] != molecule_name:
|
|
386
|
-
formatted += f"**Common Name:** {metadata['name']}\n"
|
|
387
|
-
|
|
388
|
-
if metadata.get('iupac_name'):
|
|
389
|
-
formatted += f"**IUPAC Name:** {metadata['iupac_name']}\n"
|
|
390
|
-
|
|
391
|
-
if metadata.get('formula'):
|
|
392
|
-
formatted += f"**Formula:** {metadata['formula']}\n"
|
|
393
|
-
|
|
394
|
-
if metadata.get('cid'):
|
|
395
|
-
formatted += f"**PubChem CID:** {metadata['cid']}\n"
|
|
396
|
-
|
|
397
|
-
# Add molecular properties if requested or available
|
|
398
|
-
if show_properties or any(key in metadata for key in ['molecular_weight', 'logp', 'hbd', 'hba']):
|
|
399
|
-
formatted += "\n**Molecular Properties:**\n"
|
|
400
|
-
|
|
401
|
-
if metadata.get('molecular_weight'):
|
|
402
|
-
formatted += f"• Molecular Weight: {metadata['molecular_weight']:.2f} g/mol\n"
|
|
403
|
-
|
|
404
|
-
if metadata.get('logp') is not None:
|
|
405
|
-
formatted += f"• LogP: {metadata['logp']:.2f}\n"
|
|
406
|
-
|
|
407
|
-
if metadata.get('hbd') is not None:
|
|
408
|
-
formatted += f"• H-bond Donors: {metadata['hbd']}\n"
|
|
409
|
-
|
|
410
|
-
if metadata.get('hba') is not None:
|
|
411
|
-
formatted += f"• H-bond Acceptors: {metadata['hba']}\n"
|
|
412
|
-
|
|
413
|
-
if metadata.get('rotatable_bonds') is not None:
|
|
414
|
-
formatted += f"• Rotatable Bonds: {metadata['rotatable_bonds']}\n"
|
|
415
|
-
|
|
416
|
-
if metadata.get('aromatic_rings') is not None:
|
|
417
|
-
formatted += f"• Aromatic Rings: {metadata['aromatic_rings']}\n"
|
|
418
|
-
|
|
419
|
-
formatted += f"\n**Usage:** Use '{smiles}' in Rowan calculations for consistent results\n"
|
|
420
|
-
|
|
421
|
-
return formatted
|
|
422
|
-
|
|
423
|
-
def test_rowan_molecule_lookup():
|
|
424
|
-
"""Test the advanced molecule lookup function."""
|
|
425
|
-
try:
|
|
426
|
-
print("Testing advanced molecule lookup...")
|
|
427
|
-
|
|
428
|
-
# Test common molecule
|
|
429
|
-
print("1. Testing phenol...")
|
|
430
|
-
result1 = rowan_molecule_lookup("phenol")
|
|
431
|
-
print("✓ Phenol lookup successful")
|
|
432
|
-
|
|
433
|
-
# Test cache stats
|
|
434
|
-
print("2. Testing cache statistics...")
|
|
435
|
-
result2 = rowan_molecule_lookup("")
|
|
436
|
-
print("✓ Cache statistics successful")
|
|
437
|
-
|
|
438
|
-
print("Advanced molecule lookup test successful!")
|
|
439
|
-
return True
|
|
440
|
-
except Exception as e:
|
|
441
|
-
print(f"Advanced molecule lookup test failed: {e}")
|
|
442
|
-
return False
|
|
443
|
-
|
|
444
|
-
if __name__ == "__main__":
|
|
445
|
-
test_rowan_molecule_lookup()
|
|
56
|
+
return results
|
|
446
57
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rowan-mcp
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Model Context Protocol server for Rowan computational chemistry platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/k-yenko/rowan-mcp
|
|
6
|
-
Author-email: Katherine Yenko <
|
|
6
|
+
Author-email: Katherine Yenko <katherineayenko@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
9
9
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# Rowan MCP Server
|
|
34
34
|
|
|
35
|
-
This project wraps an MCP (Model Context Protocol) around Rowan's tools, making it easy for users to
|
|
35
|
+
This project wraps an MCP (Model Context Protocol) around Rowan's tools, making it easy for users to design molecuels and run simulations in natural everyday language.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -50,31 +50,34 @@ That's it - no command line setup needed!
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## **
|
|
53
|
+
## **Package Installation**
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
### **Option 1: Auto-Install (No manual installation needed!)**
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
git clone https://github.com/k-yenko/rowan-mcp.git
|
|
60
|
-
cd rowan-mcp
|
|
61
|
-
uv sync
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### **2. Get API Key**
|
|
65
|
-
- Visit [labs.rowansci.com](https://labs.rowansci.com)
|
|
66
|
-
- Create free account → Generate API key
|
|
57
|
+
Just add this to your MCP configuration and it will automatically install and run:
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
**Using uvx (simplest):**
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"rowan": {
|
|
64
|
+
"command": "uvx",
|
|
65
|
+
"args": ["--from", "rowan-mcp", "rowan-mcp"],
|
|
66
|
+
"env": {
|
|
67
|
+
"ROWAN_API_KEY": "your_api_key_here"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
69
73
|
|
|
70
|
-
**
|
|
74
|
+
**Using uv run (alternative):**
|
|
71
75
|
```json
|
|
72
76
|
{
|
|
73
77
|
"mcpServers": {
|
|
74
78
|
"rowan": {
|
|
75
79
|
"command": "uv",
|
|
76
|
-
"args": ["run", "
|
|
77
|
-
"cwd": "/path/to/rowan-mcp",
|
|
80
|
+
"args": ["run", "--with", "rowan-mcp", "-m", "rowan_mcp"],
|
|
78
81
|
"env": {
|
|
79
82
|
"ROWAN_API_KEY": "your_api_key_here"
|
|
80
83
|
}
|
|
@@ -83,77 +86,68 @@ uv sync
|
|
|
83
86
|
}
|
|
84
87
|
```
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
### **Option 2: Manual Installation**
|
|
87
90
|
|
|
88
|
-
|
|
91
|
+
If you prefer to install the package first:
|
|
92
|
+
|
|
93
|
+
**Using uv:**
|
|
89
94
|
```bash
|
|
90
|
-
|
|
91
|
-
pwd
|
|
92
|
-
# Copy the output and use it as your "cwd" value
|
|
95
|
+
uv add rowan-mcp
|
|
93
96
|
```
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
Ask your AI: *"Calculate the pKa of aspirin"* or *"Optimize the geometry of caffeine"*
|
|
97
|
-
|
|
98
|
-
### **Alternative: Use .env file**
|
|
99
|
-
Instead of putting your API key in the MCP config, create a `.env` file:
|
|
98
|
+
**Using pip:**
|
|
100
99
|
```bash
|
|
101
|
-
|
|
102
|
-
echo "ROWAN_API_KEY=your_actual_api_key_here" > .env
|
|
100
|
+
pip install rowan-mcp
|
|
103
101
|
```
|
|
104
102
|
|
|
105
|
-
Then use this
|
|
103
|
+
Then use this configuration:
|
|
106
104
|
```json
|
|
107
105
|
{
|
|
108
106
|
"mcpServers": {
|
|
109
107
|
"rowan": {
|
|
110
|
-
"command": "
|
|
111
|
-
"
|
|
112
|
-
|
|
108
|
+
"command": "rowan-mcp",
|
|
109
|
+
"env": {
|
|
110
|
+
"ROWAN_API_KEY": "your_api_key_here"
|
|
111
|
+
}
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
```
|
|
117
116
|
|
|
117
|
+
### **Get API Key**
|
|
118
|
+
|
|
119
|
+
Visit [labs.rowansci.com](https://labs.rowansci.com) → Create account → Generate API key
|
|
120
|
+
|
|
121
|
+
### **Start Using**
|
|
122
|
+
|
|
123
|
+
Ask your AI: *"Calculate the pKa of aspirin"* or *"Optimize the geometry of caffeine"*
|
|
124
|
+
|
|
118
125
|
---
|
|
119
126
|
|
|
120
127
|
## **What You Can Do**
|
|
121
128
|
|
|
122
129
|
Ask the LLM to:
|
|
123
130
|
- **Calculate drug properties**: *"Predict drug-likeness of aspirin"*
|
|
124
|
-
- **Optimize molecular structures**: *"Optimize the geometry of aspirin"*
|
|
131
|
+
- **Optimize molecular structures**: *"Optimize the geometry of aspirin"*
|
|
125
132
|
- **Predict chemical behavior**: *"What's the pKa of acetic acid?"*
|
|
126
133
|
- **Run calculations**: *"Calculate the HOMO and LUMO of benzene"*
|
|
127
134
|
|
|
128
135
|
## **System Requirements**
|
|
129
136
|
|
|
130
137
|
- **Python 3.10+** (Python 3.11+ recommended)
|
|
131
|
-
- **[uv](https://docs.astral.sh/uv/)
|
|
138
|
+
- **Package manager**: [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
132
139
|
- **Rowan API key** (free at [labs.rowansci.com](https://labs.rowansci.com))
|
|
133
|
-
- **MCP-compatible client** (Claude Desktop,
|
|
134
|
-
|
|
135
|
-
## **Testing Your Setup**
|
|
140
|
+
- **MCP-compatible client** (Claude Desktop, etc.)
|
|
136
141
|
|
|
137
|
-
|
|
142
|
+
**Development commands** (if you cloned the repo):
|
|
138
143
|
```bash
|
|
139
|
-
#
|
|
140
|
-
uv run python -m rowan_mcp --help
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## **Development**
|
|
144
|
-
|
|
145
|
-
The installation above is the same for development! Additional commands:
|
|
146
|
-
```bash
|
|
147
|
-
# Run server in HTTP/SSE mode
|
|
144
|
+
# Run from source
|
|
148
145
|
uv run python -m rowan_mcp --http
|
|
149
|
-
|
|
150
|
-
# Run server in STDIO mode (default)
|
|
151
|
-
uv run python -m rowan_mcp
|
|
152
146
|
```
|
|
153
147
|
|
|
154
148
|
---
|
|
155
149
|
|
|
156
|
-
## Available Tools
|
|
150
|
+
## **Available Tools**
|
|
157
151
|
|
|
158
152
|
### Chemistry Calculations
|
|
159
153
|
- `rowan_basic_calculation` - Energy, optimization, frequencies
|
|
@@ -172,7 +166,6 @@ uv run python -m rowan_mcp
|
|
|
172
166
|
### Drug Discovery
|
|
173
167
|
- `rowan_admet` - ADME-Tox properties
|
|
174
168
|
|
|
175
|
-
|
|
176
169
|
### Reactivity Analysis
|
|
177
170
|
- `rowan_fukui` - Reactivity sites
|
|
178
171
|
- `rowan_spin_states` - Spin multiplicities
|
|
@@ -181,13 +174,13 @@ uv run python -m rowan_mcp
|
|
|
181
174
|
- `rowan_folder_create/list/update/delete` - Organize calculations
|
|
182
175
|
- `rowan_workflow_create/list/status/stop` - Manage workflows
|
|
183
176
|
|
|
184
|
-
## Requirements
|
|
177
|
+
## **Requirements**
|
|
185
178
|
|
|
186
179
|
- Python 3.10+
|
|
187
180
|
- Rowan API key
|
|
188
181
|
- MCP-compatible AI assistant (Claude Desktop, etc.)
|
|
189
182
|
|
|
190
|
-
## Getting Help
|
|
183
|
+
## **Getting Help**
|
|
191
184
|
|
|
192
185
|
- **Documentation**: [docs.rowansci.com](https://docs.rowansci.com/)
|
|
193
186
|
- or ping me!
|
|
@@ -207,10 +200,28 @@ uv run python -m rowan_mcp
|
|
|
207
200
|
- [ ] Multistage optimization sometimes shows unexpected imaginary frequencies
|
|
208
201
|
- [ ] Some calculations show as finished in logs but not in Rowan UI
|
|
209
202
|
|
|
210
|
-
## Citation
|
|
203
|
+
## **Citation**
|
|
211
204
|
|
|
212
205
|
If you use this MCP tool in your research, please cite the underlying Rowan platform:
|
|
213
206
|
|
|
214
207
|
Rowan Scientific. https://www.rowansci.com (accessed 2025-07-01).
|
|
215
208
|
|
|
216
209
|
For complete citation information including specific computational engines, methods, and workflows used in your calculations, please refer to [Rowan's citation guidelines](https://docs.rowansci.com/citations).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## **Publishing (Maintainer Notes)**
|
|
214
|
+
|
|
215
|
+
To publish a new version to PyPI:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Update version in pyproject.toml and rowan_mcp/__init__.py
|
|
219
|
+
# Build the package
|
|
220
|
+
uv build
|
|
221
|
+
|
|
222
|
+
# Publish to PyPI (requires API token)
|
|
223
|
+
uv publish
|
|
224
|
+
|
|
225
|
+
# Or publish to TestPyPI first
|
|
226
|
+
uv publish --index-url https://test.pypi.org/simple/
|
|
227
|
+
```
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
rowan_mcp/__init__.py,sha256=
|
|
1
|
+
rowan_mcp/__init__.py,sha256=P0K_WQJCQuyQtG4fNFto6Wutijtle32lG9sFB96cYiw,381
|
|
2
2
|
rowan_mcp/__main__.py,sha256=I7wCpoCPLrCx_tWozFBtCtnoL65lmgDiZnEumj3vijM,388
|
|
3
3
|
rowan_mcp/server.py,sha256=47MsJchxzqRVd4mFQXR81wYXBX8B8fFUkl7wDYB5uMs,7702
|
|
4
4
|
rowan_mcp/functions/admet.py,sha256=m_RD7OJ8GDocEInHw4zOmk4tBf2mWBGTL3LVt-vo_mU,2438
|
|
5
5
|
rowan_mcp/functions/bde.py,sha256=x6Wmnqzy3zRDvSM_AlxSm_0ksg9lI2zV1PiBzXjU_sU,3634
|
|
6
6
|
rowan_mcp/functions/calculation_retrieve.py,sha256=jL28RKvZX3QUeZ_qDlBfLdGG_Lsk6LyA1xjsHvPM-mg,3376
|
|
7
|
-
rowan_mcp/functions/conformers.py,sha256=
|
|
7
|
+
rowan_mcp/functions/conformers.py,sha256=2Bjim0v5xajWykVwRU9YPwtNyqIo6n2wiLDgR2_5wE8,2552
|
|
8
8
|
rowan_mcp/functions/descriptors.py,sha256=HJXsMZmgx-PdqfSe39D0BcMxzRYmR38gt98fXIMc69w,2747
|
|
9
9
|
rowan_mcp/functions/docking.py,sha256=J-bcgig_68x07eCpVPG1ZJKdUFsPVeDuvhoeM9Y9V9I,13954
|
|
10
10
|
rowan_mcp/functions/docking_enhanced.py,sha256=lwviIWg_26VW8VlITIlHd-Lbw8E25tnvNrLfzK6fLJs,5528
|
|
11
|
-
rowan_mcp/functions/electronic_properties.py,sha256=
|
|
11
|
+
rowan_mcp/functions/electronic_properties.py,sha256=hBpUz0lP5ImRS8U8Cqd04tl_vzyoUSl3eP1sHKvG7Yg,8154
|
|
12
12
|
rowan_mcp/functions/folder_management.py,sha256=qPZ6cjC2AFxr1BhXpRsJk2AZAF7GV5ojy6oJDQjYKbw,4874
|
|
13
|
-
rowan_mcp/functions/fukui.py,sha256=
|
|
13
|
+
rowan_mcp/functions/fukui.py,sha256=7uz0kQ-Bt9_m4YXto4BlEJFbAknusnwZlfl67tWrwmg,7750
|
|
14
14
|
rowan_mcp/functions/hydrogen_bond_basicity.py,sha256=xH7czHEF_stKnQoh-F93b8G8936DCTYjkf6nTa7kmKo,3081
|
|
15
15
|
rowan_mcp/functions/irc.py,sha256=ulMfkpVTXoQDwFVzAJRcEbyZpFxM2LucO7Mq-XkgNTs,4096
|
|
16
|
-
rowan_mcp/functions/macropka.py,sha256=
|
|
16
|
+
rowan_mcp/functions/macropka.py,sha256=ze0D8R5-1-FLfOLXNe06I-BUAUKF9CHeWuTYw30sT1s,3985
|
|
17
17
|
rowan_mcp/functions/molecular_converter.py,sha256=j9YeCnaHZahwkhG2EZNPu5VVmGy2sqIHPB_qf1ojoec,17349
|
|
18
18
|
rowan_mcp/functions/molecular_dynamics.py,sha256=yzA03LeFv8K59Cg1SAnavWwmodl4_KW667pRHJQTXNw,6990
|
|
19
|
-
rowan_mcp/functions/
|
|
20
|
-
rowan_mcp/functions/molecule_lookup.py,sha256=I0infAB-9zN6LNCEyEQNDvLvlQ5L5nOrVJGWF6jLD9s,18466
|
|
19
|
+
rowan_mcp/functions/molecule_lookup.py,sha256=Ff3ARljNbLlGgSinREl6OFxoJ-HVXtbXPxy-r_6CMKs,1467
|
|
21
20
|
rowan_mcp/functions/multistage_opt.py,sha256=lWwgXZgpXnWsjgonkA1toks4t01Cdxo822xmT2EOssM,6185
|
|
22
21
|
rowan_mcp/functions/pdb_handler.py,sha256=EnhRqxStnke5kiSnDaWOzcJT8fAHW6VVIhTaH6ODkWE,6241
|
|
23
22
|
rowan_mcp/functions/pka.py,sha256=EFGIFtq2HrtNzcU5-3-ncgmpiwIGnOE-vROjr_eC1Nk,5014
|
|
@@ -29,7 +28,7 @@ rowan_mcp/functions/spin_states.py,sha256=NG7uJjTi_Fx-E4Qr7RzjNhfFmKlHfIGMD0Uhyo
|
|
|
29
28
|
rowan_mcp/functions/system_management.py,sha256=UwdKD46FNEJh1zEPpvFW7-JBD6g8x-xSbmH7lrcubx0,20089
|
|
30
29
|
rowan_mcp/functions/tautomers.py,sha256=oXFUpMgCVtXy2JnyCb8G04vYj_anJj4WThD26ZGOsZ0,2694
|
|
31
30
|
rowan_mcp/functions/workflow_management.py,sha256=EqXRqj0EuJz7h2igqOHBpq23Qyo-KT9geWp39URacxw,21130
|
|
32
|
-
rowan_mcp-0.1.
|
|
33
|
-
rowan_mcp-0.1.
|
|
34
|
-
rowan_mcp-0.1.
|
|
35
|
-
rowan_mcp-0.1.
|
|
31
|
+
rowan_mcp-1.0.1.dist-info/METADATA,sha256=LMlceF8ZjrMjgaJdx7sxCEskrF7E2jMBQsnCyOVFZl0,6262
|
|
32
|
+
rowan_mcp-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
rowan_mcp-1.0.1.dist-info/entry_points.txt,sha256=QkmK3GHkTNA6gqyTIFrl2V2eVBm-VBdRAlDNsvi4Rl0,52
|
|
34
|
+
rowan_mcp-1.0.1.dist-info/RECORD,,
|
|
Binary file
|
|
File without changes
|
|
File without changes
|