edda-framework 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edda/integrations/__init__.py +1 -0
- edda/integrations/mcp/__init__.py +40 -0
- edda/integrations/mcp/decorators.py +188 -0
- edda/integrations/mcp/server.py +261 -0
- {edda_framework-0.2.0.dist-info → edda_framework-0.3.1.dist-info}/METADATA +47 -1
- {edda_framework-0.2.0.dist-info → edda_framework-0.3.1.dist-info}/RECORD +9 -5
- {edda_framework-0.2.0.dist-info → edda_framework-0.3.1.dist-info}/WHEEL +0 -0
- {edda_framework-0.2.0.dist-info → edda_framework-0.3.1.dist-info}/entry_points.txt +0 -0
- {edda_framework-0.2.0.dist-info → edda_framework-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,188 @@
|
|
|
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, cast
|
|
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[..., Any],
|
|
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 = cast(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[str, Any]:
|
|
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) # type: ignore[misc]
|
|
98
|
+
async def status_tool(instance_id: str) -> dict[str, Any]:
|
|
99
|
+
"""Check workflow status."""
|
|
100
|
+
try:
|
|
101
|
+
instance = await server._edda_app.storage.get_instance(instance_id)
|
|
102
|
+
if instance is None:
|
|
103
|
+
return {
|
|
104
|
+
"content": [
|
|
105
|
+
{
|
|
106
|
+
"type": "text",
|
|
107
|
+
"text": f"Workflow instance not found: {instance_id}",
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"isError": True,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
status = instance["status"]
|
|
114
|
+
current_activity_id = instance.get("current_activity_id", "N/A")
|
|
115
|
+
|
|
116
|
+
status_text = (
|
|
117
|
+
f"Workflow Status: {status}\n"
|
|
118
|
+
f"Current Activity: {current_activity_id}\n"
|
|
119
|
+
f"Instance ID: {instance_id}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"content": [{"type": "text", "text": status_text}],
|
|
124
|
+
"isError": False,
|
|
125
|
+
}
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return {
|
|
128
|
+
"content": [
|
|
129
|
+
{
|
|
130
|
+
"type": "text",
|
|
131
|
+
"text": f"Error checking status: {str(e)}",
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"isError": True,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# 4. Generate result tool
|
|
138
|
+
result_tool_name = f"{workflow_name}_result"
|
|
139
|
+
result_tool_description = f"Get result of {workflow_name} workflow (if completed)"
|
|
140
|
+
|
|
141
|
+
@server._mcp.tool(name=result_tool_name, description=result_tool_description) # type: ignore[misc]
|
|
142
|
+
async def result_tool(instance_id: str) -> dict[str, Any]:
|
|
143
|
+
"""Get workflow result (if completed)."""
|
|
144
|
+
try:
|
|
145
|
+
instance = await server._edda_app.storage.get_instance(instance_id)
|
|
146
|
+
if instance is None:
|
|
147
|
+
return {
|
|
148
|
+
"content": [
|
|
149
|
+
{
|
|
150
|
+
"type": "text",
|
|
151
|
+
"text": f"Workflow instance not found: {instance_id}",
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"isError": True,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
status = instance["status"]
|
|
158
|
+
|
|
159
|
+
if status != "completed":
|
|
160
|
+
return {
|
|
161
|
+
"content": [
|
|
162
|
+
{
|
|
163
|
+
"type": "text",
|
|
164
|
+
"text": f"Workflow not completed yet. Current status: {status}",
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"isError": True,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
output_data = instance.get("output_data")
|
|
171
|
+
result_text = f"Workflow Result:\n{output_data}"
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"content": [{"type": "text", "text": result_text}],
|
|
175
|
+
"isError": False,
|
|
176
|
+
}
|
|
177
|
+
except Exception as e:
|
|
178
|
+
return {
|
|
179
|
+
"content": [
|
|
180
|
+
{
|
|
181
|
+
"type": "text",
|
|
182
|
+
"text": f"Error getting result: {str(e)}",
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"isError": True,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return workflow_instance
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""MCP Server implementation for Edda workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from edda.app import EddaApp
|
|
9
|
+
from edda.workflow import Workflow
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"MCP Python SDK is required for MCP integration. "
|
|
16
|
+
"Install it with: pip install edda-framework[mcp]"
|
|
17
|
+
) from e
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EddaMCPServer:
|
|
21
|
+
"""
|
|
22
|
+
MCP (Model Context Protocol) server for Edda durable workflows.
|
|
23
|
+
|
|
24
|
+
Integrates EddaApp (CloudEvents + Workflows) with FastMCP to provide
|
|
25
|
+
long-running workflow tools via the MCP protocol.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
30
|
+
from edda import WorkflowContext, activity
|
|
31
|
+
|
|
32
|
+
server = EddaMCPServer(
|
|
33
|
+
name="Order Service",
|
|
34
|
+
db_url="postgresql://user:pass@localhost/orders",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@activity
|
|
38
|
+
async def reserve_inventory(ctx, items):
|
|
39
|
+
return {"reserved": True}
|
|
40
|
+
|
|
41
|
+
@server.durable_tool(description="Process order workflow")
|
|
42
|
+
async def process_order(ctx: WorkflowContext, order_id: str):
|
|
43
|
+
await reserve_inventory(ctx, [order_id], activity_id="reserve:1")
|
|
44
|
+
return {"status": "completed"}
|
|
45
|
+
|
|
46
|
+
# Deploy with uvicorn (HTTP transport)
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
import uvicorn
|
|
49
|
+
uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
|
|
50
|
+
|
|
51
|
+
# Or deploy with stdio (for MCP clients, e.g., Claude Desktop)
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
await server.initialize()
|
|
57
|
+
await server.run_stdio()
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The server automatically generates three MCP tools for each @durable_tool:
|
|
63
|
+
- `tool_name`: Start the workflow, returns instance_id
|
|
64
|
+
- `tool_name_status`: Check workflow status
|
|
65
|
+
- `tool_name_result`: Get workflow result (if completed)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
name: str,
|
|
71
|
+
db_url: str,
|
|
72
|
+
*,
|
|
73
|
+
outbox_enabled: bool = False,
|
|
74
|
+
broker_url: str | None = None,
|
|
75
|
+
token_verifier: Callable[[str], bool] | None = None,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Initialize MCP server.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: Service name (shown in MCP client)
|
|
82
|
+
db_url: Database URL for workflow storage
|
|
83
|
+
outbox_enabled: Enable transactional outbox pattern
|
|
84
|
+
broker_url: Message broker URL (if outbox enabled)
|
|
85
|
+
token_verifier: Optional function to verify authentication tokens
|
|
86
|
+
"""
|
|
87
|
+
self._name = name
|
|
88
|
+
self._edda_app = EddaApp(
|
|
89
|
+
service_name=name,
|
|
90
|
+
db_url=db_url,
|
|
91
|
+
outbox_enabled=outbox_enabled,
|
|
92
|
+
broker_url=broker_url or "",
|
|
93
|
+
)
|
|
94
|
+
self._mcp = FastMCP(name, json_response=True, stateless_http=True)
|
|
95
|
+
self._token_verifier = token_verifier
|
|
96
|
+
|
|
97
|
+
# Registry of durable tools (workflow_name -> Workflow instance)
|
|
98
|
+
self._workflows: dict[str, Workflow] = {}
|
|
99
|
+
|
|
100
|
+
def durable_tool(
|
|
101
|
+
self,
|
|
102
|
+
func: Callable[..., Any] | None = None,
|
|
103
|
+
*,
|
|
104
|
+
description: str = "",
|
|
105
|
+
) -> Callable[..., Any]:
|
|
106
|
+
"""
|
|
107
|
+
Decorator to define a durable workflow tool.
|
|
108
|
+
|
|
109
|
+
Automatically generates three MCP tools:
|
|
110
|
+
1. Main tool: Starts the workflow, returns instance_id
|
|
111
|
+
2. Status tool: Checks workflow status
|
|
112
|
+
3. Result tool: Gets workflow result (if completed)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
func: Workflow function (async)
|
|
116
|
+
description: Tool description for MCP clients
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Decorated workflow instance
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
```python
|
|
123
|
+
@server.durable_tool(description="Long-running order processing")
|
|
124
|
+
async def process_order(ctx, order_id: str):
|
|
125
|
+
# Workflow logic
|
|
126
|
+
return {"status": "completed"}
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
from edda.integrations.mcp.decorators import create_durable_tool
|
|
130
|
+
|
|
131
|
+
def decorator(f: Callable[..., Any]) -> Workflow:
|
|
132
|
+
return create_durable_tool(self, f, description=description)
|
|
133
|
+
|
|
134
|
+
if func is None:
|
|
135
|
+
return decorator
|
|
136
|
+
return decorator(func)
|
|
137
|
+
|
|
138
|
+
def asgi_app(self) -> Callable[..., Any]:
|
|
139
|
+
"""
|
|
140
|
+
Create ASGI application with MCP + CloudEvents support.
|
|
141
|
+
|
|
142
|
+
This method uses the Issue #1367 workaround: instead of using Mount,
|
|
143
|
+
we get the MCP's Starlette app directly and add Edda endpoints to it.
|
|
144
|
+
|
|
145
|
+
Routing:
|
|
146
|
+
- POST / -> FastMCP (MCP tools via streamable HTTP)
|
|
147
|
+
- POST /cancel/{instance_id} -> Workflow cancellation
|
|
148
|
+
- Other POST -> CloudEvents
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
ASGI callable (Starlette app)
|
|
152
|
+
"""
|
|
153
|
+
from starlette.requests import Request # type: ignore[import-not-found]
|
|
154
|
+
from starlette.responses import Response # type: ignore[import-not-found]
|
|
155
|
+
|
|
156
|
+
# Get MCP's Starlette app (Issue #1367 workaround: use directly)
|
|
157
|
+
app = self._mcp.streamable_http_app()
|
|
158
|
+
|
|
159
|
+
# Add Edda endpoints to Starlette router BEFORE wrapping with middleware
|
|
160
|
+
# Note: MCP's streamable HTTP is already mounted at "/" by default
|
|
161
|
+
# We add additional routes for Edda's CloudEvents and cancellation
|
|
162
|
+
|
|
163
|
+
async def edda_cancel_handler(request: Request) -> Response:
|
|
164
|
+
"""Handle workflow cancellation."""
|
|
165
|
+
instance_id = request.path_params["instance_id"]
|
|
166
|
+
|
|
167
|
+
# Create ASGI scope for EddaApp
|
|
168
|
+
scope = dict(request.scope)
|
|
169
|
+
scope["path"] = f"/cancel/{instance_id}"
|
|
170
|
+
|
|
171
|
+
# Capture response
|
|
172
|
+
response_data: dict[str, Any] = {"status": 200, "headers": [], "body": b""}
|
|
173
|
+
|
|
174
|
+
async def send(message: dict[str, Any]) -> None:
|
|
175
|
+
if message["type"] == "http.response.start":
|
|
176
|
+
response_data["status"] = message["status"]
|
|
177
|
+
response_data["headers"] = message.get("headers", [])
|
|
178
|
+
elif message["type"] == "http.response.body":
|
|
179
|
+
response_data["body"] += message.get("body", b"")
|
|
180
|
+
|
|
181
|
+
# Forward to EddaApp
|
|
182
|
+
await self._edda_app(scope, request.receive, send)
|
|
183
|
+
|
|
184
|
+
# Return response
|
|
185
|
+
return Response(
|
|
186
|
+
content=response_data["body"],
|
|
187
|
+
status_code=response_data["status"],
|
|
188
|
+
headers=cast(dict[str, str], dict(response_data["headers"])),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Add cancel route
|
|
192
|
+
app.router.add_route("/cancel/{instance_id}", edda_cancel_handler, methods=["POST"])
|
|
193
|
+
|
|
194
|
+
# Add authentication middleware if token_verifier provided (AFTER adding routes)
|
|
195
|
+
if self._token_verifier is not None:
|
|
196
|
+
from starlette.middleware.base import ( # type: ignore[import-not-found]
|
|
197
|
+
BaseHTTPMiddleware,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
class AuthMiddleware(BaseHTTPMiddleware): # type: ignore[misc]
|
|
201
|
+
def __init__(self, app: Any, token_verifier: Callable[[str], bool]):
|
|
202
|
+
super().__init__(app)
|
|
203
|
+
self.token_verifier = token_verifier
|
|
204
|
+
|
|
205
|
+
async def dispatch(
|
|
206
|
+
self, request: Request, call_next: Callable[..., Any]
|
|
207
|
+
) -> Response:
|
|
208
|
+
auth_header = request.headers.get("authorization", "")
|
|
209
|
+
if auth_header.startswith("Bearer "):
|
|
210
|
+
token = auth_header[7:]
|
|
211
|
+
if not self.token_verifier(token):
|
|
212
|
+
return Response("Unauthorized", status_code=401)
|
|
213
|
+
return await call_next(request)
|
|
214
|
+
|
|
215
|
+
# Wrap app with auth middleware
|
|
216
|
+
app = AuthMiddleware(app, self._token_verifier)
|
|
217
|
+
|
|
218
|
+
return cast(Callable[..., Any], app)
|
|
219
|
+
|
|
220
|
+
async def initialize(self) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Initialize the EddaApp (setup replay engine, storage, etc.).
|
|
223
|
+
|
|
224
|
+
This method must be called before running the server in stdio mode.
|
|
225
|
+
For HTTP mode (asgi_app()), initialization happens automatically
|
|
226
|
+
when the ASGI app is deployed.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
```python
|
|
230
|
+
async def main():
|
|
231
|
+
await server.initialize()
|
|
232
|
+
await server.run_stdio()
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
import asyncio
|
|
236
|
+
asyncio.run(main())
|
|
237
|
+
```
|
|
238
|
+
"""
|
|
239
|
+
await self._edda_app.initialize()
|
|
240
|
+
|
|
241
|
+
async def run_stdio(self) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Run MCP server with stdio transport (for MCP clients, e.g., Claude Desktop).
|
|
244
|
+
|
|
245
|
+
This method uses stdin/stdout for JSON-RPC communication.
|
|
246
|
+
stderr can be used for diagnostic messages.
|
|
247
|
+
|
|
248
|
+
The server will block until terminated (Ctrl+C or SIGTERM).
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
```python
|
|
252
|
+
async def main():
|
|
253
|
+
await server.initialize()
|
|
254
|
+
await server.run_stdio()
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
import asyncio
|
|
258
|
+
asyncio.run(main())
|
|
259
|
+
```
|
|
260
|
+
"""
|
|
261
|
+
await self._mcp.run_stdio_async()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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:
|
|
@@ -12,6 +12,10 @@ edda/replay.py,sha256=5RIRd0q2ZrH9iiiy35eOUii2cipYg9dlua56OAXvIk4,32499
|
|
|
12
12
|
edda/retry.py,sha256=t4_E1skrhotA1XWHTLbKi-DOgCMasOUnhI9OT-O_eCE,6843
|
|
13
13
|
edda/workflow.py,sha256=daSppYAzgXkjY_9-HS93Zi7_tPR6srmchxY5YfwgU-4,7239
|
|
14
14
|
edda/wsgi.py,sha256=1pGE5fhHpcsYnDR8S3NEFKWUs5P0JK4roTAzX9BsIj0,2391
|
|
15
|
+
edda/integrations/__init__.py,sha256=F_CaTvlDEbldfOpPKq_U9ve1E573tS6XzqXnOtyHcXI,33
|
|
16
|
+
edda/integrations/mcp/__init__.py,sha256=YK-8m0DIdP-RSqewlIX7xnWU7TD3NioCiW2_aZSgnn8,1232
|
|
17
|
+
edda/integrations/mcp/decorators.py,sha256=MIHDrcP_OelxOXISkX6a561Gl3DcVNT9yRakd4O4COo,6333
|
|
18
|
+
edda/integrations/mcp/server.py,sha256=sSemdl7uR133YgZn34uUWzjJUqnPi13HauzXOrf8Kq8,9175
|
|
15
19
|
edda/outbox/__init__.py,sha256=azXG1rtheJEjOyoWmMsBeR2jp8Bz02R3wDEd5tQnaWA,424
|
|
16
20
|
edda/outbox/relayer.py,sha256=2tnN1aOQ8pKWfwEGIlYwYLLwyOKXBjZ4XZsIr1HjgK4,9454
|
|
17
21
|
edda/outbox/transactional.py,sha256=LFfUjunqRlGibaINi-efGXFFivWGO7v3mhqrqyGW6Nw,3808
|
|
@@ -29,8 +33,8 @@ edda/viewer_ui/data_service.py,sha256=mXV6bL6REa_UKsk8xMGBIFbsbLpIxe91lX3wgn-FOj
|
|
|
29
33
|
edda/visualizer/__init__.py,sha256=DOpDstNhR0VcXAs_eMKxaL30p_0u4PKZ4o2ndnYhiRo,343
|
|
30
34
|
edda/visualizer/ast_analyzer.py,sha256=plmx7C9X_X35xLY80jxOL3ljg3afXxBePRZubqUIkxY,13663
|
|
31
35
|
edda/visualizer/mermaid_generator.py,sha256=XWa2egoOTNDfJEjPcwoxwQmblUqXf7YInWFjFRI1QGo,12457
|
|
32
|
-
edda_framework-0.
|
|
33
|
-
edda_framework-0.
|
|
34
|
-
edda_framework-0.
|
|
35
|
-
edda_framework-0.
|
|
36
|
-
edda_framework-0.
|
|
36
|
+
edda_framework-0.3.1.dist-info/METADATA,sha256=uOiXJuq8Xy4fgfyblF3FwbJpo-cJrXVWgHhoIjdIi9s,29421
|
|
37
|
+
edda_framework-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
+
edda_framework-0.3.1.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
|
|
39
|
+
edda_framework-0.3.1.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
|
|
40
|
+
edda_framework-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|