"""MCP-Server, ihre entdeckten Tools und die Freischaltung je Workspace (→ Konzept 7.1–7.3). Zwei Arten von Servern in einer Tabelle: - zentral (workspace_id leer): vom Plattform-Admin registriert, von Workspaces freigeschaltet - Verbindung eines Workspace (workspace_id gesetzt): vom Workspace-Admin angelegt, nur dort nutzbar; eine neue URL braucht die Freigabe eines Plattform-Admins. Datenbanken sind solche Verbindungen zu einem zentral registrierten DB-Dienst (template_id), mit den Zugangsdaten als Header. """ from datetime import datetime from typing import Any from sqlalchemy import JSON, Boolean, ForeignKey, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base, now # Freigabe einer Verbindung durch einen Plattform-Admin APPROVED, PENDING, REJECTED = "approved", "pending", "rejected" class McpServer(Base): """Server mit Streamable HTTP; Endpunkte nur aus AIW_MCP_ALLOWED_HOSTS.""" __tablename__ = "mcp_servers" id: Mapped[int] = mapped_column(primary_key=True) # Leer: zentraler Server; gesetzt: Verbindung nur dieses Workspace workspace_id: Mapped[int | None] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), index=True) # Datenbank-Verbindung: der zentrale DB-Dienst, dessen URL und Schlüssel sie nutzt template_id: Mapped[int | None] = mapped_column(ForeignKey("mcp_servers.id", ondelete="RESTRICT")) # Kurzname, Präfix der Tool-Namen (__) name: Mapped[str] = mapped_column(String(16), unique=True) label: Mapped[str] = mapped_column(String(200)) url: Mapped[str] = mapped_column(String(500)) # Bearer-Token, verschlüsselt; ein Workspace kann ein eigenes setzen (z. B. Munin-Key je Workspace) secret: Mapped[str | None] = mapped_column(Text) secret_hint: Mapped[str] = mapped_column(String(8), default="") # Zusätzliche Header je Aufruf (z. B. X-DB-Password), als JSON verschlüsselt; angezeigt nur die Namen headers: Mapped[str | None] = mapped_column(Text) header_names: Mapped[list[str]] = mapped_column(JSON, default=list) # Datenbank-Verbindung: host, port, database, user, sslmode (ohne Passwort) config: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) active: Mapped[bool] = mapped_column(Boolean, default=True) # memory: Gedächtnis wie Munin (→ Konzept 7.6); Einträge im Workspace sichtbar, löschen nur Menschen # database: DB-Dienst (services/db-mcp), Vorlage für Datenbank-Verbindungen der Workspaces role: Mapped[str] = mapped_column(String(20), default="", server_default="") approval: Mapped[str] = mapped_column(String(20), default=APPROVED, server_default=APPROVED) approval_note: Mapped[str] = mapped_column(Text, default="", server_default="") status: Mapped[str] = mapped_column(String(20), default="new") # new | ok | error last_error: Mapped[str] = mapped_column(Text, default="") # Ergebnis des letzten Tests, z. B. „Oracle 23…, 4 Tabellen sichtbar“ status_message: Mapped[str] = mapped_column(Text, default="", server_default="") # Bestätigung des Workspace-Admins, dass alle Mitglieder sehen dürfen, was der Server liefert exposure_confirmed_by: Mapped[int | None] = mapped_column(ForeignKey("users.id")) exposure_confirmed_at: Mapped[datetime | None] discovered_at: Mapped[datetime | None] server_info: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) created_by: Mapped[int] = mapped_column(ForeignKey("users.id")) created: Mapped[datetime] = mapped_column(default=now) class McpTool(Base): """Stand der letzten Discovery. Eingestuft wird über tool_definitions (Hash-Pinning).""" __tablename__ = "mcp_tools" __table_args__ = (UniqueConstraint("server_id", "remote_name"),) id: Mapped[int] = mapped_column(primary_key=True) server_id: Mapped[int] = mapped_column(ForeignKey("mcp_servers.id", ondelete="CASCADE")) name: Mapped[str] = mapped_column(String(64), unique=True) # Name für das Modell remote_name: Mapped[str] = mapped_column(String(200)) # Name auf dem Server description: Mapped[str] = mapped_column(Text, default="") input_schema: Mapped[dict[str, Any]] = mapped_column(JSON) # MCP-Annotations (readOnlyHint …): nur Vorschlag für die Einstufung, nie automatisch übernommen annotations: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) # Hash über URL des Servers, Name auf dem Server, Beschreibung und Schema: gleiche Tools desselben # Dienstes (z. B. query jeder Oracle-Verbindung) erben die Einstufung fingerprint: Mapped[str] = mapped_column(String(64), default="", server_default="") class WorkspaceMcpServer(Base): """Freischaltung eines Servers für einen Workspace durch dessen Admin.""" __tablename__ = "workspace_mcp_servers" workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), primary_key=True) server_id: Mapped[int] = mapped_column(ForeignKey("mcp_servers.id", ondelete="CASCADE"), primary_key=True) secret: Mapped[str | None] = mapped_column(Text) secret_hint: Mapped[str] = mapped_column(String(8), default="") enabled_by: Mapped[int] = mapped_column(ForeignKey("users.id")) enabled: Mapped[datetime] = mapped_column(default=now)