refactor: move mcp source registry to wf_sources_mcp
This commit is contained in:
@@ -279,6 +279,10 @@ implementation state.
|
|||||||
Keep `wf_mcp` re-export shims for compatibility and add import-direction
|
Keep `wf_mcp` re-export shims for compatibility and add import-direction
|
||||||
tests so `wf_sources_mcp` does not depend on workflow/admin surface,
|
tests so `wf_sources_mcp` does not depend on workflow/admin surface,
|
||||||
frontend server, or proxy modules.
|
frontend server, or proxy modules.
|
||||||
|
Second `wf_sources_mcp` slice complete: MCP desired source registry
|
||||||
|
models, file store, and conversion helpers now live in
|
||||||
|
`wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained
|
||||||
|
as a compatibility shim.
|
||||||
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
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
|
|||||||
|
|
||||||
1. Complete: MCP auth helpers and focused auth/catalog stores moved to
|
1. Complete: MCP auth helpers and focused auth/catalog stores moved to
|
||||||
`wf_sources_mcp`, with `wf_mcp` shims preserved.
|
`wf_sources_mcp`, with `wf_mcp` shims preserved.
|
||||||
2. MCP source registry models/conversion.
|
2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim.
|
||||||
3. Upstream transport/discovery/session services.
|
3. Upstream transport/discovery/session services.
|
||||||
|
|
||||||
Each slice should add import-direction tests so the new source-provider package
|
Each slice should add import-direction tests so the new source-provider package
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ 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.source_registry import (
|
||||||
|
FileSourceRegistryStore,
|
||||||
|
workflow_mcp_source_to_connection_config,
|
||||||
|
)
|
||||||
from wf_sources_mcp.storage import FileAuthStore, FileCatalogStore, FileStore
|
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
|
||||||
from ..runtime import McpRuntimePool, PersistentSessionFactory
|
from ..runtime import McpRuntimePool, PersistentSessionFactory
|
||||||
from ..sdk import McpSdkAdapter
|
from ..sdk import McpSdkAdapter
|
||||||
from ..source_registry import (
|
|
||||||
FileSourceRegistryStore,
|
|
||||||
workflow_mcp_source_to_connection_config,
|
|
||||||
)
|
|
||||||
from .models import BrokerStoreRoots
|
from .models import BrokerStoreRoots
|
||||||
from .service import WfMcpService
|
from .service import WfMcpService
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ from wf_api import (
|
|||||||
from wf_api.stores import WorkflowStores
|
from wf_api.stores import WorkflowStores
|
||||||
from wf_config import WorkflowConfigFile
|
from wf_config import WorkflowConfigFile
|
||||||
from wf_server import WorkflowServer, WorkflowServerConfig
|
from wf_server import WorkflowServer, WorkflowServerConfig
|
||||||
|
from wf_sources_mcp.source_registry import FileSourceRegistryStore, SourceRegistryStore
|
||||||
|
|
||||||
from ..models import BrokerConfig
|
from ..models import BrokerConfig
|
||||||
from ..sdk.adapter import McpSdkAdapter
|
from ..sdk.adapter import McpSdkAdapter
|
||||||
from ..source_registry import FileSourceRegistryStore, SourceRegistryStore
|
|
||||||
from .artifact_tools import register_artifact_tools
|
from .artifact_tools import register_artifact_tools
|
||||||
from .config import broker_config_from_workflow_config, build_service_from_config
|
from .config import broker_config_from_workflow_config, build_service_from_config
|
||||||
from .prompts import register_broker_prompts
|
from .prompts import register_broker_prompts
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from ...connections import ConnectionRegistry, parse_connection_id
|
from wf_sources_mcp.source_registry import (
|
||||||
from ...models import BrokerConfig, ConnectionConfig
|
|
||||||
from ...shared.names import RESERVED_CONNECTION_IDS
|
|
||||||
from ...source_registry import (
|
|
||||||
SourceRegistryFile,
|
SourceRegistryFile,
|
||||||
SourceRegistryStore,
|
SourceRegistryStore,
|
||||||
connection_config_to_registry_entry,
|
connection_config_to_registry_entry,
|
||||||
registry_entry_to_connection_config,
|
registry_entry_to_connection_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ...connections import ConnectionRegistry, parse_connection_id
|
||||||
|
from ...models import BrokerConfig, ConnectionConfig
|
||||||
|
from ...shared.names import RESERVED_CONNECTION_IDS
|
||||||
from .events import BrokerEventRecorder
|
from .events import BrokerEventRecorder
|
||||||
from .source_catalog import SourceCatalogService
|
from .source_catalog import SourceCatalogService
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from wf_platform import (
|
|||||||
CapabilitySource,
|
CapabilitySource,
|
||||||
)
|
)
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
|
from wf_sources_mcp.source_registry import SourceRegistryStore
|
||||||
from wf_sources_mcp.storage import AuthStore, CatalogStore, Store
|
from wf_sources_mcp.storage import AuthStore, CatalogStore, Store
|
||||||
|
|
||||||
from ...connections import ConnectionRegistry
|
from ...connections import ConnectionRegistry
|
||||||
@@ -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 ..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
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider
|
from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider
|
||||||
|
from wf_sources_mcp.source_registry import (
|
||||||
from ...auth import AuthRecord, connection_auth_diagnostic
|
|
||||||
from ...models import BrokerConfig, ConnectionConfig
|
|
||||||
from ...source_registry import (
|
|
||||||
McpSourceRegistryEntry,
|
McpSourceRegistryEntry,
|
||||||
SourceRegistryFile,
|
SourceRegistryFile,
|
||||||
SourceRegistryStore,
|
SourceRegistryStore,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ...auth import AuthRecord, connection_auth_diagnostic
|
||||||
|
from ...models import BrokerConfig, ConnectionConfig
|
||||||
from .connection_service import ConnectionService
|
from .connection_service import ConnectionService
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from fastmcp import FastMCP
|
|||||||
from fastmcp.client import Client
|
from fastmcp.client import Client
|
||||||
from fastmcp.client.transports.memory import FastMCPTransport
|
from fastmcp.client.transports.memory import FastMCPTransport
|
||||||
|
|
||||||
|
from wf_sources_mcp.source_registry import FileSourceRegistryStore
|
||||||
|
|
||||||
from ..admin_surface import register_service_admin_tools
|
from ..admin_surface import register_service_admin_tools
|
||||||
from ..broker.config import build_service_from_config
|
from ..broker.config import build_service_from_config
|
||||||
from ..broker.transport import normalize_transport
|
from ..broker.transport import normalize_transport
|
||||||
@@ -14,7 +16,6 @@ from ..documentation import build_local_documentation_source
|
|||||||
from ..models import BrokerConfig
|
from ..models import BrokerConfig
|
||||||
from ..proxy.runtime import ProxyRuntime
|
from ..proxy.runtime import ProxyRuntime
|
||||||
from ..sdk import McpSdkAdapter
|
from ..sdk import McpSdkAdapter
|
||||||
from ..source_registry import FileSourceRegistryStore
|
|
||||||
from ..workflow_surface import register_workflow_tools
|
from ..workflow_surface import register_workflow_tools
|
||||||
from .prompts import register_documentation_prompts
|
from .prompts import register_documentation_prompts
|
||||||
from .resources import register_documentation_resources
|
from .resources import register_documentation_resources
|
||||||
|
|||||||
+16
-252
@@ -1,259 +1,23 @@
|
|||||||
|
"""Compatibility shim for MCP source registry models.
|
||||||
|
|
||||||
|
Canonical implementation lives in `wf_sources_mcp.source_registry`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from wf_sources_mcp.source_registry import (
|
||||||
from typing import TYPE_CHECKING, Annotated, Literal, Protocol
|
FileSourceRegistryStore,
|
||||||
|
HttpSourceTransport,
|
||||||
from pydantic import (
|
McpSourceRegistryEntry,
|
||||||
AnyHttpUrl,
|
SourceRegistryFile,
|
||||||
Field,
|
SourceRegistryStore,
|
||||||
field_validator,
|
SourceTransport,
|
||||||
model_validator,
|
StdioSourceTransport,
|
||||||
|
connection_config_to_registry_entry,
|
||||||
|
registry_entry_to_connection_config,
|
||||||
|
workflow_mcp_source_to_connection_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
from wf_api.source_registry import (
|
|
||||||
AtomicJsonRegistryStore,
|
|
||||||
SourceRegistryBaseModel,
|
|
||||||
validate_unique_source_ids,
|
|
||||||
)
|
|
||||||
from wf_api.source_registry import (
|
|
||||||
SourceRegistryStore as GenericSourceRegistryStore,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .connections import parse_connection_id
|
|
||||||
from .shared.names import RESERVED_CONNECTION_IDS
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .models import ConnectionConfig
|
|
||||||
|
|
||||||
_FLAT_HTTP_TRANSPORTS = {"http", "streamable-http", "streamable_http", "sse"}
|
|
||||||
_TRANSPORT_METADATA_KEYS = {
|
|
||||||
"transport",
|
|
||||||
"command",
|
|
||||||
"args",
|
|
||||||
"env",
|
|
||||||
"cwd",
|
|
||||||
"url",
|
|
||||||
"headers",
|
|
||||||
"profile",
|
|
||||||
"auth_ref",
|
|
||||||
"source_registry",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StdioSourceTransport(SourceRegistryBaseModel):
|
|
||||||
kind: Literal["stdio"] = "stdio"
|
|
||||||
command: str = Field(min_length=1)
|
|
||||||
args: tuple[str, ...] = ()
|
|
||||||
env: dict[str, str] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class HttpSourceTransport(SourceRegistryBaseModel):
|
|
||||||
kind: Literal["http"] = "http"
|
|
||||||
url: AnyHttpUrl
|
|
||||||
headers: dict[str, str] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
SourceTransport = Annotated[
|
|
||||||
StdioSourceTransport | HttpSourceTransport,
|
|
||||||
Field(discriminator="kind"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class McpSourceRegistryEntry(SourceRegistryBaseModel):
|
|
||||||
"""Desired MCP source configuration persisted by server-owned mutation."""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
kind: Literal["mcp"] = "mcp"
|
|
||||||
enabled: bool = True
|
|
||||||
provider: str = Field(min_length=1)
|
|
||||||
account: str = Field(min_length=1)
|
|
||||||
profile: str | None = None
|
|
||||||
transport: SourceTransport
|
|
||||||
auth_ref: str | None = None
|
|
||||||
metadata: dict[str, object] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
@field_validator("id")
|
|
||||||
@classmethod
|
|
||||||
def validate_id(cls, value: str) -> str:
|
|
||||||
parse_connection_id(value)
|
|
||||||
if value in RESERVED_CONNECTION_IDS:
|
|
||||||
raise ValueError(f"source id {value!r} is reserved")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class SourceRegistryFile(SourceRegistryBaseModel):
|
|
||||||
version: Literal[1] = 1
|
|
||||||
sources: list[McpSourceRegistryEntry] = Field(default_factory=list)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def validate_unique_source_ids(self) -> SourceRegistryFile:
|
|
||||||
validate_unique_source_ids(self.sources)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def source_map(self) -> dict[str, McpSourceRegistryEntry]:
|
|
||||||
return {source.id: source for source in self.sources}
|
|
||||||
|
|
||||||
|
|
||||||
class SourceRegistryStore(GenericSourceRegistryStore[SourceRegistryFile], Protocol):
|
|
||||||
"""MCP-specialized persistence boundary for desired source configuration."""
|
|
||||||
|
|
||||||
|
|
||||||
class FileSourceRegistryStore:
|
|
||||||
"""Filesystem implementation for desired source registry state."""
|
|
||||||
|
|
||||||
def __init__(self, root: Path) -> None:
|
|
||||||
self._delegate = AtomicJsonRegistryStore(
|
|
||||||
root,
|
|
||||||
filename="source_registry.json",
|
|
||||||
registry_type=SourceRegistryFile,
|
|
||||||
empty_factory=SourceRegistryFile,
|
|
||||||
corrupt_label="source registry file",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Path:
|
|
||||||
return self._delegate.path
|
|
||||||
|
|
||||||
def load_registry(self) -> SourceRegistryFile:
|
|
||||||
return self._delegate.load_registry()
|
|
||||||
|
|
||||||
def save_registry(self, registry: SourceRegistryFile) -> None:
|
|
||||||
self._delegate.save_registry(registry)
|
|
||||||
|
|
||||||
|
|
||||||
def registry_entry_to_connection_config(
|
|
||||||
entry: McpSourceRegistryEntry,
|
|
||||||
) -> ConnectionConfig:
|
|
||||||
"""Convert a registry entry to a broker connection config."""
|
|
||||||
from .models import ConnectionConfig
|
|
||||||
|
|
||||||
return ConnectionConfig(
|
|
||||||
id=entry.id,
|
|
||||||
server=entry.provider,
|
|
||||||
account=entry.account,
|
|
||||||
enabled=entry.enabled,
|
|
||||||
metadata={
|
|
||||||
**entry.metadata,
|
|
||||||
"auth_ref": entry.auth_ref,
|
|
||||||
"profile": entry.profile,
|
|
||||||
"transport": entry.transport.model_dump(mode="json"),
|
|
||||||
"source_registry": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def connection_config_to_registry_entry(
|
|
||||||
connection: ConnectionConfig,
|
|
||||||
) -> McpSourceRegistryEntry:
|
|
||||||
"""Materialize a seed config connection into persisted registry state.
|
|
||||||
|
|
||||||
Seed config is bootstrap-only. The registry entry must carry enough source
|
|
||||||
identity to become the future desired-state owner after first startup.
|
|
||||||
"""
|
|
||||||
transport = connection.metadata.get("transport")
|
|
||||||
legacy_transport_value: str | None = None
|
|
||||||
if isinstance(transport, dict):
|
|
||||||
pass
|
|
||||||
elif isinstance(transport, str):
|
|
||||||
if transport == "stdio":
|
|
||||||
transport = {
|
|
||||||
"kind": "stdio",
|
|
||||||
"command": connection.metadata.get("command", ""),
|
|
||||||
"args": list(connection.metadata.get("args", [])),
|
|
||||||
"env": dict(connection.metadata.get("env", {})),
|
|
||||||
}
|
|
||||||
elif transport in _FLAT_HTTP_TRANSPORTS:
|
|
||||||
legacy_transport_value = transport
|
|
||||||
transport = {
|
|
||||||
"kind": "http",
|
|
||||||
"url": connection.metadata.get("url", ""),
|
|
||||||
"headers": dict(connection.metadata.get("headers", {})),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"seed connection {connection.id!r} has unrecognized transport {transport!r}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"seed connection {connection.id!r} requires metadata.transport"
|
|
||||||
)
|
|
||||||
profile = connection.metadata.get("profile")
|
|
||||||
auth_ref = connection.metadata.get("auth_ref")
|
|
||||||
source_metadata = {
|
|
||||||
key: value
|
|
||||||
for key, value in connection.metadata.items()
|
|
||||||
if key not in _TRANSPORT_METADATA_KEYS
|
|
||||||
}
|
|
||||||
if legacy_transport_value is not None:
|
|
||||||
source_metadata["legacy_transport"] = legacy_transport_value
|
|
||||||
entry = McpSourceRegistryEntry.model_validate(
|
|
||||||
{
|
|
||||||
"id": connection.id,
|
|
||||||
"enabled": connection.enabled,
|
|
||||||
"provider": connection.server,
|
|
||||||
"account": connection.account,
|
|
||||||
"profile": profile if isinstance(profile, str) else None,
|
|
||||||
"transport": transport,
|
|
||||||
"auth_ref": auth_ref if isinstance(auth_ref, str) else None,
|
|
||||||
"metadata": source_metadata,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return entry
|
|
||||||
|
|
||||||
|
|
||||||
def workflow_mcp_source_to_connection_config(source: object) -> ConnectionConfig:
|
|
||||||
"""Convert neutral wf_config MCP source config into a broker connection.
|
|
||||||
|
|
||||||
Keep this adapter in wf_mcp because the output is MCP broker runtime state.
|
|
||||||
The input is intentionally typed as object to avoid making wf_mcp's public
|
|
||||||
registry module part of wf_config's import graph.
|
|
||||||
"""
|
|
||||||
from .models import ConnectionConfig
|
|
||||||
|
|
||||||
if getattr(source, "kind", None) != "mcp":
|
|
||||||
raise ValueError("expected wf_config MCP source")
|
|
||||||
for field in ("id", "provider", "account", "enabled", "ownership", "transport"):
|
|
||||||
if getattr(source, field, None) is None:
|
|
||||||
raise ValueError(f"wf_config MCP source missing required field: {field}")
|
|
||||||
transport = getattr(source, "transport")
|
|
||||||
metadata = dict(getattr(source, "metadata", {}))
|
|
||||||
if transport.kind == "stdio":
|
|
||||||
metadata.update(
|
|
||||||
{
|
|
||||||
"transport": "stdio",
|
|
||||||
"command": transport.command,
|
|
||||||
"args": list(transport.args),
|
|
||||||
"env": dict(transport.env),
|
|
||||||
"source_registry": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif transport.kind == "http":
|
|
||||||
metadata.update(
|
|
||||||
{
|
|
||||||
"transport": "streamable_http",
|
|
||||||
"url": str(transport.url),
|
|
||||||
"headers": dict(transport.headers),
|
|
||||||
"source_registry": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"unsupported wf_config MCP transport {transport.kind!r}")
|
|
||||||
profile = getattr(source, "profile", None)
|
|
||||||
if profile is not None:
|
|
||||||
metadata["profile"] = profile
|
|
||||||
auth_ref = getattr(source, "auth_ref", None)
|
|
||||||
if auth_ref is not None:
|
|
||||||
metadata["auth_ref"] = auth_ref
|
|
||||||
return ConnectionConfig(
|
|
||||||
id=getattr(source, "id"),
|
|
||||||
server=getattr(source, "provider"),
|
|
||||||
account=getattr(source, "account"),
|
|
||||||
enabled=getattr(source, "enabled"),
|
|
||||||
metadata=metadata,
|
|
||||||
source_config_ownership=getattr(source, "ownership"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"FileSourceRegistryStore",
|
"FileSourceRegistryStore",
|
||||||
"HttpSourceTransport",
|
"HttpSourceTransport",
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
"""MCP upstream-source provider helpers.
|
||||||
|
|
||||||
|
Source registry symbols are exported lazily because importing them eagerly pulls
|
||||||
|
in compatibility `wf_mcp` DTOs, which can re-enter this package through
|
||||||
|
`wf_mcp.auth` during startup.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .auth import (
|
from .auth import (
|
||||||
AuthRecord,
|
AuthRecord,
|
||||||
auth_missing_diagnostic,
|
auth_missing_diagnostic,
|
||||||
@@ -11,13 +20,56 @@ from .auth import (
|
|||||||
neutral_auth_from_mcp,
|
neutral_auth_from_mcp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .source_registry import (
|
||||||
|
FileSourceRegistryStore,
|
||||||
|
HttpSourceTransport,
|
||||||
|
McpSourceRegistryEntry,
|
||||||
|
SourceRegistryFile,
|
||||||
|
SourceRegistryStore,
|
||||||
|
SourceTransport,
|
||||||
|
StdioSourceTransport,
|
||||||
|
connection_config_to_registry_entry,
|
||||||
|
registry_entry_to_connection_config,
|
||||||
|
workflow_mcp_source_to_connection_config,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AuthRecord",
|
"AuthRecord",
|
||||||
|
"FileSourceRegistryStore",
|
||||||
|
"HttpSourceTransport",
|
||||||
|
"McpSourceRegistryEntry",
|
||||||
|
"SourceRegistryFile",
|
||||||
|
"SourceRegistryStore",
|
||||||
|
"SourceTransport",
|
||||||
|
"StdioSourceTransport",
|
||||||
"auth_missing_diagnostic",
|
"auth_missing_diagnostic",
|
||||||
"auth_ref_for_connection",
|
"auth_ref_for_connection",
|
||||||
"connection_auth_diagnostic",
|
"connection_auth_diagnostic",
|
||||||
|
"connection_config_to_registry_entry",
|
||||||
"mcp_auth_env",
|
"mcp_auth_env",
|
||||||
"mcp_auth_from_neutral",
|
"mcp_auth_from_neutral",
|
||||||
"mcp_auth_headers",
|
"mcp_auth_headers",
|
||||||
"neutral_auth_from_mcp",
|
"neutral_auth_from_mcp",
|
||||||
|
"registry_entry_to_connection_config",
|
||||||
|
"workflow_mcp_source_to_connection_config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> object:
|
||||||
|
if name in {
|
||||||
|
"FileSourceRegistryStore",
|
||||||
|
"HttpSourceTransport",
|
||||||
|
"McpSourceRegistryEntry",
|
||||||
|
"SourceRegistryFile",
|
||||||
|
"SourceRegistryStore",
|
||||||
|
"SourceTransport",
|
||||||
|
"StdioSourceTransport",
|
||||||
|
"connection_config_to_registry_entry",
|
||||||
|
"registry_entry_to_connection_config",
|
||||||
|
"workflow_mcp_source_to_connection_config",
|
||||||
|
}:
|
||||||
|
from . import source_registry
|
||||||
|
|
||||||
|
return getattr(source_registry, name)
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""MCP upstream-source registry models and conversion helpers.
|
||||||
|
|
||||||
|
This module is canonical for MCP-as-source desired registry state. The temporary
|
||||||
|
runtime dependency on `wf_mcp.models.ConnectionConfig` remains until broker
|
||||||
|
runtime DTOs move out of the compatibility MCP facade.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Annotated, Literal, Protocol
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AnyHttpUrl,
|
||||||
|
Field,
|
||||||
|
field_validator,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
from wf_api.source_registry import (
|
||||||
|
AtomicJsonRegistryStore,
|
||||||
|
SourceRegistryBaseModel,
|
||||||
|
validate_unique_source_ids,
|
||||||
|
)
|
||||||
|
from wf_api.source_registry import (
|
||||||
|
SourceRegistryStore as GenericSourceRegistryStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Temporary low-level compatibility imports. `wf_mcp.shared.names` currently
|
||||||
|
# pulls in FastMCP transitively; keep this visible until reserved-name parsing
|
||||||
|
# moves to a neutral/source package.
|
||||||
|
from wf_mcp.connections import parse_connection_id
|
||||||
|
from wf_mcp.shared.names import RESERVED_CONNECTION_IDS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wf_mcp.models import ConnectionConfig
|
||||||
|
|
||||||
|
_FLAT_HTTP_TRANSPORTS = {"http", "streamable-http", "streamable_http", "sse"}
|
||||||
|
_TRANSPORT_METADATA_KEYS = {
|
||||||
|
"transport",
|
||||||
|
"command",
|
||||||
|
"args",
|
||||||
|
"env",
|
||||||
|
"cwd",
|
||||||
|
"url",
|
||||||
|
"headers",
|
||||||
|
"profile",
|
||||||
|
"auth_ref",
|
||||||
|
"source_registry",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StdioSourceTransport(SourceRegistryBaseModel):
|
||||||
|
kind: Literal["stdio"] = "stdio"
|
||||||
|
command: str = Field(min_length=1)
|
||||||
|
args: tuple[str, ...] = ()
|
||||||
|
env: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class HttpSourceTransport(SourceRegistryBaseModel):
|
||||||
|
kind: Literal["http"] = "http"
|
||||||
|
url: AnyHttpUrl
|
||||||
|
headers: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
SourceTransport = Annotated[
|
||||||
|
StdioSourceTransport | HttpSourceTransport,
|
||||||
|
Field(discriminator="kind"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class McpSourceRegistryEntry(SourceRegistryBaseModel):
|
||||||
|
"""Desired MCP source configuration persisted by server-owned mutation."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
kind: Literal["mcp"] = "mcp"
|
||||||
|
enabled: bool = True
|
||||||
|
provider: str = Field(min_length=1)
|
||||||
|
account: str = Field(min_length=1)
|
||||||
|
profile: str | None = None
|
||||||
|
transport: SourceTransport
|
||||||
|
auth_ref: str | None = None
|
||||||
|
metadata: dict[str, object] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
@field_validator("id")
|
||||||
|
@classmethod
|
||||||
|
def validate_id(cls, value: str) -> str:
|
||||||
|
parse_connection_id(value)
|
||||||
|
if value in RESERVED_CONNECTION_IDS:
|
||||||
|
raise ValueError(f"source id {value!r} is reserved")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class SourceRegistryFile(SourceRegistryBaseModel):
|
||||||
|
version: Literal[1] = 1
|
||||||
|
sources: list[McpSourceRegistryEntry] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_unique_source_ids(self) -> SourceRegistryFile:
|
||||||
|
validate_unique_source_ids(self.sources)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def source_map(self) -> dict[str, McpSourceRegistryEntry]:
|
||||||
|
return {source.id: source for source in self.sources}
|
||||||
|
|
||||||
|
|
||||||
|
class SourceRegistryStore(GenericSourceRegistryStore[SourceRegistryFile], Protocol):
|
||||||
|
"""MCP-specialized persistence boundary for desired source configuration."""
|
||||||
|
|
||||||
|
|
||||||
|
class FileSourceRegistryStore:
|
||||||
|
"""Filesystem implementation for desired source registry state."""
|
||||||
|
|
||||||
|
def __init__(self, root: Path) -> None:
|
||||||
|
self._delegate = AtomicJsonRegistryStore(
|
||||||
|
root,
|
||||||
|
filename="source_registry.json",
|
||||||
|
registry_type=SourceRegistryFile,
|
||||||
|
empty_factory=SourceRegistryFile,
|
||||||
|
corrupt_label="source registry file",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> Path:
|
||||||
|
return self._delegate.path
|
||||||
|
|
||||||
|
def load_registry(self) -> SourceRegistryFile:
|
||||||
|
return self._delegate.load_registry()
|
||||||
|
|
||||||
|
def save_registry(self, registry: SourceRegistryFile) -> None:
|
||||||
|
self._delegate.save_registry(registry)
|
||||||
|
|
||||||
|
|
||||||
|
def registry_entry_to_connection_config(
|
||||||
|
entry: McpSourceRegistryEntry,
|
||||||
|
) -> ConnectionConfig:
|
||||||
|
"""Convert a registry entry to a broker connection config."""
|
||||||
|
from wf_mcp.models import ConnectionConfig
|
||||||
|
|
||||||
|
return ConnectionConfig(
|
||||||
|
id=entry.id,
|
||||||
|
server=entry.provider,
|
||||||
|
account=entry.account,
|
||||||
|
enabled=entry.enabled,
|
||||||
|
metadata={
|
||||||
|
**entry.metadata,
|
||||||
|
"auth_ref": entry.auth_ref,
|
||||||
|
"profile": entry.profile,
|
||||||
|
"transport": entry.transport.model_dump(mode="json"),
|
||||||
|
"source_registry": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def connection_config_to_registry_entry(
|
||||||
|
connection: ConnectionConfig,
|
||||||
|
) -> McpSourceRegistryEntry:
|
||||||
|
"""Materialize a seed config connection into persisted registry state.
|
||||||
|
|
||||||
|
Seed config is bootstrap-only. The registry entry must carry enough source
|
||||||
|
identity to become the future desired-state owner after first startup.
|
||||||
|
"""
|
||||||
|
transport = connection.metadata.get("transport")
|
||||||
|
legacy_transport_value: str | None = None
|
||||||
|
if isinstance(transport, dict):
|
||||||
|
pass
|
||||||
|
elif isinstance(transport, str):
|
||||||
|
if transport == "stdio":
|
||||||
|
transport = {
|
||||||
|
"kind": "stdio",
|
||||||
|
"command": connection.metadata.get("command", ""),
|
||||||
|
"args": list(connection.metadata.get("args", [])),
|
||||||
|
"env": dict(connection.metadata.get("env", {})),
|
||||||
|
}
|
||||||
|
elif transport in _FLAT_HTTP_TRANSPORTS:
|
||||||
|
legacy_transport_value = transport
|
||||||
|
transport = {
|
||||||
|
"kind": "http",
|
||||||
|
"url": connection.metadata.get("url", ""),
|
||||||
|
"headers": dict(connection.metadata.get("headers", {})),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"seed connection {connection.id!r} has unrecognized transport {transport!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"seed connection {connection.id!r} requires metadata.transport"
|
||||||
|
)
|
||||||
|
profile = connection.metadata.get("profile")
|
||||||
|
auth_ref = connection.metadata.get("auth_ref")
|
||||||
|
source_metadata = {
|
||||||
|
key: value
|
||||||
|
for key, value in connection.metadata.items()
|
||||||
|
if key not in _TRANSPORT_METADATA_KEYS
|
||||||
|
}
|
||||||
|
if legacy_transport_value is not None:
|
||||||
|
source_metadata["legacy_transport"] = legacy_transport_value
|
||||||
|
entry = McpSourceRegistryEntry.model_validate(
|
||||||
|
{
|
||||||
|
"id": connection.id,
|
||||||
|
"enabled": connection.enabled,
|
||||||
|
"provider": connection.server,
|
||||||
|
"account": connection.account,
|
||||||
|
"profile": profile if isinstance(profile, str) else None,
|
||||||
|
"transport": transport,
|
||||||
|
"auth_ref": auth_ref if isinstance(auth_ref, str) else None,
|
||||||
|
"metadata": source_metadata,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_mcp_source_to_connection_config(source: object) -> ConnectionConfig:
|
||||||
|
"""Convert neutral wf_config MCP source config into a broker connection.
|
||||||
|
|
||||||
|
This adapter remains source-provider code even though the output is the
|
||||||
|
temporary broker runtime DTO. The input is intentionally typed as object to
|
||||||
|
avoid making `wf_config` part of this package's import graph.
|
||||||
|
"""
|
||||||
|
from wf_mcp.models import ConnectionConfig
|
||||||
|
|
||||||
|
if getattr(source, "kind", None) != "mcp":
|
||||||
|
raise ValueError("expected wf_config MCP source")
|
||||||
|
for field in ("id", "provider", "account", "enabled", "ownership", "transport"):
|
||||||
|
if getattr(source, field, None) is None:
|
||||||
|
raise ValueError(f"wf_config MCP source missing required field: {field}")
|
||||||
|
transport = getattr(source, "transport")
|
||||||
|
metadata = dict(getattr(source, "metadata", {}))
|
||||||
|
if transport.kind == "stdio":
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": transport.command,
|
||||||
|
"args": list(transport.args),
|
||||||
|
"env": dict(transport.env),
|
||||||
|
"source_registry": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif transport.kind == "http":
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"transport": "streamable_http",
|
||||||
|
"url": str(transport.url),
|
||||||
|
"headers": dict(transport.headers),
|
||||||
|
"source_registry": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unsupported wf_config MCP transport {transport.kind!r}")
|
||||||
|
profile = getattr(source, "profile", None)
|
||||||
|
if profile is not None:
|
||||||
|
metadata["profile"] = profile
|
||||||
|
auth_ref = getattr(source, "auth_ref", None)
|
||||||
|
if auth_ref is not None:
|
||||||
|
metadata["auth_ref"] = auth_ref
|
||||||
|
return ConnectionConfig(
|
||||||
|
id=getattr(source, "id"),
|
||||||
|
server=getattr(source, "provider"),
|
||||||
|
account=getattr(source, "account"),
|
||||||
|
enabled=getattr(source, "enabled"),
|
||||||
|
metadata=metadata,
|
||||||
|
source_config_ownership=getattr(source, "ownership"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FileSourceRegistryStore",
|
||||||
|
"HttpSourceTransport",
|
||||||
|
"McpSourceRegistryEntry",
|
||||||
|
"SourceRegistryFile",
|
||||||
|
"SourceRegistryStore",
|
||||||
|
"SourceTransport",
|
||||||
|
"StdioSourceTransport",
|
||||||
|
"connection_config_to_registry_entry",
|
||||||
|
"registry_entry_to_connection_config",
|
||||||
|
"workflow_mcp_source_to_connection_config",
|
||||||
|
]
|
||||||
@@ -69,3 +69,18 @@ def test_wf_mcp_storage_shim_reexports_wf_sources_mcp_storage() -> None:
|
|||||||
assert CompatFileAuthStore is FileAuthStore
|
assert CompatFileAuthStore is FileAuthStore
|
||||||
assert CompatFileCatalogStore is FileCatalogStore
|
assert CompatFileCatalogStore is FileCatalogStore
|
||||||
assert CompatFileStore is FileStore
|
assert CompatFileStore is FileStore
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_mcp_source_registry_shim_reexports_wf_sources_mcp_registry() -> None:
|
||||||
|
from wf_mcp.source_registry import FileSourceRegistryStore as CompatFileStore
|
||||||
|
from wf_mcp.source_registry import McpSourceRegistryEntry as CompatEntry
|
||||||
|
from wf_mcp.source_registry import SourceRegistryFile as CompatFile
|
||||||
|
from wf_sources_mcp.source_registry import (
|
||||||
|
FileSourceRegistryStore,
|
||||||
|
McpSourceRegistryEntry,
|
||||||
|
SourceRegistryFile,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert CompatFileStore is FileSourceRegistryStore
|
||||||
|
assert CompatEntry is McpSourceRegistryEntry
|
||||||
|
assert CompatFile is SourceRegistryFile
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|||||||
import ast
|
import ast
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Temporary low-level wf_mcp imports are allowed for connection id parsing,
|
||||||
|
# reserved names, and broker DTO conversion. Frontend/proxy/workflow-surface
|
||||||
|
# imports are forbidden because wf_sources_mcp is upstream-source code.
|
||||||
FORBIDDEN_WF_MCP_PREFIXES = (
|
FORBIDDEN_WF_MCP_PREFIXES = (
|
||||||
"wf_mcp.admin_surface",
|
"wf_mcp.admin_surface",
|
||||||
"wf_mcp.workflow_surface",
|
"wf_mcp.workflow_surface",
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wf_mcp.models import ConnectionConfig
|
||||||
|
from wf_sources_mcp.source_registry import (
|
||||||
|
FileSourceRegistryStore,
|
||||||
|
HttpSourceTransport,
|
||||||
|
McpSourceRegistryEntry,
|
||||||
|
SourceRegistryFile,
|
||||||
|
StdioSourceTransport,
|
||||||
|
connection_config_to_registry_entry,
|
||||||
|
registry_entry_to_connection_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(source_id: str = "github.work") -> McpSourceRegistryEntry:
|
||||||
|
return McpSourceRegistryEntry(
|
||||||
|
id=source_id,
|
||||||
|
provider="github",
|
||||||
|
account="work",
|
||||||
|
transport=StdioSourceTransport(
|
||||||
|
command="npx",
|
||||||
|
args=("-y", "@modelcontextprotocol/server-github"),
|
||||||
|
env={"GITHUB_TOKEN": "${GITHUB_TOKEN}"},
|
||||||
|
),
|
||||||
|
auth_ref=source_id,
|
||||||
|
metadata={"purpose": "tests"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_registry_entry_keeps_identity_and_transport_structural() -> None:
|
||||||
|
entry = _entry()
|
||||||
|
|
||||||
|
assert entry.id == "github.work"
|
||||||
|
assert entry.provider == "github"
|
||||||
|
assert entry.account == "work"
|
||||||
|
assert entry.profile is None
|
||||||
|
assert entry.transport.kind == "stdio"
|
||||||
|
assert entry.transport.command == "npx"
|
||||||
|
assert entry.auth_ref == "github.work"
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_registry_accepts_http_transport() -> None:
|
||||||
|
entry = McpSourceRegistryEntry(
|
||||||
|
id="github.http",
|
||||||
|
provider="github",
|
||||||
|
account="work",
|
||||||
|
transport=HttpSourceTransport(url="https://example.test/mcp"), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entry.transport.kind == "http"
|
||||||
|
assert str(entry.transport.url) == "https://example.test/mcp"
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_registry_rejects_reserved_ids() -> None:
|
||||||
|
with pytest.raises(ValueError, match="reserved"):
|
||||||
|
_entry("wf.admin")
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_source_registry_store_round_trips_registry(tmp_path: Path) -> None:
|
||||||
|
store = FileSourceRegistryStore(tmp_path)
|
||||||
|
registry = SourceRegistryFile(sources=[_entry("github.work")])
|
||||||
|
|
||||||
|
store.save_registry(registry)
|
||||||
|
loaded = store.load_registry()
|
||||||
|
|
||||||
|
assert loaded.source_map()["github.work"].provider == "github"
|
||||||
|
assert loaded.source_map()["github.work"].transport.kind == "stdio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_source_registry_store_validates_loaded_registry(tmp_path: Path) -> None:
|
||||||
|
store = FileSourceRegistryStore(tmp_path)
|
||||||
|
store.path.write_text(
|
||||||
|
'{"version": 1, "sources": [{"id": "wf.admin", "provider": "wf", '
|
||||||
|
'"account": "admin", "transport": {"kind": "stdio", "command": "x"}}]}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="reserved"):
|
||||||
|
store.load_registry()
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_entry_to_connection_config_preserves_identity() -> None:
|
||||||
|
entry = _entry()
|
||||||
|
config = registry_entry_to_connection_config(entry)
|
||||||
|
|
||||||
|
assert config.id == "github.work"
|
||||||
|
assert config.server == "github"
|
||||||
|
assert config.account == "work"
|
||||||
|
assert config.enabled is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_entry_to_connection_config_preserves_transport_metadata() -> None:
|
||||||
|
entry = _entry()
|
||||||
|
entry.auth_ref = "github.work.auth"
|
||||||
|
config = registry_entry_to_connection_config(entry)
|
||||||
|
|
||||||
|
assert config.metadata["auth_ref"] == "github.work.auth"
|
||||||
|
assert config.metadata["profile"] is None
|
||||||
|
assert config.metadata["transport"]["kind"] == "stdio"
|
||||||
|
assert config.metadata["transport"]["command"] == "npx"
|
||||||
|
assert config.metadata["source_registry"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_entry_to_connection_config_preserves_user_metadata() -> None:
|
||||||
|
entry = _entry()
|
||||||
|
config = registry_entry_to_connection_config(entry)
|
||||||
|
|
||||||
|
assert config.metadata["purpose"] == "tests"
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_entry_to_connection_config_disabled_entry() -> None:
|
||||||
|
entry = _entry()
|
||||||
|
entry.enabled = False
|
||||||
|
config = registry_entry_to_connection_config(entry)
|
||||||
|
|
||||||
|
assert config.enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_config_to_registry_entry_preserves_transport_metadata() -> None:
|
||||||
|
connection = ConnectionConfig(
|
||||||
|
id="github.work",
|
||||||
|
server="github",
|
||||||
|
account="work",
|
||||||
|
enabled=False,
|
||||||
|
metadata={
|
||||||
|
"transport": {"kind": "stdio", "command": "npx", "args": ["server"]},
|
||||||
|
"profile": "corp",
|
||||||
|
"auth_ref": "secret://github/work",
|
||||||
|
"region": "us",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = connection_config_to_registry_entry(connection)
|
||||||
|
|
||||||
|
assert entry.id == "github.work"
|
||||||
|
assert entry.provider == "github"
|
||||||
|
assert entry.account == "work"
|
||||||
|
assert entry.enabled is False
|
||||||
|
assert entry.profile == "corp"
|
||||||
|
assert entry.auth_ref == "secret://github/work"
|
||||||
|
assert entry.transport.kind == "stdio"
|
||||||
|
assert entry.metadata["region"] == "us"
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_config_to_registry_entry_accepts_flat_stdio_metadata() -> None:
|
||||||
|
connection = ConnectionConfig(
|
||||||
|
id="github.work",
|
||||||
|
server="github",
|
||||||
|
account="work",
|
||||||
|
metadata={
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["server"],
|
||||||
|
"env": {"DEBUG": "1"},
|
||||||
|
"region": "us",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = connection_config_to_registry_entry(connection)
|
||||||
|
|
||||||
|
assert entry.transport.kind == "stdio"
|
||||||
|
assert isinstance(entry.transport, StdioSourceTransport)
|
||||||
|
assert entry.transport.command == "npx"
|
||||||
|
assert entry.transport.args == ("server",)
|
||||||
|
assert entry.transport.env == {"DEBUG": "1"}
|
||||||
|
assert entry.metadata == {"region": "us"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_config_to_registry_entry_accepts_flat_http_metadata() -> None:
|
||||||
|
connection = ConnectionConfig(
|
||||||
|
id="context7.default",
|
||||||
|
server="context7",
|
||||||
|
account="default",
|
||||||
|
metadata={
|
||||||
|
"transport": "sse",
|
||||||
|
"url": "http://127.0.0.1:3000/sse",
|
||||||
|
"headers": {"X-Test": "yes"},
|
||||||
|
"purpose": "legacy",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = connection_config_to_registry_entry(connection)
|
||||||
|
|
||||||
|
assert entry.transport.kind == "http"
|
||||||
|
assert isinstance(entry.transport, HttpSourceTransport)
|
||||||
|
assert str(entry.transport.url) == "http://127.0.0.1:3000/sse"
|
||||||
|
assert entry.transport.headers == {"X-Test": "yes"}
|
||||||
|
assert entry.metadata == {"purpose": "legacy", "legacy_transport": "sse"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_config_to_registry_entry_requires_transport_metadata() -> None:
|
||||||
|
connection = ConnectionConfig(id="github.work", server="github", account="work")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="requires metadata.transport"):
|
||||||
|
connection_config_to_registry_entry(connection)
|
||||||
Reference in New Issue
Block a user