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.

@@ -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
- ) -> str:
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
- result = log_rowan_api_call(
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
- # Enhanced result formatting for electronic properties
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
- error_msg = f"Electronic properties calculation failed: {str(e)}\n\n"
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."""
@@ -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 for common molecule names."""
58
- # Common molecule SMILES database
59
- MOLECULE_SMILES = {
60
- # Aromatics
61
- "phenol": "Oc1ccccc1",
62
- "benzene": "c1ccccc1",
63
- "toluene": "Cc1ccccc1",
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
- # Solvents
70
- "water": "O",
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
- # Others
80
- "glucose": "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O",
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
- # If no match found, return the original input (assume it's already SMILES)
103
- return molecule_name
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 SMILES string or common name (e.g., "phenol", "benzene")
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
- result = log_rowan_api_call(
216
- workflow_type="fukui",
217
- **fukui_params
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
- # Try to extract Fukui results
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
- # Fukui indices per atom
264
- fukui_available = []
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."""
@@ -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 json.dumps({"error": "min_pH must be less than max_pH"})
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 json.dumps({"error": "min_charge must be less than max_charge"})
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
- if blocking:
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 json.dumps({"error": str(e)})
108
+ return f"MacropKa calculation failed: {str(e)}"
184
109
 
185
110
 
186
111
  # Test function
@@ -1,446 +1,57 @@
1
- """
2
- Advanced molecule lookup using PubChemPy + SQLite Cache + RDKit validation.
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
- # Set up logging
12
- logger = logging.getLogger(__name__)
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
- def _cache_result(self, identifier: str, original_smiles: str, canonical_smiles: str,
244
- name: str, iupac_name: str, formula: str,
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
- def get_cache_stats(self) -> dict:
262
- """Get cache usage statistics."""
263
- cursor = self.conn.execute('''
264
- SELECT COUNT(*) as total_entries,
265
- COUNT(CASE WHEN source = 'pubchem' THEN 1 END) as pubchem_entries,
266
- COUNT(CASE WHEN source = 'input_smiles' THEN 1 END) as smiles_entries
267
- FROM molecules
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, show_properties: bool = False) -> str:
301
- """Advanced molecule lookup with PubChem API, SQLite caching, and RDKit validation.
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., "aspirin", "taxol", "remdesivir")
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
- Comprehensive molecule information with canonical SMILES
31
+ str: SMILES notation, or error message if not found
316
32
  """
33
+ smiles = CIRconvert(molecule_name)
317
34
 
318
- if not molecule_name.strip():
319
- lookup = get_lookup_instance()
320
- stats = lookup.get_cache_stats()
321
-
322
- formatted = "**Advanced Molecule SMILES Lookup**\n\n"
323
- formatted += "**Features:**\n"
324
- formatted += "• PubChemPy integration - Official PubChem API access\n"
325
- formatted += "• SQLite caching - Faster repeated lookups\n"
326
- formatted += "• RDKit validation - Canonical SMILES standardization\n"
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
- lookup = get_lookup_instance()
348
- smiles, source, metadata = lookup.get_smiles(molecule_name)
45
+ Args:
46
+ identifiers (list): List of molecule names/identifiers
349
47
 
350
- if source == 'error':
351
- formatted = f"**Lookup Error for '{molecule_name}'**\n\n"
352
- formatted += f"**Error:** {metadata.get('error', 'Unknown error')}\n\n"
353
- formatted += "**Troubleshooting:**\n"
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
- elif source == 'not_found':
360
- formatted = f"**No results found for '{molecule_name}'**\n\n"
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
- else:
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.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 <katherineyenko@example.com>
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 submit complex quantum chemistry calculations in natural everyday language.
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
- ### **Using uv (recommended):**
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
- 1. **Get your API key**: Visit [labs.rowansci.com](https://labs.rowansci.com) Create account Generate API key
57
+ Just add this to your MCP configuration and it will automatically install and run:
75
58
 
76
- 2. **Configure your MCP client** (e.g., Claude Code, VSCode, Cursor, etc.):
77
-
78
- **With uv:**
59
+ **Using uvx (simplest):**
79
60
  ```json
80
61
  {
81
62
  "mcpServers": {
82
63
  "rowan": {
83
- "command": "uv",
84
- "args": ["run", "rowan-mcp"],
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
- **With pip/system Python:**
74
+ **Using uv run (alternative):**
94
75
  ```json
95
76
  {
96
77
  "mcpServers": {
97
78
  "rowan": {
98
- "command": "rowan-mcp",
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
- ### **Start Using**
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
- **For developers or contributors:**
93
+ **Using uv:**
94
+ ```bash
95
+ uv add rowan-mcp
96
+ ```
115
97
 
116
- ### **1. Clone and Setup**
98
+ **Using pip:**
117
99
  ```bash
118
- git clone https://github.com/k-yenko/rowan-mcp.git
119
- cd rowan-mcp
120
- uv sync
100
+ pip install rowan-mcp
121
101
  ```
122
102
 
123
- ### **2. Configure for Development**
103
+ Then use this configuration:
124
104
  ```json
125
105
  {
126
106
  "mcpServers": {
127
107
  "rowan": {
128
- "command": "uv",
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
- *Replace `/path/to/rowan-mcp` with your actual clone path. Find it with `pwd` in the project directory.*
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=tzMMeZ44Vw5ZmD9wZtGxyFlwoeu1izr6wdAFNwwwGKA,5171
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=qJpn-iEXTsk_wVcHYVq-7TLK7_T3JP-z7viHxg2L434,11741
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=HvR8qC_eBJ2ojGhf50DIGVr9luowMvfPnU8txyfCVaQ,15339
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=CreAYpGKpJ9pQBgpEHXnN3kCH33qbm08Xa7jKhPkMic,8219
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/molecule_cache.db,sha256=Fq1LLzSFPy4f7MrZPLhMhU3yA_gE10YQUPLhIQcfHQU,28672
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.0.dist-info/METADATA,sha256=wqqheHW12cXQmu4Ocp-j551vbx1oJ72P5kB85Q65q8g,6628
33
- rowan_mcp-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- rowan_mcp-1.0.0.dist-info/entry_points.txt,sha256=QkmK3GHkTNA6gqyTIFrl2V2eVBm-VBdRAlDNsvi4Rl0,52
35
- rowan_mcp-1.0.0.dist-info/RECORD,,
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