Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,25 @@
|
||||
from .bridge import StdioBridge
|
||||
from .matrix import FakeVirtualServer, MatrixResult, run_matrix
|
||||
from .models import (
|
||||
BootstrapHandle,
|
||||
ClientConfig,
|
||||
ClientKind,
|
||||
ClientRecord,
|
||||
ClientRegistry,
|
||||
ClientStatus,
|
||||
generate_config,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BootstrapHandle",
|
||||
"ClientConfig",
|
||||
"ClientKind",
|
||||
"ClientRecord",
|
||||
"ClientRegistry",
|
||||
"ClientStatus",
|
||||
"FakeVirtualServer",
|
||||
"MatrixResult",
|
||||
"StdioBridge",
|
||||
"generate_config",
|
||||
"run_matrix",
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Minimal JSON-lines stdio bridge; tokens are read only for the call and never emitted."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class BridgeTransport(Protocol):
|
||||
def send(
|
||||
self, request: Mapping[str, Any], *, token: str, endpoint: str
|
||||
) -> Mapping[str, Any] | None: ...
|
||||
|
||||
|
||||
class StdioBridge:
|
||||
"""Translate bounded JSON-lines messages to remote MCP without shell or token output."""
|
||||
|
||||
ALLOWED_METHODS = frozenset(
|
||||
{
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
"ping",
|
||||
"tools/list",
|
||||
"tools/call",
|
||||
"resources/list",
|
||||
"resources/templates/list",
|
||||
"resources/read",
|
||||
"prompts/list",
|
||||
"prompts/get",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *, endpoint: str, token_env: str | None = None) -> None:
|
||||
if not endpoint.startswith("https://"):
|
||||
raise ValueError("stdio bridge endpoints must use HTTPS")
|
||||
self.endpoint = endpoint
|
||||
self.token_env = token_env or "ITWORX_MCP_ACCESS_TOKEN"
|
||||
|
||||
def forward(
|
||||
self,
|
||||
lines: Iterable[str],
|
||||
transport: BridgeTransport,
|
||||
*,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
token_provider: Callable[[], str] | None = None,
|
||||
) -> Iterator[str]:
|
||||
values = environ if environ is not None else os.environ
|
||||
static_token = values.get(self.token_env)
|
||||
if not static_token and token_provider is None:
|
||||
yield _error("authentication_required")
|
||||
return
|
||||
for raw in lines:
|
||||
if len(raw.encode("utf-8")) > 4 * 1024 * 1024:
|
||||
yield _error("request_too_large")
|
||||
continue
|
||||
try:
|
||||
request = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
yield _error("invalid_json")
|
||||
continue
|
||||
if not isinstance(request, dict) or request.get("method") not in self.ALLOWED_METHODS:
|
||||
yield _error("unsupported_method")
|
||||
continue
|
||||
try:
|
||||
token = token_provider() if token_provider is not None else static_token
|
||||
if not token:
|
||||
yield _error("authentication_required")
|
||||
continue
|
||||
result = transport.send(request, token=token, endpoint=self.endpoint)
|
||||
if result is not None:
|
||||
yield json.dumps(dict(result), separators=(",", ":"))
|
||||
except Exception:
|
||||
yield _error("upstream_unavailable")
|
||||
del static_token
|
||||
|
||||
|
||||
def _error(code: str) -> str:
|
||||
return json.dumps({"error": {"code": code}}, separators=(",", ":"))
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Deterministic MCP discovery/invocation matrix for supported client profiles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .models import ClientKind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MatrixResult:
|
||||
client_kind: ClientKind
|
||||
protocol_version: str
|
||||
connector: str
|
||||
tool: str
|
||||
discovered: bool
|
||||
invoked: bool
|
||||
detail: str
|
||||
|
||||
|
||||
class FakeVirtualServer:
|
||||
"""Offline project virtual server used for the client matrix and negative cases."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tools = {
|
||||
"gitea": "gitea.repository.get",
|
||||
"knowledge": "knowledge.document.search",
|
||||
"unraid": "unraid.container.inspect",
|
||||
}
|
||||
self.revoked: set[str] = set()
|
||||
|
||||
def discover(self, client_key: str, protocol_version: str) -> tuple[str, ...]:
|
||||
if client_key in self.revoked:
|
||||
raise PermissionError("client revoked")
|
||||
if protocol_version not in {"2026-07-28", "2025-11-25"}:
|
||||
raise ValueError("unsupported protocol version")
|
||||
return tuple(self.tools.values())
|
||||
|
||||
def invoke(
|
||||
self, client_key: str, tool: str, arguments: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
del arguments
|
||||
if client_key in self.revoked:
|
||||
raise PermissionError("client revoked")
|
||||
if tool not in self.tools.values():
|
||||
raise LookupError("tool not found")
|
||||
return {"tool": tool, "ok": True, "truncated": False}
|
||||
|
||||
|
||||
def run_matrix(server: FakeVirtualServer) -> tuple[MatrixResult, ...]:
|
||||
profiles = (
|
||||
(ClientKind.NATIVE_2026, "2026-07-28"),
|
||||
(ClientKind.COMPATIBILITY_2025, "2025-11-25"),
|
||||
(ClientKind.STDIO_BRIDGE, "2026-07-28"),
|
||||
(ClientKind.INSPECTOR, "2026-07-28"),
|
||||
)
|
||||
results: list[MatrixResult] = []
|
||||
for kind, revision in profiles:
|
||||
client_key = kind.value
|
||||
tools = server.discover(client_key, revision)
|
||||
for connector, tool in server.tools.items():
|
||||
invoked = bool(server.invoke(client_key, tool)["ok"])
|
||||
results.append(
|
||||
MatrixResult(
|
||||
kind, revision, connector, tool, tool in tools, invoked, "fixture success"
|
||||
)
|
||||
)
|
||||
return tuple(results)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Typed, secret-free machine-client lifecycle primitives for MCP onboarding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from threading import RLock
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ClientKind(StrEnum):
|
||||
NATIVE_2026 = "native-2026"
|
||||
COMPATIBILITY_2025 = "compatibility-2025"
|
||||
STDIO_BRIDGE = "stdio-bridge"
|
||||
INSPECTOR = "inspector"
|
||||
|
||||
|
||||
class ClientStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
ENABLED = "enabled"
|
||||
DISABLED = "disabled"
|
||||
REVOKED = "revoked"
|
||||
|
||||
|
||||
class ClientRecord(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
id: UUID
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
project: str = Field(min_length=1, max_length=63, pattern=r"^[a-z0-9][a-z0-9-]*$")
|
||||
environment: str = Field(min_length=1, max_length=32, pattern=r"^[a-z0-9][a-z0-9-]*$")
|
||||
kind: ClientKind
|
||||
status: ClientStatus
|
||||
credential_reference: str = Field(min_length=1, max_length=256)
|
||||
created_at: datetime
|
||||
last_seen_at: datetime | None = None
|
||||
disabled_at: datetime | None = None
|
||||
revoked_at: datetime | None = None
|
||||
|
||||
|
||||
class ClientConfig(BaseModel):
|
||||
"""Generated client configuration. It intentionally contains no token or secret."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
||||
|
||||
client_id: UUID = Field(serialization_alias="clientId")
|
||||
kind: ClientKind
|
||||
protocol_version: str = Field(serialization_alias="protocolVersion")
|
||||
endpoint: str
|
||||
transport: str
|
||||
scopes: tuple[str, ...]
|
||||
credential_reference: str = Field(serialization_alias="credentialReference")
|
||||
environment_variable: str | None = Field(
|
||||
default=None, serialization_alias="environmentVariable"
|
||||
)
|
||||
bridge_command: str | None = Field(default=None, serialization_alias="bridgeCommand")
|
||||
|
||||
|
||||
class BootstrapHandle(BaseModel):
|
||||
"""A one-time Authentik hand-off reference, never a bearer credential."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
||||
|
||||
client_id: UUID
|
||||
reference: str = Field(min_length=1, max_length=256)
|
||||
delivery: str = "authentik-service-account"
|
||||
expires_at: datetime = Field(serialization_alias="expiresAt")
|
||||
consumed: bool = False
|
||||
|
||||
|
||||
class ClientRegistry:
|
||||
"""In-memory development registry; production persistence is supplied by the control plane."""
|
||||
|
||||
def __init__(self, *, revocation_cache_ttl_seconds: int = 60) -> None:
|
||||
if not 1 <= revocation_cache_ttl_seconds <= 60:
|
||||
raise ValueError("revocation cache ceiling is 60 seconds")
|
||||
self.revocation_cache_ttl_seconds = revocation_cache_ttl_seconds
|
||||
self._clients: dict[UUID, ClientRecord] = {}
|
||||
self._bootstrap: dict[str, BootstrapHandle] = {}
|
||||
self._lock = RLock()
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
project: str,
|
||||
environment: str,
|
||||
kind: ClientKind,
|
||||
now: datetime | None = None,
|
||||
) -> ClientRecord:
|
||||
reference = _aware(now)
|
||||
client_id = uuid4()
|
||||
record = ClientRecord(
|
||||
id=client_id,
|
||||
name=name,
|
||||
project=project,
|
||||
environment=environment,
|
||||
kind=kind,
|
||||
status=ClientStatus.ENABLED,
|
||||
credential_reference=f"authentik://mcp-client/{client_id}",
|
||||
created_at=reference,
|
||||
)
|
||||
with self._lock:
|
||||
self._clients[client_id] = record
|
||||
return record
|
||||
|
||||
def list(self) -> tuple[ClientRecord, ...]:
|
||||
with self._lock:
|
||||
return tuple(self._clients.values())
|
||||
|
||||
def get(self, client_id: UUID) -> ClientRecord | None:
|
||||
with self._lock:
|
||||
return self._clients.get(client_id)
|
||||
|
||||
def disable(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
|
||||
return self._transition(client_id, ClientStatus.DISABLED, _aware(now))
|
||||
|
||||
def revoke(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
|
||||
return self._transition(client_id, ClientStatus.REVOKED, _aware(now))
|
||||
|
||||
def _transition(self, client_id: UUID, status: ClientStatus, now: datetime) -> ClientRecord:
|
||||
with self._lock:
|
||||
current = self._clients.get(client_id)
|
||||
if current is None:
|
||||
raise KeyError(client_id)
|
||||
updated = current.model_copy(
|
||||
update={
|
||||
"status": status,
|
||||
"disabled_at": now if status is ClientStatus.DISABLED else current.disabled_at,
|
||||
"revoked_at": now if status is ClientStatus.REVOKED else current.revoked_at,
|
||||
}
|
||||
)
|
||||
self._clients[client_id] = updated
|
||||
return updated
|
||||
|
||||
def touch(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
|
||||
reference = _aware(now)
|
||||
with self._lock:
|
||||
current = self._clients.get(client_id)
|
||||
if current is None:
|
||||
raise KeyError(client_id)
|
||||
if current.status is not ClientStatus.ENABLED:
|
||||
raise PermissionError("client is not enabled")
|
||||
updated = current.model_copy(update={"last_seen_at": reference})
|
||||
self._clients[client_id] = updated
|
||||
return updated
|
||||
|
||||
def authorize(self, client_id: UUID, *, project: str, environment: str) -> ClientRecord:
|
||||
record = self.get(client_id)
|
||||
if record is None or record.status is not ClientStatus.ENABLED:
|
||||
raise PermissionError("client is disabled or revoked")
|
||||
if record.project != project or record.environment != environment:
|
||||
raise PermissionError("client binding does not match endpoint scope")
|
||||
return record
|
||||
|
||||
def issue_bootstrap(
|
||||
self, client_id: UUID, *, now: datetime | None = None, ttl_seconds: int = 300
|
||||
) -> BootstrapHandle:
|
||||
if not 1 <= ttl_seconds <= 900:
|
||||
raise ValueError("bootstrap TTL must be between 1 and 900 seconds")
|
||||
if self.get(client_id) is None:
|
||||
raise KeyError(client_id)
|
||||
reference = _aware(now)
|
||||
handle = BootstrapHandle(
|
||||
client_id=client_id,
|
||||
reference=f"authentik-bootstrap://{uuid4()}",
|
||||
expires_at=reference + timedelta(seconds=ttl_seconds),
|
||||
)
|
||||
with self._lock:
|
||||
self._bootstrap[handle.reference] = handle
|
||||
return handle
|
||||
|
||||
def consume_bootstrap(self, reference: str, *, now: datetime | None = None) -> BootstrapHandle:
|
||||
current = _aware(now)
|
||||
with self._lock:
|
||||
handle = self._bootstrap.get(reference)
|
||||
if handle is None or handle.consumed or handle.expires_at <= current:
|
||||
raise PermissionError("bootstrap reference is invalid or expired")
|
||||
consumed = handle.model_copy(update={"consumed": True})
|
||||
self._bootstrap[reference] = consumed
|
||||
return consumed
|
||||
|
||||
|
||||
def _aware(value: datetime | None) -> datetime:
|
||||
result = value or datetime.now(UTC)
|
||||
if result.tzinfo is None or result.utcoffset() is None:
|
||||
raise ValueError("timestamps must be timezone-aware")
|
||||
return result.astimezone(UTC)
|
||||
|
||||
|
||||
def generate_config(record: ClientRecord, *, public_host: str = "mcp.example.com") -> ClientConfig:
|
||||
endpoint = f"https://{public_host}/mcp/{record.project}/{record.environment}"
|
||||
if record.kind is ClientKind.NATIVE_2026:
|
||||
return ClientConfig(
|
||||
client_id=record.id,
|
||||
kind=record.kind,
|
||||
protocol_version="2026-07-28",
|
||||
endpoint=endpoint,
|
||||
transport="streamable-http",
|
||||
scopes=("tools:read",),
|
||||
credential_reference=record.credential_reference,
|
||||
)
|
||||
if record.kind is ClientKind.COMPATIBILITY_2025:
|
||||
return ClientConfig(
|
||||
client_id=record.id,
|
||||
kind=record.kind,
|
||||
protocol_version="2025-11-25",
|
||||
endpoint=endpoint,
|
||||
transport="streamable-http",
|
||||
scopes=("tools:read",),
|
||||
credential_reference=record.credential_reference,
|
||||
)
|
||||
if record.kind is ClientKind.STDIO_BRIDGE:
|
||||
return ClientConfig(
|
||||
client_id=record.id,
|
||||
kind=record.kind,
|
||||
protocol_version="2026-07-28",
|
||||
endpoint=endpoint,
|
||||
transport="stdio",
|
||||
scopes=("tools:read",),
|
||||
credential_reference=record.credential_reference,
|
||||
environment_variable="ITWORX_MCP_ACCESS_TOKEN",
|
||||
bridge_command="itworx-mcp-bridge",
|
||||
)
|
||||
return ClientConfig(
|
||||
client_id=record.id,
|
||||
kind=record.kind,
|
||||
protocol_version="2026-07-28",
|
||||
endpoint=f"http://127.0.0.1:18442/mcp/{record.project}/{record.environment}",
|
||||
transport="streamable-http",
|
||||
scopes=("tools:read",),
|
||||
credential_reference=record.credential_reference,
|
||||
)
|
||||
Reference in new issue
Block a user