diff --git a/src/dedalus_mcp/context.py b/src/dedalus_mcp/context.py index 4643717..72d5b22 100644 --- a/src/dedalus_mcp/context.py +++ b/src/dedalus_mcp/context.py @@ -298,7 +298,7 @@ async def dispatch( raise RuntimeError("Dispatch backend not configured") # Resolve connection target to handle - connections = self._get_connection_handles(runtime) + connections = self._get_connections(runtime) if connection_target is None: # Single-connection server: use the only connection @@ -401,13 +401,23 @@ def _build_resolver_context(self, operation: Mapping[str, Any] | None) -> dict[s payload["operation"] = dict(operation) return payload - def _get_connection_handles(self, runtime: Mapping[str, Any]) -> dict[str, str]: - """Get connection name to handle mapping from runtime config. + def _get_connections(self, runtime: Mapping[str, Any]) -> dict[str, str]: + """Get connection mapping from JWT claims (ddls:connections).""" + auth_context = self.auth_context + if auth_context is None: + msg = """DEDALUS_DISPATCH_URL not found. + Dispatch is only available through the Dedalus-hosted MCP servers.""" + raise RuntimeError(msg) - Note: Authorization is handled by the gateway at runtime via Admin API. - The runtime config provides the name->handle mapping for dispatch routing. - """ - return dict(runtime.get("connection_handles", {})) + claims = getattr(auth_context, "claims", None) + if not isinstance(claims, dict): + raise RuntimeError("Invalid authorization claims") + + connections = claims.get("ddls:connections") + if not isinstance(connections, dict): + raise RuntimeError("Missing ddls:connections claim") + + return dict(connections) def _activate_request_context() -> Token[Context | None]: diff --git a/src/dedalus_mcp/server/core.py b/src/dedalus_mcp/server/core.py index cd692b4..4f1d71f 100644 --- a/src/dedalus_mcp/server/core.py +++ b/src/dedalus_mcp/server/core.py @@ -184,9 +184,8 @@ def __init__( raise ValueError(f"Duplicate connection name: '{conn.name}'") self._connections[conn.name] = conn - # Dispatch backend and connection handles (initialized in _build_runtime_payload) + # Dispatch backend (initialized in _build_runtime_payload) self._dispatch_backend: Any = None - self._connection_handles: dict[str, str] = {} super().__init__( name, version=version, instructions=instructions, website_url=website_url, icons=icons, lifespan=lifespan ) @@ -867,73 +866,17 @@ def _build_runtime_payload(self) -> dict[str, Any]: "server": self, "resolver": self._connection_resolver, "dispatch_backend": self._dispatch_backend, - "connection_handles": self._connection_handles, } def _initialize_dispatch_backend(self) -> None: - """Initialize dispatch backend from environment and connections. + """Initialize dispatch backend from environment. - Creates DirectDispatchBackend (OSS mode) or EnclaveDispatchBackend (production) - based on DEDALUS_DISPATCH_URL environment variable. + Requires DEDALUS_DISPATCH_URL to be set. Dispatch is only available + through Dedalus-hosted MCP servers. """ - import os + from ..dispatch import create_dispatch_backend_from_env - from ..dispatch import DirectDispatchBackend, create_dispatch_backend_from_env - - # Build connection handles map: {name: "ddls:conn:{name}"} - self._connection_handles = {name: f'ddls:conn:{name}' for name in self._connections} - - # Get backend from environment (checks DEDALUS_DISPATCH_URL) - backend = create_dispatch_backend_from_env() - - # For DirectDispatchBackend, configure credential resolver - if isinstance(backend, DirectDispatchBackend): - - def credential_resolver(handle: str) -> tuple[str, str, str]: - """Resolve connection handle to (base_url, header_name, header_value).""" - # Extract connection name from handle (ddls:conn:{name}) - if not handle.startswith('ddls:conn:'): - raise ValueError(f'Invalid handle format: {handle}') - conn_name = handle[len('ddls:conn:') :] - - conn = self._connections.get(conn_name) - if conn is None: - raise ValueError(f"Connection '{conn_name}' not found") - - # Read credentials from environment using Binding entries - creds: dict[str, str] = {} - for field_name, binding in conn.secrets.entries.items(): - raw = os.getenv(binding.name) - if raw is None or raw == '': - if binding.optional: - continue - raise RuntimeError(f'Environment variable {binding.name} is not set') - creds[field_name] = raw - - # Extract the credential value (api_key for format string) - api_key = '' - for key, value in creds.items(): - if key in ('token', 'access_token', 'key', 'apikey', 'api_key', 'service_role_key'): - api_key = value - break - else: - # Fallback: use first credential value - api_key = value - break - - # Format header value using connection's auth_header_format - header_value = conn.auth_header_format.format(api_key=api_key) - - return (conn.base_url or '', conn.auth_header_name, header_value) - - # Replace backend with one that has credential resolver - backend = DirectDispatchBackend(credential_resolver=credential_resolver) - - self._dispatch_backend = backend - self._logger.debug( - 'dispatch backend initialized', - extra={'event': 'dispatch.init', 'backend_type': type(backend).__name__}, - ) + self._dispatch_backend = create_dispatch_backend_from_env() # TODO: Quality check on this impl. async def _handle_request(