From 55969cb37f131e61710c68d8b1615c5f0ff74982 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 23:43:06 +0000 Subject: [PATCH 1/3] feat: expose cross-host MCP tools by client identity --- .changeset/host-tool-inventory.md | 5 ++++ src/mcp/grok-bot/tools/codex_send.tsx | 1 + src/mcp/grok-bot/tools/codex_threads.tsx | 1 + src/mcp/grok-bot/tools/codex_wait.tsx | 1 + src/mcp/grok-bot/tools/codex_watch.tsx | 1 + src/mcp/grok-bot/tools/gbot_codex_respond.tsx | 1 + .../grok-bot/tools/gbot_grok_approvals.tsx | 1 + src/mcp/grok-bot/tools/gbot_grok_respond.tsx | 1 + src/mcp/grok-bot/tools/gbot_send.tsx | 1 + src/mcp/grok-bot/tools/gbot_thread.tsx | 1 + src/skills/talk-to-grok-bot/SKILL.md | 8 +++++ test/host-tool-inventory.test.js | 30 +++++++++++++++++++ test/relay-surfaces.test.js | 5 ++-- 13 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .changeset/host-tool-inventory.md create mode 100644 test/host-tool-inventory.test.js diff --git a/.changeset/host-tool-inventory.md b/.changeset/host-tool-inventory.md new file mode 100644 index 0000000..ac8a672 --- /dev/null +++ b/.changeset/host-tool-inventory.md @@ -0,0 +1,5 @@ +--- +"grok-bot-cli": minor +--- + +Expose cross-host MCP messaging tools: hide Codex tools in Codex and Grok tools in truthfully identified Grok Bot clients, while retaining shared bridge controls. diff --git a/src/mcp/grok-bot/tools/codex_send.tsx b/src/mcp/grok-bot/tools/codex_send.tsx index 58d7bbd..288c02e 100644 --- a/src/mcp/grok-bot/tools/codex_send.tsx +++ b/src/mcp/grok-bot/tools/codex_send.tsx @@ -20,6 +20,7 @@ export const inputSchema = sendSchema.extend({ const resultSchema = z.union([plainResultSchema, relayResultSchema]); export default defineTool( { + excludeClients: ['codex'], description: 'Send to Codex. With replyToGrok or bindingId, managed delivery returns the terminal answer to Grok automatically. Otherwise optional wait observes completion and explicit steer requires expectedTurnId. Acceptance is not completion.', title: 'Codex send', diff --git a/src/mcp/grok-bot/tools/codex_threads.tsx b/src/mcp/grok-bot/tools/codex_threads.tsx index a66c0fa..ff4f561 100644 --- a/src/mcp/grok-bot/tools/codex_threads.tsx +++ b/src/mcp/grok-bot/tools/codex_threads.tsx @@ -3,6 +3,7 @@ import { defineTool } from 'agent-bundle/routes'; import { threadsSchema as inputSchema, resultSchema, threadsOperation, resultText } from '../../../core/codex/routes.js'; export { inputSchema }; export default defineTool({ + excludeClients: ['codex'], description: 'Discover a bounded page of Codex daemon threads.', title: 'Codex threads', annotations: { readOnlyHint: true }, render: { maxElapsedMs: 660000 }, inputSchema, resultSchema, diff --git a/src/mcp/grok-bot/tools/codex_wait.tsx b/src/mcp/grok-bot/tools/codex_wait.tsx index 2cd006b..342fc41 100644 --- a/src/mcp/grok-bot/tools/codex_wait.tsx +++ b/src/mcp/grok-bot/tools/codex_wait.tsx @@ -3,6 +3,7 @@ import { defineTool } from 'agent-bundle/routes'; import { waitSchema as inputSchema, resultSchema, observeOperation, resultText } from '../../../core/codex/routes.js'; export { inputSchema }; export default defineTool({ + excludeClients: ['codex'], description: 'Explicit diagnostic observation of one Codex turn; returns execution and final reply without interrupting it.', title: 'Codex wait', annotations: { readOnlyHint: true }, render: { maxElapsedMs: 660000 }, inputSchema, resultSchema, diff --git a/src/mcp/grok-bot/tools/codex_watch.tsx b/src/mcp/grok-bot/tools/codex_watch.tsx index 68ba3b2..d4220d6 100644 --- a/src/mcp/grok-bot/tools/codex_watch.tsx +++ b/src/mcp/grok-bot/tools/codex_watch.tsx @@ -3,6 +3,7 @@ import { defineTool } from 'agent-bundle/routes'; import { watchSchema as inputSchema, resultSchema, observeOperation, resultText } from '../../../core/codex/routes.js'; export { inputSchema }; export default defineTool({ + excludeClients: ['codex'], description: 'Watch bounded Codex thread events for diagnostics without answering approvals.', title: 'Codex watch', annotations: { readOnlyHint: true }, render: { maxElapsedMs: 660000 }, inputSchema, resultSchema, diff --git a/src/mcp/grok-bot/tools/gbot_codex_respond.tsx b/src/mcp/grok-bot/tools/gbot_codex_respond.tsx index f83eb39..25b9b14 100644 --- a/src/mcp/grok-bot/tools/gbot_codex_respond.tsx +++ b/src/mcp/grok-bot/tools/gbot_codex_respond.tsx @@ -8,6 +8,7 @@ import { export { inputSchema }; export default defineTool( { + excludeClients: ['codex'], description: 'Explicit operator response to a current scoped Codex interaction. Supports only one-time accept/decline/cancel or exact question-ID answers. Never auto-approve.', title: 'gbot_codex_respond', diff --git a/src/mcp/grok-bot/tools/gbot_grok_approvals.tsx b/src/mcp/grok-bot/tools/gbot_grok_approvals.tsx index 90d630f..509c295 100644 --- a/src/mcp/grok-bot/tools/gbot_grok_approvals.tsx +++ b/src/mcp/grok-bot/tools/gbot_grok_approvals.tsx @@ -3,6 +3,7 @@ import { defineTool } from 'agent-bundle/routes'; import { listSchema as inputSchema, resultSchema, listOperation } from '../../../core/grok-approval-routes.js'; export { inputSchema }; export default defineTool({ + excludeClients: ['grok bot', 'grokbot', 'grok-bot'], title: 'Pending Grok approvals', description: 'List pending auto-review and local-tool approval cards in the latest 200 entries for a Grok bot. Older or unsupported requests require the owning Grok UI.', annotations: { readOnlyHint: true }, inputSchema, resultSchema, inputJsonSchema: { type: 'object', properties: { target: { type: 'string' } }, required: ['target'], additionalProperties: false }, diff --git a/src/mcp/grok-bot/tools/gbot_grok_respond.tsx b/src/mcp/grok-bot/tools/gbot_grok_respond.tsx index 529f917..9394bb5 100644 --- a/src/mcp/grok-bot/tools/gbot_grok_respond.tsx +++ b/src/mcp/grok-bot/tools/gbot_grok_respond.tsx @@ -3,6 +3,7 @@ import { defineTool } from 'agent-bundle/routes'; import { respondSchema as inputSchema, resultSchema, respondOperation } from '../../../core/grok-approval-routes.js'; export { inputSchema }; export default defineTool({ + excludeClients: ['grok bot', 'grokbot', 'grok-bot'], title: 'Respond to Grok approval', description: 'Only after an explicit user decision: accept one current Grok approval once or decline it. Exact target, entryId and approval requestId required. Never auto-approve or grant persistent permissions. Success acknowledges response delivery, not execution.', annotations: { readOnlyHint: false }, inputSchema, resultSchema, inputJsonSchema: { diff --git a/src/mcp/grok-bot/tools/gbot_send.tsx b/src/mcp/grok-bot/tools/gbot_send.tsx index 676e4b4..106ee36 100644 --- a/src/mcp/grok-bot/tools/gbot_send.tsx +++ b/src/mcp/grok-bot/tools/gbot_send.tsx @@ -8,6 +8,7 @@ import { export { inputSchema }; export default defineTool( { + excludeClients: ['grok bot', 'grokbot', 'grok-bot'], description: 'Send to Grok Bot. Native Codex calls automatically receive replies in their originating thread; send once and continue work. Without a native source, supply codexThreadId or use manual gbot_thread reading.', title: 'Send a message to Grok Bot', diff --git a/src/mcp/grok-bot/tools/gbot_thread.tsx b/src/mcp/grok-bot/tools/gbot_thread.tsx index 68f08ed..98ee247 100644 --- a/src/mcp/grok-bot/tools/gbot_thread.tsx +++ b/src/mcp/grok-bot/tools/gbot_thread.tsx @@ -23,6 +23,7 @@ export const inputSchema = z.object({ export default defineTool( { + excludeClients: ['grok bot', 'grokbot', 'grok-bot'], annotations: { readOnlyHint: true }, description: 'Read a bounded Grok Bot thread tail. Returns a small receipt by default; pass the last cursor as after for an exclusive client-side delta, or full:true to include bounded entry text.', diff --git a/src/skills/talk-to-grok-bot/SKILL.md b/src/skills/talk-to-grok-bot/SKILL.md index 2fc3f6e..ba35c05 100644 --- a/src/skills/talk-to-grok-bot/SKILL.md +++ b/src/skills/talk-to-grok-bot/SKILL.md @@ -109,3 +109,11 @@ Managed Codex return routes reject `expectedTurnId` and legacy `replyTo`/`envelo options before submission; use plain `codex_send` for a caller-selected turn guard. An explicit Grok target supplied with `bindingId` must resolve to the binding's recipient. A mismatch fails instead of selecting one destination silently. + +## Host tool inventory + +Codex MCP clients receive Grok messaging and approval tools; truthfully identified +Grok Bot clients receive Codex messaging and approval tools. Bridge start/status/stop +remain shared. Cursor and unknown clients retain both sets. Filtering uses negotiated +client-name prefixes and does not provide authorization. A Grok runtime identifying +itself as Cursor needs its native MCP identity corrected before this filter applies. diff --git a/test/host-tool-inventory.test.js b/test/host-tool-inventory.test.js new file mode 100644 index 0000000..ffb8661 --- /dev/null +++ b/test/host-tool-inventory.test.js @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import test from 'node:test'; + +const sdk = createRequire(import.meta.resolve('agent-bundle/api')); +const { Client } = await import(sdk.resolve('@modelcontextprotocol/client')); +const { StdioClientTransport } = await import(sdk.resolve('@modelcontextprotocol/client/stdio')); +const config = JSON.parse(await readFile(new URL('../artifact/.mcp.json', import.meta.url), 'utf8')); +const entry = config.mcpServers['grok-bot'].args[0].replace('${CLAUDE_PLUGIN_ROOT}', resolve('artifact')); +const codex = ['codex_send', 'codex_threads', 'codex_wait', 'codex_watch', 'gbot_codex_respond']; +const grok = ['gbot_send', 'gbot_thread', 'gbot_grok_approvals', 'gbot_grok_respond']; +const shared = ['gbot_bridge_start', 'gbot_bridge_status', 'gbot_bridge_stop']; + +test('built MCP artifact exposes the other host tools and shared bridge controls', async () => { + for (const [name, hidden] of [['codex_cli_rs', codex], ['Grok Bot', grok], ['Cursor', []]]) { + const client = new Client({ name, version: '1.0.0' }); + try { + await client.connect(new StdioClientTransport({ command: process.execPath, args: [entry], stderr: 'pipe' })); + const listed = (await client.listTools()).tools.map(tool => tool.name).sort(); + assert.deepEqual(listed, [...codex, ...grok, ...shared].filter(tool => !hidden.includes(tool)).sort()); + for (const tool of hidden) { + await assert.rejects(client.callTool({ name: tool, arguments: {} }), /disabled|not found/i); + } + } finally { + await client.close(); + } + } +}); diff --git a/test/relay-surfaces.test.js b/test/relay-surfaces.test.js index 535fd30..c1c7299 100644 --- a/test/relay-surfaces.test.js +++ b/test/relay-surfaces.test.js @@ -121,7 +121,7 @@ export async function fixture({ active = false, interaction, onGateway } = {}) { export async function mcp( root, env, - name = "codex-mcp-client", + name = "cursor", manifestPath = process.env.RELAY_MCP_MANIFEST ?? "mcp.json", ) { const manifest = JSON.parse(await readFile(join(root, manifestPath), "utf8")); @@ -224,6 +224,7 @@ test( client = await mcp( resolve(process.env.RELAY_ARTIFACT_ROOT ?? "artifact"), f.env, + "codex-mcp-client", ); const names = (await client.rpc("tools/list", {})).result.tools.map( (t) => t.name, @@ -232,9 +233,9 @@ test( "gbot_bridge_start", "gbot_bridge_status", "gbot_bridge_stop", - "gbot_codex_respond", ]) assert.ok(names.includes(name), name); + assert.ok(!names.includes("gbot_codex_respond")); const manual = await client.call("gbot_send", { target: "General", message: "manual", From 688c0bf1150f19838e44cf25ea7564f9da84d9e4 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:01:35 +0000 Subject: [PATCH 2/3] build: pin published host-aware Agent Bundle build --- package-lock.json | 30 +++++++++++++++--------------- package.json | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb6df71..81bea77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "grok-bot-cli", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "grok-bot-cli", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "bin": { "gbot": "dist/bin/gbot.mjs", @@ -14,12 +14,12 @@ "grok-bot": "dist/bin/gbot.mjs" }, "devDependencies": { - "@agent-bundle/runtime": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", + "@agent-bundle/runtime": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@57a9196caf2590ece001799eaeed92613352bc21", "@changesets/cli": "3.0.3", "@rstest/core": "0.11.12", "@types/node": "^24.0.0", "@types/react": "^19.2.18", - "agent-bundle": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", + "agent-bundle": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@57a9196caf2590ece001799eaeed92613352bc21", "react": "19.3.0", "react-dom": "19.3.0", "typescript": "7.0.2", @@ -30,9 +30,9 @@ } }, "node_modules/@agent-bundle/runtime": { - "version": "0.0.0-preview-19ab901", - "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", - "integrity": "sha512-6zpWNTtkIy+yBhqcaWzEdTNqUFW5u3c7GiAOclVhInTopvRe5PZnj1PgWdcaNHKfWHTgFsMhWLYXoogUyC1g+Q==", + "version": "0.0.0-preview-57a9196", + "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@57a9196caf2590ece001799eaeed92613352bc21", + "integrity": "sha512-EVjPcJ1hqH5r4BMQpviqF0DOpG+P2VUvO2eqiWt8Cs86cEEE/RbrTt1ZbQcvcQoStUqBlX99VCnCBTcuckfFug==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -40,7 +40,7 @@ "effect": "4.0.0-rc.112", "flare-redact": "1.6.1", "react-server-dom-rspack": "0.1.0", - "rsc-markdown-stream": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@19ab901221cb80ad83b917c7cdbf5eea4e3f3901" + "rsc-markdown-stream": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@57a9196caf2590ece001799eaeed92613352bc21" }, "engines": { "node": ">=22.19.0" @@ -1825,9 +1825,9 @@ } }, "node_modules/agent-bundle": { - "version": "0.0.0-preview-19ab901", - "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", - "integrity": "sha512-31pzm7CnoFusDdKLLc5Aw1MS72VS/9edLBlxJhkT+9eOuSF6yTWbMODeZVR0oUsBGlNBZ/qa4EQNkXANJUqPow==", + "version": "0.0.0-preview-57a9196", + "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@57a9196caf2590ece001799eaeed92613352bc21", + "integrity": "sha512-pQqVWXS0lO6jBAScZ5nfEix+MnHG73Twp75MUEym+QuRHv9TPsf3y7A+pyfcMAohlu65YMBflRz3EK36ISgGPA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1861,7 +1861,7 @@ "node": ">=22.19.0" }, "peerDependencies": { - "@agent-bundle/runtime": "0.0.0-preview-19ab901", + "@agent-bundle/runtime": "0.0.0-preview-57a9196", "@rstest/core": "^0.11.10", "react": "^19.2.0" }, @@ -2693,9 +2693,9 @@ } }, "node_modules/rsc-markdown-stream": { - "version": "0.0.0-preview-19ab901", - "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", - "integrity": "sha512-Sp/EaS7vsFwjsAhsiufCNLLwuqX+gEsja119b80u7+E38rGIPgGJdg7IJQsTSneBHr4pelAEZ29xJsYEuCM6Dg==", + "version": "0.0.0-preview-57a9196", + "resolved": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@57a9196caf2590ece001799eaeed92613352bc21", + "integrity": "sha512-u8HjqN3YTJl+llLhp/flEXoJ+HITVkXpszyPqRnNSu9mfKFjTF93Oc3F2Sdzw+d4J33xfh5zwNsMBepioA5L1A==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/package.json b/package.json index 218b509..48a0e8a 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,12 @@ "LICENSE" ], "devDependencies": { - "@agent-bundle/runtime": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", + "@agent-bundle/runtime": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@57a9196caf2590ece001799eaeed92613352bc21", "@changesets/cli": "3.0.3", "@rstest/core": "0.11.12", "@types/node": "^24.0.0", "@types/react": "^19.2.18", - "agent-bundle": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@19ab901221cb80ad83b917c7cdbf5eea4e3f3901", + "agent-bundle": "https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@57a9196caf2590ece001799eaeed92613352bc21", "react": "19.3.0", "react-dom": "19.3.0", "typescript": "7.0.2", From 9ddc52558e74920dd36bb2847ae517452aa0b94e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:11:20 +0000 Subject: [PATCH 3/3] test: remove unsolicited reply from EOF ordering fixture --- test/desktop-shim.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/desktop-shim.test.js b/test/desktop-shim.test.js index a844671..4d3ec83 100644 --- a/test/desktop-shim.test.js +++ b/test/desktop-shim.test.js @@ -947,7 +947,6 @@ test("bridge flushes EOF-tail data before the WS Close", { `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`, "latin1", )); - socket.write(wsServerFrame(0x1, Buffer.from('{"jsonrpc":"2.0","id":1,"result":{}}'))); upgraded = true; return; }