diff --git a/packages/codex/.codex-plugin/plugin.json b/packages/codex/.codex-plugin/plugin.json index adf39c2..8813c2f 100644 --- a/packages/codex/.codex-plugin/plugin.json +++ b/packages/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "glean-experimental-codex", - "version": "0.2.11", + "version": "0.2.18", "description": "Glean Codex plugin for discovering skills and running tools.", "author": { "name": "Glean" diff --git a/packages/codex/dist/index.js b/packages/codex/dist/index.js index 65421a9..e01571e 100644 --- a/packages/codex/dist/index.js +++ b/packages/codex/dist/index.js @@ -31152,14 +31152,18 @@ async function handleRunTool(remoteClient, mcpServer, skillsBaseDir, args) { } throw err; } - const remoteArgs = { + return callRemoteTool( + remoteClient, + "run_tool", + buildRemoteArgs(serverId, toolName, resolvedArgs) + ); +} +function buildRemoteArgs(serverId, toolName, resolvedArgs) { + return { server_id: serverId, - tool_name: toolName + tool_name: toolName, + arguments: resolvedArgs }; - if (Object.keys(resolvedArgs).length > 0) { - remoteArgs.arguments = resolvedArgs; - } - return callRemoteTool(remoteClient, "run_tool", remoteArgs); } // src/url-config-store.ts diff --git a/plugins/glean/.claude-plugin/plugin.json b/plugins/glean/.claude-plugin/plugin.json index c5ca75f..13c1455 100644 --- a/plugins/glean/.claude-plugin/plugin.json +++ b/plugins/glean/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "glean-experimental", - "version": "0.2.17", + "version": "0.2.18", "description": "Glean plugin for discovering skills and running tools.", "author": { "name": "Glean" diff --git a/plugins/glean/.cursor-plugin/plugin.json b/plugins/glean/.cursor-plugin/plugin.json index d6acd17..67543e3 100644 --- a/plugins/glean/.cursor-plugin/plugin.json +++ b/plugins/glean/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "glean-experimental-cursor", "displayName": "Glean Cursor", - "version": "0.2.15", + "version": "0.2.18", "description": "Search and act across your company's apps — Jira, Slack, Salesforce, Google Workspace, and more — without leaving Cursor.", "author": { "name": "Glean" diff --git a/plugins/glean/dist/index.js b/plugins/glean/dist/index.js index 65421a9..e01571e 100644 --- a/plugins/glean/dist/index.js +++ b/plugins/glean/dist/index.js @@ -31152,14 +31152,18 @@ async function handleRunTool(remoteClient, mcpServer, skillsBaseDir, args) { } throw err; } - const remoteArgs = { + return callRemoteTool( + remoteClient, + "run_tool", + buildRemoteArgs(serverId, toolName, resolvedArgs) + ); +} +function buildRemoteArgs(serverId, toolName, resolvedArgs) { + return { server_id: serverId, - tool_name: toolName + tool_name: toolName, + arguments: resolvedArgs }; - if (Object.keys(resolvedArgs).length > 0) { - remoteArgs.arguments = resolvedArgs; - } - return callRemoteTool(remoteClient, "run_tool", remoteArgs); } // src/url-config-store.ts diff --git a/src/tools/run-tool.ts b/src/tools/run-tool.ts index 8fa9187..a73631c 100644 --- a/src/tools/run-tool.ts +++ b/src/tools/run-tool.ts @@ -192,12 +192,29 @@ export async function handleRunTool( throw err; } - const remoteArgs: Record = { + return callRemoteTool( + remoteClient, + "run_tool", + buildRemoteArgs(serverId, toolName, resolvedArgs), + ); +} + +/** + * Assemble the payload for the backend `run_tool` meta-tool. `arguments` is + * ALWAYS included, even when empty: the downstream MCP `tools/call` validates + * `params.arguments` as an object, and an absent field serializes to `null`, + * which strict downstream servers reject ("Expected: object, given: null"). + * Sending an explicit `{}` for no-argument tools matches what the MCP SDK + * does for direct tool calls. + */ +export function buildRemoteArgs( + serverId: string, + toolName: string, + resolvedArgs: Record, +): Record { + return { server_id: serverId, tool_name: toolName, + arguments: resolvedArgs, }; - if (Object.keys(resolvedArgs).length > 0) { - remoteArgs.arguments = resolvedArgs; - } - return callRemoteTool(remoteClient, "run_tool", remoteArgs); } diff --git a/tests/run-tool.test.ts b/tests/run-tool.test.ts index cfc21bd..0d3a09c 100644 --- a/tests/run-tool.test.ts +++ b/tests/run-tool.test.ts @@ -2,7 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; -import { resolveFileArgs, FileArgsError } from "../src/tools/run-tool.js"; +import { + resolveFileArgs, + buildRemoteArgs, + FileArgsError, +} from "../src/tools/run-tool.js"; describe("resolveFileArgs", () => { let tmpDir: string; @@ -132,3 +136,26 @@ describe("resolveFileArgs", () => { ).rejects.toThrow(/conflicts/); }); }); + +describe("buildRemoteArgs", () => { + // Regression: a no-argument downstream call (e.g. slack_read_user_profile) + // must forward `arguments: {}`, not omit the key. An absent field serializes + // to null downstream, which strict MCP servers reject. + it("always includes arguments, even when empty", () => { + expect(buildRemoteArgs("srv", "tool", {})).toEqual({ + server_id: "srv", + tool_name: "tool", + arguments: {}, + }); + }); + + it("forwards populated arguments unchanged", () => { + expect( + buildRemoteArgs("srv", "tool", { response_format: "detailed" }), + ).toEqual({ + server_id: "srv", + tool_name: "tool", + arguments: { response_format: "detailed" }, + }); + }); +});