rowan-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rowan-mcp might be problematic. Click here for more details.
- rowan_mcp/__init__.py +14 -0
- rowan_mcp/__main__.py +14 -0
- rowan_mcp/functions/admet.py +94 -0
- rowan_mcp/functions/bde.py +113 -0
- rowan_mcp/functions/calculation_retrieve.py +89 -0
- rowan_mcp/functions/conformers.py +135 -0
- rowan_mcp/functions/descriptors.py +92 -0
- rowan_mcp/functions/docking.py +340 -0
- rowan_mcp/functions/docking_enhanced.py +174 -0
- rowan_mcp/functions/electronic_properties.py +263 -0
- rowan_mcp/functions/folder_management.py +137 -0
- rowan_mcp/functions/fukui.py +355 -0
- rowan_mcp/functions/hydrogen_bond_basicity.py +94 -0
- rowan_mcp/functions/irc.py +125 -0
- rowan_mcp/functions/macropka.py +195 -0
- rowan_mcp/functions/molecular_converter.py +423 -0
- rowan_mcp/functions/molecular_dynamics.py +191 -0
- rowan_mcp/functions/molecule_cache.db +0 -0
- rowan_mcp/functions/molecule_lookup.py +446 -0
- rowan_mcp/functions/multistage_opt.py +171 -0
- rowan_mcp/functions/pdb_handler.py +200 -0
- rowan_mcp/functions/pka.py +137 -0
- rowan_mcp/functions/redox_potential.py +352 -0
- rowan_mcp/functions/scan.py +536 -0
- rowan_mcp/functions/scan_analyzer.py +347 -0
- rowan_mcp/functions/solubility.py +277 -0
- rowan_mcp/functions/spin_states.py +747 -0
- rowan_mcp/functions/system_management.py +368 -0
- rowan_mcp/functions/tautomers.py +91 -0
- rowan_mcp/functions/workflow_management.py +422 -0
- rowan_mcp/server.py +169 -0
- rowan_mcp-0.1.0.dist-info/METADATA +216 -0
- rowan_mcp-0.1.0.dist-info/RECORD +35 -0
- rowan_mcp-0.1.0.dist-info/WHEEL +4 -0
- rowan_mcp-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan workflow management functions for MCP tool integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Dict, Any, Union, List, Literal
|
|
6
|
+
import rowan
|
|
7
|
+
|
|
8
|
+
def safe_get_attr(obj, attr: str, default=None):
|
|
9
|
+
"""Safely get an attribute from an object, returning default if it doesn't exist."""
|
|
10
|
+
try:
|
|
11
|
+
return getattr(obj, attr, default)
|
|
12
|
+
except (AttributeError, TypeError):
|
|
13
|
+
return default
|
|
14
|
+
|
|
15
|
+
def rowan_workflow_management(
|
|
16
|
+
action: Literal['create', 'retrieve', 'update', 'stop', 'status', 'is_finished', 'delete', 'list'],
|
|
17
|
+
workflow_uuid: Optional[str] = None,
|
|
18
|
+
name: Optional[str] = None,
|
|
19
|
+
workflow_type: Optional[str] = None,
|
|
20
|
+
initial_molecule: Optional[str] = None,
|
|
21
|
+
parent_uuid: Optional[str] = None,
|
|
22
|
+
notes: Optional[str] = None,
|
|
23
|
+
starred: Optional[bool] = None,
|
|
24
|
+
public: Optional[bool] = None,
|
|
25
|
+
email_when_complete: Optional[bool] = None,
|
|
26
|
+
workflow_data: Optional[Dict[str, Any]] = None,
|
|
27
|
+
name_contains: Optional[str] = None,
|
|
28
|
+
object_status: Optional[int] = None,
|
|
29
|
+
object_type: Optional[str] = None,
|
|
30
|
+
page: int = 0,
|
|
31
|
+
size: int = 50
|
|
32
|
+
) -> str:
|
|
33
|
+
"""Unified workflow management tool for all workflow operations. Available actions: create, retrieve, update, stop, status, is_finished, delete, list.
|
|
34
|
+
|
|
35
|
+
**Available Actions:**
|
|
36
|
+
- **create**: Create a new workflow (requires: name, workflow_type, initial_molecule)
|
|
37
|
+
- **retrieve**: retrieve workflow details (requires: workflow_uuid)
|
|
38
|
+
- **update**: Update workflow properties (requires: workflow_uuid, optional: name, parent_uuid, notes, starred, public, email_when_complete)
|
|
39
|
+
- **stop**: Stop a running workflow (requires: workflow_uuid)
|
|
40
|
+
- **status**: Check workflow status (requires: workflow_uuid)
|
|
41
|
+
- **is_finished**: Check if workflow is finished (requires: workflow_uuid)
|
|
42
|
+
- **delete**: Delete a workflow (requires: workflow_uuid)
|
|
43
|
+
- **list**: List workflows with filters (optional: name_contains, parent_uuid, starred, public, object_status, object_type, page, size)
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
action: Action to perform - must be one of: 'create', 'retrieve', 'update', 'stop', 'status', 'is_finished', 'delete', 'list'
|
|
47
|
+
workflow_uuid: UUID of the workflow (required for retrieve, update, stop, status, is_finished, delete)
|
|
48
|
+
name: Workflow name (required for create, optional for update)
|
|
49
|
+
workflow_type: Type of workflow (required for create)
|
|
50
|
+
initial_molecule: Initial molecule SMILES (required for create)
|
|
51
|
+
parent_uuid: Parent folder UUID (optional for create/update)
|
|
52
|
+
notes: Workflow notes (optional for create/update)
|
|
53
|
+
starred: Star the workflow (optional for create/update)
|
|
54
|
+
public: Make workflow public (optional for create/update)
|
|
55
|
+
email_when_complete: Email when complete (optional for create/update)
|
|
56
|
+
workflow_data: Additional workflow data (optional for create)
|
|
57
|
+
name_contains: Filter by name containing text (optional for list)
|
|
58
|
+
object_status: Filter by status (0=queued, 1=running, 2=completed, 3=failed, 4=stopped, optional for list)
|
|
59
|
+
object_type: Filter by workflow type (optional for list)
|
|
60
|
+
page: Page number for pagination (default: 1, for list)
|
|
61
|
+
size: Results per page (default: 50, for list)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Results of the workflow operation
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
action = action.lower()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
if action == "create":
|
|
71
|
+
if not all([name, workflow_type, initial_molecule]):
|
|
72
|
+
return " Error: 'name', 'workflow_type', and 'initial_molecule' are required for creating a workflow"
|
|
73
|
+
|
|
74
|
+
# Validate workflow type
|
|
75
|
+
VALID_WORKFLOWS = {
|
|
76
|
+
"admet", "basic_calculation", "conformer_search", "descriptors",
|
|
77
|
+
"docking", "electronic_properties", "fukui",
|
|
78
|
+
"irc", "molecular_dynamics", "multistage_opt", "pka", "redox_potential",
|
|
79
|
+
"scan", "solubility", "spin_states", "tautomers"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if workflow_type not in VALID_WORKFLOWS:
|
|
83
|
+
error_msg = f" Invalid workflow_type '{workflow_type}'.\n\n"
|
|
84
|
+
error_msg += " **Available Rowan Workflow Types:**\n"
|
|
85
|
+
error_msg += f"{', '.join(sorted(VALID_WORKFLOWS))}"
|
|
86
|
+
return error_msg
|
|
87
|
+
|
|
88
|
+
workflow = rowan.Workflow.create(
|
|
89
|
+
name=name,
|
|
90
|
+
workflow_type=workflow_type,
|
|
91
|
+
initial_molecule=initial_molecule,
|
|
92
|
+
parent_uuid=parent_uuid,
|
|
93
|
+
notes=notes or "",
|
|
94
|
+
starred=starred or False,
|
|
95
|
+
public=public or False,
|
|
96
|
+
email_when_complete=email_when_complete or False,
|
|
97
|
+
workflow_data=workflow_data or {}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
formatted = f" Workflow '{name}' created successfully!\n\n"
|
|
101
|
+
formatted += f" UUID: {safe_get_attr(workflow, 'uuid', 'N/A')}\n"
|
|
102
|
+
formatted += f" Type: {workflow_type}\n"
|
|
103
|
+
formatted += f" Status: {safe_get_attr(workflow, 'object_status', 'Unknown')}\n"
|
|
104
|
+
formatted += f" Created: {safe_get_attr(workflow, 'created_at', 'N/A')}\n"
|
|
105
|
+
return formatted
|
|
106
|
+
|
|
107
|
+
elif action == "retrieve":
|
|
108
|
+
if not workflow_uuid:
|
|
109
|
+
return " Error: 'workflow_uuid' is required for retrieving a workflow"
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
workflow = rowan.Workflow.retrieve(uuid=workflow_uuid)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return f" Error retrieving workflow: {str(e)}"
|
|
115
|
+
|
|
116
|
+
# Handle workflow as dictionary (which is what Rowan API returns)
|
|
117
|
+
def safe_get_dict_value(data, key, default='N/A'):
|
|
118
|
+
"""Safely get a value from a dictionary."""
|
|
119
|
+
if isinstance(data, dict):
|
|
120
|
+
return data.get(key, default)
|
|
121
|
+
return safe_get_attr(data, key, default)
|
|
122
|
+
|
|
123
|
+
# Get status and interpret it
|
|
124
|
+
status = safe_get_dict_value(workflow, 'object_status', 'Unknown')
|
|
125
|
+
status_names = {
|
|
126
|
+
0: "Queued",
|
|
127
|
+
1: "Running",
|
|
128
|
+
2: "Completed",
|
|
129
|
+
3: "Failed",
|
|
130
|
+
4: "Stopped",
|
|
131
|
+
5: "Awaiting Queue"
|
|
132
|
+
}
|
|
133
|
+
status_name = status_names.get(status, f"Unknown ({status})")
|
|
134
|
+
|
|
135
|
+
formatted = f" Workflow Details:\n\n"
|
|
136
|
+
formatted += f" Name: {safe_get_dict_value(workflow, 'name', 'N/A')}\n"
|
|
137
|
+
formatted += f" UUID: {safe_get_dict_value(workflow, 'uuid', 'N/A')}\n"
|
|
138
|
+
formatted += f" Type: {safe_get_dict_value(workflow, 'object_type', 'N/A')}\n"
|
|
139
|
+
formatted += f" Status: {status_name} ({status})\n"
|
|
140
|
+
formatted += f" Parent: {safe_get_dict_value(workflow, 'parent_uuid', 'Root')}\n"
|
|
141
|
+
formatted += f" Starred: {'Yes' if safe_get_dict_value(workflow, 'starred', False) else 'No'}\n"
|
|
142
|
+
formatted += f" Public: {'Yes' if safe_get_dict_value(workflow, 'public', False) else 'No'}\n"
|
|
143
|
+
formatted += f" Created: {safe_get_dict_value(workflow, 'created_at', 'N/A')}\n"
|
|
144
|
+
formatted += f" Elapsed: {safe_get_dict_value(workflow, 'elapsed', 0):.2f}s\n"
|
|
145
|
+
formatted += f" Credits: {safe_get_dict_value(workflow, 'credits_charged', 0)}\n"
|
|
146
|
+
formatted += f" Notes: {safe_get_dict_value(workflow, 'notes', 'None')}\n\n"
|
|
147
|
+
|
|
148
|
+
# If workflow is completed (status 2), extract and show results
|
|
149
|
+
if status == 2:
|
|
150
|
+
formatted += f" **Workflow Completed Successfully!**\n\n"
|
|
151
|
+
|
|
152
|
+
# Show basic completion details
|
|
153
|
+
credits_charged = safe_get_dict_value(workflow, 'credits_charged', 0)
|
|
154
|
+
elapsed_time = safe_get_dict_value(workflow, 'elapsed', 0)
|
|
155
|
+
if credits_charged or elapsed_time:
|
|
156
|
+
formatted += f" Workflow used {credits_charged} credits and ran for {elapsed_time:.2f}s\n\n"
|
|
157
|
+
|
|
158
|
+
# Debug: Show all available keys in the workflow dictionary
|
|
159
|
+
if isinstance(workflow, dict):
|
|
160
|
+
workflow_keys = list(workflow.keys())
|
|
161
|
+
formatted += f" **Debug - Available Workflow Keys:**\n"
|
|
162
|
+
formatted += f" {', '.join(workflow_keys)}\n\n"
|
|
163
|
+
else:
|
|
164
|
+
# Fallback for object-based workflows
|
|
165
|
+
workflow_attrs = []
|
|
166
|
+
for attr in dir(workflow):
|
|
167
|
+
if not attr.startswith('_'):
|
|
168
|
+
try:
|
|
169
|
+
value = getattr(workflow, attr)
|
|
170
|
+
if not callable(value):
|
|
171
|
+
workflow_attrs.append(attr)
|
|
172
|
+
except:
|
|
173
|
+
pass
|
|
174
|
+
formatted += f" **Debug - Available Workflow Attributes:**\n"
|
|
175
|
+
formatted += f" {', '.join(workflow_attrs)}\n\n"
|
|
176
|
+
|
|
177
|
+
# Extract actual results from object_data
|
|
178
|
+
object_data = safe_get_dict_value(workflow, 'object_data', {})
|
|
179
|
+
workflow_type = safe_get_dict_value(workflow, 'object_type', '')
|
|
180
|
+
|
|
181
|
+
formatted += f" **Results Analysis:**\n"
|
|
182
|
+
formatted += f" Workflow Type: {workflow_type}\n"
|
|
183
|
+
formatted += f" Object Data Present: {'Yes' if object_data else 'No'}\n"
|
|
184
|
+
|
|
185
|
+
if object_data:
|
|
186
|
+
formatted += f" Object Data Keys: {list(object_data.keys()) if isinstance(object_data, dict) else 'Not a dictionary'}\n\n"
|
|
187
|
+
formatted += extract_workflow_results(workflow_type, object_data)
|
|
188
|
+
else:
|
|
189
|
+
formatted += f" **No results data found in workflow object_data**\n"
|
|
190
|
+
formatted += f" This could mean:\n"
|
|
191
|
+
formatted += f" ⢠The workflow completed but didn't generate data\n"
|
|
192
|
+
formatted += f" ⢠The results are stored in a different attribute\n"
|
|
193
|
+
formatted += f" ⢠There was an issue with the workflow execution\n"
|
|
194
|
+
|
|
195
|
+
elif status == 1: # Running
|
|
196
|
+
formatted += f" **Workflow is currently running...**\n"
|
|
197
|
+
formatted += f" Check back later or use `rowan_workflow_management(action='status', workflow_uuid='{workflow_uuid}')` for updates\n"
|
|
198
|
+
elif status == 0: # Queued
|
|
199
|
+
formatted += f" **Workflow is queued and waiting to start**\n"
|
|
200
|
+
formatted += f" Use `rowan_workflow_management(action='status', workflow_uuid='{workflow_uuid}')` to check progress\n"
|
|
201
|
+
elif status == 3: # Failed
|
|
202
|
+
formatted += f" **Workflow failed**\n"
|
|
203
|
+
formatted += f" Check the workflow details in the Rowan web interface for error messages\n"
|
|
204
|
+
elif status == 4: # Stopped
|
|
205
|
+
formatted += f" **Workflow was stopped**\n"
|
|
206
|
+
|
|
207
|
+
return formatted
|
|
208
|
+
|
|
209
|
+
elif action == "update":
|
|
210
|
+
if not workflow_uuid:
|
|
211
|
+
return " Error: 'workflow_uuid' is required for updating a workflow"
|
|
212
|
+
|
|
213
|
+
# Build update parameters according to Rowan API docs
|
|
214
|
+
update_params = {'uuid': workflow_uuid}
|
|
215
|
+
updates_made = []
|
|
216
|
+
|
|
217
|
+
if name is not None:
|
|
218
|
+
update_params['name'] = name
|
|
219
|
+
updates_made.append(f"name: {name}")
|
|
220
|
+
if parent_uuid is not None:
|
|
221
|
+
update_params['parent_uuid'] = parent_uuid
|
|
222
|
+
updates_made.append(f"parent_uuid: {parent_uuid}")
|
|
223
|
+
if notes is not None:
|
|
224
|
+
update_params['notes'] = notes
|
|
225
|
+
updates_made.append(f"notes: {notes}")
|
|
226
|
+
if starred is not None:
|
|
227
|
+
update_params['starred'] = starred
|
|
228
|
+
updates_made.append(f"starred: {starred}")
|
|
229
|
+
if public is not None:
|
|
230
|
+
update_params['public'] = public
|
|
231
|
+
updates_made.append(f"public: {public}")
|
|
232
|
+
if email_when_complete is not None:
|
|
233
|
+
update_params['email_when_complete'] = email_when_complete
|
|
234
|
+
updates_made.append(f"email_when_complete: {email_when_complete}")
|
|
235
|
+
|
|
236
|
+
if len(update_params) == 1: # Only UUID provided
|
|
237
|
+
return " Error: At least one field must be provided for updating (name, parent_uuid, notes, starred, public, email_when_complete)"
|
|
238
|
+
|
|
239
|
+
# Call Rowan API with correct parameter structure
|
|
240
|
+
workflow = rowan.Workflow.update(**update_params)
|
|
241
|
+
|
|
242
|
+
# Format response using the returned workflow object
|
|
243
|
+
formatted = f" Workflow updated successfully!\n\n"
|
|
244
|
+
formatted += f" UUID: {safe_get_attr(workflow, 'uuid', workflow_uuid)}\n"
|
|
245
|
+
formatted += f" Name: {safe_get_attr(workflow, 'name', 'N/A')}\n"
|
|
246
|
+
formatted += f" Type: {safe_get_attr(workflow, 'object_type', 'N/A')}\n"
|
|
247
|
+
formatted += f" Parent: {safe_get_attr(workflow, 'parent_uuid', 'Root')}\n"
|
|
248
|
+
formatted += f" Starred: {'Yes' if safe_get_attr(workflow, 'starred', False) else 'No'}\n"
|
|
249
|
+
formatted += f" Public: {'Yes' if safe_get_attr(workflow, 'public', False) else 'No'}\n"
|
|
250
|
+
formatted += f" Email on Complete: {'Yes' if safe_get_attr(workflow, 'email_when_complete', False) else 'No'}\n"
|
|
251
|
+
formatted += f" Notes: {safe_get_attr(workflow, 'notes', 'None')}\n\n"
|
|
252
|
+
|
|
253
|
+
formatted += f" **Updates Applied:**\n"
|
|
254
|
+
for update in updates_made:
|
|
255
|
+
formatted += f"⢠{update}\n"
|
|
256
|
+
|
|
257
|
+
return formatted
|
|
258
|
+
|
|
259
|
+
elif action in ["stop", "status", "is_finished"]:
|
|
260
|
+
if not workflow_uuid:
|
|
261
|
+
return f" Error: 'workflow_uuid' is required for {action} action"
|
|
262
|
+
|
|
263
|
+
if action == "stop":
|
|
264
|
+
result = rowan.Workflow.stop(uuid=workflow_uuid)
|
|
265
|
+
return f" Workflow stop request submitted. Result: {result}"
|
|
266
|
+
elif action == "status":
|
|
267
|
+
workflow = rowan.Workflow.retrieve(uuid=workflow_uuid)
|
|
268
|
+
|
|
269
|
+
# Handle workflow as dictionary
|
|
270
|
+
def safe_get_dict_value(data, key, default='N/A'):
|
|
271
|
+
if isinstance(data, dict):
|
|
272
|
+
return data.get(key, default)
|
|
273
|
+
return safe_get_attr(data, key, default)
|
|
274
|
+
|
|
275
|
+
status = safe_get_dict_value(workflow, 'object_status', 'Unknown')
|
|
276
|
+
status_names = {
|
|
277
|
+
0: "Queued",
|
|
278
|
+
1: "Running",
|
|
279
|
+
2: "Completed",
|
|
280
|
+
3: "Failed",
|
|
281
|
+
4: "Stopped",
|
|
282
|
+
5: "Awaiting Queue"
|
|
283
|
+
}
|
|
284
|
+
status_name = status_names.get(status, f"Unknown ({status})")
|
|
285
|
+
|
|
286
|
+
formatted = f" **Workflow Status**: {status_name} ({status})\n"
|
|
287
|
+
formatted += f" UUID: {workflow_uuid}\n"
|
|
288
|
+
formatted += f" Name: {safe_get_dict_value(workflow, 'name', 'N/A')}\n"
|
|
289
|
+
formatted += f" Elapsed: {safe_get_dict_value(workflow, 'elapsed', 0):.2f}s\n"
|
|
290
|
+
|
|
291
|
+
if status == 2:
|
|
292
|
+
formatted += f" **Completed successfully!** Use 'retrieve' action to get results.\n"
|
|
293
|
+
elif status == 1:
|
|
294
|
+
formatted += f" **Currently running...** Check back later for results.\n"
|
|
295
|
+
elif status == 0:
|
|
296
|
+
formatted += f" **Queued and waiting to start**\n"
|
|
297
|
+
elif status == 3:
|
|
298
|
+
formatted += f" **Failed** - Check workflow details for error information.\n"
|
|
299
|
+
elif status == 4:
|
|
300
|
+
formatted += f" **Stopped**\n"
|
|
301
|
+
|
|
302
|
+
return formatted
|
|
303
|
+
elif action == "is_finished":
|
|
304
|
+
workflow = rowan.Workflow.retrieve(uuid=workflow_uuid)
|
|
305
|
+
|
|
306
|
+
# Handle workflow as dictionary
|
|
307
|
+
def safe_get_dict_value(data, key, default='N/A'):
|
|
308
|
+
if isinstance(data, dict):
|
|
309
|
+
return data.get(key, default)
|
|
310
|
+
return safe_get_attr(data, key, default)
|
|
311
|
+
|
|
312
|
+
status = safe_get_dict_value(workflow, 'object_status', 'Unknown')
|
|
313
|
+
is_finished = status in [2, 3, 4] # Completed, Failed, or Stopped
|
|
314
|
+
|
|
315
|
+
formatted = f" **Workflow Finished Check**\n"
|
|
316
|
+
formatted += f" UUID: {workflow_uuid}\n"
|
|
317
|
+
formatted += f" Status: {status}\n"
|
|
318
|
+
formatted += f" Finished: {'Yes' if is_finished else 'No'}\n"
|
|
319
|
+
|
|
320
|
+
if is_finished:
|
|
321
|
+
if status == 2:
|
|
322
|
+
formatted += f" Use 'retrieve' action to get results\n"
|
|
323
|
+
elif status == 3:
|
|
324
|
+
formatted += f" Workflow failed - check details for error info\n"
|
|
325
|
+
elif status == 4:
|
|
326
|
+
formatted += f" Workflow was stopped\n"
|
|
327
|
+
else:
|
|
328
|
+
formatted += f" Workflow is still {['queued', 'running'][status] if status in [0, 1] else 'in progress'}\n"
|
|
329
|
+
|
|
330
|
+
return formatted
|
|
331
|
+
|
|
332
|
+
elif action == "delete":
|
|
333
|
+
if not workflow_uuid:
|
|
334
|
+
return " Error: 'workflow_uuid' is required for deleting a workflow"
|
|
335
|
+
|
|
336
|
+
result = rowan.Workflow.delete(uuid=workflow_uuid)
|
|
337
|
+
return f" Workflow deletion request submitted. Result: {result}"
|
|
338
|
+
|
|
339
|
+
elif action == "list":
|
|
340
|
+
# Build filters
|
|
341
|
+
filters = {
|
|
342
|
+
'page': page,
|
|
343
|
+
'size': min(size * 5, 250) # Get more workflows to sort properly, cap at 250
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if name_contains:
|
|
347
|
+
filters['name_contains'] = name_contains
|
|
348
|
+
if parent_uuid:
|
|
349
|
+
filters['parent_uuid'] = parent_uuid
|
|
350
|
+
if starred is not None:
|
|
351
|
+
filters['starred'] = starred
|
|
352
|
+
if public is not None:
|
|
353
|
+
filters['public'] = public
|
|
354
|
+
if object_status is not None:
|
|
355
|
+
filters['object_status'] = object_status
|
|
356
|
+
if object_type:
|
|
357
|
+
filters['object_type'] = object_type
|
|
358
|
+
|
|
359
|
+
workflows = rowan.Workflow.list(**filters)
|
|
360
|
+
|
|
361
|
+
# Sort workflows by created_at in descending order (most recent first)
|
|
362
|
+
if 'workflows' in workflows and workflows['workflows']:
|
|
363
|
+
from datetime import datetime
|
|
364
|
+
|
|
365
|
+
def parse_date(date_str):
|
|
366
|
+
try:
|
|
367
|
+
return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
368
|
+
except:
|
|
369
|
+
return datetime.min
|
|
370
|
+
|
|
371
|
+
sorted_workflows = sorted(
|
|
372
|
+
workflows['workflows'],
|
|
373
|
+
key=lambda w: parse_date(w.get('created_at', '')),
|
|
374
|
+
reverse=True
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Remove object_logfile from each workflow and return only the requested number
|
|
378
|
+
cleaned_workflows = []
|
|
379
|
+
for workflow in sorted_workflows[:size]:
|
|
380
|
+
cleaned_workflow = {k: v for k, v in workflow.items() if k != 'object_logfile'}
|
|
381
|
+
cleaned_workflows.append(cleaned_workflow)
|
|
382
|
+
|
|
383
|
+
workflows['workflows'] = cleaned_workflows
|
|
384
|
+
|
|
385
|
+
return workflows
|
|
386
|
+
|
|
387
|
+
else:
|
|
388
|
+
return f" Error: Unknown action '{action}'. Available actions: create, retrieve, update, stop, status, is_finished, delete, list"
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
return f" Error in workflow management: {str(e)}"
|
|
392
|
+
|
|
393
|
+
def extract_workflow_results(workflow_type: str, object_data: Dict[str, Any]) -> str:
|
|
394
|
+
"""Extract and format workflow results - simple raw data display."""
|
|
395
|
+
|
|
396
|
+
formatted = f" **{workflow_type.replace('_', ' ').title()} Results:**\n\n"
|
|
397
|
+
|
|
398
|
+
import json
|
|
399
|
+
try:
|
|
400
|
+
# Pretty print the object_data as JSON
|
|
401
|
+
formatted += f"```json\n{json.dumps(object_data, indent=2, default=str)}\n```\n"
|
|
402
|
+
except Exception as e:
|
|
403
|
+
# Fallback if JSON serialization fails
|
|
404
|
+
formatted += f"Raw object_data:\n{str(object_data)}\n"
|
|
405
|
+
formatted += f"(JSON serialization failed: {e})\n"
|
|
406
|
+
|
|
407
|
+
return formatted
|
|
408
|
+
|
|
409
|
+
def test_rowan_workflow_management():
|
|
410
|
+
"""Test the workflow management function."""
|
|
411
|
+
try:
|
|
412
|
+
# Test list action
|
|
413
|
+
result = rowan_workflow_management("list", size=5)
|
|
414
|
+
print(" Workflow management test successful!")
|
|
415
|
+
print(f"Sample result: {result[:200]}...")
|
|
416
|
+
return True
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print(f" Workflow management test failed: {e}")
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
if __name__ == "__main__":
|
|
422
|
+
test_rowan_workflow_management()
|
rowan_mcp/server.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan MCP Server Implementation using FastMCP
|
|
3
|
+
|
|
4
|
+
This module implements the Model Context Protocol server for Rowan's
|
|
5
|
+
computational chemistry platform using the FastMCP framework.
|
|
6
|
+
Supports both STDIO and HTTP transports.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Any, Dict, List, Optional, Literal, Union
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
from stjames import Molecule
|
|
19
|
+
|
|
20
|
+
# Import functions from functions module
|
|
21
|
+
from .functions.scan import rowan_scan as scan_function
|
|
22
|
+
from .functions.scan_analyzer import rowan_scan_analyzer as scan_analyzer_function
|
|
23
|
+
from .functions.admet import rowan_admet as admet_function
|
|
24
|
+
|
|
25
|
+
from .functions.multistage_opt import rowan_multistage_opt as multistage_opt_function
|
|
26
|
+
from .functions.descriptors import rowan_descriptors as descriptors_function
|
|
27
|
+
from .functions.tautomers import rowan_tautomers as tautomers_function
|
|
28
|
+
|
|
29
|
+
from .functions.redox_potential import rowan_redox_potential as redox_potential_function
|
|
30
|
+
from .functions.conformers import rowan_conformers as conformers_function
|
|
31
|
+
from .functions.electronic_properties import rowan_electronic_properties as electronic_properties_function
|
|
32
|
+
from .functions.fukui import rowan_fukui as fukui_function
|
|
33
|
+
from .functions.spin_states import rowan_spin_states as spin_states_function
|
|
34
|
+
from .functions.solubility import rowan_solubility as solubility_function
|
|
35
|
+
from .functions.molecular_dynamics import rowan_molecular_dynamics as molecular_dynamics_function
|
|
36
|
+
from .functions.irc import rowan_irc as irc_function
|
|
37
|
+
from .functions.docking import rowan_docking as docking_function, rowan_docking_pdb_id as docking_pdb_id_function
|
|
38
|
+
from .functions.docking_enhanced import rowan_docking_enhanced as docking_enhanced_function
|
|
39
|
+
from .functions.workflow_management import rowan_workflow_management as workflow_management_function
|
|
40
|
+
# from .functions.calculation_retrieve import rowan_calculation_retrieve as calculation_retrieve_function
|
|
41
|
+
from .functions.pka import rowan_pka as pka_function
|
|
42
|
+
from .functions.macropka import rowan_macropka as macropka_function
|
|
43
|
+
from .functions.hydrogen_bond_basicity import rowan_hydrogen_bond_basicity as hydrogen_bond_basicity_function
|
|
44
|
+
from .functions.bde import rowan_bde as bde_function
|
|
45
|
+
from .functions.folder_management import rowan_folder_management as folder_management_function
|
|
46
|
+
from .functions.system_management import rowan_system_management as system_management_function
|
|
47
|
+
|
|
48
|
+
# Import molecule lookup from functions
|
|
49
|
+
from .functions.molecule_lookup import rowan_molecule_lookup as molecule_lookup_function
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
import rowan
|
|
53
|
+
except ImportError:
|
|
54
|
+
rowan = None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
from dotenv import load_dotenv
|
|
58
|
+
load_dotenv() # Load .env file if available
|
|
59
|
+
except ImportError:
|
|
60
|
+
pass # dotenv not required, but helpful if available
|
|
61
|
+
|
|
62
|
+
# Initialize FastMCP server
|
|
63
|
+
mcp = FastMCP("Rowan MCP Server")
|
|
64
|
+
|
|
65
|
+
# Register imported functions as MCP tools
|
|
66
|
+
rowan_scan = mcp.tool()(scan_function)
|
|
67
|
+
rowan_scan_analyzer = mcp.tool()(scan_analyzer_function)
|
|
68
|
+
rowan_admet = mcp.tool()(admet_function)
|
|
69
|
+
|
|
70
|
+
rowan_multistage_opt = mcp.tool()(multistage_opt_function)
|
|
71
|
+
rowan_descriptors = mcp.tool()(descriptors_function)
|
|
72
|
+
rowan_tautomers = mcp.tool()(tautomers_function)
|
|
73
|
+
|
|
74
|
+
rowan_redox_potential = mcp.tool()(redox_potential_function)
|
|
75
|
+
rowan_conformers = mcp.tool()(conformers_function)
|
|
76
|
+
rowan_electronic_properties = mcp.tool()(electronic_properties_function)
|
|
77
|
+
rowan_fukui = mcp.tool()(fukui_function)
|
|
78
|
+
rowan_spin_states = mcp.tool()(spin_states_function)
|
|
79
|
+
rowan_solubility = mcp.tool()(solubility_function)
|
|
80
|
+
rowan_molecular_dynamics = mcp.tool()(molecular_dynamics_function)
|
|
81
|
+
rowan_irc = mcp.tool()(irc_function)
|
|
82
|
+
rowan_docking = mcp.tool()(docking_function)
|
|
83
|
+
rowan_docking_pdb_id = mcp.tool()(docking_pdb_id_function)
|
|
84
|
+
rowan_docking_enhanced = mcp.tool()(docking_enhanced_function)
|
|
85
|
+
rowan_workflow_management = mcp.tool()(workflow_management_function)
|
|
86
|
+
# rowan_calculation_retrieve = mcp.tool()(calculation_retrieve_function)
|
|
87
|
+
rowan_molecule_lookup = mcp.tool()(molecule_lookup_function)
|
|
88
|
+
rowan_pka = mcp.tool()(pka_function)
|
|
89
|
+
rowan_macropka = mcp.tool()(macropka_function)
|
|
90
|
+
rowan_hydrogen_bond_basicity = mcp.tool()(hydrogen_bond_basicity_function)
|
|
91
|
+
rowan_bde = mcp.tool()(bde_function)
|
|
92
|
+
rowan_folder_management = mcp.tool()(folder_management_function)
|
|
93
|
+
rowan_system_management = mcp.tool()(system_management_function)
|
|
94
|
+
|
|
95
|
+
# Setup API key
|
|
96
|
+
api_key = os.getenv("ROWAN_API_KEY")
|
|
97
|
+
if not api_key:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"ROWAN_API_KEY environment variable is required. "
|
|
100
|
+
"Get your API key from https://labs.rowansci.com"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if rowan is None:
|
|
104
|
+
raise ImportError(
|
|
105
|
+
"rowan-python package is required. Install with: pip install rowan-python"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
rowan.api_key = api_key
|
|
109
|
+
|
|
110
|
+
def main() -> None:
|
|
111
|
+
"""Main entry point for the MCP server."""
|
|
112
|
+
try:
|
|
113
|
+
# Check for transport mode from command line args or environment
|
|
114
|
+
transport_mode = os.getenv("ROWAN_MCP_TRANSPORT", "stdio").lower()
|
|
115
|
+
|
|
116
|
+
# Allow override from command line
|
|
117
|
+
if len(sys.argv) > 1:
|
|
118
|
+
if sys.argv[1] == "--http":
|
|
119
|
+
transport_mode = "http"
|
|
120
|
+
elif sys.argv[1] == "--stdio":
|
|
121
|
+
transport_mode = "stdio"
|
|
122
|
+
elif sys.argv[1] == "--help":
|
|
123
|
+
print("Rowan MCP Server")
|
|
124
|
+
print("Usage:")
|
|
125
|
+
print(" rowan-mcp # Default STDIO transport")
|
|
126
|
+
print(" rowan-mcp --stdio # STDIO transport")
|
|
127
|
+
print(" rowan-mcp --http # HTTP/SSE transport")
|
|
128
|
+
print("")
|
|
129
|
+
print("Development usage:")
|
|
130
|
+
print(" python -m rowan_mcp # Default STDIO transport")
|
|
131
|
+
print(" python -m rowan_mcp --stdio # STDIO transport")
|
|
132
|
+
print(" python -m rowan_mcp --http # HTTP/SSE transport")
|
|
133
|
+
print("")
|
|
134
|
+
print("Environment variables:")
|
|
135
|
+
print(" ROWAN_API_KEY # Required: Your Rowan API key")
|
|
136
|
+
print(" ROWAN_MCP_TRANSPORT # Optional: 'stdio' or 'http' (default: stdio)")
|
|
137
|
+
print(" ROWAN_MCP_HOST # Optional: HTTP host (default: 127.0.0.1)")
|
|
138
|
+
print(" ROWAN_MCP_PORT # Optional: HTTP port (default: 6276)")
|
|
139
|
+
print("")
|
|
140
|
+
print("HTTP/SSE mode endpoint: http://host:port/sse")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if transport_mode == "http":
|
|
144
|
+
host = os.getenv("ROWAN_MCP_HOST", "127.0.0.1")
|
|
145
|
+
port = int(os.getenv("ROWAN_MCP_PORT", "6276"))
|
|
146
|
+
|
|
147
|
+
print("š Starting Rowan MCP Server (HTTP/SSE mode)")
|
|
148
|
+
print(f"š” Server will be available at: http://{host}:{port}/sse")
|
|
149
|
+
print(f"š API Key loaded: {'ā' if api_key else 'ā'}")
|
|
150
|
+
print(f"š ļø Available tools: {len([attr for attr in dir() if attr.startswith('rowan_')])}")
|
|
151
|
+
print("š Connect your MCP client to this endpoint!")
|
|
152
|
+
print("\nPress Ctrl+C to stop the server")
|
|
153
|
+
|
|
154
|
+
mcp.run(transport="sse", host=host, port=port)
|
|
155
|
+
else:
|
|
156
|
+
print("š Starting Rowan MCP Server (STDIO mode)")
|
|
157
|
+
print(f"š API Key loaded: {'ā' if api_key else 'ā'}")
|
|
158
|
+
print(f"š ļø Available tools: {len([attr for attr in dir() if attr.startswith('rowan_')])}")
|
|
159
|
+
|
|
160
|
+
mcp.run() # Default STDIO transport
|
|
161
|
+
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
print("\nš Server shutdown requested by user")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"ā Server error: {e}")
|
|
166
|
+
traceback.print_exc()
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|