temporal-mcp-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- temporal_mcp/__init__.py +3 -0
- temporal_mcp/__main__.py +43 -0
- temporal_mcp/client.py +125 -0
- temporal_mcp/handlers/__init__.py +1 -0
- temporal_mcp/handlers/batch_handlers.py +250 -0
- temporal_mcp/handlers/query_handlers.py +88 -0
- temporal_mcp/handlers/schedule_handlers.py +211 -0
- temporal_mcp/handlers/workflow_handlers.py +263 -0
- temporal_mcp/server.py +134 -0
- temporal_mcp/tools/__init__.py +1 -0
- temporal_mcp/tools/tool_definitions.py +385 -0
- temporal_mcp/utils/__init__.py +1 -0
- temporal_mcp/utils/exceptions.py +168 -0
- temporal_mcp_server-0.1.0.dist-info/METADATA +192 -0
- temporal_mcp_server-0.1.0.dist-info/RECORD +19 -0
- temporal_mcp_server-0.1.0.dist-info/WHEEL +5 -0
- temporal_mcp_server-0.1.0.dist-info/entry_points.txt +2 -0
- temporal_mcp_server-0.1.0.dist-info/licenses/LICENSE +201 -0
- temporal_mcp_server-0.1.0.dist-info/top_level.txt +1 -0
temporal_mcp/__init__.py
ADDED
temporal_mcp/__main__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Entry point for Temporal MCP Server.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- `python -m temporal_mcp`
|
|
5
|
+
- `temporal-mcp-server` CLI (installed via pip)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from temporal_mcp.server import TemporalMCPServer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""Main entry point for the MCP server."""
|
|
17
|
+
temporal_host = os.environ.get("TEMPORAL_HOST", "localhost:7233")
|
|
18
|
+
namespace = os.environ.get("TEMPORAL_NAMESPACE", "default")
|
|
19
|
+
|
|
20
|
+
# Parse TLS setting: None (auto-detect), True (force enable), False (force disable)
|
|
21
|
+
tls_env = os.environ.get("TEMPORAL_TLS_ENABLED", "").lower()
|
|
22
|
+
if tls_env == "true":
|
|
23
|
+
tls_enabled = True
|
|
24
|
+
elif tls_env == "false":
|
|
25
|
+
tls_enabled = False
|
|
26
|
+
else:
|
|
27
|
+
tls_enabled = None # Auto-detect
|
|
28
|
+
|
|
29
|
+
print(
|
|
30
|
+
f"Starting MCP server with TEMPORAL_HOST={temporal_host}, TLS={tls_enabled}",
|
|
31
|
+
file=sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
server = TemporalMCPServer(
|
|
35
|
+
temporal_host=temporal_host,
|
|
36
|
+
namespace=namespace,
|
|
37
|
+
tls_enabled=tls_enabled,
|
|
38
|
+
)
|
|
39
|
+
asyncio.run(server.run())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
temporal_mcp/client.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Temporal client management and connection handling."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from temporalio.client import Client, TLSConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TemporalClientManager:
|
|
10
|
+
"""Manages connection to Temporal server."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
temporal_host: str = "localhost:7233",
|
|
15
|
+
namespace: str = "default",
|
|
16
|
+
tls_enabled: Optional[bool] = None
|
|
17
|
+
):
|
|
18
|
+
"""Initialize the Temporal client manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
temporal_host: The Temporal server host and port
|
|
22
|
+
namespace: The Temporal namespace to use
|
|
23
|
+
tls_enabled: Whether to use TLS for connection (None = auto-detect, True = force enable, False = force disable)
|
|
24
|
+
"""
|
|
25
|
+
self.temporal_host = temporal_host
|
|
26
|
+
self.namespace = namespace
|
|
27
|
+
self.tls_enabled = tls_enabled
|
|
28
|
+
self.client: Optional[Client] = None
|
|
29
|
+
|
|
30
|
+
async def connect(self) -> Client:
|
|
31
|
+
"""Connect to Temporal server.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Connected Temporal client
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
Exception: If connection fails
|
|
38
|
+
"""
|
|
39
|
+
if not self.client:
|
|
40
|
+
tls_config = self._determine_tls_config()
|
|
41
|
+
|
|
42
|
+
self._log_connection_info(tls_config)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
self.client = await Client.connect(
|
|
46
|
+
self.temporal_host,
|
|
47
|
+
namespace=self.namespace,
|
|
48
|
+
tls=tls_config,
|
|
49
|
+
)
|
|
50
|
+
print(f"Successfully connected to Temporal at {self.temporal_host}", file=sys.stderr)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Failed to connect to Temporal at {self.temporal_host}: {type(e).__name__}: {e}", file=sys.stderr)
|
|
53
|
+
import traceback
|
|
54
|
+
traceback.print_exc(file=sys.stderr)
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
return self.client
|
|
58
|
+
|
|
59
|
+
async def disconnect(self):
|
|
60
|
+
"""Disconnect from Temporal server."""
|
|
61
|
+
if self.client:
|
|
62
|
+
try:
|
|
63
|
+
await self.client.close()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"Error closing Temporal client: {e}", file=sys.stderr)
|
|
66
|
+
finally:
|
|
67
|
+
self.client = None
|
|
68
|
+
|
|
69
|
+
def ensure_connected(self) -> Client:
|
|
70
|
+
"""Ensure client is connected.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The connected client
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
RuntimeError: If not connected
|
|
77
|
+
"""
|
|
78
|
+
if not self.client:
|
|
79
|
+
raise RuntimeError("Not connected to Temporal server. Connection may have failed or been lost.")
|
|
80
|
+
return self.client
|
|
81
|
+
|
|
82
|
+
def _determine_tls_config(self) -> Optional[TLSConfig]:
|
|
83
|
+
"""Determine TLS configuration based on settings and hostname.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
TLS configuration or None
|
|
87
|
+
"""
|
|
88
|
+
# Priority: explicit tls_enabled setting > auto-detect from hostname
|
|
89
|
+
if self.tls_enabled is True:
|
|
90
|
+
return TLSConfig()
|
|
91
|
+
elif self.tls_enabled is False:
|
|
92
|
+
return None
|
|
93
|
+
elif self._is_remote_host():
|
|
94
|
+
# Auto-detect: enable TLS for remote connections
|
|
95
|
+
return TLSConfig()
|
|
96
|
+
else:
|
|
97
|
+
# Auto-detect: disable TLS for local connections
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _is_remote_host(self) -> bool:
|
|
101
|
+
"""Check if the host is a remote (non-local) host.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if host is remote, False if local
|
|
105
|
+
"""
|
|
106
|
+
local_hosts = ["localhost", "127.0.0.1", "host.docker.internal"]
|
|
107
|
+
return not any(local_host in self.temporal_host for local_host in local_hosts)
|
|
108
|
+
|
|
109
|
+
def _log_connection_info(self, tls_config: Optional[TLSConfig]):
|
|
110
|
+
"""Log connection information for debugging.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
tls_config: The TLS configuration being used
|
|
114
|
+
"""
|
|
115
|
+
if self.tls_enabled is True:
|
|
116
|
+
print(f"Connecting to {self.temporal_host} with TLS enabled (explicit)", file=sys.stderr)
|
|
117
|
+
elif self.tls_enabled is False:
|
|
118
|
+
print(f"Connecting to {self.temporal_host} without TLS (explicit)", file=sys.stderr)
|
|
119
|
+
elif tls_config is not None:
|
|
120
|
+
print(f"Connecting to {self.temporal_host} with TLS enabled (auto-detected for remote host)", file=sys.stderr)
|
|
121
|
+
else:
|
|
122
|
+
print(f"Connecting to {self.temporal_host} without TLS (auto-detected for local host)", file=sys.stderr)
|
|
123
|
+
|
|
124
|
+
print(f"Namespace: {self.namespace}", file=sys.stderr)
|
|
125
|
+
print(f"TLS Enabled: {tls_config is not None}", file=sys.stderr)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Handler modules for Temporal operations."""
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Handlers for batch workflow operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mcp.types import TextContent
|
|
8
|
+
from temporalio.client import Client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def batch_signal(client: Client, args: dict) -> list[TextContent]:
|
|
12
|
+
"""Send signal to multiple workflows.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
client: Connected Temporal client
|
|
16
|
+
args: Arguments containing query, signal_name, optional args, and optional limit
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Batch operation results with success and error counts
|
|
20
|
+
"""
|
|
21
|
+
query = args["query"]
|
|
22
|
+
signal_name = args["signal_name"]
|
|
23
|
+
signal_args = args.get("args")
|
|
24
|
+
limit = args.get("limit", 100)
|
|
25
|
+
|
|
26
|
+
workflows_signaled = []
|
|
27
|
+
errors = []
|
|
28
|
+
|
|
29
|
+
async for workflow in client.list_workflows(query):
|
|
30
|
+
if len(workflows_signaled) + len(errors) >= limit:
|
|
31
|
+
break
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
handle = client.get_workflow_handle(workflow.id)
|
|
35
|
+
await handle.signal(signal_name, signal_args)
|
|
36
|
+
workflows_signaled.append(workflow.id)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
error_detail = {
|
|
39
|
+
"workflow_id": workflow.id,
|
|
40
|
+
"error": str(e),
|
|
41
|
+
"error_type": type(e).__name__
|
|
42
|
+
}
|
|
43
|
+
errors.append(error_detail)
|
|
44
|
+
print(f"Error signaling workflow {workflow.id}: {e}", file=sys.stderr)
|
|
45
|
+
|
|
46
|
+
result = {
|
|
47
|
+
"signal_name": signal_name,
|
|
48
|
+
"workflows_signaled": workflows_signaled,
|
|
49
|
+
"success_count": len(workflows_signaled),
|
|
50
|
+
"error_count": len(errors)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if errors:
|
|
54
|
+
result["errors"] = errors
|
|
55
|
+
|
|
56
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def batch_cancel(client: Client, args: dict) -> list[TextContent]:
|
|
60
|
+
"""Cancel multiple workflows with concurrent processing.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
client: Connected Temporal client
|
|
64
|
+
args: Arguments containing query and optional limit, concurrency
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Batch operation results with success and error counts
|
|
68
|
+
"""
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
query = args["query"]
|
|
72
|
+
limit = args.get("limit", 100)
|
|
73
|
+
concurrency = args.get("concurrency", 50) # Process 50 workflows concurrently
|
|
74
|
+
|
|
75
|
+
print(f"Starting batch cancel with limit={limit}, concurrency={concurrency}", file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
workflows_cancelled = []
|
|
78
|
+
errors = []
|
|
79
|
+
|
|
80
|
+
async def cancel_workflow(workflow_id: str) -> tuple[str, Exception | None]:
|
|
81
|
+
"""Cancel a single workflow and return result."""
|
|
82
|
+
try:
|
|
83
|
+
handle = client.get_workflow_handle(workflow_id)
|
|
84
|
+
await handle.cancel()
|
|
85
|
+
return workflow_id, None
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return workflow_id, e
|
|
88
|
+
|
|
89
|
+
# Collect workflows to cancel
|
|
90
|
+
workflows_to_cancel = []
|
|
91
|
+
async for workflow in client.list_workflows(query):
|
|
92
|
+
workflows_to_cancel.append(workflow.id)
|
|
93
|
+
if len(workflows_to_cancel) >= limit:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
total = len(workflows_to_cancel)
|
|
97
|
+
print(f"Found {total} workflows to cancel. Starting cancellation...", file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
# Process in batches for concurrency
|
|
100
|
+
for i in range(0, len(workflows_to_cancel), concurrency):
|
|
101
|
+
batch = workflows_to_cancel[i:i + concurrency]
|
|
102
|
+
batch_num = i // concurrency + 1
|
|
103
|
+
total_batches = (len(workflows_to_cancel) + concurrency - 1) // concurrency
|
|
104
|
+
|
|
105
|
+
print(f"Processing batch {batch_num}/{total_batches} ({len(batch)} workflows)...", file=sys.stderr)
|
|
106
|
+
|
|
107
|
+
# Cancel workflows concurrently in this batch
|
|
108
|
+
results = await asyncio.gather(
|
|
109
|
+
*[cancel_workflow(wf_id) for wf_id in batch],
|
|
110
|
+
return_exceptions=False
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Process results
|
|
114
|
+
for workflow_id, error in results:
|
|
115
|
+
if error is None:
|
|
116
|
+
workflows_cancelled.append(workflow_id)
|
|
117
|
+
else:
|
|
118
|
+
error_detail = {
|
|
119
|
+
"workflow_id": workflow_id,
|
|
120
|
+
"error": str(error),
|
|
121
|
+
"error_type": type(error).__name__
|
|
122
|
+
}
|
|
123
|
+
errors.append(error_detail)
|
|
124
|
+
print(f"Error cancelling workflow {workflow_id}: {error}", file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
print(f"Batch {batch_num}/{total_batches} complete. Total cancelled: {len(workflows_cancelled)}, errors: {len(errors)}", file=sys.stderr)
|
|
127
|
+
|
|
128
|
+
print(f"Batch cancel complete! Cancelled: {len(workflows_cancelled)}, Errors: {len(errors)}", file=sys.stderr)
|
|
129
|
+
|
|
130
|
+
# Return only summary to avoid context overflow - do NOT include full list of IDs
|
|
131
|
+
result = {
|
|
132
|
+
"success_count": len(workflows_cancelled),
|
|
133
|
+
"error_count": len(errors),
|
|
134
|
+
"total_processed": len(workflows_cancelled) + len(errors),
|
|
135
|
+
"message": f"Successfully cancelled {len(workflows_cancelled)} workflows."
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Include first and last few IDs as samples only
|
|
139
|
+
if len(workflows_cancelled) > 0:
|
|
140
|
+
if len(workflows_cancelled) <= 10:
|
|
141
|
+
result["cancelled_workflows"] = workflows_cancelled
|
|
142
|
+
else:
|
|
143
|
+
result["sample_first"] = workflows_cancelled[:5]
|
|
144
|
+
result["sample_last"] = workflows_cancelled[-5:]
|
|
145
|
+
result["note"] = f"Showing first 5 and last 5 of {len(workflows_cancelled)} cancelled workflows to avoid context overflow"
|
|
146
|
+
|
|
147
|
+
if errors:
|
|
148
|
+
result["sample_errors"] = errors[:5] # Only show first 5 errors
|
|
149
|
+
if len(errors) > 5:
|
|
150
|
+
result["errors_note"] = f"Showing first 5 of {len(errors)} errors"
|
|
151
|
+
|
|
152
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def batch_terminate(client: Client, args: dict) -> list[TextContent]:
|
|
156
|
+
"""Terminate multiple workflows with concurrent processing.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
client: Connected Temporal client
|
|
160
|
+
args: Arguments containing query, optional reason, limit, and concurrency
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Batch operation results with success and error counts
|
|
164
|
+
"""
|
|
165
|
+
import asyncio
|
|
166
|
+
|
|
167
|
+
query = args["query"]
|
|
168
|
+
reason = args.get("reason", "Batch termination via MCP")
|
|
169
|
+
limit = args.get("limit", 100)
|
|
170
|
+
concurrency = args.get("concurrency", 50) # Process 50 workflows concurrently
|
|
171
|
+
|
|
172
|
+
print(f"Starting batch terminate with limit={limit}, concurrency={concurrency}", file=sys.stderr)
|
|
173
|
+
|
|
174
|
+
workflows_terminated = []
|
|
175
|
+
errors = []
|
|
176
|
+
|
|
177
|
+
async def terminate_workflow(workflow_id: str) -> tuple[str, Exception | None]:
|
|
178
|
+
"""Terminate a single workflow and return result."""
|
|
179
|
+
try:
|
|
180
|
+
handle = client.get_workflow_handle(workflow_id)
|
|
181
|
+
await handle.terminate(reason)
|
|
182
|
+
return workflow_id, None
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return workflow_id, e
|
|
185
|
+
|
|
186
|
+
# Collect workflows to terminate
|
|
187
|
+
workflows_to_terminate = []
|
|
188
|
+
async for workflow in client.list_workflows(query):
|
|
189
|
+
workflows_to_terminate.append(workflow.id)
|
|
190
|
+
if len(workflows_to_terminate) >= limit:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
total = len(workflows_to_terminate)
|
|
194
|
+
print(f"Found {total} workflows to terminate. Starting termination...", file=sys.stderr)
|
|
195
|
+
|
|
196
|
+
# Process in batches for concurrency
|
|
197
|
+
for i in range(0, len(workflows_to_terminate), concurrency):
|
|
198
|
+
batch = workflows_to_terminate[i:i + concurrency]
|
|
199
|
+
batch_num = i // concurrency + 1
|
|
200
|
+
total_batches = (len(workflows_to_terminate) + concurrency - 1) // concurrency
|
|
201
|
+
|
|
202
|
+
print(f"Processing batch {batch_num}/{total_batches} ({len(batch)} workflows)...", file=sys.stderr)
|
|
203
|
+
|
|
204
|
+
# Terminate workflows concurrently in this batch
|
|
205
|
+
results = await asyncio.gather(
|
|
206
|
+
*[terminate_workflow(wf_id) for wf_id in batch],
|
|
207
|
+
return_exceptions=False
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Process results
|
|
211
|
+
for workflow_id, error in results:
|
|
212
|
+
if error is None:
|
|
213
|
+
workflows_terminated.append(workflow_id)
|
|
214
|
+
else:
|
|
215
|
+
error_detail = {
|
|
216
|
+
"workflow_id": workflow_id,
|
|
217
|
+
"error": str(error),
|
|
218
|
+
"error_type": type(error).__name__
|
|
219
|
+
}
|
|
220
|
+
errors.append(error_detail)
|
|
221
|
+
print(f"Error terminating workflow {workflow_id}: {error}", file=sys.stderr)
|
|
222
|
+
|
|
223
|
+
print(f"Batch {batch_num}/{total_batches} complete. Total terminated: {len(workflows_terminated)}, errors: {len(errors)}", file=sys.stderr)
|
|
224
|
+
|
|
225
|
+
print(f"Batch terminate complete! Terminated: {len(workflows_terminated)}, Errors: {len(errors)}", file=sys.stderr)
|
|
226
|
+
|
|
227
|
+
# Return only summary to avoid context overflow - do NOT include full list of IDs
|
|
228
|
+
result = {
|
|
229
|
+
"reason": reason,
|
|
230
|
+
"success_count": len(workflows_terminated),
|
|
231
|
+
"error_count": len(errors),
|
|
232
|
+
"total_processed": len(workflows_terminated) + len(errors),
|
|
233
|
+
"message": f"Successfully terminated {len(workflows_terminated)} workflows."
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Include first and last few IDs as samples only
|
|
237
|
+
if len(workflows_terminated) > 0:
|
|
238
|
+
if len(workflows_terminated) <= 10:
|
|
239
|
+
result["terminated_workflows"] = workflows_terminated
|
|
240
|
+
else:
|
|
241
|
+
result["sample_first"] = workflows_terminated[:5]
|
|
242
|
+
result["sample_last"] = workflows_terminated[-5:]
|
|
243
|
+
result["note"] = f"Showing first 5 and last 5 of {len(workflows_terminated)} terminated workflows to avoid context overflow"
|
|
244
|
+
|
|
245
|
+
if errors:
|
|
246
|
+
result["sample_errors"] = errors[:5] # Only show first 5 errors
|
|
247
|
+
if len(errors) > 5:
|
|
248
|
+
result["errors_note"] = f"Showing first 5 of {len(errors)} errors"
|
|
249
|
+
|
|
250
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Handlers for workflow query and signal operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mcp.types import TextContent
|
|
7
|
+
from temporalio.client import Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def query_workflow(client: Client, args: dict) -> list[TextContent]:
|
|
11
|
+
"""Query a workflow execution.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
client: Connected Temporal client
|
|
15
|
+
args: Arguments containing workflow_id, query_name, and optional args
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Query result
|
|
19
|
+
"""
|
|
20
|
+
workflow_id = args["workflow_id"]
|
|
21
|
+
query_name = args["query_name"]
|
|
22
|
+
query_args = args.get("args")
|
|
23
|
+
|
|
24
|
+
handle = client.get_workflow_handle(workflow_id)
|
|
25
|
+
result = await handle.query(query_name, query_args)
|
|
26
|
+
|
|
27
|
+
return [TextContent(
|
|
28
|
+
type="text",
|
|
29
|
+
text=json.dumps({"query_result": result}, indent=2, default=str)
|
|
30
|
+
)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def signal_workflow(client: Client, args: dict) -> list[TextContent]:
|
|
34
|
+
"""Send a signal to a workflow.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
client: Connected Temporal client
|
|
38
|
+
args: Arguments containing workflow_id, signal_name, and optional args
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Success response
|
|
42
|
+
"""
|
|
43
|
+
workflow_id = args["workflow_id"]
|
|
44
|
+
signal_name = args["signal_name"]
|
|
45
|
+
signal_args = args.get("args")
|
|
46
|
+
|
|
47
|
+
handle = client.get_workflow_handle(workflow_id)
|
|
48
|
+
await handle.signal(signal_name, signal_args)
|
|
49
|
+
|
|
50
|
+
return [TextContent(
|
|
51
|
+
type="text",
|
|
52
|
+
text=json.dumps({
|
|
53
|
+
"status": "signal_sent",
|
|
54
|
+
"workflow_id": workflow_id,
|
|
55
|
+
"signal_name": signal_name
|
|
56
|
+
}, indent=2)
|
|
57
|
+
)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def continue_as_new(client: Client, args: dict) -> list[TextContent]:
|
|
61
|
+
"""Signal a workflow to continue as new.
|
|
62
|
+
|
|
63
|
+
Note: This sends a signal to the workflow. The workflow itself must be
|
|
64
|
+
designed to call workflow.continue_as_new() when it receives this signal.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
client: Connected Temporal client
|
|
68
|
+
args: Arguments containing workflow_id, signal_name, and optional signal_args
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Success response
|
|
72
|
+
"""
|
|
73
|
+
workflow_id = args["workflow_id"]
|
|
74
|
+
signal_name = args["signal_name"]
|
|
75
|
+
signal_args = args.get("signal_args", {})
|
|
76
|
+
|
|
77
|
+
handle = client.get_workflow_handle(workflow_id)
|
|
78
|
+
await handle.signal(signal_name, signal_args)
|
|
79
|
+
|
|
80
|
+
return [TextContent(
|
|
81
|
+
type="text",
|
|
82
|
+
text=json.dumps({
|
|
83
|
+
"status": "signal_sent",
|
|
84
|
+
"workflow_id": workflow_id,
|
|
85
|
+
"signal_name": signal_name,
|
|
86
|
+
"note": "Workflow must implement continue-as-new logic in signal handler"
|
|
87
|
+
}, indent=2)
|
|
88
|
+
)]
|