137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
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())
|