refactor(mcp): 封装浏览器操作为 MCP 工具,移除手动浏览器操控逻辑

This commit is contained in:
knowen
2026-03-19 17:23:03 +08:00
parent 66bba6c4e7
commit f90dd8643a
5 changed files with 1245 additions and 73 deletions

View File

@@ -125,7 +125,7 @@ const config = {
browserProtocolTimeout: envInt('BROWSER_PROTOCOL_TIMEOUT', 60_000),
/** 截图 / 图片输出目录 */
outputDir: envStr('OUTPUT_DIR', resolve('output')),
outputDir: envStr('OUTPUT_DIR', join(projectRoot, 'gemini-image')),
// ── Daemon 配置 ──

141
src/mcp-server.js Normal file
View File

@@ -0,0 +1,141 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
// 复用已有的统一入口,不修改原有逻辑
import { createGeminiSession, disconnect } from './index.js';
import config from './config.js';
const server = new McpServer({
name: "gemini-mcp-server",
version: "1.0.0",
});
// 注册工具
server.registerTool(
"gemini_generate_image",
{
description: "调用后台的 Gemini 浏览器会话生成高质量图片",
inputSchema: {
prompt: z.string().describe("图片的详细描述词"),
newSession: z.boolean().default(false).describe(
"是否新建会话。true= 开启全新对话; false = 复用当前已有的 Gemini 会话页"
),
},
},
async ({ prompt, newSession }) => {
try {
const { ops } = await createGeminiSession();
const result = await ops.generateImage(prompt, { newChat: newSession });
// 执行完毕立刻断开,交还给 Daemon 倒计时
disconnect();
if (!result.ok) {
return {
content: [{ type: "text", text: `生成失败: ${result.error}` }],
isError: true,
};
}
// 将 base64 写入本地文件
const base64Data = result.dataUrl.split(',')[1];
const mimeMatch = result.dataUrl.match(/^data:(image\/\w+);/);
const ext = mimeMatch ? mimeMatch[1].split('/')[1] : 'png';
mkdirSync(config.outputDir, { recursive: true });
const filename = `gemini_${Date.now()}.${ext}`;
const filePath = join(config.outputDir, filename);
writeFileSync(filePath, Buffer.from(base64Data, 'base64'));
console.error(`[mcp] 图片已保存至 ${filePath}`);
return {
content: [
{ type: "text", text: `图片生成成功!已保存至: ${filePath}` },
{
type: "image",
data: base64Data,
mimeType: mimeMatch ? mimeMatch[1] : "image/png",
},
],
};
} catch (err) {
return {
content: [{ type: "text", text: `执行崩溃: ${err.message}` }],
isError: true,
};
}
}
);
// 查询浏览器信息
server.registerTool(
"gemini_browser_info",
{
description: "获取 Gemini 浏览器会话的连接信息CDP 端口、WebSocket 地址、Daemon 状态等),方便外部工具直连浏览器",
inputSchema: {},
},
async () => {
const daemonUrl = `http://127.0.0.1:${config.daemonPort}`;
try {
// 1. 检查 Daemon 健康状态
const healthRes = await fetch(`${daemonUrl}/health`, { signal: AbortSignal.timeout(3000) });
const health = await healthRes.json();
if (!health.ok) {
return {
content: [{ type: "text", text: "Daemon 未就绪,浏览器可能未启动。请先调用 gemini_generate_image 触发自动启动。" }],
isError: true,
};
}
// 2. 获取浏览器连接信息
const acquireRes = await fetch(`${daemonUrl}/browser/acquire`, { signal: AbortSignal.timeout(5000) });
const acquire = await acquireRes.json();
const info = {
daemon: {
url: daemonUrl,
port: config.daemonPort,
status: "running",
},
browser: {
cdpPort: config.browserDebugPort,
wsEndpoint: acquire.wsEndpoint || null,
pid: acquire.pid || null,
headless: config.browserHeadless,
},
config: {
protocolTimeout: config.browserProtocolTimeout,
outputDir: config.outputDir,
daemonTTL: config.daemonTTL,
},
};
return {
content: [{ type: "text", text: JSON.stringify(info, null, 2) }],
};
} catch (err) {
return {
content: [{
type: "text",
text: `无法连接 Daemon (${daemonUrl}),浏览器可能未启动。\n错误: ${err.message}\n\n提示: 请先调用 gemini_generate_image 触发自动启动,或手动运行 npm run daemon`,
}],
isError: true,
};
}
}
);
// 启动标准输入输出通信
async function run() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Gemini MCP Server running on stdio"); // 必须用 console.error避免污染 stdio
}
run().catch(console.error);