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.
- kailash/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +16 -6
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
kailash/nodes/mcp/resource.py
DELETED
@@ -1,682 +0,0 @@
|
|
1
|
-
"""MCP Resource node for managing shared resources in Model Context Protocol."""
|
2
|
-
|
3
|
-
import json
|
4
|
-
from typing import Any, Dict, Optional
|
5
|
-
|
6
|
-
from kailash.nodes.base import Node, NodeParameter, register_node
|
7
|
-
|
8
|
-
|
9
|
-
@register_node()
|
10
|
-
class MCPResource(Node):
|
11
|
-
"""
|
12
|
-
Resource node for creating and managing Model Context Protocol (MCP) resources.
|
13
|
-
|
14
|
-
Design Purpose and Philosophy:
|
15
|
-
The MCPResource node provides a standardized way to create, update, and manage
|
16
|
-
resources that can be shared through the MCP protocol. It abstracts resource
|
17
|
-
lifecycle management while ensuring compatibility with MCP standards.
|
18
|
-
|
19
|
-
Upstream Dependencies:
|
20
|
-
- Source data to convert into MCP resources
|
21
|
-
- Resource metadata and schema definitions
|
22
|
-
- Authentication and access control settings
|
23
|
-
- Content transformation and validation rules
|
24
|
-
|
25
|
-
Downstream Consumers:
|
26
|
-
- MCPServer nodes that host and expose resources
|
27
|
-
- MCPClient nodes that consume resource data
|
28
|
-
- AI models that need structured context data
|
29
|
-
- External MCP-compatible applications and tools
|
30
|
-
|
31
|
-
Usage Patterns:
|
32
|
-
1. Create resources from workflow data with proper metadata
|
33
|
-
2. Update existing resources with new content versions
|
34
|
-
3. Validate resource schemas and content formats
|
35
|
-
4. Transform data into MCP-compatible resource structures
|
36
|
-
5. Manage resource access permissions and visibility
|
37
|
-
|
38
|
-
Implementation Details:
|
39
|
-
- Supports all standard MCP resource types and formats
|
40
|
-
- Implements proper resource URI schemes and namespacing
|
41
|
-
- Provides content validation and schema enforcement
|
42
|
-
- Handles resource versioning and update notifications
|
43
|
-
- Manages metadata and discovery information efficiently
|
44
|
-
|
45
|
-
Error Handling:
|
46
|
-
- ResourceValidationError: When resource content fails validation
|
47
|
-
- SchemaViolationError: When resource doesn't match expected schema
|
48
|
-
- URIFormatError: When resource URI is malformed or invalid
|
49
|
-
- ContentTypeError: When resource content type is unsupported
|
50
|
-
- PermissionError: When access control rules are violated
|
51
|
-
|
52
|
-
Side Effects:
|
53
|
-
- Creates or updates resource entries in MCP registries
|
54
|
-
- May cache resource content for improved performance
|
55
|
-
- Logs resource access and modification events
|
56
|
-
- Notifies subscribers about resource changes
|
57
|
-
|
58
|
-
Examples:
|
59
|
-
```python
|
60
|
-
# Create a simple text resource
|
61
|
-
resource = MCPResource()
|
62
|
-
result = resource.run(
|
63
|
-
operation="create",
|
64
|
-
uri="workflow://examples/data/customer_analysis.txt",
|
65
|
-
content="Customer analysis results from Q4 2024...",
|
66
|
-
metadata={
|
67
|
-
"name": "Q4 Customer Analysis",
|
68
|
-
"description": "Quarterly customer behavior analysis",
|
69
|
-
"mimeType": "text/plain",
|
70
|
-
"tags": ["analysis", "customers", "Q4"]
|
71
|
-
}
|
72
|
-
)
|
73
|
-
|
74
|
-
# Create a structured data resource
|
75
|
-
json_resource = MCPResource()
|
76
|
-
result = json_resource.run(
|
77
|
-
operation="create",
|
78
|
-
uri="data://reports/summary.json",
|
79
|
-
content={
|
80
|
-
"total_customers": 15420,
|
81
|
-
"revenue": 2450000,
|
82
|
-
"top_products": ["Product A", "Product B"]
|
83
|
-
},
|
84
|
-
metadata={
|
85
|
-
"name": "Summary Report",
|
86
|
-
"mimeType": "application/json",
|
87
|
-
"schema": "report_summary_v1"
|
88
|
-
}
|
89
|
-
)
|
90
|
-
|
91
|
-
# Update an existing resource
|
92
|
-
updated = MCPResource()
|
93
|
-
result = updated.run(
|
94
|
-
operation="update",
|
95
|
-
uri="workflow://examples/data/customer_analysis.txt",
|
96
|
-
content="Updated customer analysis with new data...",
|
97
|
-
version="2.0"
|
98
|
-
)
|
99
|
-
```
|
100
|
-
"""
|
101
|
-
|
102
|
-
def get_parameters(self) -> Dict[str, NodeParameter]:
|
103
|
-
return {
|
104
|
-
"operation": NodeParameter(
|
105
|
-
name="operation",
|
106
|
-
type=str,
|
107
|
-
required=False,
|
108
|
-
default="create",
|
109
|
-
description="Operation to perform: create, update, delete, validate, list",
|
110
|
-
),
|
111
|
-
"uri": NodeParameter(
|
112
|
-
name="uri",
|
113
|
-
type=str,
|
114
|
-
required=False,
|
115
|
-
description="Resource URI (required for create, update, delete, validate)",
|
116
|
-
),
|
117
|
-
"content": NodeParameter(
|
118
|
-
name="content",
|
119
|
-
type=object,
|
120
|
-
required=False,
|
121
|
-
description="Resource content (required for create, update)",
|
122
|
-
),
|
123
|
-
"metadata": NodeParameter(
|
124
|
-
name="metadata",
|
125
|
-
type=dict,
|
126
|
-
required=False,
|
127
|
-
default={},
|
128
|
-
description="Resource metadata (name, description, mimeType, etc.)",
|
129
|
-
),
|
130
|
-
"schema": NodeParameter(
|
131
|
-
name="schema",
|
132
|
-
type=dict,
|
133
|
-
required=False,
|
134
|
-
description="JSON schema for content validation",
|
135
|
-
),
|
136
|
-
"version": NodeParameter(
|
137
|
-
name="version",
|
138
|
-
type=str,
|
139
|
-
required=False,
|
140
|
-
description="Resource version (auto-generated if not provided)",
|
141
|
-
),
|
142
|
-
"access_control": NodeParameter(
|
143
|
-
name="access_control",
|
144
|
-
type=dict,
|
145
|
-
required=False,
|
146
|
-
default={},
|
147
|
-
description="Access control settings (permissions, visibility)",
|
148
|
-
),
|
149
|
-
"cache_ttl": NodeParameter(
|
150
|
-
name="cache_ttl",
|
151
|
-
type=int,
|
152
|
-
required=False,
|
153
|
-
default=3600,
|
154
|
-
description="Cache time-to-live in seconds",
|
155
|
-
),
|
156
|
-
"auto_notify": NodeParameter(
|
157
|
-
name="auto_notify",
|
158
|
-
type=bool,
|
159
|
-
required=False,
|
160
|
-
default=True,
|
161
|
-
description="Whether to notify subscribers of resource changes",
|
162
|
-
),
|
163
|
-
}
|
164
|
-
|
165
|
-
def run(self, **kwargs) -> Dict[str, Any]:
|
166
|
-
operation = kwargs["operation"]
|
167
|
-
uri = kwargs.get("uri")
|
168
|
-
content = kwargs.get("content")
|
169
|
-
metadata = kwargs.get("metadata", {})
|
170
|
-
schema = kwargs.get("schema")
|
171
|
-
version = kwargs.get("version")
|
172
|
-
access_control = kwargs.get("access_control", {})
|
173
|
-
cache_ttl = kwargs.get("cache_ttl", 3600)
|
174
|
-
auto_notify = kwargs.get("auto_notify", True)
|
175
|
-
|
176
|
-
try:
|
177
|
-
if operation == "create":
|
178
|
-
return self._create_resource(
|
179
|
-
uri,
|
180
|
-
content,
|
181
|
-
metadata,
|
182
|
-
schema,
|
183
|
-
version,
|
184
|
-
access_control,
|
185
|
-
cache_ttl,
|
186
|
-
auto_notify,
|
187
|
-
)
|
188
|
-
elif operation == "update":
|
189
|
-
return self._update_resource(
|
190
|
-
uri,
|
191
|
-
content,
|
192
|
-
metadata,
|
193
|
-
schema,
|
194
|
-
version,
|
195
|
-
access_control,
|
196
|
-
cache_ttl,
|
197
|
-
auto_notify,
|
198
|
-
)
|
199
|
-
elif operation == "delete":
|
200
|
-
return self._delete_resource(uri, auto_notify)
|
201
|
-
elif operation == "validate":
|
202
|
-
return self._validate_resource(uri, content, schema)
|
203
|
-
elif operation == "list":
|
204
|
-
return self._list_resources(metadata.get("filter"))
|
205
|
-
else:
|
206
|
-
return {
|
207
|
-
"success": False,
|
208
|
-
"error": f"Unsupported operation: {operation}",
|
209
|
-
"supported_operations": [
|
210
|
-
"create",
|
211
|
-
"update",
|
212
|
-
"delete",
|
213
|
-
"validate",
|
214
|
-
"list",
|
215
|
-
],
|
216
|
-
}
|
217
|
-
|
218
|
-
except Exception as e:
|
219
|
-
return {
|
220
|
-
"success": False,
|
221
|
-
"error": str(e),
|
222
|
-
"error_type": type(e).__name__,
|
223
|
-
"operation": operation,
|
224
|
-
"uri": uri,
|
225
|
-
}
|
226
|
-
|
227
|
-
def _create_resource(
|
228
|
-
self,
|
229
|
-
uri: Optional[str],
|
230
|
-
content: Any,
|
231
|
-
metadata: dict,
|
232
|
-
schema: Optional[dict],
|
233
|
-
version: Optional[str],
|
234
|
-
access_control: dict,
|
235
|
-
cache_ttl: int,
|
236
|
-
auto_notify: bool,
|
237
|
-
) -> Dict[str, Any]:
|
238
|
-
"""Create a new MCP resource."""
|
239
|
-
if not uri:
|
240
|
-
return {"success": False, "error": "URI is required for create operation"}
|
241
|
-
|
242
|
-
if content is None:
|
243
|
-
return {
|
244
|
-
"success": False,
|
245
|
-
"error": "Content is required for create operation",
|
246
|
-
}
|
247
|
-
|
248
|
-
# Validate URI format
|
249
|
-
if not self._validate_uri(uri):
|
250
|
-
return {
|
251
|
-
"success": False,
|
252
|
-
"error": f"Invalid URI format: {uri}",
|
253
|
-
"uri_requirements": [
|
254
|
-
"Must include a scheme (e.g., 'file://', 'data://', 'workflow://')",
|
255
|
-
"Must not contain invalid characters",
|
256
|
-
"Should follow MCP URI conventions",
|
257
|
-
],
|
258
|
-
}
|
259
|
-
|
260
|
-
# Determine content type and serialize if needed
|
261
|
-
content_str, mime_type = self._serialize_content(content)
|
262
|
-
|
263
|
-
# Use provided mime type or auto-detected one
|
264
|
-
final_mime_type = metadata.get("mimeType", mime_type)
|
265
|
-
|
266
|
-
# Validate content against schema if provided
|
267
|
-
if schema:
|
268
|
-
validation_result = self._validate_against_schema(content, schema)
|
269
|
-
if not validation_result["valid"]:
|
270
|
-
return {
|
271
|
-
"success": False,
|
272
|
-
"error": "Content validation failed",
|
273
|
-
"validation_errors": validation_result["errors"],
|
274
|
-
}
|
275
|
-
|
276
|
-
# Generate version if not provided
|
277
|
-
if not version:
|
278
|
-
import datetime
|
279
|
-
|
280
|
-
version = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
281
|
-
|
282
|
-
# Create resource object
|
283
|
-
resource = {
|
284
|
-
"uri": uri,
|
285
|
-
"name": metadata.get("name", self._extract_name_from_uri(uri)),
|
286
|
-
"description": metadata.get("description", ""),
|
287
|
-
"mimeType": final_mime_type,
|
288
|
-
"content": content_str,
|
289
|
-
"version": version,
|
290
|
-
"created_at": self._current_timestamp(),
|
291
|
-
"updated_at": self._current_timestamp(),
|
292
|
-
"size": len(content_str) if isinstance(content_str, str) else 0,
|
293
|
-
"metadata": {
|
294
|
-
"tags": metadata.get("tags", []),
|
295
|
-
"category": metadata.get("category", "general"),
|
296
|
-
"source": metadata.get("source", "kailash-workflow"),
|
297
|
-
"author": metadata.get("author", "system"),
|
298
|
-
**{
|
299
|
-
k: v
|
300
|
-
for k, v in metadata.items()
|
301
|
-
if k
|
302
|
-
not in [
|
303
|
-
"name",
|
304
|
-
"description",
|
305
|
-
"mimeType",
|
306
|
-
"tags",
|
307
|
-
"category",
|
308
|
-
"source",
|
309
|
-
"author",
|
310
|
-
]
|
311
|
-
},
|
312
|
-
},
|
313
|
-
"access_control": {
|
314
|
-
"visibility": access_control.get("visibility", "public"),
|
315
|
-
"permissions": access_control.get("permissions", ["read"]),
|
316
|
-
"allowed_clients": access_control.get("allowed_clients", []),
|
317
|
-
**{
|
318
|
-
k: v
|
319
|
-
for k, v in access_control.items()
|
320
|
-
if k not in ["visibility", "permissions", "allowed_clients"]
|
321
|
-
},
|
322
|
-
},
|
323
|
-
"cache": {"ttl": cache_ttl, "cacheable": cache_ttl > 0},
|
324
|
-
}
|
325
|
-
|
326
|
-
# Mock resource storage (in real implementation, this would persist to storage)
|
327
|
-
storage_id = f"res_{hash(uri) % 100000}"
|
328
|
-
storage_result = {
|
329
|
-
"stored": True,
|
330
|
-
"storage_id": storage_id,
|
331
|
-
"storage_location": f"mock://storage/resources/{storage_id}",
|
332
|
-
}
|
333
|
-
|
334
|
-
return {
|
335
|
-
"success": True,
|
336
|
-
"operation": "create",
|
337
|
-
"resource": resource,
|
338
|
-
"storage": storage_result,
|
339
|
-
"notifications": {
|
340
|
-
"sent": auto_notify,
|
341
|
-
"subscribers_notified": 3 if auto_notify else 0,
|
342
|
-
},
|
343
|
-
"next_actions": [
|
344
|
-
"Resource is now available for MCP clients",
|
345
|
-
"Use MCPServer to expose this resource",
|
346
|
-
"Monitor resource access patterns",
|
347
|
-
],
|
348
|
-
}
|
349
|
-
|
350
|
-
def _update_resource(
|
351
|
-
self,
|
352
|
-
uri: Optional[str],
|
353
|
-
content: Any,
|
354
|
-
metadata: dict,
|
355
|
-
schema: Optional[dict],
|
356
|
-
version: Optional[str],
|
357
|
-
access_control: dict,
|
358
|
-
cache_ttl: int,
|
359
|
-
auto_notify: bool,
|
360
|
-
) -> Dict[str, Any]:
|
361
|
-
"""Update an existing MCP resource."""
|
362
|
-
if not uri:
|
363
|
-
return {"success": False, "error": "URI is required for update operation"}
|
364
|
-
|
365
|
-
# Mock resource lookup
|
366
|
-
existing_resource = {
|
367
|
-
"uri": uri,
|
368
|
-
"name": "Existing Resource",
|
369
|
-
"version": "1.0",
|
370
|
-
"created_at": "2025-06-01T10:00:00Z",
|
371
|
-
"content": "Previous content...",
|
372
|
-
"mimeType": "text/plain",
|
373
|
-
}
|
374
|
-
|
375
|
-
# Update fields
|
376
|
-
updates = {}
|
377
|
-
|
378
|
-
if content is not None:
|
379
|
-
content_str, mime_type = self._serialize_content(content)
|
380
|
-
updates["content"] = content_str
|
381
|
-
updates["mimeType"] = metadata.get("mimeType", mime_type)
|
382
|
-
updates["size"] = len(content_str) if isinstance(content_str, str) else 0
|
383
|
-
|
384
|
-
# Validate new content against schema if provided
|
385
|
-
if schema:
|
386
|
-
validation_result = self._validate_against_schema(content, schema)
|
387
|
-
if not validation_result["valid"]:
|
388
|
-
return {
|
389
|
-
"success": False,
|
390
|
-
"error": "Content validation failed",
|
391
|
-
"validation_errors": validation_result["errors"],
|
392
|
-
}
|
393
|
-
|
394
|
-
if metadata:
|
395
|
-
for key in ["name", "description", "tags", "category"]:
|
396
|
-
if key in metadata:
|
397
|
-
updates[key] = metadata[key]
|
398
|
-
|
399
|
-
if version:
|
400
|
-
updates["version"] = version
|
401
|
-
else:
|
402
|
-
# Auto-increment version
|
403
|
-
old_version = existing_resource["version"]
|
404
|
-
try:
|
405
|
-
version_num = float(old_version) + 0.1
|
406
|
-
updates["version"] = f"{version_num:.1f}"
|
407
|
-
except (ValueError, TypeError):
|
408
|
-
import datetime
|
409
|
-
|
410
|
-
updates["version"] = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
411
|
-
|
412
|
-
updates["updated_at"] = self._current_timestamp()
|
413
|
-
|
414
|
-
# Apply access control updates
|
415
|
-
if access_control:
|
416
|
-
updates["access_control"] = {
|
417
|
-
**existing_resource.get("access_control", {}),
|
418
|
-
**access_control,
|
419
|
-
}
|
420
|
-
|
421
|
-
# Create updated resource
|
422
|
-
updated_resource = {**existing_resource, **updates}
|
423
|
-
|
424
|
-
return {
|
425
|
-
"success": True,
|
426
|
-
"operation": "update",
|
427
|
-
"resource": updated_resource,
|
428
|
-
"changes": updates,
|
429
|
-
"previous_version": existing_resource["version"],
|
430
|
-
"notifications": {
|
431
|
-
"sent": auto_notify,
|
432
|
-
"subscribers_notified": 5 if auto_notify else 0,
|
433
|
-
"change_summary": f"Updated {len(updates)} fields",
|
434
|
-
},
|
435
|
-
}
|
436
|
-
|
437
|
-
def _delete_resource(self, uri: Optional[str], auto_notify: bool) -> Dict[str, Any]:
|
438
|
-
"""Delete an MCP resource."""
|
439
|
-
if not uri:
|
440
|
-
return {"success": False, "error": "URI is required for delete operation"}
|
441
|
-
|
442
|
-
# Mock resource deletion
|
443
|
-
deleted_resource = {
|
444
|
-
"uri": uri,
|
445
|
-
"name": "Deleted Resource",
|
446
|
-
"deleted_at": self._current_timestamp(),
|
447
|
-
"final_version": "2.1",
|
448
|
-
}
|
449
|
-
|
450
|
-
return {
|
451
|
-
"success": True,
|
452
|
-
"operation": "delete",
|
453
|
-
"deleted_resource": deleted_resource,
|
454
|
-
"cleanup": {
|
455
|
-
"cache_cleared": True,
|
456
|
-
"references_updated": 3,
|
457
|
-
"storage_freed": "1.2 MB",
|
458
|
-
},
|
459
|
-
"notifications": {
|
460
|
-
"sent": auto_notify,
|
461
|
-
"subscribers_notified": 7 if auto_notify else 0,
|
462
|
-
},
|
463
|
-
}
|
464
|
-
|
465
|
-
def _validate_resource(
|
466
|
-
self, uri: Optional[str], content: Any, schema: Optional[dict]
|
467
|
-
) -> Dict[str, Any]:
|
468
|
-
"""Validate a resource without creating or updating it."""
|
469
|
-
validation_results = {
|
470
|
-
"uri_valid": True,
|
471
|
-
"content_valid": True,
|
472
|
-
"schema_valid": True,
|
473
|
-
"errors": [],
|
474
|
-
"warnings": [],
|
475
|
-
}
|
476
|
-
|
477
|
-
# Validate URI
|
478
|
-
if uri:
|
479
|
-
if not self._validate_uri(uri):
|
480
|
-
validation_results["uri_valid"] = False
|
481
|
-
validation_results["errors"].append(f"Invalid URI format: {uri}")
|
482
|
-
else:
|
483
|
-
validation_results["errors"].append("URI is required for validation")
|
484
|
-
|
485
|
-
# Validate content
|
486
|
-
if content is not None:
|
487
|
-
try:
|
488
|
-
content_str, mime_type = self._serialize_content(content)
|
489
|
-
if not content_str:
|
490
|
-
validation_results["warnings"].append("Content is empty")
|
491
|
-
except Exception as e:
|
492
|
-
validation_results["content_valid"] = False
|
493
|
-
validation_results["errors"].append(
|
494
|
-
f"Content serialization failed: {e}"
|
495
|
-
)
|
496
|
-
|
497
|
-
# Validate against schema
|
498
|
-
if schema:
|
499
|
-
schema_validation = self._validate_against_schema(content, schema)
|
500
|
-
if not schema_validation["valid"]:
|
501
|
-
validation_results["schema_valid"] = False
|
502
|
-
validation_results["errors"].extend(schema_validation["errors"])
|
503
|
-
else:
|
504
|
-
validation_results["warnings"].append("No content provided for validation")
|
505
|
-
|
506
|
-
overall_valid = (
|
507
|
-
validation_results["uri_valid"]
|
508
|
-
and validation_results["content_valid"]
|
509
|
-
and validation_results["schema_valid"]
|
510
|
-
)
|
511
|
-
|
512
|
-
return {
|
513
|
-
"success": True,
|
514
|
-
"operation": "validate",
|
515
|
-
"valid": overall_valid,
|
516
|
-
"results": validation_results,
|
517
|
-
"summary": {
|
518
|
-
"total_errors": len(validation_results["errors"]),
|
519
|
-
"total_warnings": len(validation_results["warnings"]),
|
520
|
-
"recommendation": (
|
521
|
-
"Resource is valid"
|
522
|
-
if overall_valid
|
523
|
-
else "Fix errors before creating resource"
|
524
|
-
),
|
525
|
-
},
|
526
|
-
}
|
527
|
-
|
528
|
-
def _list_resources(self, filter_criteria: Optional[dict] = None) -> Dict[str, Any]:
|
529
|
-
"""List available MCP resources."""
|
530
|
-
# Mock resource listing
|
531
|
-
mock_resources = [
|
532
|
-
{
|
533
|
-
"uri": "workflow://examples/data/customer_analysis.txt",
|
534
|
-
"name": "Customer Analysis",
|
535
|
-
"mimeType": "text/plain",
|
536
|
-
"size": 1024,
|
537
|
-
"created_at": "2025-06-01T10:00:00Z",
|
538
|
-
"version": "1.0",
|
539
|
-
},
|
540
|
-
{
|
541
|
-
"uri": "data://reports/summary.json",
|
542
|
-
"name": "Summary Report",
|
543
|
-
"mimeType": "application/json",
|
544
|
-
"size": 512,
|
545
|
-
"created_at": "2025-06-01T11:00:00Z",
|
546
|
-
"version": "2.1",
|
547
|
-
},
|
548
|
-
{
|
549
|
-
"uri": "file:///tmp/output.csv",
|
550
|
-
"name": "Output Data",
|
551
|
-
"mimeType": "text/csv",
|
552
|
-
"size": 2048,
|
553
|
-
"created_at": "2025-06-01T12:00:00Z",
|
554
|
-
"version": "3.0",
|
555
|
-
},
|
556
|
-
]
|
557
|
-
|
558
|
-
# Apply filters if provided
|
559
|
-
if filter_criteria:
|
560
|
-
filtered_resources = []
|
561
|
-
for resource in mock_resources:
|
562
|
-
include = True
|
563
|
-
|
564
|
-
if "mimeType" in filter_criteria:
|
565
|
-
if resource["mimeType"] != filter_criteria["mimeType"]:
|
566
|
-
include = False
|
567
|
-
|
568
|
-
if "min_size" in filter_criteria:
|
569
|
-
if resource["size"] < filter_criteria["min_size"]:
|
570
|
-
include = False
|
571
|
-
|
572
|
-
if "name_contains" in filter_criteria:
|
573
|
-
if (
|
574
|
-
filter_criteria["name_contains"].lower()
|
575
|
-
not in resource["name"].lower()
|
576
|
-
):
|
577
|
-
include = False
|
578
|
-
|
579
|
-
if include:
|
580
|
-
filtered_resources.append(resource)
|
581
|
-
|
582
|
-
mock_resources = filtered_resources
|
583
|
-
|
584
|
-
return {
|
585
|
-
"success": True,
|
586
|
-
"operation": "list",
|
587
|
-
"resources": mock_resources,
|
588
|
-
"total_count": len(mock_resources),
|
589
|
-
"filter_applied": filter_criteria is not None,
|
590
|
-
"filter_criteria": filter_criteria,
|
591
|
-
"summary": {
|
592
|
-
"mime_types": list(set(r["mimeType"] for r in mock_resources)),
|
593
|
-
"total_size": sum(r["size"] for r in mock_resources),
|
594
|
-
"version_range": (
|
595
|
-
f"{min(r['version'] for r in mock_resources)} - {max(r['version'] for r in mock_resources)}"
|
596
|
-
if mock_resources
|
597
|
-
else "N/A"
|
598
|
-
),
|
599
|
-
},
|
600
|
-
}
|
601
|
-
|
602
|
-
def _validate_uri(self, uri: str) -> bool:
|
603
|
-
"""Validate MCP resource URI format."""
|
604
|
-
if not uri:
|
605
|
-
return False
|
606
|
-
|
607
|
-
# Check for scheme
|
608
|
-
if "://" not in uri:
|
609
|
-
return False
|
610
|
-
|
611
|
-
# Check for valid characters (basic validation)
|
612
|
-
invalid_chars = [" ", "\t", "\n", "\r"]
|
613
|
-
for char in invalid_chars:
|
614
|
-
if char in uri:
|
615
|
-
return False
|
616
|
-
|
617
|
-
return True
|
618
|
-
|
619
|
-
def _serialize_content(self, content: Any) -> tuple[str, str]:
|
620
|
-
"""Serialize content and determine MIME type."""
|
621
|
-
if isinstance(content, str):
|
622
|
-
return content, "text/plain"
|
623
|
-
elif isinstance(content, (dict, list)):
|
624
|
-
return json.dumps(content, indent=2), "application/json"
|
625
|
-
elif isinstance(content, bytes):
|
626
|
-
try:
|
627
|
-
return content.decode("utf-8"), "text/plain"
|
628
|
-
except UnicodeDecodeError:
|
629
|
-
return str(content), "application/octet-stream"
|
630
|
-
else:
|
631
|
-
return str(content), "text/plain"
|
632
|
-
|
633
|
-
def _validate_against_schema(self, content: Any, schema: dict) -> Dict[str, Any]:
|
634
|
-
"""Validate content against JSON schema."""
|
635
|
-
try:
|
636
|
-
# Mock schema validation (in real implementation, use jsonschema library)
|
637
|
-
if not isinstance(schema, dict):
|
638
|
-
return {"valid": False, "errors": ["Schema must be a dictionary"]}
|
639
|
-
|
640
|
-
# Basic type checking
|
641
|
-
schema_type = schema.get("type")
|
642
|
-
if schema_type:
|
643
|
-
if schema_type == "object" and not isinstance(content, dict):
|
644
|
-
return {
|
645
|
-
"valid": False,
|
646
|
-
"errors": [f"Expected object, got {type(content).__name__}"],
|
647
|
-
}
|
648
|
-
elif schema_type == "array" and not isinstance(content, list):
|
649
|
-
return {
|
650
|
-
"valid": False,
|
651
|
-
"errors": [f"Expected array, got {type(content).__name__}"],
|
652
|
-
}
|
653
|
-
elif schema_type == "string" and not isinstance(content, str):
|
654
|
-
return {
|
655
|
-
"valid": False,
|
656
|
-
"errors": [f"Expected string, got {type(content).__name__}"],
|
657
|
-
}
|
658
|
-
|
659
|
-
return {"valid": True, "errors": []}
|
660
|
-
|
661
|
-
except Exception as e:
|
662
|
-
return {"valid": False, "errors": [f"Schema validation error: {e}"]}
|
663
|
-
|
664
|
-
def _extract_name_from_uri(self, uri: str) -> str:
|
665
|
-
"""Extract a reasonable name from URI."""
|
666
|
-
# Extract the last part of the URI path
|
667
|
-
if "/" in uri:
|
668
|
-
name = uri.split("/")[-1]
|
669
|
-
else:
|
670
|
-
name = uri
|
671
|
-
|
672
|
-
# Remove file extension for display
|
673
|
-
if "." in name:
|
674
|
-
name = name.rsplit(".", 1)[0]
|
675
|
-
|
676
|
-
return name or "Unnamed Resource"
|
677
|
-
|
678
|
-
def _current_timestamp(self) -> str:
|
679
|
-
"""Get current timestamp in ISO format."""
|
680
|
-
import datetime
|
681
|
-
|
682
|
-
return datetime.datetime.now().isoformat() + "Z"
|