Add MCP support (#1028)
This commit is contained in:
@@ -137,6 +137,10 @@ export function createDyadEngine(
|
||||
if ("dyadRequestId" in parsedBody) {
|
||||
delete parsedBody.dyadRequestId;
|
||||
}
|
||||
const dyadDisableFiles = parsedBody.dyadDisableFiles;
|
||||
if ("dyadDisableFiles" in parsedBody) {
|
||||
delete parsedBody.dyadDisableFiles;
|
||||
}
|
||||
|
||||
// Track and modify requestId with attempt number
|
||||
let modifiedRequestId = requestId;
|
||||
@@ -147,7 +151,7 @@ export function createDyadEngine(
|
||||
}
|
||||
|
||||
// Add files to the request if they exist
|
||||
if (files?.length) {
|
||||
if (files?.length && !dyadDisableFiles) {
|
||||
parsedBody.dyad_options = {
|
||||
files,
|
||||
enable_lazy_edits: options.dyadOptions.enableLazyEdits,
|
||||
|
||||
108
src/ipc/utils/mcp_consent.ts
Normal file
108
src/ipc/utils/mcp_consent.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { db } from "../../db";
|
||||
import { mcpToolConsents } from "../../db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { IpcMainInvokeEvent } from "electron";
|
||||
|
||||
export type Consent = "ask" | "always" | "denied";
|
||||
|
||||
const pendingConsentResolvers = new Map<
|
||||
string,
|
||||
(d: "accept-once" | "accept-always" | "decline") => void
|
||||
>();
|
||||
|
||||
export function waitForConsent(
|
||||
requestId: string,
|
||||
): Promise<"accept-once" | "accept-always" | "decline"> {
|
||||
return new Promise((resolve) => {
|
||||
pendingConsentResolvers.set(requestId, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveConsent(
|
||||
requestId: string,
|
||||
decision: "accept-once" | "accept-always" | "decline",
|
||||
) {
|
||||
const resolver = pendingConsentResolvers.get(requestId);
|
||||
if (resolver) {
|
||||
pendingConsentResolvers.delete(requestId);
|
||||
resolver(decision);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStoredConsent(
|
||||
serverId: number,
|
||||
toolName: string,
|
||||
): Promise<Consent> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(mcpToolConsents)
|
||||
.where(
|
||||
and(
|
||||
eq(mcpToolConsents.serverId, serverId),
|
||||
eq(mcpToolConsents.toolName, toolName),
|
||||
),
|
||||
);
|
||||
if (rows.length === 0) return "ask";
|
||||
return (rows[0].consent as Consent) ?? "ask";
|
||||
}
|
||||
|
||||
export async function setStoredConsent(
|
||||
serverId: number,
|
||||
toolName: string,
|
||||
consent: Consent,
|
||||
): Promise<void> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(mcpToolConsents)
|
||||
.where(
|
||||
and(
|
||||
eq(mcpToolConsents.serverId, serverId),
|
||||
eq(mcpToolConsents.toolName, toolName),
|
||||
),
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
await db
|
||||
.update(mcpToolConsents)
|
||||
.set({ consent })
|
||||
.where(
|
||||
and(
|
||||
eq(mcpToolConsents.serverId, serverId),
|
||||
eq(mcpToolConsents.toolName, toolName),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await db.insert(mcpToolConsents).values({ serverId, toolName, consent });
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireMcpToolConsent(
|
||||
event: IpcMainInvokeEvent,
|
||||
params: {
|
||||
serverId: number;
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
toolDescription?: string | null;
|
||||
inputPreview?: string | null;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
const current = await getStoredConsent(params.serverId, params.toolName);
|
||||
if (current === "always") return true;
|
||||
if (current === "denied") return false;
|
||||
|
||||
// Ask renderer for a decision via event bridge
|
||||
const requestId = `${params.serverId}:${params.toolName}:${Date.now()}`;
|
||||
(event.sender as any).send("mcp:tool-consent-request", {
|
||||
requestId,
|
||||
...params,
|
||||
});
|
||||
const response = await waitForConsent(requestId);
|
||||
|
||||
if (response === "accept-always") {
|
||||
await setStoredConsent(params.serverId, params.toolName, "always");
|
||||
return true;
|
||||
}
|
||||
if (response === "decline") {
|
||||
return false;
|
||||
}
|
||||
return response === "accept-once";
|
||||
}
|
||||
59
src/ipc/utils/mcp_manager.ts
Normal file
59
src/ipc/utils/mcp_manager.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { db } from "../../db";
|
||||
import { mcpServers } from "../../db/schema";
|
||||
import { experimental_createMCPClient, experimental_MCPClient } from "ai";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
|
||||
class McpManager {
|
||||
private static _instance: McpManager;
|
||||
static get instance(): McpManager {
|
||||
if (!this._instance) this._instance = new McpManager();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private clients = new Map<number, experimental_MCPClient>();
|
||||
|
||||
async getClient(serverId: number): Promise<experimental_MCPClient> {
|
||||
const existing = this.clients.get(serverId);
|
||||
if (existing) return existing;
|
||||
const server = await db
|
||||
.select()
|
||||
.from(mcpServers)
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
const s = server.find((x) => x.id === serverId);
|
||||
if (!s) throw new Error(`MCP server not found: ${serverId}`);
|
||||
let transport: StdioClientTransport | StreamableHTTPClientTransport;
|
||||
if (s.transport === "stdio") {
|
||||
const args = s.args ?? [];
|
||||
const env = s.envJson ?? undefined;
|
||||
if (!s.command) throw new Error("MCP server command is required");
|
||||
transport = new StdioClientTransport({
|
||||
command: s.command,
|
||||
args,
|
||||
env,
|
||||
});
|
||||
} else if (s.transport === "http") {
|
||||
if (!s.url) throw new Error("HTTP MCP requires url");
|
||||
transport = new StreamableHTTPClientTransport(new URL(s.url as string));
|
||||
} else {
|
||||
throw new Error(`Unsupported MCP transport: ${s.transport}`);
|
||||
}
|
||||
const client = await experimental_createMCPClient({
|
||||
transport,
|
||||
});
|
||||
this.clients.set(serverId, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
dispose(serverId: number) {
|
||||
const c = this.clients.get(serverId);
|
||||
if (c) {
|
||||
c.close();
|
||||
this.clients.delete(serverId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpManager = McpManager.instance;
|
||||
Reference in New Issue
Block a user