kailash 0.6.3__py3-none-any.whl → 0.6.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.
Files changed (120) hide show
  1. kailash/__init__.py +3 -3
  2. kailash/api/custom_nodes_secure.py +3 -3
  3. kailash/api/gateway.py +1 -1
  4. kailash/api/studio.py +2 -3
  5. kailash/api/workflow_api.py +3 -4
  6. kailash/core/resilience/bulkhead.py +460 -0
  7. kailash/core/resilience/circuit_breaker.py +92 -10
  8. kailash/edge/discovery.py +86 -0
  9. kailash/mcp_server/__init__.py +309 -33
  10. kailash/mcp_server/advanced_features.py +1022 -0
  11. kailash/mcp_server/ai_registry_server.py +27 -2
  12. kailash/mcp_server/auth.py +789 -0
  13. kailash/mcp_server/client.py +645 -378
  14. kailash/mcp_server/discovery.py +1593 -0
  15. kailash/mcp_server/errors.py +673 -0
  16. kailash/mcp_server/oauth.py +1727 -0
  17. kailash/mcp_server/protocol.py +1126 -0
  18. kailash/mcp_server/registry_integration.py +587 -0
  19. kailash/mcp_server/server.py +1213 -98
  20. kailash/mcp_server/transports.py +1169 -0
  21. kailash/mcp_server/utils/__init__.py +6 -1
  22. kailash/mcp_server/utils/cache.py +250 -7
  23. kailash/middleware/auth/auth_manager.py +3 -3
  24. kailash/middleware/communication/api_gateway.py +2 -9
  25. kailash/middleware/communication/realtime.py +1 -1
  26. kailash/middleware/mcp/enhanced_server.py +1 -1
  27. kailash/nodes/__init__.py +2 -0
  28. kailash/nodes/admin/audit_log.py +6 -6
  29. kailash/nodes/admin/permission_check.py +8 -8
  30. kailash/nodes/admin/role_management.py +32 -28
  31. kailash/nodes/admin/schema.sql +6 -1
  32. kailash/nodes/admin/schema_manager.py +13 -13
  33. kailash/nodes/admin/security_event.py +16 -20
  34. kailash/nodes/admin/tenant_isolation.py +3 -3
  35. kailash/nodes/admin/transaction_utils.py +3 -3
  36. kailash/nodes/admin/user_management.py +21 -22
  37. kailash/nodes/ai/a2a.py +11 -11
  38. kailash/nodes/ai/ai_providers.py +9 -12
  39. kailash/nodes/ai/embedding_generator.py +13 -14
  40. kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
  41. kailash/nodes/ai/iterative_llm_agent.py +2 -2
  42. kailash/nodes/ai/llm_agent.py +210 -33
  43. kailash/nodes/ai/self_organizing.py +2 -2
  44. kailash/nodes/alerts/discord.py +4 -4
  45. kailash/nodes/api/graphql.py +6 -6
  46. kailash/nodes/api/http.py +12 -17
  47. kailash/nodes/api/rate_limiting.py +4 -4
  48. kailash/nodes/api/rest.py +15 -15
  49. kailash/nodes/auth/mfa.py +3 -4
  50. kailash/nodes/auth/risk_assessment.py +2 -2
  51. kailash/nodes/auth/session_management.py +5 -5
  52. kailash/nodes/auth/sso.py +143 -0
  53. kailash/nodes/base.py +6 -2
  54. kailash/nodes/base_async.py +16 -2
  55. kailash/nodes/base_with_acl.py +2 -2
  56. kailash/nodes/cache/__init__.py +9 -0
  57. kailash/nodes/cache/cache.py +1172 -0
  58. kailash/nodes/cache/cache_invalidation.py +870 -0
  59. kailash/nodes/cache/redis_pool_manager.py +595 -0
  60. kailash/nodes/code/async_python.py +2 -1
  61. kailash/nodes/code/python.py +196 -35
  62. kailash/nodes/compliance/data_retention.py +6 -6
  63. kailash/nodes/compliance/gdpr.py +5 -5
  64. kailash/nodes/data/__init__.py +10 -0
  65. kailash/nodes/data/optimistic_locking.py +906 -0
  66. kailash/nodes/data/readers.py +8 -8
  67. kailash/nodes/data/redis.py +349 -0
  68. kailash/nodes/data/sql.py +314 -3
  69. kailash/nodes/data/streaming.py +21 -0
  70. kailash/nodes/enterprise/__init__.py +8 -0
  71. kailash/nodes/enterprise/audit_logger.py +285 -0
  72. kailash/nodes/enterprise/batch_processor.py +22 -3
  73. kailash/nodes/enterprise/data_lineage.py +1 -1
  74. kailash/nodes/enterprise/mcp_executor.py +205 -0
  75. kailash/nodes/enterprise/service_discovery.py +150 -0
  76. kailash/nodes/enterprise/tenant_assignment.py +108 -0
  77. kailash/nodes/logic/async_operations.py +2 -2
  78. kailash/nodes/logic/convergence.py +1 -1
  79. kailash/nodes/logic/operations.py +1 -1
  80. kailash/nodes/monitoring/__init__.py +11 -1
  81. kailash/nodes/monitoring/health_check.py +456 -0
  82. kailash/nodes/monitoring/log_processor.py +817 -0
  83. kailash/nodes/monitoring/metrics_collector.py +627 -0
  84. kailash/nodes/monitoring/performance_benchmark.py +137 -11
  85. kailash/nodes/rag/advanced.py +7 -7
  86. kailash/nodes/rag/agentic.py +49 -2
  87. kailash/nodes/rag/conversational.py +3 -3
  88. kailash/nodes/rag/evaluation.py +3 -3
  89. kailash/nodes/rag/federated.py +3 -3
  90. kailash/nodes/rag/graph.py +3 -3
  91. kailash/nodes/rag/multimodal.py +3 -3
  92. kailash/nodes/rag/optimized.py +5 -5
  93. kailash/nodes/rag/privacy.py +3 -3
  94. kailash/nodes/rag/query_processing.py +6 -6
  95. kailash/nodes/rag/realtime.py +1 -1
  96. kailash/nodes/rag/registry.py +2 -6
  97. kailash/nodes/rag/router.py +1 -1
  98. kailash/nodes/rag/similarity.py +7 -7
  99. kailash/nodes/rag/strategies.py +4 -4
  100. kailash/nodes/security/abac_evaluator.py +6 -6
  101. kailash/nodes/security/behavior_analysis.py +5 -6
  102. kailash/nodes/security/credential_manager.py +1 -1
  103. kailash/nodes/security/rotating_credentials.py +11 -11
  104. kailash/nodes/security/threat_detection.py +8 -8
  105. kailash/nodes/testing/credential_testing.py +2 -2
  106. kailash/nodes/transform/processors.py +5 -5
  107. kailash/runtime/local.py +162 -14
  108. kailash/runtime/parameter_injection.py +425 -0
  109. kailash/runtime/parameter_injector.py +657 -0
  110. kailash/runtime/testing.py +2 -2
  111. kailash/testing/fixtures.py +2 -2
  112. kailash/workflow/builder.py +99 -18
  113. kailash/workflow/builder_improvements.py +207 -0
  114. kailash/workflow/input_handling.py +170 -0
  115. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/METADATA +22 -9
  116. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/RECORD +120 -94
  117. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/WHEEL +0 -0
  118. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/entry_points.txt +0 -0
  119. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/licenses/LICENSE +0 -0
  120. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1022 @@
