kailash 0.6.5__py3-none-any.whl → 0.7.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.
- kailash/__init__.py +35 -4
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +1 -1
- kailash/middleware/core/agent_ui.py +5 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/security.py +16 -1
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +466 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +290 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +291 -12
- kailash/workflow/validation.py +144 -8
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/RECORD +64 -26
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,648 @@
|
|
1
|
+
"""MCP Channel implementation for Model Context Protocol integration."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
8
|
+
|
9
|
+
from .base import (
|
10
|
+
Channel,
|
11
|
+
ChannelConfig,
|
12
|
+
ChannelEvent,
|
13
|
+
ChannelResponse,
|
14
|
+
ChannelStatus,
|
15
|
+
ChannelType,
|
16
|
+
)
|
17
|
+
|
18
|
+
try:
|
19
|
+
from ..middleware.mcp.enhanced_server import MCPServerConfig, MiddlewareMCPServer
|
20
|
+
|
21
|
+
_MCP_AVAILABLE = True
|
22
|
+
except ImportError:
|
23
|
+
_MCP_AVAILABLE = False
|
24
|
+
|
25
|
+
# Create mock classes for when MCP is not available
|
26
|
+
class MiddlewareMCPServer:
|
27
|
+
def __init__(self, *args, **kwargs):
|
28
|
+
raise ImportError("MCP server not available")
|
29
|
+
|
30
|
+
class MCPServerConfig:
|
31
|
+
def __init__(self):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
from ..runtime.local import LocalRuntime
|
36
|
+
from ..workflow import Workflow
|
37
|
+
|
38
|
+
logger = logging.getLogger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class MCPToolRegistration:
|
43
|
+
"""Represents an MCP tool registration."""
|
44
|
+
|
45
|
+
name: str
|
46
|
+
description: str
|
47
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
48
|
+
handler: Optional[Callable] = None
|
49
|
+
workflow_name: Optional[str] = None
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
51
|
+
|
52
|
+
|
53
|
+
class MCPChannel(Channel):
|
54
|
+
"""Model Context Protocol channel implementation.
|
55
|
+
|
56
|
+
This channel provides MCP server capabilities, allowing external MCP clients
|
57
|
+
to connect and execute workflows through the MCP protocol.
|
58
|
+
"""
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self, config: ChannelConfig, mcp_server: Optional[MiddlewareMCPServer] = None
|
62
|
+
):
|
63
|
+
"""Initialize MCP channel.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
config: Channel configuration
|
67
|
+
mcp_server: Optional existing MCP server, will create one if not provided
|
68
|
+
"""
|
69
|
+
super().__init__(config)
|
70
|
+
|
71
|
+
# Tool and workflow registry (initialize before creating MCP server)
|
72
|
+
self._tool_registry: Dict[str, MCPToolRegistration] = {}
|
73
|
+
self._workflow_registry: Dict[str, Workflow] = {}
|
74
|
+
|
75
|
+
# Create or use provided MCP server
|
76
|
+
if mcp_server:
|
77
|
+
self.mcp_server = mcp_server
|
78
|
+
else:
|
79
|
+
self.mcp_server = self._create_mcp_server()
|
80
|
+
|
81
|
+
# Runtime for executing workflows
|
82
|
+
self.runtime = LocalRuntime()
|
83
|
+
|
84
|
+
# MCP-specific state
|
85
|
+
self._clients: Dict[str, Dict[str, Any]] = {}
|
86
|
+
self._server_task: Optional[asyncio.Task] = None
|
87
|
+
|
88
|
+
logger.info(f"Initialized MCP channel {self.name}")
|
89
|
+
|
90
|
+
def _create_mcp_server(self) -> MiddlewareMCPServer:
|
91
|
+
"""Create a new MCP server with channel configuration."""
|
92
|
+
if not _MCP_AVAILABLE:
|
93
|
+
raise ImportError("MCP server components not available")
|
94
|
+
|
95
|
+
# Extract MCP config from channel config with platform adapter support
|
96
|
+
from kailash.adapters import MCPPlatformAdapter
|
97
|
+
|
98
|
+
mcp_config = MCPServerConfig()
|
99
|
+
|
100
|
+
# Check if we have platform-format configuration
|
101
|
+
platform_config = self.config.extra_config.get("platform_config")
|
102
|
+
if platform_config and isinstance(platform_config, dict):
|
103
|
+
# Translate platform configuration to SDK format
|
104
|
+
try:
|
105
|
+
translated_config = MCPPlatformAdapter.translate_server_config(
|
106
|
+
platform_config
|
107
|
+
)
|
108
|
+
mcp_config.name = translated_config.get(
|
109
|
+
"name", f"{self.name}-mcp-server"
|
110
|
+
)
|
111
|
+
mcp_config.description = translated_config.get(
|
112
|
+
"description", f"MCP server for {self.name} channel"
|
113
|
+
)
|
114
|
+
except Exception as e:
|
115
|
+
self.logger.warning(f"Failed to translate platform config: {e}")
|
116
|
+
# Fall back to default configuration
|
117
|
+
mcp_config.name = f"{self.name}-mcp-server"
|
118
|
+
mcp_config.description = f"MCP server for {self.name} channel"
|
119
|
+
else:
|
120
|
+
# Use direct configuration
|
121
|
+
mcp_config.name = self.config.extra_config.get(
|
122
|
+
"server_name", f"{self.name}-mcp-server"
|
123
|
+
)
|
124
|
+
mcp_config.description = self.config.extra_config.get(
|
125
|
+
"description", f"MCP server for {self.name} channel"
|
126
|
+
)
|
127
|
+
|
128
|
+
# MiddlewareMCPServer only accepts config, event_stream, and agent_ui
|
129
|
+
# Host and port are handled by the channel itself, not the MCP server
|
130
|
+
server = MiddlewareMCPServer(config=mcp_config)
|
131
|
+
|
132
|
+
# Set up default tools
|
133
|
+
self._setup_default_tools(server)
|
134
|
+
|
135
|
+
return server
|
136
|
+
|
137
|
+
def _setup_default_tools(self, server: MiddlewareMCPServer) -> None:
|
138
|
+
"""Set up default MCP tools for workflow execution."""
|
139
|
+
|
140
|
+
# Tool: List available workflows
|
141
|
+
self.register_tool(
|
142
|
+
name="list_workflows",
|
143
|
+
description="List all available workflows in this channel",
|
144
|
+
parameters={},
|
145
|
+
handler=self._handle_list_workflows,
|
146
|
+
)
|
147
|
+
|
148
|
+
# Tool: Execute workflow
|
149
|
+
self.register_tool(
|
150
|
+
name="execute_workflow",
|
151
|
+
description="Execute a workflow with given parameters",
|
152
|
+
parameters={
|
153
|
+
"workflow_name": {
|
154
|
+
"type": "string",
|
155
|
+
"description": "Name of the workflow to execute",
|
156
|
+
"required": True,
|
157
|
+
},
|
158
|
+
"inputs": {
|
159
|
+
"type": "object",
|
160
|
+
"description": "Input parameters for the workflow",
|
161
|
+
"required": False,
|
162
|
+
},
|
163
|
+
},
|
164
|
+
handler=self._handle_execute_workflow,
|
165
|
+
)
|
166
|
+
|
167
|
+
# Tool: Get workflow schema
|
168
|
+
self.register_tool(
|
169
|
+
name="get_workflow_schema",
|
170
|
+
description="Get the input/output schema for a workflow",
|
171
|
+
parameters={
|
172
|
+
"workflow_name": {
|
173
|
+
"type": "string",
|
174
|
+
"description": "Name of the workflow",
|
175
|
+
"required": True,
|
176
|
+
}
|
177
|
+
},
|
178
|
+
handler=self._handle_get_workflow_schema,
|
179
|
+
)
|
180
|
+
|
181
|
+
# Tool: Channel status
|
182
|
+
self.register_tool(
|
183
|
+
name="channel_status",
|
184
|
+
description="Get status information about this MCP channel",
|
185
|
+
parameters={
|
186
|
+
"verbose": {
|
187
|
+
"type": "boolean",
|
188
|
+
"description": "Include detailed status information",
|
189
|
+
"required": False,
|
190
|
+
}
|
191
|
+
},
|
192
|
+
handler=self._handle_channel_status,
|
193
|
+
)
|
194
|
+
|
195
|
+
async def start(self) -> None:
|
196
|
+
"""Start the MCP channel server."""
|
197
|
+
if self.status == ChannelStatus.RUNNING:
|
198
|
+
logger.warning(f"MCP channel {self.name} is already running")
|
199
|
+
return
|
200
|
+
|
201
|
+
try:
|
202
|
+
self.status = ChannelStatus.STARTING
|
203
|
+
self._setup_event_queue()
|
204
|
+
|
205
|
+
# Start MCP server
|
206
|
+
await self.mcp_server.start()
|
207
|
+
|
208
|
+
# Start server task for handling connections
|
209
|
+
self._server_task = asyncio.create_task(self._server_loop())
|
210
|
+
|
211
|
+
self.status = ChannelStatus.RUNNING
|
212
|
+
|
213
|
+
# Emit startup event
|
214
|
+
await self.emit_event(
|
215
|
+
ChannelEvent(
|
216
|
+
event_id=f"mcp_startup_{asyncio.get_event_loop().time()}",
|
217
|
+
channel_name=self.name,
|
218
|
+
channel_type=self.channel_type,
|
219
|
+
event_type="channel_started",
|
220
|
+
payload={
|
221
|
+
"host": self.config.host,
|
222
|
+
"port": self.config.port,
|
223
|
+
"tools_count": len(self._tool_registry),
|
224
|
+
},
|
225
|
+
)
|
226
|
+
)
|
227
|
+
|
228
|
+
logger.info(
|
229
|
+
f"MCP channel {self.name} started on {self.config.host}:{self.config.port}"
|
230
|
+
)
|
231
|
+
|
232
|
+
except Exception as e:
|
233
|
+
self.status = ChannelStatus.ERROR
|
234
|
+
logger.error(f"Failed to start MCP channel {self.name}: {e}")
|
235
|
+
raise
|
236
|
+
|
237
|
+
async def stop(self) -> None:
|
238
|
+
"""Stop the MCP channel server."""
|
239
|
+
if self.status == ChannelStatus.STOPPED:
|
240
|
+
return
|
241
|
+
|
242
|
+
try:
|
243
|
+
self.status = ChannelStatus.STOPPING
|
244
|
+
|
245
|
+
# Emit shutdown event
|
246
|
+
await self.emit_event(
|
247
|
+
ChannelEvent(
|
248
|
+
event_id=f"mcp_shutdown_{asyncio.get_event_loop().time()}",
|
249
|
+
channel_name=self.name,
|
250
|
+
channel_type=self.channel_type,
|
251
|
+
event_type="channel_stopping",
|
252
|
+
payload={"active_clients": len(self._clients)},
|
253
|
+
)
|
254
|
+
)
|
255
|
+
|
256
|
+
# Stop server task
|
257
|
+
if self._server_task and not self._server_task.done():
|
258
|
+
self._server_task.cancel()
|
259
|
+
try:
|
260
|
+
await self._server_task
|
261
|
+
except asyncio.CancelledError:
|
262
|
+
pass
|
263
|
+
|
264
|
+
# Stop MCP server
|
265
|
+
if self.mcp_server:
|
266
|
+
await self.mcp_server.stop()
|
267
|
+
|
268
|
+
await self._cleanup()
|
269
|
+
self.status = ChannelStatus.STOPPED
|
270
|
+
|
271
|
+
logger.info(f"MCP channel {self.name} stopped")
|
272
|
+
|
273
|
+
except Exception as e:
|
274
|
+
self.status = ChannelStatus.ERROR
|
275
|
+
logger.error(f"Error stopping MCP channel {self.name}: {e}")
|
276
|
+
raise
|
277
|
+
|
278
|
+
async def handle_request(self, request: Dict[str, Any]) -> ChannelResponse:
|
279
|
+
"""Handle an MCP request.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
request: MCP request data
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
ChannelResponse with MCP execution results
|
286
|
+
"""
|
287
|
+
try:
|
288
|
+
method = request.get("method", "")
|
289
|
+
params = request.get("params", {})
|
290
|
+
request_id = request.get("id", "")
|
291
|
+
|
292
|
+
# Emit request event
|
293
|
+
await self.emit_event(
|
294
|
+
ChannelEvent(
|
295
|
+
event_id=f"mcp_request_{asyncio.get_event_loop().time()}",
|
296
|
+
channel_name=self.name,
|
297
|
+
channel_type=self.channel_type,
|
298
|
+
event_type="mcp_request",
|
299
|
+
payload={
|
300
|
+
"method": method,
|
301
|
+
"params": params,
|
302
|
+
"request_id": request_id,
|
303
|
+
},
|
304
|
+
)
|
305
|
+
)
|
306
|
+
|
307
|
+
# Handle different MCP methods
|
308
|
+
if method == "tools/list":
|
309
|
+
result = await self._handle_tools_list()
|
310
|
+
elif method == "tools/call":
|
311
|
+
result = await self._handle_tools_call(params)
|
312
|
+
elif method == "resources/list":
|
313
|
+
result = await self._handle_resources_list()
|
314
|
+
elif method == "resources/read":
|
315
|
+
result = await self._handle_resources_read(params)
|
316
|
+
else:
|
317
|
+
result = {
|
318
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"}
|
319
|
+
}
|
320
|
+
|
321
|
+
# Emit completion event
|
322
|
+
await self.emit_event(
|
323
|
+
ChannelEvent(
|
324
|
+
event_id=f"mcp_completion_{asyncio.get_event_loop().time()}",
|
325
|
+
channel_name=self.name,
|
326
|
+
channel_type=self.channel_type,
|
327
|
+
event_type="mcp_completed",
|
328
|
+
payload={
|
329
|
+
"method": method,
|
330
|
+
"request_id": request_id,
|
331
|
+
"success": "error" not in result,
|
332
|
+
},
|
333
|
+
)
|
334
|
+
)
|
335
|
+
|
336
|
+
return ChannelResponse(
|
337
|
+
success="error" not in result,
|
338
|
+
data=result,
|
339
|
+
metadata={
|
340
|
+
"channel": self.name,
|
341
|
+
"method": method,
|
342
|
+
"request_id": request_id,
|
343
|
+
},
|
344
|
+
)
|
345
|
+
|
346
|
+
except Exception as e:
|
347
|
+
logger.error(f"Error handling MCP request: {e}")
|
348
|
+
|
349
|
+
# Emit error event
|
350
|
+
await self.emit_event(
|
351
|
+
ChannelEvent(
|
352
|
+
event_id=f"mcp_error_{asyncio.get_event_loop().time()}",
|
353
|
+
channel_name=self.name,
|
354
|
+
channel_type=self.channel_type,
|
355
|
+
event_type="mcp_error",
|
356
|
+
payload={"error": str(e), "request": request},
|
357
|
+
)
|
358
|
+
)
|
359
|
+
|
360
|
+
return ChannelResponse(
|
361
|
+
success=False, error=str(e), metadata={"channel": self.name}
|
362
|
+
)
|
363
|
+
|
364
|
+
async def _server_loop(self) -> None:
|
365
|
+
"""Main server loop for handling MCP connections."""
|
366
|
+
while self.status == ChannelStatus.RUNNING:
|
367
|
+
try:
|
368
|
+
# This would handle MCP protocol connections
|
369
|
+
# For now, we'll just wait and check for shutdown
|
370
|
+
await asyncio.sleep(1)
|
371
|
+
|
372
|
+
except asyncio.CancelledError:
|
373
|
+
break
|
374
|
+
except Exception as e:
|
375
|
+
logger.error(f"Error in MCP server loop: {e}")
|
376
|
+
|
377
|
+
async def _handle_tools_list(self) -> Dict[str, Any]:
|
378
|
+
"""Handle MCP tools/list request."""
|
379
|
+
tools = []
|
380
|
+
|
381
|
+
for tool_name, registration in self._tool_registry.items():
|
382
|
+
tool_def = {
|
383
|
+
"name": tool_name,
|
384
|
+
"description": registration.description,
|
385
|
+
"inputSchema": {
|
386
|
+
"type": "object",
|
387
|
+
"properties": registration.parameters,
|
388
|
+
},
|
389
|
+
}
|
390
|
+
tools.append(tool_def)
|
391
|
+
|
392
|
+
return {"tools": tools}
|
393
|
+
|
394
|
+
async def _handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
395
|
+
"""Handle MCP tools/call request."""
|
396
|
+
tool_name = params.get("name", "")
|
397
|
+
arguments = params.get("arguments", {})
|
398
|
+
|
399
|
+
if tool_name not in self._tool_registry:
|
400
|
+
return {
|
401
|
+
"error": {"code": -32602, "message": f"Tool not found: {tool_name}"}
|
402
|
+
}
|
403
|
+
|
404
|
+
registration = self._tool_registry[tool_name]
|
405
|
+
|
406
|
+
try:
|
407
|
+
# Execute tool handler
|
408
|
+
if registration.handler:
|
409
|
+
if asyncio.iscoroutinefunction(registration.handler):
|
410
|
+
result = await registration.handler(arguments)
|
411
|
+
else:
|
412
|
+
result = registration.handler(arguments)
|
413
|
+
elif registration.workflow_name:
|
414
|
+
# Execute workflow
|
415
|
+
workflow = self._workflow_registry.get(registration.workflow_name)
|
416
|
+
if workflow:
|
417
|
+
results, run_id = await self.runtime.execute_async(
|
418
|
+
workflow, parameters=arguments
|
419
|
+
)
|
420
|
+
result = {
|
421
|
+
"results": results,
|
422
|
+
"run_id": run_id,
|
423
|
+
"workflow": registration.workflow_name,
|
424
|
+
}
|
425
|
+
else:
|
426
|
+
result = {
|
427
|
+
"error": f"Workflow not found: {registration.workflow_name}"
|
428
|
+
}
|
429
|
+
else:
|
430
|
+
result = {"error": "No handler or workflow configured for tool"}
|
431
|
+
|
432
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
433
|
+
|
434
|
+
except Exception as e:
|
435
|
+
logger.error(f"Error executing tool {tool_name}: {e}")
|
436
|
+
return {"error": {"code": -32603, "message": f"Tool execution failed: {e}"}}
|
437
|
+
|
438
|
+
async def _handle_resources_list(self) -> Dict[str, Any]:
|
439
|
+
"""Handle MCP resources/list request."""
|
440
|
+
resources = []
|
441
|
+
|
442
|
+
# Add workflow resources
|
443
|
+
for workflow_name in self._workflow_registry.keys():
|
444
|
+
resource = {
|
445
|
+
"uri": f"workflow://{workflow_name}",
|
446
|
+
"name": f"Workflow: {workflow_name}",
|
447
|
+
"description": f"Execute the {workflow_name} workflow",
|
448
|
+
"mimeType": "application/json",
|
449
|
+
}
|
450
|
+
resources.append(resource)
|
451
|
+
|
452
|
+
return {"resources": resources}
|
453
|
+
|
454
|
+
async def _handle_resources_read(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
455
|
+
"""Handle MCP resources/read request."""
|
456
|
+
uri = params.get("uri", "")
|
457
|
+
|
458
|
+
if uri.startswith("workflow://"):
|
459
|
+
workflow_name = uri[11:] # Remove "workflow://" prefix
|
460
|
+
if workflow_name in self._workflow_registry:
|
461
|
+
workflow = self._workflow_registry[workflow_name]
|
462
|
+
# Return workflow information
|
463
|
+
return {
|
464
|
+
"contents": [
|
465
|
+
{
|
466
|
+
"uri": uri,
|
467
|
+
"mimeType": "application/json",
|
468
|
+
"text": json.dumps(
|
469
|
+
{
|
470
|
+
"name": workflow_name,
|
471
|
+
"description": f"Workflow {workflow_name} definition",
|
472
|
+
"available": True,
|
473
|
+
},
|
474
|
+
indent=2,
|
475
|
+
),
|
476
|
+
}
|
477
|
+
]
|
478
|
+
}
|
479
|
+
|
480
|
+
return {"error": {"code": -32602, "message": f"Resource not found: {uri}"}}
|
481
|
+
|
482
|
+
def register_tool(
|
483
|
+
self,
|
484
|
+
name: str,
|
485
|
+
description: str,
|
486
|
+
parameters: Dict[str, Any],
|
487
|
+
handler: Optional[Callable] = None,
|
488
|
+
workflow_name: Optional[str] = None,
|
489
|
+
metadata: Optional[Dict[str, Any]] = None,
|
490
|
+
) -> None:
|
491
|
+
"""Register an MCP tool.
|
492
|
+
|
493
|
+
Args:
|
494
|
+
name: Tool name
|
495
|
+
description: Tool description
|
496
|
+
parameters: Tool parameters schema
|
497
|
+
handler: Optional handler function
|
498
|
+
workflow_name: Optional workflow to execute
|
499
|
+
metadata: Optional metadata
|
500
|
+
"""
|
501
|
+
registration = MCPToolRegistration(
|
502
|
+
name=name,
|
503
|
+
description=description,
|
504
|
+
parameters=parameters,
|
505
|
+
handler=handler,
|
506
|
+
workflow_name=workflow_name,
|
507
|
+
metadata=metadata or {},
|
508
|
+
)
|
509
|
+
|
510
|
+
self._tool_registry[name] = registration
|
511
|
+
logger.info(f"Registered MCP tool '{name}' with channel {self.name}")
|
512
|
+
|
513
|
+
def register_workflow(self, name: str, workflow: Workflow) -> None:
|
514
|
+
"""Register a workflow with this MCP channel.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
name: Workflow name
|
518
|
+
workflow: Workflow instance
|
519
|
+
"""
|
520
|
+
self._workflow_registry[name] = workflow
|
521
|
+
|
522
|
+
# Auto-register as a tool
|
523
|
+
self.register_tool(
|
524
|
+
name=f"workflow_{name}",
|
525
|
+
description=f"Execute the {name} workflow",
|
526
|
+
parameters={
|
527
|
+
"inputs": {
|
528
|
+
"type": "object",
|
529
|
+
"description": "Input parameters for the workflow",
|
530
|
+
}
|
531
|
+
},
|
532
|
+
workflow_name=name,
|
533
|
+
)
|
534
|
+
|
535
|
+
logger.info(f"Registered workflow '{name}' with MCP channel {self.name}")
|
536
|
+
|
537
|
+
async def _handle_list_workflows(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
538
|
+
"""Handle list_workflows tool."""
|
539
|
+
workflows = []
|
540
|
+
|
541
|
+
for workflow_name in self._workflow_registry.keys():
|
542
|
+
workflows.append(
|
543
|
+
{
|
544
|
+
"name": workflow_name,
|
545
|
+
"available": True,
|
546
|
+
"tool_name": f"workflow_{workflow_name}",
|
547
|
+
}
|
548
|
+
)
|
549
|
+
|
550
|
+
return {"workflows": workflows, "count": len(workflows)}
|
551
|
+
|
552
|
+
async def _handle_execute_workflow(
|
553
|
+
self, arguments: Dict[str, Any]
|
554
|
+
) -> Dict[str, Any]:
|
555
|
+
"""Handle execute_workflow tool."""
|
556
|
+
workflow_name = arguments.get("workflow_name", "")
|
557
|
+
inputs = arguments.get("inputs", {})
|
558
|
+
|
559
|
+
if workflow_name not in self._workflow_registry:
|
560
|
+
return {"error": f"Workflow not found: {workflow_name}"}
|
561
|
+
|
562
|
+
workflow = self._workflow_registry[workflow_name]
|
563
|
+
|
564
|
+
try:
|
565
|
+
results, run_id = await self.runtime.execute_async(
|
566
|
+
workflow, parameters=inputs
|
567
|
+
)
|
568
|
+
return {
|
569
|
+
"success": True,
|
570
|
+
"results": results,
|
571
|
+
"run_id": run_id,
|
572
|
+
"workflow_name": workflow_name,
|
573
|
+
}
|
574
|
+
except Exception as e:
|
575
|
+
return {"success": False, "error": str(e), "workflow_name": workflow_name}
|
576
|
+
|
577
|
+
async def _handle_get_workflow_schema(
|
578
|
+
self, arguments: Dict[str, Any]
|
579
|
+
) -> Dict[str, Any]:
|
580
|
+
"""Handle get_workflow_schema tool."""
|
581
|
+
workflow_name = arguments.get("workflow_name", "")
|
582
|
+
|
583
|
+
if workflow_name not in self._workflow_registry:
|
584
|
+
return {"error": f"Workflow not found: {workflow_name}"}
|
585
|
+
|
586
|
+
# This would extract schema from workflow
|
587
|
+
# For now, return basic information
|
588
|
+
return {
|
589
|
+
"workflow_name": workflow_name,
|
590
|
+
"schema": {
|
591
|
+
"inputs": "object",
|
592
|
+
"outputs": "object",
|
593
|
+
"description": f"Schema for {workflow_name} workflow",
|
594
|
+
},
|
595
|
+
}
|
596
|
+
|
597
|
+
async def _handle_channel_status(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
598
|
+
"""Handle channel_status tool."""
|
599
|
+
verbose = arguments.get("verbose", False)
|
600
|
+
|
601
|
+
status_info = {
|
602
|
+
"channel_name": self.name,
|
603
|
+
"channel_type": self.channel_type.value,
|
604
|
+
"status": self.status.value,
|
605
|
+
"tools_count": len(self._tool_registry),
|
606
|
+
"workflows_count": len(self._workflow_registry),
|
607
|
+
"active_clients": len(self._clients),
|
608
|
+
}
|
609
|
+
|
610
|
+
if verbose:
|
611
|
+
status_info.update(
|
612
|
+
{
|
613
|
+
"tools": list(self._tool_registry.keys()),
|
614
|
+
"workflows": list(self._workflow_registry.keys()),
|
615
|
+
"host": self.config.host,
|
616
|
+
"port": self.config.port,
|
617
|
+
"config": {
|
618
|
+
"enable_sessions": self.config.enable_sessions,
|
619
|
+
"enable_auth": self.config.enable_auth,
|
620
|
+
"enable_event_routing": self.config.enable_event_routing,
|
621
|
+
},
|
622
|
+
}
|
623
|
+
)
|
624
|
+
|
625
|
+
return status_info
|
626
|
+
|
627
|
+
async def health_check(self) -> Dict[str, Any]:
|
628
|
+
"""Perform comprehensive health check."""
|
629
|
+
base_health = await super().health_check()
|
630
|
+
|
631
|
+
# Add MCP-specific health checks
|
632
|
+
mcp_checks = {
|
633
|
+
"mcp_server_running": self.mcp_server is not None,
|
634
|
+
"tools_registered": len(self._tool_registry) > 0,
|
635
|
+
"workflows_available": len(self._workflow_registry) >= 0,
|
636
|
+
"runtime_ready": self.runtime is not None,
|
637
|
+
}
|
638
|
+
|
639
|
+
all_healthy = base_health["healthy"] and all(mcp_checks.values())
|
640
|
+
|
641
|
+
return {
|
642
|
+
**base_health,
|
643
|
+
"healthy": all_healthy,
|
644
|
+
"checks": {**base_health["checks"], **mcp_checks},
|
645
|
+
"tools": len(self._tool_registry),
|
646
|
+
"workflows": len(self._workflow_registry),
|
647
|
+
"clients": len(self._clients),
|
648
|
+
}
|