edda-framework 0.3.0__tar.gz → 0.4.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.3.0 → edda_framework-0.4.0}/PKG-INFO +27 -1
- {edda_framework-0.3.0 → edda_framework-0.4.0}/README.md +26 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/demo_app.py +4 -4
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/durable-execution/replay.md +8 -8
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/workflows-activities.md +4 -4
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/getting-started/first-workflow.md +32 -28
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/index.md +8 -7
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/integrations/mcp/decorators.py +31 -12
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/integrations/mcp/server.py +177 -21
- edda_framework-0.4.0/examples/mcp/prompts_example.py +281 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/observability_with_logfire.py +1 -1
- {edda_framework-0.3.0 → edda_framework-0.4.0}/pyproject.toml +1 -1
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/integrations/mcp/test_integration.py +6 -6
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/integrations/mcp/test_jsonrpc.py +3 -3
- edda_framework-0.4.0/tests/integrations/mcp/test_prompts.py +203 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/integrations/mcp/test_server.py +2 -2
- {edda_framework-0.3.0 → edda_framework-0.4.0}/uv.lock +1 -1
- {edda_framework-0.3.0 → edda_framework-0.4.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/.gitignore +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/.python-version +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/Justfile +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/LICENSE +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/examples/events.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/integrations/mcp.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/markdown.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/activity.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/app.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/compensation.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/context.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/events.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/exceptions.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/hooks.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/locking.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/replay.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/retry.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/storage/models.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/edda/wsgi.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/retry_example.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/conftest.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_activity.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_app.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_context.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_events.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_locking.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_replay.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_storage.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/viewer_app.py +0 -0
- {edda_framework-0.3.0 → edda_framework-0.4.0}/zensical.toml +0 -0
|
@@ -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
|
|
@@ -678,6 +678,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
|
|
|
678
678
|
|
|
679
679
|
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
680
680
|
|
|
681
|
+
### MCP Prompts
|
|
682
|
+
|
|
683
|
+
Define reusable prompt templates that can access workflow state:
|
|
684
|
+
|
|
685
|
+
```python
|
|
686
|
+
from mcp.server.fastmcp.prompts.base import UserMessage
|
|
687
|
+
from mcp.types import TextContent
|
|
688
|
+
|
|
689
|
+
@server.prompt(description="Analyze a workflow execution")
|
|
690
|
+
async def analyze_workflow(instance_id: str) -> UserMessage:
|
|
691
|
+
"""Generate analysis prompt for a specific workflow."""
|
|
692
|
+
instance = await server.storage.get_instance(instance_id)
|
|
693
|
+
history = await server.storage.get_history(instance_id)
|
|
694
|
+
|
|
695
|
+
text = f"""Analyze this workflow:
|
|
696
|
+
**Status**: {instance['status']}
|
|
697
|
+
**Activities**: {len(history)}
|
|
698
|
+
**Result**: {instance.get('output_data')}
|
|
699
|
+
|
|
700
|
+
Please provide insights and optimization suggestions."""
|
|
701
|
+
|
|
702
|
+
return UserMessage(content=TextContent(type="text", text=text))
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
AI clients can use these prompts to generate context-aware analysis of your workflows.
|
|
706
|
+
|
|
681
707
|
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
682
708
|
|
|
683
709
|
## Observability Hooks
|
|
@@ -492,10 +492,10 @@ async def order_processing_workflow(
|
|
|
492
492
|
{"item_id": "ITEM-2", "name": "Product B", "price": 49.99, "quantity": 1}
|
|
493
493
|
],
|
|
494
494
|
"shipping_address": {
|
|
495
|
-
"street": "
|
|
496
|
-
"city": "
|
|
497
|
-
"state": "
|
|
498
|
-
"zip_code": "
|
|
495
|
+
"street": "221B Baker Street",
|
|
496
|
+
"city": "London",
|
|
497
|
+
"state": "Greater London",
|
|
498
|
+
"zip_code": "NW1 6XE"
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
501
|
|
{edda_framework-0.3.0 → edda_framework-0.4.0}/docs/core-features/durable-execution/replay.md
RENAMED
|
@@ -100,9 +100,9 @@ async def arrange_shipping(ctx: WorkflowContext, order_id: str):
|
|
|
100
100
|
@workflow
|
|
101
101
|
async def order_workflow(ctx: WorkflowContext, order_id: str):
|
|
102
102
|
# Activity IDs are auto-generated for sequential calls
|
|
103
|
-
inventory = await reserve_inventory(ctx, order_id
|
|
104
|
-
payment = await process_payment(ctx, order_id
|
|
105
|
-
shipping = await arrange_shipping(ctx, order_id
|
|
103
|
+
inventory = await reserve_inventory(ctx, order_id)
|
|
104
|
+
payment = await process_payment(ctx, order_id)
|
|
105
|
+
shipping = await arrange_shipping(ctx, order_id)
|
|
106
106
|
|
|
107
107
|
return {"status": "completed"}
|
|
108
108
|
```
|
|
@@ -497,16 +497,16 @@ async with workflow_lock(storage, instance_id, worker_id):
|
|
|
497
497
|
```python
|
|
498
498
|
@workflow
|
|
499
499
|
async def order_workflow(ctx: WorkflowContext, order_id: str):
|
|
500
|
-
# Activity 1
|
|
501
|
-
inventory = await reserve_inventory(ctx, order_id
|
|
500
|
+
# Activity 1 (auto-generated ID: "reserve_inventory:1")
|
|
501
|
+
inventory = await reserve_inventory(ctx, order_id)
|
|
502
502
|
# → DB saved: activity_id="reserve_inventory:1", result={"reservation_id": "R123"}
|
|
503
503
|
|
|
504
|
-
# Activity 2
|
|
505
|
-
payment = await process_payment(ctx, order_id
|
|
504
|
+
# Activity 2 (auto-generated ID: "process_payment:1")
|
|
505
|
+
payment = await process_payment(ctx, order_id)
|
|
506
506
|
# → DB saved: activity_id="process_payment:1", result={"transaction_id": "T456"}
|
|
507
507
|
|
|
508
508
|
# Activity 3: Exception occurs (e.g., network error)
|
|
509
|
-
shipping = await arrange_shipping(ctx, order_id
|
|
509
|
+
shipping = await arrange_shipping(ctx, order_id)
|
|
510
510
|
# → Exception thrown, workflow interrupted
|
|
511
511
|
```
|
|
512
512
|
|
|
@@ -147,8 +147,8 @@ async def async_activity(ctx: WorkflowContext, data: str) -> dict:
|
|
|
147
147
|
async def mixed_workflow(ctx: WorkflowContext, user_id: str) -> dict:
|
|
148
148
|
# Workflows are always async (for deterministic replay)
|
|
149
149
|
# But can call both sync and async activities
|
|
150
|
-
user = await create_user_record(ctx, user_id, "user@example.com"
|
|
151
|
-
data = await async_activity(ctx, user_id
|
|
150
|
+
user = await create_user_record(ctx, user_id, "user@example.com")
|
|
151
|
+
data = await async_activity(ctx, user_id)
|
|
152
152
|
return {"user": user, "data": data}
|
|
153
153
|
```
|
|
154
154
|
|
|
@@ -823,8 +823,8 @@ def process_legacy_data(ctx: WorkflowContext, data: str) -> dict:
|
|
|
823
823
|
@workflow
|
|
824
824
|
async def order_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
825
825
|
# Both sync and async activities work fine
|
|
826
|
-
user = await create_user_record(ctx, order_id
|
|
827
|
-
payment = await process_payment(ctx, 99.99
|
|
826
|
+
user = await create_user_record(ctx, order_id) # Sync
|
|
827
|
+
payment = await process_payment(ctx, 99.99) # Async
|
|
828
828
|
return {"user": user, "payment": payment}
|
|
829
829
|
```
|
|
830
830
|
|
|
@@ -272,36 +272,40 @@ async def main():
|
|
|
272
272
|
# Initialize the app (required before starting workflows)
|
|
273
273
|
await app.initialize()
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
try:
|
|
276
|
+
# Create order input
|
|
277
|
+
order = OrderInput(
|
|
278
|
+
order_id="ORD-12345",
|
|
279
|
+
customer_email="customer@example.com",
|
|
280
|
+
items=[
|
|
281
|
+
OrderItem(product_id="PROD-1", quantity=2, unit_price=29.99),
|
|
282
|
+
OrderItem(product_id="PROD-2", quantity=1, unit_price=49.99),
|
|
283
|
+
],
|
|
284
|
+
shipping_address=ShippingAddress(
|
|
285
|
+
street="1-2-3 Dogenzaka",
|
|
286
|
+
city="Shibuya",
|
|
287
|
+
postal_code="150-0001",
|
|
288
|
+
country="Japan"
|
|
289
|
+
)
|
|
288
290
|
)
|
|
289
|
-
)
|
|
290
291
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
# Start workflow
|
|
293
|
+
print("Starting order processing workflow...")
|
|
294
|
+
instance_id = await order_processing_workflow.start(input=order)
|
|
295
|
+
|
|
296
|
+
print(f"\n✅ Workflow started: {instance_id}")
|
|
294
297
|
|
|
295
|
-
|
|
298
|
+
# Get result
|
|
299
|
+
instance = await app.storage.get_instance(instance_id)
|
|
300
|
+
if instance["status"] == "completed":
|
|
301
|
+
result = instance["output_data"]
|
|
302
|
+
print(f"📊 Order completed:")
|
|
303
|
+
print(f" - Order ID: {result['order_id']}")
|
|
304
|
+
print(f" - Total: ${result['total_amount']:.2f}")
|
|
305
|
+
print(f" - Tracking: {result['confirmation_number']}")
|
|
296
306
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if instance["status"] == "completed":
|
|
300
|
-
result = instance["output_data"]
|
|
301
|
-
print(f"📊 Order completed:")
|
|
302
|
-
print(f" - Order ID: {result['order_id']}")
|
|
303
|
-
print(f" - Total: ${result['total_amount']:.2f}")
|
|
304
|
-
print(f" - Tracking: {result['confirmation_number']}")
|
|
307
|
+
finally:
|
|
308
|
+
await app.shutdown()
|
|
305
309
|
|
|
306
310
|
if __name__ == "__main__":
|
|
307
311
|
asyncio.run(main())
|
|
@@ -322,7 +326,7 @@ Starting order processing workflow...
|
|
|
322
326
|
|
|
323
327
|
📦 Reserving inventory for ORD-12345: $109.97
|
|
324
328
|
💳 Processing payment for ORD-12345: $109.97
|
|
325
|
-
🚚 Shipping ORD-12345 to
|
|
329
|
+
🚚 Shipping ORD-12345 to Shibuya, Japan
|
|
326
330
|
|
|
327
331
|
✅ Workflow started: <instance_id>
|
|
328
332
|
📊 Order completed:
|
|
@@ -364,7 +368,7 @@ uv run python order_workflow.py
|
|
|
364
368
|
```
|
|
365
369
|
📦 Reserving inventory for ORD-12345: $109.97
|
|
366
370
|
💳 Processing payment for ORD-12345: $109.97
|
|
367
|
-
🚚 Shipping ORD-12345 to
|
|
371
|
+
🚚 Shipping ORD-12345 to Shibuya, Japan
|
|
368
372
|
💥 Exception: Shipping service unavailable!
|
|
369
373
|
|
|
370
374
|
❌ Refunding payment for ORD-12345: $109.97
|
|
@@ -93,21 +93,22 @@ from edda import EddaApp, workflow, activity, WorkflowContext
|
|
|
93
93
|
|
|
94
94
|
@activity
|
|
95
95
|
async def process_payment(ctx: WorkflowContext, amount: float):
|
|
96
|
-
# Durable execution - automatically recorded in history
|
|
97
96
|
print(f"Processing payment: ${amount}")
|
|
98
97
|
return {"status": "paid", "amount": amount}
|
|
99
98
|
|
|
100
99
|
@workflow
|
|
101
100
|
async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
|
|
102
|
-
# Workflow orchestrates activities with automatic retry on crash
|
|
103
101
|
result = await process_payment(ctx, amount)
|
|
104
102
|
return {"order_id": order_id, **result}
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
|
|
104
|
+
async def main():
|
|
105
|
+
app = EddaApp(service_name="demo-service", db_url="sqlite:///workflow.db")
|
|
106
|
+
await app.initialize()
|
|
107
|
+
try:
|
|
108
|
+
instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
|
|
109
|
+
print(f"Started workflow: {instance_id}")
|
|
110
|
+
finally:
|
|
111
|
+
await app.shutdown()
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
**What happens on crash?**
|
|
@@ -4,18 +4,17 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
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(
|
|
17
16
|
server: EddaMCPServer,
|
|
18
|
-
func: Callable,
|
|
17
|
+
func: Callable[..., Any],
|
|
19
18
|
*,
|
|
20
19
|
description: str = "",
|
|
21
20
|
) -> Workflow:
|
|
@@ -38,7 +37,7 @@ def create_durable_tool(
|
|
|
38
37
|
Workflow instance
|
|
39
38
|
"""
|
|
40
39
|
# 1. Create Edda workflow
|
|
41
|
-
workflow_instance
|
|
40
|
+
workflow_instance = cast(Workflow, workflow(func, event_handler=False))
|
|
42
41
|
workflow_name = func.__name__
|
|
43
42
|
|
|
44
43
|
# Register in server's workflow registry
|
|
@@ -56,7 +55,7 @@ def create_durable_tool(
|
|
|
56
55
|
]
|
|
57
56
|
|
|
58
57
|
# Create the tool function
|
|
59
|
-
async def start_tool(**kwargs: Any) -> dict:
|
|
58
|
+
async def start_tool(**kwargs: Any) -> dict[str, Any]:
|
|
60
59
|
"""
|
|
61
60
|
Start workflow and return instance_id.
|
|
62
61
|
|
|
@@ -94,11 +93,21 @@ def create_durable_tool(
|
|
|
94
93
|
status_tool_name = f"{workflow_name}_status"
|
|
95
94
|
status_tool_description = f"Check status of {workflow_name} workflow"
|
|
96
95
|
|
|
97
|
-
@server._mcp.tool(name=status_tool_name, description=status_tool_description)
|
|
98
|
-
async def status_tool(instance_id: str) -> dict:
|
|
96
|
+
@server._mcp.tool(name=status_tool_name, description=status_tool_description) # type: ignore[misc]
|
|
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)
|
|
101
|
+
if instance is None:
|
|
102
|
+
return {
|
|
103
|
+
"content": [
|
|
104
|
+
{
|
|
105
|
+
"type": "text",
|
|
106
|
+
"text": f"Workflow instance not found: {instance_id}",
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"isError": True,
|
|
110
|
+
}
|
|
102
111
|
|
|
103
112
|
status = instance["status"]
|
|
104
113
|
current_activity_id = instance.get("current_activity_id", "N/A")
|
|
@@ -128,11 +137,21 @@ def create_durable_tool(
|
|
|
128
137
|
result_tool_name = f"{workflow_name}_result"
|
|
129
138
|
result_tool_description = f"Get result of {workflow_name} workflow (if completed)"
|
|
130
139
|
|
|
131
|
-
@server._mcp.tool(name=result_tool_name, description=result_tool_description)
|
|
132
|
-
async def result_tool(instance_id: str) -> dict:
|
|
140
|
+
@server._mcp.tool(name=result_tool_name, description=result_tool_description) # type: ignore[misc]
|
|
141
|
+
async def result_tool(instance_id: str) -> dict[str, Any]:
|
|
133
142
|
"""Get workflow result (if completed)."""
|
|
134
143
|
try:
|
|
135
|
-
instance = await server.
|
|
144
|
+
instance = await server.storage.get_instance(instance_id)
|
|
145
|
+
if instance is None:
|
|
146
|
+
return {
|
|
147
|
+
"content": [
|
|
148
|
+
{
|
|
149
|
+
"type": "text",
|
|
150
|
+
"text": f"Workflow instance not found: {instance_id}",
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"isError": True,
|
|
154
|
+
}
|
|
136
155
|
|
|
137
156
|
status = instance["status"]
|
|
138
157
|
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
-
from typing import Any
|
|
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
|
-
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
|
|
13
16
|
except ImportError as e:
|
|
14
17
|
raise ImportError(
|
|
15
18
|
"MCP Python SDK is required for MCP integration. "
|
|
@@ -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)
|
|
@@ -89,7 +98,7 @@ class EddaMCPServer:
|
|
|
89
98
|
service_name=name,
|
|
90
99
|
db_url=db_url,
|
|
91
100
|
outbox_enabled=outbox_enabled,
|
|
92
|
-
broker_url=broker_url,
|
|
101
|
+
broker_url=broker_url or "",
|
|
93
102
|
)
|
|
94
103
|
self._mcp = FastMCP(name, json_response=True, stateless_http=True)
|
|
95
104
|
self._token_verifier = token_verifier
|
|
@@ -97,12 +106,28 @@ 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
|
-
func: Callable | None = None,
|
|
127
|
+
func: Callable[..., Any] | None = None,
|
|
103
128
|
*,
|
|
104
129
|
description: str = "",
|
|
105
|
-
) -> Callable:
|
|
130
|
+
) -> Callable[..., Any]:
|
|
106
131
|
"""
|
|
107
132
|
Decorator to define a durable workflow tool.
|
|
108
133
|
|
|
@@ -128,14 +153,67 @@ class EddaMCPServer:
|
|
|
128
153
|
"""
|
|
129
154
|
from edda.integrations.mcp.decorators import create_durable_tool
|
|
130
155
|
|
|
131
|
-
def decorator(f: Callable) -> Workflow:
|
|
156
|
+
def decorator(f: Callable[..., Any]) -> Workflow:
|
|
132
157
|
return create_durable_tool(self, f, description=description)
|
|
133
158
|
|
|
134
159
|
if func is None:
|
|
135
160
|
return decorator
|
|
136
161
|
return decorator(func)
|
|
137
162
|
|
|
138
|
-
def
|
|
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
|
+
|
|
216
|
+
def asgi_app(self) -> Callable[..., Any]:
|
|
139
217
|
"""
|
|
140
218
|
Create ASGI application with MCP + CloudEvents support.
|
|
141
219
|
|
|
@@ -150,8 +228,8 @@ class EddaMCPServer:
|
|
|
150
228
|
Returns:
|
|
151
229
|
ASGI callable (Starlette app)
|
|
152
230
|
"""
|
|
153
|
-
from starlette.requests import Request
|
|
154
|
-
from starlette.responses import Response
|
|
231
|
+
from starlette.requests import Request # type: ignore[import-not-found]
|
|
232
|
+
from starlette.responses import Response # type: ignore[import-not-found]
|
|
155
233
|
|
|
156
234
|
# Get MCP's Starlette app (Issue #1367 workaround: use directly)
|
|
157
235
|
app = self._mcp.streamable_http_app()
|
|
@@ -169,9 +247,9 @@ class EddaMCPServer:
|
|
|
169
247
|
scope["path"] = f"/cancel/{instance_id}"
|
|
170
248
|
|
|
171
249
|
# Capture response
|
|
172
|
-
response_data = {"status": 200, "headers": [], "body": b""}
|
|
250
|
+
response_data: dict[str, Any] = {"status": 200, "headers": [], "body": b""}
|
|
173
251
|
|
|
174
|
-
async def send(message: dict) -> None:
|
|
252
|
+
async def send(message: dict[str, Any]) -> None:
|
|
175
253
|
if message["type"] == "http.response.start":
|
|
176
254
|
response_data["status"] = message["status"]
|
|
177
255
|
response_data["headers"] = message.get("headers", [])
|
|
@@ -185,7 +263,7 @@ class EddaMCPServer:
|
|
|
185
263
|
return Response(
|
|
186
264
|
content=response_data["body"],
|
|
187
265
|
status_code=response_data["status"],
|
|
188
|
-
headers=dict(response_data["headers"]),
|
|
266
|
+
headers=cast(dict[str, str], dict(response_data["headers"])),
|
|
189
267
|
)
|
|
190
268
|
|
|
191
269
|
# Add cancel route
|
|
@@ -193,14 +271,18 @@ class EddaMCPServer:
|
|
|
193
271
|
|
|
194
272
|
# Add authentication middleware if token_verifier provided (AFTER adding routes)
|
|
195
273
|
if self._token_verifier is not None:
|
|
196
|
-
from starlette.middleware.base import
|
|
274
|
+
from starlette.middleware.base import ( # type: ignore[import-not-found]
|
|
275
|
+
BaseHTTPMiddleware,
|
|
276
|
+
)
|
|
197
277
|
|
|
198
|
-
class AuthMiddleware(BaseHTTPMiddleware):
|
|
199
|
-
def __init__(self, app: Any, token_verifier: Callable):
|
|
278
|
+
class AuthMiddleware(BaseHTTPMiddleware): # type: ignore[misc]
|
|
279
|
+
def __init__(self, app: Any, token_verifier: Callable[[str], bool]):
|
|
200
280
|
super().__init__(app)
|
|
201
281
|
self.token_verifier = token_verifier
|
|
202
282
|
|
|
203
|
-
async def dispatch(
|
|
283
|
+
async def dispatch(
|
|
284
|
+
self, request: Request, call_next: Callable[..., Any]
|
|
285
|
+
) -> Response:
|
|
204
286
|
auth_header = request.headers.get("authorization", "")
|
|
205
287
|
if auth_header.startswith("Bearer "):
|
|
206
288
|
token = auth_header[7:]
|
|
@@ -211,17 +293,15 @@ class EddaMCPServer:
|
|
|
211
293
|
# Wrap app with auth middleware
|
|
212
294
|
app = AuthMiddleware(app, self._token_verifier)
|
|
213
295
|
|
|
214
|
-
return app
|
|
296
|
+
return cast(Callable[..., Any], app)
|
|
215
297
|
|
|
216
298
|
async def initialize(self) -> None:
|
|
217
299
|
"""
|
|
218
300
|
Initialize the EddaApp (setup replay engine, storage, etc.).
|
|
219
301
|
|
|
220
|
-
This method must be called before running the server in stdio mode.
|
|
221
|
-
For HTTP mode (asgi_app()), initialization happens automatically
|
|
222
|
-
when the ASGI app is deployed.
|
|
302
|
+
This method must be called before running the server in either stdio or HTTP mode.
|
|
223
303
|
|
|
224
|
-
Example:
|
|
304
|
+
Example (stdio mode):
|
|
225
305
|
```python
|
|
226
306
|
async def main():
|
|
227
307
|
await server.initialize()
|
|
@@ -231,9 +311,85 @@ class EddaMCPServer:
|
|
|
231
311
|
import asyncio
|
|
232
312
|
asyncio.run(main())
|
|
233
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
|
+
```
|
|
234
326
|
"""
|
|
235
327
|
await self._edda_app.initialize()
|
|
236
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
|
+
|
|
237
393
|
async def run_stdio(self) -> None:
|
|
238
394
|
"""
|
|
239
395
|
Run MCP server with stdio transport (for MCP clients, e.g., Claude Desktop).
|