@itoseo/agy-mcp
Version:
MCP Server for Antigravity CLI — 让 CodeX/Claude Code 调用 AGY 作为执行子代理
282 lines (276 loc) • 9.91 kB
JavaScript
// 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;
});