kailash 0.1.5__py3-none-any.whl → 0.2.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.
Files changed (75) 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/readers.py +16 -6
  32. kailash/nodes/data/writers.py +16 -6
  33. kailash/nodes/logic/__init__.py +8 -0
  34. kailash/nodes/logic/convergence.py +642 -0
  35. kailash/nodes/logic/loop.py +153 -0
  36. kailash/nodes/logic/operations.py +187 -27
  37. kailash/nodes/mixins/__init__.py +11 -0
  38. kailash/nodes/mixins/mcp.py +228 -0
  39. kailash/nodes/mixins.py +387 -0
  40. kailash/runtime/__init__.py +2 -1
  41. kailash/runtime/access_controlled.py +458 -0
  42. kailash/runtime/local.py +106 -33
  43. kailash/runtime/parallel_cyclic.py +529 -0
  44. kailash/sdk_exceptions.py +90 -5
  45. kailash/security.py +845 -0
  46. kailash/tracking/manager.py +38 -15
  47. kailash/tracking/models.py +1 -1
  48. kailash/tracking/storage/filesystem.py +30 -2
  49. kailash/utils/__init__.py +8 -0
  50. kailash/workflow/__init__.py +18 -0
  51. kailash/workflow/convergence.py +270 -0
  52. kailash/workflow/cycle_analyzer.py +768 -0
  53. kailash/workflow/cycle_builder.py +573 -0
  54. kailash/workflow/cycle_config.py +709 -0
  55. kailash/workflow/cycle_debugger.py +760 -0
  56. kailash/workflow/cycle_exceptions.py +601 -0
  57. kailash/workflow/cycle_profiler.py +671 -0
  58. kailash/workflow/cycle_state.py +338 -0
  59. kailash/workflow/cyclic_runner.py +985 -0
  60. kailash/workflow/graph.py +500 -39
  61. kailash/workflow/migration.py +768 -0
  62. kailash/workflow/safety.py +365 -0
  63. kailash/workflow/templates.py +744 -0
  64. kailash/workflow/validation.py +693 -0
  65. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
  66. kailash-0.2.0.dist-info/RECORD +125 -0
  67. kailash/nodes/mcp/__init__.py +0 -11
  68. kailash/nodes/mcp/client.py +0 -554
  69. kailash/nodes/mcp/resource.py +0 -682
  70. kailash/nodes/mcp/server.py +0 -577
  71. kailash-0.1.5.dist-info/RECORD +0 -88
  72. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  73. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  74. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  75. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,554 +0,0 @@
