Initial commit: SIC harness (backend, web, pi-adapter, configs, docs)

- pnpm monorepo: apps/api (Fastify + SQLite + SSE), apps/web (React+Vite), packages/shared, packages/pi-adapter
- Local auth (admin/webhook-runner roles) + Keycloak JWT ready
- Multi-session chat with reliable history (user persisted before LLM, assistant persisted after stream)
- Markdown knowledge base with /api/docs/search + /api/docs/:id
- YAML webhook catalog with backend-only execution, retry/backoff, audit (webhook_runs), and per-user rate limit
- Skills config (sre-on-call, blameless-postmortem, security-incident) injected into LLM system prompt
- LLM provider failover chain (config/models.yml fallback + LLM_FALLBACK_CHAIN override)
- Context-aware webhooks panel + backend id-mention safety net
- Per-message stats (time/duration/tokens/model), Markdown+GFM render, code & table copy/download buttons
- Vitest suite, end-to-end smoke test (scripts/smoke.mjs), per-session system prompt override
- /metrics Prometheus endpoint + /api/metrics JSON, request-id correlation
- dotenv with explicit repo-root path; envString/envNumber helpers (handles empty-string env)
- Runbooks + SOPs under knowledge/ in English; README, docs, and INDEX.md in English
This commit is contained in:
2026-06-29 16:20:53 +02:00
commit 62728b2200
89 changed files with 11992 additions and 0 deletions

106
apps/api/src/mcp/config.ts Normal file
View File

@@ -0,0 +1,106 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { parse } from "yaml";
import { envString } from "../env.js";
export type McpToolParameterSchema = {
type: "object";
required?: string[];
properties?: Record<string, unknown>;
};
export type McpToolDefinition = {
id: string;
name: string;
description: string;
server: string | null;
parameters: McpToolParameterSchema;
tags: string[];
enabled: boolean;
};
export type McpServerDefinition = {
id: string;
name: string;
description: string;
endpoint: string;
};
export type PublicMcpToolDefinition = Omit<McpToolDefinition, "server"> & {
server: string | null;
};
export type PublicMcpServerDefinition = McpServerDefinition;
type McpFile = {
mcp_servers?: McpServerDefinition[];
mcp_tools?: McpToolDefinition[];
};
const defaultPath = (): string =>
envString(process.env.MCP_CONFIG_PATH, resolve(process.cwd(), "../../config/mcp.yml"));
const isToolParameterSchema = (value: unknown): value is McpToolParameterSchema => {
if (!value || typeof value !== "object") return false;
const v = value as McpToolParameterSchema;
return v.type === "object";
};
export const loadMcpTools = (
configPath: string = defaultPath(),
): McpToolDefinition[] => {
let raw: string;
try {
raw = readFileSync(configPath, "utf8");
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") return [];
throw error;
}
const parsed = parse(raw) as McpFile | null;
if (!parsed || !Array.isArray(parsed.mcp_tools)) return [];
return parsed.mcp_tools
.filter((tool) => tool && typeof tool === "object" && typeof tool.id === "string")
.map((tool) => ({
id: String(tool.id).trim(),
name: String(tool.name ?? tool.id).trim(),
description: String(tool.description ?? "").trim(),
server: typeof tool.server === "string" ? tool.server : null,
parameters: isToolParameterSchema(tool.parameters)
? tool.parameters
: ({ type: "object", properties: {}, required: [] } satisfies McpToolParameterSchema),
tags: Array.isArray(tool.tags) ? tool.tags.map(String) : [],
enabled: tool.enabled !== false,
}))
.filter((tool) => tool.id.length > 0);
};
export const loadMcpServers = (
configPath: string = defaultPath(),
): McpServerDefinition[] => {
let raw: string;
try {
raw = readFileSync(configPath, "utf8");
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") return [];
throw error;
}
const parsed = parse(raw) as McpFile | null;
if (!parsed || !Array.isArray(parsed.mcp_servers)) return [];
return parsed.mcp_servers
.filter((s) => s && typeof s === "object" && typeof s.id === "string")
.map((s) => ({
id: String(s.id).trim(),
name: String(s.name ?? s.id).trim(),
description: String(s.description ?? "").trim(),
endpoint: String(s.endpoint ?? "").trim(),
}))
.filter((s) => s.id.length > 0);
};
export const enabledMcpTools = (tools: McpToolDefinition[] = loadMcpTools()): McpToolDefinition[] =>
tools.filter((tool) => tool.enabled);
export const toPublicMcpTool = (tool: McpToolDefinition): PublicMcpToolDefinition => ({ ...tool });
export const toPublicMcpServer = (server: McpServerDefinition): PublicMcpServerDefinition => ({ ...server });

View File

@@ -0,0 +1,19 @@
import type { FastifyInstance } from "fastify";
import {
enabledMcpTools,
loadMcpServers,
toPublicMcpServer,
toPublicMcpTool,
} from "./config.js";
export const registerMcpRoutes = async (app: FastifyInstance) => {
app.get("/api/mcp/tools", async () => {
const items = enabledMcpTools().map(toPublicMcpTool);
return { items };
});
app.get("/api/mcp/servers", async () => {
const items = loadMcpServers().map(toPublicMcpServer);
return { items };
});
};