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:
106
apps/api/src/mcp/config.ts
Normal file
106
apps/api/src/mcp/config.ts
Normal 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 });
|
||||
19
apps/api/src/mcp/routes.ts
Normal file
19
apps/api/src/mcp/routes.ts
Normal 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 };
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user