1
- """MCP Client node for connecting to Model Context Protocol servers."""
2
-
3
- import json
4
- from typing import Any, Dict, List, Optional
5
-
6
- from kailash.nodes.base import Node, NodeParameter, register_node
7
-
8
-
9
- @register_node()
10
- class MCPClient(Node):
11
- """
12
- Client node for connecting to Model Context Protocol (MCP) servers.
13
-
14
- Design Purpose and Philosophy:
15
- The MCPClient node provides a standardized way to connect to MCP servers and access
16
- their resources, tools, and prompts. It abstracts the complexity of the MCP protocol
17
- while providing a simple interface for workflow integration.
18
-
19
- Upstream Dependencies:
20
- - Configuration data specifying server details
21
- - Authentication credentials for secure connections
22
- - Input parameters for resource requests and tool calls
23
-
24
- Downstream Consumers:
25
- - LLMAgentNode nodes that need context from MCP servers
26
- - Workflow nodes that orchestrate multi-step MCP interactions
27
- - Data processing nodes that consume MCP resources
28
-
29
- Usage Patterns:
30
- 1. Connect to MCP servers using stdio, SSE, or HTTP transports
31
- 2. List available resources, tools, and prompts from servers
32
- 3. Fetch specific resources to provide context to AI models
33
- 4. Execute tools on MCP servers with proper error handling
34
- 5. Use prompts from servers for standardized interactions
35
-
36
- Implementation Details:
37
- - Uses the official MCP Python SDK for protocol compliance
38
- - Supports all standard MCP transports (stdio, SSE, HTTP)
39
- - Implements proper connection lifecycle management
40
- - Provides caching for frequently accessed resources
41
- - Handles authentication and rate limiting transparently
42
-
43
- Error Handling:
44
- - ConnectionError: When unable to connect to MCP server
45
- - TimeoutError: When server operations exceed timeout limits
46
- - AuthenticationError: When credentials are invalid or expired
47
- - ProtocolError: When MCP protocol violations occur
48
- - ResourceNotFoundError: When requested resources don't exist
49
-
50
- Side Effects:
51
- - Establishes network connections to external MCP servers
52
- - May cache resource data locally for performance
53
- - Logs connection events and errors for debugging
54
-
55
- Examples:
56
- >>> # Connect to an MCP server and list resources
57
- >>> client = MCPClient()
58
- >>> result = client.run(
59
- ... server_config={
60
- ... "name": "filesystem-server",
61
- ... "command": "python",
62
- ... "args": ["-m", "mcp_filesystem"]
63
- ... },
64
- ... operation="list_resources"
65
- ... )
66
-
67
- >>> # Fetch a specific resource
68
- >>> resource = client.run(
69
- ... server_config=server_config,
70
- ... operation="read_resource",
71
- ... resource_uri="file:///path/to/document.txt"
72
- ... )
73
-
74
- >>> # Call a tool on the server
75
- >>> tool_result = client.run(
76
- ... server_config=server_config,
77
- ... operation="call_tool",
78
- ... tool_name="create_file",
79
- ... tool_arguments={
80
- ... "path": "/path/to/new_file.txt",
81
- ... "content": "Hello, World!"
82
- ... }
83
- ... )
84
- """
85
-
86
- def get_parameters(self) -> Dict[str, NodeParameter]:
87
- return {
88
- "server_config": NodeParameter(
89
- name="server_config",
90
- type=dict,
91
- required=False,
92
- default={},
93
- description="MCP server configuration (name, command, args, transport)",
94
- ),
95
- "operation": NodeParameter(
96
- name="operation",
97
- type=str,
98
- required=False,
99
- default="list_resources",
100
- description="Operation to perform: list_resources, read_resource, list_tools, call_tool, list_prompts, get_prompt",
101
- ),
102
- "resource_uri": NodeParameter(
103
- name="resource_uri",
104
- type=str,
105
- required=False,
106
- description="URI of the resource to read (for read_resource operation)",
107
- ),
108
- "tool_name": NodeParameter(
109
- name="tool_name",
110
- type=str,
111
- required=False,
112
- description="Name of the tool to call (for call_tool operation)",
113
- ),
114
- "tool_arguments": NodeParameter(
115
- name="tool_arguments",
116
- type=dict,
117
- required=False,
118
- default={},
119
- description="Arguments to pass to the tool (for call_tool operation)",
120
- ),
121
- "prompt_name": NodeParameter(
122
- name="prompt_name",
123
- type=str,
124
- required=False,
125
- description="Name of the prompt to get (for get_prompt operation)",
126
- ),
127
- "prompt_arguments": NodeParameter(
128
- name="prompt_arguments",
129
- type=dict,
130
- required=False,
131
- default={},
132
- description="Arguments to pass to the prompt (for get_prompt operation)",
133
- ),
134
- "timeout": NodeParameter(
135
- name="timeout",
136
- type=int,
137
- required=False,
138
- default=30,
139
- description="Timeout in seconds for MCP operations",
140
- ),
141
- "max_retries": NodeParameter(
142
- name="max_retries",
143
- type=int,
144
- required=False,
145
- default=3,
146
- description="Maximum number of retry attempts",
147
- ),
148
- }
149
-
150
- def run(self, **kwargs) -> Dict[str, Any]:
151
- server_config = kwargs["server_config"]
152
- operation = kwargs["operation"]
153
- resource_uri = kwargs.get("resource_uri")
154
- tool_name = kwargs.get("tool_name")
155
- tool_arguments = kwargs.get("tool_arguments", {})
156
- prompt_name = kwargs.get("prompt_name")
157
- prompt_arguments = kwargs.get("prompt_arguments", {})
158
- # timeout = kwargs.get("timeout", 30) # unused for now
159
- # max_retries = kwargs.get("max_retries", 3) # unused for now
160
-
161
- try:
162
- # Import MCP SDK (graceful fallback if not installed)
163
- try:
164
- import mcp.client.session # noqa: F401
165
-
166
- mcp_available = True
167
- except ImportError:
168
- mcp_available = False
169
-
170
- if not mcp_available:
171
- # Provide mock functionality when MCP SDK is not available
172
- return self._mock_mcp_operation(
173
- operation,
174
- server_config,
175
- resource_uri,
176
- tool_name,
177
- tool_arguments,
178
- prompt_name,
179
- prompt_arguments,
180
- )
181
-
182
- # Extract server configuration
183
- server_name = server_config.get("name", "unknown-server")
184
- transport_type = server_config.get("transport", "stdio")
185
-
186
- if transport_type == "stdio":
187
- command = server_config.get("command")
188
- args = server_config.get("args", [])
189
-
190
- if not command:
191
- raise ValueError(
192
- "stdio transport requires 'command' in server_config"
193
- )
194
-
195
- # For now, provide mock implementation as we need the actual MCP server running
196
- return self._mock_stdio_operation(
197
- operation,
198
- server_name,
199
- command,
200
- args,
201
- resource_uri,
202
- tool_name,
203
- tool_arguments,
204
- prompt_name,
205
- prompt_arguments,
206
- )
207
-
208
- elif transport_type in ["sse", "http"]:
209
- # HTTP/SSE transport implementation
210
- url = server_config.get("url")
211
- headers = server_config.get("headers", {})
212
-
213
- if not url:
214
- raise ValueError(
215
- f"{transport_type} transport requires 'url' in server_config"
216
- )
217
-
218
- return self._mock_http_operation(
219
- operation,
220
- server_name,
221
- url,
222
- headers,
223
- resource_uri,
224
- tool_name,
225
- tool_arguments,
226
- prompt_name,
227
- prompt_arguments,
228
- )
229
-
230
- else:
231
- raise ValueError(f"Unsupported transport type: {transport_type}")
232
-
233
- except Exception as e:
234
- return {
235
- "success": False,
236
- "error": str(e),
237
- "error_type": type(e).__name__,
238
- "operation": operation,
239
- "server": server_config.get("name", "unknown"),
240
- }
241
-
242
- def _mock_mcp_operation(
243
- self,
244
- operation: str,
245
- server_config: dict,
246
- resource_uri: Optional[str],
247
- tool_name: Optional[str],
248
- tool_arguments: dict,
249
- prompt_name: Optional[str],
250
- prompt_arguments: dict,
251
- ) -> Dict[str, Any]:
252
- """Mock MCP operations when SDK is not available."""
253
- server_name = server_config.get("name", "mock-server")
254
-
255
- if operation == "list_resources":
256
- return {
257
- "success": True,
258
- "operation": operation,
259
- "server": server_name,
260
- "resources": [
261
- {
262
- "uri": "file:///example/document.txt",
263
- "name": "Example Document",
264
- "description": "A sample text document",
265
- "mimeType": "text/plain",
266
- },
267
- {
268
- "uri": "data://config/settings.json",
269
- "name": "Configuration Settings",
270
- "description": "Application configuration",
271
- "mimeType": "application/json",
272
- },
273
- ],
274
- "resource_count": 2,
275
- "mock": True,
276
- }
277
-
278
- elif operation == "read_resource":
279
- if not resource_uri:
280
- return {
281
- "success": False,
282
- "error": "resource_uri is required for read_resource operation",
283
- "operation": operation,
284
- "server": server_name,
285
- }
286
-
287
- # Mock resource content based on URI
288
- if "document.txt" in resource_uri:
289
- content = "This is the content of the example document.\nIt contains sample text for testing MCP functionality."
290
- elif "settings.json" in resource_uri:
291
- content = json.dumps(
292
- {
293
- "app_name": "MCP Demo",
294
- "version": "1.0.0",
295
- "features": ["resource_access", "tool_calling", "prompts"],
296
- },
297
- indent=2,
298
- )
299
- else:
300
- content = f"Mock content for resource: {resource_uri}"
301
-
302
- return {
303
- "success": True,
304
- "operation": operation,
305
- "server": server_name,
306
- "resource": {
307
- "uri": resource_uri,
308
- "content": content,
309
- "mimeType": (
310
- "text/plain"
311
- if resource_uri.endswith(".txt")
312
- else "application/json"
313
- ),
314
- },
315
- "mock": True,
316
- }
317
-
318
- elif operation == "list_tools":
319
- return {
320
- "success": True,
321
- "operation": operation,
322
- "server": server_name,
323
- "tools": [
324
- {
325
- "name": "create_file",
326
- "description": "Create a new file with specified content",
327
- "inputSchema": {
328
- "type": "object",
329
- "properties": {
330
- "path": {"type": "string", "description": "File path"},
331
- "content": {
332
- "type": "string",
333
- "description": "File content",
334
- },
335
- },
336
- "required": ["path", "content"],
337
- },
338
- },
339
- {
340
- "name": "search_files",
341
- "description": "Search for files matching a pattern",
342
- "inputSchema": {
343
- "type": "object",
344
- "properties": {
345
- "pattern": {
346
- "type": "string",
347
- "description": "Search pattern",
348
- },
349
- "directory": {
350
- "type": "string",
351
- "description": "Directory to search",
352
- },
353
- },
354
- "required": ["pattern"],
355
- },
356
- },
357
- ],
358
- "tool_count": 2,
359
- "mock": True,
360
- }
361
-
362
- elif operation == "call_tool":
363
- if not tool_name:
364
- return {
365
- "success": False,
366
- "error": "tool_name is required for call_tool operation",
367
- "operation": operation,
368
- "server": server_name,
369
- }
370
-
371
- # Mock tool execution results
372
- if tool_name == "create_file":
373
- path = tool_arguments.get("path", "/unknown/path")
374
- content = tool_arguments.get("content", "")
375
- result = {
376
- "success": True,
377
- "message": f"File created successfully at {path}",
378
- "file_size": len(content),
379
- "created_at": "2025-06-01T12:00:00Z",
380
- }
381
- elif tool_name == "search_files":
382
- pattern = tool_arguments.get("pattern", "*")
383
- directory = tool_arguments.get("directory", "/")
384
- result = {
385
- "matches": [
386
- f"{directory}/example1.txt",
387
- f"{directory}/example2.txt",
388
- ],
389
- "pattern": pattern,
390
- "total_matches": 2,
391
- }
392
- else:
393
- result = {
394
- "message": f"Mock execution of tool '{tool_name}'",
395
- "arguments": tool_arguments,
396
- }
397
-
398
- return {
399
- "success": True,
400
- "operation": operation,
401
- "server": server_name,
402
- "tool_name": tool_name,
403
- "tool_arguments": tool_arguments,
404
- "result": result,
405
- "mock": True,
406
- }
407
-
408
- elif operation == "list_prompts":
409
- return {
410
- "success": True,
411
- "operation": operation,
412
- "server": server_name,
413
- "prompts": [
414
- {
415
- "name": "summarize_document",
416
- "description": "Summarize a document with specified length",
417
- "arguments": [
418
- {
419
- "name": "document",
420
- "description": "Document to summarize",
421
- "required": True,
422
- },
423
- {
424
- "name": "max_length",
425
- "description": "Maximum summary length",
426
- "required": False,
427
- },
428
- ],
429
- },
430
- {
431
- "name": "analyze_code",
432
- "description": "Analyze code for issues and improvements",
433
- "arguments": [
434
- {
435
- "name": "code",
436
- "description": "Code to analyze",
437
- "required": True,
438
- },
439
- {
440
- "name": "language",
441
- "description": "Programming language",
442
- "required": False,
443
- },
444
- ],
445
- },
446
- ],
447
- "prompt_count": 2,
448
- "mock": True,
449
- }
450
-
451
- elif operation == "get_prompt":
452
- if not prompt_name:
453
- return {
454
- "success": False,
455
- "error": "prompt_name is required for get_prompt operation",
456
- "operation": operation,
457
- "server": server_name,
458
- }
459
-
460
- # Mock prompt content
461
- if prompt_name == "summarize_document":
462
- document = prompt_arguments.get("document", "[DOCUMENT CONTENT]")
463
- max_length = prompt_arguments.get("max_length", 200)
464
- prompt_content = f"Please summarize the following document in no more than {max_length} words:\n\n{document}"
465
- elif prompt_name == "analyze_code":
466
- code = prompt_arguments.get("code", "[CODE CONTENT]")
467
- language = prompt_arguments.get("language", "python")
468
- prompt_content = f"Please analyze the following {language} code and provide feedback on potential issues and improvements:\n\n```{language}\n{code}\n```"
469
- else:
470
- prompt_content = f"Mock prompt content for '{prompt_name}' with arguments: {prompt_arguments}"
471
-
472
- return {
473
- "success": True,
474
- "operation": operation,
475
- "server": server_name,
476
- "prompt_name": prompt_name,
477
- "prompt_arguments": prompt_arguments,
478
- "prompt": {
479
- "name": prompt_name,
480
- "content": prompt_content,
481
- "arguments": prompt_arguments,
482
- },
483
- "mock": True,
484
- }
485
-
486
- else:
487
- return {
488
- "success": False,
489
- "error": f"Unsupported operation: {operation}",
490
- "operation": operation,
491
- "server": server_name,
492
- "supported_operations": [
493
- "list_resources",
494
- "read_resource",
495
- "list_tools",
496
- "call_tool",
497
- "list_prompts",
498
- "get_prompt",
499
- ],
500
- }
501
-
502
- def _mock_stdio_operation(
503
- self,
504
- operation: str,
505
- server_name: str,
506
- command: str,
507
- args: List[str],
508
- resource_uri: Optional[str],
509
- tool_name: Optional[str],
510
- tool_arguments: dict,
511
- prompt_name: Optional[str],
512
- prompt_arguments: dict,
513
- ) -> Dict[str, Any]:
514
- """Mock stdio transport operations."""
515
- result = self._mock_mcp_operation(
516
- operation,
517
- {"name": server_name},
518
- resource_uri,
519
- tool_name,
520
- tool_arguments,
521
- prompt_name,
522
- prompt_arguments,
523
- )
524
- result.update(
525
- {"transport": "stdio", "command": command, "args": args, "mock": True}
526
- )
527
- return result
528
-
529
- def _mock_http_operation(
530
- self,
531
- operation: str,
532
- server_name: str,
533
- url: str,
534
- headers: dict,
535
- resource_uri: Optional[str],
536
- tool_name: Optional[str],
537
- tool_arguments: dict,
538
- prompt_name: Optional[str],
539
- prompt_arguments: dict,
540
- ) -> Dict[str, Any]:
541
- """Mock HTTP/SSE transport operations."""
542
- result = self._mock_mcp_operation(
543
- operation,
544
- {"name": server_name},
545
- resource_uri,
546
- tool_name,
547
- tool_arguments,
548
- prompt_name,
549
- prompt_arguments,
550
- )
551
- result.update(
552
- {"transport": "http", "url": url, "headers": headers, "mock": True}
553
- )
554
- return result