UNPKG

@itoseo/agy-mcp

Version:

MCP Server for Antigravity CLI — 让 CodeX/Claude Code 调用 AGY 作为执行子代理

282 lines (276 loc) 9.91 kB
#!/usr/bin/env node // src/index.ts import { McpServer } from "@modelcontextprotocol/server"; import { serveStdio } from "@modelcontextprotocol/server/stdio"; // src/tools.ts import { z } from "zod"; // src/agy-executor.ts import { spawn } from "child_process"; import { createInterface } from "readline"; var DEFAULT_MODEL = "Gemini 3.7 Flash (High)"; var DEFAULT_EFFORT = "high"; function resolveModelArgs(model, effort) { const targetModel = model ?? DEFAULT_MODEL; const hasEffortInName = /\((high|medium|low)\)/i.test(targetModel) || /-(high|medium|low)$/i.test(targetModel); if (hasEffortInName) { return ["--model", targetModel]; } const targetEffort = effort ?? DEFAULT_EFFORT; const capitalized = targetEffort.charAt(0).toUpperCase() + targetEffort.slice(1).toLowerCase(); const candidateWithEffort = `${targetModel} (${capitalized})`; return ["--model", candidateWithEffort]; } async function executeAgy(options) { const args = []; args.push(`--print=${options.prompt}`); args.push("--output-format", "stream-json"); args.push(...resolveModelArgs(options.model, options.effort)); if (options.conversationId) { args.push("--conversation", options.conversationId); } if (options.addDirs) { for (const dir of options.addDirs) { args.push("--add-dir", dir); } } if (options.mode) { args.push("--mode", options.mode); } args.push("--dangerously-skip-permissions"); args.push("--sandbox"); const timeoutMs = (options.timeoutSeconds ?? 300) * 1e3; return new Promise((resolve, reject) => { const child = spawn("agy", args, { cwd: options.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] }); let result; let stderrChunks = []; const timer = setTimeout(() => { child.kill("SIGTERM"); reject(new Error(`agy \u6267\u884C\u8D85\u65F6\uFF08${options.timeoutSeconds ?? 300}s\uFF09`)); }, timeoutMs); child.stderr.on("data", (chunk) => { stderrChunks.push(chunk.toString()); }); const rl = createInterface({ input: child.stdout }); rl.on("line", async (line) => { const trimmed = line.trim(); if (!trimmed) return; try { const event = JSON.parse(trimmed); if (event.event === "result") { result = event.result; } if (options.onEvent) { await options.onEvent(event); } } catch { } }); child.on("error", (err) => { clearTimeout(timer); reject(new Error(`agy \u8FDB\u7A0B\u542F\u52A8\u5931\u8D25: ${err.message}`)); }); child.on("close", (code) => { clearTimeout(timer); if (result) { resolve(result); return; } const stderr = stderrChunks.join(""); reject( new Error( `agy \u8FDB\u7A0B\u9000\u51FA\uFF08code=${code}\uFF09\u4F46\u672A\u8FD4\u56DE\u7ED3\u679C\u3002` + (stderr ? ` stderr: ${stderr}` : "") ) ); }); }); } async function listAgyModels() { return new Promise((resolve, reject) => { const child = spawn("agy", ["models"], { stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); const timer = setTimeout(() => { child.kill("SIGTERM"); reject(new Error("agy models \u8D85\u65F6")); }, 15e3); child.on("error", (err) => { clearTimeout(timer); reject(new Error(`agy models \u542F\u52A8\u5931\u8D25: ${err.message}`)); }); child.on("close", (code) => { clearTimeout(timer); if (code === 0) { resolve(stdout.trim()); } else { reject(new Error(`agy models \u9000\u51FA code=${code}\u3002stderr: ${stderr}`)); } }); }); } // src/tools.ts import { appendFileSync, mkdirSync, symlinkSync, unlinkSync, writeFileSync } from "fs"; import { randomBytes } from "crypto"; var LOG_DIR = "/tmp/agy-mcp"; try { mkdirSync(LOG_DIR, { recursive: true }); } catch { } function formatResult(result, logFile) { if (result.status === "ERROR") { return { content: [ { type: "text", text: `\u9519\u8BEF: ${result.error ?? result.response}` } ], isError: true }; } const meta = [ `conversation_id: ${result.conversation_id}`, `duration: ${result.duration_seconds.toFixed(1)}s`, `turns: ${result.num_turns}`, `tokens: ${result.usage.total_tokens}`, `log: ${logFile}` ].join(" | "); return { content: [ { type: "text", text: result.response }, { type: "text", text: `--- ${meta}` } ] }; } function createExecutionSession(ctx, executionId) { const progressToken = ctx.mcpReq._meta?.progressToken; const shortId = executionId.slice(-6); const logFile = `${LOG_DIR}/${executionId}.log`; const latestSymlink = `${LOG_DIR}/latest.log`; let stepCount = 0; try { writeFileSync(logFile, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${executionId}] AGY \u6267\u884C\u5F00\u59CB... `, "utf-8"); try { unlinkSync(latestSymlink); } catch { } symlinkSync(logFile, latestSymlink); } catch { } const onEvent = async (event) => { if (event.event !== "step_update") return; const delta = event.step_update.text_delta; if (!delta) return; process.stderr.write(`[agy:${shortId}] ${delta}`); try { appendFileSync(logFile, delta, "utf-8"); } catch { } if (progressToken !== void 0) { stepCount++; await ctx.mcpReq.notify({ method: "notifications/progress", params: { progressToken, progress: stepCount, total: 0, message: delta } }); } }; return { onEvent, logFile }; } function registerTools(server) { server.registerTool( "agy_prompt", { description: "\u5411 Antigravity AI \u53D1\u9001\u63D0\u793A\u8BCD\u5E76\u83B7\u53D6\u54CD\u5E94\u3002Antigravity \u662F\u4E00\u4E2A\u5F3A\u5927\u7684 AI \u7F16\u7801\u52A9\u624B\uFF0C\u53EF\u4EE5\u8BFB\u5199\u6587\u4EF6\u3001\u8FD0\u884C\u547D\u4EE4\u3001\u641C\u7D22\u4EE3\u7801\u5E93\u3002\u9002\u7528\u4E8E\u4EE3\u7801\u751F\u6210\u3001\u4EE3\u7801\u5BA1\u67E5\u3001\u91CD\u6784\u3001\u8C03\u8BD5\u7B49\u7F16\u7801\u4EFB\u52A1\u3002\u8FD4\u56DE\u7ED3\u679C\u4E2D\u5305\u542B conversation_id\uFF0C\u53EF\u914D\u5408 agy_conversation \u5DE5\u5177\u8FDB\u884C\u591A\u8F6E\u5BF9\u8BDD\u3002", inputSchema: z.object({ prompt: z.string().describe("\u53D1\u9001\u7ED9 Antigravity \u7684\u4EFB\u52A1\u63CF\u8FF0"), cwd: z.string().optional().describe("\u5DE5\u4F5C\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09"), add_dirs: z.array(z.string()).optional().describe("\u989D\u5916\u7684\u5DE5\u4F5C\u76EE\u5F55\u5217\u8868\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09"), mode: z.enum(["accept-edits", "plan"]).optional().describe( "\u6267\u884C\u6A21\u5F0F\uFF1Aaccept-edits\uFF08\u9ED8\u8BA4\uFF0C\u81EA\u52A8\u5E94\u7528\u4FEE\u6539\uFF09\u6216 plan\uFF08\u4EC5\u89C4\u5212\u4E0D\u6267\u884C\uFF09" ), model: z.string().optional().describe( "\u6A21\u578B\u540D\u79F0\uFF0C\u9ED8\u8BA4 'Gemini 3.7 Flash (High)'\u3002\u4F7F\u7528 agy_models \u67E5\u770B\u53EF\u7528\u6A21\u578B" ), effort: z.enum(["low", "medium", "high"]).optional().describe("\u63A8\u7406\u5F3A\u5EA6\uFF0C\u9ED8\u8BA4 high"), timeout_seconds: z.number().optional().describe("\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09\uFF0C\u9ED8\u8BA4 300") }) }, async (params, ctx) => { const executionId = `prompt-${Date.now()}-${randomBytes(3).toString("hex")}`; const { onEvent, logFile } = createExecutionSession(ctx, executionId); const result = await executeAgy({ prompt: params.prompt, cwd: params.cwd, addDirs: params.add_dirs, mode: params.mode, model: params.model, effort: params.effort, timeoutSeconds: params.timeout_seconds, onEvent }); return formatResult(result, logFile); } ); server.registerTool( "agy_conversation", { description: "\u5EF6\u7EED\u4E00\u4E2A\u5DF2\u6709\u7684 Antigravity \u5BF9\u8BDD\u3002\u4F7F\u7528 agy_prompt \u8FD4\u56DE\u7684 conversation_id \u6765\u7EE7\u7EED\u4E4B\u524D\u7684\u4E0A\u4E0B\u6587\u3002\u9002\u5408\u591A\u8F6E\u4EA4\u4E92\u573A\u666F\uFF0C\u5982\u8FED\u4EE3\u4EE3\u7801\u4FEE\u6539\u3001\u8FFD\u95EE\u7EC6\u8282\u7B49\u3002", inputSchema: z.object({ conversation_id: z.string().describe("\u8981\u5EF6\u7EED\u7684\u5BF9\u8BDD ID\uFF08\u7531 agy_prompt \u8FD4\u56DE\uFF09"), prompt: z.string().describe("\u8FFD\u52A0\u7684\u6307\u4EE4"), timeout_seconds: z.number().optional().describe("\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09\uFF0C\u9ED8\u8BA4 300") }) }, async (params, ctx) => { const executionId = `conv-${Date.now()}-${randomBytes(3).toString("hex")}`; const { onEvent, logFile } = createExecutionSession(ctx, executionId); const result = await executeAgy({ prompt: params.prompt, conversationId: params.conversation_id, timeoutSeconds: params.timeout_seconds, onEvent }); return formatResult(result, logFile); } ); server.registerTool( "agy_models", { description: "\u67E5\u8BE2 Antigravity \u53EF\u7528\u7684 AI \u6A21\u578B\u5217\u8868\u3002", inputSchema: z.object({}) }, async () => { const output = await listAgyModels(); return { content: [{ type: "text", text: output }] }; } ); } // src/index.ts serveStdio(() => { const server = new McpServer({ name: "agy-mcp", version: "1.0.0" }); registerTools(server); return server; });