edda-framework 0.2.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.2.0 → edda_framework-0.3.0}/PKG-INFO +47 -1
- {edda_framework-0.2.0 → edda_framework-0.3.0}/README.md +44 -0
- edda_framework-0.3.0/docs/integrations/mcp.md +230 -0
- 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/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.2.0 → edda_framework-0.3.0}/pyproject.toml +4 -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.2.0 → edda_framework-0.3.0}/uv.lock +229 -2
- {edda_framework-0.2.0 → edda_framework-0.3.0}/zensical.toml +3 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/.gitignore +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/.python-version +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/Justfile +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/LICENSE +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/demo_app.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/examples/events.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/index.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/markdown.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/activity.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/app.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/compensation.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/context.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/events.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/exceptions.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/hooks.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/locking.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/replay.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/retry.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/storage/models.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/edda/wsgi.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/retry_example.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/__init__.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/conftest.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_activity.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_app.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_context.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_events.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_locking.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_replay.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_storage.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.2.0 → edda_framework-0.3.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.2.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
|
|
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.14.2; extra == 'dev'
|
|
|
38
38
|
Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
|
|
39
39
|
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
|
|
40
40
|
Requires-Dist: tsuno>=0.1.3; extra == 'dev'
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp>=1.22.0; extra == 'mcp'
|
|
41
43
|
Provides-Extra: mysql
|
|
42
44
|
Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
|
|
43
45
|
Provides-Extra: postgresql
|
|
@@ -77,6 +79,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
77
79
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
78
80
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
79
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
|
|
80
83
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
81
84
|
|
|
82
85
|
## Use Cases
|
|
@@ -686,6 +689,49 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
|
686
689
|
|
|
687
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.
|
|
688
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
|
+
|
|
689
735
|
## Observability Hooks
|
|
690
736
|
|
|
691
737
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -27,6 +27,7 @@ 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
|
|
30
31
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
31
32
|
|
|
32
33
|
## Use Cases
|
|
@@ -636,6 +637,49 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
|
636
637
|
|
|
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.
|
|
638
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
|
+
|
|
639
683
|
## Observability Hooks
|
|
640
684
|
|
|
641
685
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Edda integrations package."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edda MCP (Model Context Protocol) Integration.
|
|
3
|
+
|
|
4
|
+
Provides MCP server functionality for Edda durable workflows,
|
|
5
|
+
enabling long-running workflow tools via the MCP protocol.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
10
|
+
from edda import WorkflowContext, activity
|
|
11
|
+
|
|
12
|
+
server = EddaMCPServer(
|
|
13
|
+
name="Order Service",
|
|
14
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@activity
|
|
18
|
+
async def reserve_inventory(ctx, items):
|
|
19
|
+
return {"reserved": True}
|
|
20
|
+
|
|
21
|
+
@server.durable_tool(description="Process order workflow")
|
|
22
|
+
async def process_order(ctx: WorkflowContext, order_id: str):
|
|
23
|
+
await reserve_inventory(ctx, [order_id], activity_id="reserve:1")
|
|
24
|
+
return {"status": "completed"}
|
|
25
|
+
|
|
26
|
+
# Deploy with uvicorn
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
import uvicorn
|
|
29
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The server automatically generates three MCP tools for each @durable_tool:
|
|
33
|
+
- `tool_name`: Start the workflow, returns instance_id
|
|
34
|
+
- `tool_name_status`: Check workflow status
|
|
35
|
+
- `tool_name_result`: Get workflow result (if completed)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from edda.integrations.mcp.server import EddaMCPServer
|
|
39
|
+
|
|
40
|
+
__all__ = ["EddaMCPServer"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Decorators for MCP durable tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from edda.workflow import workflow
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from edda.integrations.mcp.server import EddaMCPServer
|
|
13
|
+
from edda.workflow import Workflow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_durable_tool(
|
|
17
|
+
server: EddaMCPServer,
|
|
18
|
+
func: Callable,
|
|
19
|
+
*,
|
|
20
|
+
description: str = "",
|
|
21
|
+
) -> Workflow:
|
|
22
|
+
"""
|
|
23
|
+
Create a durable workflow tool with auto-generated status/result tools.
|
|
24
|
+
|
|
25
|
+
This function:
|
|
26
|
+
1. Wraps the function as an Edda @workflow
|
|
27
|
+
2. Registers three MCP tools:
|
|
28
|
+
- {name}: Start workflow, return instance_id
|
|
29
|
+
- {name}_status: Check workflow status
|
|
30
|
+
- {name}_result: Get workflow result
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
server: EddaMCPServer instance
|
|
34
|
+
func: Async workflow function
|
|
35
|
+
description: Tool description
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Workflow instance
|
|
39
|
+
"""
|
|
40
|
+
# 1. Create Edda workflow
|
|
41
|
+
workflow_instance: Workflow = workflow(func, event_handler=False)
|
|
42
|
+
workflow_name = func.__name__
|
|
43
|
+
|
|
44
|
+
# Register in server's workflow registry
|
|
45
|
+
server._workflows[workflow_name] = workflow_instance
|
|
46
|
+
|
|
47
|
+
# 2. Generate main tool (start workflow)
|
|
48
|
+
tool_description = description or func.__doc__ or f"Start {workflow_name} workflow"
|
|
49
|
+
|
|
50
|
+
# Extract parameters from workflow function (excluding ctx)
|
|
51
|
+
sig = inspect.signature(func)
|
|
52
|
+
params = [
|
|
53
|
+
param
|
|
54
|
+
for name, param in sig.parameters.items()
|
|
55
|
+
if name != "ctx" # Exclude WorkflowContext parameter
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Create the tool function
|
|
59
|
+
async def start_tool(**kwargs: Any) -> dict:
|
|
60
|
+
"""
|
|
61
|
+
Start workflow and return instance_id.
|
|
62
|
+
|
|
63
|
+
This is the main entry point for the durable tool.
|
|
64
|
+
"""
|
|
65
|
+
# Remove 'ctx' if provided by client (workflow will inject it)
|
|
66
|
+
kwargs.pop("ctx", None)
|
|
67
|
+
|
|
68
|
+
# Start Edda workflow
|
|
69
|
+
instance_id = await workflow_instance.start(**kwargs)
|
|
70
|
+
|
|
71
|
+
# Return MCP-compliant response
|
|
72
|
+
return {
|
|
73
|
+
"content": [
|
|
74
|
+
{
|
|
75
|
+
"type": "text",
|
|
76
|
+
"text": (
|
|
77
|
+
f"Workflow '{workflow_name}' started successfully.\n"
|
|
78
|
+
f"Instance ID: {instance_id}\n\n"
|
|
79
|
+
f"Use '{workflow_name}_status' tool with instance_id='{instance_id}' to check progress.\n"
|
|
80
|
+
f"Use '{workflow_name}_result' tool to get the final result once completed."
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"isError": False,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Override the function's signature for introspection (FastMCP uses this for schema generation)
|
|
88
|
+
start_tool.__signature__ = inspect.Signature(parameters=params) # type: ignore[attr-defined]
|
|
89
|
+
|
|
90
|
+
# Register with FastMCP (call as function, not decorator syntax)
|
|
91
|
+
server._mcp.tool(name=workflow_name, description=tool_description)(start_tool)
|
|
92
|
+
|
|
93
|
+
# 3. Generate status tool
|
|
94
|
+
status_tool_name = f"{workflow_name}_status"
|
|
95
|
+
status_tool_description = f"Check status of {workflow_name} workflow"
|
|
96
|
+
|
|
97
|
+
@server._mcp.tool(name=status_tool_name, description=status_tool_description)
|
|
98
|
+
async def status_tool(instance_id: str) -> dict:
|
|
99
|
+
"""Check workflow status."""
|
|
100
|
+
try:
|
|
101
|
+
instance = await server._edda_app.storage.get_instance(instance_id)
|
|
102
|
+
|
|
103
|
+
status = instance["status"]
|
|
104
|
+
current_activity_id = instance.get("current_activity_id", "N/A")
|
|
105
|
+
|
|
106
|
+
status_text = (
|
|
107
|
+
f"Workflow Status: {status}\n"
|
|
108
|
+
f"Current Activity: {current_activity_id}\n"
|
|
109
|
+
f"Instance ID: {instance_id}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"content": [{"type": "text", "text": status_text}],
|
|
114
|
+
"isError": False,
|
|
115
|
+
}
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return {
|
|
118
|
+
"content": [
|
|
119
|
+
{
|
|
120
|
+
"type": "text",
|
|
121
|
+
"text": f"Error checking status: {str(e)}",
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"isError": True,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# 4. Generate result tool
|
|
128
|
+
result_tool_name = f"{workflow_name}_result"
|
|
129
|
+
result_tool_description = f"Get result of {workflow_name} workflow (if completed)"
|
|
130
|
+
|
|
131
|
+
@server._mcp.tool(name=result_tool_name, description=result_tool_description)
|
|
132
|
+
async def result_tool(instance_id: str) -> dict:
|
|
133
|
+
"""Get workflow result (if completed)."""
|
|
134
|
+
try:
|
|
135
|
+
instance = await server._edda_app.storage.get_instance(instance_id)
|
|
136
|
+
|
|
137
|
+
status = instance["status"]
|
|
138
|
+
|
|
139
|
+
if status != "completed":
|
|
140
|
+
return {
|
|
141
|
+
"content": [
|
|
142
|
+
{
|
|
143
|
+
"type": "text",
|
|
144
|
+
"text": f"Workflow not completed yet. Current status: {status}",
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"isError": True,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
output_data = instance.get("output_data")
|
|
151
|
+
result_text = f"Workflow Result:\n{output_data}"
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"content": [{"type": "text", "text": result_text}],
|
|
155
|
+
"isError": False,
|
|
156
|
+
}
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return {
|
|
159
|
+
"content": [
|
|
160
|
+
{
|
|
161
|
+
"type": "text",
|
|
162
|
+
"text": f"Error getting result: {str(e)}",
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"isError": True,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return workflow_instance
|