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.
Files changed (35) hide show
  1. kailash/__init__.py +5 -5
  2. kailash/channels/__init__.py +2 -1
  3. kailash/channels/mcp_channel.py +23 -4
  4. kailash/cli/validate_imports.py +202 -0
  5. kailash/core/resilience/bulkhead.py +15 -5
  6. kailash/core/resilience/circuit_breaker.py +4 -1
  7. kailash/core/resilience/health_monitor.py +312 -84
  8. kailash/edge/migration/edge_migration_service.py +384 -0
  9. kailash/mcp_server/server.py +351 -8
  10. kailash/mcp_server/transports.py +305 -0
  11. kailash/middleware/gateway/event_store.py +1 -0
  12. kailash/nodes/base.py +77 -1
  13. kailash/nodes/code/python.py +44 -3
  14. kailash/nodes/data/async_sql.py +42 -20
  15. kailash/nodes/edge/edge_migration_node.py +16 -12
  16. kailash/nodes/governance.py +410 -0
  17. kailash/nodes/rag/registry.py +1 -1
  18. kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
  19. kailash/nodes/transaction/saga_state_storage.py +2 -1
  20. kailash/nodes/validation.py +8 -8
  21. kailash/runtime/local.py +30 -0
  22. kailash/runtime/validation/__init__.py +7 -15
  23. kailash/runtime/validation/import_validator.py +446 -0
  24. kailash/runtime/validation/suggestion_engine.py +5 -5
  25. kailash/utils/data_paths.py +74 -0
  26. kailash/workflow/builder.py +183 -4
  27. kailash/workflow/mermaid_visualizer.py +3 -1
  28. kailash/workflow/templates.py +6 -6
  29. kailash/workflow/validation.py +134 -3
  30. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/METADATA +19 -17
  31. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/RECORD +35 -30
  32. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/WHEEL +0 -0
  33. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
  34. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
  35. {kailash-0.8.5.dist-info → kailash-0.8.6.dist-info}/top_level.txt +0 -0
@@ -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": "stdio",
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
- func = self.metrics.track_tool(f"resource:{uri}")(func)
1230
+ wrapped_func = self.metrics.track_tool(f"resource:{uri}")(func)
1212
1231
 
1213
- return self._mcp.resource(uri)(func)
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
- func = self.metrics.track_tool(f"prompt:{name}")(func)
1267
+ wrapped_func = self.metrics.track_tool(f"prompt:{name}")(func)
1235
1268
 
1236
- return self._mcp.prompt(name)(func)
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 the FastMCP server
1494
- logger.info("Starting FastMCP server...")
1495
- self._mcp.run()
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: