refactor: introduce wf sources mcp auth storage

This commit is contained in:
lda
2026-06-06 23:19:56 +07:00 Verified
parent fc43671940
commit 87d84767f8
20 changed files with 605 additions and 374 deletions
+6 -5
View File
@@ -273,11 +273,12 @@ implementation state.
as workflow capabilities. The current `wf_mcp` package still contains both as workflow capabilities. The current `wf_mcp` package still contains both
roles plus compatibility entrypoints; new server/transport work should avoid roles plus compatibility entrypoints; new server/transport work should avoid
depending on that combined facade. depending on that combined facade.
Next concrete slice: create `wf_sources_mcp` and move leaf upstream-source First `wf_sources_mcp` slice complete: MCP auth helpers and focused
pieces first, starting with MCP auth helpers and focused auth/catalog stores. auth/catalog stores now live in `wf_sources_mcp`, with `wf_mcp` compatibility
Keep `wf_mcp` re-export shims for compatibility and add import-direction shims preserved. Runtime/session/source-registry moves remain future slices.
tests so `wf_sources_mcp` does not depend on workflow/admin surface, Keep `wf_mcp` re-export shims for compatibility and add import-direction
frontend server, or proxy modules. tests so `wf_sources_mcp` does not depend on workflow/admin surface,
frontend server, or proxy modules.
The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the
preferred durable workflow server. New product paths should target preferred durable workflow server. New product paths should target
`wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep `wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep
@@ -87,7 +87,8 @@ packages, or compatibility shims.
First slices should move leaf modules only and leave `wf_mcp` re-export shims: First slices should move leaf modules only and leave `wf_mcp` re-export shims:
1. MCP auth helpers and focused auth/catalog stores. 1. Complete: MCP auth helpers and focused auth/catalog stores moved to
`wf_sources_mcp`, with `wf_mcp` shims preserved.
2. MCP source registry models/conversion. 2. MCP source registry models/conversion.
3. Upstream transport/discovery/session services. 3. Upstream transport/discovery/session services.
+15 -133
View File
@@ -1,138 +1,20 @@
"""Compatibility shim for MCP source auth helpers.
Canonical implementation lives in `wf_sources_mcp.auth`.
"""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from wf_sources_mcp.auth import (
from dataclasses import dataclass, field AuthRecord,
from typing import TYPE_CHECKING, Any auth_missing_diagnostic,
auth_ref_for_connection,
from wf_api.auth import AuthRecord as NeutralAuthRecord connection_auth_diagnostic,
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity mcp_auth_env,
mcp_auth_from_neutral,
if TYPE_CHECKING: mcp_auth_headers,
from .broker.models import ConnectionConfig neutral_auth_from_mcp,
)
@dataclass(slots=True)
class AuthRecord:
connection_id: str
scheme: str
payload: dict[str, Any] = field(default_factory=dict)
def mcp_auth_from_neutral(record: NeutralAuthRecord) -> AuthRecord:
"""Adapt neutral auth to the current MCP compatibility record."""
return AuthRecord(
connection_id=record.id,
scheme=record.scheme,
payload=dict(record.payload),
)
def neutral_auth_from_mcp(record: AuthRecord) -> NeutralAuthRecord:
"""Adapt legacy MCP auth into the neutral record shape."""
return NeutralAuthRecord(
id=record.connection_id,
scheme=record.scheme,
payload=dict(record.payload),
)
def mcp_auth_headers(auth: AuthRecord | None) -> dict[str, str]:
"""Return HTTP headers understood by MCP HTTP transports.
This is intentionally MCP-specific. Neutral code must not inspect payload
keys such as `headers` or `token`.
"""
if auth is None:
return {}
raw_headers = auth.payload.get("headers", {})
headers = (
{
str(key): str(value)
for key, value in raw_headers.items()
if isinstance(key, str) and isinstance(value, str)
}
if isinstance(raw_headers, dict)
else {}
)
token = auth.payload.get("token")
if isinstance(token, str) and "Authorization" not in headers:
headers["Authorization"] = f"Bearer {token}"
return headers
def mcp_auth_env(auth: AuthRecord | None) -> dict[str, str]:
"""Return environment variables understood by MCP stdio transports."""
if auth is None:
return {}
raw_env = auth.payload.get("env", {})
if not isinstance(raw_env, dict):
return {}
return {
str(key): str(value)
for key, value in raw_env.items()
if isinstance(key, str) and isinstance(value, str)
}
def auth_ref_for_connection(connection: ConnectionConfig) -> str | None:
"""Return the explicit auth ref for one source connection, if present."""
auth_ref = connection.metadata.get("auth_ref")
return auth_ref if isinstance(auth_ref, str) else None
def auth_missing_diagnostic(
*,
auth_ref: str,
source_id: str,
logical_ref: str | None = None,
) -> DependencyDiagnostic:
"""Build a stable diagnostic without including secret payload data."""
return DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR,
code="auth_not_found",
logical_ref=logical_ref or "",
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."
),
)
def connection_auth_diagnostic(
connection: ConnectionConfig,
*,
load_auth_ref: Callable[[str], AuthRecord | None],
logical_ref: str | None = None,
) -> DependencyDiagnostic | None:
"""Return an auth diagnostic for explicit auth_ref misses.
Connections without explicit auth_ref keep legacy no-auth behavior. This
makes the new auth boundary observable without treating every unauthenticated
MCP source as an error.
"""
auth_ref = auth_ref_for_connection(connection)
if auth_ref is None:
return None
if load_auth_ref(auth_ref) is not None:
return None
return auth_missing_diagnostic(
auth_ref=auth_ref,
source_id=connection.id,
logical_ref=logical_ref,
)
__all__ = [ __all__ = [
"AuthRecord", "AuthRecord",
+1 -1
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from wf_api import file_workflow_stores from wf_api import file_workflow_stores
from wf_config import WorkflowConfigFile from wf_config import WorkflowConfigFile
from wf_config.models import FilesystemStoreConfig, McpSourceConfig, ServerConfig from wf_config.models import FilesystemStoreConfig, McpSourceConfig, ServerConfig
from wf_sources_mcp.storage import FileAuthStore, FileCatalogStore, FileStore
from ..control import BrokerConfigFile, ConnectionConfigFile from ..control import BrokerConfigFile, ConnectionConfigFile
from ..models import BrokerConfig from ..models import BrokerConfig
@@ -15,7 +16,6 @@ from ..source_registry import (
FileSourceRegistryStore, FileSourceRegistryStore,
workflow_mcp_source_to_connection_config, workflow_mcp_source_to_connection_config,
) )
from ..storage import FileAuthStore, FileCatalogStore, FileStore
from .models import BrokerStoreRoots from .models import BrokerStoreRoots
from .service import WfMcpService from .service import WfMcpService
+1 -2
View File
@@ -5,8 +5,7 @@ from typing import Any
from wf_api import WorkflowAdminAuthProvider from wf_api import WorkflowAdminAuthProvider
from wf_api.auth import AuthRecord as NeutralAuthRecord from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_sources_mcp.storage import AuthStore
from ...storage import AuthStore
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
+2 -2
View File
@@ -25,8 +25,9 @@ from wf_mcp.capabilities import (
from wf_platform import ( from wf_platform import (
CapabilitySource, CapabilitySource,
) )
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.storage import AuthStore, CatalogStore, Store
from ...auth import AuthRecord
from ...connections import ConnectionRegistry from ...connections import ConnectionRegistry
from ...events import EventBus, McpEvent from ...events import EventBus, McpEvent
from ...models import ( from ...models import (
@@ -37,7 +38,6 @@ from ...models import (
from ...runtime import ToolExecutor from ...runtime import ToolExecutor
from ...sdk import BackendAdapter from ...sdk import BackendAdapter
from ...source_registry import SourceRegistryStore from ...source_registry import SourceRegistryStore
from ...storage import AuthStore, CatalogStore, Store
from ..admin_capabilities import admin_source from ..admin_capabilities import admin_source
from ..catalog import CombinedCatalog from ..catalog import CombinedCatalog
from .builtins import builtin_sources from .builtins import builtin_sources
+2 -2
View File
@@ -22,15 +22,15 @@ from wf_platform import (
SourceVisibility, SourceVisibility,
page_items, page_items,
) )
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.storage import CatalogStore
from ...auth import AuthRecord
from ...connections import ConnectionConfig, qualify_node_name from ...connections import ConnectionConfig, qualify_node_name
from ...events import McpEvent, make_event from ...events import McpEvent, make_event
from ...models import ( from ...models import (
CatalogSnapshot, CatalogSnapshot,
) )
from ...runtime import ToolExecutor from ...runtime import ToolExecutor
from ...storage import CatalogStore
from ...workflow.wrappers import _model_from_schema from ...workflow.wrappers import _model_from_schema
from ..catalog import CombinedCatalog, snapshot_from_specs from ..catalog import CombinedCatalog, snapshot_from_specs
from .specs import get_qualified_spec, qualify_spec from .specs import get_qualified_spec, qualify_spec
@@ -17,7 +17,6 @@ from wf_artifacts import (
WorkflowArtifact, WorkflowArtifact,
WorkflowDeployment, WorkflowDeployment,
) )
from wf_mcp.auth import AuthRecord
from wf_mcp.broker.catalog import snapshot_from_specs from wf_mcp.broker.catalog import snapshot_from_specs
from wf_mcp.broker.discovery import ( from wf_mcp.broker.discovery import (
discover_connection_capabilities, discover_connection_capabilities,
@@ -28,9 +27,9 @@ from wf_mcp.models import CatalogSnapshot, ConnectionConfig
from wf_mcp.runtime import ToolExecutor from wf_mcp.runtime import ToolExecutor
from wf_mcp.sdk import BackendAdapter from wf_mcp.sdk import BackendAdapter
from wf_mcp.shared.errors import error_payload from wf_mcp.shared.errors import error_payload
from wf_mcp.storage import AuthStore, CatalogStore from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic
from wf_sources_mcp.storage import AuthStore, CatalogStore
from ...auth import connection_auth_diagnostic
from .adapters import require_adapter from .adapters import require_adapter
from .source_catalog import SourceCatalogService from .source_catalog import SourceCatalogService
+2 -1
View File
@@ -10,7 +10,8 @@ from mcp.client.stdio import StdioServerParameters, stdio_client
from mcp.client.streamable_http import streamable_http_client from mcp.client.streamable_http import streamable_http_client
from mcp.types import CallToolResult from mcp.types import CallToolResult
from ..auth import AuthRecord, mcp_auth_env, mcp_auth_headers from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers
from ..models import ConnectionConfig from ..models import ConnectionConfig
from .session import PersistentMcpSession from .session import PersistentMcpSession
+2 -1
View File
@@ -17,7 +17,8 @@ from mcp.types import (
) )
from pydantic import AnyUrl from pydantic import AnyUrl
from ..auth import AuthRecord, mcp_auth_env, mcp_auth_headers from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers
from ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from ..models import ConnectionConfig from ..models import ConnectionConfig
from .base import BackendAdapter, ToolCallResult from .base import BackendAdapter, ToolCallResult
+3 -1
View File
@@ -1,4 +1,6 @@
from .store import ( from __future__ import annotations
from wf_sources_mcp.storage import (
AuthStore, AuthStore,
CatalogStore, CatalogStore,
FileAuthStore, FileAuthStore,
+20 -222
View File
@@ -1,226 +1,24 @@
"""Compatibility shim for MCP source auth/catalog stores.
Canonical implementation lives in `wf_sources_mcp.storage.store`.
"""
from __future__ import annotations from __future__ import annotations
import json from wf_sources_mcp.storage.store import (
from pathlib import Path AuthStore,
CatalogStore,
from wf_api.auth import AuthRecord as NeutralAuthRecord FileAuthStore,
from wf_api.auth import validate_auth_id FileCatalogStore,
from wf_mcp.capabilities import ( FileStore,
CatalogNodeEntry, Store,
CatalogPromptEntry,
CatalogResourceEntry,
) )
from ..auth import AuthRecord, mcp_auth_from_neutral, neutral_auth_from_mcp __all__ = [
from ..connections import parse_connection_id "AuthStore",
from ..models import ( "CatalogStore",
CatalogSnapshot, "FileAuthStore",
dump_catalog_snapshot, "FileCatalogStore",
) "FileStore",
"Store",
]
class AuthStore:
def save_auth(self, record: AuthRecord) -> None:
raise NotImplementedError
def load_auth(self, connection_id: str) -> AuthRecord | None:
raise NotImplementedError
def list_auth_refs(self) -> list[str]:
raise NotImplementedError
def save_auth_record(self, record: NeutralAuthRecord) -> None:
raise NotImplementedError
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
raise NotImplementedError
def delete_auth(self, connection_id: str) -> bool:
raise NotImplementedError
def delete_auth_record(self, auth_ref: str) -> bool:
raise NotImplementedError
class CatalogStore:
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
raise NotImplementedError
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
raise NotImplementedError
class Store(AuthStore, CatalogStore):
"""Compatibility store combining MCP auth and catalog/cache storage."""
class FileAuthStore(AuthStore):
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self.auth_dir.mkdir(parents=True, exist_ok=True)
@property
def auth_dir(self) -> Path:
return self.root / "auth"
def _auth_path(self, auth_ref: str) -> Path:
"""Map one auth ref to one file.
Auth refs used to be connection ids, but neutral auth refs now carry no
provider/account semantics. Keep catalog paths on connection-id
validation while auth storage accepts the wider auth-id contract.
"""
validate_auth_id(auth_ref)
root = self.auth_dir.resolve()
path = (self.auth_dir / f"{auth_ref}.json").resolve()
if path.parent != root:
raise ValueError(f"auth ref escapes store directory: {auth_ref!r}")
return path
def save_auth(self, record: AuthRecord) -> None:
self._auth_path(record.connection_id).write_text(
json.dumps(
{
"connection_id": record.connection_id,
"scheme": record.scheme,
"payload": record.payload,
},
indent=2,
),
encoding="utf-8",
)
def load_auth(self, connection_id: str) -> AuthRecord | None:
path = self._auth_path(connection_id)
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
return AuthRecord(**data)
def list_auth_refs(self) -> list[str]:
"""Return auth refs present in the local file auth store."""
return sorted(path.stem for path in self.auth_dir.glob("*.json"))
def save_auth_record(self, record: NeutralAuthRecord) -> None:
"""Save neutral auth through the legacy MCP file shape."""
self.save_auth(mcp_auth_from_neutral(record))
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
"""Load neutral auth from the legacy MCP file shape."""
record = self.load_auth(auth_ref)
if record is None:
return None
return neutral_auth_from_mcp(record)
def delete_auth(self, connection_id: str) -> bool:
path = self._auth_path(connection_id)
if not path.exists():
return False
path.unlink()
return True
def delete_auth_record(self, auth_ref: str) -> bool:
"""Delete neutral auth through the legacy MCP file shape."""
return self.delete_auth(auth_ref)
class FileCatalogStore(CatalogStore):
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self.catalog_dir.mkdir(parents=True, exist_ok=True)
@property
def catalog_dir(self) -> Path:
return self.root / "catalog"
def _catalog_path(self, connection_id: str) -> Path:
return self._connection_path(self.catalog_dir, connection_id)
@staticmethod
def _connection_path(directory: Path, connection_id: str) -> Path:
"""Map one validated connection id to one file inside a store directory."""
parse_connection_id(connection_id)
root = directory.resolve()
path = (directory / f"{connection_id}.json").resolve()
if path.parent != root:
raise ValueError(
f"connection id escapes store directory: {connection_id!r}"
)
return path
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
self._catalog_path(snapshot.connection_id).write_text(
json.dumps(dump_catalog_snapshot(snapshot), indent=2),
encoding="utf-8",
)
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
path = self._catalog_path(connection_id)
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
return CatalogSnapshot(
connection_id=data["connection_id"],
fetched_at_epoch_ms=data["fetched_at_epoch_ms"],
max_age_seconds=data["max_age_seconds"],
nodes=[CatalogNodeEntry(**node) for node in data.get("nodes", [])],
resources=[
CatalogResourceEntry(**resource)
for resource in data.get("resources", [])
],
prompts=[
CatalogPromptEntry(**prompt) for prompt in data.get("prompts", [])
],
metadata=data.get("metadata", {}),
)
class FileStore(Store):
"""Compatibility file store that combines auth and catalog stores."""
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self._auth = FileAuthStore(root)
self._catalog = FileCatalogStore(root)
@property
def auth_dir(self) -> Path:
return self._auth.auth_dir
@property
def catalog_dir(self) -> Path:
return self._catalog.catalog_dir
def save_auth(self, record: AuthRecord) -> None:
self._auth.save_auth(record)
def load_auth(self, connection_id: str) -> AuthRecord | None:
return self._auth.load_auth(connection_id)
def list_auth_refs(self) -> list[str]:
return self._auth.list_auth_refs()
def save_auth_record(self, record: NeutralAuthRecord) -> None:
self._auth.save_auth_record(record)
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
return self._auth.load_auth_record(auth_ref)
def delete_auth(self, connection_id: str) -> bool:
return self._auth.delete_auth(connection_id)
def delete_auth_record(self, auth_ref: str) -> bool:
return self._auth.delete_auth_record(auth_ref)
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
self._catalog.save_catalog(snapshot)
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
return self._catalog.load_catalog(connection_id)
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
from .auth import (
AuthRecord,
auth_missing_diagnostic,
auth_ref_for_connection,
connection_auth_diagnostic,
mcp_auth_env,
mcp_auth_from_neutral,
mcp_auth_headers,
neutral_auth_from_mcp,
)
__all__ = [
"AuthRecord",
"auth_missing_diagnostic",
"auth_ref_for_connection",
"connection_auth_diagnostic",
"mcp_auth_env",
"mcp_auth_from_neutral",
"mcp_auth_headers",
"neutral_auth_from_mcp",
]
+153
View File
@@ -0,0 +1,153 @@
"""MCP upstream-source auth helpers.
This module is canonical for MCP-as-source auth interpretation. The temporary
TYPE_CHECKING dependency on `wf_mcp.broker.models.ConnectionConfig` exists until
connection runtime DTOs move out of the compatibility MCP facade.
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity
if TYPE_CHECKING:
from wf_mcp.broker.models import ConnectionConfig
@dataclass(slots=True)
class AuthRecord:
connection_id: str
scheme: str
payload: dict[str, Any] = field(default_factory=dict)
def mcp_auth_from_neutral(record: NeutralAuthRecord) -> AuthRecord:
"""Adapt neutral auth to the current MCP compatibility record."""
return AuthRecord(
connection_id=record.id,
scheme=record.scheme,
payload=dict(record.payload),
)
def neutral_auth_from_mcp(record: AuthRecord) -> NeutralAuthRecord:
"""Adapt legacy MCP auth into the neutral record shape."""
return NeutralAuthRecord(
id=record.connection_id,
scheme=record.scheme,
payload=dict(record.payload),
)
def mcp_auth_headers(auth: AuthRecord | None) -> dict[str, str]:
"""Return HTTP headers understood by MCP HTTP transports.
This is intentionally MCP-specific. Neutral code must not inspect payload
keys such as `headers` or `token`.
"""
if auth is None:
return {}
raw_headers = auth.payload.get("headers", {})
headers = (
{
str(key): str(value)
for key, value in raw_headers.items()
if isinstance(key, str) and isinstance(value, str)
}
if isinstance(raw_headers, dict)
else {}
)
token = auth.payload.get("token")
if isinstance(token, str) and "Authorization" not in headers:
headers["Authorization"] = f"Bearer {token}"
return headers
def mcp_auth_env(auth: AuthRecord | None) -> dict[str, str]:
"""Return environment variables understood by MCP stdio transports."""
if auth is None:
return {}
raw_env = auth.payload.get("env", {})
if not isinstance(raw_env, dict):
return {}
return {
str(key): str(value)
for key, value in raw_env.items()
if isinstance(key, str) and isinstance(value, str)
}
def auth_ref_for_connection(connection: ConnectionConfig) -> str | None:
"""Return the explicit auth ref for one source connection, if present."""
auth_ref = connection.metadata.get("auth_ref")
return auth_ref if isinstance(auth_ref, str) else None
def auth_missing_diagnostic(
*,
auth_ref: str,
source_id: str,
logical_ref: str | None = None,
) -> DependencyDiagnostic:
"""Build a stable diagnostic without including secret payload data."""
return DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR,
code="auth_not_found",
logical_ref=logical_ref or "",
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."
),
)
def connection_auth_diagnostic(
connection: ConnectionConfig,
*,
load_auth_ref: Callable[[str], AuthRecord | None],
logical_ref: str | None = None,
) -> DependencyDiagnostic | None:
"""Return an auth diagnostic for explicit auth_ref misses.
Connections without explicit auth_ref keep legacy no-auth behavior. This
makes the new auth boundary observable without treating every unauthenticated
MCP source as an error.
"""
auth_ref = auth_ref_for_connection(connection)
if auth_ref is None:
return None
if load_auth_ref(auth_ref) is not None:
return None
return auth_missing_diagnostic(
auth_ref=auth_ref,
source_id=connection.id,
logical_ref=logical_ref,
)
__all__ = [
"AuthRecord",
"auth_missing_diagnostic",
"auth_ref_for_connection",
"connection_auth_diagnostic",
"mcp_auth_env",
"mcp_auth_from_neutral",
"mcp_auth_headers",
"neutral_auth_from_mcp",
]
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
from .store import (
AuthStore,
CatalogStore,
FileAuthStore,
FileCatalogStore,
FileStore,
Store,
)
__all__ = [
"AuthStore",
"CatalogStore",
"FileAuthStore",
"FileCatalogStore",
"FileStore",
"Store",
]
+222
View File
@@ -0,0 +1,222 @@
"""MCP upstream-source auth and catalog file stores.
These stores preserve the current MCP compatibility JSON shapes. Catalog entry
types still come from `wf_mcp` until catalog DTOs finish moving to a neutral or
source-provider package.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_api.auth import validate_auth_id
from wf_sources_mcp.auth import AuthRecord, mcp_auth_from_neutral, neutral_auth_from_mcp
if TYPE_CHECKING:
from wf_mcp.catalog.models import CatalogSnapshot
class AuthStore:
def save_auth(self, record: AuthRecord) -> None:
raise NotImplementedError
def load_auth(self, connection_id: str) -> AuthRecord | None:
raise NotImplementedError
def list_auth_refs(self) -> list[str]:
raise NotImplementedError
def save_auth_record(self, record: NeutralAuthRecord) -> None:
raise NotImplementedError
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
raise NotImplementedError
def delete_auth(self, connection_id: str) -> bool:
raise NotImplementedError
def delete_auth_record(self, auth_ref: str) -> bool:
raise NotImplementedError
class CatalogStore:
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
raise NotImplementedError
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
raise NotImplementedError
class Store(AuthStore, CatalogStore):
"""Compatibility store combining MCP auth and catalog/cache storage."""
class FileAuthStore(AuthStore):
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self.auth_dir.mkdir(parents=True, exist_ok=True)
@property
def auth_dir(self) -> Path:
return self.root / "auth"
def _auth_path(self, auth_ref: str) -> Path:
validate_auth_id(auth_ref)
root = self.auth_dir.resolve()
path = (self.auth_dir / f"{auth_ref}.json").resolve()
if path.parent != root:
raise ValueError(f"auth ref escapes store directory: {auth_ref!r}")
return path
def save_auth(self, record: AuthRecord) -> None:
self._auth_path(record.connection_id).write_text(
json.dumps(
{
"connection_id": record.connection_id,
"scheme": record.scheme,
"payload": record.payload,
},
indent=2,
),
encoding="utf-8",
)
def load_auth(self, connection_id: str) -> AuthRecord | None:
path = self._auth_path(connection_id)
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
return AuthRecord(**data)
def list_auth_refs(self) -> list[str]:
return sorted(path.stem for path in self.auth_dir.glob("*.json"))
def save_auth_record(self, record: NeutralAuthRecord) -> None:
self.save_auth(mcp_auth_from_neutral(record))
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
record = self.load_auth(auth_ref)
if record is None:
return None
return neutral_auth_from_mcp(record)
def delete_auth(self, connection_id: str) -> bool:
path = self._auth_path(connection_id)
if not path.exists():
return False
path.unlink()
return True
def delete_auth_record(self, auth_ref: str) -> bool:
return self.delete_auth(auth_ref)
class FileCatalogStore(CatalogStore):
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self.catalog_dir.mkdir(parents=True, exist_ok=True)
@property
def catalog_dir(self) -> Path:
return self.root / "catalog"
def _catalog_path(self, connection_id: str) -> Path:
return self._connection_path(self.catalog_dir, connection_id)
@staticmethod
def _connection_path(directory: Path, connection_id: str) -> Path:
from wf_mcp.connections import parse_connection_id
parse_connection_id(connection_id)
root = directory.resolve()
path = (directory / f"{connection_id}.json").resolve()
if path.parent != root:
raise ValueError(
f"connection id escapes store directory: {connection_id!r}"
)
return path
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
from wf_mcp.catalog.models import dump_catalog_snapshot
self._catalog_path(snapshot.connection_id).write_text(
json.dumps(dump_catalog_snapshot(snapshot), indent=2),
encoding="utf-8",
)
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
from wf_mcp.capabilities import (
CatalogNodeEntry,
CatalogPromptEntry,
CatalogResourceEntry,
)
from wf_mcp.catalog.models import CatalogSnapshot as CatalogSnapshotType
path = self._catalog_path(connection_id)
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
return CatalogSnapshotType(
connection_id=data["connection_id"],
fetched_at_epoch_ms=data["fetched_at_epoch_ms"],
max_age_seconds=data["max_age_seconds"],
nodes=[CatalogNodeEntry(**node) for node in data.get("nodes", [])],
resources=[
CatalogResourceEntry(**resource)
for resource in data.get("resources", [])
],
prompts=[
CatalogPromptEntry(**prompt) for prompt in data.get("prompts", [])
],
metadata=data.get("metadata", {}),
)
class FileStore(Store):
"""Compatibility file store that combines auth and catalog stores."""
def __init__(self, root: Path) -> None:
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
self._auth = FileAuthStore(root)
self._catalog = FileCatalogStore(root)
@property
def auth_dir(self) -> Path:
return self._auth.auth_dir
@property
def catalog_dir(self) -> Path:
return self._catalog.catalog_dir
def save_auth(self, record: AuthRecord) -> None:
self._auth.save_auth(record)
def load_auth(self, connection_id: str) -> AuthRecord | None:
return self._auth.load_auth(connection_id)
def list_auth_refs(self) -> list[str]:
return self._auth.list_auth_refs()
def save_auth_record(self, record: NeutralAuthRecord) -> None:
self._auth.save_auth_record(record)
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
return self._auth.load_auth_record(auth_ref)
def delete_auth(self, connection_id: str) -> bool:
return self._auth.delete_auth(connection_id)
def delete_auth_record(self, auth_ref: str) -> bool:
return self._auth.delete_auth_record(auth_ref)
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
self._catalog.save_catalog(snapshot)
def load_catalog(self, connection_id: str) -> CatalogSnapshot | None:
return self._catalog.load_catalog(connection_id)
+18
View File
@@ -51,3 +51,21 @@ def test_models_module_reexports_canonical_model_owners() -> None:
assert CompatBrokerConfig is BrokerConfig assert CompatBrokerConfig is BrokerConfig
assert CompatCatalogSnapshot is CatalogSnapshot assert CompatCatalogSnapshot is CatalogSnapshot
assert CompatConnectionConfig is ConnectionConfig assert CompatConnectionConfig is ConnectionConfig
def test_wf_mcp_auth_shim_reexports_wf_sources_mcp_auth() -> None:
from wf_mcp.auth import AuthRecord as CompatAuthRecord
from wf_sources_mcp.auth import AuthRecord
assert CompatAuthRecord is AuthRecord
def test_wf_mcp_storage_shim_reexports_wf_sources_mcp_storage() -> None:
from wf_mcp.storage import FileAuthStore as CompatFileAuthStore
from wf_mcp.storage import FileCatalogStore as CompatFileCatalogStore
from wf_mcp.storage import FileStore as CompatFileStore
from wf_sources_mcp.storage import FileAuthStore, FileCatalogStore, FileStore
assert CompatFileAuthStore is FileAuthStore
assert CompatFileCatalogStore is FileCatalogStore
assert CompatFileStore is FileStore
@@ -0,0 +1,73 @@
from __future__ import annotations
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_mcp.catalog.models import CatalogSnapshot
from wf_sources_mcp.auth import (
AuthRecord,
mcp_auth_env,
mcp_auth_from_neutral,
mcp_auth_headers,
neutral_auth_from_mcp,
)
from wf_sources_mcp.storage import FileAuthStore, FileCatalogStore, FileStore
def test_wf_sources_mcp_auth_round_trips_neutral_record() -> None:
neutral = NeutralAuthRecord(
id="github.work",
scheme="bearer",
payload={"token": "secret", "env": {"GITHUB_TOKEN": "secret"}},
)
mcp = mcp_auth_from_neutral(neutral)
round_trip = neutral_auth_from_mcp(mcp)
assert isinstance(mcp, AuthRecord)
assert mcp.connection_id == "github.work"
assert round_trip.id == "github.work"
assert round_trip.scheme == "bearer"
assert round_trip.payload["token"] == "secret"
def test_wf_sources_mcp_auth_adapters_interpret_mcp_payload() -> None:
auth = AuthRecord(
connection_id="github.work",
scheme="bearer",
payload={
"token": "secret",
"headers": {"X-Test": "yes"},
"env": {"GITHUB_TOKEN": "secret"},
},
)
assert mcp_auth_headers(auth) == {
"X-Test": "yes",
"Authorization": "Bearer secret",
}
assert mcp_auth_env(auth) == {"GITHUB_TOKEN": "secret"}
def test_wf_sources_mcp_file_stores_keep_existing_disk_shape(tmp_path) -> None:
auth_store = FileAuthStore(tmp_path / "auth-root")
catalog_store = FileCatalogStore(tmp_path / "catalog-root")
combined_store = FileStore(tmp_path / "combined-root")
auth = AuthRecord(connection_id="demo.personal", scheme="bearer")
snapshot = CatalogSnapshot(
connection_id="demo.personal",
fetched_at_epoch_ms=1,
max_age_seconds=300,
nodes=[],
resources=[],
prompts=[],
metadata={},
)
auth_store.save_auth(auth)
catalog_store.save_catalog(snapshot)
combined_store.save_auth(auth)
combined_store.save_catalog(snapshot)
assert (tmp_path / "auth-root" / "auth" / "demo.personal.json").exists()
assert (tmp_path / "catalog-root" / "catalog" / "demo.personal.json").exists()
assert (tmp_path / "combined-root" / "auth" / "demo.personal.json").exists()
assert (tmp_path / "combined-root" / "catalog" / "demo.personal.json").exists()
@@ -0,0 +1,39 @@
from __future__ import annotations
import ast
from pathlib import Path
FORBIDDEN_WF_MCP_PREFIXES = (
"wf_mcp.admin_surface",
"wf_mcp.workflow_surface",
"wf_mcp.server",
"wf_mcp.proxy",
"wf_mcp.cli",
)
def test_wf_sources_mcp_does_not_import_frontend_mcp_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
violations: list[str] = []
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module is not None:
if node.module.startswith(FORBIDDEN_WF_MCP_PREFIXES):
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith(FORBIDDEN_WF_MCP_PREFIXES):
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp imports frontend/proxy MCP modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
)