edda-framework 0.1.0__tar.gz → 0.3.0__tar.gz
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_framework-0.1.0 → edda_framework-0.3.0}/PKG-INFO +95 -1
- {edda_framework-0.1.0 → edda_framework-0.3.0}/README.md +90 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/demo_app.py +6 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/workflows-activities.md +64 -9
- edda_framework-0.3.0/docs/integrations/mcp.md +230 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/__init__.py +2 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/activity.py +32 -19
- edda_framework-0.3.0/edda/integrations/__init__.py +1 -0
- edda_framework-0.3.0/edda/integrations/mcp/__init__.py +40 -0
- edda_framework-0.3.0/edda/integrations/mcp/decorators.py +168 -0
- edda_framework-0.3.0/edda/integrations/mcp/server.py +257 -0
- edda_framework-0.3.0/edda/wsgi.py +77 -0
- edda_framework-0.3.0/examples/mcp/README.md +301 -0
- edda_framework-0.3.0/examples/mcp/order_processing_mcp.py +189 -0
- edda_framework-0.3.0/examples/mcp/remote_server_example.py +122 -0
- edda_framework-0.3.0/examples/mcp/simple_mcp_server.py +82 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/pyproject.toml +6 -1
- edda_framework-0.3.0/tests/integrations/__init__.py +1 -0
- edda_framework-0.3.0/tests/integrations/mcp/__init__.py +1 -0
- edda_framework-0.3.0/tests/integrations/mcp/test_integration.py +183 -0
- edda_framework-0.3.0/tests/integrations/mcp/test_jsonrpc.py +139 -0
- edda_framework-0.3.0/tests/integrations/mcp/test_server.py +104 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_activity.py +12 -6
- edda_framework-0.3.0/tests/test_activity_sync.py +218 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/uv.lock +242 -2
- {edda_framework-0.1.0 → edda_framework-0.3.0}/zensical.toml +3 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/.gitignore +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/.python-version +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/Justfile +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/LICENSE +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/examples/events.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/index.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/markdown.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/context.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/exceptions.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/hooks.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/locking.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/replay.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/retry.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/storage/models.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/edda/workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/retry_example.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/conftest.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_context.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_locking.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_replay.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.3.0}/viewer_app.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -20,7 +20,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
21
|
Classifier: Topic :: System :: Distributed Computing
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: a2wsgi>=1.10.0
|
|
23
24
|
Requires-Dist: aiosqlite>=0.21.0
|
|
25
|
+
Requires-Dist: anyio>=4.0.0
|
|
24
26
|
Requires-Dist: cloudevents>=1.12.0
|
|
25
27
|
Requires-Dist: httpx>=0.28.1
|
|
26
28
|
Requires-Dist: pydantic>=2.0.0
|
|
@@ -36,6 +38,8 @@ Requires-Dist: ruff>=0.14.2; extra == 'dev'
|
|
|
36
38
|
Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
|
|
37
39
|
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
|
|
38
40
|
Requires-Dist: tsuno>=0.1.3; extra == 'dev'
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp>=1.22.0; extra == 'mcp'
|
|
39
43
|
Provides-Extra: mysql
|
|
40
44
|
Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
|
|
41
45
|
Provides-Extra: postgresql
|
|
@@ -75,6 +79,8 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
75
79
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
76
80
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
77
81
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
82
|
+
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
83
|
+
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
78
84
|
|
|
79
85
|
## Use Cases
|
|
80
86
|
|
|
@@ -638,6 +644,94 @@ api.mount("/workflows", edda_app)
|
|
|
638
644
|
|
|
639
645
|
This works with any ASGI framework (Starlette, FastAPI, Quart, etc.)
|
|
640
646
|
|
|
647
|
+
### WSGI Integration
|
|
648
|
+
|
|
649
|
+
For WSGI environments (gunicorn, uWSGI, Flask, Django), use the WSGI adapter:
|
|
650
|
+
|
|
651
|
+
```python
|
|
652
|
+
from edda import EddaApp
|
|
653
|
+
from edda.wsgi import create_wsgi_app
|
|
654
|
+
|
|
655
|
+
# Create Edda app
|
|
656
|
+
edda_app = EddaApp(db_url="sqlite:///workflow.db")
|
|
657
|
+
|
|
658
|
+
# Convert to WSGI
|
|
659
|
+
wsgi_application = create_wsgi_app(edda_app)
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Running with WSGI servers:**
|
|
663
|
+
|
|
664
|
+
```bash
|
|
665
|
+
# With Gunicorn
|
|
666
|
+
gunicorn demo_app:wsgi_application --workers 4
|
|
667
|
+
|
|
668
|
+
# With uWSGI
|
|
669
|
+
uwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Sync Activities**: For WSGI environments or legacy codebases, you can write synchronous activities:
|
|
673
|
+
|
|
674
|
+
```python
|
|
675
|
+
from edda import activity, WorkflowContext
|
|
676
|
+
|
|
677
|
+
@activity
|
|
678
|
+
def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
679
|
+
# Sync function - automatically executed in thread pool
|
|
680
|
+
# No async/await needed!
|
|
681
|
+
return {"status": "paid", "amount": amount}
|
|
682
|
+
|
|
683
|
+
@workflow
|
|
684
|
+
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
685
|
+
# Workflows still use async (for deterministic replay)
|
|
686
|
+
result = await process_payment(ctx, 99.99, activity_id="pay:1")
|
|
687
|
+
return result
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.
|
|
691
|
+
|
|
692
|
+
## MCP Integration
|
|
693
|
+
|
|
694
|
+
Edda integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants like Claude to interact with your durable workflows as long-running tools.
|
|
695
|
+
|
|
696
|
+
### Quick Example
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
700
|
+
from edda import WorkflowContext, activity
|
|
701
|
+
|
|
702
|
+
# Create MCP server
|
|
703
|
+
server = EddaMCPServer(
|
|
704
|
+
name="Order Service",
|
|
705
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
@activity
|
|
709
|
+
async def process_payment(ctx: WorkflowContext, amount: float):
|
|
710
|
+
return {"status": "paid", "amount": amount}
|
|
711
|
+
|
|
712
|
+
@server.durable_tool(description="Process customer order")
|
|
713
|
+
async def process_order(ctx: WorkflowContext, order_id: str):
|
|
714
|
+
await process_payment(ctx, 99.99)
|
|
715
|
+
return {"status": "completed", "order_id": order_id}
|
|
716
|
+
|
|
717
|
+
# Deploy with uvicorn
|
|
718
|
+
if __name__ == "__main__":
|
|
719
|
+
import uvicorn
|
|
720
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Auto-Generated Tools
|
|
724
|
+
|
|
725
|
+
Each `@durable_tool` automatically generates **three MCP tools**:
|
|
726
|
+
|
|
727
|
+
1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
|
|
728
|
+
2. **Status tool** (`process_order_status`): Checks workflow progress
|
|
729
|
+
3. **Result tool** (`process_order_result`): Gets final result when completed
|
|
730
|
+
|
|
731
|
+
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
732
|
+
|
|
733
|
+
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
734
|
+
|
|
641
735
|
## Observability Hooks
|
|
642
736
|
|
|
643
737
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -27,6 +27,8 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
27
27
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
28
28
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
29
29
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
30
|
+
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
31
|
+
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
30
32
|
|
|
31
33
|
## Use Cases
|
|
32
34
|
|
|
@@ -590,6 +592,94 @@ api.mount("/workflows", edda_app)
|
|
|
590
592
|
|
|
591
593
|
This works with any ASGI framework (Starlette, FastAPI, Quart, etc.)
|
|
592
594
|
|
|
595
|
+
### WSGI Integration
|
|
596
|
+
|
|
597
|
+
For WSGI environments (gunicorn, uWSGI, Flask, Django), use the WSGI adapter:
|
|
598
|
+
|
|
599
|
+
```python
|
|
600
|
+
from edda import EddaApp
|
|
601
|
+
from edda.wsgi import create_wsgi_app
|
|
602
|
+
|
|
603
|
+
# Create Edda app
|
|
604
|
+
edda_app = EddaApp(db_url="sqlite:///workflow.db")
|
|
605
|
+
|
|
606
|
+
# Convert to WSGI
|
|
607
|
+
wsgi_application = create_wsgi_app(edda_app)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**Running with WSGI servers:**
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
# With Gunicorn
|
|
614
|
+
gunicorn demo_app:wsgi_application --workers 4
|
|
615
|
+
|
|
616
|
+
# With uWSGI
|
|
617
|
+
uwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Sync Activities**: For WSGI environments or legacy codebases, you can write synchronous activities:
|
|
621
|
+
|
|
622
|
+
```python
|
|
623
|
+
from edda import activity, WorkflowContext
|
|
624
|
+
|
|
625
|
+
@activity
|
|
626
|
+
def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
627
|
+
# Sync function - automatically executed in thread pool
|
|
628
|
+
# No async/await needed!
|
|
629
|
+
return {"status": "paid", "amount": amount}
|
|
630
|
+
|
|
631
|
+
@workflow
|
|
632
|
+
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
633
|
+
# Workflows still use async (for deterministic replay)
|
|
634
|
+
result = await process_payment(ctx, 99.99, activity_id="pay:1")
|
|
635
|
+
return result
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.
|
|
639
|
+
|
|
640
|
+
## MCP Integration
|
|
641
|
+
|
|
642
|
+
Edda integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants like Claude to interact with your durable workflows as long-running tools.
|
|
643
|
+
|
|
644
|
+
### Quick Example
|
|
645
|
+
|
|
646
|
+
```python
|
|
647
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
648
|
+
from edda import WorkflowContext, activity
|
|
649
|
+
|
|
650
|
+
# Create MCP server
|
|
651
|
+
server = EddaMCPServer(
|
|
652
|
+
name="Order Service",
|
|
653
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
@activity
|
|
657
|
+
async def process_payment(ctx: WorkflowContext, amount: float):
|
|
658
|
+
return {"status": "paid", "amount": amount}
|
|
659
|
+
|
|
660
|
+
@server.durable_tool(description="Process customer order")
|
|
661
|
+
async def process_order(ctx: WorkflowContext, order_id: str):
|
|
662
|
+
await process_payment(ctx, 99.99)
|
|
663
|
+
return {"status": "completed", "order_id": order_id}
|
|
664
|
+
|
|
665
|
+
# Deploy with uvicorn
|
|
666
|
+
if __name__ == "__main__":
|
|
667
|
+
import uvicorn
|
|
668
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Auto-Generated Tools
|
|
672
|
+
|
|
673
|
+
Each `@durable_tool` automatically generates **three MCP tools**:
|
|
674
|
+
|
|
675
|
+
1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
|
|
676
|
+
2. **Status tool** (`process_order_status`): Checks workflow progress
|
|
677
|
+
3. **Result tool** (`process_order_result`): Gets final result when completed
|
|
678
|
+
|
|
679
|
+
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
680
|
+
|
|
681
|
+
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
682
|
+
|
|
593
683
|
## Observability Hooks
|
|
594
684
|
|
|
595
685
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -1817,3 +1817,9 @@ async def scheduled_order_shipment_workflow(
|
|
|
1817
1817
|
# Export as ASGI application
|
|
1818
1818
|
# No need to manually register event handlers!
|
|
1819
1819
|
application = app
|
|
1820
|
+
|
|
1821
|
+
# Export as WSGI application (for gunicorn, uWSGI, etc.)
|
|
1822
|
+
# Usage: gunicorn demo_app:wsgi_application --workers 4
|
|
1823
|
+
from edda.wsgi import create_wsgi_app
|
|
1824
|
+
|
|
1825
|
+
wsgi_application = create_wsgi_app(app)
|
|
@@ -121,6 +121,46 @@ async def create_order_with_db(ctx: WorkflowContext, order_id: str):
|
|
|
121
121
|
return {"order_id": order_id}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
### Sync Activities (WSGI Compatibility)
|
|
125
|
+
|
|
126
|
+
For WSGI environments (gunicorn, uWSGI) or legacy codebases, Edda supports synchronous activities:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from edda import activity, WorkflowContext
|
|
130
|
+
|
|
131
|
+
@activity
|
|
132
|
+
def create_user_record(ctx: WorkflowContext, user_id: str, email: str) -> dict:
|
|
133
|
+
"""Sync activity - executed in thread pool"""
|
|
134
|
+
# Traditional sync code - no async/await needed!
|
|
135
|
+
user = User(user_id=user_id, email=email)
|
|
136
|
+
db.session.add(user)
|
|
137
|
+
db.session.commit()
|
|
138
|
+
return {"user_id": user.id}
|
|
139
|
+
|
|
140
|
+
@activity
|
|
141
|
+
async def async_activity(ctx: WorkflowContext, data: str) -> dict:
|
|
142
|
+
"""Async activity - recommended for I/O operations"""
|
|
143
|
+
result = await httpx.get(f"https://api.example.com/{data}")
|
|
144
|
+
return result.json()
|
|
145
|
+
|
|
146
|
+
@workflow
|
|
147
|
+
async def mixed_workflow(ctx: WorkflowContext, user_id: str) -> dict:
|
|
148
|
+
# Workflows are always async (for deterministic replay)
|
|
149
|
+
# But can call both sync and async activities
|
|
150
|
+
user = await create_user_record(ctx, user_id, "user@example.com", activity_id="create:1")
|
|
151
|
+
data = await async_activity(ctx, user_id, activity_id="fetch:1")
|
|
152
|
+
return {"user": user, "data": data}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**When to use sync activities:**
|
|
156
|
+
|
|
157
|
+
- ✅ Existing sync codebases (Flask, Django)
|
|
158
|
+
- ✅ WSGI deployments (gunicorn, uWSGI)
|
|
159
|
+
- ✅ Libraries without async support
|
|
160
|
+
- ✅ Simple CPU-bound operations
|
|
161
|
+
|
|
162
|
+
**Performance note:** Async activities are recommended for I/O-bound operations (database queries, HTTP requests, file I/O) for better performance. Sync activities are executed in a thread pool to avoid blocking the event loop.
|
|
163
|
+
|
|
124
164
|
## Retry Policies
|
|
125
165
|
|
|
126
166
|
Activities automatically retry on failure with exponential backoff. This provides resilience against transient failures like network timeouts or temporary service unavailability.
|
|
@@ -755,26 +795,41 @@ async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
|
|
|
755
795
|
pass
|
|
756
796
|
```
|
|
757
797
|
|
|
758
|
-
### 4.
|
|
798
|
+
### 4. Choose Async or Sync Appropriately
|
|
759
799
|
|
|
760
|
-
|
|
800
|
+
✅ **Preferred: Async activities** (better performance for I/O)
|
|
761
801
|
|
|
762
802
|
```python
|
|
763
803
|
@activity
|
|
764
|
-
def
|
|
765
|
-
#
|
|
766
|
-
|
|
804
|
+
async def fetch_user_data(ctx: WorkflowContext, user_id: str) -> dict:
|
|
805
|
+
# Async I/O operations (recommended)
|
|
806
|
+
result = await httpx.get(f"https://api.example.com/users/{user_id}")
|
|
807
|
+
return result.json()
|
|
767
808
|
```
|
|
768
809
|
|
|
769
|
-
✅ **
|
|
810
|
+
✅ **Valid: Sync activities** (WSGI compatibility, legacy code)
|
|
770
811
|
|
|
771
812
|
```python
|
|
772
813
|
@activity
|
|
773
|
-
|
|
774
|
-
#
|
|
775
|
-
|
|
814
|
+
def process_legacy_data(ctx: WorkflowContext, data: str) -> dict:
|
|
815
|
+
# Sync operations (executed in thread pool)
|
|
816
|
+
result = legacy_library.process(data) # No async support
|
|
817
|
+
return {"processed": result}
|
|
776
818
|
```
|
|
777
819
|
|
|
820
|
+
✅ **Good: Mix sync and async in same workflow**
|
|
821
|
+
|
|
822
|
+
```python
|
|
823
|
+
@workflow
|
|
824
|
+
async def order_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
825
|
+
# Both sync and async activities work fine
|
|
826
|
+
user = await create_user_record(ctx, order_id, activity_id="user:1") # Sync
|
|
827
|
+
payment = await process_payment(ctx, 99.99, activity_id="pay:1") # Async
|
|
828
|
+
return {"user": user, "payment": payment}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**Performance tip**: Prefer async activities for I/O-bound operations (database queries, HTTP requests, file I/O). Use sync activities when integrating with legacy code or libraries without async support.
|
|
832
|
+
|
|
778
833
|
## Next Steps
|
|
779
834
|
|
|
780
835
|
- **[Durable Execution](durable-execution/replay.md)**: Learn how Edda ensures workflows never lose progress
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# MCP (Model Context Protocol) Integration
|
|
2
|
+
|
|
3
|
+
Edda provides seamless integration with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants like Claude to interact with your durable workflows as long-running tools.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
MCP is a standardized protocol for AI tool integration. Edda's MCP integration automatically converts your durable workflows into MCP-compliant tools that:
|
|
8
|
+
|
|
9
|
+
- **Start workflows** and return instance IDs immediately
|
|
10
|
+
- **Check workflow status** to monitor progress
|
|
11
|
+
- **Retrieve results** when workflows complete
|
|
12
|
+
|
|
13
|
+
This enables AI assistants to work with long-running processes that may take minutes, hours, or even days to complete.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install Edda with MCP support:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install edda-framework[mcp]
|
|
21
|
+
|
|
22
|
+
# Or using uv
|
|
23
|
+
uv add edda-framework --extra mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Create an MCP Server
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
32
|
+
from edda import WorkflowContext, activity
|
|
33
|
+
|
|
34
|
+
# Create MCP server
|
|
35
|
+
server = EddaMCPServer(
|
|
36
|
+
name="Order Service",
|
|
37
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@activity
|
|
41
|
+
async def reserve_inventory(ctx: WorkflowContext, items: list[str]):
|
|
42
|
+
# Your business logic here
|
|
43
|
+
return {"reserved": True}
|
|
44
|
+
|
|
45
|
+
@activity
|
|
46
|
+
async def process_payment(ctx: WorkflowContext, amount: float):
|
|
47
|
+
# Payment processing logic
|
|
48
|
+
return {"transaction_id": "txn_123"}
|
|
49
|
+
|
|
50
|
+
@server.durable_tool(description="Process customer order workflow")
|
|
51
|
+
async def process_order(ctx: WorkflowContext, order_id: str, items: list[str]):
|
|
52
|
+
"""
|
|
53
|
+
Long-running order processing workflow.
|
|
54
|
+
|
|
55
|
+
This workflow reserves inventory, processes payment, and ships the order.
|
|
56
|
+
"""
|
|
57
|
+
# Reserve inventory
|
|
58
|
+
await reserve_inventory(ctx, items)
|
|
59
|
+
|
|
60
|
+
# Process payment
|
|
61
|
+
await process_payment(ctx, 99.99)
|
|
62
|
+
|
|
63
|
+
return {"status": "completed", "order_id": order_id}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Deploy the Server
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Deploy with uvicorn (production)
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
import uvicorn
|
|
72
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Run the server
|
|
77
|
+
uvicorn your_app:server.asgi_app --host 0.0.0.0 --port 8000
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Use from MCP Clients (e.g., Claude Desktop)
|
|
81
|
+
|
|
82
|
+
Add to your MCP client configuration (e.g., Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"order-service": {
|
|
88
|
+
"command": "uvicorn",
|
|
89
|
+
"args": ["your_app:server.asgi_app", "--host", "127.0.0.1", "--port", "8000"],
|
|
90
|
+
"env": {
|
|
91
|
+
"DATABASE_URL": "postgresql://user:pass@localhost/orders"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Auto-Generated Tools
|
|
99
|
+
|
|
100
|
+
Each `@durable_tool` automatically generates **three MCP tools**:
|
|
101
|
+
|
|
102
|
+
### 1. Main Tool: Start Workflow
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Tool Name: process_order
|
|
106
|
+
Description: Process customer order workflow
|
|
107
|
+
|
|
108
|
+
Input: {"order_id": "ORD-123", "items": ["item1", "item2"]}
|
|
109
|
+
Output: {
|
|
110
|
+
"content": [{
|
|
111
|
+
"type": "text",
|
|
112
|
+
"text": "Workflow 'process_order' started successfully.\nInstance ID: abc123...\n\nUse 'process_order_status' tool to check progress."
|
|
113
|
+
}],
|
|
114
|
+
"isError": false
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Status Tool: Check Progress
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Tool Name: process_order_status
|
|
122
|
+
Description: Check status of process_order workflow
|
|
123
|
+
|
|
124
|
+
Input: {"instance_id": "abc123..."}
|
|
125
|
+
Output: {
|
|
126
|
+
"content": [{
|
|
127
|
+
"type": "text",
|
|
128
|
+
"text": "Workflow Status: running\nCurrent Activity: payment:1\nInstance ID: abc123..."
|
|
129
|
+
}],
|
|
130
|
+
"isError": false
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3. Result Tool: Get Final Result
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Tool Name: process_order_result
|
|
138
|
+
Description: Get result of process_order workflow (if completed)
|
|
139
|
+
|
|
140
|
+
Input: {"instance_id": "abc123..."}
|
|
141
|
+
Output: {
|
|
142
|
+
"content": [{
|
|
143
|
+
"type": "text",
|
|
144
|
+
"text": "Workflow Result:\n{'status': 'completed', 'order_id': 'ORD-123'}"
|
|
145
|
+
}],
|
|
146
|
+
"isError": false
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Advanced Configuration
|
|
151
|
+
|
|
152
|
+
### Authentication
|
|
153
|
+
|
|
154
|
+
Protect your MCP server with token-based authentication:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def verify_token(token: str) -> bool:
|
|
158
|
+
# Your token verification logic
|
|
159
|
+
return token == "secret-token-123"
|
|
160
|
+
|
|
161
|
+
server = EddaMCPServer(
|
|
162
|
+
name="Order Service",
|
|
163
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
164
|
+
token_verifier=verify_token,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Clients must include the token in the Authorization header:
|
|
169
|
+
|
|
170
|
+
```http
|
|
171
|
+
Authorization: Bearer secret-token-123
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Transactional Outbox Pattern
|
|
175
|
+
|
|
176
|
+
Enable event-driven architecture with outbox pattern:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
server = EddaMCPServer(
|
|
180
|
+
name="Order Service",
|
|
181
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
182
|
+
outbox_enabled=True,
|
|
183
|
+
broker_url="nats://localhost:4222",
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## MCP Protocol Compliance
|
|
188
|
+
|
|
189
|
+
Edda's MCP integration follows the [MCP Tools specification](https://modelcontextprotocol.io/docs/concepts/tools):
|
|
190
|
+
|
|
191
|
+
- **JSON-RPC 2.0**: All communication uses JSON-RPC 2.0 protocol
|
|
192
|
+
- **Content Arrays**: Responses include `content` array with text/image/resource items
|
|
193
|
+
- **Error Handling**: Errors are reported with `isError: true` flag
|
|
194
|
+
- **Stateless HTTP**: Uses MCP's streamable HTTP transport for production deployments
|
|
195
|
+
|
|
196
|
+
## Architecture
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
┌─────────────────┐
|
|
200
|
+
│ MCP Client │
|
|
201
|
+
└────────┬────────┘
|
|
202
|
+
│ JSON-RPC 2.0
|
|
203
|
+
│ (HTTP Transport)
|
|
204
|
+
▼
|
|
205
|
+
┌─────────────────┐
|
|
206
|
+
│ EddaMCPServer │
|
|
207
|
+
│ ┌─────────┐ │
|
|
208
|
+
│ │ FastMCP │ │ ← Official MCP SDK
|
|
209
|
+
│ └─────────┘ │
|
|
210
|
+
│ ┌─────────┐ │
|
|
211
|
+
│ │ EddaApp │ │ ← Durable Execution
|
|
212
|
+
│ └─────────┘ │
|
|
213
|
+
└────────┬────────┘
|
|
214
|
+
│
|
|
215
|
+
▼
|
|
216
|
+
┌─────────────────┐
|
|
217
|
+
│ Database │
|
|
218
|
+
└─────────────────┘
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
## Related Documentation
|
|
223
|
+
|
|
224
|
+
- [Edda Workflows and Activities](../core-features/workflows-activities.md)
|
|
225
|
+
- [Transactional Outbox Pattern](../core-features/transactional-outbox.md)
|
|
226
|
+
- [MCP Protocol Specification](https://modelcontextprotocol.io/)
|
|
227
|
+
|
|
228
|
+
## Examples
|
|
229
|
+
|
|
230
|
+
See the [examples/mcp/](../../examples/mcp/) directory for complete working examples.
|
|
@@ -30,6 +30,7 @@ from edda.hooks import HooksBase, WorkflowHooks
|
|
|
30
30
|
from edda.outbox import OutboxRelayer, send_event_transactional
|
|
31
31
|
from edda.retry import RetryPolicy
|
|
32
32
|
from edda.workflow import workflow
|
|
33
|
+
from edda.wsgi import create_wsgi_app
|
|
33
34
|
|
|
34
35
|
__version__ = "0.1.0"
|
|
35
36
|
|
|
@@ -53,4 +54,5 @@ __all__ = [
|
|
|
53
54
|
"RetryPolicy",
|
|
54
55
|
"RetryExhaustedError",
|
|
55
56
|
"TerminalError",
|
|
57
|
+
"create_wsgi_app",
|
|
56
58
|
]
|