feat: add source auth diagnostics
This commit is contained in:
@@ -22,6 +22,7 @@ from .prompts import register_broker_prompts
|
||||
from .resources import register_broker_resources
|
||||
from .service import WfMcpService
|
||||
from .service.auth_admin import McpAuthAdminProvider
|
||||
from .service.source_diagnostics import SourceDiagnosticsProvider
|
||||
from .service.source_registry_admin import SourceRegistryAdminProvider
|
||||
from .service.workflow_operation_context import context_from_service
|
||||
from .tools import register_broker_tools
|
||||
@@ -64,7 +65,15 @@ def workflow_server_from_service(
|
||||
|
||||
context = context_from_service(service)
|
||||
api: WorkflowApi = durable_workflow_api(context)
|
||||
source_admin = WorkflowSourceAdminApi(context)
|
||||
source_diagnostics = SourceDiagnosticsProvider(
|
||||
connection_lookup=service.connections.get,
|
||||
auth_store=service.auth_store or service.store,
|
||||
catalog_store=service.catalog_store or service.store,
|
||||
)
|
||||
source_admin = WorkflowSourceAdminApi(
|
||||
context,
|
||||
diagnostics=source_diagnostics,
|
||||
)
|
||||
admin = WorkflowAdminApi(
|
||||
connections=service.connection_service,
|
||||
events=service.events,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity
|
||||
from wf_sources_mcp.storage import AuthStore, CatalogStore
|
||||
|
||||
from ...models import ConnectionConfig
|
||||
|
||||
ConnectionLookup = Callable[[str], ConnectionConfig]
|
||||
|
||||
|
||||
def _auth_ref(connection: ConnectionConfig) -> str | None:
|
||||
value = connection.metadata.get("auth_ref")
|
||||
return value if isinstance(value, str) and value else None
|
||||
|
||||
|
||||
def _transport_kind(connection: ConnectionConfig) -> str | None:
|
||||
value = connection.metadata.get("transport")
|
||||
return value if isinstance(value, str) and value else None
|
||||
|
||||
|
||||
def _auth_scheme_supported(
|
||||
*,
|
||||
transport_kind: str | None,
|
||||
scheme: str | None,
|
||||
) -> bool:
|
||||
if scheme is None:
|
||||
return True
|
||||
if transport_kind == "stdio":
|
||||
return scheme == "env"
|
||||
if transport_kind == "http":
|
||||
return scheme in {"bearer", "headers", "oauth_refresh_token"}
|
||||
return False
|
||||
|
||||
|
||||
def _unsupported_auth_diagnostic(
|
||||
*,
|
||||
source_id: str,
|
||||
auth_ref: str,
|
||||
scheme: str,
|
||||
transport_kind: str | None,
|
||||
) -> dict[str, Any]:
|
||||
return DependencyDiagnostic(
|
||||
severity=DiagnosticSeverity.ERROR,
|
||||
code="auth_scheme_not_supported",
|
||||
logical_ref=auth_ref,
|
||||
bound_source=source_id,
|
||||
message=(
|
||||
f"Source {source_id!r} uses {transport_kind or 'unknown'} transport, "
|
||||
f"but auth record {auth_ref!r} has unsupported scheme {scheme!r}."
|
||||
),
|
||||
repair_hint=(
|
||||
"Use env auth for stdio MCP sources, or bearer/headers/"
|
||||
"oauth_refresh_token auth for HTTP MCP sources."
|
||||
),
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
def _missing_auth_diagnostic(
|
||||
*,
|
||||
source_id: str,
|
||||
auth_ref: str,
|
||||
) -> dict[str, Any]:
|
||||
return DependencyDiagnostic(
|
||||
severity=DiagnosticSeverity.ERROR,
|
||||
code="auth_not_found",
|
||||
logical_ref=auth_ref,
|
||||
bound_source=source_id,
|
||||
message=(
|
||||
f"Source {source_id!r} references auth record {auth_ref!r}, "
|
||||
"but no auth record was found."
|
||||
),
|
||||
repair_hint=(
|
||||
"Add an auth record for this auth_ref, update the source auth_ref, "
|
||||
"or bind the deployment to a source that does not require it."
|
||||
),
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
def _missing_transport_diagnostic(*, source_id: str) -> dict[str, Any]:
|
||||
return DependencyDiagnostic(
|
||||
severity=DiagnosticSeverity.ERROR,
|
||||
code="source_transport_missing",
|
||||
logical_ref=source_id,
|
||||
bound_source=source_id,
|
||||
message=f"Source {source_id!r} has no MCP transport configured.",
|
||||
repair_hint=(
|
||||
"Configure the source with an MCP transport such as stdio or http, "
|
||||
"then apply or restart the server."
|
||||
),
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SourceDiagnosticsProvider:
|
||||
"""MCP broker diagnostics for source auth, transport, and catalog state."""
|
||||
|
||||
connection_lookup: ConnectionLookup
|
||||
auth_store: AuthStore
|
||||
catalog_store: CatalogStore
|
||||
|
||||
def diagnose_source(self, source_id: str) -> dict[str, Any]:
|
||||
connection = self.connection_lookup(source_id)
|
||||
auth_ref = _auth_ref(connection)
|
||||
auth = self.auth_store.load_auth(auth_ref) if auth_ref else None
|
||||
transport_kind = _transport_kind(connection)
|
||||
snapshot = self.catalog_store.load_catalog(source_id)
|
||||
diagnostics: list[dict[str, Any]] = []
|
||||
|
||||
if transport_kind is None:
|
||||
diagnostics.append(_missing_transport_diagnostic(source_id=source_id))
|
||||
if auth_ref is not None and auth is None:
|
||||
diagnostics.append(
|
||||
_missing_auth_diagnostic(source_id=source_id, auth_ref=auth_ref)
|
||||
)
|
||||
|
||||
transport_supported = _auth_scheme_supported(
|
||||
transport_kind=transport_kind,
|
||||
scheme=None if auth is None else auth.scheme,
|
||||
)
|
||||
if auth_ref and auth is not None and not transport_supported:
|
||||
diagnostics.append(
|
||||
_unsupported_auth_diagnostic(
|
||||
source_id=source_id,
|
||||
auth_ref=auth_ref,
|
||||
scheme=auth.scheme,
|
||||
transport_kind=transport_kind,
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"source_id": source_id,
|
||||
"status": "error" if diagnostics else "ok",
|
||||
"enabled": connection.enabled,
|
||||
"transport": {
|
||||
"kind": transport_kind,
|
||||
"configured": transport_kind is not None,
|
||||
},
|
||||
"auth": {
|
||||
"auth_ref": auth_ref,
|
||||
"record_present": auth is not None if auth_ref else None,
|
||||
"scheme": None if auth is None else auth.scheme,
|
||||
"transport_supported": transport_supported,
|
||||
},
|
||||
"catalog": {
|
||||
"has_snapshot": snapshot is not None,
|
||||
"fetched_at_epoch_ms": None
|
||||
if snapshot is None
|
||||
else snapshot.fetched_at_epoch_ms,
|
||||
"max_age_seconds": None if snapshot is None else snapshot.max_age_seconds,
|
||||
"node_count": 0 if snapshot is None else len(snapshot.nodes),
|
||||
"resource_count": 0 if snapshot is None else len(snapshot.resources),
|
||||
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
|
||||
},
|
||||
"diagnostics": diagnostics,
|
||||
}
|
||||
Reference in New Issue
Block a user