kailash 0.6.3__py3-none-any.whl → 0.6.5__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 +3 -3
- kailash/api/custom_nodes_secure.py +3 -3
- kailash/api/gateway.py +1 -1
- kailash/api/studio.py +1 -1
- kailash/api/workflow_api.py +2 -2
- kailash/core/resilience/bulkhead.py +475 -0
- kailash/core/resilience/circuit_breaker.py +92 -10
- kailash/core/resilience/health_monitor.py +578 -0
- kailash/edge/discovery.py +86 -0
- kailash/mcp_server/__init__.py +309 -33
- kailash/mcp_server/advanced_features.py +1022 -0
- kailash/mcp_server/ai_registry_server.py +27 -2
- kailash/mcp_server/auth.py +789 -0
- kailash/mcp_server/client.py +645 -378
- kailash/mcp_server/discovery.py +1593 -0
- kailash/mcp_server/errors.py +673 -0
- kailash/mcp_server/oauth.py +1727 -0
- kailash/mcp_server/protocol.py +1126 -0
- kailash/mcp_server/registry_integration.py +587 -0
- kailash/mcp_server/server.py +1228 -96
- kailash/mcp_server/transports.py +1169 -0
- kailash/mcp_server/utils/__init__.py +6 -1
- kailash/mcp_server/utils/cache.py +250 -7
- kailash/middleware/auth/auth_manager.py +3 -3
- kailash/middleware/communication/api_gateway.py +1 -1
- kailash/middleware/communication/realtime.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +1 -1
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/admin/audit_log.py +6 -6
- kailash/nodes/admin/permission_check.py +8 -8
- kailash/nodes/admin/role_management.py +32 -28
- kailash/nodes/admin/schema.sql +6 -1
- kailash/nodes/admin/schema_manager.py +13 -13
- kailash/nodes/admin/security_event.py +15 -15
- kailash/nodes/admin/tenant_isolation.py +3 -3
- kailash/nodes/admin/transaction_utils.py +3 -3
- kailash/nodes/admin/user_management.py +21 -21
- kailash/nodes/ai/a2a.py +11 -11
- kailash/nodes/ai/ai_providers.py +9 -12
- kailash/nodes/ai/embedding_generator.py +13 -14
- kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
- kailash/nodes/ai/iterative_llm_agent.py +2 -2
- kailash/nodes/ai/llm_agent.py +210 -33
- kailash/nodes/ai/self_organizing.py +2 -2
- kailash/nodes/alerts/discord.py +4 -4
- kailash/nodes/api/graphql.py +6 -6
- kailash/nodes/api/http.py +10 -10
- kailash/nodes/api/rate_limiting.py +4 -4
- kailash/nodes/api/rest.py +15 -15
- kailash/nodes/auth/mfa.py +3 -3
- kailash/nodes/auth/risk_assessment.py +2 -2
- kailash/nodes/auth/session_management.py +5 -5
- kailash/nodes/auth/sso.py +143 -0
- kailash/nodes/base.py +8 -2
- kailash/nodes/base_async.py +16 -2
- kailash/nodes/base_with_acl.py +2 -2
- kailash/nodes/cache/__init__.py +9 -0
- kailash/nodes/cache/cache.py +1172 -0
- kailash/nodes/cache/cache_invalidation.py +874 -0
- kailash/nodes/cache/redis_pool_manager.py +595 -0
- kailash/nodes/code/async_python.py +2 -1
- kailash/nodes/code/python.py +194 -30
- kailash/nodes/compliance/data_retention.py +6 -6
- kailash/nodes/compliance/gdpr.py +5 -5
- kailash/nodes/data/__init__.py +10 -0
- kailash/nodes/data/async_sql.py +1956 -129
- kailash/nodes/data/optimistic_locking.py +906 -0
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/redis.py +378 -0
- kailash/nodes/data/sql.py +314 -3
- kailash/nodes/data/streaming.py +21 -0
- kailash/nodes/enterprise/__init__.py +8 -0
- kailash/nodes/enterprise/audit_logger.py +285 -0
- kailash/nodes/enterprise/batch_processor.py +22 -3
- kailash/nodes/enterprise/data_lineage.py +1 -1
- kailash/nodes/enterprise/mcp_executor.py +205 -0
- kailash/nodes/enterprise/service_discovery.py +150 -0
- kailash/nodes/enterprise/tenant_assignment.py +108 -0
- kailash/nodes/logic/async_operations.py +2 -2
- kailash/nodes/logic/convergence.py +1 -1
- kailash/nodes/logic/operations.py +1 -1
- kailash/nodes/monitoring/__init__.py +11 -1
- kailash/nodes/monitoring/health_check.py +456 -0
- kailash/nodes/monitoring/log_processor.py +817 -0
- kailash/nodes/monitoring/metrics_collector.py +627 -0
- kailash/nodes/monitoring/performance_benchmark.py +137 -11
- kailash/nodes/rag/advanced.py +7 -7
- kailash/nodes/rag/agentic.py +49 -2
- kailash/nodes/rag/conversational.py +3 -3
- kailash/nodes/rag/evaluation.py +3 -3
- kailash/nodes/rag/federated.py +3 -3
- kailash/nodes/rag/graph.py +3 -3
- kailash/nodes/rag/multimodal.py +3 -3
- kailash/nodes/rag/optimized.py +5 -5
- kailash/nodes/rag/privacy.py +3 -3
- kailash/nodes/rag/query_processing.py +6 -6
- kailash/nodes/rag/realtime.py +1 -1
- kailash/nodes/rag/registry.py +1 -1
- kailash/nodes/rag/router.py +1 -1
- kailash/nodes/rag/similarity.py +7 -7
- kailash/nodes/rag/strategies.py +4 -4
- kailash/nodes/security/abac_evaluator.py +6 -6
- kailash/nodes/security/behavior_analysis.py +5 -5
- kailash/nodes/security/credential_manager.py +1 -1
- kailash/nodes/security/rotating_credentials.py +11 -11
- kailash/nodes/security/threat_detection.py +8 -8
- kailash/nodes/testing/credential_testing.py +2 -2
- kailash/nodes/transform/processors.py +5 -5
- kailash/runtime/local.py +163 -9
- kailash/runtime/parameter_injection.py +425 -0
- kailash/runtime/parameter_injector.py +657 -0
- kailash/runtime/testing.py +2 -2
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +99 -14
- kailash/workflow/builder_improvements.py +207 -0
- kailash/workflow/input_handling.py +170 -0
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/METADATA +22 -9
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/RECORD +122 -95
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/WHEEL +0 -0
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.3.dist-info → kailash-0.6.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,587 @@
|
|
1
|
+
"""
|
2
|
+
Registry Integration for MCP Servers.
|
3
|
+
|
4
|
+
This module provides automatic registration and deregistration of MCP servers
|
5
|
+
with the service discovery system. It includes health announcement, capability
|
6
|
+
broadcasting, and graceful shutdown handling.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- Automatic server registration on startup
|
10
|
+
- Periodic health announcements
|
11
|
+
- Graceful deregistration on shutdown
|
12
|
+
- Capability discovery and broadcasting
|
13
|
+
- Network announcement protocols
|
14
|
+
- Registry backend integration
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
Auto-registration with file registry:
|
18
|
+
|
19
|
+
>>> from kailash.mcp_server import MCPServer
|
20
|
+
>>> from kailash.mcp_server.registry_integration import ServerRegistrar
|
21
|
+
>>>
|
22
|
+
>>> server = MCPServer("my-server")
|
23
|
+
>>> registrar = ServerRegistrar(server)
|
24
|
+
>>>
|
25
|
+
>>> @server.tool()
|
26
|
+
>>> def search(query: str) -> str:
|
27
|
+
... return f"Results: {query}"
|
28
|
+
>>>
|
29
|
+
>>> # Server will auto-register when started
|
30
|
+
>>> registrar.start_with_registration()
|
31
|
+
|
32
|
+
Custom registry backend:
|
33
|
+
|
34
|
+
>>> from kailash.mcp_server.discovery import ServiceRegistry, FileBasedDiscovery
|
35
|
+
>>>
|
36
|
+
>>> registry = ServiceRegistry([FileBasedDiscovery("custom_registry.json")])
|
37
|
+
>>> registrar = ServerRegistrar(server, registry=registry)
|
38
|
+
>>> registrar.start_with_registration()
|
39
|
+
"""
|
40
|
+
|
41
|
+
import asyncio
|
42
|
+
import atexit
|
43
|
+
import json
|
44
|
+
import logging
|
45
|
+
import signal
|
46
|
+
import socket
|
47
|
+
import time
|
48
|
+
import uuid
|
49
|
+
from pathlib import Path
|
50
|
+
from typing import Any, Dict, List, Optional, Set
|
51
|
+
from urllib.parse import urlparse
|
52
|
+
|
53
|
+
from .discovery import (
|
54
|
+
NetworkDiscovery,
|
55
|
+
ServerInfo,
|
56
|
+
ServiceRegistry,
|
57
|
+
create_default_registry,
|
58
|
+
)
|
59
|
+
from .errors import ServiceDiscoveryError
|
60
|
+
|
61
|
+
logger = logging.getLogger(__name__)
|
62
|
+
|
63
|
+
|
64
|
+
class ServerRegistrar:
|
65
|
+
"""Handles automatic registration and lifecycle management for MCP servers."""
|
66
|
+
|
67
|
+
def __init__(
|
68
|
+
self,
|
69
|
+
server: Any, # MCPServer instance
|
70
|
+
registry: Optional[ServiceRegistry] = None,
|
71
|
+
auto_announce: bool = True,
|
72
|
+
announce_interval: float = 30.0,
|
73
|
+
enable_network_discovery: bool = False,
|
74
|
+
server_metadata: Optional[Dict[str, Any]] = None,
|
75
|
+
):
|
76
|
+
"""Initialize server registrar.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
server: MCP server instance to register
|
80
|
+
registry: Service registry to use (creates default if None)
|
81
|
+
auto_announce: Enable periodic health announcements
|
82
|
+
announce_interval: Health announcement interval in seconds
|
83
|
+
enable_network_discovery: Enable UDP network announcements
|
84
|
+
server_metadata: Additional server metadata
|
85
|
+
"""
|
86
|
+
self.server = server
|
87
|
+
self.registry = registry or create_default_registry()
|
88
|
+
self.auto_announce = auto_announce
|
89
|
+
self.announce_interval = announce_interval
|
90
|
+
self.enable_network_discovery = enable_network_discovery
|
91
|
+
self.server_metadata = server_metadata or {}
|
92
|
+
|
93
|
+
# Server info
|
94
|
+
self.server_id = str(uuid.uuid4())
|
95
|
+
self.server_info: Optional[ServerInfo] = None
|
96
|
+
self.registered = False
|
97
|
+
|
98
|
+
# Announcement tracking
|
99
|
+
self.announcement_task: Optional[asyncio.Task] = None
|
100
|
+
self.network_announcer: Optional[NetworkAnnouncer] = None
|
101
|
+
|
102
|
+
# Setup cleanup handlers
|
103
|
+
self._setup_cleanup_handlers()
|
104
|
+
|
105
|
+
def _setup_cleanup_handlers(self):
|
106
|
+
"""Setup cleanup handlers for graceful shutdown."""
|
107
|
+
|
108
|
+
def cleanup():
|
109
|
+
if self.registered:
|
110
|
+
try:
|
111
|
+
asyncio.run(self.deregister())
|
112
|
+
except Exception as e:
|
113
|
+
logger.error(f"Error during cleanup: {e}")
|
114
|
+
|
115
|
+
atexit.register(cleanup)
|
116
|
+
|
117
|
+
# Handle signals
|
118
|
+
for sig in [signal.SIGTERM, signal.SIGINT]:
|
119
|
+
try:
|
120
|
+
signal.signal(sig, lambda s, f: cleanup())
|
121
|
+
except (ValueError, OSError):
|
122
|
+
# Signal handling not available (e.g., in threads)
|
123
|
+
pass
|
124
|
+
|
125
|
+
async def register(self) -> bool:
|
126
|
+
"""Register server with the discovery system.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
True if registration succeeded
|
130
|
+
"""
|
131
|
+
try:
|
132
|
+
# Discover server capabilities
|
133
|
+
capabilities = await self._discover_capabilities()
|
134
|
+
|
135
|
+
# Create server info
|
136
|
+
self.server_info = ServerInfo(
|
137
|
+
id=self.server_id,
|
138
|
+
name=self.server.name,
|
139
|
+
transport=self._determine_transport(),
|
140
|
+
endpoint=self._determine_endpoint(),
|
141
|
+
capabilities=capabilities,
|
142
|
+
metadata=self._create_metadata(),
|
143
|
+
health_status="healthy",
|
144
|
+
version=getattr(self.server, "version", "1.0.0"),
|
145
|
+
auth_required=getattr(self.server, "auth_manager", None) is not None,
|
146
|
+
)
|
147
|
+
|
148
|
+
# Register with registry
|
149
|
+
success = await self.registry.register_server(self.server_info.to_dict())
|
150
|
+
|
151
|
+
if success:
|
152
|
+
self.registered = True
|
153
|
+
logger.info(
|
154
|
+
f"Successfully registered server {self.server.name} ({self.server_id})"
|
155
|
+
)
|
156
|
+
|
157
|
+
# Start announcements if enabled
|
158
|
+
if self.auto_announce:
|
159
|
+
await self._start_announcements()
|
160
|
+
|
161
|
+
# Start network discovery if enabled
|
162
|
+
if self.enable_network_discovery:
|
163
|
+
self.network_announcer = NetworkAnnouncer(self.server_info)
|
164
|
+
await self.network_announcer.start()
|
165
|
+
|
166
|
+
return True
|
167
|
+
else:
|
168
|
+
logger.error(f"Failed to register server {self.server.name}")
|
169
|
+
return False
|
170
|
+
|
171
|
+
except Exception as e:
|
172
|
+
logger.error(f"Error registering server: {e}")
|
173
|
+
return False
|
174
|
+
|
175
|
+
async def deregister(self) -> bool:
|
176
|
+
"""Deregister server from the discovery system.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
True if deregistration succeeded
|
180
|
+
"""
|
181
|
+
if not self.registered:
|
182
|
+
return True
|
183
|
+
|
184
|
+
try:
|
185
|
+
# Stop announcements
|
186
|
+
if self.announcement_task:
|
187
|
+
self.announcement_task.cancel()
|
188
|
+
try:
|
189
|
+
await self.announcement_task
|
190
|
+
except asyncio.CancelledError:
|
191
|
+
pass
|
192
|
+
self.announcement_task = None
|
193
|
+
|
194
|
+
# Stop network announcer
|
195
|
+
if self.network_announcer:
|
196
|
+
await self.network_announcer.stop()
|
197
|
+
self.network_announcer = None
|
198
|
+
|
199
|
+
# Deregister from registry
|
200
|
+
success = await self.registry.deregister_server(self.server_id)
|
201
|
+
|
202
|
+
if success:
|
203
|
+
self.registered = False
|
204
|
+
logger.info(f"Successfully deregistered server {self.server.name}")
|
205
|
+
return True
|
206
|
+
else:
|
207
|
+
logger.error(f"Failed to deregister server {self.server.name}")
|
208
|
+
return False
|
209
|
+
|
210
|
+
except Exception as e:
|
211
|
+
logger.error(f"Error deregistering server: {e}")
|
212
|
+
return False
|
213
|
+
|
214
|
+
async def update_health(
|
215
|
+
self, health_status: str = "healthy", response_time: Optional[float] = None
|
216
|
+
):
|
217
|
+
"""Update server health status.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
health_status: New health status
|
221
|
+
response_time: Optional response time measurement
|
222
|
+
"""
|
223
|
+
if not self.registered:
|
224
|
+
return
|
225
|
+
|
226
|
+
try:
|
227
|
+
# Update in all registry backends
|
228
|
+
for backend in self.registry.backends:
|
229
|
+
await backend.update_server_health(
|
230
|
+
self.server_id, health_status, response_time
|
231
|
+
)
|
232
|
+
|
233
|
+
# Update local server info
|
234
|
+
if self.server_info:
|
235
|
+
self.server_info.health_status = health_status
|
236
|
+
self.server_info.last_seen = time.time()
|
237
|
+
if response_time is not None:
|
238
|
+
self.server_info.response_time = response_time
|
239
|
+
|
240
|
+
except Exception as e:
|
241
|
+
logger.error(f"Error updating health: {e}")
|
242
|
+
|
243
|
+
def start_with_registration(self):
|
244
|
+
"""Start the server with automatic registration.
|
245
|
+
|
246
|
+
This is a convenience method that handles registration and then
|
247
|
+
starts the server. Use this instead of server.run() for automatic
|
248
|
+
service discovery integration.
|
249
|
+
"""
|
250
|
+
|
251
|
+
async def startup_sequence():
|
252
|
+
# Register server
|
253
|
+
success = await self.register()
|
254
|
+
if not success:
|
255
|
+
logger.warning("Server registration failed, but continuing startup")
|
256
|
+
|
257
|
+
# Start health monitoring
|
258
|
+
if hasattr(self.server, "start_health_checking"):
|
259
|
+
self.server.start_health_checking()
|
260
|
+
|
261
|
+
# Run registration in event loop
|
262
|
+
try:
|
263
|
+
asyncio.run(startup_sequence())
|
264
|
+
except RuntimeError:
|
265
|
+
# Already in event loop, schedule as task
|
266
|
+
asyncio.create_task(startup_sequence())
|
267
|
+
|
268
|
+
# Start the actual server
|
269
|
+
try:
|
270
|
+
self.server.run()
|
271
|
+
finally:
|
272
|
+
# Ensure cleanup happens
|
273
|
+
try:
|
274
|
+
asyncio.run(self.deregister())
|
275
|
+
except Exception as e:
|
276
|
+
logger.error(f"Error during final cleanup: {e}")
|
277
|
+
|
278
|
+
async def _discover_capabilities(self) -> List[str]:
|
279
|
+
"""Discover server capabilities by examining registered tools."""
|
280
|
+
capabilities = []
|
281
|
+
|
282
|
+
# Get tools from server registry
|
283
|
+
if hasattr(self.server, "_tool_registry"):
|
284
|
+
capabilities.extend(self.server._tool_registry.keys())
|
285
|
+
|
286
|
+
# Get resources
|
287
|
+
if hasattr(self.server, "_resource_registry"):
|
288
|
+
for uri in self.server._resource_registry.keys():
|
289
|
+
capabilities.append(f"resource:{uri}")
|
290
|
+
|
291
|
+
# Get prompts
|
292
|
+
if hasattr(self.server, "_prompt_registry"):
|
293
|
+
for name in self.server._prompt_registry.keys():
|
294
|
+
capabilities.append(f"prompt:{name}")
|
295
|
+
|
296
|
+
# Add custom capabilities from metadata
|
297
|
+
custom_capabilities = self.server_metadata.get("capabilities", [])
|
298
|
+
capabilities.extend(custom_capabilities)
|
299
|
+
|
300
|
+
return list(set(capabilities)) # Remove duplicates
|
301
|
+
|
302
|
+
def _determine_transport(self) -> str:
|
303
|
+
"""Determine the transport type used by the server."""
|
304
|
+
# Check server configuration
|
305
|
+
if (
|
306
|
+
hasattr(self.server, "enable_http_transport")
|
307
|
+
and self.server.enable_http_transport
|
308
|
+
):
|
309
|
+
return "http"
|
310
|
+
elif (
|
311
|
+
hasattr(self.server, "enable_sse_transport")
|
312
|
+
and self.server.enable_sse_transport
|
313
|
+
):
|
314
|
+
return "sse"
|
315
|
+
else:
|
316
|
+
return "stdio"
|
317
|
+
|
318
|
+
def _determine_endpoint(self) -> str:
|
319
|
+
"""Determine the server endpoint."""
|
320
|
+
transport = self._determine_transport()
|
321
|
+
|
322
|
+
if transport in ["http", "sse"]:
|
323
|
+
# For HTTP/SSE, construct URL
|
324
|
+
host = getattr(self.server, "host", "localhost")
|
325
|
+
port = getattr(self.server, "port", 8080)
|
326
|
+
return f"http://{host}:{port}"
|
327
|
+
|
328
|
+
elif transport == "stdio":
|
329
|
+
# For stdio, provide command to start server
|
330
|
+
# This assumes the server can be started with Python
|
331
|
+
server_script = self.server_metadata.get("startup_command")
|
332
|
+
if server_script:
|
333
|
+
return server_script
|
334
|
+
else:
|
335
|
+
# Default command
|
336
|
+
return f"python -m {self.server.__class__.__module__}"
|
337
|
+
|
338
|
+
else:
|
339
|
+
return "unknown"
|
340
|
+
|
341
|
+
def _create_metadata(self) -> Dict[str, Any]:
|
342
|
+
"""Create metadata for server registration."""
|
343
|
+
metadata = self.server_metadata.copy()
|
344
|
+
|
345
|
+
# Add server configuration
|
346
|
+
if hasattr(self.server, "config"):
|
347
|
+
metadata["config"] = self.server.config.to_dict()
|
348
|
+
|
349
|
+
# Add feature flags
|
350
|
+
metadata["features"] = {
|
351
|
+
"caching": getattr(self.server, "cache", None) is not None,
|
352
|
+
"metrics": getattr(self.server, "metrics", None) is not None,
|
353
|
+
"auth": getattr(self.server, "auth_manager", None) is not None,
|
354
|
+
"circuit_breaker": getattr(self.server, "circuit_breaker", None)
|
355
|
+
is not None,
|
356
|
+
"streaming": getattr(self.server, "enable_streaming", False),
|
357
|
+
}
|
358
|
+
|
359
|
+
# Add authentication config if present
|
360
|
+
if hasattr(self.server, "auth_manager") and self.server.auth_manager:
|
361
|
+
auth_config = {
|
362
|
+
"type": type(self.server.auth_manager.provider)
|
363
|
+
.__name__.lower()
|
364
|
+
.replace("auth", ""),
|
365
|
+
"required": True,
|
366
|
+
}
|
367
|
+
metadata["auth_config"] = auth_config
|
368
|
+
|
369
|
+
# Add runtime info
|
370
|
+
metadata["runtime"] = {
|
371
|
+
"python_version": f"{__import__('sys').version_info.major}.{__import__('sys').version_info.minor}",
|
372
|
+
"platform": __import__("platform").system(),
|
373
|
+
"registered_at": time.time(),
|
374
|
+
}
|
375
|
+
|
376
|
+
return metadata
|
377
|
+
|
378
|
+
async def _start_announcements(self):
|
379
|
+
"""Start periodic health announcements."""
|
380
|
+
|
381
|
+
async def announce_health():
|
382
|
+
while self.registered:
|
383
|
+
try:
|
384
|
+
# Perform health check
|
385
|
+
health_status = await self._check_health()
|
386
|
+
await self.update_health(health_status)
|
387
|
+
|
388
|
+
await asyncio.sleep(self.announce_interval)
|
389
|
+
|
390
|
+
except asyncio.CancelledError:
|
391
|
+
break
|
392
|
+
except Exception as e:
|
393
|
+
logger.error(f"Error in health announcement: {e}")
|
394
|
+
await asyncio.sleep(min(self.announce_interval, 10))
|
395
|
+
|
396
|
+
self.announcement_task = asyncio.create_task(announce_health())
|
397
|
+
|
398
|
+
async def _check_health(self) -> str:
|
399
|
+
"""Check server health status.
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
Health status string
|
403
|
+
"""
|
404
|
+
try:
|
405
|
+
# Use server's health check if available
|
406
|
+
if hasattr(self.server, "health_check"):
|
407
|
+
health_result = self.server.health_check()
|
408
|
+
return health_result.get("status", "unknown")
|
409
|
+
|
410
|
+
# Basic health check - ensure server is running
|
411
|
+
if hasattr(self.server, "_running") and self.server._running:
|
412
|
+
return "healthy"
|
413
|
+
else:
|
414
|
+
return "unknown"
|
415
|
+
|
416
|
+
except Exception as e:
|
417
|
+
logger.debug(f"Health check failed: {e}")
|
418
|
+
return "unhealthy"
|
419
|
+
|
420
|
+
|
421
|
+
class NetworkAnnouncer:
|
422
|
+
"""Handles UDP network announcements for MCP servers."""
|
423
|
+
|
424
|
+
def __init__(self, server_info: ServerInfo, port: int = 8765):
|
425
|
+
"""Initialize network announcer.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
server_info: Server information to announce
|
429
|
+
port: UDP port for announcements
|
430
|
+
"""
|
431
|
+
self.server_info = server_info
|
432
|
+
self.port = port
|
433
|
+
self.running = False
|
434
|
+
self.announcement_task: Optional[asyncio.Task] = None
|
435
|
+
self.socket: Optional[socket.socket] = None
|
436
|
+
|
437
|
+
async def start(self):
|
438
|
+
"""Start network announcements."""
|
439
|
+
self.running = True
|
440
|
+
|
441
|
+
# Create UDP socket
|
442
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
443
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
444
|
+
|
445
|
+
# Start announcement task
|
446
|
+
self.announcement_task = asyncio.create_task(self._announce_loop())
|
447
|
+
|
448
|
+
logger.info(f"Started network announcements for {self.server_info.name}")
|
449
|
+
|
450
|
+
async def stop(self):
|
451
|
+
"""Stop network announcements."""
|
452
|
+
self.running = False
|
453
|
+
|
454
|
+
if self.announcement_task:
|
455
|
+
self.announcement_task.cancel()
|
456
|
+
try:
|
457
|
+
await self.announcement_task
|
458
|
+
except asyncio.CancelledError:
|
459
|
+
pass
|
460
|
+
|
461
|
+
if self.socket:
|
462
|
+
self.socket.close()
|
463
|
+
self.socket = None
|
464
|
+
|
465
|
+
logger.info(f"Stopped network announcements for {self.server_info.name}")
|
466
|
+
|
467
|
+
async def _announce_loop(self):
|
468
|
+
"""Main announcement loop."""
|
469
|
+
while self.running:
|
470
|
+
try:
|
471
|
+
await self._send_announcement()
|
472
|
+
await asyncio.sleep(30) # Announce every 30 seconds
|
473
|
+
|
474
|
+
except asyncio.CancelledError:
|
475
|
+
break
|
476
|
+
except Exception as e:
|
477
|
+
logger.error(f"Error in network announcement: {e}")
|
478
|
+
await asyncio.sleep(10) # Back off on error
|
479
|
+
|
480
|
+
async def _send_announcement(self):
|
481
|
+
"""Send UDP announcement."""
|
482
|
+
if not self.socket:
|
483
|
+
return
|
484
|
+
|
485
|
+
announcement = {
|
486
|
+
"type": "mcp_server_announcement",
|
487
|
+
"id": self.server_info.id,
|
488
|
+
"name": self.server_info.name,
|
489
|
+
"transport": self.server_info.transport,
|
490
|
+
"endpoint": self.server_info.endpoint,
|
491
|
+
"capabilities": self.server_info.capabilities,
|
492
|
+
"metadata": self.server_info.metadata,
|
493
|
+
"health_status": self.server_info.health_status,
|
494
|
+
"version": self.server_info.version,
|
495
|
+
"auth_required": self.server_info.auth_required,
|
496
|
+
"timestamp": time.time(),
|
497
|
+
}
|
498
|
+
|
499
|
+
message = json.dumps(announcement).encode()
|
500
|
+
|
501
|
+
# Broadcast to network
|
502
|
+
try:
|
503
|
+
self.socket.sendto(message, ("<broadcast>", self.port))
|
504
|
+
except Exception as e:
|
505
|
+
logger.debug(f"Failed to send broadcast: {e}")
|
506
|
+
|
507
|
+
# Send to multicast group
|
508
|
+
try:
|
509
|
+
self.socket.sendto(message, (NetworkDiscovery.MULTICAST_GROUP, self.port))
|
510
|
+
except Exception as e:
|
511
|
+
logger.debug(f"Failed to send multicast: {e}")
|
512
|
+
|
513
|
+
|
514
|
+
def enable_auto_discovery(server, **kwargs):
|
515
|
+
"""Enable automatic discovery for an MCP server.
|
516
|
+
|
517
|
+
This is a convenience function that creates a ServerRegistrar
|
518
|
+
and configures it for the given server.
|
519
|
+
|
520
|
+
Args:
|
521
|
+
server: MCP server instance
|
522
|
+
**kwargs: Configuration options for ServerRegistrar
|
523
|
+
|
524
|
+
Returns:
|
525
|
+
ServerRegistrar instance
|
526
|
+
|
527
|
+
Examples:
|
528
|
+
>>> from kailash.mcp_server import MCPServer
|
529
|
+
>>> from kailash.mcp_server.registry_integration import enable_auto_discovery
|
530
|
+
>>>
|
531
|
+
>>> server = MCPServer("my-server")
|
532
|
+
>>> registrar = enable_auto_discovery(server, enable_network_discovery=True)
|
533
|
+
>>> registrar.start_with_registration()
|
534
|
+
"""
|
535
|
+
return ServerRegistrar(server, **kwargs)
|
536
|
+
|
537
|
+
|
538
|
+
def register_server_manually(
|
539
|
+
name: str,
|
540
|
+
transport: str,
|
541
|
+
endpoint: str,
|
542
|
+
capabilities: List[str],
|
543
|
+
metadata: Optional[Dict[str, Any]] = None,
|
544
|
+
registry: Optional[ServiceRegistry] = None,
|
545
|
+
) -> bool:
|
546
|
+
"""Manually register a server with the discovery system.
|
547
|
+
|
548
|
+
This is useful for registering external servers that don't use
|
549
|
+
the Kailash MCP server framework.
|
550
|
+
|
551
|
+
Args:
|
552
|
+
name: Server name
|
553
|
+
transport: Transport type (stdio, http, sse)
|
554
|
+
endpoint: Server endpoint
|
555
|
+
capabilities: List of capabilities
|
556
|
+
metadata: Optional metadata
|
557
|
+
registry: Service registry to use
|
558
|
+
|
559
|
+
Returns:
|
560
|
+
True if registration succeeded
|
561
|
+
|
562
|
+
Examples:
|
563
|
+
>>> register_server_manually(
|
564
|
+
... name="external-server",
|
565
|
+
... transport="http",
|
566
|
+
... endpoint="http://external-host:8080",
|
567
|
+
... capabilities=["search", "analyze"],
|
568
|
+
... metadata={"version": "2.0", "external": True}
|
569
|
+
... )
|
570
|
+
"""
|
571
|
+
if registry is None:
|
572
|
+
registry = create_default_registry()
|
573
|
+
|
574
|
+
server_config = {
|
575
|
+
"name": name,
|
576
|
+
"transport": transport,
|
577
|
+
"endpoint": endpoint,
|
578
|
+
"capabilities": capabilities,
|
579
|
+
"metadata": metadata or {},
|
580
|
+
"auth_required": False,
|
581
|
+
}
|
582
|
+
|
583
|
+
try:
|
584
|
+
return asyncio.run(registry.register_server(server_config))
|
585
|
+
except RuntimeError:
|
586
|
+
# Already in event loop
|
587
|
+
return asyncio.create_task(registry.register_server(server_config))
|