Files
Unraid-AppOps-Gateway-Public/backend/app/mcp_server.py
T

926 lines
37 KiB
Python

from __future__ import annotations
import hashlib
import json
import os
from collections import defaultdict
from datetime import UTC, datetime
from functools import wraps
from pathlib import Path
from threading import Lock
from time import perf_counter
from typing import Any
from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
from mcp.types import ToolAnnotations
from sqlmodel import Session, select
from .api.routes_actions import action_registry as build_action_registry
from .api.routes_system import system_summary as build_system_summary
from .connectors.gitea_actions import GiteaActionsConnector
from .core.config import settings
from .core.redaction import redact_message, redact_value
from .core.time import utc_now
from .core.version import APP_VERSION
from .db.models import AppSetting, AuditEvent
from .db.session import engine
from .schemas.deployment import DeploymentPlanV1
from .schemas.enrollment import EnrollmentPlanV1
from .schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from .schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from .services import knowledge_service, mcp_operations, safety_center_service
from .services.action_execution import mcp_execution_supported
from .services.action_registry import ACTION_RISK, ACTIONS, action_capability
from .services.app_registry import list_active_apps
from .services.audit_logger import log_event
from .services.deployment_broker import DeploymentBrokerClient
from .services.deployment_control_plane import (
DeploymentControlError,
DeploymentControlPlane,
)
from .services.enrollment_plane import EnrollmentError, EnrollmentPlane
from .services.gitea_project_auto_registration import GiteaProjectAutoRegistration
from .services.inventory_discovery import list_inventory_containers
from .services.policy_engine import evaluate_policy
from .services.privatemuse_artifact_installer import ArtifactInstallError
from .services.privatemuse_artifact_installer import install as install_artifact_file
from .services.privatemuse_artifact_installer import preflight as artifact_preflight
from .services.runtime_service import runtime_info
from .services.runner_statistics import RunnerStatisticsService
_SESSION_RUNNING = False
_SESSION_VERIFIED_AT: str | None = None
_TOOL_METRICS: dict[str, dict[str, float | int | str | None]] = defaultdict(
lambda: {
"calls": 0,
"errors": 0,
"totalMs": 0.0,
"maxMs": 0.0,
"lastCalledAt": None,
}
)
_TOOL_METRICS_LOCK = Lock()
_TOOL_METRICS_SETTING = "observability:mcp_tool_metrics"
_CANARY_STATUS: dict[str, Any] = {
"status": "never",
"lastCheckedAt": None,
"durationMs": None,
}
_MCP_EXECUTION_LOCK = Lock()
_ARTIFACT_INSTALL_LOCK = Lock()
_SELF_UPGRADE_LOCK = Lock()
def _artifact_roots() -> tuple[Path, Path]:
return (Path(os.environ.get("APPOPS_PRIVATEMUSE_COMFYUI_MODEL_ROOT", "/mnt/privatemuse-comfyui-models")),
Path(os.environ.get("APPOPS_PRIVATEMUSE_ARTIFACT_RECEIPT_ROOT", "/data/privatemuse-artifact-receipts")))
_READ_ONLY_TOOL = ToolAnnotations(
read_only_hint=True,
destructive_hint=False,
idempotent_hint=True,
open_world_hint=False,
)
_OPERATION_RUN_TOOL = ToolAnnotations(
read_only_hint=False,
destructive_hint=True,
idempotent_hint=True,
open_world_hint=False,
)
_LEGACY_APPROVAL_EXECUTION_TOOL = ToolAnnotations(
read_only_hint=False,
destructive_hint=True,
idempotent_hint=True,
open_world_hint=False,
)
_CANCEL_TOOL = ToolAnnotations(
read_only_hint=False,
destructive_hint=False,
idempotent_hint=True,
open_world_hint=False,
)
def _tracked(tool_name: str):
def decorate(function):
@wraps(function)
def wrapped(*args, **kwargs):
started = perf_counter()
failed = False
try:
return function(*args, **kwargs)
except Exception:
failed = True
raise
finally:
duration_ms = (perf_counter() - started) * 1000
with _TOOL_METRICS_LOCK:
metrics = _TOOL_METRICS[tool_name]
metrics["calls"] = int(metrics["calls"]) + 1
metrics["errors"] = int(metrics["errors"]) + int(failed)
metrics["totalMs"] = float(metrics["totalMs"]) + duration_ms
metrics["maxMs"] = max(float(metrics["maxMs"]), duration_ms)
metrics["lastCalledAt"] = datetime.now(UTC).isoformat()
return wrapped
return decorate
def set_session_running(value: bool) -> None:
global _SESSION_RUNNING, _SESSION_VERIFIED_AT
_SESSION_RUNNING = value
_SESSION_VERIFIED_AT = datetime.now(UTC).isoformat() if value else None
def runtime_status() -> dict[str, Any]:
with _TOOL_METRICS_LOCK:
tool_metrics = {
name: {
"calls": int(values["calls"]),
"errors": int(values["errors"]),
"averageMs": round(
float(values["totalMs"]) / max(1, int(values["calls"])), 2
),
"maxMs": round(float(values["maxMs"]), 2),
"lastCalledAt": values["lastCalledAt"],
}
for name, values in _TOOL_METRICS.items()
}
return {
"state": "listening"
if settings.mcp_enabled and _SESSION_RUNNING
else "configured"
if settings.mcp_enabled
else "disabled",
"lastVerifiedAt": _SESSION_VERIFIED_AT,
"toolCount": 25,
"toolMetrics": tool_metrics,
"canary": dict(_CANARY_STATUS),
}
def restore_tool_metrics(session: Session) -> int:
row = session.get(AppSetting, _TOOL_METRICS_SETTING)
payload = (row.value_json or {}).get("tools", {}) if row else {}
restored = 0
with _TOOL_METRICS_LOCK:
if _TOOL_METRICS:
return 0
for name, values in payload.items():
if not isinstance(values, dict):
continue
_TOOL_METRICS[str(name)[:120]] = {
"calls": max(0, int(values.get("calls", 0))),
"errors": max(0, int(values.get("errors", 0))),
"totalMs": max(0.0, float(values.get("totalMs", 0.0))),
"maxMs": max(0.0, float(values.get("maxMs", 0.0))),
"lastCalledAt": values.get("lastCalledAt"),
}
restored += 1
return restored
def persist_tool_metrics(session: Session) -> int:
with _TOOL_METRICS_LOCK:
tools = {name: dict(values) for name, values in _TOOL_METRICS.items()}
row = session.get(AppSetting, _TOOL_METRICS_SETTING) or AppSetting(
key=_TOOL_METRICS_SETTING
)
row.value_json = {"tools": tools, "persistedAt": datetime.now(UTC).isoformat()}
row.is_secret = False
row.updated_at = utc_now()
session.add(row)
session.commit()
return len(tools)
def run_read_only_canary() -> dict[str, Any]:
started = perf_counter()
try:
result = list_apps(limit=1)
status = "ok" if isinstance(result, dict) and "items" in result else "failed"
except Exception: # noqa: BLE001 - a canary must degrade safely on any read failure
status = "failed"
_CANARY_STATUS.update(
{
"status": status,
"lastCheckedAt": datetime.now(UTC).isoformat(),
"durationMs": round((perf_counter() - started) * 1000, 2),
}
)
return dict(_CANARY_STATUS)
appops_mcp = MCPServer(
"Unraid AppOps Gateway",
version=APP_VERSION,
instructions=(
"Private, governed AppOps context. Use read-only tools for inspection. "
"Authenticated MCP mutations are evaluated against central AppOps policy and execute immediately "
"through an allowlisted typed executor when possible. MCP never asks the operator for a human approval: "
"blocked actions stay blocked and actions without a typed executor fail closed. Never invent actions, "
"targets, policy state or execution results. Legacy approval status/execute tools exist only for approval "
"records created before this no-human-approval MCP contract."
),
)
@appops_mcp.tool(name="gitea.actions.runners.status", annotations=_READ_ONLY_TOOL)
@_tracked("gitea.actions.runners.status")
def gitea_actions_runners_status() -> dict[str, Any]:
"""Inspect Gitea Actions runners and queue age; this never restarts a runner."""
return _json(GiteaActionsConnector().snapshot())
@appops_mcp.tool(name="gitea.actions.runners.statistics", annotations=_READ_ONLY_TOOL)
@_tracked("gitea.actions.runners.statistics")
def gitea_actions_runner_statistics() -> dict[str, Any]:
"""Read bounded CI profile budgets, measured usage and job statistics."""
return _json(RunnerStatisticsService().snapshot())
def _json(value: Any) -> Any:
if hasattr(value, "model_dump"):
return _json(value.model_dump(mode="json"))
if isinstance(value, list):
return [_json(item) for item in value]
if isinstance(value, dict):
return redact_value({str(key): _json(item) for key, item in value.items()})
return redact_value(value)
@appops_mcp.tool(name="gitea.projects.catalog", annotations=_READ_ONLY_TOOL)
@_tracked("gitea.projects.catalog")
def gitea_projects_catalog() -> dict[str, Any]:
"""List the persisted owned Gitea project catalog and deployment registration state."""
with Session(engine) as session:
return _json(GiteaProjectAutoRegistration.catalog(session))
@appops_mcp.tool(name="gitea.projects.reconcile", annotations=_OPERATION_RUN_TOOL)
@_tracked("gitea.projects.reconcile")
def gitea_projects_reconcile() -> dict[str, Any]:
"""Reconcile all owned Gitea projects through the bounded registration plane."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
with Session(engine) as session:
return _json(GiteaProjectAutoRegistration().sync(session, force=True))
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("get_system_summary")
def get_system_summary() -> dict[str, Any]:
"""Get the current AppOps, Unraid and Portainer summary without changing state."""
with Session(engine) as session:
return _json(build_system_summary(session))
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("list_apps")
def list_apps(limit: int = 100) -> dict[str, Any]:
"""List managed applications and their current policy-safe metadata."""
with Session(engine) as session:
items = list_active_apps(session)[: max(1, min(limit, 250))]
return {"items": [_json(item) for item in items], "count": len(items)}
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("list_containers")
def list_containers(limit: int = 100) -> dict[str, Any]:
"""List the latest inventory container view; no Docker socket or shell access is used."""
with Session(engine) as session:
items = list_inventory_containers(session, include_ignored=False)[
: max(1, min(limit, 250))
]
return {"items": _json(items), "count": len(items)}
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("get_safety_findings")
def get_safety_findings() -> dict[str, Any]:
"""Get current safety summary and redacted findings."""
with Session(engine) as session:
return {
"summary": _json(safety_center_service.get_summary(session)),
"findings": _json(safety_center_service.get_findings(session)),
}
def _mcp_action_registry(session: Session) -> dict[str, Any]:
"""Add the MCP execution posture without changing manual/voice registry semantics."""
registry = build_action_registry(session)
direct = 0
blocked_mutations = 0
read_only = 0
items: list[dict[str, Any]] = []
for raw in registry.get("items", []):
item = dict(raw)
action = str(item.get("id") or "")
target = str(item.get("target") or "")
capability = str(item.get("capability") or "operator")
mcp_policy = evaluate_policy(
session, action=action, target=target, source="automation"
)
original_decision = str(mcp_policy.get("decision") or "blocked")
if capability == "read_only":
mcp_mode = "read_only"
mcp_supported = False
mcp_decision = original_decision
read_only += 1
elif original_decision == "blocked":
mcp_mode = "blocked"
mcp_supported = False
mcp_decision = "blocked"
blocked_mutations += 1
elif mcp_execution_supported(action):
mcp_mode = "direct_typed"
mcp_supported = True
mcp_decision = "allowed"
direct += 1
else:
mcp_mode = "blocked_no_typed_executor"
mcp_supported = False
mcp_decision = "blocked"
blocked_mutations += 1
items.append(
{
**item,
"manualPolicyDecision": item.get("policyDecision"),
"manualExecutionMode": item.get("executionMode"),
"mcpOriginalPolicyDecision": original_decision,
"mcpPolicyDecision": mcp_decision,
"mcpExecutionMode": mcp_mode,
"mcpExecutionSupported": mcp_supported,
"mcpApprovalRequired": False,
}
)
summary = {
**dict(registry.get("summary", {})),
"mcpDirectExecutable": direct,
"mcpBlockedMutations": blocked_mutations,
"mcpReadOnly": read_only,
"mcpApprovalRequired": 0,
}
return {"summary": summary, "items": items}
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("get_action_registry")
def get_action_registry() -> dict[str, Any]:
"""List typed AppOps actions plus the no-human-approval MCP execution posture."""
with Session(engine) as session:
return _json(_mcp_action_registry(session))
def _mcp_preview_mode(action: str, decision: str) -> tuple[str, bool]:
capability = action_capability(action)
if capability == "read_only":
return "read_only", False
if decision == "blocked":
return "blocked", False
if mcp_execution_supported(action):
return "direct_typed", True
return "blocked_no_typed_executor", False
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("preview_action")
def preview_action(action: str, target: str) -> dict[str, Any]:
"""Preview policy for an action. This never executes anything or creates an approval."""
safe_action = action[:160]
with Session(engine) as session:
decision = evaluate_policy(
session, action=safe_action, target=target[:240], source="automation"
)
mcp_mode, mcp_supported = _mcp_preview_mode(
safe_action, str(decision["decision"])
)
return {
"action": safe_action,
"target": target[:240],
"knownAction": safe_action in ACTIONS,
"risk": decision.get("risk", ACTION_RISK.get(safe_action, "critical")),
"policyDecision": decision["decision"],
"reason": redact_message(str(decision["reason"])),
"executed": False,
"mcpApprovalRequired": False,
"mcpExecutionSupported": mcp_supported,
"mcpExecutionMode": mcp_mode,
}
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("search_audit")
def search_audit(query: str = "", limit: int = 30) -> dict[str, Any]:
"""Search recent redacted audit metadata."""
needle = query.casefold().strip()[:200]
with Session(engine) as session:
rows = session.exec(
select(AuditEvent).order_by(AuditEvent.timestamp.desc()).limit(500)
).all()
matches = []
for row in rows:
value = f"{row.action_id} {row.target} {row.source} {row.result} {row.reason or ''}".casefold()
if needle and needle not in value:
continue
matches.append(
{
"id": row.id,
"timestamp": row.timestamp.isoformat(),
"actionId": row.action_id,
"target": row.target,
"source": row.source,
"risk": row.risk,
"policyDecision": row.policy_decision,
"result": row.result,
"reason": redact_message(str(row.reason or "")),
}
)
if len(matches) >= max(1, min(limit, 100)):
break
return _json({"items": matches, "count": len(matches)})
@appops_mcp.tool(annotations=_READ_ONLY_TOOL)
@_tracked("query_knowledge")
def query_knowledge(question: str, limit: int = 6) -> dict[str, Any]:
"""Retrieve grounded AppOps documentation and live context with citations."""
with Session(engine) as session:
return _json(
knowledge_service.query(session, question, limit=max(1, min(limit, 10)))
)
@appops_mcp.tool(name="server.operation.request", annotations=_OPERATION_RUN_TOOL)
@_tracked("server.operation.request")
def request_operation(
action: str,
target: str,
reason: str,
idempotencyKey: str,
) -> dict[str, Any]:
"""Run an exact governed server operation immediately when policy permits; MCP never creates a human approval."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
with Session(engine) as session:
return _json(
mcp_operations.request_operation(
session,
action=action,
target=target,
reason=reason,
idempotency_key=idempotencyKey,
)
)
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="privatemuse.artifact.install.preflight", annotations=_READ_ONLY_TOOL)
@_tracked("privatemuse.artifact.install.preflight")
def privatemuse_artifact_install_preflight(
artifact_type: str, source_url: str, sha256: str, filename: str,
source_id: str, license: str, reason: str,
) -> dict[str, Any]:
"""Check one pinned ComfyUI artifact against the fixed persistent model mount."""
payload = {"artifact_type": artifact_type, "source_url": source_url, "sha256": sha256,
"filename": filename, "source_id": source_id, "license": license}
if not reason.strip() or len(reason) > 1000:
raise ArtifactInstallError("a bounded reason is required")
model_root, _ = _artifact_roots()
return _json({**artifact_preflight(payload, model_root=model_root), "executed": False})
@appops_mcp.tool(name="privatemuse.artifact.install", annotations=_OPERATION_RUN_TOOL)
@_tracked("privatemuse.artifact.install")
def privatemuse_artifact_install(
artifact_type: str, source_url: str, sha256: str, filename: str,
source_id: str, license: str, reason: str, expected_preflight_fingerprint: str,
idempotency_key: str,
) -> dict[str, Any]:
"""Install one immutable SHA-pinned artifact into its server-selected ComfyUI directory."""
if not _ARTIFACT_INSTALL_LOCK.acquire(blocking=False):
raise ArtifactInstallError("artifact installation already active")
try:
if not reason.strip() or len(reason) > 1000 or not idempotency_key.strip() or len(idempotency_key) > 200:
raise ArtifactInstallError("bounded reason and idempotency key are required")
payload = {"artifact_type": artifact_type, "source_url": source_url, "sha256": sha256,
"filename": filename, "source_id": source_id, "license": license}
model_root, receipt_root = _artifact_roots()
key_hash = hashlib.sha256(idempotency_key.encode()).hexdigest()
request_fingerprint = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
request_dir = receipt_root / "mcp-requests"
request_path = request_dir / f"{key_hash}.json"
if request_path.exists():
previous = json.loads(request_path.read_text(encoding="utf-8"))
if previous.get("request_fingerprint") != request_fingerprint:
raise ArtifactInstallError("idempotency key already used for different artifact")
return _json(previous["result"])
checked = artifact_preflight(payload, model_root=model_root)
if checked["fingerprint"] != expected_preflight_fingerprint:
raise ArtifactInstallError("preflight changed; run preflight again")
result = install_artifact_file(payload, model_root=model_root, receipt_root=receipt_root)
with Session(engine) as session:
audit = log_event(session, source="action", identity="mcp", action_id="privatemuse.comfyui.install_artifact",
target="privatemuse/comfyui", risk="high", policy_decision="typed_contract_allowed",
reason=reason, result="success", after_state={"status": result["status"], "sha256": result["sha256"], "bytes": result["bytes"]},
redacted_metadata={"filename": filename, "source_id": source_id})
result["auditId"] = audit.id
request_dir.mkdir(parents=True, exist_ok=True)
temp_path = request_dir / f".{key_hash}.{os.getpid()}.tmp"
with temp_path.open("w", encoding="utf-8") as handle:
json.dump({"request_fingerprint": request_fingerprint, "result": result}, handle, sort_keys=True)
handle.flush()
os.fsync(handle.fileno())
os.replace(temp_path, request_path)
return _json(result)
finally:
_ARTIFACT_INSTALL_LOCK.release()
@appops_mcp.tool(name="server.operation.status", annotations=_READ_ONLY_TOOL)
@_tracked("server.operation.status")
def operation_status(approvalId: str) -> dict[str, Any]:
"""Legacy: read an approval record created before MCP no-human-approval mode."""
with Session(engine) as session:
return _json(mcp_operations.operation_status(session, approvalId))
@appops_mcp.tool(name="appops.deployment.status", annotations=_READ_ONLY_TOOL)
@_tracked("appops.deployment.status")
def appops_deployment_status() -> dict[str, Any]:
"""Report the exact OCI source revision baked into this AppOps image."""
with Session(engine) as session:
info = runtime_info(session)
return _json({"repository": info["sourceRepository"], "revision": info["sourceRevision"],
"sourceTree": info["sourceTree"], "health": "ok" if info["dbStatus"] == "ok" and info["liveReady"] else "degraded"})
@appops_mcp.tool(name="appops.self_upgrade.preflight", annotations=_READ_ONLY_TOOL)
@_tracked("appops.self_upgrade.preflight")
def appops_self_upgrade_preflight(
repository: str, revision: str, reason: str,
expected_current_revision: str | None = None,
) -> dict[str, Any]:
"""Check a fixed protected-main AppOps revision in the private controller."""
if repository != "jens/unraid-appops-gateway":
raise ValueError("repository must be jens/unraid-appops-gateway")
return _json(DeploymentBrokerClient().appops_self_upgrade_preflight(
revision, reason, expected_current_revision,
))
@appops_mcp.tool(name="appops.self_upgrade.apply", annotations=_OPERATION_RUN_TOOL)
@_tracked("appops.self_upgrade.apply")
def appops_self_upgrade_apply(
repository: str, revision: str, reason: str,
expected_preflight_fingerprint: str, idempotency_key: str,
expected_current_revision: str | None = None,
) -> dict[str, Any]:
"""Queue one exact revision through the controller's validated single-instance executor."""
if repository != "jens/unraid-appops-gateway":
raise ValueError("repository must be jens/unraid-appops-gateway")
if not _SELF_UPGRADE_LOCK.acquire(blocking=False):
raise RuntimeError("appops self-upgrade already active")
try:
with Session(engine) as session:
audit = log_event(
session, source="action", identity="mcp", action_id="appops.self_upgrade",
target="Unraid-AppOps-Gateway", risk="high",
policy_decision="typed_contract_requested",
reason="Typed self-upgrade reason SHA-256 " + hashlib.sha256(reason.encode("utf-8")).hexdigest(),
result="requested",
after_state={"revision": revision},
redacted_metadata={"repository": repository},
)
result = DeploymentBrokerClient().appops_self_upgrade_apply(
revision, reason, expected_current_revision,
expected_preflight_fingerprint, idempotency_key,
)
result["auditId"] = audit.id
return _json(result)
finally:
_SELF_UPGRADE_LOCK.release()
@appops_mcp.tool(name="appops.self_upgrade.status", annotations=_READ_ONLY_TOOL)
@_tracked("appops.self_upgrade.status")
def appops_self_upgrade_status(operation_id: str) -> dict[str, Any]:
"""Read the controller's durable progress and exact deployment receipt."""
return _json(DeploymentBrokerClient().appops_self_upgrade_status(operation_id))
@appops_mcp.tool(name="server.operation.execute", annotations=_LEGACY_APPROVAL_EXECUTION_TOOL)
@_tracked("server.operation.execute")
def execute_operation(
approvalId: str, expectedActionFingerprint: str
) -> dict[str, Any]:
"""Legacy: execute an already-approved pre-existing approval record; new MCP calls never create one."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
with Session(engine) as session:
return _json(
mcp_operations.execute_operation(
session,
approval_id=approvalId,
expected_action_fingerprint=expectedActionFingerprint,
)
)
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="server.job.status", annotations=_READ_ONLY_TOOL)
@_tracked("server.job.status")
def job_status(jobId: str) -> dict[str, Any]:
"""Read bounded progress and the redacted result of one server-side AppOps job."""
with Session(engine) as session:
return _json(mcp_operations.job_status(session, jobId))
@appops_mcp.tool(name="server.job.cancel", annotations=_CANCEL_TOOL)
@_tracked("server.job.cancel")
def cancel_job(jobId: str) -> dict[str, Any]:
"""Cancel a queued or running AppOps job record; completed jobs remain immutable."""
with Session(engine) as session:
return _json(mcp_operations.cancel_operation_job(session, jobId))
@appops_mcp.tool(name="deployment.preflight", annotations=_READ_ONLY_TOOL)
@_tracked("deployment.preflight")
def deployment_preflight(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Validate a strict V1 plan or fixed Forge bootstrap profile read-only."""
try:
if plan.get("api_version") == "itworx.forge-bootstrap-preflight/v1":
from .services.forge_bootstrap_preflight import (
ForgeBootstrapPreflightService,
)
profile = ForgeBootstrapPreflightV1.model_validate(plan)
with Session(engine) as session:
return _json(
ForgeBootstrapPreflightService().preflight(
session, profile, idempotency_key=idempotencyKey
)
)
if "api_version" in plan:
return {
"status": "invalid",
"executed": False,
"reason": "Unsupported deployment.preflight profile discriminator.",
}
typed_plan = DeploymentPlanV1.model_validate(plan)
with Session(engine) as session:
return _json(
DeploymentControlPlane().execute(
session,
plan=typed_plan,
idempotency_key=idempotencyKey,
operation="preflight",
)
)
except (ValueError, DeploymentControlError) as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
@appops_mcp.tool(name="forge.bootstrap.apply", annotations=_OPERATION_RUN_TOOL)
@_tracked("forge.bootstrap.apply")
def forge_bootstrap_apply(profile: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Apply one passed, exact-profile Forge first install through its fixed broker route."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
typed_profile = ForgeBootstrapApplyV1.model_validate(profile)
from .services.forge_bootstrap_apply import (
ForgeBootstrapApplyService,
)
with Session(engine) as session:
return _json(
ForgeBootstrapApplyService().apply(
session,
typed_profile,
idempotency_key=idempotencyKey,
)
)
except ValueError as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="forge.bootstrap.status", annotations=_READ_ONLY_TOOL)
@_tracked("forge.bootstrap.status")
def forge_bootstrap_status(jobId: str) -> dict[str, Any]:
"""Reconcile one existing Forge first-install job using the signed read-only route."""
from .services.forge_bootstrap_apply import ForgeBootstrapApplyService
with Session(engine) as session:
result = ForgeBootstrapApplyService().status(session, jobId)
return _json(result or {"status": "not_found"})
@appops_mcp.tool(name="forge.bootstrap.resume", annotations=_OPERATION_RUN_TOOL)
@_tracked("forge.bootstrap.resume")
def forge_bootstrap_resume(jobId: str) -> dict[str, Any]:
"""Resume only a signed, pre-migration Forge database-credentials pause."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
from .services.forge_bootstrap_apply import ForgeBootstrapApplyService
with Session(engine) as session:
result = ForgeBootstrapApplyService().resume(session, jobId)
return _json(result or {"status": "not_found"})
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="deployment.capabilities", annotations=_READ_ONLY_TOOL)
@_tracked("deployment.capabilities")
def deployment_capabilities() -> dict[str, Any]:
"""Read machine-actionable enrollment and deployment readiness without mutations."""
with Session(engine) as session:
return _json(EnrollmentPlane().capabilities(session))
@appops_mcp.tool(name="enrollment.preflight", annotations=_READ_ONLY_TOOL)
@_tracked("enrollment.preflight")
def enrollment_preflight(plan: dict[str, Any]) -> dict[str, Any]:
"""Read-only takeover-safe preflight for one strict EnrollmentPlanV1."""
try:
typed_plan = EnrollmentPlanV1.model_validate(plan)
with Session(engine) as session:
return _json(EnrollmentPlane().preflight(session, typed_plan))
except (ValueError, EnrollmentError) as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
@appops_mcp.tool(name="target.enroll", annotations=_OPERATION_RUN_TOOL)
@_tracked("target.enroll")
def target_enroll(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Enroll exactly one clean-profile application identity; no arbitrary target is accepted."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
typed_plan = EnrollmentPlanV1.model_validate(plan)
with Session(engine) as session:
return _json(EnrollmentPlane().enroll(session, typed_plan, idempotency_key=idempotencyKey))
except (ValueError, EnrollmentError) as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="enrollment.status", annotations=_READ_ONLY_TOOL)
@_tracked("enrollment.status")
def enrollment_status(enrollmentId: str) -> dict[str, Any]:
"""Read a redacted durable enrollment state and its restart-safe job evidence."""
with Session(engine) as session:
try:
return _json(EnrollmentPlane().get_enrollment(session, enrollmentId))
except EnrollmentError as exc:
return {"status": "not_found", "reason": redact_message(str(exc))}
@appops_mcp.tool(name="deployment.apply", annotations=_OPERATION_RUN_TOOL)
@_tracked("deployment.apply")
def deployment_apply(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Submit one preflighted typed deployment to the private controller."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
typed_plan = DeploymentPlanV1.model_validate(plan)
with Session(engine) as session:
return _json(
DeploymentControlPlane().execute(
session,
plan=typed_plan,
idempotency_key=idempotencyKey,
operation="apply",
)
)
except (ValueError, DeploymentControlError) as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="deployment.status", annotations=_READ_ONLY_TOOL)
@_tracked("deployment.status")
def deployment_status(jobId: str) -> dict[str, Any]:
"""Read durable deployment state and refresh a bounded broker request."""
if jobId.startswith("forge-install-"):
return {
"status": "invalid",
"reason": "Use forge.bootstrap.status for Forge first-install jobs.",
}
with Session(engine) as session:
try:
return _json(DeploymentControlPlane().refresh(session, jobId))
except DeploymentControlError as exc:
return {"status": "not_found", "reason": redact_message(str(exc))}
@appops_mcp.tool(name="deployment.verify", annotations=_READ_ONLY_TOOL)
@_tracked("deployment.verify")
def deployment_verify(jobId: str) -> dict[str, Any]:
"""Refresh the controller receipt of an exact previously submitted deployment."""
return deployment_status(jobId)
@appops_mcp.tool(name="deployment.rollback", annotations=_OPERATION_RUN_TOOL)
@_tracked("deployment.rollback")
def deployment_rollback(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Ask the controller to run its receipt-bound rollback for one typed plan."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
typed_plan = DeploymentPlanV1.model_validate(plan)
if not typed_plan.rollback_enabled:
return {"status": "blocked", "executed": False, "reason": "rollback_disabled_by_typed_plan"}
with Session(engine) as session:
return _json(
DeploymentControlPlane().execute(
session,
plan=typed_plan,
idempotency_key=idempotencyKey,
operation="rollback",
)
)
except (ValueError, DeploymentControlError) as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.resource("appops://capabilities", mime_type="application/json")
def capabilities() -> str:
"""Describe the immutable security boundary of this MCP server."""
return json.dumps(
{
"scope": "governed_operations",
"transport": "streamable_http",
"mutationTools": True,
"directShell": False,
"dockerSocket": False,
"secrets": "excluded",
"execution": "typed AppOps executors immediately after central policy evaluation",
"approvalRequiredForMcpMutations": False,
"humanApprovalWorkflowExposedByMcp": False,
"blockedActionsRemainBlocked": True,
"unsupportedActionsFailClosed": True,
"legacyApprovalCompatibility": True,
"maxConcurrentExecutions": 1,
"deploymentControlPlane": {
"version": "v1",
"typedPlanOnly": True,
"rawCompose": False,
"directShell": False,
"brokerEvidenceRequired": True,
},
"enrollmentProvisioningPlane": {
"version": "v1",
"typedPlanOnly": True,
"profileControlled": True,
"rawCompose": False,
"bootstrapCredentials": "write_only",
"wildcardPolicies": False,
},
"giteaProjectRegistration": {
"version": "v1",
"automatic": True,
"catalogTool": "gitea.projects.catalog",
"reconcileTool": "gitea.projects.reconcile",
"wildcardDeploymentPolicy": False,
},
}
)
mcp_http_app = appops_mcp.streamable_http_app(
streamable_http_path="/",
stateless_http=True,
json_response=True,
max_request_body_size=1_048_576,
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=list(settings.mcp_allowed_hosts or []),
allowed_origins=[settings.frontend_origin],
),
)