From b22df408e938f548c8c92a181443f218e7061fd7 Mon Sep 17 00:00:00 2001 From: Eshwar Sundar Date: Sat, 20 Jun 2026 17:31:45 +0530 Subject: [PATCH 1/2] Clarify Glean is already authenticated on connector AUTH_REQUIRED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When run_tool returns a downstream connector AUTH_REQUIRED result (isError + authUrls JSON envelope), append a clarification text block: Glean itself is already authenticated and this is NOT [SETUP_REQUIRED], so the model must not call `setup`. The gateway's original content (including the link) is left untouched — no dialog, no link reformatting; only the suffix is added. Bump plugin manifests to 0.2.27. --- plugins/glean/.claude-plugin/plugin.json | 2 +- plugins/glean/.codex-plugin/plugin.json | 2 +- plugins/glean/.cursor-plugin/plugin.json | 2 +- plugins/glean/dist/index.js | 30 +++++++-- src/tools/run-tool.ts | 53 ++++++++++++++- tests/run-tool.test.ts | 85 ++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 8 deletions(-) diff --git a/plugins/glean/.claude-plugin/plugin.json b/plugins/glean/.claude-plugin/plugin.json index 66a02ad..5b5ffa1 100644 --- a/plugins/glean/.claude-plugin/plugin.json +++ b/plugins/glean/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "glean-vnext", - "version": "0.2.26", + "version": "0.2.27", "description": "Glean plugin for discovering skills and running tools.", "author": { "name": "Glean" diff --git a/plugins/glean/.codex-plugin/plugin.json b/plugins/glean/.codex-plugin/plugin.json index f2ae95e..18d7b9d 100644 --- a/plugins/glean/.codex-plugin/plugin.json +++ b/plugins/glean/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "glean-vnext", - "version": "0.2.26", + "version": "0.2.27", "description": "Glean Codex 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 cc12e91..5bd0562 100644 --- a/plugins/glean/.cursor-plugin/plugin.json +++ b/plugins/glean/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "glean-vnext", "displayName": "Glean vNext", - "version": "0.2.26", + "version": "0.2.27", "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 3ae54b0..cec3f0d 100644 --- a/plugins/glean/dist/index.js +++ b/plugins/glean/dist/index.js @@ -25769,6 +25769,24 @@ function buildApprovalMessage(mcpServer, toolName, args) { } return [`Action: ${toolName}`, "Arguments:", formatArguments(args)].join("\n"); } +var CONNECTOR_AUTH_SUFFIX = "Note: Glean itself is already authenticated. This is a downstream connector/tool authorization request \u2014 NOT [SETUP_REQUIRED]. Do not call the `setup` tool. Have the user authorize using the link above, then retry this tool."; +function isConnectorAuth(result) { + if (!result.isError) return false; + const text = result.content?.find((c) => c.type === "text"); + if (!text || text.type !== "text") return false; + try { + const parsed = JSON.parse(text.text); + return !!parsed && typeof parsed === "object" && Array.isArray(parsed.authUrls) && parsed.authUrls.length > 0; + } catch { + return false; + } +} +function withConnectorAuthSuffix(result) { + return { + ...result, + content: [...result.content, { type: "text", text: CONNECTOR_AUTH_SUFFIX }] + }; +} async function handleRunTool(remoteClient, mcpServer, skillsBaseDir, args) { const serverId = args.server_id; const toolName = args.tool_name; @@ -25787,19 +25805,19 @@ async function handleRunTool(remoteClient, mcpServer, skillsBaseDir, args) { const message = buildApprovalMessage(mcpServer, toolName, args.arguments); const timeout = hitlTimeoutMs(); try { - const result = await mcpServer.elicitInput( + const result2 = await mcpServer.elicitInput( { message, requestedSchema: { type: "object", properties: {} } }, { timeout } ); - if (result.action !== "accept") { + if (result2.action !== "accept") { return { content: [ { type: "text", - text: `Action ${toolName} was ${result.action === "decline" ? "declined" : "cancelled"} by the user.` + text: `Action ${toolName} was ${result2.action === "decline" ? "declined" : "cancelled"} by the user.` } ] }; @@ -25831,11 +25849,15 @@ async function handleRunTool(remoteClient, mcpServer, skillsBaseDir, args) { } throw err; } - return callRemoteTool( + const result = await callRemoteTool( remoteClient, "run_tool", buildRemoteArgs(serverId, toolName, resolvedArgs) ); + if (isConnectorAuth(result)) { + return withConnectorAuthSuffix(result); + } + return result; } function buildRemoteArgs(serverId, toolName, resolvedArgs) { return { diff --git a/src/tools/run-tool.ts b/src/tools/run-tool.ts index 59ac747..7258c5b 100644 --- a/src/tools/run-tool.ts +++ b/src/tools/run-tool.ts @@ -170,6 +170,45 @@ function buildApprovalMessage( return [`Action: ${toolName}`, "Arguments:", formatArguments(args)].join("\n"); } +// Appended to a connector AUTH_REQUIRED result so the model understands the +// downstream connector (not Glean) needs authorization, and does not confuse it +// with the plugin's own [SETUP_REQUIRED] Glean sign-in. +const CONNECTOR_AUTH_SUFFIX = + "Note: Glean itself is already authenticated. This is a downstream " + + "connector/tool authorization request — NOT [SETUP_REQUIRED]. Do not call " + + "the `setup` tool. Have the user authorize using the link above, then retry " + + "this tool."; + +// Detect the gateway's connector AUTH_REQUIRED envelope: an error result whose +// first text content is JSON carrying a non-empty `authUrls` array. This is +// third-party connector auth (the user authorizing e.g. Jira/Slack) — distinct +// from the plugin's own [SETUP_REQUIRED] Glean sign-in. +function isConnectorAuth(result: CallToolResult): boolean { + if (!result.isError) return false; + const text = result.content?.find((c) => c.type === "text"); + if (!text || text.type !== "text") return false; + try { + const parsed = JSON.parse(text.text); + return ( + !!parsed && + typeof parsed === "object" && + Array.isArray((parsed as { authUrls?: unknown }).authUrls) && + (parsed as { authUrls: unknown[] }).authUrls.length > 0 + ); + } catch { + return false; + } +} + +// Append the connector-auth clarification as an extra text block, leaving the +// gateway's original content (links and all) untouched. +function withConnectorAuthSuffix(result: CallToolResult): CallToolResult { + return { + ...result, + content: [...result.content, { type: "text", text: CONNECTOR_AUTH_SUFFIX }], + }; +} + export async function handleRunTool( remoteClient: Client, mcpServer: Server, @@ -250,11 +289,23 @@ export async function handleRunTool( throw err; } - return callRemoteTool( + const result = await callRemoteTool( remoteClient, "run_tool", buildRemoteArgs(serverId, toolName, resolvedArgs), ); + + // A downstream connector (Jira/Slack/...) can require the user to authorize + // their account even though Glean itself is authenticated. The gateway + // surfaces that as an error result whose text is a JSON envelope with + // `authUrls`. Append a clarification so the model doesn't confuse it with the + // plugin's own [SETUP_REQUIRED] and wrongly call `setup`. The gateway's + // original content (including the link) is left untouched. + if (isConnectorAuth(result)) { + return withConnectorAuthSuffix(result); + } + + return result; } /** diff --git a/tests/run-tool.test.ts b/tests/run-tool.test.ts index 57a24af..1d93491 100644 --- a/tests/run-tool.test.ts +++ b/tests/run-tool.test.ts @@ -342,6 +342,91 @@ describe("handleRunTool (HITL)", () => { }); }); +describe("handleRunTool (connector auth suffix)", () => { + let tmpDir: string; + const baseArgs = { + server_id: "composio/jira-pack", + tool_name: "jirasearch", + arguments: { project: "ABC" }, + }; + // What the gateway returns when a downstream connector needs the user to + // authorize their account (Glean itself is already authenticated). + const authResult = { + isError: true, + content: [ + { + type: "text", + text: JSON.stringify({ + error: "This tool requires authentication...", + authUrls: ["https://connect.example.com/jira"], + }), + }, + ], + }; + + function remoteReturning(result: unknown) { + return { + callTool: vi.fn().mockResolvedValue(result), + close: vi.fn(), + } as any; + } + + function lastText(result: { + content: Array<{ type: string; text?: string }>; + }): string { + const block = result.content[result.content.length - 1]; + return block.type === "text" ? block.text ?? "" : ""; + } + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "run-tool-connauth-test-")); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + vi.unstubAllEnvs(); + }); + + it("appends the Glean-authenticated clarification on connector AUTH_REQUIRED, leaving original content intact", async () => { + const remote = remoteReturning(authResult); + const server = makeServer({ elicitation: true }); + + const result = await handleRunTool(remote, server, tmpDir, baseArgs); + + // Original envelope preserved (links untouched) + suffix appended. + expect(result.content).toHaveLength(2); + expect((result.content[0] as { text: string }).text).toContain("authUrls"); + expect(lastText(result)).toContain("Glean itself is already authenticated"); + expect(lastText(result)).toContain("NOT [SETUP_REQUIRED]"); + expect(result.isError).toBe(true); + // Suffix only — no dialog. + expect(server.elicitInput).not.toHaveBeenCalled(); + }); + + it("passes a normal (non-error) result through unchanged", async () => { + const remote = remoteReturning({ content: [{ type: "text", text: "ok" }] }); + const server = makeServer({ elicitation: true }); + + const result = await handleRunTool(remote, server, tmpDir, baseArgs); + + expect(result.content).toHaveLength(1); + expect((result.content[0] as { text: string }).text).toBe("ok"); + }); + + it("passes an ordinary (non-JSON) error through unchanged", async () => { + const remote = remoteReturning({ + isError: true, + content: [{ type: "text", text: "Backend exploded" }], + }); + const server = makeServer({ elicitation: true }); + + const result = await handleRunTool(remote, server, tmpDir, baseArgs); + + expect(result.content).toHaveLength(1); + expect(lastText(result)).not.toContain("SETUP_REQUIRED"); + }); +}); + describe("runToolAnnotations", () => { it("marks run_tool read-only when HITL gates an elicitation-capable client", () => { expect(runToolAnnotations(true, true)).toEqual({ readOnlyHint: true }); From 6463fb1417cadcd185fbea77688b9b6cd6f5b894 Mon Sep 17 00:00:00 2001 From: Eshwar Sundar Date: Sat, 20 Jun 2026 18:47:58 +0530 Subject: [PATCH 2/2] Reword connector-auth suffix as a minimal assistant instruction The previous suffix leaked internals into the user-facing message (the model relayed 'Glean is already authenticated...'). Reframe it as a terse instruction to the assistant: ask the user only to authorize via the link, do not mention Glean/connectors/setup or any auth internals, and do not call setup. --- plugins/glean/dist/index.js | 2 +- src/tools/run-tool.ts | 13 ++++++------- tests/run-tool.test.ts | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/plugins/glean/dist/index.js b/plugins/glean/dist/index.js index cec3f0d..e8f7162 100644 --- a/plugins/glean/dist/index.js +++ b/plugins/glean/dist/index.js @@ -25769,7 +25769,7 @@ function buildApprovalMessage(mcpServer, toolName, args) { } return [`Action: ${toolName}`, "Arguments:", formatArguments(args)].join("\n"); } -var CONNECTOR_AUTH_SUFFIX = "Note: Glean itself is already authenticated. This is a downstream connector/tool authorization request \u2014 NOT [SETUP_REQUIRED]. Do not call the `setup` tool. Have the user authorize using the link above, then retry this tool."; +var CONNECTOR_AUTH_SUFFIX = "Assistant: ask the user only to authorize using the link above. Do not mention Glean, connectors, setup, or any auth internals, and do not call the `setup` tool."; function isConnectorAuth(result) { if (!result.isError) return false; const text = result.content?.find((c) => c.type === "text"); diff --git a/src/tools/run-tool.ts b/src/tools/run-tool.ts index 7258c5b..4558dce 100644 --- a/src/tools/run-tool.ts +++ b/src/tools/run-tool.ts @@ -170,14 +170,13 @@ function buildApprovalMessage( return [`Action: ${toolName}`, "Arguments:", formatArguments(args)].join("\n"); } -// Appended to a connector AUTH_REQUIRED result so the model understands the -// downstream connector (not Glean) needs authorization, and does not confuse it -// with the plugin's own [SETUP_REQUIRED] Glean sign-in. +// Instruction to the ASSISTANT (not user-facing) appended to a connector +// AUTH_REQUIRED result: keep the user-facing ask minimal and don't leak auth +// internals, while still preventing a wrong `setup` call. const CONNECTOR_AUTH_SUFFIX = - "Note: Glean itself is already authenticated. This is a downstream " + - "connector/tool authorization request — NOT [SETUP_REQUIRED]. Do not call " + - "the `setup` tool. Have the user authorize using the link above, then retry " + - "this tool."; + "Assistant: ask the user only to authorize using the link above. Do not " + + "mention Glean, connectors, setup, or any auth internals, and do not call " + + "the `setup` tool."; // Detect the gateway's connector AUTH_REQUIRED envelope: an error result whose // first text content is JSON carrying a non-empty `authUrls` array. This is diff --git a/tests/run-tool.test.ts b/tests/run-tool.test.ts index 1d93491..9f242cf 100644 --- a/tests/run-tool.test.ts +++ b/tests/run-tool.test.ts @@ -387,7 +387,7 @@ describe("handleRunTool (connector auth suffix)", () => { vi.unstubAllEnvs(); }); - it("appends the Glean-authenticated clarification on connector AUTH_REQUIRED, leaving original content intact", async () => { + it("appends a minimal authorize instruction on connector AUTH_REQUIRED, leaving original content intact", async () => { const remote = remoteReturning(authResult); const server = makeServer({ elicitation: true }); @@ -396,8 +396,8 @@ describe("handleRunTool (connector auth suffix)", () => { // Original envelope preserved (links untouched) + suffix appended. expect(result.content).toHaveLength(2); expect((result.content[0] as { text: string }).text).toContain("authUrls"); - expect(lastText(result)).toContain("Glean itself is already authenticated"); - expect(lastText(result)).toContain("NOT [SETUP_REQUIRED]"); + expect(lastText(result)).toContain("authorize"); + expect(lastText(result)).toContain("setup"); expect(result.isError).toBe(true); // Suffix only — no dialog. expect(server.elicitInput).not.toHaveBeenCalled();