from __future__ import annotations import os from pathlib import Path from typing import Any from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.context import Context from connectors.unraid import RegisteredContainer, UnraidConnector from connectors.unraid.client import FileBrokerTokenProvider, HttpBrokerClient from packages.connector_kit import ConnectorContext, PageRequest from .common import ( connector_context, deterministic_id, load_connector_identity_key, read_regular_json, request_headers, ) from .mcp_server import add_operational_routes, add_workflow_prompts, run_mcp def build_connector() -> tuple[UnraidConnector, set[str]]: path = Path(os.environ.get("UNRAID_REGISTRATIONS_FILE", "/etc/itworx/registered-targets.json")) document = read_regular_json(path) registrations: list[RegisteredContainer] = [] projects: set[str] = set() for item in document.get("containers", []): if not isinstance(item, dict): raise ValueError("Unraid registration contains an invalid entry") project = str(item.get("project", "")).strip().lower() environment = str(item.get("environment", "")).strip().lower() registered_id = str(item.get("registeredId", "")).strip() registrations.append( RegisteredContainer( registered_id, deterministic_id("project", project), environment, ) ) projects.add(project) if not projects: raise ValueError("at least one Unraid project registration is required") client = HttpBrokerClient( os.environ.get("BROKER_URL", "http://docker-read-broker:8080"), FileBrokerTokenProvider( os.environ.get("BROKER_TOKEN_FILE", "/run/secrets/docker_broker_service_token") ), ) return UnraidConnector(client, tuple(registrations)), projects def build_server() -> MCPServer[Any]: connector, projects = build_connector() signing_key = load_connector_identity_key() server: MCPServer[Any] = MCPServer( "itworx-unraid", title="ITWorx Unraid read-only connector", version="1.0.0", instructions=( "Only broker-registered, bounded read operations are available. Inspect health " "before requesting logs, keep log windows small, distinguish observed facts from " "hypotheses, and never claim or request a container mutation." ), ) add_operational_routes(server, "unraid-connector") add_workflow_prompts(server, "unraid") def current(context: Context[Any, Any]) -> ConnectorContext: return connector_context(request_headers(context), projects, signing_key=signing_key) @server.tool(name="unraid.host.status", structured_output=True) async def host_status(context: Context[Any, Any]) -> dict[str, Any]: return connector.host_status(current(context)) @server.tool(name="unraid.project.summary", structured_output=True) async def project_summary(context: Context[Any, Any]) -> dict[str, Any]: return connector.project_summary(current(context)) @server.tool(name="unraid.storage.list", structured_output=True) async def storage_list( context: Context[Any, Any], limit: int = 25, cursor: str | None = None ) -> dict[str, Any]: return connector.storage_list( current(context), page=PageRequest(limit=limit, cursor=cursor) ) @server.tool(name="unraid.container.list", structured_output=True) async def container_list( context: Context[Any, Any], state: str | None = None, limit: int = 25, cursor: str | None = None, ) -> dict[str, Any]: return connector.container_list( current(context), state=state, page=PageRequest(limit=limit, cursor=cursor) ) @server.tool(name="unraid.container.inspect", structured_output=True) async def container_inspect(containerRef: str, context: Context[Any, Any]) -> dict[str, Any]: return connector.container_inspect(current(context), containerRef) @server.tool(name="unraid.container.health", structured_output=True) async def container_health(containerRef: str, context: Context[Any, Any]) -> dict[str, Any]: return connector.container_health(current(context), containerRef) @server.tool(name="unraid.container.logs", structured_output=True) async def container_logs( containerRef: str, context: Context[Any, Any], tail: int = 200, sinceSeconds: int | None = None, contains: str | None = None, ) -> dict[str, Any]: return connector.container_logs( current(context), containerRef, tail=tail, since_seconds=sinceSeconds, contains=contains, ) @server.tool(name="unraid.network.list", structured_output=True) async def network_list( context: Context[Any, Any], limit: int = 25, cursor: str | None = None ) -> dict[str, Any]: return connector.network_list( current(context), page=PageRequest(limit=limit, cursor=cursor) ) return server def run() -> None: run_mcp(build_server())