Add MCP support (#1028)

This commit is contained in:
Will Chen
2025-09-19 15:43:39 -07:00
committed by GitHub
parent 7b160b7d0b
commit 6d3c397d40
39 changed files with 3865 additions and 650 deletions

View File

@@ -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,

View 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";
}

View 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;