rowan-mcp 1.0.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/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-1.0.0.dist-info → rowan_mcp-1.0.1.dist-info}/METADATA +32 -50
- {rowan_mcp-1.0.0.dist-info → rowan_mcp-1.0.1.dist-info}/RECORD +9 -10
- rowan_mcp/functions/molecule_cache.db +0 -0
- {rowan_mcp-1.0.0.dist-info → rowan_mcp-1.0.1.dist-info}/WHEEL +0 -0
- {rowan_mcp-1.0.0.dist-info → rowan_mcp-1.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -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: 1.0.
|
|
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
|
|
|
@@ -52,36 +52,17 @@ That's it - no command line setup needed!
|
|
|
52
52
|
|
|
53
53
|
## **Package Installation**
|
|
54
54
|
|
|
55
|
-
### **
|
|
56
|
-
```bash
|
|
57
|
-
# Install the package
|
|
58
|
-
uv add rowan-mcp
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### **Using pip:**
|
|
62
|
-
```bash
|
|
63
|
-
# Install the package
|
|
64
|
-
pip install rowan-mcp
|
|
65
|
-
|
|
66
|
-
# Or in a virtual environment
|
|
67
|
-
python -m venv venv
|
|
68
|
-
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
69
|
-
pip install rowan-mcp
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### **Get API Key & Configure**
|
|
55
|
+
### **Option 1: Auto-Install (No manual installation needed!)**
|
|
73
56
|
|
|
74
|
-
|
|
57
|
+
Just add this to your MCP configuration and it will automatically install and run:
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
**With uv:**
|
|
59
|
+
**Using uvx (simplest):**
|
|
79
60
|
```json
|
|
80
61
|
{
|
|
81
62
|
"mcpServers": {
|
|
82
63
|
"rowan": {
|
|
83
|
-
"command": "
|
|
84
|
-
"args": ["
|
|
64
|
+
"command": "uvx",
|
|
65
|
+
"args": ["--from", "rowan-mcp", "rowan-mcp"],
|
|
85
66
|
"env": {
|
|
86
67
|
"ROWAN_API_KEY": "your_api_key_here"
|
|
87
68
|
}
|
|
@@ -90,12 +71,13 @@ pip install rowan-mcp
|
|
|
90
71
|
}
|
|
91
72
|
```
|
|
92
73
|
|
|
93
|
-
**
|
|
74
|
+
**Using uv run (alternative):**
|
|
94
75
|
```json
|
|
95
76
|
{
|
|
96
77
|
"mcpServers": {
|
|
97
78
|
"rowan": {
|
|
98
|
-
"command": "
|
|
79
|
+
"command": "uv",
|
|
80
|
+
"args": ["run", "--with", "rowan-mcp", "-m", "rowan_mcp"],
|
|
99
81
|
"env": {
|
|
100
82
|
"ROWAN_API_KEY": "your_api_key_here"
|
|
101
83
|
}
|
|
@@ -104,30 +86,26 @@ pip install rowan-mcp
|
|
|
104
86
|
}
|
|
105
87
|
```
|
|
106
88
|
|
|
107
|
-
### **
|
|
108
|
-
Ask your AI: *"Calculate the pKa of aspirin"* or *"Optimize the geometry of caffeine"*
|
|
89
|
+
### **Option 2: Manual Installation**
|
|
109
90
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
## **Development Installation**
|
|
91
|
+
If you prefer to install the package first:
|
|
113
92
|
|
|
114
|
-
**
|
|
93
|
+
**Using uv:**
|
|
94
|
+
```bash
|
|
95
|
+
uv add rowan-mcp
|
|
96
|
+
```
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
**Using pip:**
|
|
117
99
|
```bash
|
|
118
|
-
|
|
119
|
-
cd rowan-mcp
|
|
120
|
-
uv sync
|
|
100
|
+
pip install rowan-mcp
|
|
121
101
|
```
|
|
122
102
|
|
|
123
|
-
|
|
103
|
+
Then use this configuration:
|
|
124
104
|
```json
|
|
125
105
|
{
|
|
126
106
|
"mcpServers": {
|
|
127
107
|
"rowan": {
|
|
128
|
-
"command": "
|
|
129
|
-
"args": ["run", "python", "-m", "rowan_mcp"],
|
|
130
|
-
"cwd": "/path/to/rowan-mcp",
|
|
108
|
+
"command": "rowan-mcp",
|
|
131
109
|
"env": {
|
|
132
110
|
"ROWAN_API_KEY": "your_api_key_here"
|
|
133
111
|
}
|
|
@@ -136,7 +114,13 @@ uv sync
|
|
|
136
114
|
}
|
|
137
115
|
```
|
|
138
116
|
|
|
139
|
-
|
|
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"*
|
|
140
124
|
|
|
141
125
|
---
|
|
142
126
|
|
|
@@ -155,7 +139,6 @@ Ask the LLM to:
|
|
|
155
139
|
- **Rowan API key** (free at [labs.rowansci.com](https://labs.rowansci.com))
|
|
156
140
|
- **MCP-compatible client** (Claude Desktop, etc.)
|
|
157
141
|
|
|
158
|
-
|
|
159
142
|
**Development commands** (if you cloned the repo):
|
|
160
143
|
```bash
|
|
161
144
|
# Run from source
|
|
@@ -164,7 +147,7 @@ uv run python -m rowan_mcp --http
|
|
|
164
147
|
|
|
165
148
|
---
|
|
166
149
|
|
|
167
|
-
## Available Tools
|
|
150
|
+
## **Available Tools**
|
|
168
151
|
|
|
169
152
|
### Chemistry Calculations
|
|
170
153
|
- `rowan_basic_calculation` - Energy, optimization, frequencies
|
|
@@ -183,7 +166,6 @@ uv run python -m rowan_mcp --http
|
|
|
183
166
|
### Drug Discovery
|
|
184
167
|
- `rowan_admet` - ADME-Tox properties
|
|
185
168
|
|
|
186
|
-
|
|
187
169
|
### Reactivity Analysis
|
|
188
170
|
- `rowan_fukui` - Reactivity sites
|
|
189
171
|
- `rowan_spin_states` - Spin multiplicities
|
|
@@ -192,13 +174,13 @@ uv run python -m rowan_mcp --http
|
|
|
192
174
|
- `rowan_folder_create/list/update/delete` - Organize calculations
|
|
193
175
|
- `rowan_workflow_create/list/status/stop` - Manage workflows
|
|
194
176
|
|
|
195
|
-
## Requirements
|
|
177
|
+
## **Requirements**
|
|
196
178
|
|
|
197
179
|
- Python 3.10+
|
|
198
180
|
- Rowan API key
|
|
199
181
|
- MCP-compatible AI assistant (Claude Desktop, etc.)
|
|
200
182
|
|
|
201
|
-
## Getting Help
|
|
183
|
+
## **Getting Help**
|
|
202
184
|
|
|
203
185
|
- **Documentation**: [docs.rowansci.com](https://docs.rowansci.com/)
|
|
204
186
|
- or ping me!
|
|
@@ -218,7 +200,7 @@ uv run python -m rowan_mcp --http
|
|
|
218
200
|
- [ ] Multistage optimization sometimes shows unexpected imaginary frequencies
|
|
219
201
|
- [ ] Some calculations show as finished in logs but not in Rowan UI
|
|
220
202
|
|
|
221
|
-
## Citation
|
|
203
|
+
## **Citation**
|
|
222
204
|
|
|
223
205
|
If you use this MCP tool in your research, please cite the underlying Rowan platform:
|
|
224
206
|
|
|
@@ -4,20 +4,19 @@ 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-1.0.
|
|
33
|
-
rowan_mcp-1.0.
|
|
34
|
-
rowan_mcp-1.0.
|
|
35
|
-
rowan_mcp-1.0.
|
|
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
|