openzoo 0.50.53 → 0.50.55
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/lib/cursorbackend.js +45 -9
- package/lib/grokbotAccount.js +110 -13
- package/lib/mcpbridge.js +350 -0
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -40,6 +40,7 @@ import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetG
|
|
|
40
40
|
import {
|
|
41
41
|
accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
|
|
42
42
|
readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
|
|
43
|
+
preferNamedAgent, looksLikeAgentId,
|
|
43
44
|
readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
|
|
44
45
|
DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
|
|
45
46
|
} from './grokbotAccount.js';
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
import {
|
|
52
53
|
desktopAction, displayBounds, imageSize, noteShotMeta, resolveAppName,
|
|
53
54
|
} from './grokbotDesktop.js';
|
|
55
|
+
import { startHostMcps, hostMcpTools, hostMcpHas, callHostMcp, hostMcpServers } from './mcpbridge.js';
|
|
54
56
|
|
|
55
57
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
56
58
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
@@ -365,7 +367,12 @@ function useHouseRoster() {
|
|
|
365
367
|
function loadAgents() {
|
|
366
368
|
const house = filterDeleted(readHouseRoster(HOME, activeAccountId) || [], HOME);
|
|
367
369
|
const shaped = house.map(shapeAgent);
|
|
368
|
-
const dirty = shaped.some((a, i) =>
|
|
370
|
+
const dirty = shaped.some((a, i) => {
|
|
371
|
+
const prev = house[i] || {};
|
|
372
|
+
return (a.brief && a.brief !== String(prev.brief || prev.description || ''))
|
|
373
|
+
|| (a.name && a.name !== String(prev.name || prev.title || ''))
|
|
374
|
+
|| looksLikeAgentId(prev.name, prev.id);
|
|
375
|
+
});
|
|
369
376
|
if (dirty && shaped.length) {
|
|
370
377
|
writeJsonFile(houseAgentsPath(HOME), shaped);
|
|
371
378
|
if (activeAccountId) {
|
|
@@ -944,7 +951,7 @@ function bumpAgent(id, { preview = '', notify = true } = {}) {
|
|
|
944
951
|
agentActivity.set(id, a);
|
|
945
952
|
const list = cachedAgentList() || [];
|
|
946
953
|
const idx = list.findIndex((x) => x.id === id);
|
|
947
|
-
const base = idx >= 0 ? list[idx] : { id, name:
|
|
954
|
+
const base = idx >= 0 ? list[idx] : { id, name: 'chat' };
|
|
948
955
|
const agent = shapeAgent(stampActivity({ ...base, updatedAt: now }));
|
|
949
956
|
if (idx >= 0) list[idx] = agent;
|
|
950
957
|
else list.unshift(agent);
|
|
@@ -1533,12 +1540,17 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1533
1540
|
}
|
|
1534
1541
|
function mergeAgentLists(remote) {
|
|
1535
1542
|
const local = cachedAgentList() || [];
|
|
1536
|
-
const seen = new
|
|
1543
|
+
const seen = new Map();
|
|
1537
1544
|
const out = [];
|
|
1538
1545
|
for (const a of [...local, ...(Array.isArray(remote) ? remote : [])]) {
|
|
1539
|
-
if (!a?.id
|
|
1540
|
-
seen.
|
|
1541
|
-
|
|
1546
|
+
if (!a?.id) continue;
|
|
1547
|
+
const idx = seen.get(a.id);
|
|
1548
|
+
if (idx == null) {
|
|
1549
|
+
seen.set(a.id, out.length);
|
|
1550
|
+
out.push(stampActivity(a));
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
out[idx] = stampActivity(preferNamedAgent(out[idx], a));
|
|
1542
1554
|
}
|
|
1543
1555
|
return sortAgentsByActivity(filterDeleted(out, HOME));
|
|
1544
1556
|
}
|
|
@@ -2163,6 +2175,11 @@ const LOCAL_TOOLS = [
|
|
|
2163
2175
|
|
|
2164
2176
|
export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
|
|
2165
2177
|
|
|
2178
|
+
function liveTools() {
|
|
2179
|
+
const extra = hostMcpTools();
|
|
2180
|
+
return extra.length ? [...LOCAL_TOOLS, ...extra] : LOCAL_TOOLS;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2166
2183
|
async function captureScreenshot(log) {
|
|
2167
2184
|
const dir = path.join(os.tmpdir(), 'openzoo-screens');
|
|
2168
2185
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -2350,6 +2367,9 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2350
2367
|
if (!who) return 'ERROR no such agent';
|
|
2351
2368
|
return JSON.stringify(cancelAgentWakeup(who.id));
|
|
2352
2369
|
}
|
|
2370
|
+
if (hostMcpHas(name)) {
|
|
2371
|
+
return await callHostMcp(name, args);
|
|
2372
|
+
}
|
|
2353
2373
|
return `unknown tool ${name}`;
|
|
2354
2374
|
} catch (e) {
|
|
2355
2375
|
return `ERROR ${e.message}`;
|
|
@@ -2447,10 +2467,25 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2447
2467
|
})(),
|
|
2448
2468
|
`You HAVE local tools on the user's computer via ${via}.`,
|
|
2449
2469
|
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent, schedule_wakeup, cancel_wakeup.',
|
|
2470
|
+
(() => {
|
|
2471
|
+
const extra = hostMcpTools();
|
|
2472
|
+
const names = extra.map((t) => t.function.name);
|
|
2473
|
+
const servers = hostMcpServers();
|
|
2474
|
+
if (!names.length) {
|
|
2475
|
+
return 'Host MCP servers (Claude/Grok chrome-devtools, brave, …) are connecting. When chrome-devtools__* tools appear, use them for web pages.';
|
|
2476
|
+
}
|
|
2477
|
+
const chrome = names.filter((n) => /chrome|devtools|browser/i.test(n));
|
|
2478
|
+
return [
|
|
2479
|
+
`Host MCP tools from the user's local Claude/Grok config are attached (${servers.join(', ') || 'mcp'}): ${names.slice(0, 36).join(', ')}${names.length > 36 ? '…' : ''}.`,
|
|
2480
|
+
chrome.length
|
|
2481
|
+
? 'For any web page or HTML form, use chrome-devtools tools (navigate_page / take_snapshot / fill / click). Do NOT use osascript, Quartz, Python Foundation, or AppleScript to read Brave. screenshot/click/type_text are for native Mac UI only.'
|
|
2482
|
+
: 'Use matching MCP tools instead of inventing shell one-liners.',
|
|
2483
|
+
].join(' ');
|
|
2484
|
+
})(),
|
|
2450
2485
|
'create_agent mints a sidebar bot. ALWAYS pass brief so they keep the job across restart. set_brief updates it. list_agents + message_agent talk to other bots (one hop). Do not tell the human to copy-paste between canvases.',
|
|
2451
2486
|
'NEVER STOP / cron / keep working between human messages: call schedule_wakeup every="5m". That is a host timer. There is no crontab. Do not spawn more bots for persistence. Do not re-read SITREP-NOW.md or STANDING-ORDERS.md every turn — do the next file or click. exec sysctl/uptime is not the job. If the tray already has named workers, answer "no" to spawning more.',
|
|
2452
2487
|
'You CAN click the Mac and fill/submit browser forms. That is required when the user asks. Do not write a markdown briefing instead of clicking. Do not tell the user to click.',
|
|
2453
|
-
'Form loop:
|
|
2488
|
+
'Form loop: if chrome-devtools MCP tools exist, navigate_page + take_snapshot + fill the fields + click the submit button. Else fallback: focus_app Brave Browser → screenshot → click the field → type_text → screenshot → click Submit.',
|
|
2454
2489
|
'screenshot captures the display and attaches the image. For any on-screen form, dashboard, or click target, screenshot first this turn. Do not guess at UI you have not seen.',
|
|
2455
2490
|
'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
|
|
2456
2491
|
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
@@ -2549,7 +2584,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2549
2584
|
throwIfAborted();
|
|
2550
2585
|
const payload = { model, messages, max_tokens: maxTok };
|
|
2551
2586
|
if (!chatOnly) {
|
|
2552
|
-
payload.tools =
|
|
2587
|
+
payload.tools = liveTools();
|
|
2553
2588
|
payload.tool_choice = 'auto';
|
|
2554
2589
|
}
|
|
2555
2590
|
const { r, data } = await zooPost(payload);
|
|
@@ -3113,7 +3148,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3113
3148
|
setHostSettings: { ok: true },
|
|
3114
3149
|
setBoxSecrets: { ok: true },
|
|
3115
3150
|
listAgents: rosterForEvent(cachedAgentList()
|
|
3116
|
-
|| [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name:
|
|
3151
|
+
|| [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: 'chat', status: 'ready' })), agentActivity),
|
|
3117
3152
|
countAgents: (cachedAgentList() || [...new Set([...transcripts.keys(), ...tailedAgents])]).length,
|
|
3118
3153
|
searchAgents: rosterForEvent(cachedAgentList() || [], agentActivity),
|
|
3119
3154
|
getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
|
|
@@ -3452,6 +3487,7 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
3452
3487
|
// the platform refuses (v6 disabled), fall back to v4 so we still work.
|
|
3453
3488
|
server.on('error', (e) => log(`cursor-backend: server error ${e.code || e.message}`));
|
|
3454
3489
|
const onUp = (what) => log(`cursor-backend: listening on ${what}:${port} as ${CURSOR_HOSTS[0]}`);
|
|
3490
|
+
startHostMcps({ log }).catch((e) => log(`cursor-backend: mcp start ${e.message}`));
|
|
3455
3491
|
try {
|
|
3456
3492
|
server.listen({ port, host: '::', ipv6Only: false }, () => onUp('[::]+127.0.0.1'));
|
|
3457
3493
|
} catch {
|
package/lib/grokbotAccount.js
CHANGED
|
@@ -41,16 +41,21 @@ function readJsonFile(p) {
|
|
|
41
41
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** First-seen id wins.
|
|
44
|
+
/** First-seen id wins. A later pile can only *upgrade* a UUID name/stub brief. */
|
|
45
45
|
export function mergeAgentRecords(piles) {
|
|
46
|
-
const seen = new
|
|
46
|
+
const seen = new Map();
|
|
47
47
|
const out = [];
|
|
48
48
|
for (const pile of piles) {
|
|
49
49
|
if (!Array.isArray(pile)) continue;
|
|
50
50
|
for (const a of pile) {
|
|
51
|
-
if (!a?.id
|
|
52
|
-
seen.
|
|
53
|
-
|
|
51
|
+
if (!a?.id) continue;
|
|
52
|
+
const idx = seen.get(a.id);
|
|
53
|
+
if (idx == null) {
|
|
54
|
+
seen.set(a.id, out.length);
|
|
55
|
+
out.push(a);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out[idx] = preferNamedAgent(out[idx], a);
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
return out;
|
|
@@ -111,10 +116,94 @@ function activityTs(agent, activity) {
|
|
|
111
116
|
* nulls, then clears the whole persisted tray. That is how a group vanished
|
|
112
117
|
* after the first send: bumpAgent wrote a partial row, restore returned null.
|
|
113
118
|
*/
|
|
119
|
+
const AGENT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
120
|
+
|
|
121
|
+
/** bumpAgent used `{id, name:id}` and 1340 listAgents often echoes that — sidebar then paints UUIDs. */
|
|
122
|
+
export function looksLikeAgentId(s, id) {
|
|
123
|
+
const n = String(s || '').trim();
|
|
124
|
+
if (!n) return true;
|
|
125
|
+
if (id && n === String(id)) return true;
|
|
126
|
+
return AGENT_UUID_RE.test(n);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isStubBrief(brief, id) {
|
|
130
|
+
const b = String(brief || '').trim();
|
|
131
|
+
if (!b) return true;
|
|
132
|
+
if (id && b.startsWith(`You are ${id}. Your job is ${id}`)) return true;
|
|
133
|
+
return /^You are [0-9a-f-]{36}\. Your job is [0-9a-f-]{36}/i.test(b);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Pull a human name out of a standing brief: "You are 6 · Content Studio…" / "Job: Product Simplification". */
|
|
137
|
+
export function nameFromBrief(brief, id) {
|
|
138
|
+
const b = String(brief || '').replace(/^\[brief\]\s*/i, '').trim();
|
|
139
|
+
if (!b) return '';
|
|
140
|
+
const job = b.match(/\bJob:\s*([^\n.]+)/i);
|
|
141
|
+
if (job) {
|
|
142
|
+
const n = job[1].trim();
|
|
143
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
144
|
+
}
|
|
145
|
+
const numbered = b.match(/^You are\s+(\d+\s*[·.•.\-—–]+\s*[^\n.]+)/i);
|
|
146
|
+
if (numbered) {
|
|
147
|
+
const n = numbered[1].replace(/\s+for\s+.*$/i, '').trim();
|
|
148
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
149
|
+
}
|
|
150
|
+
const you = b.match(/^You are\s+(.+?)(?:\.\s|$)/i);
|
|
151
|
+
if (you) {
|
|
152
|
+
const n = you[1].replace(/\s+for\s+Stacc(?:'s)?(?:\s+LLC)?$/i, '').trim();
|
|
153
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function nameQuality(a) {
|
|
159
|
+
const n = String(a?.name || a?.title || '').trim();
|
|
160
|
+
if (!n || looksLikeAgentId(n, a?.id)) return 0;
|
|
161
|
+
if (/^(chat|group)$/i.test(n)) return 1;
|
|
162
|
+
if (/^new bot$/i.test(n)) return 2;
|
|
163
|
+
return 3;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Keep the record that still has a human name / real brief. */
|
|
167
|
+
export function preferNamedAgent(keep, incoming) {
|
|
168
|
+
if (!keep) return incoming;
|
|
169
|
+
if (!incoming) return keep;
|
|
170
|
+
const kq = nameQuality(keep);
|
|
171
|
+
const iq = nameQuality(incoming);
|
|
172
|
+
const keepBrief = String(keep.brief || keep.instructions || '').trim();
|
|
173
|
+
const inBrief = String(incoming.brief || incoming.instructions || '').trim();
|
|
174
|
+
const keepStub = isStubBrief(keepBrief, keep.id);
|
|
175
|
+
const inStub = isStubBrief(inBrief, incoming.id);
|
|
176
|
+
if (iq <= kq && !(keepStub && !inStub)) return keep;
|
|
177
|
+
const nameSrc = iq > kq ? incoming : keep;
|
|
178
|
+
const briefSrc = (!inStub && keepStub) ? incoming : keep;
|
|
179
|
+
return {
|
|
180
|
+
...keep,
|
|
181
|
+
...incoming,
|
|
182
|
+
name: nameSrc.name || nameSrc.title,
|
|
183
|
+
title: nameSrc.title || nameSrc.name,
|
|
184
|
+
brief: briefSrc.brief || briefSrc.instructions || keepBrief || inBrief,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function displayName(raw = {}) {
|
|
189
|
+
const a = raw && typeof raw === 'object' ? raw : {};
|
|
190
|
+
const id = String(a.id || '');
|
|
191
|
+
const isGroup = a.isGroup === true
|
|
192
|
+
|| (Array.isArray(a.memberIds) && a.memberIds.length > 0)
|
|
193
|
+
|| (Array.isArray(a.memberAgentIds) && a.memberAgentIds.length > 0);
|
|
194
|
+
for (const cand of [a.name, a.title]) {
|
|
195
|
+
const n = String(cand || '').trim();
|
|
196
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
197
|
+
}
|
|
198
|
+
const fromBrief = nameFromBrief(a.brief || a.instructions || a.description, id);
|
|
199
|
+
if (fromBrief) return fromBrief;
|
|
200
|
+
return isGroup ? 'group' : 'chat';
|
|
201
|
+
}
|
|
202
|
+
|
|
114
203
|
/** Standing job from a sidebar name like "6 · Content Studio" or "Bot 1 — Marketing (X)". */
|
|
115
204
|
export function briefFromName(name) {
|
|
116
205
|
const n = String(name || '').trim();
|
|
117
|
-
if (!n || /^(new bot|chat|group)$/i.test(n)) return '';
|
|
206
|
+
if (!n || /^(new bot|chat|group)$/i.test(n) || looksLikeAgentId(n)) return '';
|
|
118
207
|
const role = n.replace(/^\d+\s*[·.•.\-—–]+\s*/, '').trim() || n;
|
|
119
208
|
if (role.length < 2) return '';
|
|
120
209
|
return `You are ${n}. Your job is ${role}. Do that job. Do not ask the human to re-brief you. Coordinate with list_agents and message_agent.`;
|
|
@@ -123,13 +212,21 @@ export function briefFromName(name) {
|
|
|
123
212
|
/** Standing job text. shapeAgent used to drop this, so every restart was amnesia. */
|
|
124
213
|
export function agentBrief(raw = {}) {
|
|
125
214
|
const a = raw && typeof raw === 'object' ? raw : {};
|
|
215
|
+
const id = String(a.id || '');
|
|
126
216
|
for (const k of ['brief', 'instructions', 'customInstructions', 'systemPrompt']) {
|
|
127
217
|
const v = a[k];
|
|
128
|
-
if (typeof v === 'string' && v.trim()
|
|
218
|
+
if (typeof v === 'string' && v.trim() && !isStubBrief(v, id)) {
|
|
219
|
+
const name = displayName(a);
|
|
220
|
+
const text = v.trim();
|
|
221
|
+
if (id && name && !looksLikeAgentId(name, id) && text.startsWith(`You are ${id}`)) {
|
|
222
|
+
return (`You are ${name}` + text.slice(`You are ${id}`.length)).slice(0, 8000);
|
|
223
|
+
}
|
|
224
|
+
return text.slice(0, 8000);
|
|
225
|
+
}
|
|
129
226
|
}
|
|
130
227
|
const d = String(a.description || '').trim();
|
|
131
|
-
if (d) return d.slice(0, 8000);
|
|
132
|
-
return briefFromName(a
|
|
228
|
+
if (d && !isStubBrief(d, id)) return d.slice(0, 8000);
|
|
229
|
+
return briefFromName(displayName(a)).slice(0, 8000);
|
|
133
230
|
}
|
|
134
231
|
|
|
135
232
|
export function shapeAgent(raw = {}) {
|
|
@@ -139,14 +236,14 @@ export function shapeAgent(raw = {}) {
|
|
|
139
236
|
? a.memberIds.map((x) => String(x)).filter(Boolean)
|
|
140
237
|
: (Array.isArray(a.memberAgentIds) ? a.memberAgentIds.map((x) => String(x)).filter(Boolean) : []);
|
|
141
238
|
const isGroup = a.isGroup === true || memberIds.length > 0;
|
|
142
|
-
const name =
|
|
143
|
-
const brief = agentBrief(a);
|
|
239
|
+
const name = displayName({ ...a, isGroup, memberIds });
|
|
240
|
+
const brief = agentBrief({ ...a, name, title: a.title || name });
|
|
144
241
|
return {
|
|
145
242
|
id,
|
|
146
243
|
name,
|
|
147
244
|
brief,
|
|
148
|
-
description: String(a.description || brief || ''),
|
|
149
|
-
title: String(a.title || name),
|
|
245
|
+
description: String((a.description && !looksLikeAgentId(a.description, id) && a.description) || brief || ''),
|
|
246
|
+
title: String((!looksLikeAgentId(a.title, id) && a.title) || name),
|
|
150
247
|
origin: String(a.origin || 'user'),
|
|
151
248
|
path: String(a.path || (id ? `/local/${id}` : '/local')),
|
|
152
249
|
createdAt: Number(a.createdAt) || Date.now(),
|
package/lib/mcpbridge.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load the operator's local MCP servers (Grok / Claude / Cursor) and expose
|
|
3
|
+
* them as OpenAI function tools for Grok Bot zoo turns.
|
|
4
|
+
*
|
|
5
|
+
* chrome-devtools is always attached if missing — that is Claude-in-Chrome
|
|
6
|
+
* for this hijack: navigate / snapshot / fill the live page, not osascript.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
13
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
14
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
15
|
+
|
|
16
|
+
const SKIP = /^(openzoo|openzoo-mcp)$/i;
|
|
17
|
+
const NAME_RE = /[^a-zA-Z0-9_-]/g;
|
|
18
|
+
|
|
19
|
+
const registry = new Map();
|
|
20
|
+
let openaiTools = [];
|
|
21
|
+
let started = null;
|
|
22
|
+
let serversUp = [];
|
|
23
|
+
|
|
24
|
+
export function hostMcpTools() {
|
|
25
|
+
return openaiTools;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hostMcpHas(name) {
|
|
29
|
+
return registry.has(String(name || ''));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hostMcpServers() {
|
|
33
|
+
return [...serversUp];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toolOpenaiName(server, toolName) {
|
|
37
|
+
return `${String(server || 'mcp').replace(NAME_RE, '_') }__${String(toolName || 'tool').replace(NAME_RE, '_')}`.slice(0, 64);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function flattenMcpResult(r) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
const content = Array.isArray(r?.content) ? r.content : [];
|
|
43
|
+
for (const c of content) {
|
|
44
|
+
if (!c || typeof c !== 'object') continue;
|
|
45
|
+
if (c.type === 'text' && c.text) parts.push(String(c.text));
|
|
46
|
+
else if (c.type === 'image') parts.push(`[image ${c.mimeType || 'png'} ${(c.data || '').length}b]`);
|
|
47
|
+
else parts.push(JSON.stringify(c));
|
|
48
|
+
}
|
|
49
|
+
const body = parts.join('\n').trim() || JSON.stringify(r ?? {});
|
|
50
|
+
const out = r?.isError ? `ERROR ${body}` : body;
|
|
51
|
+
return out.slice(0, 20_000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripJsonComments(s) {
|
|
55
|
+
return String(s || '').replace(/^\s*\/\/.*$/gm, '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readJsonFile(p) {
|
|
59
|
+
try { return JSON.parse(stripJsonComments(fs.readFileSync(p, 'utf8'))); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Enough TOML for ~/.grok/config.toml [mcp_servers.*] tables. */
|
|
63
|
+
export function parseTomlMcpServers(text) {
|
|
64
|
+
const servers = {};
|
|
65
|
+
let cur = null;
|
|
66
|
+
let nested = null;
|
|
67
|
+
let arrayKey = null;
|
|
68
|
+
let arrayBuf = [];
|
|
69
|
+
const ensure = (name) => {
|
|
70
|
+
if (!servers[name]) servers[name] = { name };
|
|
71
|
+
return servers[name];
|
|
72
|
+
};
|
|
73
|
+
const flushArray = () => {
|
|
74
|
+
if (!cur || !arrayKey) return;
|
|
75
|
+
const vals = arrayBuf.map((x) => unquote(x)).filter((x) => x !== '');
|
|
76
|
+
cur[arrayKey] = vals;
|
|
77
|
+
arrayKey = null;
|
|
78
|
+
arrayBuf = [];
|
|
79
|
+
};
|
|
80
|
+
const unquote = (raw) => {
|
|
81
|
+
let s = String(raw || '').trim().replace(/,$/, '').trim();
|
|
82
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
83
|
+
s = s.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
if (s === 'true') return true;
|
|
86
|
+
if (s === 'false') return false;
|
|
87
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
|
|
88
|
+
return s;
|
|
89
|
+
};
|
|
90
|
+
for (const rawLine of String(text || '').split(/\n/)) {
|
|
91
|
+
const line = rawLine.trim();
|
|
92
|
+
if (!line || line.startsWith('#')) continue;
|
|
93
|
+
if (arrayKey) {
|
|
94
|
+
if (line.startsWith(']')) {
|
|
95
|
+
flushArray();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
arrayBuf.push(line.replace(/,$/, ''));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const nestedSec = line.match(/^\[mcp_servers\.([^\]]+?)\.([a-zA-Z0-9_-]+)\]$/);
|
|
102
|
+
if (nestedSec) {
|
|
103
|
+
cur = ensure(nestedSec[1]);
|
|
104
|
+
nested = nestedSec[2];
|
|
105
|
+
if (!cur[nested] || typeof cur[nested] !== 'object' || Array.isArray(cur[nested])) cur[nested] = {};
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const sec = line.match(/^\[mcp_servers\.([^\]]+)\]$/);
|
|
109
|
+
if (sec) {
|
|
110
|
+
cur = ensure(sec[1]);
|
|
111
|
+
nested = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (line.startsWith('[')) {
|
|
115
|
+
cur = null;
|
|
116
|
+
nested = null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!cur) continue;
|
|
120
|
+
const kv = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.*)$/);
|
|
121
|
+
if (!kv) continue;
|
|
122
|
+
const key = kv[1];
|
|
123
|
+
const rest = kv[2].trim();
|
|
124
|
+
if (rest === '[' || rest.startsWith('[')) {
|
|
125
|
+
arrayKey = key;
|
|
126
|
+
arrayBuf = [];
|
|
127
|
+
const inner = rest.replace(/^\[/, '').replace(/\]\s*$/, '').trim();
|
|
128
|
+
if (rest.includes(']') && rest !== '[') {
|
|
129
|
+
if (inner) arrayBuf = inner.split(',').map((x) => x.trim());
|
|
130
|
+
flushArray();
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const val = unquote(rest);
|
|
135
|
+
if (nested) cur[nested][key] = val;
|
|
136
|
+
else cur[key] = val;
|
|
137
|
+
}
|
|
138
|
+
flushArray();
|
|
139
|
+
return Object.values(servers);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function fromJsonMcpServers(obj, fallbackName) {
|
|
143
|
+
if (!obj || typeof obj !== 'object') return [];
|
|
144
|
+
const src = obj.mcpServers && typeof obj.mcpServers === 'object' ? obj.mcpServers : obj;
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const [name, raw] of Object.entries(src)) {
|
|
147
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
148
|
+
out.push({
|
|
149
|
+
name: String(name || fallbackName || 'mcp'),
|
|
150
|
+
command: raw.command,
|
|
151
|
+
args: Array.isArray(raw.args) ? raw.args.map(String) : (Array.isArray(raw.command) ? raw.command.slice(1) : undefined),
|
|
152
|
+
url: raw.url,
|
|
153
|
+
type: raw.type,
|
|
154
|
+
env: raw.env && typeof raw.env === 'object' ? raw.env : undefined,
|
|
155
|
+
headers: raw.headers && typeof raw.headers === 'object' ? raw.headers : undefined,
|
|
156
|
+
enabled: raw.enabled !== false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function shapeServer(raw) {
|
|
163
|
+
const name = String(raw?.name || '').trim();
|
|
164
|
+
if (!name || SKIP.test(name) || raw?.enabled === false) return null;
|
|
165
|
+
const command = Array.isArray(raw.command) ? raw.command[0] : raw.command;
|
|
166
|
+
const args = Array.isArray(raw.args)
|
|
167
|
+
? raw.args.map(String)
|
|
168
|
+
: (Array.isArray(raw.command) ? raw.command.slice(1).map(String) : []);
|
|
169
|
+
const url = raw.url ? String(raw.url) : '';
|
|
170
|
+
if (!url && !command) return null;
|
|
171
|
+
return {
|
|
172
|
+
name,
|
|
173
|
+
command: command ? String(command) : '',
|
|
174
|
+
args,
|
|
175
|
+
url,
|
|
176
|
+
env: raw.env && typeof raw.env === 'object' ? Object.fromEntries(Object.entries(raw.env).map(([k, v]) => [k, String(v)])) : undefined,
|
|
177
|
+
headers: raw.headers && typeof raw.headers === 'object' ? raw.headers : undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function loadHostMcpConfigs(home = os.homedir()) {
|
|
182
|
+
const piles = [];
|
|
183
|
+
const grokToml = path.join(home, '.grok', 'config.toml');
|
|
184
|
+
try { piles.push(...parseTomlMcpServers(fs.readFileSync(grokToml, 'utf8'))); } catch { /* */ }
|
|
185
|
+
for (const p of [
|
|
186
|
+
path.join(home, '.claude', 'mcp.json'),
|
|
187
|
+
path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
188
|
+
path.join(home, '.cursor', 'mcp.json'),
|
|
189
|
+
]) {
|
|
190
|
+
const j = readJsonFile(p);
|
|
191
|
+
if (j) piles.push(...fromJsonMcpServers(j));
|
|
192
|
+
}
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const raw of piles) {
|
|
196
|
+
const s = shapeServer(raw);
|
|
197
|
+
if (!s || seen.has(s.name)) continue;
|
|
198
|
+
seen.add(s.name);
|
|
199
|
+
out.push(s);
|
|
200
|
+
}
|
|
201
|
+
if (![...seen].some((n) => /chrome|devtools|browser/i.test(n))) {
|
|
202
|
+
out.push({
|
|
203
|
+
name: 'chrome-devtools',
|
|
204
|
+
command: 'npx',
|
|
205
|
+
args: ['-y', 'chrome-devtools-mcp@latest'],
|
|
206
|
+
url: '',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function mcpToOpenAiTool(server, tool) {
|
|
213
|
+
const schema = tool?.inputSchema && typeof tool.inputSchema === 'object'
|
|
214
|
+
? { ...tool.inputSchema }
|
|
215
|
+
: { type: 'object', properties: {} };
|
|
216
|
+
if (!schema.type) schema.type = 'object';
|
|
217
|
+
if (!schema.properties) schema.properties = {};
|
|
218
|
+
delete schema.$schema;
|
|
219
|
+
return {
|
|
220
|
+
type: 'function',
|
|
221
|
+
function: {
|
|
222
|
+
name: toolOpenaiName(server, tool.name),
|
|
223
|
+
description: `[MCP ${server}] ${String(tool.description || tool.name || '').slice(0, 900)}`,
|
|
224
|
+
parameters: schema,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function portOpen(port, host = '127.0.0.1', ms = 150) {
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
const sock = net.connect({ port, host });
|
|
232
|
+
const done = (ok) => {
|
|
233
|
+
try { sock.destroy(); } catch { /* */ }
|
|
234
|
+
resolve(ok);
|
|
235
|
+
};
|
|
236
|
+
sock.setTimeout(ms);
|
|
237
|
+
sock.on('connect', () => done(true));
|
|
238
|
+
sock.on('timeout', () => done(false));
|
|
239
|
+
sock.on('error', () => done(false));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function chromeArgs(baseArgs) {
|
|
244
|
+
const args = [...(baseArgs || ['-y', 'chrome-devtools-mcp@latest'])];
|
|
245
|
+
if (args.some((a) => String(a).includes('browserUrl') || String(a) === '--browserUrl')) return args;
|
|
246
|
+
for (const port of [9222, 9333]) {
|
|
247
|
+
if (await portOpen(port)) {
|
|
248
|
+
args.push('--browserUrl', `http://127.0.0.1:${port}`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return args;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function connectOne(cfg, log) {
|
|
256
|
+
const client = new Client({ name: 'openzoo-grokbot', version: '0.50.55' });
|
|
257
|
+
let transport;
|
|
258
|
+
if (cfg.url) {
|
|
259
|
+
const headers = {};
|
|
260
|
+
for (const [k, v] of Object.entries(cfg.headers || {})) headers[k] = String(v);
|
|
261
|
+
transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
262
|
+
requestInit: { headers },
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
let args = cfg.args || [];
|
|
266
|
+
if (cfg.name === 'chrome-devtools') args = await chromeArgs(args);
|
|
267
|
+
const env = { ...getDefaultEnvironment(), PATH: process.env.PATH || '', ...(cfg.env || {}) };
|
|
268
|
+
if (process.env.NVM_DIR) env.NVM_DIR = process.env.NVM_DIR;
|
|
269
|
+
transport = new StdioClientTransport({
|
|
270
|
+
command: cfg.command,
|
|
271
|
+
args,
|
|
272
|
+
env,
|
|
273
|
+
stderr: 'pipe',
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
transport.stderr?.on?.('data', (buf) => {
|
|
277
|
+
const line = String(buf).trim().split('\n')[0];
|
|
278
|
+
if (line) log?.(`cursor-backend: mcp ${cfg.name} ${line.slice(0, 160)}`);
|
|
279
|
+
});
|
|
280
|
+
} catch { /* */ }
|
|
281
|
+
}
|
|
282
|
+
const ms = cfg.name === 'chrome-devtools' ? 90_000 : 45_000;
|
|
283
|
+
await Promise.race([
|
|
284
|
+
client.connect(transport),
|
|
285
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error(`timeout ${ms}ms`)), ms)),
|
|
286
|
+
]);
|
|
287
|
+
const listed = await client.listTools();
|
|
288
|
+
const tools = Array.isArray(listed?.tools) ? listed.tools : [];
|
|
289
|
+
return { client, tools };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function rebuildOpenai() {
|
|
293
|
+
openaiTools = [];
|
|
294
|
+
for (const [name, rec] of registry) {
|
|
295
|
+
openaiTools.push(mcpToOpenAiTool(rec.server, { name: rec.tool, description: rec.description, inputSchema: rec.schema }));
|
|
296
|
+
void name;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function callHostMcp(name, args) {
|
|
301
|
+
const rec = registry.get(String(name || ''));
|
|
302
|
+
if (!rec) throw new Error(`no mcp tool ${name}`);
|
|
303
|
+
const r = await rec.client.callTool({ name: rec.tool, arguments: args && typeof args === 'object' ? args : {} });
|
|
304
|
+
return flattenMcpResult(r);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function resetHostMcpForTests() {
|
|
308
|
+
registry.clear();
|
|
309
|
+
openaiTools = [];
|
|
310
|
+
serversUp = [];
|
|
311
|
+
started = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function startHostMcps({ log = () => {}, home = os.homedir() } = {}) {
|
|
315
|
+
if (started) return started;
|
|
316
|
+
if (process.env.OZ_GROKBOT_MCP === '0') {
|
|
317
|
+
started = { tools: [], servers: [] };
|
|
318
|
+
return started;
|
|
319
|
+
}
|
|
320
|
+
started = (async () => {
|
|
321
|
+
const configs = loadHostMcpConfigs(home);
|
|
322
|
+
log(`cursor-backend: mcp loading n=${configs.length} ${configs.map((c) => c.name).join(',')}`);
|
|
323
|
+
const results = await Promise.allSettled(configs.map((cfg) => connectOne(cfg, log)));
|
|
324
|
+
for (let i = 0; i < results.length; i++) {
|
|
325
|
+
const cfg = configs[i];
|
|
326
|
+
const r = results[i];
|
|
327
|
+
if (r.status !== 'fulfilled') {
|
|
328
|
+
log(`cursor-backend: mcp ${cfg.name} FAIL ${r.reason?.message || r.reason}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
serversUp.push(cfg.name);
|
|
332
|
+
for (const tool of r.value.tools) {
|
|
333
|
+
const openaiName = toolOpenaiName(cfg.name, tool.name);
|
|
334
|
+
if (registry.has(openaiName)) continue;
|
|
335
|
+
registry.set(openaiName, {
|
|
336
|
+
client: r.value.client,
|
|
337
|
+
server: cfg.name,
|
|
338
|
+
tool: tool.name,
|
|
339
|
+
description: tool.description || tool.name,
|
|
340
|
+
schema: tool.inputSchema,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
log(`cursor-backend: mcp ${cfg.name} tools=${r.value.tools.length}`);
|
|
344
|
+
}
|
|
345
|
+
rebuildOpenai();
|
|
346
|
+
log(`cursor-backend: mcp ready servers=${serversUp.join(',') || 'none'} tools=${openaiTools.length}`);
|
|
347
|
+
return { tools: openaiTools, servers: serversUp };
|
|
348
|
+
})();
|
|
349
|
+
return started;
|
|
350
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.55",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|