openzoo 0.50.54 → 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 +27 -2
- package/lib/mcpbridge.js +350 -0
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
import {
|
|
53
53
|
desktopAction, displayBounds, imageSize, noteShotMeta, resolveAppName,
|
|
54
54
|
} from './grokbotDesktop.js';
|
|
55
|
+
import { startHostMcps, hostMcpTools, hostMcpHas, callHostMcp, hostMcpServers } from './mcpbridge.js';
|
|
55
56
|
|
|
56
57
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
57
58
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
@@ -2174,6 +2175,11 @@ const LOCAL_TOOLS = [
|
|
|
2174
2175
|
|
|
2175
2176
|
export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
|
|
2176
2177
|
|
|
2178
|
+
function liveTools() {
|
|
2179
|
+
const extra = hostMcpTools();
|
|
2180
|
+
return extra.length ? [...LOCAL_TOOLS, ...extra] : LOCAL_TOOLS;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2177
2183
|
async function captureScreenshot(log) {
|
|
2178
2184
|
const dir = path.join(os.tmpdir(), 'openzoo-screens');
|
|
2179
2185
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -2361,6 +2367,9 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2361
2367
|
if (!who) return 'ERROR no such agent';
|
|
2362
2368
|
return JSON.stringify(cancelAgentWakeup(who.id));
|
|
2363
2369
|
}
|
|
2370
|
+
if (hostMcpHas(name)) {
|
|
2371
|
+
return await callHostMcp(name, args);
|
|
2372
|
+
}
|
|
2364
2373
|
return `unknown tool ${name}`;
|
|
2365
2374
|
} catch (e) {
|
|
2366
2375
|
return `ERROR ${e.message}`;
|
|
@@ -2458,10 +2467,25 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2458
2467
|
})(),
|
|
2459
2468
|
`You HAVE local tools on the user's computer via ${via}.`,
|
|
2460
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
|
+
})(),
|
|
2461
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.',
|
|
2462
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.',
|
|
2463
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.',
|
|
2464
|
-
'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.',
|
|
2465
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.',
|
|
2466
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.',
|
|
2467
2491
|
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
@@ -2560,7 +2584,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2560
2584
|
throwIfAborted();
|
|
2561
2585
|
const payload = { model, messages, max_tokens: maxTok };
|
|
2562
2586
|
if (!chatOnly) {
|
|
2563
|
-
payload.tools =
|
|
2587
|
+
payload.tools = liveTools();
|
|
2564
2588
|
payload.tool_choice = 'auto';
|
|
2565
2589
|
}
|
|
2566
2590
|
const { r, data } = await zooPost(payload);
|
|
@@ -3463,6 +3487,7 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
3463
3487
|
// the platform refuses (v6 disabled), fall back to v4 so we still work.
|
|
3464
3488
|
server.on('error', (e) => log(`cursor-backend: server error ${e.code || e.message}`));
|
|
3465
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}`));
|
|
3466
3491
|
try {
|
|
3467
3492
|
server.listen({ port, host: '::', ipv6Only: false }, () => onUp('[::]+127.0.0.1'));
|
|
3468
3493
|
} catch {
|
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",
|