@bool01master/gemini-web-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +363 -0
- package/extension/background.js +118 -0
- package/extension/content.js +1475 -0
- package/extension/manifest.json +33 -0
- package/extension/popup.css +43 -0
- package/extension/popup.html +18 -0
- package/extension/popup.js +37 -0
- package/package.json +38 -0
- package/src/extension-bridge.js +749 -0
- package/src/gemini-web-client.js +678 -0
- package/src/index.js +56 -0
- package/src/server.js +272 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
import { createApp } from "./server.js";
|
|
8
|
+
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const extensionPath = path.resolve(here, "../extension");
|
|
11
|
+
|
|
12
|
+
function maybeHandleCliArgs() {
|
|
13
|
+
const args = new Set(process.argv.slice(2));
|
|
14
|
+
|
|
15
|
+
if (args.has("--extension-path")) {
|
|
16
|
+
console.log(extensionPath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (args.has("--help") || args.has("-h")) {
|
|
21
|
+
console.log(`gemini-web-mcp
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
gemini-web-mcp Start the MCP stdio server
|
|
25
|
+
gemini-web-mcp --extension-path Print the unpacked Chrome extension path
|
|
26
|
+
gemini-web-mcp --help Show this help
|
|
27
|
+
`);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
if (maybeHandleCliArgs()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const app = await createApp({ startBridge: true });
|
|
40
|
+
const transport = new StdioServerTransport();
|
|
41
|
+
await app.server.connect(transport);
|
|
42
|
+
|
|
43
|
+
const shutdown = async () => {
|
|
44
|
+
await app.close().catch(() => {});
|
|
45
|
+
process.exit(0);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
49
|
+
process.on(signal, shutdown);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch(async (error) => {
|
|
54
|
+
console.error("Gemini web MCP failed:", error);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
package/src/server.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
|
|
4
|
+
import { GeminiExtensionBridge } from "./extension-bridge.js";
|
|
5
|
+
|
|
6
|
+
export const TOOL_NAMES = [
|
|
7
|
+
"gemini_bridge_status",
|
|
8
|
+
"gemini_page_status",
|
|
9
|
+
"gemini_capture_images",
|
|
10
|
+
"gemini_run_prompt",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function formatSummary(result) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
|
|
16
|
+
if (typeof result.clientCount === "number") {
|
|
17
|
+
lines.push(`Connected extension clients: ${result.clientCount}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if ("ready" in result) {
|
|
21
|
+
lines.push(
|
|
22
|
+
result.ready
|
|
23
|
+
? "Gemini session is ready."
|
|
24
|
+
: "Gemini session needs manual login.",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result.bridgeUrl) {
|
|
29
|
+
lines.push(`Bridge: ${result.bridgeUrl}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (result.url) {
|
|
33
|
+
lines.push(`URL: ${result.url}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.runDir) {
|
|
37
|
+
lines.push(`Run dir: ${result.runDir}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (result.mode) {
|
|
41
|
+
lines.push(`Mode: ${result.mode}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(result.imagePaths) && result.imagePaths.length > 0) {
|
|
45
|
+
lines.push(`Saved images: ${result.imagePaths.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(result.curlCommands) && result.curlCommands.length > 0) {
|
|
49
|
+
lines.push(
|
|
50
|
+
`\nImages could not be saved directly. Use these curl commands to download:\n${result.curlCommands.join("\n")}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asToolResult(result) {
|
|
58
|
+
const content = [];
|
|
59
|
+
const summary = formatSummary(result);
|
|
60
|
+
|
|
61
|
+
if (summary) {
|
|
62
|
+
content.push({
|
|
63
|
+
type: "text",
|
|
64
|
+
text: summary,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result.text) {
|
|
69
|
+
content.push({
|
|
70
|
+
type: "text",
|
|
71
|
+
text: result.text,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content,
|
|
77
|
+
structuredContent: result,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function commandTimeoutForWait(waitTimeoutMs) {
|
|
82
|
+
return Math.max(Number(waitTimeoutMs || 0) + 30_000, 180_000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function createApp(options = {}) {
|
|
86
|
+
const bridge = new GeminiExtensionBridge(options.bridge);
|
|
87
|
+
if (options.startBridge !== false) {
|
|
88
|
+
await bridge.start();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const server = new McpServer({
|
|
92
|
+
name: "gemini-extension-local",
|
|
93
|
+
version: "0.2.0",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
server.registerTool(
|
|
97
|
+
"gemini_bridge_status",
|
|
98
|
+
{
|
|
99
|
+
description:
|
|
100
|
+
"Return the local Gemini bridge status and list connected extension clients.",
|
|
101
|
+
inputSchema: {},
|
|
102
|
+
},
|
|
103
|
+
async () => {
|
|
104
|
+
const result = await bridge.getStatus();
|
|
105
|
+
return asToolResult(result);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
server.registerTool(
|
|
110
|
+
"gemini_page_status",
|
|
111
|
+
{
|
|
112
|
+
description:
|
|
113
|
+
"Ask the active Gemini browser tab for its current page status through the extension bridge.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
targetClientId: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Optional bridge client id to query a specific Gemini tab."),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
async ({ targetClientId } = {}) => {
|
|
122
|
+
const result = await bridge.runCommand(
|
|
123
|
+
"get_status",
|
|
124
|
+
{},
|
|
125
|
+
{ timeoutMs: 20_000, targetClientId },
|
|
126
|
+
);
|
|
127
|
+
return asToolResult(result);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
server.registerTool(
|
|
132
|
+
"gemini_capture_images",
|
|
133
|
+
{
|
|
134
|
+
description:
|
|
135
|
+
"Capture currently visible generated images from the active Gemini tab and save them to disk without sending a new prompt.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
outputDir: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe(
|
|
141
|
+
"Directory where this capture should save result.json and extracted images.",
|
|
142
|
+
),
|
|
143
|
+
targetClientId: z
|
|
144
|
+
.string()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe("Optional bridge client id to capture from a specific Gemini tab."),
|
|
147
|
+
maxImages: z
|
|
148
|
+
.number()
|
|
149
|
+
.int()
|
|
150
|
+
.positive()
|
|
151
|
+
.max(8)
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Maximum number of visible images to save. Defaults to 4."),
|
|
154
|
+
label: z
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("Optional label used in the saved run directory name."),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
async ({
|
|
161
|
+
outputDir,
|
|
162
|
+
targetClientId,
|
|
163
|
+
maxImages = 4,
|
|
164
|
+
label = "capture-images",
|
|
165
|
+
} = {}) => {
|
|
166
|
+
const rawResult = await bridge.runCommand(
|
|
167
|
+
"capture_images",
|
|
168
|
+
{ maxImages },
|
|
169
|
+
{ timeoutMs: 60_000, targetClientId },
|
|
170
|
+
);
|
|
171
|
+
const result = await bridge.finalizeRunResult(label, rawResult, {
|
|
172
|
+
outputDir,
|
|
173
|
+
targetClientId,
|
|
174
|
+
});
|
|
175
|
+
return asToolResult(result);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
server.registerTool(
|
|
180
|
+
"gemini_run_prompt",
|
|
181
|
+
{
|
|
182
|
+
description:
|
|
183
|
+
"Send one prompt through the active Gemini tab, optionally upload local images, optionally select a Gemini mode, then save returned images to disk.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
prompt: z.string().min(1).describe("Prompt to send to Gemini."),
|
|
186
|
+
mode: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Visible Gemini mode/model label to try to select before sending."),
|
|
190
|
+
images: z
|
|
191
|
+
.array(z.string())
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Local image file paths to upload before sending."),
|
|
194
|
+
outputDir: z
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe(
|
|
198
|
+
"Directory where this run should save result.json and any extracted images.",
|
|
199
|
+
),
|
|
200
|
+
targetClientId: z
|
|
201
|
+
.string()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("Optional bridge client id to run against a specific Gemini tab."),
|
|
204
|
+
newChat: z
|
|
205
|
+
.boolean()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Try to start a fresh chat before sending. Defaults to true."),
|
|
208
|
+
waitTimeoutMs: z
|
|
209
|
+
.number()
|
|
210
|
+
.int()
|
|
211
|
+
.positive()
|
|
212
|
+
.max(600000)
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("How long to wait for a stable response. Defaults to 120000."),
|
|
215
|
+
maxImages: z
|
|
216
|
+
.number()
|
|
217
|
+
.int()
|
|
218
|
+
.nonnegative()
|
|
219
|
+
.max(8)
|
|
220
|
+
.optional()
|
|
221
|
+
.describe(
|
|
222
|
+
"Maximum number of visible output images to save. Use 0 to skip extraction. Defaults to 4.",
|
|
223
|
+
),
|
|
224
|
+
takeScreenshot: z
|
|
225
|
+
.boolean()
|
|
226
|
+
.optional()
|
|
227
|
+
.describe("Save a full-page screenshot. Defaults to true."),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
async ({
|
|
231
|
+
prompt,
|
|
232
|
+
mode,
|
|
233
|
+
images = [],
|
|
234
|
+
outputDir,
|
|
235
|
+
targetClientId,
|
|
236
|
+
newChat = true,
|
|
237
|
+
waitTimeoutMs = 120000,
|
|
238
|
+
maxImages = 4,
|
|
239
|
+
}) => {
|
|
240
|
+
const preparedImages = await bridge.readLocalImages(images);
|
|
241
|
+
const rawResult = await bridge.runCommand(
|
|
242
|
+
"run_prompt",
|
|
243
|
+
{
|
|
244
|
+
prompt,
|
|
245
|
+
mode,
|
|
246
|
+
images: preparedImages,
|
|
247
|
+
newChat,
|
|
248
|
+
waitTimeoutMs,
|
|
249
|
+
maxImages,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
targetClientId,
|
|
253
|
+
timeoutMs: commandTimeoutForWait(waitTimeoutMs),
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
const result = await bridge.finalizeRunResult(prompt, rawResult, {
|
|
257
|
+
outputDir,
|
|
258
|
+
targetClientId,
|
|
259
|
+
});
|
|
260
|
+
return asToolResult(result);
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
server,
|
|
266
|
+
bridge,
|
|
267
|
+
async close() {
|
|
268
|
+
await server.close().catch(() => {});
|
|
269
|
+
await bridge.close().catch(() => {});
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|