edda-framework 0.3.1__py3-none-any.whl → 0.4.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.
- edda/integrations/mcp/decorators.py +3 -4
- edda/integrations/mcp/server.py +157 -5
- {edda_framework-0.3.1.dist-info → edda_framework-0.4.0.dist-info}/METADATA +27 -1
- {edda_framework-0.3.1.dist-info → edda_framework-0.4.0.dist-info}/RECORD +7 -7
- {edda_framework-0.3.1.dist-info → edda_framework-0.4.0.dist-info}/WHEEL +0 -0
- {edda_framework-0.3.1.dist-info → edda_framework-0.4.0.dist-info}/entry_points.txt +0 -0
- {edda_framework-0.3.1.dist-info → edda_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,11 +6,10 @@ import inspect
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from typing import TYPE_CHECKING, Any, cast
|
|
8
8
|
|
|
9
|
-
from edda.workflow import workflow
|
|
9
|
+
from edda.workflow import Workflow, workflow
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from edda.integrations.mcp.server import EddaMCPServer
|
|
13
|
-
from edda.workflow import Workflow
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def create_durable_tool(
|
|
@@ -98,7 +97,7 @@ def create_durable_tool(
|
|
|
98
97
|
async def status_tool(instance_id: str) -> dict[str, Any]:
|
|
99
98
|
"""Check workflow status."""
|
|
100
99
|
try:
|
|
101
|
-
instance = await server.
|
|
100
|
+
instance = await server.storage.get_instance(instance_id)
|
|
102
101
|
if instance is None:
|
|
103
102
|
return {
|
|
104
103
|
"content": [
|
|
@@ -142,7 +141,7 @@ def create_durable_tool(
|
|
|
142
141
|
async def result_tool(instance_id: str) -> dict[str, Any]:
|
|
143
142
|
"""Get workflow result (if completed)."""
|
|
144
143
|
try:
|
|
145
|
-
instance = await server.
|
|
144
|
+
instance = await server.storage.get_instance(instance_id)
|
|
146
145
|
if instance is None:
|
|
147
146
|
return {
|
|
148
147
|
"content": [
|
edda/integrations/mcp/server.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
-
from typing import Any, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
7
|
|
|
8
8
|
from edda.app import EddaApp
|
|
9
9
|
from edda.workflow import Workflow
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from edda.storage.protocol import StorageProtocol
|
|
13
|
+
|
|
11
14
|
try:
|
|
12
15
|
from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
|
|
13
16
|
except ImportError as e:
|
|
@@ -45,7 +48,13 @@ class EddaMCPServer:
|
|
|
45
48
|
|
|
46
49
|
# Deploy with uvicorn (HTTP transport)
|
|
47
50
|
if __name__ == "__main__":
|
|
51
|
+
import asyncio
|
|
48
52
|
import uvicorn
|
|
53
|
+
|
|
54
|
+
async def startup():
|
|
55
|
+
await server.initialize()
|
|
56
|
+
|
|
57
|
+
asyncio.run(startup())
|
|
49
58
|
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
50
59
|
|
|
51
60
|
# Or deploy with stdio (for MCP clients, e.g., Claude Desktop)
|
|
@@ -97,6 +106,22 @@ class EddaMCPServer:
|
|
|
97
106
|
# Registry of durable tools (workflow_name -> Workflow instance)
|
|
98
107
|
self._workflows: dict[str, Workflow] = {}
|
|
99
108
|
|
|
109
|
+
@property
|
|
110
|
+
def storage(self) -> StorageProtocol:
|
|
111
|
+
"""
|
|
112
|
+
Access workflow storage for querying instances and history.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
StorageProtocol: Storage backend for workflow state
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```python
|
|
119
|
+
instance = await server.storage.get_instance(instance_id)
|
|
120
|
+
history = await server.storage.get_history(instance_id)
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
return self._edda_app.storage
|
|
124
|
+
|
|
100
125
|
def durable_tool(
|
|
101
126
|
self,
|
|
102
127
|
func: Callable[..., Any] | None = None,
|
|
@@ -135,6 +160,59 @@ class EddaMCPServer:
|
|
|
135
160
|
return decorator
|
|
136
161
|
return decorator(func)
|
|
137
162
|
|
|
163
|
+
def prompt(
|
|
164
|
+
self,
|
|
165
|
+
func: Callable[..., Any] | None = None,
|
|
166
|
+
*,
|
|
167
|
+
description: str = "",
|
|
168
|
+
) -> Callable[..., Any]:
|
|
169
|
+
"""
|
|
170
|
+
Decorator to define a prompt template.
|
|
171
|
+
|
|
172
|
+
Prompts can access workflow state to generate dynamic, context-aware
|
|
173
|
+
prompts for AI clients (Claude Desktop, etc.).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
func: Prompt function (async or sync)
|
|
177
|
+
description: Prompt description for MCP clients
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Decorated function
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
```python
|
|
184
|
+
from fastmcp.prompts.prompt import PromptMessage, TextContent
|
|
185
|
+
|
|
186
|
+
@server.prompt(description="Analyze workflow results")
|
|
187
|
+
async def analyze_workflow(instance_id: str) -> PromptMessage:
|
|
188
|
+
'''Generate a prompt to analyze a specific workflow execution.'''
|
|
189
|
+
instance = await server.storage.get_instance(instance_id)
|
|
190
|
+
history = await server.storage.get_history(instance_id)
|
|
191
|
+
|
|
192
|
+
text = f'''Analyze this workflow:
|
|
193
|
+
|
|
194
|
+
Instance ID: {instance_id}
|
|
195
|
+
Status: {instance['status']}
|
|
196
|
+
Activities: {len(history)}
|
|
197
|
+
|
|
198
|
+
Please identify any issues or optimization opportunities.'''
|
|
199
|
+
|
|
200
|
+
return PromptMessage(
|
|
201
|
+
role="user",
|
|
202
|
+
content=TextContent(type="text", text=text)
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
208
|
+
# Use FastMCP's native prompt decorator
|
|
209
|
+
prompt_desc = description or f.__doc__ or f"Prompt: {f.__name__}"
|
|
210
|
+
return cast(Callable[..., Any], self._mcp.prompt(description=prompt_desc)(f))
|
|
211
|
+
|
|
212
|
+
if func is None:
|
|
213
|
+
return decorator
|
|
214
|
+
return decorator(func)
|
|
215
|
+
|
|
138
216
|
def asgi_app(self) -> Callable[..., Any]:
|
|
139
217
|
"""
|
|
140
218
|
Create ASGI application with MCP + CloudEvents support.
|
|
@@ -221,11 +299,9 @@ class EddaMCPServer:
|
|
|
221
299
|
"""
|
|
222
300
|
Initialize the EddaApp (setup replay engine, storage, etc.).
|
|
223
301
|
|
|
224
|
-
This method must be called before running the server in stdio mode.
|
|
225
|
-
For HTTP mode (asgi_app()), initialization happens automatically
|
|
226
|
-
when the ASGI app is deployed.
|
|
302
|
+
This method must be called before running the server in either stdio or HTTP mode.
|
|
227
303
|
|
|
228
|
-
Example:
|
|
304
|
+
Example (stdio mode):
|
|
229
305
|
```python
|
|
230
306
|
async def main():
|
|
231
307
|
await server.initialize()
|
|
@@ -235,9 +311,85 @@ class EddaMCPServer:
|
|
|
235
311
|
import asyncio
|
|
236
312
|
asyncio.run(main())
|
|
237
313
|
```
|
|
314
|
+
|
|
315
|
+
Example (HTTP mode):
|
|
316
|
+
```python
|
|
317
|
+
import asyncio
|
|
318
|
+
import uvicorn
|
|
319
|
+
|
|
320
|
+
async def startup():
|
|
321
|
+
await server.initialize()
|
|
322
|
+
|
|
323
|
+
asyncio.run(startup())
|
|
324
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
325
|
+
```
|
|
238
326
|
"""
|
|
239
327
|
await self._edda_app.initialize()
|
|
240
328
|
|
|
329
|
+
async def shutdown(self) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Shutdown the server and cleanup resources.
|
|
332
|
+
|
|
333
|
+
Stops background tasks (auto-resume, timer checks, event timeouts),
|
|
334
|
+
closes storage connections, and performs graceful shutdown.
|
|
335
|
+
|
|
336
|
+
This method should be called when the server is shutting down.
|
|
337
|
+
|
|
338
|
+
Example (stdio mode):
|
|
339
|
+
```python
|
|
340
|
+
import signal
|
|
341
|
+
import asyncio
|
|
342
|
+
|
|
343
|
+
async def main():
|
|
344
|
+
server = EddaMCPServer(...)
|
|
345
|
+
await server.initialize()
|
|
346
|
+
|
|
347
|
+
# Setup signal handlers for graceful shutdown
|
|
348
|
+
loop = asyncio.get_running_loop()
|
|
349
|
+
shutdown_event = asyncio.Event()
|
|
350
|
+
|
|
351
|
+
def signal_handler():
|
|
352
|
+
shutdown_event.set()
|
|
353
|
+
|
|
354
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
355
|
+
loop.add_signal_handler(sig, signal_handler)
|
|
356
|
+
|
|
357
|
+
# Run server
|
|
358
|
+
try:
|
|
359
|
+
await server.run_stdio()
|
|
360
|
+
finally:
|
|
361
|
+
await server.shutdown()
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
asyncio.run(main())
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Example (HTTP mode with uvicorn):
|
|
368
|
+
```python
|
|
369
|
+
import asyncio
|
|
370
|
+
import uvicorn
|
|
371
|
+
|
|
372
|
+
async def startup():
|
|
373
|
+
await server.initialize()
|
|
374
|
+
|
|
375
|
+
async def shutdown_handler():
|
|
376
|
+
await server.shutdown()
|
|
377
|
+
|
|
378
|
+
# Use uvicorn lifecycle events
|
|
379
|
+
config = uvicorn.Config(
|
|
380
|
+
server.asgi_app(),
|
|
381
|
+
host="0.0.0.0",
|
|
382
|
+
port=8000,
|
|
383
|
+
)
|
|
384
|
+
server_instance = uvicorn.Server(config)
|
|
385
|
+
|
|
386
|
+
# Uvicorn handles SIGTERM/SIGINT automatically
|
|
387
|
+
await server_instance.serve()
|
|
388
|
+
await shutdown_handler()
|
|
389
|
+
```
|
|
390
|
+
"""
|
|
391
|
+
await self._edda_app.shutdown()
|
|
392
|
+
|
|
241
393
|
async def run_stdio(self) -> None:
|
|
242
394
|
"""
|
|
243
395
|
Run MCP server with stdio transport (for MCP clients, e.g., Claude Desktop).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lightweight Durable Execution Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2y/edda
|
|
6
6
|
Project-URL: Documentation, https://github.com/i2y/edda#readme
|
|
@@ -730,6 +730,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
|
|
|
730
730
|
|
|
731
731
|
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
732
732
|
|
|
733
|
+
### MCP Prompts
|
|
734
|
+
|
|
735
|
+
Define reusable prompt templates that can access workflow state:
|
|
736
|
+
|
|
737
|
+
```python
|
|
738
|
+
from mcp.server.fastmcp.prompts.base import UserMessage
|
|
739
|
+
from mcp.types import TextContent
|
|
740
|
+
|
|
741
|
+
@server.prompt(description="Analyze a workflow execution")
|
|
742
|
+
async def analyze_workflow(instance_id: str) -> UserMessage:
|
|
743
|
+
"""Generate analysis prompt for a specific workflow."""
|
|
744
|
+
instance = await server.storage.get_instance(instance_id)
|
|
745
|
+
history = await server.storage.get_history(instance_id)
|
|
746
|
+
|
|
747
|
+
text = f"""Analyze this workflow:
|
|
748
|
+
**Status**: {instance['status']}
|
|
749
|
+
**Activities**: {len(history)}
|
|
750
|
+
**Result**: {instance.get('output_data')}
|
|
751
|
+
|
|
752
|
+
Please provide insights and optimization suggestions."""
|
|
753
|
+
|
|
754
|
+
return UserMessage(content=TextContent(type="text", text=text))
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
AI clients can use these prompts to generate context-aware analysis of your workflows.
|
|
758
|
+
|
|
733
759
|
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
734
760
|
|
|
735
761
|
## Observability Hooks
|
|
@@ -14,8 +14,8 @@ edda/workflow.py,sha256=daSppYAzgXkjY_9-HS93Zi7_tPR6srmchxY5YfwgU-4,7239
|
|
|
14
14
|
edda/wsgi.py,sha256=1pGE5fhHpcsYnDR8S3NEFKWUs5P0JK4roTAzX9BsIj0,2391
|
|
15
15
|
edda/integrations/__init__.py,sha256=F_CaTvlDEbldfOpPKq_U9ve1E573tS6XzqXnOtyHcXI,33
|
|
16
16
|
edda/integrations/mcp/__init__.py,sha256=YK-8m0DIdP-RSqewlIX7xnWU7TD3NioCiW2_aZSgnn8,1232
|
|
17
|
-
edda/integrations/mcp/decorators.py,sha256=
|
|
18
|
-
edda/integrations/mcp/server.py,sha256=
|
|
17
|
+
edda/integrations/mcp/decorators.py,sha256=UTBb-Un2JK938pDZmANOvfsdKOMI2AF9yGtfSuy8VrE,6284
|
|
18
|
+
edda/integrations/mcp/server.py,sha256=pzCG46Zko9hHmQ3REbo1w3A23SjrGFLiZupwSkIPhOA,13942
|
|
19
19
|
edda/outbox/__init__.py,sha256=azXG1rtheJEjOyoWmMsBeR2jp8Bz02R3wDEd5tQnaWA,424
|
|
20
20
|
edda/outbox/relayer.py,sha256=2tnN1aOQ8pKWfwEGIlYwYLLwyOKXBjZ4XZsIr1HjgK4,9454
|
|
21
21
|
edda/outbox/transactional.py,sha256=LFfUjunqRlGibaINi-efGXFFivWGO7v3mhqrqyGW6Nw,3808
|
|
@@ -33,8 +33,8 @@ edda/viewer_ui/data_service.py,sha256=mXV6bL6REa_UKsk8xMGBIFbsbLpIxe91lX3wgn-FOj
|
|
|
33
33
|
edda/visualizer/__init__.py,sha256=DOpDstNhR0VcXAs_eMKxaL30p_0u4PKZ4o2ndnYhiRo,343
|
|
34
34
|
edda/visualizer/ast_analyzer.py,sha256=plmx7C9X_X35xLY80jxOL3ljg3afXxBePRZubqUIkxY,13663
|
|
35
35
|
edda/visualizer/mermaid_generator.py,sha256=XWa2egoOTNDfJEjPcwoxwQmblUqXf7YInWFjFRI1QGo,12457
|
|
36
|
-
edda_framework-0.
|
|
37
|
-
edda_framework-0.
|
|
38
|
-
edda_framework-0.
|
|
39
|
-
edda_framework-0.
|
|
40
|
-
edda_framework-0.
|
|
36
|
+
edda_framework-0.4.0.dist-info/METADATA,sha256=YEsevYgVMtBEBJlAhQL1lALOOiNrejsC9Z6QpyZsFRg,30272
|
|
37
|
+
edda_framework-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
+
edda_framework-0.4.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
|
|
39
|
+
edda_framework-0.4.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
|
|
40
|
+
edda_framework-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|