kailash 0.1.5__py3-none-any.whl → 0.2.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.
Files changed (77) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +2 -0
  21. kailash/nodes/ai/a2a.py +714 -67
  22. kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
  23. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  24. kailash/nodes/ai/llm_agent.py +324 -1
  25. kailash/nodes/ai/self_organizing.py +5 -6
  26. kailash/nodes/base.py +15 -2
  27. kailash/nodes/base_async.py +45 -0
  28. kailash/nodes/base_cycle_aware.py +374 -0
  29. kailash/nodes/base_with_acl.py +338 -0
  30. kailash/nodes/code/python.py +135 -27
  31. kailash/nodes/data/__init__.py +1 -2
  32. kailash/nodes/data/readers.py +16 -6
  33. kailash/nodes/data/sql.py +699 -256
  34. kailash/nodes/data/writers.py +16 -6
  35. kailash/nodes/logic/__init__.py +8 -0
  36. kailash/nodes/logic/convergence.py +642 -0
  37. kailash/nodes/logic/loop.py +153 -0
  38. kailash/nodes/logic/operations.py +187 -27
  39. kailash/nodes/mixins/__init__.py +11 -0
  40. kailash/nodes/mixins/mcp.py +228 -0
  41. kailash/nodes/mixins.py +387 -0
  42. kailash/runtime/__init__.py +2 -1
  43. kailash/runtime/access_controlled.py +458 -0
  44. kailash/runtime/local.py +106 -33
  45. kailash/runtime/parallel_cyclic.py +529 -0
  46. kailash/sdk_exceptions.py +90 -5
  47. kailash/security.py +845 -0
  48. kailash/tracking/manager.py +38 -15
  49. kailash/tracking/models.py +1 -1
  50. kailash/tracking/storage/filesystem.py +30 -2
  51. kailash/utils/__init__.py +8 -0
  52. kailash/workflow/__init__.py +18 -0
  53. kailash/workflow/convergence.py +270 -0
  54. kailash/workflow/cycle_analyzer.py +889 -0
  55. kailash/workflow/cycle_builder.py +579 -0
  56. kailash/workflow/cycle_config.py +725 -0
  57. kailash/workflow/cycle_debugger.py +860 -0
  58. kailash/workflow/cycle_exceptions.py +615 -0
  59. kailash/workflow/cycle_profiler.py +741 -0
  60. kailash/workflow/cycle_state.py +338 -0
  61. kailash/workflow/cyclic_runner.py +985 -0
  62. kailash/workflow/graph.py +500 -39
  63. kailash/workflow/migration.py +809 -0
  64. kailash/workflow/safety.py +365 -0
  65. kailash/workflow/templates.py +763 -0
  66. kailash/workflow/validation.py +751 -0
  67. {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/METADATA +259 -12
  68. kailash-0.2.1.dist-info/RECORD +125 -0
  69. kailash/nodes/mcp/__init__.py +0 -11
  70. kailash/nodes/mcp/client.py +0 -554
  71. kailash/nodes/mcp/resource.py +0 -682
  72. kailash/nodes/mcp/server.py +0 -577
  73. kailash-0.1.5.dist-info/RECORD +0 -88
  74. {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/WHEEL +0 -0
  75. {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/entry_points.txt +0 -0
  76. {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/licenses/LICENSE +0 -0
  77. {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,334 @@
1
+ """
2
+ MCP Client - Clean implementation using official Anthropic MCP SDK.
3
+
4
+ This is NOT a node - it's a utility class used by LLM agents to interact with MCP servers.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ # Will use official MCP SDK when available
12
+ try:
13
+ from mcp import ClientSession, StdioServerParameters
14
+ from mcp.client.stdio import stdio_client
15
+
16
+ MCP_AVAILABLE = True
17
+ except ImportError:
18
+ MCP_AVAILABLE = False
19
+
20
+
21
+ class MCPClient:
22
+ """
23
+ Clean MCP client for connecting to Model Context Protocol servers.
24
+
25
+ This client is used internally by LLMAgentNode and other components
26
+ that need MCP capabilities. It provides a simple interface over the
27
+ official MCP SDK.
28
+
29
+ Examples:
30
+ >>> client = MCPClient()
31
+ >>>
32
+ >>> # Connect to a server
33
+ >>> async with client.connect_stdio("python", ["-m", "my_mcp_server"]) as session:
34
+ ... # Discover available tools
35
+ ... tools = await client.discover_tools(session)
36
+ ...
37
+ ... # Call a tool
38
+ ... result = await client.call_tool(
39
+ ... session,
40
+ ... "search",
41
+ ... {"query": "AI in healthcare"}
42
+ ... )
43
+ """
44
+
45
+ def __init__(self):
46
+ """Initialize MCP client."""
47
+ self.logger = logging.getLogger(__name__)
48
+
49
+ async def connect_stdio(
50
+ self, command: str, args: List[str], env: Optional[Dict[str, str]] = None
51
+ ):
52
+ """
53
+ Connect to an MCP server via stdio transport.
54
+
55
+ Args:
56
+ command: Command to run the server
57
+ args: Arguments for the command
58
+ env: Environment variables
59
+
60
+ Returns:
61
+ Context manager yielding ClientSession
62
+
63
+ Raises:
64
+ ImportError: If MCP SDK is not available
65
+ RuntimeError: If connection fails
66
+ """
67
+ if not MCP_AVAILABLE:
68
+ raise ImportError("MCP SDK not available. Install with: pip install mcp")
69
+
70
+ # Merge environment
71
+ server_env = os.environ.copy()
72
+ if env:
73
+ server_env.update(env)
74
+
75
+ # Create server parameters
76
+ server_params = StdioServerParameters(
77
+ command=command, args=args, env=server_env
78
+ )
79
+
80
+ try:
81
+ # Connect to server
82
+ async with stdio_client(server_params) as (read_stream, write_stream):
83
+ async with ClientSession(read_stream, write_stream) as session:
84
+ # Initialize session
85
+ await session.initialize()
86
+ yield session
87
+ except Exception as e:
88
+ self.logger.error(f"Failed to connect to MCP server: {e}")
89
+ raise RuntimeError(f"MCP connection failed: {e}")
90
+
91
+ async def discover_tools(self, session: "ClientSession") -> List[Dict[str, Any]]:
92
+ """
93
+ Discover available tools from an MCP server.
94
+
95
+ Args:
96
+ session: Active MCP client session
97
+
98
+ Returns:
99
+ List of tool definitions with name, description, and schema
100
+ """
101
+ try:
102
+ result = await session.list_tools()
103
+ tools = []
104
+
105
+ for tool in result.tools:
106
+ tools.append(
107
+ {
108
+ "name": tool.name,
109
+ "description": tool.description,
110
+ "inputSchema": tool.inputSchema,
111
+ }
112
+ )
113
+
114
+ return tools
115
+
116
+ except Exception as e:
117
+ self.logger.error(f"Failed to discover tools: {e}")
118
+ return []
119
+
120
+ async def call_tool(
121
+ self, session: "ClientSession", name: str, arguments: Dict[str, Any]
122
+ ) -> Any:
123
+ """
124
+ Call a tool on the MCP server.
125
+
126
+ Args:
127
+ session: Active MCP client session
128
+ name: Tool name
129
+ arguments: Tool arguments
130
+
131
+ Returns:
132
+ Tool execution result
133
+ """
134
+ try:
135
+ result = await session.call_tool(name=name, arguments=arguments)
136
+
137
+ # Extract content from result
138
+ if hasattr(result, "content"):
139
+ content = []
140
+ for item in result.content:
141
+ if hasattr(item, "text"):
142
+ content.append({"type": "text", "text": item.text})
143
+ else:
144
+ content.append(str(item))
145
+ return content
146
+ else:
147
+ return str(result)
148
+
149
+ except Exception as e:
150
+ self.logger.error(f"Failed to call tool '{name}': {e}")
151
+ raise
152
+
153
+ async def list_resources(self, session: "ClientSession") -> List[Dict[str, Any]]:
154
+ """
155
+ List available resources from an MCP server.
156
+
157
+ Args:
158
+ session: Active MCP client session
159
+
160
+ Returns:
161
+ List of resource definitions
162
+ """
163
+ try:
164
+ result = await session.list_resources()
165
+ resources = []
166
+
167
+ for resource in result.resources:
168
+ resources.append(
169
+ {
170
+ "uri": resource.uri,
171
+ "name": resource.name,
172
+ "description": resource.description,
173
+ "mimeType": resource.mimeType,
174
+ }
175
+ )
176
+
177
+ return resources
178
+
179
+ except Exception as e:
180
+ self.logger.error(f"Failed to list resources: {e}")
181
+ return []
182
+
183
+ async def read_resource(self, session: "ClientSession", uri: str) -> Any:
184
+ """
185
+ Read a specific resource from an MCP server.
186
+
187
+ Args:
188
+ session: Active MCP client session
189
+ uri: Resource URI
190
+
191
+ Returns:
192
+ Resource content
193
+ """
194
+ try:
195
+ result = await session.read_resource(uri=uri)
196
+
197
+ # Extract content
198
+ if hasattr(result, "contents"):
199
+ content = []
200
+ for item in result.contents:
201
+ if hasattr(item, "text"):
202
+ content.append({"type": "text", "text": item.text})
203
+ elif hasattr(item, "blob"):
204
+ content.append({"type": "blob", "data": item.blob})
205
+ else:
206
+ content.append(str(item))
207
+ return content
208
+ else:
209
+ return str(result)
210
+
211
+ except Exception as e:
212
+ self.logger.error(f"Failed to read resource '{uri}': {e}")
213
+ raise
214
+
215
+ async def list_prompts(self, session: "ClientSession") -> List[Dict[str, Any]]:
216
+ """
217
+ List available prompts from an MCP server.
218
+
219
+ Args:
220
+ session: Active MCP client session
221
+
222
+ Returns:
223
+ List of prompt definitions
224
+ """
225
+ try:
226
+ result = await session.list_prompts()
227
+ prompts = []
228
+
229
+ for prompt in result.prompts:
230
+ prompt_dict = {
231
+ "name": prompt.name,
232
+ "description": prompt.description,
233
+ "arguments": [],
234
+ }
235
+
236
+ if hasattr(prompt, "arguments"):
237
+ for arg in prompt.arguments:
238
+ prompt_dict["arguments"].append(
239
+ {
240
+ "name": arg.name,
241
+ "description": arg.description,
242
+ "required": arg.required,
243
+ }
244
+ )
245
+
246
+ prompts.append(prompt_dict)
247
+
248
+ return prompts
249
+
250
+ except Exception as e:
251
+ self.logger.error(f"Failed to list prompts: {e}")
252
+ return []
253
+
254
+ async def get_prompt(
255
+ self, session: "ClientSession", name: str, arguments: Dict[str, Any]
256
+ ) -> Dict[str, Any]:
257
+ """
258
+ Get a prompt from an MCP server.
259
+
260
+ Args:
261
+ session: Active MCP client session
262
+ name: Prompt name
263
+ arguments: Prompt arguments
264
+
265
+ Returns:
266
+ Prompt with messages
267
+ """
268
+ try:
269
+ result = await session.get_prompt(name=name, arguments=arguments)
270
+
271
+ # Extract messages
272
+ messages = []
273
+ if hasattr(result, "messages"):
274
+ for msg in result.messages:
275
+ messages.append(
276
+ {
277
+ "role": msg.role,
278
+ "content": (
279
+ msg.content.text
280
+ if hasattr(msg.content, "text")
281
+ else str(msg.content)
282
+ ),
283
+ }
284
+ )
285
+
286
+ return {"name": name, "messages": messages, "arguments": arguments}
287
+
288
+ except Exception as e:
289
+ self.logger.error(f"Failed to get prompt '{name}': {e}")
290
+ raise
291
+
292
+
293
+ # Convenience functions for LLM agents
294
+ async def discover_and_prepare_tools(
295
+ mcp_servers: List[Union[str, Dict[str, Any]]]
296
+ ) -> List[Dict[str, Any]]:
297
+ """
298
+ Discover tools from multiple MCP servers and prepare them for LLM use.
299
+
300
+ Args:
301
+ mcp_servers: List of server URLs or configurations
302
+
303
+ Returns:
304
+ List of tool definitions ready for LLM function calling
305
+ """
306
+ client = MCPClient()
307
+ all_tools = []
308
+
309
+ for server in mcp_servers:
310
+ try:
311
+ # Parse server configuration
312
+ if isinstance(server, str):
313
+ # Simple URL format - not supported in stdio
314
+ continue
315
+
316
+ if server.get("transport") == "stdio":
317
+ command = server.get("command", "python")
318
+ args = server.get("args", [])
319
+ env = server.get("env", {})
320
+
321
+ async with client.connect_stdio(command, args, env) as session:
322
+ tools = await client.discover_tools(session)
323
+
324
+ # Tag tools with server info
325
+ for tool in tools:
326
+ tool["server"] = server.get("name", "mcp_server")
327
+ tool["server_config"] = server
328
+
329
+ all_tools.extend(tools)
330
+
331
+ except Exception as e:
332
+ logging.warning(f"Failed to discover tools from {server}: {e}")
333
+
334
+ return all_tools
kailash/mcp/server.py ADDED
@@ -0,0 +1,293 @@
1
+ """MCP Server Framework using official Anthropic SDK.
2
+
3
+ This module provides a comprehensive framework for creating MCP servers using
4
+ the official FastMCP framework from Anthropic. Servers run as long-lived
5
+ services that expose tools, resources, and prompts to MCP clients, enabling
6
+ dynamic capability extension for AI workflows.
7
+
8
+ Note:
9
+ This module requires the FastMCP framework to be installed.
10
+ Install with: pip install 'mcp[server]'
11
+
12
+ Examples:
13
+ Basic server with tools:
14
+
15
+ >>> from kailash.mcp.server import MCPServer
16
+ >>> class MyServer(MCPServer):
17
+ ... def setup(self):
18
+ ... @self.add_tool()
19
+ ... def calculate(a: int, b: int) -> int:
20
+ ... return a + b
21
+ >>> server = MyServer("calculator", port=8080)
22
+ >>> server.start()
23
+
24
+ Quick server creation:
25
+
26
+ >>> from kailash.mcp.server import SimpleMCPServer
27
+ >>> server = SimpleMCPServer("my-tools")
28
+ >>> @server.tool()
29
+ ... def search(query: str) -> list:
30
+ ... return [f"Result for {query}"]
31
+ >>> server.start()
32
+ """
33
+
34
+ import logging
35
+ from abc import ABC, abstractmethod
36
+ from typing import Callable
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class MCPServer(ABC):
42
+ """Base class for MCP servers using FastMCP.
43
+
44
+ This provides a framework for creating MCP servers that expose
45
+ tools, resources, and prompts via the Model Context Protocol.
46
+
47
+ Examples:
48
+ Creating a custom server:
49
+
50
+ >>> class MyServer(MCPServer):
51
+ ... def setup(self):
52
+ ... @self.add_tool()
53
+ ... def search(query: str) -> str:
54
+ ... return f"Results for: {query}"
55
+ ... @self.add_resource("data://example")
56
+ ... def get_example():
57
+ ... return "Example data"
58
+ >>> server = MyServer("my-server", port=8080)
59
+ >>> server.start() # Runs until stopped
60
+ """
61
+
62
+ def __init__(self, name: str, port: int = 8080, host: str = "localhost"):
63
+ """Initialize the MCP server.
64
+
65
+ Args:
66
+ name: Name of the server.
67
+ port: Port to listen on (default: 8080).
68
+ host: Host to bind to (default: "localhost").
69
+ """
70
+ self.name = name
71
+ self.port = port
72
+ self.host = host
73
+ self._mcp = None
74
+ self._running = False
75
+
76
+ @abstractmethod
77
+ def setup(self):
78
+ """Setup server tools, resources, and prompts.
79
+
80
+ This method should be implemented by subclasses to define
81
+ the server's capabilities using decorators.
82
+
83
+ Note:
84
+ Use @self.add_tool(), @self.add_resource(uri), and
85
+ @self.add_prompt(name) decorators to register capabilities.
86
+ """
87
+ pass
88
+
89
+ def add_tool(self):
90
+ """Decorator to add a tool to the server.
91
+
92
+ Returns:
93
+ Function decorator for registering tools.
94
+
95
+ Examples:
96
+ >>> @server.add_tool()
97
+ ... def calculate(a: int, b: int) -> int:
98
+ ... '''Add two numbers'''
99
+ ... return a + b
100
+ """
101
+
102
+ def decorator(func: Callable):
103
+ if self._mcp is None:
104
+ self._init_mcp()
105
+
106
+ # Use FastMCP's tool decorator
107
+ return self._mcp.tool()(func)
108
+
109
+ return decorator
110
+
111
+ def add_resource(self, uri: str):
112
+ """Decorator to add a resource to the server.
113
+
114
+ Args:
115
+ uri: URI pattern for the resource (supports wildcards).
116
+
117
+ Returns:
118
+ Function decorator for registering resources.
119
+
120
+ Examples:
121
+ >>> @server.add_resource("file:///data/*")
122
+ ... def get_file(path: str) -> str:
123
+ ... return f"Content of {path}"
124
+ """
125
+
126
+ def decorator(func: Callable):
127
+ if self._mcp is None:
128
+ self._init_mcp()
129
+
130
+ # Use FastMCP's resource decorator
131
+ return self._mcp.resource(uri)(func)
132
+
133
+ return decorator
134
+
135
+ def add_prompt(self, name: str):
136
+ """Decorator to add a prompt template to the server.
137
+
138
+ Args:
139
+ name: Name of the prompt.
140
+
141
+ Returns:
142
+ Function decorator for registering prompts.
143
+
144
+ Examples:
145
+ >>> @server.add_prompt("analyze")
146
+ ... def analyze_prompt(data: str) -> str:
147
+ ... return f"Please analyze the following data: {data}"
148
+ """
149
+
150
+ def decorator(func: Callable):
151
+ if self._mcp is None:
152
+ self._init_mcp()
153
+
154
+ # Use FastMCP's prompt decorator
155
+ return self._mcp.prompt(name)(func)
156
+
157
+ return decorator
158
+
159
+ def _init_mcp(self):
160
+ """Initialize the FastMCP instance."""
161
+ try:
162
+ from mcp.server.fastmcp import FastMCP
163
+
164
+ self._mcp = FastMCP(self.name)
165
+ except ImportError:
166
+ logger.error(
167
+ "FastMCP not available. Install with: pip install 'mcp[server]'"
168
+ )
169
+ raise
170
+
171
+ def start(self):
172
+ """Start the MCP server.
173
+
174
+ This runs the server as a long-lived process until stopped.
175
+
176
+ Raises:
177
+ ImportError: If FastMCP is not available.
178
+ Exception: If server fails to start.
179
+ """
180
+ if self._mcp is None:
181
+ self._init_mcp()
182
+
183
+ # Run setup to register tools/resources
184
+ self.setup()
185
+
186
+ logger.info(f"Starting MCP server '{self.name}' on {self.host}:{self.port}")
187
+ self._running = True
188
+
189
+ try:
190
+ # Run the FastMCP server
191
+ logger.info("Running FastMCP server in stdio mode")
192
+ self._mcp.run()
193
+ except Exception as e:
194
+ logger.error(f"Failed to start server: {e}")
195
+ raise
196
+ finally:
197
+ self._running = False
198
+
199
+ def stop(self):
200
+ """Stop the MCP server."""
201
+ logger.info(f"Stopping MCP server '{self.name}'")
202
+ self._running = False
203
+ # In a real implementation, we'd need to handle graceful shutdown
204
+
205
+
206
+ class SimpleMCPServer(MCPServer):
207
+ """Simple MCP server for basic use cases.
208
+
209
+ This provides an easy way to create MCP servers without subclassing.
210
+
211
+ Examples:
212
+ >>> server = SimpleMCPServer("my-server")
213
+ >>> @server.tool()
214
+ ... def add(a: int, b: int) -> int:
215
+ ... return a + b
216
+ >>> server.start()
217
+ """
218
+
219
+ def __init__(self, name: str, port: int = 8080, host: str = "localhost"):
220
+ """Initialize the simple MCP server.
221
+
222
+ Args:
223
+ name: Name of the server.
224
+ port: Port to listen on (default: 8080).
225
+ host: Host to bind to (default: "localhost").
226
+ """
227
+ super().__init__(name, port, host)
228
+ self._tools = []
229
+ self._resources = []
230
+ self._prompts = []
231
+
232
+ def tool(self):
233
+ """Decorator to add a tool.
234
+
235
+ Returns:
236
+ Function decorator for registering tools.
237
+ """
238
+
239
+ def decorator(func):
240
+ self._tools.append(func)
241
+ return func
242
+
243
+ return decorator
244
+
245
+ def resource(self, uri: str):
246
+ """Decorator to add a resource.
247
+
248
+ Args:
249
+ uri: URI pattern for the resource.
250
+
251
+ Returns:
252
+ Function decorator for registering resources.
253
+ """
254
+
255
+ def decorator(func):
256
+ self._resources.append((uri, func))
257
+ return func
258
+
259
+ return decorator
260
+
261
+ def prompt(self, name: str):
262
+ """Decorator to add a prompt.
263
+
264
+ Args:
265
+ name: Name of the prompt.
266
+
267
+ Returns:
268
+ Function decorator for registering prompts.
269
+ """
270
+
271
+ def decorator(func):
272
+ self._prompts.append((name, func))
273
+ return func
274
+
275
+ return decorator
276
+
277
+ def setup(self):
278
+ """Setup the server with registered components.
279
+
280
+ Registers all tools, resources, and prompts that were decorated
281
+ before calling start().
282
+ """
283
+ # Register all tools
284
+ for tool_func in self._tools:
285
+ self.add_tool()(tool_func)
286
+
287
+ # Register all resources
288
+ for uri, resource_func in self._resources:
289
+ self.add_resource(uri)(resource_func)
290
+
291
+ # Register all prompts
292
+ for name, prompt_func in self._prompts:
293
+ self.add_prompt(name)(prompt_func)