Add MCP support (#1028)
This commit is contained in:
163
src/ipc/handlers/mcp_handlers.ts
Normal file
163
src/ipc/handlers/mcp_handlers.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user