1
+ """
2
+ Advanced MCP Features Implementation.
3
+
4
+ This module implements advanced MCP features including structured outputs,
5
+ resource templates, streaming support, progress reporting, and other
6
+ sophisticated protocol features that extend the basic MCP functionality.
7
+
8
+ Features:
9
+ - Structured tool outputs with JSON Schema validation
10
+ - Resource templates with URI template patterns
11
+ - Resource subscriptions and change notifications
12
+ - Multi-modal content support (text, images, audio)
13
+ - Binary resource handling with Base64 encoding
14
+ - Progress reporting for long-running operations
15
+ - Request cancellation and cleanup
16
+ - Tool annotations and metadata
17
+ - Content streaming for large responses
18
+ - Elicitation system for interactive user input
19
+
20
+ Examples:
21
+ Structured tool with validation:
22
+
23
+ >>> from kailash.mcp_server.advanced_features import StructuredTool
24
+ >>>
25
+ >>> @StructuredTool(
26
+ ... output_schema={
27
+ ... "type": "object",
28
+ ... "properties": {
29
+ ... "results": {"type": "array"},
30
+ ... "count": {"type": "integer"}
31
+ ... },
32
+ ... "required": ["results", "count"]
33
+ ... }
34
+ ... )
35
+ ... def search_tool(query: str) -> dict:
36
+ ... return {"results": ["item1", "item2"], "count": 2}
37
+
38
+ Resource template with dynamic URIs:
39
+
40
+ >>> from kailash.mcp_server.advanced_features import ResourceTemplate
41
+ >>>
42
+ >>> template = ResourceTemplate(
43
+ ... uri_template="files://{path}",
44
+ ... name="File Access",
45
+ ... description="Access files by path"
46
+ ... )
47
+ >>>
48
+ >>> # Subscribe to resource changes
49
+ >>> subscription = await template.subscribe(
50
+ ... uri="files://documents/report.pdf",
51
+ ... callback=lambda change: print(f"File changed: {change}")
52
+ ... )
53
+
54
+ Multi-modal content:
55
+
56
+ >>> from kailash.mcp_server.advanced_features import MultiModalContent
57
+ >>>
58
+ >>> content = MultiModalContent()
59
+ >>> content.add_text("Here is the analysis:")
60
+ >>> content.add_image(image_data, "image/png")
61
+ >>> content.add_resource("files://data.csv", "text/csv")
62
+ """
63
+
64
+ import asyncio
65
+ import base64
66
+ import json
67
+ import logging
68
+ import mimetypes
69
+ import time
70
+ import uuid
71
+ from abc import ABC, abstractmethod
72
+ from dataclasses import asdict, dataclass, field
73
+ from enum import Enum
74
+ from pathlib import Path
75
+ from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
76
+ from urllib.parse import urlparse
77
+
78
+ import jsonschema
79
+
80
+ from .errors import MCPError, MCPErrorCode, ValidationError
81
+ from .protocol import ProgressToken, get_protocol_manager
82
+
83
+ logger = logging.getLogger(__name__)
84
+
85
+
86
+ class ContentType(Enum):
87
+ """Content types for multi-modal content."""
88
+
89
+ TEXT = "text"
90
+ IMAGE = "image"
91
+ AUDIO = "audio"
92
+ RESOURCE = "resource"
93
+ ANNOTATION = "annotation"
94
+
95
+
96
+ class ChangeType(Enum):
97
+ """Resource change types."""
98
+
99
+ CREATED = "created"
100
+ UPDATED = "updated"
101
+ DELETED = "deleted"
102
+
103
+
104
+ @dataclass
105
+ class Content:
106
+ """Multi-modal content item."""
107
+
108
+ type: ContentType
109
+ data: Any
110
+ mime_type: Optional[str] = None
111
+ annotations: Dict[str, Any] = field(default_factory=dict)
112
+
113
+ def to_dict(self) -> Dict[str, Any]:
114
+ """Convert to dictionary format."""
115
+ result = {
116
+ "type": self.type.value,
117
+ }
118
+
119
+ if self.type == ContentType.TEXT:
120
+ result["text"] = self.data
121
+ elif self.type == ContentType.IMAGE:
122
+ result["data"] = self.data
123
+ if self.mime_type:
124
+ result["mimeType"] = self.mime_type
125
+ elif self.type == ContentType.AUDIO:
126
+ result["data"] = self.data
127
+ if self.mime_type:
128
+ result["mimeType"] = self.mime_type
129
+ elif self.type == ContentType.RESOURCE:
130
+ result["resource"] = self.data
131
+ elif self.type == ContentType.ANNOTATION:
132
+ result["annotation"] = self.data
133
+
134
+ if self.annotations:
135
+ result["annotations"] = self.annotations
136
+
137
+ return result
138
+
139
+
140
+ @dataclass
141
+ class ResourceChange:
142
+ """Resource change notification."""
143
+
144
+ uri: str
145
+ change_type: ChangeType
146
+ content: Optional[Dict[str, Any]] = None
147
+ timestamp: float = field(default_factory=time.time)
148
+ metadata: Dict[str, Any] = field(default_factory=dict)
149
+
150
+ def to_dict(self) -> Dict[str, Any]:
151
+ """Convert to dictionary format."""
152
+ return {
153
+ "uri": self.uri,
154
+ "type": self.change_type.value,
155
+ "content": self.content,
156
+ "timestamp": self.timestamp,
157
+ "metadata": self.metadata,
158
+ }
159
+
160
+
161
+ @dataclass
162
+ class ToolAnnotation:
163
+ """Tool annotation for metadata."""
164
+
165
+ is_read_only: bool = False
166
+ is_destructive: bool = False
167
+ is_idempotent: bool = True
168
+ estimated_duration: Optional[float] = None
169
+ requires_confirmation: bool = False
170
+ security_level: str = "normal" # normal, elevated, admin
171
+ rate_limit: Optional[Dict[str, Any]] = None
172
+
173
+ def to_dict(self) -> Dict[str, Any]:
174
+ """Convert to dictionary format."""
175
+ return asdict(self)
176
+
177
+
178
+ class MultiModalContent:
179
+ """Multi-modal content container."""
180
+
181
+ def __init__(self):
182
+ """Initialize multi-modal content."""
183
+ self.content_items: List[Content] = []
184
+
185
+ def add_text(self, text: str, annotations: Optional[Dict[str, Any]] = None) -> None:
186
+ """Add text content.
187
+
188
+ Args:
189
+ text: Text content
190
+ annotations: Optional annotations
191
+ """
192
+ self.content_items.append(
193
+ Content(type=ContentType.TEXT, data=text, annotations=annotations or {})
194
+ )
195
+
196
+ def add_image(
197
+ self,
198
+ image_data: Union[str, bytes],
199
+ mime_type: str,
200
+ annotations: Optional[Dict[str, Any]] = None,
201
+ ) -> None:
202
+ """Add image content.
203
+
204
+ Args:
205
+ image_data: Image data (base64 string or bytes)
206
+ mime_type: Image MIME type
207
+ annotations: Optional annotations
208
+ """
209
+ if isinstance(image_data, bytes):
210
+ image_data = base64.b64encode(image_data).decode()
211
+
212
+ self.content_items.append(
213
+ Content(
214
+ type=ContentType.IMAGE,
215
+ data=image_data,
216
+ mime_type=mime_type,
217
+ annotations=annotations or {},
218
+ )
219
+ )
220
+
221
+ def add_audio(
222
+ self,
223
+ audio_data: Union[str, bytes],
224
+ mime_type: str,
225
+ annotations: Optional[Dict[str, Any]] = None,
226
+ ) -> None:
227
+ """Add audio content.
228
+
229
+ Args:
230
+ audio_data: Audio data (base64 string or bytes)
231
+ mime_type: Audio MIME type
232
+ annotations: Optional annotations
233
+ """
234
+ if isinstance(audio_data, bytes):
235
+ audio_data = base64.b64encode(audio_data).decode()
236
+
237
+ self.content_items.append(
238
+ Content(
239
+ type=ContentType.AUDIO,
240
+ data=audio_data,
241
+ mime_type=mime_type,
242
+ annotations=annotations or {},
243
+ )
244
+ )
245
+
246
+ def add_resource(
247
+ self,
248
+ uri: str,
249
+ text: Optional[str] = None,
250
+ mime_type: Optional[str] = None,
251
+ annotations: Optional[Dict[str, Any]] = None,
252
+ ) -> None:
253
+ """Add resource reference.
254
+
255
+ Args:
256
+ uri: Resource URI
257
+ text: Optional text content
258
+ mime_type: Optional MIME type
259
+ annotations: Optional annotations
260
+ """
261
+ resource_data = {"uri": uri}
262
+ if text:
263
+ resource_data["text"] = text
264
+ if mime_type:
265
+ resource_data["mimeType"] = mime_type
266
+
267
+ self.content_items.append(
268
+ Content(
269
+ type=ContentType.RESOURCE,
270
+ data=resource_data,
271
+ annotations=annotations or {},
272
+ )
273
+ )
274
+
275
+ def add_annotation(
276
+ self,
277
+ annotation_type: str,
278
+ data: Dict[str, Any],
279
+ annotations: Optional[Dict[str, Any]] = None,
280
+ ) -> None:
281
+ """Add annotation content.
282
+
283
+ Args:
284
+ annotation_type: Type of annotation
285
+ data: Annotation data
286
+ annotations: Optional meta-annotations
287
+ """
288
+ annotation_data = {"type": annotation_type, "data": data}
289
+
290
+ self.content_items.append(
291
+ Content(
292
+ type=ContentType.ANNOTATION,
293
+ data=annotation_data,
294
+ annotations=annotations or {},
295
+ )
296
+ )
297
+
298
+ def to_list(self) -> List[Dict[str, Any]]:
299
+ """Convert to list format for MCP protocol.
300
+
301
+ Returns:
302
+ List of content dictionaries
303
+ """
304
+ return [item.to_dict() for item in self.content_items]
305
+
306
+ def is_empty(self) -> bool:
307
+ """Check if content is empty."""
308
+ return len(self.content_items) == 0
309
+
310
+
311
+ class SchemaValidator:
312
+ """JSON Schema validator for tool outputs and inputs."""
313
+
314
+ def __init__(self, schema: Dict[str, Any]):
315
+ """Initialize schema validator.
316
+
317
+ Args:
318
+ schema: JSON Schema definition
319
+ """
320
+ self.schema = schema
321
+ self._validator = jsonschema.Draft7Validator(schema)
322
+
323
+ def validate(self, data: Any) -> None:
324
+ """Validate data against schema.
325
+
326
+ Args:
327
+ data: Data to validate
328
+
329
+ Raises:
330
+ ValidationError: If validation fails
331
+ """
332
+ errors = list(self._validator.iter_errors(data))
333
+ if errors:
334
+ error_messages = [
335
+ f"{'.'.join(str(p) for p in error.absolute_path)}: {error.message}"
336
+ for error in errors
337
+ ]
338
+ raise ValidationError(
339
+ f"Schema validation failed: {'; '.join(error_messages)}"
340
+ )
341
+
342
+ def is_valid(self, data: Any) -> bool:
343
+ """Check if data is valid.
344
+
345
+ Args:
346
+ data: Data to check
347
+
348
+ Returns:
349
+ True if valid
350
+ """
351
+ try:
352
+ self.validate(data)
353
+ return True
354
+ except ValidationError:
355
+ return False
356
+
357
+
358
+ class StructuredTool:
359
+ """Tool with structured input/output validation."""
360
+
361
+ def __init__(
362
+ self,
363
+ input_schema: Optional[Dict[str, Any]] = None,
364
+ output_schema: Optional[Dict[str, Any]] = None,
365
+ annotations: Optional[ToolAnnotation] = None,
366
+ progress_reporting: bool = False,
367
+ ):
368
+ """Initialize structured tool.
369
+
370
+ Args:
371
+ input_schema: JSON Schema for input validation
372
+ output_schema: JSON Schema for output validation
373
+ annotations: Tool annotations
374
+ progress_reporting: Enable progress reporting
375
+ """
376
+ self.input_schema = input_schema
377
+ self.output_schema = output_schema
378
+ self.annotations = annotations or ToolAnnotation()
379
+ self.progress_reporting = progress_reporting
380
+
381
+ # Create validators
382
+ self.input_validator = SchemaValidator(input_schema) if input_schema else None
383
+ self.output_validator = (
384
+ SchemaValidator(output_schema) if output_schema else None
385
+ )
386
+
387
+ def __call__(self, func: Callable) -> Callable:
388
+ """Decorator to wrap function with validation."""
389
+
390
+ async def async_wrapper(*args, **kwargs):
391
+ # Input validation
392
+ if self.input_validator and kwargs:
393
+ try:
394
+ self.input_validator.validate(kwargs)
395
+ except ValidationError as e:
396
+ raise MCPError(
397
+ f"Input validation failed: {e}",
398
+ error_code=MCPErrorCode.INVALID_PARAMS,
399
+ )
400
+
401
+ # Progress reporting setup
402
+ progress_token = None
403
+ if self.progress_reporting:
404
+ protocol = get_protocol_manager()
405
+ progress_token = protocol.progress.start_progress(func.__name__)
406
+ kwargs["progress_token"] = progress_token
407
+
408
+ try:
409
+ # Execute function
410
+ result = await func(*args, **kwargs)
411
+
412
+ # Output validation
413
+ if self.output_validator:
414
+ try:
415
+ self.output_validator.validate(result)
416
+ except ValidationError as e:
417
+ raise MCPError(
418
+ f"Output validation failed: {e}",
419
+ error_code=MCPErrorCode.INTERNAL_ERROR,
420
+ )
421
+
422
+ # Complete progress
423
+ if progress_token:
424
+ await protocol.progress.complete_progress(
425
+ progress_token, "completed"
426
+ )
427
+
428
+ return result
429
+
430
+ except Exception as e:
431
+ # Error handling
432
+ if progress_token:
433
+ await protocol.progress.complete_progress(
434
+ progress_token, f"failed: {str(e)}"
435
+ )
436
+ raise
437
+
438
+ def sync_wrapper(*args, **kwargs):
439
+ # Input validation
440
+ if self.input_validator and kwargs:
441
+ try:
442
+ self.input_validator.validate(kwargs)
443
+ except ValidationError as e:
444
+ raise MCPError(
445
+ f"Input validation failed: {e}",
446
+ error_code=MCPErrorCode.INVALID_PARAMS,
447
+ )
448
+
449
+ # Execute function
450
+ result = func(*args, **kwargs)
451
+
452
+ # Output validation
453
+ if self.output_validator:
454
+ try:
455
+ self.output_validator.validate(result)
456
+ except ValidationError as e:
457
+ raise MCPError(
458
+ f"Output validation failed: {e}",
459
+ error_code=MCPErrorCode.INTERNAL_ERROR,
460
+ )
461
+
462
+ return result
463
+
464
+ # Return appropriate wrapper
465
+ if asyncio.iscoroutinefunction(func):
466
+ return async_wrapper
467
+ else:
468
+ return sync_wrapper
469
+
470
+
471
+ class ResourceTemplate:
472
+ """Resource template with URI patterns and subscriptions."""
473
+
474
+ def __init__(
475
+ self,
476
+ uri_template: str,
477
+ name: Optional[str] = None,
478
+ description: Optional[str] = None,
479
+ mime_type: Optional[str] = None,
480
+ supports_subscription: bool = True,
481
+ ):
482
+ """Initialize resource template.
483
+
484
+ Args:
485
+ uri_template: URI template pattern
486
+ name: Template name
487
+ description: Template description
488
+ mime_type: Default MIME type
489
+ supports_subscription: Support change subscriptions
490
+ """
491
+ self.uri_template = uri_template
492
+ self.name = name
493
+ self.description = description
494
+ self.mime_type = mime_type
495
+ self.supports_subscription = supports_subscription
496
+
497
+ # Subscription management
498
+ self._subscriptions: Dict[str, List[Callable]] = {}
499
+ self._subscription_ids: Dict[str, str] = {}
500
+
501
+ def matches_uri(self, uri: str) -> bool:
502
+ """Check if URI matches this template.
503
+
504
+ Args:
505
+ uri: URI to check
506
+
507
+ Returns:
508
+ True if matches template
509
+ """
510
+ # Simple pattern matching - check scheme first
511
+ if "://" in self.uri_template and "://" in uri:
512
+ template_scheme = self.uri_template.split("://")[0]
513
+ uri_scheme = uri.split("://")[0]
514
+
515
+ if template_scheme != uri_scheme:
516
+ return False
517
+
518
+ # For basic matching, just check if URI starts with the template prefix
519
+ # (without the variable parts)
520
+ template_prefix = self.uri_template.split("{")[0]
521
+ return uri.startswith(template_prefix)
522
+
523
+ async def subscribe(
524
+ self, uri: str, callback: Callable[[ResourceChange], None]
525
+ ) -> str:
526
+ """Subscribe to resource changes.
527
+
528
+ Args:
529
+ uri: Resource URI to monitor
530
+ callback: Change notification callback
531
+
532
+ Returns:
533
+ Subscription ID
534
+ """
535
+ if not self.supports_subscription:
536
+ raise MCPError(
537
+ "Resource does not support subscriptions",
538
+ error_code=MCPErrorCode.METHOD_NOT_FOUND,
539
+ )
540
+
541
+ if not self.matches_uri(uri):
542
+ raise MCPError(
543
+ "URI does not match template", error_code=MCPErrorCode.INVALID_PARAMS
544
+ )
545
+
546
+ subscription_id = str(uuid.uuid4())
547
+
548
+ if uri not in self._subscriptions:
549
+ self._subscriptions[uri] = []
550
+
551
+ self._subscriptions[uri].append(callback)
552
+ self._subscription_ids[subscription_id] = uri
553
+
554
+ logger.info(f"Created subscription {subscription_id} for {uri}")
555
+ return subscription_id
556
+
557
+ async def unsubscribe(self, subscription_id: str) -> bool:
558
+ """Unsubscribe from resource changes.
559
+
560
+ Args:
561
+ subscription_id: Subscription ID to remove
562
+
563
+ Returns:
564
+ True if subscription was removed
565
+ """
566
+ if subscription_id not in self._subscription_ids:
567
+ return False
568
+
569
+ uri = self._subscription_ids[subscription_id]
570
+
571
+ # Find and remove callback (simplified - in production, track callback references)
572
+ if uri in self._subscriptions and self._subscriptions[uri]:
573
+ self._subscriptions[uri].pop() # Remove last callback (simplified)
574
+
575
+ if not self._subscriptions[uri]:
576
+ del self._subscriptions[uri]
577
+
578
+ del self._subscription_ids[subscription_id]
579
+
580
+ logger.info(f"Removed subscription {subscription_id} for {uri}")
581
+ return True
582
+
583
+ async def notify_change(self, change: ResourceChange) -> None:
584
+ """Notify subscribers of resource change.
585
+
586
+ Args:
587
+ change: Resource change details
588
+ """
589
+ uri = change.uri
590
+ if uri not in self._subscriptions:
591
+ return
592
+
593
+ # Notify all subscribers
594
+ for callback in self._subscriptions[uri]:
595
+ try:
596
+ if asyncio.iscoroutinefunction(callback):
597
+ await callback(change)
598
+ else:
599
+ callback(change)
600
+ except Exception as e:
601
+ logger.error(f"Subscription callback error: {e}")
602
+
603
+ def to_dict(self) -> Dict[str, Any]:
604
+ """Convert to dictionary format."""
605
+ result = {"uriTemplate": self.uri_template}
606
+
607
+ if self.name:
608
+ result["name"] = self.name
609
+ if self.description:
610
+ result["description"] = self.description
611
+ if self.mime_type:
612
+ result["mimeType"] = self.mime_type
613
+
614
+ return result
615
+
616
+
617
+ class BinaryResourceHandler:
618
+ """Handler for binary resources with Base64 encoding."""
619
+
620
+ def __init__(self, max_size: int = 10_000_000): # 10MB default
621
+ """Initialize binary resource handler.
622
+
623
+ Args:
624
+ max_size: Maximum file size in bytes
625
+ """
626
+ self.max_size = max_size
627
+
628
+ async def read_binary_file(self, file_path: Union[str, Path]) -> Dict[str, Any]:
629
+ """Read binary file and encode as Base64.
630
+
631
+ Args:
632
+ file_path: Path to binary file
633
+
634
+ Returns:
635
+ Resource content with Base64 data
636
+ """
637
+ file_path = Path(file_path)
638
+
639
+ if not file_path.exists():
640
+ raise MCPError(
641
+ f"File not found: {file_path}",
642
+ error_code=MCPErrorCode.RESOURCE_NOT_FOUND,
643
+ )
644
+
645
+ file_size = file_path.stat().st_size
646
+ if file_size > self.max_size:
647
+ raise MCPError(
648
+ f"File too large: {file_size} bytes",
649
+ error_code=MCPErrorCode.INVALID_PARAMS,
650
+ )
651
+
652
+ # Read file and encode
653
+ with open(file_path, "rb") as f:
654
+ binary_data = f.read()
655
+
656
+ encoded_data = base64.b64encode(binary_data).decode()
657
+
658
+ # Detect MIME type
659
+ mime_type, _ = mimetypes.guess_type(str(file_path))
660
+ if not mime_type:
661
+ mime_type = "application/octet-stream"
662
+
663
+ return {
664
+ "uri": f"file://{file_path.absolute()}",
665
+ "mimeType": mime_type,
666
+ "blob": encoded_data,
667
+ }
668
+
669
+ def decode_base64_content(self, encoded_data: str) -> bytes:
670
+ """Decode Base64 content to bytes.
671
+
672
+ Args:
673
+ encoded_data: Base64 encoded data
674
+
675
+ Returns:
676
+ Binary data
677
+ """
678
+ try:
679
+ return base64.b64decode(encoded_data)
680
+ except Exception as e:
681
+ raise MCPError(
682
+ f"Invalid Base64 data: {e}", error_code=MCPErrorCode.INVALID_PARAMS
683
+ )
684
+
685
+
686
+ class StreamingHandler:
687
+ """Handler for streaming large responses."""
688
+
689
+ def __init__(self, chunk_size: int = 8192):
690
+ """Initialize streaming handler.
691
+
692
+ Args:
693
+ chunk_size: Size of each chunk in bytes
694
+ """
695
+ self.chunk_size = chunk_size
696
+
697
+ async def stream_text(self, text: str) -> AsyncGenerator[str, None]:
698
+ """Stream text content in chunks.
699
+
700
+ Args:
701
+ text: Text to stream
702
+
703
+ Yields:
704
+ Text chunks
705
+ """
706
+ for i in range(0, len(text), self.chunk_size):
707
+ chunk = text[i : i + self.chunk_size]
708
+ yield chunk
709
+ await asyncio.sleep(0) # Allow other tasks to run
710
+
711
+ async def stream_binary(self, data: bytes) -> AsyncGenerator[str, None]:
712
+ """Stream binary content as Base64 chunks.
713
+
714
+ Args:
715
+ data: Binary data to stream
716
+
717
+ Yields:
718
+ Base64 encoded chunks
719
+ """
720
+ for i in range(0, len(data), self.chunk_size):
721
+ chunk = data[i : i + self.chunk_size]
722
+ encoded_chunk = base64.b64encode(chunk).decode()
723
+ yield encoded_chunk
724
+ await asyncio.sleep(0) # Allow other tasks to run
725
+
726
+ async def stream_file(
727
+ self, file_path: Union[str, Path]
728
+ ) -> AsyncGenerator[str, None]:
729
+ """Stream file content as Base64 chunks.
730
+
731
+ Args:
732
+ file_path: Path to file
733
+
734
+ Yields:
735
+ Base64 encoded chunks
736
+ """
737
+ file_path = Path(file_path)
738
+
739
+ if not file_path.exists():
740
+ raise MCPError(
741
+ f"File not found: {file_path}",
742
+ error_code=MCPErrorCode.RESOURCE_NOT_FOUND,
743
+ )
744
+
745
+ with open(file_path, "rb") as f:
746
+ while True:
747
+ chunk = f.read(self.chunk_size)
748
+ if not chunk:
749
+ break
750
+
751
+ encoded_chunk = base64.b64encode(chunk).decode()
752
+ yield encoded_chunk
753
+ await asyncio.sleep(0) # Allow other tasks to run
754
+
755
+
756
+ class ElicitationSystem:
757
+ """Interactive user input collection system."""
758
+
759
+ def __init__(self):
760
+ """Initialize elicitation system."""
761
+ self._pending_requests: Dict[str, Dict[str, Any]] = {}
762
+ self._response_callbacks: Dict[str, Callable] = {}
763
+
764
+ async def request_input(
765
+ self,
766
+ prompt: str,
767
+ input_schema: Optional[Dict[str, Any]] = None,
768
+ timeout: Optional[float] = 300.0,
769
+ ) -> Any:
770
+ """Request input from user with schema validation.
771
+
772
+ Args:
773
+ prompt: Input prompt for user
774
+ input_schema: JSON Schema for input validation
775
+ timeout: Request timeout in seconds
776
+
777
+ Returns:
778
+ User input
779
+ """
780
+ request_id = str(uuid.uuid4())
781
+
782
+ # Store request
783
+ self._pending_requests[request_id] = {
784
+ "prompt": prompt,
785
+ "schema": input_schema,
786
+ "timestamp": time.time(),
787
+ }
788
+
789
+ # Create future for response
790
+ response_future = asyncio.Future()
791
+ self._response_callbacks[request_id] = lambda data: response_future.set_result(
792
+ data
793
+ )
794
+
795
+ try:
796
+ # Send elicitation request (would be sent to client in real implementation)
797
+ await self._send_elicitation_request(request_id, prompt, input_schema)
798
+
799
+ # Wait for response
800
+ if timeout:
801
+ response = await asyncio.wait_for(response_future, timeout=timeout)
802
+ else:
803
+ response = await response_future
804
+
805
+ # Validate response
806
+ if input_schema:
807
+ validator = SchemaValidator(input_schema)
808
+ validator.validate(response)
809
+
810
+ return response
811
+
812
+ except asyncio.TimeoutError:
813
+ raise MCPError(
814
+ "Input request timed out", error_code=MCPErrorCode.REQUEST_TIMEOUT
815
+ )
816
+ finally:
817
+ # Clean up
818
+ self._pending_requests.pop(request_id, None)
819
+ self._response_callbacks.pop(request_id, None)
820
+
821
+ async def provide_input(self, request_id: str, input_data: Any) -> bool:
822
+ """Provide input for pending request.
823
+
824
+ Args:
825
+ request_id: Request ID
826
+ input_data: User input data
827
+
828
+ Returns:
829
+ True if input was accepted
830
+ """
831
+ if request_id not in self._pending_requests:
832
+ return False
833
+
834
+ callback = self._response_callbacks.get(request_id)
835
+ if callback:
836
+ callback(input_data)
837
+ return True
838
+
839
+ return False
840
+
841
+ async def _send_elicitation_request(
842
+ self, request_id: str, prompt: str, schema: Optional[Dict[str, Any]]
843
+ ) -> None:
844
+ """Send elicitation request to client.
845
+
846
+ Args:
847
+ request_id: Request ID
848
+ prompt: Input prompt
849
+ schema: Input schema
850
+ """
851
+ # In a real implementation, this would send the request to the MCP client
852
+ # For now, we'll just log it
853
+ logger.info(f"Elicitation request {request_id}: {prompt}")
854
+
855
+ # Simulate automatic response for testing
856
+ if prompt.lower().startswith("test"):
857
+ await asyncio.sleep(0.1) # Simulate user thinking time
858
+ await self.provide_input(request_id, "test response")
859
+
860
+
861
+ class ProgressReporter:
862
+ """Enhanced progress reporting for long-running operations."""
863
+
864
+ def __init__(self, operation_name: str, total: Optional[float] = None):
865
+ """Initialize progress reporter.
866
+
867
+ Args:
868
+ operation_name: Name of the operation
869
+ total: Total progress units (if known)
870
+ """
871
+ self.operation_name = operation_name
872
+ self.total = total
873
+ self.current = 0.0
874
+ self.status = "started"
875
+
876
+ # Get progress token from protocol manager
877
+ protocol = get_protocol_manager()
878
+ self.progress_token = protocol.progress.start_progress(operation_name, total)
879
+
880
+ async def update(
881
+ self,
882
+ progress: Optional[float] = None,
883
+ status: Optional[str] = None,
884
+ increment: Optional[float] = None,
885
+ ) -> None:
886
+ """Update progress.
887
+
888
+ Args:
889
+ progress: Current progress value
890
+ status: Status message
891
+ increment: Amount to increment progress
892
+ """
893
+ protocol = get_protocol_manager()
894
+
895
+ if progress is not None:
896
+ self.current = progress
897
+ elif increment is not None:
898
+ self.current += increment
899
+
900
+ if status is not None:
901
+ self.status = status
902
+
903
+ await protocol.progress.update_progress(
904
+ self.progress_token,
905
+ progress=self.current,
906
+ status=self.status,
907
+ increment=increment,
908
+ )
909
+
910
+ async def complete(self, status: str = "completed") -> None:
911
+ """Complete progress reporting.
912
+
913
+ Args:
914
+ status: Final status
915
+ """
916
+ protocol = get_protocol_manager()
917
+ await protocol.progress.complete_progress(self.progress_token, status)
918
+
919
+ async def __aenter__(self):
920
+ """Async context manager entry."""
921
+ return self
922
+
923
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
924
+ """Async context manager exit."""
925
+ if exc_type:
926
+ await self.complete(f"failed: {exc_val}")
927
+ else:
928
+ await self.complete()
929
+
930
+
931
+ class CancellationContext:
932
+ """Context for handling request cancellation."""
933
+
934
+ def __init__(self, request_id: str):
935
+ """Initialize cancellation context.
936
+
937
+ Args:
938
+ request_id: Request ID to monitor for cancellation
939
+ """
940
+ self.request_id = request_id
941
+ self._cleanup_functions: List[Callable] = []
942
+
943
+ def is_cancelled(self) -> bool:
944
+ """Check if request is cancelled."""
945
+ protocol = get_protocol_manager()
946
+ return protocol.cancellation.is_cancelled(self.request_id)
947
+
948
+ def check_cancellation(self) -> None:
949
+ """Check for cancellation and raise exception if cancelled."""
950
+ if self.is_cancelled():
951
+ raise MCPError(
952
+ "Operation was cancelled", error_code=MCPErrorCode.REQUEST_CANCELLED
953
+ )
954
+
955
+ def add_cleanup(self, cleanup_func: Callable) -> None:
956
+ """Add cleanup function.
957
+
958
+ Args:
959
+ cleanup_func: Function to call on cancellation
960
+ """
961
+ self._cleanup_functions.append(cleanup_func)
962
+
963
+ # Register with protocol manager
964
+ protocol = get_protocol_manager()
965
+ protocol.cancellation.add_cleanup_function(self.request_id, cleanup_func)
966
+
967
+ async def __aenter__(self):
968
+ """Async context manager entry."""
969
+ return self
970
+
971
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
972
+ """Async context manager exit."""
973
+ # Cleanup is handled by protocol manager
974
+ pass
975
+
976
+
977
+ # Convenience functions
978
+ def structured_tool(
979
+ input_schema: Optional[Dict[str, Any]] = None,
980
+ output_schema: Optional[Dict[str, Any]] = None,
981
+ annotations: Optional[ToolAnnotation] = None,
982
+ progress_reporting: bool = False,
983
+ ):
984
+ """Decorator for creating structured tools.
985
+
986
+ Args:
987
+ input_schema: JSON Schema for input validation
988
+ output_schema: JSON Schema for output validation
989
+ annotations: Tool annotations
990
+ progress_reporting: Enable progress reporting
991
+
992
+ Returns:
993
+ Tool decorator
994
+ """
995
+ return StructuredTool(input_schema, output_schema, annotations, progress_reporting)
996
+
997
+
998
+ async def create_progress_reporter(
999
+ operation_name: str, total: Optional[float] = None
1000
+ ) -> ProgressReporter:
1001
+ """Create progress reporter.
1002
+
1003
+ Args:
1004
+ operation_name: Operation name
1005
+ total: Total progress units
1006
+
1007
+ Returns:
1008
+ Progress reporter
1009
+ """
1010
+ return ProgressReporter(operation_name, total)
1011
+
1012
+
1013
+ def create_cancellation_context(request_id: str) -> CancellationContext:
1014
+ """Create cancellation context.
1015
+
1016
+ Args:
1017
+ request_id: Request ID
1018
+
1019
+ Returns:
1020
+ Cancellation context
1021
+ """
1022
+ return CancellationContext(request_id)