kailash 0.8.5__py3-none-any.whl → 0.8.6__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 +5 -5
- kailash/channels/__init__.py +2 -1
- kailash/channels/mcp_channel.py +23 -4
- kailash/cli/validate_imports.py +202 -0
- kailash/core/resilience/bulkhead.py +15 -5
- kailash/core/resilience/circuit_breaker.py +4 -1
- kailash/core/resilience/health_monitor.py +312 -84
- kailash/edge/migration/edge_migration_service.py +384 -0
- kailash/mcp_server/server.py +351 -8
- kailash/mcp_server/transports.py +305 -0
- kailash/middleware/gateway/event_store.py +1 -0
- kailash/nodes/base.py +77 -1
- kailash/nodes/code/python.py +44 -3
- kailash/nodes/data/async_sql.py +42 -20
- kailash/nodes/edge/edge_migration_node.py +16 -12
- kailash/nodes/governance.py +410 -0
- kailash/nodes/rag/registry.py +1 -1
- kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
- kailash/nodes/transaction/saga_state_storage.py +2 -1
- kailash/nodes/validation.py +8 -8
- kailash/runtime/local.py +30 -0
- kailash/runtime/validation/__init__.py +7 -15
- kailash/runtime/validation/import_validator.py +446 -0
- kailash/runtime/validation/suggestion_engine.py +5 -5
- kailash/utils/data_paths.py +74 -0
- kailash/workflow/builder.py +183 -4
- kailash/workflow/mermaid_visualizer.py +3 -1
- kailash/workflow/templates.py +6 -6
- kailash/workflow/validation.py +134 -3
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/METADATA +19 -17
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/RECORD +35 -30
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/WHEEL +0 -0
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/top_level.txt +0 -0
kailash/mcp_server/server.py
CHANGED
@@ -345,6 +345,11 @@ class MCPServer:
|
|
345
345
|
self,
|
346
346
|
name: str,
|
347
347
|
config_file: Optional[Union[str, Path]] = None,
|
348
|
+
# Transport configuration
|
349
|
+
transport: str = "stdio", # "stdio", "websocket", "http", "sse"
|
350
|
+
websocket_host: str = "0.0.0.0",
|
351
|
+
websocket_port: int = 3001,
|
352
|
+
# Caching configuration
|
348
353
|
enable_cache: bool = True,
|
349
354
|
cache_ttl: int = 300,
|
350
355
|
cache_backend: str = "memory", # "memory" or "redis"
|
@@ -371,6 +376,9 @@ class MCPServer:
|
|
371
376
|
Args:
|
372
377
|
name: Server name
|
373
378
|
config_file: Optional configuration file path
|
379
|
+
transport: Transport to use ("stdio", "websocket", "http", "sse")
|
380
|
+
websocket_host: Host for WebSocket server (default: "0.0.0.0")
|
381
|
+
websocket_port: Port for WebSocket server (default: 3001)
|
374
382
|
enable_cache: Whether to enable caching (default: True)
|
375
383
|
cache_ttl: Default cache TTL in seconds (default: 300)
|
376
384
|
cache_backend: Cache backend ("memory" or "redis")
|
@@ -391,6 +399,11 @@ class MCPServer:
|
|
391
399
|
"""
|
392
400
|
self.name = name
|
393
401
|
|
402
|
+
# Transport configuration
|
403
|
+
self.transport = transport
|
404
|
+
self.websocket_host = websocket_host
|
405
|
+
self.websocket_port = websocket_port
|
406
|
+
|
394
407
|
# Enhanced features
|
395
408
|
self.auth_provider = auth_provider
|
396
409
|
self.enable_http_transport = enable_http_transport
|
@@ -410,7 +423,9 @@ class MCPServer:
|
|
410
423
|
"server": {
|
411
424
|
"name": name,
|
412
425
|
"version": "1.0.0",
|
413
|
-
"transport":
|
426
|
+
"transport": transport,
|
427
|
+
"websocket_host": websocket_host,
|
428
|
+
"websocket_port": websocket_port,
|
414
429
|
"enable_http": enable_http_transport,
|
415
430
|
"enable_sse": enable_sse_transport,
|
416
431
|
"timeout": transport_timeout,
|
@@ -500,6 +515,9 @@ class MCPServer:
|
|
500
515
|
self._resource_registry: Dict[str, Dict[str, Any]] = {}
|
501
516
|
self._prompt_registry: Dict[str, Dict[str, Any]] = {}
|
502
517
|
|
518
|
+
# Transport instance (for WebSocket and other transports)
|
519
|
+
self._transport = None
|
520
|
+
|
503
521
|
def _init_mcp(self):
|
504
522
|
"""Initialize FastMCP server."""
|
505
523
|
if self._mcp is not None:
|
@@ -1207,10 +1225,24 @@ class MCPServer:
|
|
1207
1225
|
self._init_mcp()
|
1208
1226
|
|
1209
1227
|
# Wrap with metrics if enabled
|
1228
|
+
wrapped_func = func
|
1210
1229
|
if self.metrics.enabled:
|
1211
|
-
|
1230
|
+
wrapped_func = self.metrics.track_tool(f"resource:{uri}")(func)
|
1212
1231
|
|
1213
|
-
|
1232
|
+
# Register with FastMCP
|
1233
|
+
mcp_resource = self._mcp.resource(uri)(wrapped_func)
|
1234
|
+
|
1235
|
+
# Track in registry
|
1236
|
+
self._resource_registry[uri] = {
|
1237
|
+
"handler": mcp_resource,
|
1238
|
+
"original_handler": func,
|
1239
|
+
"name": uri,
|
1240
|
+
"description": func.__doc__ or f"Resource: {uri}",
|
1241
|
+
"mime_type": "text/plain",
|
1242
|
+
"created_at": time.time(),
|
1243
|
+
}
|
1244
|
+
|
1245
|
+
return mcp_resource
|
1214
1246
|
|
1215
1247
|
return decorator
|
1216
1248
|
|
@@ -1230,10 +1262,23 @@ class MCPServer:
|
|
1230
1262
|
self._init_mcp()
|
1231
1263
|
|
1232
1264
|
# Wrap with metrics if enabled
|
1265
|
+
wrapped_func = func
|
1233
1266
|
if self.metrics.enabled:
|
1234
|
-
|
1267
|
+
wrapped_func = self.metrics.track_tool(f"prompt:{name}")(func)
|
1235
1268
|
|
1236
|
-
|
1269
|
+
# Register with FastMCP
|
1270
|
+
mcp_prompt = self._mcp.prompt(name)(wrapped_func)
|
1271
|
+
|
1272
|
+
# Track in registry
|
1273
|
+
self._prompt_registry[name] = {
|
1274
|
+
"handler": mcp_prompt,
|
1275
|
+
"original_handler": func,
|
1276
|
+
"description": func.__doc__ or f"Prompt: {name}",
|
1277
|
+
"arguments": [], # Could be extracted from function signature
|
1278
|
+
"created_at": time.time(),
|
1279
|
+
}
|
1280
|
+
|
1281
|
+
return mcp_prompt
|
1237
1282
|
|
1238
1283
|
return decorator
|
1239
1284
|
|
@@ -1454,6 +1499,47 @@ class MCPServer:
|
|
1454
1499
|
return True
|
1455
1500
|
return False
|
1456
1501
|
|
1502
|
+
def _execute_tool(self, tool_name: str, arguments: dict) -> Any:
|
1503
|
+
"""Execute a tool directly (for testing purposes)."""
|
1504
|
+
if tool_name not in self._tool_registry:
|
1505
|
+
raise ValueError(f"Tool '{tool_name}' not found in registry")
|
1506
|
+
|
1507
|
+
tool_info = self._tool_registry[tool_name]
|
1508
|
+
if tool_info.get("disabled", False):
|
1509
|
+
raise ValueError(f"Tool '{tool_name}' is currently disabled")
|
1510
|
+
|
1511
|
+
# Get the tool handler (the enhanced function)
|
1512
|
+
if "handler" in tool_info:
|
1513
|
+
handler = tool_info["handler"]
|
1514
|
+
elif "function" in tool_info:
|
1515
|
+
handler = tool_info["function"]
|
1516
|
+
else:
|
1517
|
+
raise ValueError(f"Tool '{tool_name}' has no valid handler")
|
1518
|
+
|
1519
|
+
# Update statistics
|
1520
|
+
tool_info["call_count"] = tool_info.get("call_count", 0) + 1
|
1521
|
+
tool_info["last_called"] = time.time()
|
1522
|
+
|
1523
|
+
try:
|
1524
|
+
# Execute the tool
|
1525
|
+
if asyncio.iscoroutinefunction(handler):
|
1526
|
+
# For async functions, we need to run in event loop
|
1527
|
+
try:
|
1528
|
+
loop = asyncio.get_event_loop()
|
1529
|
+
if loop.is_running():
|
1530
|
+
# Already in async context - create task
|
1531
|
+
return asyncio.create_task(handler(**arguments))
|
1532
|
+
else:
|
1533
|
+
return loop.run_until_complete(handler(**arguments))
|
1534
|
+
except RuntimeError:
|
1535
|
+
# No event loop - create new one
|
1536
|
+
return asyncio.run(handler(**arguments))
|
1537
|
+
else:
|
1538
|
+
return handler(**arguments)
|
1539
|
+
except Exception as e:
|
1540
|
+
tool_info["error_count"] = tool_info.get("error_count", 0) + 1
|
1541
|
+
raise
|
1542
|
+
|
1457
1543
|
def run(self):
|
1458
1544
|
"""Run the enhanced MCP server with all features."""
|
1459
1545
|
if self._mcp is None:
|
@@ -1464,6 +1550,7 @@ class MCPServer:
|
|
1464
1550
|
|
1465
1551
|
# Log enhanced server startup
|
1466
1552
|
logger.info(f"Starting enhanced MCP server: {self.name}")
|
1553
|
+
logger.info(f"Transport: {self.transport}")
|
1467
1554
|
logger.info("Features enabled:")
|
1468
1555
|
logger.info(f" - Cache: {self.cache.enabled if self.cache else False}")
|
1469
1556
|
logger.info(f" - Metrics: {self.metrics.enabled if self.metrics else False}")
|
@@ -1490,9 +1577,16 @@ class MCPServer:
|
|
1490
1577
|
if health["status"] != "healthy":
|
1491
1578
|
logger.warning(f"Server health check shows issues: {health['issues']}")
|
1492
1579
|
|
1493
|
-
# Run
|
1494
|
-
|
1495
|
-
|
1580
|
+
# Run server based on transport type
|
1581
|
+
if self.transport == "websocket":
|
1582
|
+
logger.info(
|
1583
|
+
f"Starting WebSocket server on {self.websocket_host}:{self.websocket_port}..."
|
1584
|
+
)
|
1585
|
+
asyncio.run(self._run_websocket())
|
1586
|
+
else:
|
1587
|
+
# Default to FastMCP (STDIO) server
|
1588
|
+
logger.info("Starting FastMCP server in STDIO mode...")
|
1589
|
+
self._mcp.run()
|
1496
1590
|
|
1497
1591
|
except KeyboardInterrupt:
|
1498
1592
|
logger.info("Server stopped by user")
|
@@ -1527,6 +1621,255 @@ class MCPServer:
|
|
1527
1621
|
self._running = False
|
1528
1622
|
logger.info(f"Enhanced MCP server '{self.name}' stopped")
|
1529
1623
|
|
1624
|
+
async def _run_websocket(self):
|
1625
|
+
"""Run the server using WebSocket transport."""
|
1626
|
+
from .transports import WebSocketServerTransport
|
1627
|
+
|
1628
|
+
try:
|
1629
|
+
# Create WebSocket transport
|
1630
|
+
self._transport = WebSocketServerTransport(
|
1631
|
+
host=self.websocket_host,
|
1632
|
+
port=self.websocket_port,
|
1633
|
+
message_handler=self._handle_websocket_message,
|
1634
|
+
auth_provider=self.auth_provider,
|
1635
|
+
timeout=self.transport_timeout,
|
1636
|
+
max_message_size=self.max_request_size,
|
1637
|
+
enable_metrics=self.metrics.enabled if self.metrics else False,
|
1638
|
+
)
|
1639
|
+
|
1640
|
+
# Start WebSocket server
|
1641
|
+
await self._transport.connect()
|
1642
|
+
logger.info(
|
1643
|
+
f"WebSocket server started on {self.websocket_host}:{self.websocket_port}"
|
1644
|
+
)
|
1645
|
+
|
1646
|
+
# Keep server running
|
1647
|
+
try:
|
1648
|
+
await asyncio.Future() # Run forever
|
1649
|
+
except asyncio.CancelledError:
|
1650
|
+
logger.info("WebSocket server cancelled")
|
1651
|
+
|
1652
|
+
finally:
|
1653
|
+
# Clean up
|
1654
|
+
if self._transport:
|
1655
|
+
await self._transport.disconnect()
|
1656
|
+
self._transport = None
|
1657
|
+
|
1658
|
+
async def _handle_websocket_message(
|
1659
|
+
self, request: Dict[str, Any], client_id: str
|
1660
|
+
) -> Dict[str, Any]:
|
1661
|
+
"""Handle incoming WebSocket message."""
|
1662
|
+
try:
|
1663
|
+
method = request.get("method", "")
|
1664
|
+
params = request.get("params", {})
|
1665
|
+
request_id = request.get("id")
|
1666
|
+
|
1667
|
+
# Log request
|
1668
|
+
logger.debug(f"WebSocket request from {client_id}: {method}")
|
1669
|
+
|
1670
|
+
# Route to appropriate handler
|
1671
|
+
if method == "initialize":
|
1672
|
+
return await self._handle_initialize(params, request_id)
|
1673
|
+
elif method == "tools/list":
|
1674
|
+
return await self._handle_list_tools(params, request_id)
|
1675
|
+
elif method == "tools/call":
|
1676
|
+
return await self._handle_call_tool(params, request_id)
|
1677
|
+
elif method == "resources/list":
|
1678
|
+
return await self._handle_list_resources(params, request_id)
|
1679
|
+
elif method == "resources/read":
|
1680
|
+
return await self._handle_read_resource(params, request_id)
|
1681
|
+
elif method == "prompts/list":
|
1682
|
+
return await self._handle_list_prompts(params, request_id)
|
1683
|
+
elif method == "prompts/get":
|
1684
|
+
return await self._handle_get_prompt(params, request_id)
|
1685
|
+
else:
|
1686
|
+
return {
|
1687
|
+
"jsonrpc": "2.0",
|
1688
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
1689
|
+
"id": request_id,
|
1690
|
+
}
|
1691
|
+
|
1692
|
+
except Exception as e:
|
1693
|
+
logger.error(f"Error handling WebSocket message: {e}")
|
1694
|
+
return {
|
1695
|
+
"jsonrpc": "2.0",
|
1696
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"},
|
1697
|
+
"id": request.get("id"),
|
1698
|
+
}
|
1699
|
+
|
1700
|
+
async def _handle_initialize(
|
1701
|
+
self, params: Dict[str, Any], request_id: Any
|
1702
|
+
) -> Dict[str, Any]:
|
1703
|
+
"""Handle initialize request."""
|
1704
|
+
return {
|
1705
|
+
"jsonrpc": "2.0",
|
1706
|
+
"result": {
|
1707
|
+
"protocolVersion": "2024-11-05",
|
1708
|
+
"capabilities": {
|
1709
|
+
"tools": {"listSupported": True, "callSupported": True},
|
1710
|
+
"resources": {"listSupported": True, "readSupported": True},
|
1711
|
+
"prompts": {"listSupported": True, "getSupported": True},
|
1712
|
+
},
|
1713
|
+
"serverInfo": {
|
1714
|
+
"name": self.name,
|
1715
|
+
"version": self.config.get("server.version", "1.0.0"),
|
1716
|
+
},
|
1717
|
+
},
|
1718
|
+
"id": request_id,
|
1719
|
+
}
|
1720
|
+
|
1721
|
+
async def _handle_list_tools(
|
1722
|
+
self, params: Dict[str, Any], request_id: Any
|
1723
|
+
) -> Dict[str, Any]:
|
1724
|
+
"""Handle tools/list request."""
|
1725
|
+
tools = []
|
1726
|
+
for name, info in self._tool_registry.items():
|
1727
|
+
if not info.get("disabled", False):
|
1728
|
+
tools.append(
|
1729
|
+
{
|
1730
|
+
"name": name,
|
1731
|
+
"description": info.get("description", ""),
|
1732
|
+
"inputSchema": info.get("input_schema", {}),
|
1733
|
+
}
|
1734
|
+
)
|
1735
|
+
|
1736
|
+
return {"jsonrpc": "2.0", "result": {"tools": tools}, "id": request_id}
|
1737
|
+
|
1738
|
+
async def _handle_call_tool(
|
1739
|
+
self, params: Dict[str, Any], request_id: Any
|
1740
|
+
) -> Dict[str, Any]:
|
1741
|
+
"""Handle tools/call request."""
|
1742
|
+
tool_name = params.get("name")
|
1743
|
+
arguments = params.get("arguments", {})
|
1744
|
+
|
1745
|
+
try:
|
1746
|
+
result = self._execute_tool(tool_name, arguments)
|
1747
|
+
|
1748
|
+
# Handle async results
|
1749
|
+
if asyncio.iscoroutine(result) or asyncio.isfuture(result):
|
1750
|
+
result = await result
|
1751
|
+
|
1752
|
+
return {
|
1753
|
+
"jsonrpc": "2.0",
|
1754
|
+
"result": {"content": [{"type": "text", "text": str(result)}]},
|
1755
|
+
"id": request_id,
|
1756
|
+
}
|
1757
|
+
except Exception as e:
|
1758
|
+
return {
|
1759
|
+
"jsonrpc": "2.0",
|
1760
|
+
"error": {"code": -32603, "message": f"Tool execution error: {str(e)}"},
|
1761
|
+
"id": request_id,
|
1762
|
+
}
|
1763
|
+
|
1764
|
+
async def _handle_list_resources(
|
1765
|
+
self, params: Dict[str, Any], request_id: Any
|
1766
|
+
) -> Dict[str, Any]:
|
1767
|
+
"""Handle resources/list request."""
|
1768
|
+
resources = []
|
1769
|
+
for uri, info in self._resource_registry.items():
|
1770
|
+
resources.append(
|
1771
|
+
{
|
1772
|
+
"uri": uri,
|
1773
|
+
"name": info.get("name", uri),
|
1774
|
+
"description": info.get("description", ""),
|
1775
|
+
"mimeType": info.get("mime_type", "text/plain"),
|
1776
|
+
}
|
1777
|
+
)
|
1778
|
+
|
1779
|
+
return {"jsonrpc": "2.0", "result": {"resources": resources}, "id": request_id}
|
1780
|
+
|
1781
|
+
async def _handle_read_resource(
|
1782
|
+
self, params: Dict[str, Any], request_id: Any
|
1783
|
+
) -> Dict[str, Any]:
|
1784
|
+
"""Handle resources/read request."""
|
1785
|
+
uri = params.get("uri")
|
1786
|
+
|
1787
|
+
if uri not in self._resource_registry:
|
1788
|
+
return {
|
1789
|
+
"jsonrpc": "2.0",
|
1790
|
+
"error": {"code": -32602, "message": f"Resource not found: {uri}"},
|
1791
|
+
"id": request_id,
|
1792
|
+
}
|
1793
|
+
|
1794
|
+
try:
|
1795
|
+
resource_info = self._resource_registry[uri]
|
1796
|
+
handler = resource_info.get("handler")
|
1797
|
+
|
1798
|
+
if handler:
|
1799
|
+
content = handler()
|
1800
|
+
if asyncio.iscoroutine(content):
|
1801
|
+
content = await content
|
1802
|
+
else:
|
1803
|
+
content = ""
|
1804
|
+
|
1805
|
+
return {
|
1806
|
+
"jsonrpc": "2.0",
|
1807
|
+
"result": {"contents": [{"uri": uri, "text": str(content)}]},
|
1808
|
+
"id": request_id,
|
1809
|
+
}
|
1810
|
+
except Exception as e:
|
1811
|
+
return {
|
1812
|
+
"jsonrpc": "2.0",
|
1813
|
+
"error": {"code": -32603, "message": f"Resource read error: {str(e)}"},
|
1814
|
+
"id": request_id,
|
1815
|
+
}
|
1816
|
+
|
1817
|
+
async def _handle_list_prompts(
|
1818
|
+
self, params: Dict[str, Any], request_id: Any
|
1819
|
+
) -> Dict[str, Any]:
|
1820
|
+
"""Handle prompts/list request."""
|
1821
|
+
prompts = []
|
1822
|
+
for name, info in self._prompt_registry.items():
|
1823
|
+
prompts.append(
|
1824
|
+
{
|
1825
|
+
"name": name,
|
1826
|
+
"description": info.get("description", ""),
|
1827
|
+
"arguments": info.get("arguments", []),
|
1828
|
+
}
|
1829
|
+
)
|
1830
|
+
|
1831
|
+
return {"jsonrpc": "2.0", "result": {"prompts": prompts}, "id": request_id}
|
1832
|
+
|
1833
|
+
async def _handle_get_prompt(
|
1834
|
+
self, params: Dict[str, Any], request_id: Any
|
1835
|
+
) -> Dict[str, Any]:
|
1836
|
+
"""Handle prompts/get request."""
|
1837
|
+
name = params.get("name")
|
1838
|
+
arguments = params.get("arguments", {})
|
1839
|
+
|
1840
|
+
if name not in self._prompt_registry:
|
1841
|
+
return {
|
1842
|
+
"jsonrpc": "2.0",
|
1843
|
+
"error": {"code": -32602, "message": f"Prompt not found: {name}"},
|
1844
|
+
"id": request_id,
|
1845
|
+
}
|
1846
|
+
|
1847
|
+
try:
|
1848
|
+
prompt_info = self._prompt_registry[name]
|
1849
|
+
handler = prompt_info.get("handler")
|
1850
|
+
|
1851
|
+
if handler:
|
1852
|
+
messages = handler(**arguments)
|
1853
|
+
if asyncio.iscoroutine(messages):
|
1854
|
+
messages = await messages
|
1855
|
+
else:
|
1856
|
+
messages = []
|
1857
|
+
|
1858
|
+
return {
|
1859
|
+
"jsonrpc": "2.0",
|
1860
|
+
"result": {"messages": messages},
|
1861
|
+
"id": request_id,
|
1862
|
+
}
|
1863
|
+
except Exception as e:
|
1864
|
+
return {
|
1865
|
+
"jsonrpc": "2.0",
|
1866
|
+
"error": {
|
1867
|
+
"code": -32603,
|
1868
|
+
"message": f"Prompt generation error: {str(e)}",
|
1869
|
+
},
|
1870
|
+
"id": request_id,
|
1871
|
+
}
|
1872
|
+
|
1530
1873
|
async def run_stdio(self):
|
1531
1874
|
"""Run the server using stdio transport for testing."""
|
1532
1875
|
if self._mcp is None:
|