Files
moreminimore-vibe/src/ipc/handlers/mcp_handlers.ts
2025-09-19 15:43:39 -07:00

164 lines
4.8 KiB
TypeScript

import { IpcMainInvokeEvent } from "electron";
import log from "electron-log";
import { db } from "../../db";
import { mcpServers, mcpToolConsents } from "../../db/schema";
import { eq, and } from "drizzle-orm";
import { createLoggedHandler } from "./safe_handle";
import { resolveConsent } from "../utils/mcp_consent";
import { getStoredConsent } from "../utils/mcp_consent";
import { mcpManager } from "../utils/mcp_manager";
import { CreateMcpServer, McpServerUpdate, McpTool } from "../ipc_types";
const logger = log.scope("mcp_handlers");
const handle = createLoggedHandler(logger);
type ConsentDecision = "accept-once" | "accept-always" | "decline";
export function registerMcpHandlers() {
// CRUD for MCP servers
handle("mcp:list-servers", async () => {
return await db.select().from(mcpServers);
});
handle(
"mcp:create-server",
async (_event: IpcMainInvokeEvent, params: CreateMcpServer) => {
const { name, transport, command, args, envJson, url, enabled } = params;
const result = await db
.insert(mcpServers)
.values({
name,
transport,
command: command || null,
args: args || null,
envJson: envJson || null,
url: url || null,
enabled: !!enabled,
})
.returning();
return result[0];
},
);
handle(
"mcp:update-server",
async (_event: IpcMainInvokeEvent, params: McpServerUpdate) => {
const update: any = {};
if (params.name !== undefined) update.name = params.name;
if (params.transport !== undefined) update.transport = params.transport;
if (params.command !== undefined) update.command = params.command;
if (params.args !== undefined) update.args = params.args || null;
if (params.cwd !== undefined) update.cwd = params.cwd;
if (params.envJson !== undefined) update.envJson = params.envJson || null;
if (params.url !== undefined) update.url = params.url;
if (params.enabled !== undefined) update.enabled = !!params.enabled;
const result = await db
.update(mcpServers)
.set(update)
.where(eq(mcpServers.id, params.id))
.returning();
// If server config changed, dispose cached client to be recreated on next use
try {
mcpManager.dispose(params.id);
} catch {}
return result[0];
},
);
handle(
"mcp:delete-server",
async (_event: IpcMainInvokeEvent, id: number) => {
try {
mcpManager.dispose(id);
} catch {}
await db.delete(mcpServers).where(eq(mcpServers.id, id));
return { success: true };
},
);
// Tools listing (dynamic)
handle(
"mcp:list-tools",
async (
_event: IpcMainInvokeEvent,
serverId: number,
): Promise<McpTool[]> => {
try {
const client = await mcpManager.getClient(serverId);
const remoteTools = await client.tools();
const tools = await Promise.all(
Object.entries(remoteTools).map(async ([name, tool]) => ({
name,
description: tool.description ?? null,
consent: await getStoredConsent(serverId, name),
})),
);
return tools;
} catch (e) {
logger.error("Failed to list tools", e);
return [];
}
},
);
// Consents
handle("mcp:get-tool-consents", async () => {
return await db.select().from(mcpToolConsents);
});
handle(
"mcp:set-tool-consent",
async (
_event: IpcMainInvokeEvent,
params: {
serverId: number;
toolName: string;
consent: "ask" | "always" | "denied";
},
) => {
const existing = await db
.select()
.from(mcpToolConsents)
.where(
and(
eq(mcpToolConsents.serverId, params.serverId),
eq(mcpToolConsents.toolName, params.toolName),
),
);
if (existing.length > 0) {
const result = await db
.update(mcpToolConsents)
.set({ consent: params.consent })
.where(
and(
eq(mcpToolConsents.serverId, params.serverId),
eq(mcpToolConsents.toolName, params.toolName),
),
)
.returning();
return result[0];
} else {
const result = await db
.insert(mcpToolConsents)
.values({
serverId: params.serverId,
toolName: params.toolName,
consent: params.consent,
})
.returning();
return result[0];
}
},
);
// Tool consent request/response handshake
// Receive consent response from renderer
handle(
"mcp:tool-consent-response",
async (_event, data: { requestId: string; decision: ConsentDecision }) => {
resolveConsent(data.requestId, data.decision);
},
);
}