From 7ade05314eb80941f8a0dbec812b138f9a16b2ca Mon Sep 17 00:00:00 2001 From: Eshwar Sundar Date: Sat, 13 Jun 2026 10:33:40 +0530 Subject: [PATCH] Fix run_tool dropping empty arguments for no-arg downstream tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run_tool handler omitted the `arguments` key entirely when the resolved args were empty (Object.keys(...).length > 0 guard). The backend run_tool meta-tool then forwarded a downstream tools/call with no arguments, which serializes to null — and strict MCP servers reject it with "Expected: object, given: null". This broke every no-argument downstream tool (e.g. slack_read_user_profile). Always include `arguments`, defaulting to an explicit {} when empty, matching what the MCP SDK sends for direct tool calls. Extracted the payload assembly into a pure buildRemoteArgs() helper and added regression tests covering empty and populated args. Rebuilt both shipped bundles and aligned all three plugin manifests to 0.2.18 (max prior version + mandatory version bump). --- packages/codex/.codex-plugin/plugin.json | 2 +- packages/codex/dist/index.js | 16 ++++++++----- plugins/glean/.claude-plugin/plugin.json | 2 +- plugins/glean/.cursor-plugin/plugin.json | 2 +- plugins/glean/dist/index.js | 16 ++++++++----- src/tools/run-tool.ts | 27 ++++++++++++++++++---- tests/run-tool.test.ts | 29 +++++++++++++++++++++++- 7 files changed, 73 insertions(+), 21 deletions(-) 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" }, + }); + }); +});