kibbutz-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/build/index.js +298 -0
- package/package.json +31 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
let wss;
|
|
11
|
+
let extensionSocket = null;
|
|
12
|
+
let token = null;
|
|
13
|
+
const map = new Map();
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: 'kibbutz-mcp',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
});
|
|
18
|
+
function findChromeExecutable() {
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
if (platform === 'linux') {
|
|
21
|
+
return '/opt/google/chrome/chrome';
|
|
22
|
+
}
|
|
23
|
+
if (platform === 'darwin') {
|
|
24
|
+
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
25
|
+
}
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
const suffix = '\\Google\\Chrome\\Application\\chrome.exe';
|
|
28
|
+
const bases = [
|
|
29
|
+
process.env.LOCALAPPDATA,
|
|
30
|
+
process.env.PROGRAMFILES,
|
|
31
|
+
process.env['PROGRAMFILES(X86)'],
|
|
32
|
+
process.env.HOMEDRIVE && path.join(process.env.HOMEDRIVE, 'Program Files'),
|
|
33
|
+
process.env.HOMEDRIVE && path.join(process.env.HOMEDRIVE, 'Program Files (x86)'),
|
|
34
|
+
].filter(Boolean);
|
|
35
|
+
for (const base of bases) {
|
|
36
|
+
const candidate = path.join(base, suffix);
|
|
37
|
+
if (fs.existsSync(candidate)) {
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Chrome executable not found for platform: ${platform}`);
|
|
43
|
+
}
|
|
44
|
+
function openChromeWithExtension(wsPort) {
|
|
45
|
+
const executablePath = findChromeExecutable();
|
|
46
|
+
const url = new URL('chrome-extension://bpfjmggaaiigpfahhmpmacfhlemnhhip/kibbutz-mcp.html');
|
|
47
|
+
token = uuidv4();
|
|
48
|
+
url.searchParams.set('wsPort', String(wsPort));
|
|
49
|
+
url.searchParams.set('token', token);
|
|
50
|
+
const child = spawn(executablePath, [url.toString()], {
|
|
51
|
+
detached: true,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
shell: false,
|
|
54
|
+
stdio: 'ignore',
|
|
55
|
+
});
|
|
56
|
+
child.unref();
|
|
57
|
+
}
|
|
58
|
+
const sendAndWaitForReply = (message, args) => {
|
|
59
|
+
if (!extensionSocket || extensionSocket.readyState !== WebSocket.OPEN) {
|
|
60
|
+
return Promise.resolve({
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: `Connection failed: The MCP server cannot reach the Chrome extension.
|
|
65
|
+
|
|
66
|
+
Please check the following:
|
|
67
|
+
1. Is the browser open?
|
|
68
|
+
2. Is the MCP switch inside the extension popup turned ON?
|
|
69
|
+
|
|
70
|
+
If the issue persists, copy and paste this URL into your browser address bar:
|
|
71
|
+
chrome-extension://bpfjmggaaiigpfahhmpmacfhlemnhhip/kibbutz-mcp.html`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
isError: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const id = uuidv4();
|
|
79
|
+
map.set(id, {
|
|
80
|
+
resolve: (result) => {
|
|
81
|
+
resolve({
|
|
82
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
83
|
+
isError: false,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
reject,
|
|
87
|
+
});
|
|
88
|
+
extensionSocket?.send(JSON.stringify({
|
|
89
|
+
id,
|
|
90
|
+
message,
|
|
91
|
+
args,
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
server.registerTool('SNAPSHOT_MCP', {
|
|
96
|
+
title: 'Get window snapshot',
|
|
97
|
+
description: 'Get the current window tabs organized as a nested tree structure. Returns groups and their child tabs, along with standalone tabs, titles, and URLs.',
|
|
98
|
+
}, (args) => sendAndWaitForReply('SNAPSHOT_MCP', args));
|
|
99
|
+
server.registerTool('CLOSE_GROUPS_MCP', {
|
|
100
|
+
title: 'Close groups',
|
|
101
|
+
description: 'Close specific browser groups and all their contained tabs.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
groupIds: z
|
|
104
|
+
.array(z.number())
|
|
105
|
+
.min(1)
|
|
106
|
+
.describe('List of group identifiers (integers) to close.'),
|
|
107
|
+
},
|
|
108
|
+
}, (args) => sendAndWaitForReply('CLOSE_GROUPS_MCP', args));
|
|
109
|
+
server.registerTool('UNGROUPS_MCP', {
|
|
110
|
+
title: 'Ungroup groups',
|
|
111
|
+
description: 'Ungroup specific browser groups using their group IDs. The tabs will become standalone.',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
groupIds: z
|
|
114
|
+
.array(z.number())
|
|
115
|
+
.min(1)
|
|
116
|
+
.describe('List of group identifiers (integers) to ungroup.'),
|
|
117
|
+
},
|
|
118
|
+
}, (args) => sendAndWaitForReply('UNGROUPS_MCP', args));
|
|
119
|
+
server.registerTool('CLOSE_TABS_MCP', {
|
|
120
|
+
title: 'Close tabs',
|
|
121
|
+
description: 'Close specific browser tabs using their unique IDs.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
tabIds: z
|
|
124
|
+
.array(z.number())
|
|
125
|
+
.min(1)
|
|
126
|
+
.describe('List of specific tab identifiers (integers) to close.'),
|
|
127
|
+
},
|
|
128
|
+
}, (args) => sendAndWaitForReply('CLOSE_TABS_MCP', args));
|
|
129
|
+
server.registerTool('UNGROUP_TABS_MCP', {
|
|
130
|
+
title: 'Ungroup tabs',
|
|
131
|
+
description: 'Remove specific tabs from their assigned groups using their tab IDs. The tabs will become standalone.',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
tabIds: z
|
|
134
|
+
.array(z.number())
|
|
135
|
+
.min(1)
|
|
136
|
+
.describe('List of specific tab identifiers (integers) to remove from their groups.'),
|
|
137
|
+
},
|
|
138
|
+
}, (args) => sendAndWaitForReply('UNGROUP_TABS_MCP', args));
|
|
139
|
+
server.registerTool('ADD_TO_GROUP_MCP', {
|
|
140
|
+
title: 'Add tabs to group',
|
|
141
|
+
description: 'Move specific tabs into an existing browser group.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
tabIds: z
|
|
144
|
+
.array(z.number())
|
|
145
|
+
.min(1)
|
|
146
|
+
.describe('List of tab IDs to move into the target group.'),
|
|
147
|
+
groupId: z
|
|
148
|
+
.number()
|
|
149
|
+
.describe('The integer ID of an existing group to move the tabs into.'),
|
|
150
|
+
},
|
|
151
|
+
}, (args) => sendAndWaitForReply('ADD_TO_GROUP_MCP', args));
|
|
152
|
+
server.registerTool('MOVE_GROUP_MCP', {
|
|
153
|
+
title: 'Move group',
|
|
154
|
+
description: 'Reposition a browser group to a new index.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
groupId: z.number().describe('The ID of the group to move.'),
|
|
157
|
+
index: z
|
|
158
|
+
.number()
|
|
159
|
+
.describe('The new position index. Use 0 for the start, -1 for the end.'),
|
|
160
|
+
},
|
|
161
|
+
}, (args) => sendAndWaitForReply('MOVE_GROUP_MCP', args));
|
|
162
|
+
server.registerTool('MOVE_TABS_MCP', {
|
|
163
|
+
title: 'Move tabs',
|
|
164
|
+
description: 'Move one or more tabs to a specific index position. Multiple tabs will be placed contiguously starting at the target index.',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
tabIds: z.array(z.number()).min(1).describe('List of tab IDs to move.'),
|
|
167
|
+
index: z
|
|
168
|
+
.number()
|
|
169
|
+
.describe('The new position index. Use 0 for the start, -1 for the end.'),
|
|
170
|
+
},
|
|
171
|
+
}, (args) => sendAndWaitForReply('MOVE_TABS_MCP', args));
|
|
172
|
+
server.registerTool('ADD_TO_NEW_GROUP_MCP', {
|
|
173
|
+
title: 'Create group',
|
|
174
|
+
description: 'Create a new browser group from a list of tabs, with a title and color.',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
tabIds: z
|
|
177
|
+
.array(z.number())
|
|
178
|
+
.min(1)
|
|
179
|
+
.describe('List of tab IDs to group together into the new group.'),
|
|
180
|
+
title: z.string().describe('The title of the new group'),
|
|
181
|
+
color: z
|
|
182
|
+
.enum([
|
|
183
|
+
'grey',
|
|
184
|
+
'blue',
|
|
185
|
+
'red',
|
|
186
|
+
'yellow',
|
|
187
|
+
'green',
|
|
188
|
+
'pink',
|
|
189
|
+
'purple',
|
|
190
|
+
'cyan',
|
|
191
|
+
'orange',
|
|
192
|
+
])
|
|
193
|
+
.describe('The color of the new group. Must be one of the supported Chrome colors.'),
|
|
194
|
+
},
|
|
195
|
+
}, (args) => sendAndWaitForReply('ADD_TO_NEW_GROUP_MCP', args));
|
|
196
|
+
server.registerTool('UPDATE_GROUP_MCP', {
|
|
197
|
+
title: 'Update group',
|
|
198
|
+
description: 'Update the title or color of an existing browser group. At least one property (title or color) should be provided.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
groupId: z.number().describe('The ID of the group to update'),
|
|
201
|
+
title: z
|
|
202
|
+
.string()
|
|
203
|
+
.optional()
|
|
204
|
+
.describe('The new title. Leave undefined to keep the current title.'),
|
|
205
|
+
color: z
|
|
206
|
+
.enum([
|
|
207
|
+
'grey',
|
|
208
|
+
'blue',
|
|
209
|
+
'red',
|
|
210
|
+
'yellow',
|
|
211
|
+
'green',
|
|
212
|
+
'pink',
|
|
213
|
+
'purple',
|
|
214
|
+
'cyan',
|
|
215
|
+
'orange',
|
|
216
|
+
])
|
|
217
|
+
.optional()
|
|
218
|
+
.describe('The new color. Leave undefined to keep the current color.'),
|
|
219
|
+
},
|
|
220
|
+
}, (args) => sendAndWaitForReply('UPDATE_GROUP_MCP', args));
|
|
221
|
+
async function main() {
|
|
222
|
+
const httpServer = createServer((req, res) => {
|
|
223
|
+
try {
|
|
224
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
225
|
+
const retToken = url.searchParams.get('token');
|
|
226
|
+
const verify = retToken === token;
|
|
227
|
+
if (verify) {
|
|
228
|
+
token = uuidv4();
|
|
229
|
+
}
|
|
230
|
+
res.writeHead(404);
|
|
231
|
+
res.end(JSON.stringify({ verify }));
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
console.error('Failed to parse URL:', req.url, e);
|
|
235
|
+
res.writeHead(404);
|
|
236
|
+
res.end(JSON.stringify({ verify: false }));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
wss = new WebSocketServer({ noServer: true });
|
|
240
|
+
wss.on('connection', (ws, request) => {
|
|
241
|
+
extensionSocket = ws;
|
|
242
|
+
ws.on('error', console.error);
|
|
243
|
+
ws.on('close', () => {
|
|
244
|
+
if (extensionSocket === ws) {
|
|
245
|
+
extensionSocket = null;
|
|
246
|
+
}
|
|
247
|
+
for (const { reject } of map.values()) {
|
|
248
|
+
reject(new Error('Extension WebSocket closed'));
|
|
249
|
+
}
|
|
250
|
+
map.clear();
|
|
251
|
+
});
|
|
252
|
+
ws.on('message', (data) => {
|
|
253
|
+
try {
|
|
254
|
+
const text = data.toString();
|
|
255
|
+
if (text === 'ping')
|
|
256
|
+
return;
|
|
257
|
+
const response = JSON.parse(text);
|
|
258
|
+
if (response.id && map.has(response.id)) {
|
|
259
|
+
const pending = map.get(response.id);
|
|
260
|
+
pending.resolve(response.result);
|
|
261
|
+
map.delete(response.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (_e) {
|
|
265
|
+
console.error('Failed to parse message from extension:', data);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
270
|
+
if (request.headers.origin !== 'chrome-extension://bpfjmggaaiigpfahhmpmacfhlemnhhip') {
|
|
271
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
272
|
+
socket.destroy();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
wss.clients.forEach((ws) => ws.terminate());
|
|
276
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
277
|
+
wss.emit('connection', ws, request);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
httpServer.listen(0, () => {
|
|
281
|
+
const port = httpServer.address().port;
|
|
282
|
+
openChromeWithExtension(port);
|
|
283
|
+
});
|
|
284
|
+
const transport = new StdioServerTransport();
|
|
285
|
+
await server.connect(transport);
|
|
286
|
+
process.stdin.on('end', () => {
|
|
287
|
+
try {
|
|
288
|
+
openChromeWithExtension(0);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
console.error('Failed to open Chrome with extension:', err);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
main().catch((error) => {
|
|
296
|
+
console.error('Server error:', error);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kibbutz-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"build": "tsc && chmod 755 build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"kibbutz-mcp": "build/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"build"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
22
|
+
"uuid": "^13.0.0",
|
|
23
|
+
"ws": "^8.18.3",
|
|
24
|
+
"zod": "^4.1.13"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.10.2",
|
|
28
|
+
"@types/ws": "^8.18.1",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|