diff --git a/Dockerfile.openclaw b/Dockerfile.openclaw index 8efe44a78..a27e909b2 100644 --- a/Dockerfile.openclaw +++ b/Dockerfile.openclaw @@ -2,7 +2,7 @@ FROM node:22-slim RUN apt-get update && apt-get install -y git inotify-tools && rm -rf /var/lib/apt/lists/* -RUN npm install -g openclaw@2026.3.7 +RUN npm install -g openclaw@2026.3.7 mcporter@0.7.3 COPY config/openclaw.json /root/.openclaw/openclaw.json COPY config/ensure-gateway-token.js /ensure-gateway-token.js diff --git a/Dockerfile.pinchy b/Dockerfile.pinchy index 41e4197a2..c2182ac32 100644 --- a/Dockerfile.pinchy +++ b/Dockerfile.pinchy @@ -24,6 +24,7 @@ WORKDIR /app/packages/web COPY packages/plugins/pinchy-files /openclaw-extensions/pinchy-files COPY packages/plugins/pinchy-context /openclaw-extensions/pinchy-context COPY packages/plugins/pinchy-audit /openclaw-extensions/pinchy-audit +COPY packages/plugins/pinchy-mcp /openclaw-extensions/pinchy-mcp HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD node -e "fetch('http://localhost:7777/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" diff --git a/packages/plugins/pinchy-mcp/index.test.ts b/packages/plugins/pinchy-mcp/index.test.ts new file mode 100644 index 000000000..a00bf11d9 --- /dev/null +++ b/packages/plugins/pinchy-mcp/index.test.ts @@ -0,0 +1,188 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock mcporter +vi.mock("mcporter", () => ({ + createRuntime: vi.fn().mockResolvedValue({ + callTool: vi.fn().mockResolvedValue({ text: "tool result" }), + close: vi.fn().mockResolvedValue(undefined), + }), +})); + +import plugin from "./index"; + +interface RegisteredFactory { + name: string; + factory: (ctx: { agentId?: string }) => unknown; +} + +function createMockApi(config: unknown) { + const registeredTools: RegisteredFactory[] = []; + + return { + pluginConfig: config, + registerTool: vi.fn( + (factory: (ctx: { agentId?: string }) => unknown, opts?: { name?: string }) => { + registeredTools.push({ name: opts?.name ?? "unnamed", factory }); + } + ), + registeredTools, + }; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("pinchy-mcp plugin", () => { + it("has correct id and name", () => { + expect(plugin.id).toBe("pinchy-mcp"); + expect(plugin.name).toBe("Pinchy MCP"); + }); + + it("validates config schema", () => { + expect(plugin.configSchema.validate(null)).toEqual( + expect.objectContaining({ ok: false }) + ); + expect( + plugin.configSchema.validate({ mcporterConfigPath: "/path", agents: {} }) + ).toEqual(expect.objectContaining({ ok: true })); + }); + + it("skips registration when no config provided", () => { + const api = createMockApi(undefined); + plugin.register(api); + expect(api.registerTool).not.toHaveBeenCalled(); + }); + + it("registers tools from agent config", () => { + const api = createMockApi({ + mcporterConfigPath: "/config/mcporter.json", + agents: { + "agent-1": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + { serverId: "srv-1", serverSlug: "github", toolName: "list_repos" }, + ], + }, + }, + }); + + plugin.register(api); + + expect(api.registerTool).toHaveBeenCalledTimes(2); + expect(api.registeredTools.map((t) => t.name)).toContain("mcp_github_create_issue"); + expect(api.registeredTools.map((t) => t.name)).toContain("mcp_github_list_repos"); + }); + + it("factory returns null for unauthorized agent", () => { + const api = createMockApi({ + mcporterConfigPath: "/config/mcporter.json", + agents: { + "agent-1": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + ], + }, + }, + }); + + plugin.register(api); + + const factory = api.registeredTools[0].factory; + + // Authorized agent + expect(factory({ agentId: "agent-1" })).not.toBeNull(); + + // Unauthorized agent + expect(factory({ agentId: "agent-2" })).toBeNull(); + + // No agent ID + expect(factory({})).toBeNull(); + }); + + it("deduplicates tools shared across agents", () => { + const api = createMockApi({ + mcporterConfigPath: "/config/mcporter.json", + agents: { + "agent-1": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + ], + }, + "agent-2": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + ], + }, + }, + }); + + plugin.register(api); + + // Only one tool registered (not two) + expect(api.registerTool).toHaveBeenCalledTimes(1); + + const factory = api.registeredTools[0].factory; + // Both agents can use it + expect(factory({ agentId: "agent-1" })).not.toBeNull(); + expect(factory({ agentId: "agent-2" })).not.toBeNull(); + }); + + it("tool execute returns text content on success", async () => { + const api = createMockApi({ + mcporterConfigPath: "/config/mcporter.json", + agents: { + "agent-1": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + ], + }, + }, + }); + + plugin.register(api); + + const tool = api.registeredTools[0].factory({ agentId: "agent-1" }) as { + execute: (id: string, params: Record) => Promise<{ + content: Array<{ type: string; text: string }>; + }>; + }; + + const result = await tool.execute("call-1", { args: { title: "Bug" } }); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe("text"); + }); + + it("tool execute returns error text on failure (never throws)", async () => { + // Override mcporter mock to throw + const mcporter = await import("mcporter"); + vi.mocked(mcporter.createRuntime).mockResolvedValueOnce({ + callTool: vi.fn().mockRejectedValue(new Error("Connection refused")), + close: vi.fn(), + } as never); + + const api = createMockApi({ + mcporterConfigPath: "/config/mcporter.json", + agents: { + "agent-1": { + allowedMcpTools: [ + { serverId: "srv-1", serverSlug: "github", toolName: "create_issue" }, + ], + }, + }, + }); + + plugin.register(api); + + const tool = api.registeredTools[0].factory({ agentId: "agent-1" }) as { + execute: (id: string, params: Record) => Promise<{ + content: Array<{ type: string; text: string }>; + }>; + }; + + // Should NOT throw — returns error text + const result = await tool.execute("call-1", {}); + expect(result.content[0].text).toContain("Error calling MCP tool"); + expect(result.content[0].text).toContain("Connection refused"); + }); +}); diff --git a/packages/plugins/pinchy-mcp/index.ts b/packages/plugins/pinchy-mcp/index.ts new file mode 100644 index 000000000..57598d9e9 --- /dev/null +++ b/packages/plugins/pinchy-mcp/index.ts @@ -0,0 +1,183 @@ +interface PluginToolContext { + agentId?: string; +} + +interface AgentMcpToolGrant { + serverId: string; + serverSlug: string; + toolName: string; +} + +interface AgentMcpConfig { + allowedMcpTools: AgentMcpToolGrant[]; +} + +interface PluginConfig { + mcporterConfigPath: string; + agents: Record; +} + +interface PluginApi { + pluginConfig?: PluginConfig; + registerTool: ( + factory: (ctx: PluginToolContext) => AgentTool | null, + opts?: { name?: string } + ) => void; +} + +interface AgentTool { + name: string; + label: string; + description: string; + parameters: Record; + execute: ( + toolCallId: string, + params: Record, + signal?: AbortSignal + ) => Promise<{ content: Array<{ type: string; text: string }> }>; +} + +interface McpRuntime { + listTools: ( + server: string, + options?: { includeSchema?: boolean } + ) => Promise>; + callTool: ( + server: string, + toolName: string, + options?: { args?: Record; timeoutMs?: number } + ) => Promise; + close: (server?: string) => Promise; +} + +const plugin = { + id: "pinchy-mcp", + name: "Pinchy MCP", + description: "Bridges MCP server tools into OpenClaw agents via mcporter.", + configSchema: { + validate: (value: unknown) => { + if ( + value && + typeof value === "object" && + "mcporterConfigPath" in value && + "agents" in value + ) { + return { ok: true as const, value }; + } + return { + ok: false as const, + errors: ["Missing 'mcporterConfigPath' or 'agents' key in config"], + }; + }, + }, + + register(api: PluginApi) { + const config = api.pluginConfig; + if (!config?.mcporterConfigPath || !config?.agents) { + console.warn("[pinchy-mcp] No config provided, skipping registration"); + return; + } + + // Collect all unique tool registrations needed across all agents + const toolRegistrations = new Map< + string, + { serverSlug: string; toolName: string; agentIds: Set } + >(); + + for (const [agentId, agentConfig] of Object.entries(config.agents)) { + for (const grant of agentConfig.allowedMcpTools) { + const toolKey = `mcp_${grant.serverSlug}_${grant.toolName}`; + const existing = toolRegistrations.get(toolKey); + if (existing) { + existing.agentIds.add(agentId); + } else { + toolRegistrations.set(toolKey, { + serverSlug: grant.serverSlug, + toolName: grant.toolName, + agentIds: new Set([agentId]), + }); + } + } + } + + // Lazy-initialize mcporter runtime on first tool call + let runtimePromise: Promise | null = null; + + function getRuntime(): Promise { + if (!runtimePromise) { + runtimePromise = import("mcporter").then((m) => + m.createRuntime({ configPath: config!.mcporterConfigPath }) + ); + runtimePromise.catch((err) => { + console.error("[pinchy-mcp] Failed to create mcporter runtime:", err); + runtimePromise = null; // Allow retry on next call + }); + } + return runtimePromise; + } + + // Register each unique tool with a factory that scopes by agent + for (const [toolKey, registration] of toolRegistrations) { + api.registerTool( + (ctx: PluginToolContext) => { + const agentId = ctx.agentId; + if (!agentId) return null; + if (!registration.agentIds.has(agentId)) return null; + + return { + name: toolKey, + label: `${registration.serverSlug}: ${registration.toolName}`, + description: `MCP tool: ${registration.toolName} from ${registration.serverSlug}`, + parameters: { + type: "object", + properties: { + args: { + type: "object", + description: "Arguments to pass to the MCP tool", + additionalProperties: true, + }, + }, + }, + async execute( + _toolCallId: string, + params: Record + ) { + try { + const runtime = await getRuntime(); + const args = (params.args as Record) ?? {}; + const result = await runtime.callTool( + registration.serverSlug, + registration.toolName, + { args, timeoutMs: 30000 } + ); + + // Convert result to text + const text = + typeof result === "string" + ? result + : JSON.stringify(result, null, 2); + + return { content: [{ type: "text", text }] }; + } catch (error) { + const message = + error instanceof Error ? error.message : "MCP tool call failed"; + console.error( + `[pinchy-mcp] Tool ${toolKey} error:`, + message + ); + return { + content: [ + { type: "text", text: `Error calling MCP tool: ${message}` }, + ], + }; + } + }, + }; + }, + { name: toolKey } + ); + } + }, +}; + +export default plugin; diff --git a/packages/plugins/pinchy-mcp/openclaw.plugin.json b/packages/plugins/pinchy-mcp/openclaw.plugin.json new file mode 100644 index 000000000..00d867fc5 --- /dev/null +++ b/packages/plugins/pinchy-mcp/openclaw.plugin.json @@ -0,0 +1,37 @@ +{ + "id": "pinchy-mcp", + "name": "Pinchy MCP", + "description": "Bridges MCP server tools into OpenClaw agents via mcporter.", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "mcporterConfigPath": { + "type": "string", + "description": "Path to mcporter.json config file" + }, + "agents": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "allowedMcpTools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "serverId": { "type": "string" }, + "serverSlug": { "type": "string" }, + "toolName": { "type": "string" } + }, + "required": ["serverId", "serverSlug", "toolName"] + } + } + }, + "required": ["allowedMcpTools"] + } + } + }, + "required": ["mcporterConfigPath", "agents"] + } +} diff --git a/packages/plugins/pinchy-mcp/package.json b/packages/plugins/pinchy-mcp/package.json new file mode 100644 index 000000000..da731cafb --- /dev/null +++ b/packages/plugins/pinchy-mcp/package.json @@ -0,0 +1,17 @@ +{ + "name": "@pinchy/pinchy-mcp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "mcporter": "^0.7.3" + }, + "devDependencies": { + "vitest": "^3.0.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/plugins/pinchy-mcp/tsconfig.json b/packages/plugins/pinchy-mcp/tsconfig.json new file mode 100644 index 000000000..4150d0014 --- /dev/null +++ b/packages/plugins/pinchy-mcp/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "rootDir": ".", + "esModuleInterop": true + }, + "include": ["*.ts"], + "exclude": ["*.test.ts"] +} diff --git a/packages/web/drizzle/0019_icy_guardsmen.sql b/packages/web/drizzle/0019_icy_guardsmen.sql new file mode 100644 index 000000000..9696ab97f --- /dev/null +++ b/packages/web/drizzle/0019_icy_guardsmen.sql @@ -0,0 +1,16 @@ +CREATE TABLE "mcp_servers" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "transport" text DEFAULT 'stdio' NOT NULL, + "command" text, + "args" jsonb DEFAULT '[]'::jsonb NOT NULL, + "url" text, + "env_vars" text, + "status" text DEFAULT 'unknown' NOT NULL, + "status_message" text, + "last_checked_at" timestamp, + "tool_manifest" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "mcp_servers_name_unique" UNIQUE("name") +); diff --git a/packages/web/drizzle/meta/0019_snapshot.json b/packages/web/drizzle/meta/0019_snapshot.json new file mode 100644 index 000000000..14c33f225 --- /dev/null +++ b/packages/web/drizzle/meta/0019_snapshot.json @@ -0,0 +1,1129 @@ +{ + "id": "fe94c09a-d8d1-41b4-a257-e0c8c5b578d4", + "prevId": "36adc001-61d7-43d9-947e-fb9a188b6b00", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_groups": { + "name": "agent_groups", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agent_groups_agent_id_agents_id_fk": { + "name": "agent_groups_agent_id_agents_id_fk", + "tableFrom": "agent_groups", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_groups_group_id_groups_id_fk": { + "name": "agent_groups_group_id_groups_id_fk", + "tableFrom": "agent_groups", + "tableTo": "groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_groups_agent_id_group_id_pk": { + "name": "agent_groups_agent_id_group_id_pk", + "columns": ["agent_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Smithers'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plugin_config": { + "name": "plugin_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_personal": { + "name": "is_personal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'restricted'" + }, + "greeting_message": { + "name": "greeting_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_seed": { + "name": "avatar_seed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personality_preset_id": { + "name": "personality_preset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_owner_id_user_id_fk": { + "name": "agents_owner_id_user_id_fk", + "tableFrom": "agents", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "row_hmac": { + "name": "row_hmac", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_audit_timestamp": { + "name": "idx_audit_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_actor": { + "name": "idx_audit_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_event": { + "name": "idx_audit_event", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_groups": { + "name": "invite_groups", + "schema": "", + "columns": { + "invite_id": { + "name": "invite_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invite_groups_invite_id_invites_id_fk": { + "name": "invite_groups_invite_id_invites_id_fk", + "tableFrom": "invite_groups", + "tableTo": "invites", + "columnsFrom": ["invite_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invite_groups_group_id_groups_id_fk": { + "name": "invite_groups_group_id_groups_id_fk", + "tableFrom": "invite_groups", + "tableTo": "groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_groups_invite_id_group_id_pk": { + "name": "invite_groups_invite_id_group_id_pk", + "columns": ["invite_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'invite'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by_user_id": { + "name": "claimed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invites_created_by_user_id_fk": { + "name": "invites_created_by_user_id_fk", + "tableFrom": "invites", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invites_claimed_by_user_id_user_id_fk": { + "name": "invites_claimed_by_user_id_user_id_fk", + "tableFrom": "invites", + "tableTo": "user", + "columnsFrom": ["claimed_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invites_token_hash_unique": { + "name": "invites_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stdio'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "status_message": { + "name": "status_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tool_manifest": { + "name": "tool_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mcp_servers_name_unique": { + "name": "mcp_servers_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_groups": { + "name": "user_groups", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_groups_user_id_user_id_fk": { + "name": "user_groups_user_id_user_id_fk", + "tableFrom": "user_groups", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_groups_group_id_groups_id_fk": { + "name": "user_groups_group_id_groups_id_fk", + "tableFrom": "user_groups", + "tableTo": "groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_groups_user_id_group_id_pk": { + "name": "user_groups_user_id_group_id_pk", + "columns": ["user_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": ["user", "agent", "system"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.active_agents": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Smithers'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plugin_config": { + "name": "plugin_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_personal": { + "name": "is_personal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'restricted'" + }, + "greeting_message": { + "name": "greeting_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_seed": { + "name": "avatar_seed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personality_preset_id": { + "name": "personality_preset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"name\", \"model\", \"template_id\", \"plugin_config\", \"allowed_tools\", \"owner_id\", \"is_personal\", \"visibility\", \"greeting_message\", \"tagline\", \"avatar_seed\", \"personality_preset_id\", \"created_at\", \"deleted_at\" from \"agents\" where \"agents\".\"deleted_at\" is null", + "name": "active_agents", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/web/drizzle/meta/_journal.json b/packages/web/drizzle/meta/_journal.json index aca89183b..f5306158e 100644 --- a/packages/web/drizzle/meta/_journal.json +++ b/packages/web/drizzle/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1773086377479, "tag": "0018_nasty_mysterio", "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1773823125552, + "tag": "0019_icy_guardsmen", + "breakpoints": true } ] } diff --git a/packages/web/next.config.ts b/packages/web/next.config.ts index 7b992aa0b..6b0032f77 100644 --- a/packages/web/next.config.ts +++ b/packages/web/next.config.ts @@ -5,6 +5,7 @@ import { join } from "path"; const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8")); const nextConfig: NextConfig = { + serverExternalPackages: ["mcporter"], env: { NEXT_PUBLIC_PINCHY_VERSION: pkg.version, }, diff --git a/packages/web/package.json b/packages/web/package.json index b0f506b23..0a8982dd4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -30,7 +30,8 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", "lucide-react": "^0.575.0", - "next": "16.1.6", + "mcporter": "^0.7.3", + "next": "16.1.7", "next-themes": "^0.4.6", "openclaw-node": "0.2.1", "postgres": "^3.4.8", diff --git a/packages/web/src/__tests__/api/mcp-servers.test.ts b/packages/web/src/__tests__/api/mcp-servers.test.ts new file mode 100644 index 000000000..0270abe58 --- /dev/null +++ b/packages/web/src/__tests__/api/mcp-servers.test.ts @@ -0,0 +1,426 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest } from "next/server"; + +// ── Mocks ──────────────────────────────────────────────────────────────── + +vi.mock("next/headers", () => ({ + headers: vi.fn().mockResolvedValue(new Headers()), +})); + +vi.mock("@/lib/auth", () => { + const mockGetSession = vi.fn(); + return { + getSession: mockGetSession, + auth: { + api: { + getSession: mockGetSession, + }, + }, + }; +}); + +vi.mock("@/lib/audit", () => ({ + appendAuditLog: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/lib/openclaw-config", () => ({ + regenerateOpenClawConfig: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/lib/mcp-servers", () => ({ + listMcpServers: vi.fn(), + getMcpServer: vi.fn(), + createMcpServer: vi.fn(), + updateMcpServer: vi.fn(), + deleteMcpServer: vi.fn(), + discoverTools: vi.fn(), + testConnection: vi.fn(), + validateCommand: vi.fn(), + getMcpToolDefinitions: vi.fn(), +})); + +import { auth } from "@/lib/auth"; +import { appendAuditLog } from "@/lib/audit"; +import { + listMcpServers, + getMcpServer, + createMcpServer, + deleteMcpServer, + discoverTools, + testConnection, + validateCommand, + getMcpToolDefinitions, +} from "@/lib/mcp-servers"; + +const adminSession = { + user: { id: "admin-1", role: "admin" }, + expires: "", +} as any; + +const memberSession = { + user: { id: "user-1", role: "member" }, + expires: "", +} as any; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(validateCommand).mockReturnValue(null); +}); + +// ── GET /api/mcp-servers ───────────────────────────────────────────────── + +describe("GET /api/mcp-servers", () => { + let GET: typeof import("@/app/api/mcp-servers/route").GET; + + beforeEach(async () => { + const mod = await import("@/app/api/mcp-servers/route"); + GET = mod.GET; + }); + + it("returns servers for admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(listMcpServers).mockResolvedValueOnce([ + { + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: [], + url: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + envVarKeys: ["GITHUB_TOKEN"], + }, + ]); + + const response = await GET(); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toHaveLength(1); + expect(body[0].name).toBe("GitHub"); + }); + + it("returns 403 for non-admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(memberSession); + const response = await GET(); + expect(response.status).toBe(403); + }); + + it("returns 401 when not authenticated", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(null); + const response = await GET(); + expect(response.status).toBe(401); + }); +}); + +// ── POST /api/mcp-servers ──────────────────────────────────────────────── + +describe("POST /api/mcp-servers", () => { + let POST: typeof import("@/app/api/mcp-servers/route").POST; + + beforeEach(async () => { + const mod = await import("@/app/api/mcp-servers/route"); + POST = mod.POST; + }); + + it("creates stdio server for admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const fakeServer = { + id: "srv-new", + name: "GitHub", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + url: null, + status: "unknown", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + vi.mocked(createMcpServer).mockResolvedValueOnce(fakeServer); + vi.mocked(discoverTools).mockResolvedValueOnce({ + success: true, + tools: [{ name: "create_issue", description: "Create issue", inputSchema: {} }], + }); + vi.mocked(getMcpServer).mockResolvedValueOnce({ + ...fakeServer, + envVarKeys: ["GITHUB_TOKEN"], + }); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ + name: "GitHub", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + envVars: { GITHUB_TOKEN: "ghp_abc" }, + }), + }); + + const response = await POST(request); + expect(response.status).toBe(201); + + expect(createMcpServer).toHaveBeenCalledWith( + expect.objectContaining({ name: "GitHub", transport: "stdio" }) + ); + expect(discoverTools).toHaveBeenCalledWith("srv-new"); + expect(appendAuditLog).toHaveBeenCalledWith( + expect.objectContaining({ + eventType: "mcp_server.created", + resource: "mcp_server:srv-new", + }) + ); + }); + + it("rejects missing name", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ transport: "stdio", command: "npx" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + }); + + it("rejects invalid transport", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Test", transport: "websocket" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + }); + + it("rejects missing command for stdio", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Test", transport: "stdio" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + }); + + it("rejects missing URL for http", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Test", transport: "http" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + }); + + it("rejects invalid URL protocol", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Test", transport: "http", url: "ftp://example.com" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + }); + + it("rejects shell metacharacters in command", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(validateCommand).mockReturnValueOnce( + "Command contains prohibited shell metacharacters" + ); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Evil", transport: "stdio", command: "npx; rm -rf /" }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("shell metacharacters"); + }); + + it("rejects invalid env var keys", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ + name: "Test", + transport: "stdio", + command: "npx", + envVars: { "bad-key": "value" }, + }), + }); + const response = await POST(request); + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("Invalid env var key"); + }); + + it("returns 403 for non-admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(memberSession); + const request = new NextRequest("http://localhost:7777/api/mcp-servers", { + method: "POST", + body: JSON.stringify({ name: "Test", transport: "stdio", command: "npx" }), + }); + const response = await POST(request); + expect(response.status).toBe(403); + }); +}); + +// ── DELETE /api/mcp-servers/[serverId] ──────────────────────────────────── + +describe("DELETE /api/mcp-servers/[serverId]", () => { + let DELETE: typeof import("@/app/api/mcp-servers/[serverId]/route").DELETE; + + beforeEach(async () => { + const mod = await import("@/app/api/mcp-servers/[serverId]/route"); + DELETE = mod.DELETE; + }); + + it("deletes server for admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(getMcpServer).mockResolvedValueOnce({ + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: [], + url: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: [{ name: "create_issue", description: "", inputSchema: {} }], + createdAt: new Date(), + updatedAt: new Date(), + envVarKeys: [], + }); + vi.mocked(deleteMcpServer).mockResolvedValueOnce({ + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: [], + url: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: [{ name: "create_issue", description: "", inputSchema: {} }], + createdAt: new Date(), + updatedAt: new Date(), + }); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers/srv-1", { + method: "DELETE", + }); + const response = await DELETE(request, { + params: Promise.resolve({ serverId: "srv-1" }), + }); + + expect(response.status).toBe(200); + expect(appendAuditLog).toHaveBeenCalledWith( + expect.objectContaining({ + eventType: "mcp_server.deleted", + detail: { name: "GitHub", toolCount: 1 }, + }) + ); + }); + + it("returns 404 for unknown server", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(getMcpServer).mockResolvedValueOnce(null); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers/nonexistent", { + method: "DELETE", + }); + const response = await DELETE(request, { + params: Promise.resolve({ serverId: "nonexistent" }), + }); + expect(response.status).toBe(404); + }); +}); + +// ── POST /api/mcp-servers/[serverId]/test ───────────────────────────────── + +describe("POST /api/mcp-servers/[serverId]/test", () => { + let POST: typeof import("@/app/api/mcp-servers/[serverId]/test/route").POST; + + beforeEach(async () => { + const mod = await import("@/app/api/mcp-servers/[serverId]/test/route"); + POST = mod.POST; + }); + + it("returns success when connection works", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(testConnection).mockResolvedValueOnce({ + success: true, + tools: [{ name: "tool1", description: "", inputSchema: {} }], + }); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers/srv-1/test", { + method: "POST", + }); + const response = await POST(request, { + params: Promise.resolve({ serverId: "srv-1" }), + }); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.success).toBe(true); + expect(body.toolCount).toBe(1); + }); + + it("returns error when connection fails", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(testConnection).mockResolvedValueOnce({ + success: false, + error: "Connection refused", + }); + + const request = new NextRequest("http://localhost:7777/api/mcp-servers/srv-1/test", { + method: "POST", + }); + const response = await POST(request, { + params: Promise.resolve({ serverId: "srv-1" }), + }); + + const body = await response.json(); + expect(body.success).toBe(false); + expect(body.error).toBe("Connection refused"); + }); +}); + +// ── GET /api/mcp-servers/tools ─────────────────────────────────────────── + +describe("GET /api/mcp-servers/tools", () => { + let GET: typeof import("@/app/api/mcp-servers/tools/route").GET; + + beforeEach(async () => { + const mod = await import("@/app/api/mcp-servers/tools/route"); + GET = mod.GET; + }); + + it("returns flat tool list for admin", async () => { + vi.mocked(auth.api.getSession).mockResolvedValueOnce(adminSession); + vi.mocked(getMcpToolDefinitions).mockResolvedValueOnce([ + { + id: "mcp:srv-1:create_issue", + label: "create_issue", + description: "Create issue", + category: "mcp", + serverName: "GitHub", + }, + ]); + + const response = await GET(); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toHaveLength(1); + expect(body[0].id).toBe("mcp:srv-1:create_issue"); + }); +}); diff --git a/packages/web/src/__tests__/app/settings-page.test.tsx b/packages/web/src/__tests__/app/settings-page.test.tsx index e7c06d1e0..fe51eb304 100644 --- a/packages/web/src/__tests__/app/settings-page.test.tsx +++ b/packages/web/src/__tests__/app/settings-page.test.tsx @@ -205,7 +205,8 @@ describe("Settings Page", () => { render(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + // Multiple loading states may exist (provider, MCP servers, etc.) — verify at least one + expect(screen.getAllByText("Loading...").length).toBeGreaterThanOrEqual(1); }); it("should pass configuredProviders and defaultProvider to ProviderKeyForm after fetch", async () => { diff --git a/packages/web/src/__tests__/lib/mcp-servers.test.ts b/packages/web/src/__tests__/lib/mcp-servers.test.ts new file mode 100644 index 000000000..083882fad --- /dev/null +++ b/packages/web/src/__tests__/lib/mcp-servers.test.ts @@ -0,0 +1,450 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// ── Mocks ──────────────────────────────────────────────────────────────── + +const { + mockReturning, + mockValues, + mockInsert, + mockSelectWhere, + mockSelectFrom, + mockSelect, + mockUpdateReturning, + mockUpdateWhere, + mockUpdateSet, + mockUpdate, + mockDeleteReturning, + mockDeleteWhere, + mockDelete, +} = vi.hoisted(() => { + const mockReturning = vi.fn(); + const mockValues = vi.fn().mockReturnValue({ returning: mockReturning }); + const mockInsert = vi.fn().mockReturnValue({ values: mockValues }); + + const mockSelectWhere = vi.fn(); + const mockSelectFrom = vi.fn().mockReturnValue({ where: mockSelectWhere }); + const mockSelect = vi.fn().mockReturnValue({ from: mockSelectFrom }); + + const mockUpdateReturning = vi.fn(); + const mockUpdateWhere = vi.fn().mockReturnValue({ returning: mockUpdateReturning }); + const mockUpdateSet = vi.fn().mockReturnValue({ where: mockUpdateWhere }); + const mockUpdate = vi.fn().mockReturnValue({ set: mockUpdateSet }); + + const mockDeleteReturning = vi.fn(); + const mockDeleteWhere = vi.fn().mockReturnValue({ returning: mockDeleteReturning }); + const mockDelete = vi.fn().mockReturnValue({ where: mockDeleteWhere }); + + return { + mockReturning, + mockValues, + mockInsert, + mockSelectWhere, + mockSelectFrom, + mockSelect, + mockUpdateReturning, + mockUpdateWhere, + mockUpdateSet, + mockUpdate, + mockDeleteReturning, + mockDeleteWhere, + mockDelete, + }; +}); + +vi.mock("@/lib/encryption", () => ({ + encrypt: vi.fn((v: string) => `encrypted:${v}`), + decrypt: vi.fn((v: string) => v.replace("encrypted:", "")), + getOrCreateSecret: vi.fn(() => Buffer.alloc(32)), +})); + +vi.mock("@/db", () => ({ + db: { + select: mockSelect, + insert: mockInsert, + update: mockUpdate, + delete: mockDelete, + }, +})); + +vi.mock("@/db/schema", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual }; +}); + +import { + getServerSlug, + validateCommand, + createMcpServer, + updateMcpServer, + deleteMcpServer, + listMcpServers, + getMcpToolDefinitions, + buildMcporterServerConfig, + decryptEnvVars, +} from "@/lib/mcp-servers"; +import { encrypt, decrypt } from "@/lib/encryption"; + +beforeEach(() => { + vi.clearAllMocks(); + // Restore default chain returns after clearAllMocks + mockValues.mockReturnValue({ returning: mockReturning }); + mockInsert.mockReturnValue({ values: mockValues }); + mockSelectFrom.mockReturnValue({ where: mockSelectWhere }); + mockSelect.mockReturnValue({ from: mockSelectFrom }); + mockUpdateSet.mockReturnValue({ where: mockUpdateWhere }); + mockUpdateWhere.mockReturnValue({ returning: mockUpdateReturning }); + mockUpdate.mockReturnValue({ set: mockUpdateSet }); + mockDeleteWhere.mockReturnValue({ returning: mockDeleteReturning }); + mockDelete.mockReturnValue({ where: mockDeleteWhere }); +}); + +// ── getServerSlug ──────────────────────────────────────────────────────── + +describe("getServerSlug", () => { + it("converts name to lowercase slug", () => { + expect(getServerSlug("GitHub MCP")).toBe("github_mcp"); + }); + + it("collapses non-alphanumeric chars", () => { + expect(getServerSlug("Google -- Workspace")).toBe("google_workspace"); + }); + + it("trims leading/trailing underscores", () => { + expect(getServerSlug(" --hello-- ")).toBe("hello"); + }); + + it("truncates to 30 chars", () => { + const long = "a".repeat(50); + expect(getServerSlug(long).length).toBe(30); + }); +}); + +// ── validateCommand ────────────────────────────────────────────────────── + +describe("validateCommand", () => { + it("returns null for safe commands", () => { + expect(validateCommand("npx")).toBeNull(); + expect(validateCommand("node")).toBeNull(); + expect(validateCommand("/usr/bin/python3")).toBeNull(); + }); + + it("rejects shell metacharacters", () => { + expect(validateCommand("npx; rm -rf /")).not.toBeNull(); + expect(validateCommand("cmd | evil")).not.toBeNull(); + expect(validateCommand("cmd && evil")).not.toBeNull(); + expect(validateCommand("$(evil)")).not.toBeNull(); + expect(validateCommand("`evil`")).not.toBeNull(); + }); +}); + +// ── createMcpServer ────────────────────────────────────────────────────── + +describe("createMcpServer", () => { + it("inserts server with encrypted env vars", async () => { + const fakeRow = { + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + url: null, + envVars: 'encrypted:{"GITHUB_TOKEN":"ghp_abc"}', + status: "unknown", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + mockReturning.mockResolvedValueOnce([fakeRow]); + + const result = await createMcpServer({ + name: "GitHub", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + envVars: { GITHUB_TOKEN: "ghp_abc" }, + }); + + expect(result.id).toBe("srv-1"); + expect(result.name).toBe("GitHub"); + expect(encrypt).toHaveBeenCalledWith('{"GITHUB_TOKEN":"ghp_abc"}'); + }); + + it("inserts server without env vars when empty", async () => { + const fakeRow = { + id: "srv-2", + name: "Test", + transport: "http", + command: null, + args: [], + url: "https://example.com", + envVars: null, + status: "unknown", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + mockReturning.mockResolvedValueOnce([fakeRow]); + + await createMcpServer({ + name: "Test", + transport: "http", + url: "https://example.com", + }); + + expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({ envVars: null })); + }); +}); + +// ── updateMcpServer ────────────────────────────────────────────────────── + +describe("updateMcpServer", () => { + it("re-encrypts env vars when updated", async () => { + const fakeRow = { + id: "srv-1", + name: "GitHub Updated", + transport: "stdio", + command: "npx", + args: [], + url: null, + envVars: 'encrypted:{"NEW_TOKEN":"abc"}', + status: "unknown", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + mockUpdateReturning.mockResolvedValueOnce([fakeRow]); + + const result = await updateMcpServer("srv-1", { + name: "GitHub Updated", + envVars: { NEW_TOKEN: "abc" }, + }); + + expect(result?.name).toBe("GitHub Updated"); + expect(encrypt).toHaveBeenCalledWith('{"NEW_TOKEN":"abc"}'); + }); + + it("returns null when server not found", async () => { + mockUpdateReturning.mockResolvedValueOnce([]); + + const result = await updateMcpServer("nonexistent", { name: "Foo" }); + expect(result).toBeNull(); + }); +}); + +// ── deleteMcpServer ────────────────────────────────────────────────────── + +describe("deleteMcpServer", () => { + it("strips mcp tools from agents before deleting", async () => { + // First select: list all agents + mockSelectFrom.mockReturnValueOnce( + Promise.resolve([ + { + id: "agent-1", + allowedTools: ["shell", "mcp:srv-1:create_issue", "mcp:srv-1:list_repos", "pinchy_ls"], + }, + { + id: "agent-2", + allowedTools: ["mcp:srv-2:other_tool"], + }, + ]) + ); + + mockDeleteReturning.mockResolvedValueOnce([ + { + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: [], + url: null, + envVars: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: [{ name: "create_issue", description: "", inputSchema: {} }], + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + const result = await deleteMcpServer("srv-1"); + + expect(result?.id).toBe("srv-1"); + // Should have called update for agent-1 (had mcp:srv-1:* tools) + // agent-2 only has mcp:srv-2 so should NOT be updated + expect(mockUpdateSet).toHaveBeenCalledWith( + expect.objectContaining({ + allowedTools: ["shell", "pinchy_ls"], + }) + ); + }); +}); + +// ── listMcpServers ─────────────────────────────────────────────────────── + +describe("listMcpServers", () => { + it("returns servers with env var keys (not values)", async () => { + mockSelectFrom.mockReturnValueOnce( + Promise.resolve([ + { + id: "srv-1", + name: "GitHub", + transport: "stdio", + command: "npx", + args: [], + url: null, + envVars: 'encrypted:{"GITHUB_TOKEN":"secret","OTHER":"val"}', + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]) + ); + + const servers = await listMcpServers(); + expect(servers).toHaveLength(1); + expect(servers[0].envVarKeys).toEqual(["GITHUB_TOKEN", "OTHER"]); + }); +}); + +// ── getMcpToolDefinitions ──────────────────────────────────────────────── + +describe("getMcpToolDefinitions", () => { + it("returns tool definitions with mcp category", async () => { + mockSelectFrom.mockReturnValueOnce( + Promise.resolve([ + { + id: "srv-1", + name: "GitHub MCP", + transport: "stdio", + command: "npx", + args: [], + url: null, + envVars: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: [ + { name: "create_issue", description: "Create a GitHub issue", inputSchema: {} }, + { name: "list_repos", description: "List repos", inputSchema: {} }, + ], + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "srv-2", + name: "No Tools Server", + transport: "http", + command: null, + args: [], + url: "https://example.com", + envVars: null, + status: "unknown", + statusMessage: null, + lastCheckedAt: null, + toolManifest: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]) + ); + + const tools = await getMcpToolDefinitions(); + expect(tools).toHaveLength(2); + expect(tools[0]).toEqual({ + id: "mcp:srv-1:create_issue", + label: "create_issue", + description: "Create a GitHub issue", + category: "mcp", + serverName: "GitHub MCP", + }); + expect(tools[1].id).toBe("mcp:srv-1:list_repos"); + }); +}); + +// ── buildMcporterServerConfig ──────────────────────────────────────────── + +describe("buildMcporterServerConfig", () => { + it("builds stdio config", () => { + const config = buildMcporterServerConfig( + { + name: "GitHub", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + url: null, + envVars: null, + }, + { GITHUB_TOKEN: "ghp_abc" } + ); + + expect(config).toEqual({ + command: "npx", + args: ["-y", "@mcp/server-github"], + env: { GITHUB_TOKEN: "ghp_abc" }, + }); + }); + + it("builds http config", () => { + const config = buildMcporterServerConfig( + { + name: "Workspace", + transport: "http", + command: null, + args: [], + url: "https://workspace-mcp.example.com", + envVars: null, + }, + {} + ); + + expect(config).toEqual({ + baseUrl: "https://workspace-mcp.example.com", + }); + }); + + it("omits env when empty", () => { + const config = buildMcporterServerConfig( + { + name: "Test", + transport: "stdio", + command: "node", + args: ["server.js"], + url: null, + envVars: null, + }, + {} + ); + + expect(config).not.toHaveProperty("env"); + }); +}); + +// ── decryptEnvVars ─────────────────────────────────────────────────────── + +describe("decryptEnvVars", () => { + it("returns empty object for null", () => { + expect(decryptEnvVars(null)).toEqual({}); + }); + + it("decrypts and parses env vars", () => { + const result = decryptEnvVars('encrypted:{"KEY":"value"}'); + expect(result).toEqual({ KEY: "value" }); + }); + + it("returns empty object on decrypt failure", () => { + vi.mocked(decrypt).mockImplementationOnce(() => { + throw new Error("bad ciphertext"); + }); + expect(decryptEnvVars("garbage")).toEqual({}); + }); +}); diff --git a/packages/web/src/__tests__/lib/openclaw-config.test.ts b/packages/web/src/__tests__/lib/openclaw-config.test.ts index d204f1d99..467ea6185 100644 --- a/packages/web/src/__tests__/lib/openclaw-config.test.ts +++ b/packages/web/src/__tests__/lib/openclaw-config.test.ts @@ -674,6 +674,112 @@ describe("regenerateOpenClawConfig", () => { }); }); +describe("MCP server config generation", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockedExistsSync.mockReturnValue(true); + mockedReadFileSync.mockImplementation(() => { + throw new Error("ENOENT: no such file or directory"); + }); + mockedGetSetting.mockResolvedValue(null); + }); + + it("should include pinchy-mcp when agents have MCP tools", async () => { + const existingConfig = { + gateway: { mode: "local", bind: "lan", auth: { token: "gw-token" } }, + }; + mockedReadFileSync.mockReturnValue(JSON.stringify(existingConfig)); + + // First db.select().from(agents), then db.select().from(mcpServers) + let selectCallCount = 0; + mockedDb.select.mockReturnValue({ + from: vi.fn().mockImplementation(() => { + selectCallCount++; + if (selectCallCount === 1) { + return Promise.resolve([ + { + id: "agent-1", + name: "Dev Agent", + model: "anthropic/claude-opus-4-6", + pluginConfig: null, + allowedTools: ["shell", "mcp:srv-1:create_issue"], + ownerId: null, + isPersonal: false, + createdAt: new Date(), + }, + ]); + } + // MCP servers + return Promise.resolve([ + { + id: "srv-1", + name: "GitHub MCP", + transport: "stdio", + command: "npx", + args: ["-y", "@mcp/server-github"], + url: null, + envVars: null, + status: "connected", + statusMessage: null, + lastCheckedAt: null, + toolManifest: [{ name: "create_issue", description: "Create issue", inputSchema: {} }], + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + }), + } as never); + + await regenerateOpenClawConfig(); + + // Find the mcporter.json write (0o600 permissions) + const mcporterWrite = mockedWriteFileSync.mock.calls.find((call) => + (call[0] as string).includes("mcporter.json") + ); + expect(mcporterWrite).toBeDefined(); + expect(mcporterWrite![2]).toEqual(expect.objectContaining({ mode: 0o600 })); + + const mcporterConfig = JSON.parse(mcporterWrite![1] as string); + expect(mcporterConfig.mcpServers.github_mcp).toBeDefined(); + expect(mcporterConfig.mcpServers.github_mcp.command).toBe("npx"); + + // Check openclaw.json has pinchy-mcp plugin + const openclawWrite = mockedWriteFileSync.mock.calls.find((call) => + (call[0] as string).includes("openclaw.json") + ); + const config = JSON.parse(openclawWrite![1] as string); + + expect(config.plugins.entries["pinchy-mcp"]).toBeDefined(); + expect(config.plugins.entries["pinchy-mcp"].enabled).toBe(true); + expect(config.plugins.entries["pinchy-mcp"].config.agents["agent-1"]).toBeDefined(); + expect(config.plugins.entries["pinchy-mcp"].config.agents["agent-1"].allowedMcpTools).toEqual([ + { serverId: "srv-1", serverSlug: "github_mcp", toolName: "create_issue" }, + ]); + expect(config.plugins.allow).toContain("pinchy-mcp"); + }); + + it("should not include pinchy-mcp when no agents have MCP tools", async () => { + mockedDb.select.mockReturnValue({ + from: vi.fn().mockResolvedValue([]), + } as never); + + await regenerateOpenClawConfig(); + + const openclawWrite = mockedWriteFileSync.mock.calls.find((call) => + (call[0] as string).includes("openclaw.json") + ); + const config = JSON.parse(openclawWrite![1] as string); + + expect(config.plugins?.entries?.["pinchy-mcp"]).toBeUndefined(); + + // No mcporter.json should be written + const mcporterWrite = mockedWriteFileSync.mock.calls.find((call) => + (call[0] as string).includes("mcporter.json") + ); + expect(mcporterWrite).toBeUndefined(); + }); +}); + describe("restart-state integration", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/web/src/__tests__/lib/tool-registry.test.ts b/packages/web/src/__tests__/lib/tool-registry.test.ts index afdfd4ded..9b1697665 100644 --- a/packages/web/src/__tests__/lib/tool-registry.test.ts +++ b/packages/web/src/__tests__/lib/tool-registry.test.ts @@ -43,6 +43,10 @@ describe("getToolById", () => { it("returns undefined for unknown ID", () => { expect(getToolById("nonexistent")).toBeUndefined(); }); + + it("returns undefined for MCP tool IDs (not in static registry)", () => { + expect(getToolById("mcp:srv-1:create_issue")).toBeUndefined(); + }); }); describe("getToolsByCategory", () => { @@ -83,4 +87,11 @@ describe("computeDeniedGroups", () => { expect(denied).toContain("group:fs"); expect(denied).toContain("group:web"); }); + + it("ignores MCP tools for group computation", () => { + const denied = computeDeniedGroups(["mcp:srv-1:create_issue", "mcp:srv-2:search"]); + expect(denied).toContain("group:runtime"); + expect(denied).toContain("group:fs"); + expect(denied).toContain("group:web"); + }); }); diff --git a/packages/web/src/app/(app)/settings/page.tsx b/packages/web/src/app/(app)/settings/page.tsx index 071e06533..aa932eca7 100644 --- a/packages/web/src/app/(app)/settings/page.tsx +++ b/packages/web/src/app/(app)/settings/page.tsx @@ -9,6 +9,7 @@ import { SettingsUsers } from "@/components/settings-users"; import { SettingsContext } from "@/components/settings-context"; import { SettingsProfile } from "@/components/settings-profile"; import { SettingsGroups } from "@/components/settings-groups"; +import { SettingsMcpServers } from "@/components/settings-mcp-servers"; interface ProviderStatus { defaultProvider: string | null; @@ -99,6 +100,7 @@ export default function SettingsPage() { )} {isAdmin && Users} {isAdmin && Groups} + {isAdmin && MCP Servers} @@ -151,6 +153,12 @@ export default function SettingsPage() { )} + + {isAdmin && ( + + + + )} diff --git a/packages/web/src/app/api/mcp-servers/[serverId]/discover/route.ts b/packages/web/src/app/api/mcp-servers/[serverId]/discover/route.ts new file mode 100644 index 000000000..7049f35db --- /dev/null +++ b/packages/web/src/app/api/mcp-servers/[serverId]/discover/route.ts @@ -0,0 +1,33 @@ +// audit-exempt: discover only refreshes cached tool manifest, no state change worth auditing +import { NextRequest, NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/api-auth"; +import { discoverTools, getMcpServer } from "@/lib/mcp-servers"; +import { regenerateOpenClawConfig } from "@/lib/openclaw-config"; + +type Params = { params: Promise<{ serverId: string }> }; + +export async function POST(_request: NextRequest, { params }: Params) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + + const { serverId } = await params; + const result = await discoverTools(serverId); + + if (!result.success) { + if (result.error === "Server not found") { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + return NextResponse.json({ + success: false, + error: result.error, + }); + } + + regenerateOpenClawConfig().catch(() => {}); + + const server = await getMcpServer(serverId); + return NextResponse.json({ + success: true, + server, + }); +} diff --git a/packages/web/src/app/api/mcp-servers/[serverId]/route.ts b/packages/web/src/app/api/mcp-servers/[serverId]/route.ts new file mode 100644 index 000000000..37f262d1d --- /dev/null +++ b/packages/web/src/app/api/mcp-servers/[serverId]/route.ts @@ -0,0 +1,223 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/api-auth"; +import { appendAuditLog, type UpdateDetail } from "@/lib/audit"; +import { + getMcpServer, + updateMcpServer, + deleteMcpServer, + discoverTools, + validateCommand, +} from "@/lib/mcp-servers"; +import { regenerateOpenClawConfig } from "@/lib/openclaw-config"; + +type Params = { params: Promise<{ serverId: string }> }; + +export async function GET(_request: NextRequest, { params }: Params) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + + const { serverId } = await params; + const server = await getMcpServer(serverId); + + if (!server) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + return NextResponse.json(server); +} + +export async function PATCH(request: NextRequest, { params }: Params) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + const session = sessionOrError; + + const { serverId } = await params; + const existing = await getMcpServer(serverId); + + if (!existing) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + const body = await request.json(); + const { name, transport, command, args, url, envVars } = body; + + // ── Validation ────────────────────────────────────────────────────── + if (name !== undefined) { + if (typeof name !== "string" || !name.trim()) { + return NextResponse.json({ error: "Name cannot be empty" }, { status: 400 }); + } + if (name.trim().length > 100) { + return NextResponse.json({ error: "Name must be 100 characters or fewer" }, { status: 400 }); + } + } + + if (transport !== undefined && !["stdio", "http"].includes(transport)) { + return NextResponse.json({ error: 'Transport must be "stdio" or "http"' }, { status: 400 }); + } + + if (command !== undefined && command !== null) { + if (typeof command !== "string" || !command.trim()) { + return NextResponse.json({ error: "Command cannot be empty" }, { status: 400 }); + } + if (command.trim().length > 200) { + return NextResponse.json( + { error: "Command must be 200 characters or fewer" }, + { status: 400 } + ); + } + const cmdError = validateCommand(command.trim()); + if (cmdError) { + return NextResponse.json({ error: cmdError }, { status: 400 }); + } + } + + if (url !== undefined && url !== null) { + if (typeof url !== "string" || !url.trim()) { + return NextResponse.json({ error: "URL cannot be empty" }, { status: 400 }); + } + try { + const parsed = new URL(url.trim()); + if (!["http:", "https:"].includes(parsed.protocol)) { + throw new Error("bad protocol"); + } + } catch { + return NextResponse.json({ error: "URL must be a valid http/https URL" }, { status: 400 }); + } + } + + if (args !== undefined) { + if (!Array.isArray(args) || args.length > 20) { + return NextResponse.json( + { error: "Args must be an array of at most 20 items" }, + { status: 400 } + ); + } + if (!args.every((a: unknown) => typeof a === "string")) { + return NextResponse.json({ error: "Args must be strings" }, { status: 400 }); + } + } + + if (envVars !== undefined && envVars !== null) { + if (typeof envVars !== "object" || Array.isArray(envVars)) { + return NextResponse.json( + { error: "envVars must be an object of key-value pairs" }, + { status: 400 } + ); + } + const envKeyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/; + for (const key of Object.keys(envVars)) { + if (!envKeyPattern.test(key)) { + return NextResponse.json( + { error: `Invalid env var key: "${key}". Keys must be alphanumeric + underscore.` }, + { status: 400 } + ); + } + } + } + + // ── Update ────────────────────────────────────────────────────────── + let updated; + try { + updated = await updateMcpServer(serverId, { + name: name?.trim(), + transport, + command: command !== undefined ? (command?.trim() ?? null) : undefined, + args, + url: url !== undefined ? (url?.trim() ?? null) : undefined, + envVars: envVars !== undefined ? (envVars ?? null) : undefined, + }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to update server"; + if (message.includes("unique")) { + return NextResponse.json( + { error: "A server with this name already exists" }, + { status: 409 } + ); + } + return NextResponse.json({ error: message }, { status: 500 }); + } + + if (!updated) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + // ── Audit (only on actual changes) ───────────────────────────────── + const changes: Record = {}; + if (name !== undefined && name.trim() !== existing.name) { + changes.name = { from: existing.name, to: name.trim() }; + } + if (transport !== undefined && transport !== existing.transport) { + changes.transport = { from: existing.transport, to: transport }; + } + if (command !== undefined && command?.trim() !== existing.command) { + changes.command = { from: existing.command, to: command?.trim() ?? null }; + } + if (url !== undefined && url?.trim() !== existing.url) { + changes.url = { from: existing.url, to: url?.trim() ?? null }; + } + + if (Object.keys(changes).length > 0) { + const detail: UpdateDetail = { + server: { id: serverId, name: updated.name }, + changes, + }; + appendAuditLog({ + actorType: "user", + actorId: session.user.id!, + eventType: "mcp_server.updated", + resource: `mcp_server:${serverId}`, + detail, + }).catch(() => {}); + } + + // ── Re-discover on connection config changes ─────────────────────── + const connectionChanged = + transport !== undefined || + command !== undefined || + url !== undefined || + args !== undefined || + envVars !== undefined; + + if (connectionChanged) { + discoverTools(serverId).catch(() => {}); + } + + regenerateOpenClawConfig().catch(() => {}); + + const fresh = await getMcpServer(serverId); + return NextResponse.json(fresh); +} + +export async function DELETE(_request: NextRequest, { params }: Params) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + const session = sessionOrError; + + const { serverId } = await params; + + // Read before deleting (for audit snapshot) + const existing = await getMcpServer(serverId); + if (!existing) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + const deleted = await deleteMcpServer(serverId); + if (!deleted) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + appendAuditLog({ + actorType: "user", + actorId: session.user.id!, + eventType: "mcp_server.deleted", + resource: `mcp_server:${serverId}`, + detail: { + name: existing.name, + toolCount: existing.toolManifest?.length ?? 0, + }, + }).catch(() => {}); + + regenerateOpenClawConfig().catch(() => {}); + + return NextResponse.json({ success: true }); +} diff --git a/packages/web/src/app/api/mcp-servers/[serverId]/test/route.ts b/packages/web/src/app/api/mcp-servers/[serverId]/test/route.ts new file mode 100644 index 000000000..af6941769 --- /dev/null +++ b/packages/web/src/app/api/mcp-servers/[serverId]/test/route.ts @@ -0,0 +1,26 @@ +// audit-exempt: test connection is a read-only diagnostic action, no state change +import { NextRequest, NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/api-auth"; +import { testConnection } from "@/lib/mcp-servers"; + +type Params = { params: Promise<{ serverId: string }> }; + +export async function POST(_request: NextRequest, { params }: Params) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + + const { serverId } = await params; + const result = await testConnection(serverId); + + if (!result.success) { + return NextResponse.json( + { success: false, error: result.error }, + { status: result.error === "Server not found" ? 404 : 200 } + ); + } + + return NextResponse.json({ + success: true, + toolCount: result.tools?.length ?? 0, + }); +} diff --git a/packages/web/src/app/api/mcp-servers/route.ts b/packages/web/src/app/api/mcp-servers/route.ts new file mode 100644 index 000000000..9a928bceb --- /dev/null +++ b/packages/web/src/app/api/mcp-servers/route.ts @@ -0,0 +1,144 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/api-auth"; +import { appendAuditLog } from "@/lib/audit"; +import { listMcpServers, createMcpServer, discoverTools, validateCommand } from "@/lib/mcp-servers"; +import { regenerateOpenClawConfig } from "@/lib/openclaw-config"; + +export async function GET() { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + + const servers = await listMcpServers(); + return NextResponse.json(servers); +} + +export async function POST(request: NextRequest) { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + const session = sessionOrError; + + const body = await request.json(); + const { name, transport, command, args, url, envVars } = body; + + // ── Validation ────────────────────────────────────────────────────── + if (!name || typeof name !== "string" || !name.trim()) { + return NextResponse.json({ error: "Name is required" }, { status: 400 }); + } + if (name.trim().length > 100) { + return NextResponse.json({ error: "Name must be 100 characters or fewer" }, { status: 400 }); + } + + if (!transport || !["stdio", "http"].includes(transport)) { + return NextResponse.json({ error: 'Transport must be "stdio" or "http"' }, { status: 400 }); + } + + if (transport === "stdio") { + if (!command || typeof command !== "string" || !command.trim()) { + return NextResponse.json( + { error: "Command is required for stdio transport" }, + { status: 400 } + ); + } + if (command.trim().length > 200) { + return NextResponse.json( + { error: "Command must be 200 characters or fewer" }, + { status: 400 } + ); + } + const cmdError = validateCommand(command.trim()); + if (cmdError) { + return NextResponse.json({ error: cmdError }, { status: 400 }); + } + } + + if (transport === "http") { + if (!url || typeof url !== "string" || !url.trim()) { + return NextResponse.json({ error: "URL is required for HTTP transport" }, { status: 400 }); + } + try { + const parsed = new URL(url.trim()); + if (!["http:", "https:"].includes(parsed.protocol)) { + throw new Error("bad protocol"); + } + } catch { + return NextResponse.json({ error: "URL must be a valid http/https URL" }, { status: 400 }); + } + } + + if (args !== undefined) { + if (!Array.isArray(args) || args.length > 20) { + return NextResponse.json( + { error: "Args must be an array of at most 20 items" }, + { status: 400 } + ); + } + if (!args.every((a: unknown) => typeof a === "string")) { + return NextResponse.json({ error: "Args must be strings" }, { status: 400 }); + } + } + + if (envVars !== undefined && envVars !== null) { + if (typeof envVars !== "object" || Array.isArray(envVars)) { + return NextResponse.json( + { error: "envVars must be an object of key-value pairs" }, + { status: 400 } + ); + } + const envKeyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/; + for (const key of Object.keys(envVars)) { + if (!envKeyPattern.test(key)) { + return NextResponse.json( + { error: `Invalid env var key: "${key}". Keys must be alphanumeric + underscore.` }, + { status: 400 } + ); + } + } + } + + // ── Create ────────────────────────────────────────────────────────── + let server; + try { + server = await createMcpServer({ + name: name.trim(), + transport, + command: command?.trim(), + args: args ?? [], + url: url?.trim(), + envVars: envVars ?? undefined, + }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to create server"; + if (message.includes("unique")) { + return NextResponse.json( + { error: "A server with this name already exists" }, + { status: 409 } + ); + } + return NextResponse.json({ error: message }, { status: 500 }); + } + + // ── Discover tools (best-effort) ─────────────────────────────────── + const discovery = await discoverTools(server.id); + + // ── Audit ────────────────────────────────────────────────────────── + appendAuditLog({ + actorType: "user", + actorId: session.user.id!, + eventType: "mcp_server.created", + resource: `mcp_server:${server.id}`, + detail: { + server: { id: server.id, name: server.name }, + transport, + command: command?.trim() ?? null, + toolCount: discovery.tools?.length ?? 0, + }, + }).catch(() => {}); + + // ── Regenerate config ────────────────────────────────────────────── + regenerateOpenClawConfig().catch(() => {}); + + // Return the server with fresh tool data + const { getMcpServer } = await import("@/lib/mcp-servers"); + const fresh = await getMcpServer(server.id); + return NextResponse.json(fresh, { status: 201 }); +} diff --git a/packages/web/src/app/api/mcp-servers/tools/route.ts b/packages/web/src/app/api/mcp-servers/tools/route.ts new file mode 100644 index 000000000..6e510bf48 --- /dev/null +++ b/packages/web/src/app/api/mcp-servers/tools/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/api-auth"; +import { getMcpToolDefinitions } from "@/lib/mcp-servers"; + +export async function GET() { + const sessionOrError = await requireAdmin(); + if (sessionOrError instanceof NextResponse) return sessionOrError; + + const tools = await getMcpToolDefinitions(); + return NextResponse.json(tools); +} diff --git a/packages/web/src/components/agent-settings-permissions.tsx b/packages/web/src/components/agent-settings-permissions.tsx index 771ff5673..7cb669e17 100644 --- a/packages/web/src/components/agent-settings-permissions.tsx +++ b/packages/web/src/components/agent-settings-permissions.tsx @@ -3,9 +3,18 @@ import { useState, useRef, useEffect } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; import { DirectoryPicker } from "@/components/directory-picker"; import { getToolsByCategory } from "@/lib/tool-registry"; +interface McpToolDef { + id: string; + label: string; + description: string; + category: string; + serverName?: string; +} + interface AgentSettingsPermissionsProps { agent: { id: string; @@ -29,11 +38,37 @@ export function AgentSettingsPermissions({ const initialAllowedTools = useRef(agent.allowedTools); const initialAllowedPaths = useRef(agent.pluginConfig?.allowed_paths ?? []); + const [mcpTools, setMcpTools] = useState([]); + + useEffect(() => { + let cancelled = false; + fetch("/api/mcp-servers/tools") + .then((res) => (res.ok ? res.json() : [])) + .then((data) => { + if (!cancelled) setMcpTools(data); + }) + .catch(() => { + // MCP tools unavailable — non-admin or no servers + }); + return () => { + cancelled = true; + }; + }, []); + const safeTools = getToolsByCategory("safe"); const powerfulTools = getToolsByCategory("powerful"); const hasSafeToolChecked = safeTools.some((tool) => allowedTools.includes(tool.id)); + // Group MCP tools by server name + const mcpToolsByServer = new Map(); + for (const tool of mcpTools) { + const server = tool.serverName ?? "Unknown Server"; + const existing = mcpToolsByServer.get(server) ?? []; + existing.push(tool); + mcpToolsByServer.set(server, existing); + } + // Notify parent after every state change (and on mount) useEffect(() => { const isDirty = @@ -110,6 +145,46 @@ export function AgentSettingsPermissions({ ))} + +
+

MCP Tools

+ {mcpToolsByServer.size === 0 ? ( +

+ No MCP servers configured. Admins can add them in Settings → MCP Servers. +

+ ) : ( +
+ {Array.from(mcpToolsByServer.entries()).map(([serverName, tools]) => ( +
+
+

{serverName}

+ + {tools.length} {tools.length === 1 ? "tool" : "tools"} + +
+
+ {tools.map((tool) => ( +
+ handleToolToggle(tool.id)} + aria-label={tool.label} + /> + +
+ ))} +
+
+ ))} +
+ )} +
); } diff --git a/packages/web/src/components/settings-mcp-servers.tsx b/packages/web/src/components/settings-mcp-servers.tsx new file mode 100644 index 000000000..0075a62b1 --- /dev/null +++ b/packages/web/src/components/settings-mcp-servers.tsx @@ -0,0 +1,541 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface McpServer { + id: string; + name: string; + transport: string; + command: string | null; + args: string[]; + url: string | null; + status: string; + statusMessage: string | null; + lastCheckedAt: string | null; + toolManifest: Array<{ name: string; description: string }> | null; + envVarKeys: string[]; +} + +interface EnvVarRow { + key: string; + value: string; +} + +export function SettingsMcpServers() { + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [createOpen, setCreateOpen] = useState(false); + const [editServer, setEditServer] = useState(null); + const [deleteServerId, setDeleteServerId] = useState(null); + const [testingId, setTestingId] = useState(null); + const [discoveringId, setDiscoveringId] = useState(null); + + // Form state + const [formName, setFormName] = useState(""); + const [formTransport, setFormTransport] = useState<"stdio" | "http">("stdio"); + const [formCommand, setFormCommand] = useState(""); + const [formArgs, setFormArgs] = useState(""); + const [formUrl, setFormUrl] = useState(""); + const [formEnvVars, setFormEnvVars] = useState([]); + + const fetchServers = useCallback(async () => { + try { + const res = await fetch("/api/mcp-servers"); + if (res.ok) { + setServers(await res.json()); + } + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchServers(); + }, [fetchServers]); + + function resetForm() { + setFormName(""); + setFormTransport("stdio"); + setFormCommand(""); + setFormArgs(""); + setFormUrl(""); + setFormEnvVars([]); + } + + function openCreateDialog() { + resetForm(); + setCreateOpen(true); + } + + function openEditDialog(server: McpServer) { + setFormName(server.name); + setFormTransport(server.transport as "stdio" | "http"); + setFormCommand(server.command ?? ""); + setFormArgs((server.args ?? []).join(", ")); + setFormUrl(server.url ?? ""); + // Show existing env var keys with empty values (user must re-enter to change) + setFormEnvVars(server.envVarKeys.map((key) => ({ key, value: "" }))); + setEditServer(server); + } + + function buildEnvVarsPayload(): Record | undefined { + const envVars: Record = {}; + let hasValues = false; + for (const row of formEnvVars) { + if (row.key.trim() && row.value.trim()) { + envVars[row.key.trim()] = row.value.trim(); + hasValues = true; + } + } + return hasValues ? envVars : undefined; + } + + function parseArgs(input: string): string[] { + return input + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + } + + async function handleCreate() { + const body: Record = { + name: formName, + transport: formTransport, + }; + if (formTransport === "stdio") { + body.command = formCommand; + body.args = parseArgs(formArgs); + } else { + body.url = formUrl; + } + const envVars = buildEnvVarsPayload(); + if (envVars) body.envVars = envVars; + + const res = await fetch("/api/mcp-servers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (res.ok) { + setCreateOpen(false); + fetchServers(); + } + } + + async function handleEdit() { + if (!editServer) return; + + const body: Record = { + name: formName, + transport: formTransport, + }; + if (formTransport === "stdio") { + body.command = formCommand; + body.args = parseArgs(formArgs); + } else { + body.url = formUrl; + } + const envVars = buildEnvVarsPayload(); + if (envVars) body.envVars = envVars; + + await fetch(`/api/mcp-servers/${editServer.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + setEditServer(null); + fetchServers(); + } + + async function handleDelete(serverId: string) { + await fetch(`/api/mcp-servers/${serverId}`, { method: "DELETE" }); + setDeleteServerId(null); + fetchServers(); + } + + async function handleTestConnection(serverId: string) { + setTestingId(serverId); + try { + await fetch(`/api/mcp-servers/${serverId}/test`, { method: "POST" }); + fetchServers(); + } finally { + setTestingId(null); + } + } + + async function handleDiscover(serverId: string) { + setDiscoveringId(serverId); + try { + await fetch(`/api/mcp-servers/${serverId}/discover`, { method: "POST" }); + fetchServers(); + } finally { + setDiscoveringId(null); + } + } + + function addEnvVar() { + setFormEnvVars((prev) => [...prev, { key: "", value: "" }]); + } + + function removeEnvVar(index: number) { + setFormEnvVars((prev) => prev.filter((_, i) => i !== index)); + } + + function updateEnvVar(index: number, field: "key" | "value", val: string) { + setFormEnvVars((prev) => prev.map((row, i) => (i === index ? { ...row, [field]: val } : row))); + } + + function statusBadge(server: McpServer) { + if (server.status === "connected") { + return ( + + Connected + + ); + } + if (server.status === "error") { + return ( + + Error + + ); + } + return Unknown; + } + + if (loading) return

Loading...

; + + const formDialog = ( +
+
+ + setFormName(e.target.value)} + placeholder="e.g. GitHub MCP" + /> +
+ +
+ + +
+ + {formTransport === "stdio" && ( + <> +
+ + setFormCommand(e.target.value)} + placeholder="e.g. npx" + /> +
+
+ + setFormArgs(e.target.value)} + placeholder="e.g. -y, @modelcontextprotocol/server-github" + /> +
+ + )} + + {formTransport === "http" && ( +
+ + setFormUrl(e.target.value)} + placeholder="e.g. https://mcp.example.com" + /> +
+ )} + +
+
+ + +
+ {formEnvVars.map((row, i) => ( +
+ updateEnvVar(i, "key", e.target.value)} + className="flex-1" + /> + updateEnvVar(i, "value", e.target.value)} + className="flex-1" + /> + +
+ ))} + {editServer && formEnvVars.length > 0 && ( +

+ Leave values empty to keep existing credentials. +

+ )} +
+
+ ); + + return ( +
+ + + MCP Servers + + + + {servers.length === 0 ? ( +

+ No MCP servers configured. Add one to connect external tools to your agents. +

+ ) : ( + <> + {/* Mobile card view */} +
+ {servers.map((server) => ( +
+
+ {server.name} +
+ + {server.transport} + + {statusBadge(server)} +
+
+

+ {server.transport === "stdio" ? server.command : server.url} +

+

+ {server.toolManifest?.length ?? 0} tools +

+
+ + + + +
+
+ ))} +
+ + {/* Desktop table */} +
+ + + + Name + Transport + Connection + Status + Tools + Actions + + + + {servers.map((server) => ( + + {server.name} + + {server.transport} + + + {server.transport === "stdio" ? server.command : server.url} + + {statusBadge(server)} + {server.toolManifest?.length ?? 0} + + + + + + + + ))} + +
+
+ + )} +
+
+ + {/* Create dialog */} + + + + Add MCP Server + + Connect an MCP server to make its tools available to your agents. + + + {formDialog} +
+ +
+
+
+ + {/* Edit dialog */} + !open && setEditServer(null)}> + + + Edit MCP Server + Update the server configuration. + + {formDialog} +
+ +
+
+
+ + {/* Delete confirmation */} + !open && setDeleteServerId(null)} + > + + + Delete MCP Server + + This will permanently delete the server and remove its tools from all agents. This + action cannot be undone. + + + + Cancel + deleteServerId && handleDelete(deleteServerId)} + > + Delete + + + + +
+ ); +} diff --git a/packages/web/src/db/schema.ts b/packages/web/src/db/schema.ts index 9343c93bf..57018b24c 100644 --- a/packages/web/src/db/schema.ts +++ b/packages/web/src/db/schema.ts @@ -175,6 +175,32 @@ export const settings = pgTable("settings", { encrypted: boolean("encrypted").default(false), }); +// ── MCP Servers ───────────────────────────────────────────────────── + +export interface McpToolManifestEntry { + name: string; + description: string; + inputSchema: Record; +} + +export const mcpServers = pgTable("mcp_servers", { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text("name").notNull().unique(), + transport: text("transport").notNull().default("stdio"), // "stdio" | "http" + command: text("command"), // stdio: e.g. "npx" + args: jsonb("args").$type().notNull().default([]), // stdio: e.g. ["-y", "@modelcontextprotocol/server-github"] + url: text("url"), // http: e.g. "https://workspace-mcp.example.com" + envVars: text("env_vars"), // Encrypted JSON of env vars (nullable) + status: text("status").notNull().default("unknown"), // "connected" | "error" | "unknown" + statusMessage: text("status_message"), + lastCheckedAt: timestamp("last_checked_at"), + toolManifest: jsonb("tool_manifest").$type(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + // ── Audit Trail ────────────────────────────────────────────────────── export const actorTypeEnum = pgEnum("actor_type", ["user", "agent", "system"]); diff --git a/packages/web/src/lib/audit.ts b/packages/web/src/lib/audit.ts index 95458bc67..296a1f05d 100644 --- a/packages/web/src/lib/audit.ts +++ b/packages/web/src/lib/audit.ts @@ -22,7 +22,7 @@ export type MembershipDetail = { [key: string]: unknown; }; -export type AuditResource = "agent" | "group" | "user" | "settings" | "config"; +export type AuditResource = "agent" | "group" | "user" | "settings" | "config" | "mcp_server"; export type AuditEventType = | `tool.${string}` @@ -42,7 +42,10 @@ export type AuditEventType = | "group.deleted" | "group.members_updated" | "user.groups_updated" - | "user.role_updated"; + | "user.role_updated" + | "mcp_server.created" + | "mcp_server.updated" + | "mcp_server.deleted"; interface HmacFields { timestamp: Date; diff --git a/packages/web/src/lib/mcp-servers.ts b/packages/web/src/lib/mcp-servers.ts new file mode 100644 index 000000000..9988a8dd9 --- /dev/null +++ b/packages/web/src/lib/mcp-servers.ts @@ -0,0 +1,350 @@ +import { writeFileSync, unlinkSync, mkdirSync, existsSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { randomBytes } from "crypto"; +import { eq } from "drizzle-orm"; +import { db } from "@/db"; +import { mcpServers, agents, type McpToolManifestEntry } from "@/db/schema"; +import { encrypt, decrypt } from "@/lib/encryption"; +import type { ToolDefinition } from "@/lib/tool-registry"; + +// ── Types ──────────────────────────────────────────────────────────────── + +export interface CreateMcpServerInput { + name: string; + transport: "stdio" | "http"; + command?: string; + args?: string[]; + url?: string; + envVars?: Record; +} + +export interface UpdateMcpServerInput { + name?: string; + transport?: "stdio" | "http"; + command?: string | null; + args?: string[]; + url?: string | null; + envVars?: Record | null; +} + +export interface McpServer { + id: string; + name: string; + transport: string; + command: string | null; + args: string[]; + url: string | null; + status: string; + statusMessage: string | null; + lastCheckedAt: Date | null; + toolManifest: McpToolManifestEntry[] | null; + createdAt: Date; + updatedAt: Date; +} + +export interface McpServerWithEnvKeys extends McpServer { + envVarKeys: string[]; +} + +interface DiscoverResult { + success: boolean; + tools?: McpToolManifestEntry[]; + error?: string; +} + +// ── Slug generation ────────────────────────────────────────────────────── + +export function getServerSlug(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 30); +} + +// ── Shell injection validation ─────────────────────────────────────────── + +const SHELL_METACHARACTERS = /[;|&`$(){}]/; + +export function validateCommand(command: string): string | null { + if (SHELL_METACHARACTERS.test(command)) { + return "Command contains prohibited shell metacharacters"; + } + return null; +} + +// ── CRUD ───────────────────────────────────────────────────────────────── + +export async function createMcpServer(input: CreateMcpServerInput): Promise { + const encryptedEnvVars = + input.envVars && Object.keys(input.envVars).length > 0 + ? encrypt(JSON.stringify(input.envVars)) + : null; + + const [server] = await db + .insert(mcpServers) + .values({ + name: input.name, + transport: input.transport, + command: input.command ?? null, + args: input.args ?? [], + url: input.url ?? null, + envVars: encryptedEnvVars, + }) + .returning(); + + return toMcpServer(server); +} + +export async function updateMcpServer( + id: string, + input: UpdateMcpServerInput +): Promise { + const updates: Record = { updatedAt: new Date() }; + + if (input.name !== undefined) updates.name = input.name; + if (input.transport !== undefined) updates.transport = input.transport; + if (input.command !== undefined) updates.command = input.command; + if (input.args !== undefined) updates.args = input.args; + if (input.url !== undefined) updates.url = input.url; + if (input.envVars !== undefined) { + updates.envVars = + input.envVars && Object.keys(input.envVars).length > 0 + ? encrypt(JSON.stringify(input.envVars)) + : null; + } + + const [updated] = await db + .update(mcpServers) + .set(updates) + .where(eq(mcpServers.id, id)) + .returning(); + + if (!updated) return null; + return toMcpServer(updated); +} + +export async function deleteMcpServer(id: string): Promise { + // Strip mcp::* from all agents' allowedTools + const allAgents = await db.select().from(agents); + const prefix = `mcp:${id}:`; + + for (const agent of allAgents) { + const tools = (agent.allowedTools as string[]) || []; + const filtered = tools.filter((t) => !t.startsWith(prefix)); + if (filtered.length !== tools.length) { + await db.update(agents).set({ allowedTools: filtered }).where(eq(agents.id, agent.id)); + } + } + + const [deleted] = await db.delete(mcpServers).where(eq(mcpServers.id, id)).returning(); + + if (!deleted) return null; + return toMcpServer(deleted); +} + +export async function listMcpServers(): Promise { + const servers = await db.select().from(mcpServers); + return servers.map(toMcpServerWithEnvKeys); +} + +export async function getMcpServer(id: string): Promise { + const [server] = await db.select().from(mcpServers).where(eq(mcpServers.id, id)); + + if (!server) return null; + return toMcpServerWithEnvKeys(server); +} + +// ── Tool discovery via mcporter ────────────────────────────────────────── + +export async function discoverTools(serverId: string): Promise { + const [server] = await db.select().from(mcpServers).where(eq(mcpServers.id, serverId)); + + if (!server) return { success: false, error: "Server not found" }; + + let configPath: string | undefined; + + try { + const { createRuntime } = await import("mcporter"); + + // Build temp mcporter config with decrypted env vars + configPath = writeTempMcporterConfig(server); + const slug = getServerSlug(server.name); + + const runtime = await createRuntime({ configPath }); + + try { + const tools = await runtime.listTools(slug, { includeSchema: true }); + const manifest: McpToolManifestEntry[] = tools.map((t) => ({ + name: t.name, + description: t.description ?? "", + inputSchema: (t.inputSchema as Record) ?? {}, + })); + + await db + .update(mcpServers) + .set({ + toolManifest: manifest, + status: "connected", + statusMessage: null, + lastCheckedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(mcpServers.id, serverId)); + + return { success: true, tools: manifest }; + } finally { + await runtime.close(); + } + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + + await db + .update(mcpServers) + .set({ + status: "error", + statusMessage: message, + lastCheckedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(mcpServers.id, serverId)); + + return { success: false, error: message }; + } finally { + if (configPath) { + try { + unlinkSync(configPath); + } catch { + // best effort cleanup + } + } + } +} + +export async function testConnection(serverId: string): Promise { + return discoverTools(serverId); +} + +// ── MCP tool definitions for the tool registry ─────────────────────────── + +export async function getMcpToolDefinitions(): Promise { + const servers = await db.select().from(mcpServers); + const tools: ToolDefinition[] = []; + + for (const server of servers) { + if (!server.toolManifest) continue; + const manifest = server.toolManifest as McpToolManifestEntry[]; + + for (const tool of manifest) { + tools.push({ + id: `mcp:${server.id}:${tool.name}`, + label: tool.name, + description: tool.description, + category: "mcp", + serverName: server.name, + }); + } + } + + return tools; +} + +// ── mcporter config building ───────────────────────────────────────────── + +interface McpServerRow { + name: string; + transport: string; + command: string | null; + args: string[] | unknown; + url: string | null; + envVars: string | null; +} + +export function buildMcporterServerConfig( + server: McpServerRow, + decryptedEnv?: Record +): Record { + const env = decryptedEnv ?? decryptEnvVars(server.envVars); + + if (server.transport === "stdio") { + return { + command: server.command, + args: Array.isArray(server.args) ? server.args : [], + ...(Object.keys(env).length > 0 ? { env } : {}), + }; + } + + // HTTP transport + return { + baseUrl: server.url, + ...(Object.keys(env).length > 0 ? { env } : {}), + }; +} + +export function decryptEnvVars(encryptedEnvVars: string | null): Record { + if (!encryptedEnvVars) return {}; + try { + return JSON.parse(decrypt(encryptedEnvVars)); + } catch { + return {}; + } +} + +// ── Helpers ────────────────────────────────────────────────────────────── + +function writeTempMcporterConfig(server: McpServerRow): string { + const slug = getServerSlug(server.name); + const env = decryptEnvVars(server.envVars); + const serverConfig = buildMcporterServerConfig(server, env); + + const config = { + mcpServers: { + [slug]: serverConfig, + }, + }; + + const dir = join(tmpdir(), "pinchy-mcp"); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const filePath = join(dir, `mcporter-${randomBytes(8).toString("hex")}.json`); + writeFileSync(filePath, JSON.stringify(config, null, 2), { + encoding: "utf-8", + mode: 0o600, + }); + + return filePath; +} + +function toMcpServer(row: typeof mcpServers.$inferSelect): McpServer { + return { + id: row.id, + name: row.name, + transport: row.transport, + command: row.command, + args: (row.args as string[]) ?? [], + url: row.url, + status: row.status, + statusMessage: row.statusMessage, + lastCheckedAt: row.lastCheckedAt, + toolManifest: row.toolManifest as McpToolManifestEntry[] | null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +function toMcpServerWithEnvKeys(row: typeof mcpServers.$inferSelect): McpServerWithEnvKeys { + const server = toMcpServer(row); + let envVarKeys: string[] = []; + if (row.envVars) { + try { + const parsed = JSON.parse(decrypt(row.envVars)); + envVarKeys = Object.keys(parsed); + } catch { + // corrupted env vars — return empty keys + } + } + return { ...server, envVarKeys }; +} diff --git a/packages/web/src/lib/openclaw-config.ts b/packages/web/src/lib/openclaw-config.ts index 5b4e48464..11a987140 100644 --- a/packages/web/src/lib/openclaw-config.ts +++ b/packages/web/src/lib/openclaw-config.ts @@ -1,14 +1,16 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs"; import { randomBytes } from "crypto"; -import { dirname } from "path"; +import { dirname, join } from "path"; import { PROVIDERS, type ProviderName } from "@/lib/providers"; import { db } from "@/db"; -import { agents } from "@/db/schema"; +import { agents, mcpServers } from "@/db/schema"; import { getSetting } from "@/lib/settings"; import { computeDeniedGroups } from "@/lib/tool-registry"; import { getOpenClawWorkspacePath } from "@/lib/workspace"; import { restartState } from "@/server/restart-state"; import { migrateExistingSmithers } from "@/lib/migrate-onboarding"; +import { getServerSlug, buildMcporterServerConfig, decryptEnvVars } from "@/lib/mcp-servers"; +import type { McpToolManifestEntry } from "@/db/schema"; const CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH || "/openclaw-config/openclaw.json"; @@ -213,6 +215,82 @@ export async function regenerateOpenClawConfig() { // Note: pinchy-files is only included when agents use it (via pluginConfigs loop above). + // ── MCP servers: build pinchy-mcp plugin config + mcporter.json ────── + const allMcpServers = await db.select().from(mcpServers); + const serversWithTools = allMcpServers.filter( + (s) => s.toolManifest && (s.toolManifest as McpToolManifestEntry[]).length > 0 + ); + + // Collect per-agent MCP tool grants + const mcpAgentConfig: Record< + string, + { allowedMcpTools: Array<{ serverId: string; serverSlug: string; toolName: string }> } + > = {}; + const referencedServerIds = new Set(); + + for (const agent of allAgents) { + const allowedTools = (agent.allowedTools as string[]) || []; + const mcpTools = allowedTools.filter((t: string) => t.startsWith("mcp:")); + + if (mcpTools.length === 0) continue; + + const agentMcpTools: Array<{ serverId: string; serverSlug: string; toolName: string }> = []; + + for (const toolId of mcpTools) { + // Format: mcp:: + const parts = toolId.split(":"); + if (parts.length < 3) continue; + const serverId = parts[1]; + const toolName = parts.slice(2).join(":"); + + const server = serversWithTools.find((s) => s.id === serverId); + if (!server) continue; + + referencedServerIds.add(serverId); + agentMcpTools.push({ + serverId, + serverSlug: getServerSlug(server.name), + toolName, + }); + } + + if (agentMcpTools.length > 0) { + mcpAgentConfig[agent.id] = { allowedMcpTools: agentMcpTools }; + } + } + + // Only include pinchy-mcp when at least one agent has MCP tools + if (Object.keys(mcpAgentConfig).length > 0) { + // Write mcporter.json with only referenced servers + const mcporterServers: Record = {}; + for (const server of serversWithTools) { + if (!referencedServerIds.has(server.id)) continue; + const slug = getServerSlug(server.name); + const env = decryptEnvVars(server.envVars); + mcporterServers[slug] = buildMcporterServerConfig(server, env); + } + + const mcporterConfig = { mcpServers: mcporterServers }; + const configDir = dirname(CONFIG_PATH); + const mcporterPath = join(configDir, "mcporter.json"); + + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true }); + } + writeFileSync(mcporterPath, JSON.stringify(mcporterConfig, null, 2), { + encoding: "utf-8", + mode: 0o600, + }); + + entries["pinchy-mcp"] = { + enabled: true, + config: { + mcporterConfigPath: join("/root/.openclaw", "mcporter.json"), + agents: mcpAgentConfig, + }, + }; + } + // Set plugins.allow to only the enabled plugin IDs. This prevents OpenClaw from // auto-discovering unused plugins from the extensions directory, which would cause // either a restart loop (invalid config) or "disabled but config present" warning spam. diff --git a/packages/web/src/lib/smithers-soul.ts b/packages/web/src/lib/smithers-soul.ts index 866af058f..ad732511e 100644 --- a/packages/web/src/lib/smithers-soul.ts +++ b/packages/web/src/lib/smithers-soul.ts @@ -113,6 +113,19 @@ things work, guide them confidently. Here's what you know: - Once you've saved their context, the onboarding instructions go away and you have their info for all future conversations +### MCP Servers +- MCP (Model Context Protocol) servers let admins connect external tools to agents +- Admins configure MCP servers in Settings → MCP Servers: add the server name, + transport (stdio for local processes, HTTP for remote), and any credentials +- Once connected, Pinchy discovers the server's available tools automatically +- Admins then grant individual tools to specific agents via Agent Settings → + Permissions → MCP Tools section +- This follows the same allow-list model as built-in tools — agents start with + zero MCP tools and admins enable them one by one +- MCP server credentials are encrypted at rest (AES-256-GCM) +- If an MCP server goes down, the agent gets a clear error message (no crash) +- MCP tool calls appear in the audit trail just like built-in tool calls + ### Common Tasks - **Change AI model**: Agent Settings → General tab → Model dropdown - **Add a provider**: Settings → Providers → enter API key @@ -124,5 +137,7 @@ things work, guide them confidently. Here's what you know: - **Add organization context**: Settings → Context tab (admin only) - **Manage groups**: Settings → Groups (admin only) - **Set agent access**: Agent Settings → Access tab (admin only) +- **Add MCP server**: Settings → MCP Servers → Add Server (admin only) +- **Grant MCP tool to agent**: Agent Settings → Permissions → MCP Tools section - **View audit log**: Go to /audit (admin only) for a complete activity log `; diff --git a/packages/web/src/lib/tool-registry.ts b/packages/web/src/lib/tool-registry.ts index 6b4290d22..5c1c5add3 100644 --- a/packages/web/src/lib/tool-registry.ts +++ b/packages/web/src/lib/tool-registry.ts @@ -2,9 +2,10 @@ export interface ToolDefinition { id: string; label: string; description: string; - category: "safe" | "powerful"; + category: "safe" | "powerful" | "mcp"; group?: string; requiresDirectories?: boolean; + serverName?: string; } export const TOOL_REGISTRY: readonly ToolDefinition[] = [ diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index d27dbc396..626f8e529 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ environment: "jsdom", setupFiles: ["./src/test-setup.ts"], globals: true, - exclude: ["node_modules", "e2e"], + exclude: ["node_modules", ".next", "e2e"], }, resolve: { alias: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 928e29d74..06442fb73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: version: 3.0.3 better-auth: specifier: ^1.5.3 - version: 1.5.3(@better-auth/drizzle-adapter@1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.5.3(@better-auth/drizzle-adapter@1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.7(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -58,9 +58,12 @@ importers: lucide-react: specifier: ^0.575.0 version: 0.575.0(react@19.2.4) + mcporter: + specifier: ^0.7.3 + version: 0.7.3 next: - specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 16.1.7 + version: 16.1.7(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -321,6 +324,10 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -432,6 +439,9 @@ packages: '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -817,6 +827,12 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -838,8 +854,11 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} '@img/sharp-darwin-arm64@0.34.5': @@ -1007,63 +1026,76 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@next/env@16.1.7': + resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==} '@next/eslint-plugin-next@16.1.6': resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==} - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + '@next/swc-darwin-arm64@16.1.7': + resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + '@next/swc-darwin-x64@16.1.7': + resolution: {integrity: sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + '@next/swc-linux-arm64-gnu@16.1.7': + resolution: {integrity: sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + '@next/swc-linux-arm64-musl@16.1.7': + resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + '@next/swc-linux-x64-gnu@16.1.7': + resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + '@next/swc-linux-x64-musl@16.1.7': + resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + '@next/swc-win32-arm64-msvc@16.1.7': + resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + '@next/swc-win32-x64-msvc@16.1.7': + resolution: {integrity: sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1092,6 +1124,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oxc-project/types@0.103.0': + resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} + '@playwright/test@1.58.2': resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} @@ -1818,6 +1853,90 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-OqIUyNid1M4xTj6VRXp/Lht/qIP8fo25QyAZlCP+p6D2ATCEhyW4ZIFLnC9zAGN/HMbXoCzvwfa8Jjg/8J4YEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.57': + resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -2363,6 +2482,10 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2377,9 +2500,20 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -2485,6 +2619,11 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.10.8: + resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -2575,6 +2714,10 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2593,6 +2736,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2612,6 +2759,9 @@ packages: caniuse-lite@1.0.30001770: resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2623,6 +2773,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -2645,6 +2799,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + cli-truncate@5.1.1: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} @@ -2676,9 +2834,29 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2753,6 +2931,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2877,6 +3059,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} @@ -2886,6 +3071,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -2933,6 +3122,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2952,6 +3144,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3090,13 +3285,35 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3113,6 +3330,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3133,6 +3353,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3148,6 +3372,14 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3265,6 +3497,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3272,6 +3508,10 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3285,6 +3525,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3305,6 +3549,9 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -3312,6 +3559,14 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -3379,6 +3634,10 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3402,6 +3661,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3426,6 +3688,10 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3482,6 +3748,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3494,6 +3766,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3606,6 +3881,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -3643,6 +3922,11 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mcporter@0.7.3: + resolution: {integrity: sha512-egoPVYqTnWb3NjRIxo+xc8OrAI0dlPrJm9pAiZx0pImuNIV5rKhGtTnIfH/Y1ldGPVu74ibj3KR5c9U/QSdQFA==} + engines: {node: '>=20.11.0'} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -3691,6 +3975,14 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3783,6 +4075,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3830,14 +4130,18 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + next@16.1.7: + resolution: {integrity: sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -3899,6 +4203,13 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -3911,6 +4222,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.3.0: + resolution: {integrity: sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==} + engines: {node: '>=20'} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -3933,6 +4248,10 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3944,6 +4263,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3963,6 +4285,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + playwright-core@1.58.2: resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} @@ -4012,10 +4338,18 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4032,6 +4366,14 @@ packages: '@types/react-dom': optional: true + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -4165,6 +4507,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-beta.57: + resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4173,6 +4520,10 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4191,6 +4542,9 @@ packages: safe-regex@2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -4210,6 +4564,14 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} @@ -4225,6 +4587,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4290,9 +4655,17 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.3.1: + resolution: {integrity: sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==} + engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4417,6 +4790,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -4455,6 +4832,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4512,6 +4893,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -4581,6 +4966,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -4711,6 +5100,9 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -4742,6 +5134,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -4960,6 +5361,8 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -5069,6 +5472,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 @@ -5307,6 +5715,10 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@hookform/resolvers@5.2.2(react-hook-form@7.71.2(react@19.2.4))': dependencies: '@standard-schema/utils': 0.3.0 @@ -5323,7 +5735,9 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.0.0': + '@iarna/toml@2.2.5': {} + + '@img/colour@1.1.0': optional: true '@img/sharp-darwin-arm64@0.34.5': @@ -5408,7 +5822,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.9.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -5439,6 +5853,28 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -5446,34 +5882,41 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.1.6': {} + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.1.7': {} '@next/eslint-plugin-next@16.1.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.1.6': + '@next/swc-darwin-arm64@16.1.7': optional: true - '@next/swc-darwin-x64@16.1.6': + '@next/swc-darwin-x64@16.1.7': optional: true - '@next/swc-linux-arm64-gnu@16.1.6': + '@next/swc-linux-arm64-gnu@16.1.7': optional: true - '@next/swc-linux-arm64-musl@16.1.6': + '@next/swc-linux-arm64-musl@16.1.7': optional: true - '@next/swc-linux-x64-gnu@16.1.6': + '@next/swc-linux-x64-gnu@16.1.7': optional: true - '@next/swc-linux-x64-musl@16.1.6': + '@next/swc-linux-x64-musl@16.1.7': optional: true - '@next/swc-win32-arm64-msvc@16.1.6': + '@next/swc-win32-arm64-msvc@16.1.7': optional: true - '@next/swc-win32-x64-msvc@16.1.6': + '@next/swc-win32-x64-msvc@16.1.7': optional: true '@noble/ciphers@2.1.1': {} @@ -5494,6 +5937,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oxc-project/types@0.103.0': {} + '@playwright/test@1.58.2': dependencies: playwright: 1.58.2 @@ -6267,6 +6712,49 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.57': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.59.0': @@ -6426,7 +6914,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -6741,6 +7229,11 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -6749,6 +7242,10 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6756,6 +7253,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -6879,11 +7383,13 @@ snapshots: balanced-match@1.0.2: {} + baseline-browser-mapping@2.10.8: {} + baseline-browser-mapping@2.9.19: {} bcryptjs@3.0.3: {} - better-auth@1.5.3(@better-auth/drizzle-adapter@1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): + better-auth@1.5.3(@better-auth/drizzle-adapter@1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.7(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@better-auth/core': 1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/kysely-adapter': 1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) @@ -6903,7 +7409,7 @@ snapshots: '@better-auth/drizzle-adapter': 1.5.3(@better-auth/core@1.5.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8)) drizzle-kit: 0.31.9 drizzle-orm: 0.45.1(kysely@0.28.11)(postgres@3.4.8) - next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.7(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) vitest: 4.0.18(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) @@ -6923,6 +7429,20 @@ snapshots: dependencies: require-from-string: 2.0.2 + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6946,6 +7466,8 @@ snapshots: buffer-from@1.1.2: {} + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6967,6 +7489,8 @@ snapshots: caniuse-lite@1.0.30001770: {} + caniuse-lite@1.0.30001780: {} + ccount@2.0.1: {} chai@6.2.2: {} @@ -6976,6 +7500,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -6994,6 +7520,8 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-spinners@3.4.0: {} + cli-truncate@5.1.1: dependencies: slice-ansi: 7.1.2 @@ -7017,8 +7545,21 @@ snapshots: concat-map@0.0.1: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7098,6 +7639,8 @@ snapshots: defu@6.1.4: {} + depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -7136,12 +7679,16 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ee-first@1.1.1: {} + electron-to-chromium@1.5.286: {} emoji-regex@10.6.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -7254,6 +7801,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.45.1: {} + esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3 @@ -7321,6 +7870,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -7540,10 +8091,56 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -7560,6 +8157,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -7576,6 +8175,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7592,6 +8202,10 @@ snapshots: dependencies: is-callable: 1.2.7 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.2: optional: true @@ -7720,6 +8334,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.12.8: {} + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): dependencies: '@exodus/bytes': 1.14.1(@noble/hashes@2.0.1) @@ -7728,6 +8344,14 @@ snapshots: html-url-attributes@3.0.1: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -7744,6 +8368,10 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -7757,6 +8385,8 @@ snapshots: indent-string@4.0.0: {} + inherits@2.0.4: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -7765,6 +8395,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -7842,6 +8476,8 @@ snapshots: is-hexadecimal@2.0.1: {} + is-interactive@2.0.0: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -7857,6 +8493,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -7885,6 +8523,8 @@ snapshots: dependencies: which-typed-array: 1.1.20 + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -7952,6 +8592,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -7960,6 +8604,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -8058,6 +8704,11 @@ snapshots: lodash.merge@4.6.2: {} + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + log-update@6.1.0: dependencies: ansi-escapes: 7.3.0 @@ -8092,6 +8743,21 @@ snapshots: math-intrinsics@1.1.0: {} + mcporter@0.7.3: + dependencies: + '@iarna/toml': 2.2.5 + '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) + acorn: 8.16.0 + commander: 14.0.3 + es-toolkit: 1.45.1 + jsonc-parser: 3.3.1 + ora: 9.3.0 + rolldown: 1.0.0-beta.57 + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -8247,6 +8913,10 @@ snapshots: mdn-data@2.12.2: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -8445,6 +9115,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -8473,30 +9149,32 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.7(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 16.1.6 + '@next/env': 16.1.7 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001770 + baseline-browser-mapping: 2.10.8 + caniuse-lite: 1.0.30001780 postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 + '@next/swc-darwin-arm64': 16.1.7 + '@next/swc-darwin-x64': 16.1.7 + '@next/swc-linux-arm64-gnu': 16.1.7 + '@next/swc-linux-arm64-musl': 16.1.7 + '@next/swc-linux-x64-gnu': 16.1.7 + '@next/swc-linux-x64-musl': 16.1.7 + '@next/swc-win32-arm64-msvc': 16.1.7 + '@next/swc-win32-x64-msvc': 16.1.7 '@playwright/test': 1.58.2 sharp: 0.34.5 transitivePeerDependencies: @@ -8556,6 +9234,14 @@ snapshots: obug@2.1.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -8576,6 +9262,17 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.3.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.1 + string-width: 8.2.0 + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -8608,12 +9305,16 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} path-parse@1.0.7: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -8624,6 +9325,8 @@ snapshots: pidtree@0.6.0: {} + pkce-challenge@5.0.1: {} + playwright-core@1.58.2: {} playwright@1.58.2: @@ -8668,8 +9371,17 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -8735,6 +9447,15 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -8902,6 +9623,25 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-beta.57: + dependencies: + '@oxc-project/types': 0.103.0 + '@rolldown/pluginutils': 1.0.0-beta.57 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-x64': 1.0.0-beta.57 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.57 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.57 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.57 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.57 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57 + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -8935,6 +9675,16 @@ snapshots: rou3@0.7.12: {} + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -8962,6 +9712,8 @@ snapshots: dependencies: regexp-tree: 0.1.27 + safer-buffer@2.1.2: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -8974,6 +9726,31 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@3.0.1: {} set-function-length@1.2.2: @@ -8998,9 +9775,11 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: - '@img/colour': 1.0.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.7.4 optionalDependencies: @@ -9093,8 +9872,12 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.3.1: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -9230,6 +10013,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.23 @@ -9268,6 +10053,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -9358,6 +10149,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -9434,6 +10227,8 @@ snapshots: dependencies: react: 19.2.4 + vary@1.1.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -9572,6 +10367,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {} @@ -9584,6 +10381,12 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6