134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""Read-only alias projection from Hub-owned persistence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from collections import defaultdict
|
|
from typing import Any, cast
|
|
from urllib.parse import urlsplit
|
|
|
|
from pydantic import AnyHttpUrl, TypeAdapter
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from packages.hub_contracts.dataplane import (
|
|
SUPPORTED_PROTOCOLS,
|
|
AliasRecord,
|
|
ClientBinding,
|
|
EnvironmentName,
|
|
)
|
|
|
|
from .models import (
|
|
ClientBindingRow,
|
|
ClientRow,
|
|
EnvironmentRow,
|
|
ProjectRow,
|
|
VirtualServerRow,
|
|
)
|
|
|
|
|
|
class SqlAlchemyAliasSource:
|
|
"""Projects only enabled Hub virtual servers and active explicit bindings."""
|
|
|
|
def __init__(self, session: Session, *, workspace_id: Any, gateway_base_url: str) -> None:
|
|
self.session = session
|
|
self.workspace_id = workspace_id
|
|
base = TypeAdapter(AnyHttpUrl).validate_python(gateway_base_url)
|
|
internal_contextforge = (
|
|
base.scheme == "http"
|
|
and base.host in {"contextforge", "contextforge-control"}
|
|
and base.port == 4444
|
|
and (base.path or "").rstrip("/") == "/mcp"
|
|
)
|
|
if (
|
|
base.username is not None
|
|
or base.password is not None
|
|
or (base.scheme != "https" and not internal_contextforge)
|
|
):
|
|
raise ValueError("gateway URL must be HTTPS or the fixed internal gateway")
|
|
self.gateway_base_url = str(base).rstrip("/")
|
|
self.internal_contextforge = internal_contextforge
|
|
|
|
def aliases(self) -> tuple[AliasRecord, ...]:
|
|
projects = {
|
|
row.id: row
|
|
for row in self.session.scalars(
|
|
select(ProjectRow).where(ProjectRow.workspace_id == self.workspace_id)
|
|
).all()
|
|
}
|
|
environments = {
|
|
row.id: row
|
|
for row in self.session.scalars(
|
|
select(EnvironmentRow).where(EnvironmentRow.workspace_id == self.workspace_id)
|
|
).all()
|
|
}
|
|
clients = {
|
|
row.id: row
|
|
for row in self.session.scalars(
|
|
select(ClientRow).where(ClientRow.workspace_id == self.workspace_id)
|
|
).all()
|
|
}
|
|
bindings_by_server: dict[Any, list[ClientBinding]] = defaultdict(list)
|
|
for row in self.session.scalars(
|
|
select(ClientBindingRow).where(
|
|
ClientBindingRow.workspace_id == self.workspace_id,
|
|
ClientBindingRow.enabled.is_(True),
|
|
)
|
|
).all():
|
|
client = clients.get(row.client_id)
|
|
if client is None or client.status != "active":
|
|
continue
|
|
bindings_by_server[row.virtual_server_id].append(
|
|
ClientBinding(
|
|
client_id=client.id,
|
|
issuer=client.issuer,
|
|
subject=client.subject,
|
|
enabled=True,
|
|
)
|
|
)
|
|
values: list[AliasRecord] = []
|
|
for server in self.session.scalars(
|
|
select(VirtualServerRow).where(
|
|
VirtualServerRow.workspace_id == self.workspace_id,
|
|
VirtualServerRow.enabled.is_(True),
|
|
VirtualServerRow.health_state.in_(("healthy", "degraded")),
|
|
)
|
|
).all():
|
|
if self.internal_contextforge and not server.gateway_external_id:
|
|
continue
|
|
project = projects.get(server.project_id)
|
|
environment = environments.get(server.environment_id)
|
|
if project is None or environment is None:
|
|
continue
|
|
if environment.name not in {"dev", "test", "staging", "prod"}:
|
|
continue
|
|
metadata = server.record_metadata
|
|
endpoint_alias = metadata.get("endpointAlias") if isinstance(metadata, dict) else None
|
|
if not isinstance(endpoint_alias, str):
|
|
endpoint_alias = f"/mcp/{project.slug}/{environment.name}"
|
|
try:
|
|
if self.internal_contextforge:
|
|
external_id = server.gateway_external_id or ""
|
|
if not re.fullmatch(r"[0-9a-f]{32}", external_id):
|
|
raise ValueError("invalid ContextForge server route identifier")
|
|
parsed = urlsplit(self.gateway_base_url)
|
|
upstream = f"{parsed.scheme}://{parsed.netloc}/servers/{external_id}/mcp"
|
|
else:
|
|
upstream = f"{self.gateway_base_url}{endpoint_alias}"
|
|
values.append(
|
|
AliasRecord(
|
|
project_slug=project.slug,
|
|
workspace_id=self.workspace_id,
|
|
project_id=project.id,
|
|
environment=cast(EnvironmentName, environment.name),
|
|
alias=endpoint_alias,
|
|
upstream_url=TypeAdapter(AnyHttpUrl).validate_python(upstream),
|
|
enabled=True,
|
|
protocol_revisions=tuple(SUPPORTED_PROTOCOLS),
|
|
bindings=tuple(bindings_by_server.get(server.id, ())),
|
|
)
|
|
)
|
|
except ValueError:
|
|
continue
|
|
return tuple(sorted(values, key=lambda item: (item.project_slug, item.environment)))
|