kailash 0.1.2__py3-none-any.whl → 0.1.4__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 +1 -1
- kailash/api/__init__.py +17 -0
- kailash/api/gateway.py +394 -0
- kailash/api/mcp_integration.py +478 -0
- kailash/api/workflow_api.py +399 -0
- kailash/nodes/ai/__init__.py +4 -4
- kailash/nodes/ai/agents.py +4 -4
- kailash/nodes/ai/ai_providers.py +18 -22
- kailash/nodes/ai/embedding_generator.py +34 -38
- kailash/nodes/ai/llm_agent.py +351 -356
- kailash/nodes/api/http.py +0 -4
- kailash/nodes/api/rest.py +1 -1
- kailash/nodes/base.py +60 -64
- kailash/nodes/code/python.py +61 -42
- kailash/nodes/data/__init__.py +10 -10
- kailash/nodes/data/readers.py +27 -29
- kailash/nodes/data/retrieval.py +1 -1
- kailash/nodes/data/sharepoint_graph.py +23 -25
- kailash/nodes/data/sql.py +27 -29
- kailash/nodes/data/vector_db.py +2 -2
- kailash/nodes/data/writers.py +41 -44
- kailash/nodes/logic/__init__.py +10 -3
- kailash/nodes/logic/async_operations.py +14 -14
- kailash/nodes/logic/operations.py +18 -22
- kailash/nodes/logic/workflow.py +439 -0
- kailash/nodes/mcp/client.py +29 -33
- kailash/nodes/mcp/resource.py +1 -1
- kailash/nodes/mcp/server.py +10 -4
- kailash/nodes/transform/formatters.py +1 -1
- kailash/nodes/transform/processors.py +5 -3
- kailash/runtime/docker.py +2 -0
- kailash/tracking/metrics_collector.py +6 -7
- kailash/tracking/models.py +0 -20
- kailash/tracking/storage/database.py +4 -4
- kailash/tracking/storage/filesystem.py +0 -1
- kailash/utils/export.py +2 -2
- kailash/utils/templates.py +16 -16
- kailash/visualization/performance.py +7 -7
- kailash/visualization/reports.py +1 -1
- kailash/workflow/graph.py +4 -4
- kailash/workflow/mock_registry.py +1 -1
- {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/METADATA +198 -27
- kailash-0.1.4.dist-info/RECORD +85 -0
- kailash-0.1.2.dist-info/RECORD +0 -80
- {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/WHEEL +0 -0
- {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,478 @@
|
|
1
|
+
"""MCP (Model Context Protocol) integration for Kailash workflows.
|
2
|
+
|
3
|
+
This module provides integration between MCP servers and Kailash workflows,
|
4
|
+
allowing workflows to access MCP tools and resources.
|
5
|
+
|
6
|
+
Design Philosophy:
|
7
|
+
MCP servers provide tools and context that can be used by workflows.
|
8
|
+
This integration allows workflows to discover and use MCP tools dynamically,
|
9
|
+
bridging the gap between workflow automation and AI-powered tools.
|
10
|
+
|
11
|
+
Example:
|
12
|
+
Basic MCP integration:
|
13
|
+
|
14
|
+
>>> from kailash.api.mcp_integration import MCPIntegration
|
15
|
+
>>> # from kailash.workflow import Workflow # doctest: +SKIP
|
16
|
+
>>> # Create MCP integration
|
17
|
+
>>> mcp = MCPIntegration("tools_server")
|
18
|
+
>>> # Add tools
|
19
|
+
>>> # mcp.add_tool("search", search_function) # doctest: +SKIP
|
20
|
+
>>> # mcp.add_tool("calculate", calculator_function) # doctest: +SKIP
|
21
|
+
>>> # Use in workflow
|
22
|
+
>>> # workflow = Workflow("mcp_workflow") # doctest: +SKIP
|
23
|
+
>>> # workflow.register_mcp_server(mcp) # doctest: +SKIP
|
24
|
+
>>> # Nodes can now access MCP tools
|
25
|
+
>>> # node = MCPToolNode(tool_name="search") # doctest: +SKIP
|
26
|
+
>>> # workflow.add_node("search_data", node) # doctest: +SKIP
|
27
|
+
"""
|
28
|
+
|
29
|
+
import asyncio
|
30
|
+
import logging
|
31
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
32
|
+
|
33
|
+
from pydantic import BaseModel, Field
|
34
|
+
|
35
|
+
from ..nodes.base_async import AsyncNode
|
36
|
+
|
37
|
+
logger = logging.getLogger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class MCPTool(BaseModel):
|
41
|
+
"""Definition of an MCP tool."""
|
42
|
+
|
43
|
+
name: str
|
44
|
+
description: str
|
45
|
+
parameters: Dict[str, Any] = Field(default_factory=dict)
|
46
|
+
function: Optional[Callable] = None
|
47
|
+
async_function: Optional[Callable] = None
|
48
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
49
|
+
|
50
|
+
|
51
|
+
class MCPResource(BaseModel):
|
52
|
+
"""Definition of an MCP resource."""
|
53
|
+
|
54
|
+
name: str
|
55
|
+
uri: str
|
56
|
+
description: str
|
57
|
+
mime_type: str = "text/plain"
|
58
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
59
|
+
|
60
|
+
|
61
|
+
class MCPIntegration:
|
62
|
+
"""MCP server integration for Kailash workflows.
|
63
|
+
|
64
|
+
Provides:
|
65
|
+
- Tool registration and discovery
|
66
|
+
- Resource management
|
67
|
+
- Async/sync tool execution
|
68
|
+
- Context sharing between workflows and MCP
|
69
|
+
|
70
|
+
Attributes:
|
71
|
+
name: MCP server name
|
72
|
+
tools: Registry of available tools
|
73
|
+
resources: Registry of available resources
|
74
|
+
"""
|
75
|
+
|
76
|
+
def __init__(
|
77
|
+
self, name: str, description: str = "", capabilities: List[str] = None
|
78
|
+
):
|
79
|
+
"""Initialize MCP integration.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
name: MCP server name
|
83
|
+
description: Server description
|
84
|
+
capabilities: List of server capabilities
|
85
|
+
"""
|
86
|
+
self.name = name
|
87
|
+
self.description = description
|
88
|
+
self.capabilities = capabilities or ["tools", "resources"]
|
89
|
+
self.tools: Dict[str, MCPTool] = {}
|
90
|
+
self.resources: Dict[str, MCPResource] = {}
|
91
|
+
self._context: Dict[str, Any] = {}
|
92
|
+
|
93
|
+
def add_tool(
|
94
|
+
self,
|
95
|
+
name: str,
|
96
|
+
function: Union[Callable, Callable[..., asyncio.Future]],
|
97
|
+
description: str = "",
|
98
|
+
parameters: Dict[str, Any] = None,
|
99
|
+
):
|
100
|
+
"""Add a tool to the MCP server.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
name: Tool name
|
104
|
+
function: Tool implementation (sync or async)
|
105
|
+
description: Tool description
|
106
|
+
parameters: Tool parameter schema
|
107
|
+
"""
|
108
|
+
tool = MCPTool(
|
109
|
+
name=name,
|
110
|
+
description=description,
|
111
|
+
parameters=parameters or {},
|
112
|
+
function=function if not asyncio.iscoroutinefunction(function) else None,
|
113
|
+
async_function=function if asyncio.iscoroutinefunction(function) else None,
|
114
|
+
)
|
115
|
+
|
116
|
+
self.tools[name] = tool
|
117
|
+
logger.info(f"Added tool '{name}' to MCP server '{self.name}'")
|
118
|
+
|
119
|
+
def add_resource(
|
120
|
+
self, name: str, uri: str, description: str = "", mime_type: str = "text/plain"
|
121
|
+
):
|
122
|
+
"""Add a resource to the MCP server.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
name: Resource name
|
126
|
+
uri: Resource URI
|
127
|
+
description: Resource description
|
128
|
+
mime_type: MIME type of the resource
|
129
|
+
"""
|
130
|
+
resource = MCPResource(
|
131
|
+
name=name, uri=uri, description=description, mime_type=mime_type
|
132
|
+
)
|
133
|
+
|
134
|
+
self.resources[name] = resource
|
135
|
+
logger.info(f"Added resource '{name}' to MCP server '{self.name}'")
|
136
|
+
|
137
|
+
async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
|
138
|
+
"""Execute a tool asynchronously.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
tool_name: Name of the tool to execute
|
142
|
+
parameters: Tool parameters
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Tool execution result
|
146
|
+
"""
|
147
|
+
tool = self.tools.get(tool_name)
|
148
|
+
if not tool:
|
149
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
150
|
+
|
151
|
+
# Add context to parameters only if function accepts it
|
152
|
+
import inspect
|
153
|
+
|
154
|
+
if tool.async_function:
|
155
|
+
sig = inspect.signature(tool.async_function)
|
156
|
+
else:
|
157
|
+
sig = inspect.signature(tool.function)
|
158
|
+
|
159
|
+
if "_context" in sig.parameters:
|
160
|
+
params = {**parameters, "_context": self._context}
|
161
|
+
else:
|
162
|
+
params = parameters
|
163
|
+
|
164
|
+
if tool.async_function:
|
165
|
+
return await tool.async_function(**params)
|
166
|
+
elif tool.function:
|
167
|
+
# Run sync function in executor
|
168
|
+
loop = asyncio.get_event_loop()
|
169
|
+
from functools import partial
|
170
|
+
|
171
|
+
func = partial(tool.function, **params)
|
172
|
+
return await loop.run_in_executor(None, func)
|
173
|
+
else:
|
174
|
+
raise ValueError(f"Tool '{tool_name}' has no implementation")
|
175
|
+
|
176
|
+
def execute_tool_sync(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
|
177
|
+
"""Execute a tool synchronously.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
tool_name: Name of the tool to execute
|
181
|
+
parameters: Tool parameters
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Tool execution result
|
185
|
+
"""
|
186
|
+
tool = self.tools.get(tool_name)
|
187
|
+
if not tool:
|
188
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
189
|
+
|
190
|
+
if tool.function:
|
191
|
+
# Check if function accepts _context parameter
|
192
|
+
import inspect
|
193
|
+
|
194
|
+
sig = inspect.signature(tool.function)
|
195
|
+
if "_context" in sig.parameters:
|
196
|
+
params = {**parameters, "_context": self._context}
|
197
|
+
else:
|
198
|
+
params = parameters
|
199
|
+
return tool.function(**params)
|
200
|
+
else:
|
201
|
+
raise ValueError(f"Tool '{tool_name}' requires async execution")
|
202
|
+
|
203
|
+
def get_resource(self, resource_name: str) -> Optional[MCPResource]:
|
204
|
+
"""Get a resource by name.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
resource_name: Resource name
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Resource if found
|
211
|
+
"""
|
212
|
+
return self.resources.get(resource_name)
|
213
|
+
|
214
|
+
def set_context(self, key: str, value: Any):
|
215
|
+
"""Set context value.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
key: Context key
|
219
|
+
value: Context value
|
220
|
+
"""
|
221
|
+
self._context[key] = value
|
222
|
+
|
223
|
+
def get_context(self, key: str) -> Any:
|
224
|
+
"""Get context value.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
key: Context key
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Context value
|
231
|
+
"""
|
232
|
+
return self._context.get(key)
|
233
|
+
|
234
|
+
def list_tools(self) -> List[Dict[str, Any]]:
|
235
|
+
"""List all available tools.
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
List of tool definitions
|
239
|
+
"""
|
240
|
+
return [
|
241
|
+
{
|
242
|
+
"name": tool.name,
|
243
|
+
"description": tool.description,
|
244
|
+
"parameters": tool.parameters,
|
245
|
+
}
|
246
|
+
for tool in self.tools.values()
|
247
|
+
]
|
248
|
+
|
249
|
+
def list_resources(self) -> List[Dict[str, Any]]:
|
250
|
+
"""List all available resources.
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
List of resource definitions
|
254
|
+
"""
|
255
|
+
return [
|
256
|
+
{
|
257
|
+
"name": resource.name,
|
258
|
+
"uri": resource.uri,
|
259
|
+
"description": resource.description,
|
260
|
+
"mime_type": resource.mime_type,
|
261
|
+
}
|
262
|
+
for resource in self.resources.values()
|
263
|
+
]
|
264
|
+
|
265
|
+
def to_mcp_protocol(self) -> Dict[str, Any]:
|
266
|
+
"""Convert to MCP protocol format.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
MCP server definition
|
270
|
+
"""
|
271
|
+
return {
|
272
|
+
"name": self.name,
|
273
|
+
"description": self.description,
|
274
|
+
"capabilities": self.capabilities,
|
275
|
+
"tools": self.list_tools(),
|
276
|
+
"resources": self.list_resources(),
|
277
|
+
}
|
278
|
+
|
279
|
+
|
280
|
+
class MCPToolNode(AsyncNode):
|
281
|
+
"""Node that executes MCP tools within a workflow.
|
282
|
+
|
283
|
+
This node allows workflows to use tools provided by MCP servers,
|
284
|
+
bridging the gap between workflow automation and AI-powered tools.
|
285
|
+
|
286
|
+
Example:
|
287
|
+
Using an MCP tool in a workflow:
|
288
|
+
|
289
|
+
>>> # Create node for specific tool
|
290
|
+
>>> search_node = MCPToolNode(
|
291
|
+
... mcp_server="tools",
|
292
|
+
... tool_name="web_search"
|
293
|
+
... )
|
294
|
+
>>> # Add to workflow
|
295
|
+
>>> # workflow.add_node("search", search_node) # doctest: +SKIP
|
296
|
+
>>> # Execute with parameters
|
297
|
+
>>> # result = workflow.run({"search": {"query": "Kailash SDK documentation"}}) # doctest: +SKIP
|
298
|
+
"""
|
299
|
+
|
300
|
+
def __init__(
|
301
|
+
self, mcp_server: str, tool_name: str, parameter_mapping: Dict[str, str] = None
|
302
|
+
):
|
303
|
+
"""Initialize MCP tool node.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
mcp_server: Name of the MCP server
|
307
|
+
tool_name: Name of the tool to execute
|
308
|
+
parameter_mapping: Map input keys to tool parameters
|
309
|
+
"""
|
310
|
+
super().__init__()
|
311
|
+
self.mcp_server = mcp_server
|
312
|
+
self.tool_name = tool_name
|
313
|
+
self.parameter_mapping = parameter_mapping or {}
|
314
|
+
self._mcp_integration: Optional[MCPIntegration] = None
|
315
|
+
|
316
|
+
def set_mcp_integration(self, mcp: MCPIntegration):
|
317
|
+
"""Set the MCP integration instance."""
|
318
|
+
self._mcp_integration = mcp
|
319
|
+
|
320
|
+
def get_parameters(self) -> Dict[str, Any]:
|
321
|
+
"""Get node parameters.
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
Dictionary of parameters
|
325
|
+
"""
|
326
|
+
# For MCP tools, parameters are dynamic based on the tool
|
327
|
+
return {}
|
328
|
+
|
329
|
+
def validate_inputs(self, **kwargs) -> Dict[str, Any]:
|
330
|
+
"""Validate runtime inputs.
|
331
|
+
|
332
|
+
For MCPToolNode, we accept any inputs since the parameters
|
333
|
+
are dynamic based on the MCP tool being used.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
**kwargs: Runtime inputs
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
All inputs as-is
|
340
|
+
"""
|
341
|
+
# For MCP tools, pass through all inputs without validation
|
342
|
+
# The actual validation happens in the MCP tool itself
|
343
|
+
return kwargs
|
344
|
+
|
345
|
+
def run(self, **kwargs) -> Any:
|
346
|
+
"""Run the node synchronously.
|
347
|
+
|
348
|
+
Args:
|
349
|
+
**kwargs: Input parameters
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
Execution result
|
353
|
+
"""
|
354
|
+
if not self._mcp_integration:
|
355
|
+
raise RuntimeError("MCP integration not set")
|
356
|
+
|
357
|
+
# Map parameters if needed
|
358
|
+
tool_params = {}
|
359
|
+
for input_key, tool_key in self.parameter_mapping.items():
|
360
|
+
if input_key in kwargs:
|
361
|
+
tool_params[tool_key] = kwargs[input_key]
|
362
|
+
|
363
|
+
# Add unmapped parameters
|
364
|
+
for key, value in kwargs.items():
|
365
|
+
if key not in self.parameter_mapping:
|
366
|
+
tool_params[key] = value
|
367
|
+
|
368
|
+
# Use synchronous execution
|
369
|
+
result = self._mcp_integration.execute_tool_sync(self.tool_name, tool_params)
|
370
|
+
|
371
|
+
# Ensure result is wrapped in a dict for consistency
|
372
|
+
if not isinstance(result, dict):
|
373
|
+
return {"result": result}
|
374
|
+
return result
|
375
|
+
|
376
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
377
|
+
"""Run the node asynchronously.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
**kwargs: Input parameters
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
Dictionary of outputs
|
384
|
+
"""
|
385
|
+
if not self._mcp_integration:
|
386
|
+
raise RuntimeError("MCP integration not set")
|
387
|
+
|
388
|
+
# Map parameters if needed
|
389
|
+
tool_params = {}
|
390
|
+
for input_key, tool_key in self.parameter_mapping.items():
|
391
|
+
if input_key in kwargs:
|
392
|
+
tool_params[tool_key] = kwargs[input_key]
|
393
|
+
|
394
|
+
# Add unmapped parameters
|
395
|
+
for key, value in kwargs.items():
|
396
|
+
if key not in self.parameter_mapping:
|
397
|
+
tool_params[key] = value
|
398
|
+
|
399
|
+
# Execute tool asynchronously
|
400
|
+
result = await self._mcp_integration.execute_tool(self.tool_name, tool_params)
|
401
|
+
|
402
|
+
# Ensure result is wrapped in a dict for consistency
|
403
|
+
if not isinstance(result, dict):
|
404
|
+
return {"result": result}
|
405
|
+
return result
|
406
|
+
|
407
|
+
|
408
|
+
def create_example_mcp_server() -> MCPIntegration:
|
409
|
+
"""Create an example MCP server with common tools."""
|
410
|
+
|
411
|
+
mcp = MCPIntegration("example_tools", "Example MCP server with utility tools")
|
412
|
+
|
413
|
+
# Add search tool
|
414
|
+
async def web_search(query: str, max_results: int = 10, **kwargs):
|
415
|
+
"""Simulate web search."""
|
416
|
+
return {
|
417
|
+
"query": query,
|
418
|
+
"results": [
|
419
|
+
{"title": f"Result {i}", "url": f"https://example.com/{i}"}
|
420
|
+
for i in range(max_results)
|
421
|
+
],
|
422
|
+
}
|
423
|
+
|
424
|
+
mcp.add_tool(
|
425
|
+
"web_search",
|
426
|
+
web_search,
|
427
|
+
"Search the web",
|
428
|
+
{
|
429
|
+
"query": {"type": "string", "required": True},
|
430
|
+
"max_results": {"type": "integer", "default": 10},
|
431
|
+
},
|
432
|
+
)
|
433
|
+
|
434
|
+
# Add calculator tool
|
435
|
+
def calculate(expression: str, **kwargs):
|
436
|
+
"""Evaluate mathematical expression."""
|
437
|
+
try:
|
438
|
+
# Safe evaluation of mathematical expressions
|
439
|
+
import ast
|
440
|
+
import operator as op
|
441
|
+
|
442
|
+
# Operators would be used for safe eval implementation
|
443
|
+
# Currently using simple eval for the expression
|
444
|
+
_ = {
|
445
|
+
ast.Add: op.add,
|
446
|
+
ast.Sub: op.sub,
|
447
|
+
ast.Mult: op.mul,
|
448
|
+
ast.Div: op.truediv,
|
449
|
+
ast.Pow: op.pow,
|
450
|
+
}
|
451
|
+
|
452
|
+
def eval_expr(expr):
|
453
|
+
return eval(expr, {"__builtins__": {}}, {})
|
454
|
+
|
455
|
+
result = eval_expr(expression)
|
456
|
+
return {"expression": expression, "result": result}
|
457
|
+
except Exception as e:
|
458
|
+
return {"error": str(e)}
|
459
|
+
|
460
|
+
mcp.add_tool(
|
461
|
+
"calculate",
|
462
|
+
calculate,
|
463
|
+
"Evaluate mathematical expressions",
|
464
|
+
{"expression": {"type": "string", "required": True}},
|
465
|
+
)
|
466
|
+
|
467
|
+
# Add resources
|
468
|
+
mcp.add_resource(
|
469
|
+
"documentation", "https://docs.kailash.io", "Kailash SDK documentation"
|
470
|
+
)
|
471
|
+
|
472
|
+
mcp.add_resource(
|
473
|
+
"examples",
|
474
|
+
"https://github.com/kailash/examples",
|
475
|
+
"Example workflows and patterns",
|
476
|
+
)
|
477
|
+
|
478
|
+
return mcp
|