From f2c1123e7b8a121d81131ca94dfabf843311b935 Mon Sep 17 00:00:00 2001 From: aditya-scio Date: Thu, 17 Sep 2026 14:00:35 -0700 Subject: [PATCH 1/2] fix(mcp): resolve remote find_skills name via tools/list The upstream server exposes either find_skills or find_skills_and_tools depending on the rename experiment. Resolve the name once per client from tools/list and call whichever exists, preferring the new name. Co-Authored-By: Claude --- shared/glean/mcp/src/tools/find-skills.ts | 24 ++++++++++++++-- shared/glean/mcp/tests/find-skills.test.ts | 33 ++++++++++++++++++---- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/shared/glean/mcp/src/tools/find-skills.ts b/shared/glean/mcp/src/tools/find-skills.ts index 07ddd35..9b8947c 100644 --- a/shared/glean/mcp/src/tools/find-skills.ts +++ b/shared/glean/mcp/src/tools/find-skills.ts @@ -3,6 +3,23 @@ import { callRemoteTool } from "../remote-client.js"; import { writeSkillsToDisk, formatAvailableSkillsPrompt } from "../skill-writer.js"; import type { SkillsMap } from "../types.js"; +// ponytail: the remote exposes either the new or the legacy name during the +// rename rollout. Resolve once per client via tools/list (the client is a +// process singleton, so this is one extra round trip per process). +// Drop this and hardcode "find_skills_and_tools" once every host has upgraded. +const REMOTE_NAMES = ["find_skills_and_tools", "find_skills"] as const; +const resolvedRemoteName = new WeakMap(); + +async function resolveRemoteName(client: Client): Promise { + const cached = resolvedRemoteName.get(client); + if (cached) return cached; + const { tools } = await client.listTools(); + const names = new Set(tools.map((t) => t.name)); + const name = REMOTE_NAMES.find((n) => names.has(n)) ?? REMOTE_NAMES[0]; + resolvedRemoteName.set(client, name); + return name; +} + export async function handleFindSkills( remoteClient: Client, skillsBaseDir: string, @@ -15,7 +32,8 @@ export async function handleFindSkills( toolArgs.queries = [args.query]; } - const result = await callRemoteTool(remoteClient, "find_skills", toolArgs); + const remoteName = await resolveRemoteName(remoteClient); + const result = await callRemoteTool(remoteClient, remoteName, toolArgs); const textContent = result.content.find((c) => c.type === "text"); if (!textContent || textContent.type !== "text") { @@ -23,13 +41,13 @@ export async function handleFindSkills( } if (result.isError) { - throw new Error(textContent.text || "find_skills failed"); + throw new Error(textContent.text || `${remoteName} failed`); } const parsed = JSON.parse(textContent.text) as { skills?: SkillsMap }; if (!parsed.skills || typeof parsed.skills !== "object") { console.error( - `find_skills: unexpected response shape, keys: ${Object.keys(parsed).join(", ")}`, + `${remoteName}: unexpected response shape, keys: ${Object.keys(parsed).join(", ")}`, ); return ""; } diff --git a/shared/glean/mcp/tests/find-skills.test.ts b/shared/glean/mcp/tests/find-skills.test.ts index f89cbdd..e728120 100644 --- a/shared/glean/mcp/tests/find-skills.test.ts +++ b/shared/glean/mcp/tests/find-skills.test.ts @@ -5,8 +5,12 @@ import os from "node:os"; import { handleFindSkills } from "../src/tools/find-skills.js"; import type { SkillsMap } from "../src/types.js"; -function createMockClient(skills: SkillsMap) { +const listTools = (...names: string[]) => + vi.fn().mockResolvedValue({ tools: names.map((name) => ({ name })) }); + +function createMockClient(skills: SkillsMap, remoteName = "find_skills_and_tools") { return { + listTools: listTools(remoteName), callTool: vi.fn().mockResolvedValue({ content: [ { @@ -32,7 +36,7 @@ describe("handleFindSkills", () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); - it("calls find_skills and writes skill files", async () => { + it("calls find_skills_and_tools and writes skill files", async () => { const mockClient = createMockClient({ "search-jira": { "SKILL.md": @@ -50,7 +54,7 @@ describe("handleFindSkills", () => { expect(mockClient.callTool).toHaveBeenCalledWith( expect.objectContaining({ - name: "find_skills", + name: "find_skills_and_tools", arguments: {}, }), expect.objectContaining({ timeout: expect.any(Number) }), @@ -75,7 +79,7 @@ describe("handleFindSkills", () => { expect(mockClient.callTool).toHaveBeenCalledWith( expect.objectContaining({ - name: "find_skills", + name: "find_skills_and_tools", arguments: { queries: ["create a calendar event"] }, }), expect.objectContaining({ timeout: expect.any(Number) }), @@ -91,7 +95,7 @@ describe("handleFindSkills", () => { expect(mockClient.callTool).toHaveBeenCalledWith( expect.objectContaining({ - name: "find_skills", + name: "find_skills_and_tools", arguments: { queries: ["search emails", "create calendar event"] }, }), expect.objectContaining({ timeout: expect.any(Number) }), @@ -100,6 +104,7 @@ describe("handleFindSkills", () => { it("returns empty XML when response has no skills field", async () => { const mockClient = { + listTools: listTools("find_skills_and_tools"), callTool: vi.fn().mockResolvedValue({ content: [{ type: "text", text: JSON.stringify({ unexpected: true }) }], }), @@ -120,6 +125,7 @@ describe("handleFindSkills", () => { it("handles missing text content gracefully", async () => { const mockClient = { + listTools: listTools("find_skills_and_tools"), callTool: vi.fn().mockResolvedValue({ content: [] }), close: vi.fn(), } as any; @@ -131,6 +137,7 @@ describe("handleFindSkills", () => { it("throws with upstream message when find_skills returns an error", async () => { const mockClient = { + listTools: listTools("find_skills"), callTool: vi.fn().mockResolvedValue({ content: [{ type: "text", text: "backend unavailable" }], isError: true, @@ -142,4 +149,20 @@ describe("handleFindSkills", () => { handleFindSkills(mockClient, tmpDir, {}), ).rejects.toThrow("backend unavailable"); }); + + it("falls back to legacy find_skills when the host has not been renamed", async () => { + const mockClient = createMockClient({}, "find_skills"); + await handleFindSkills(mockClient, tmpDir, {}); + expect(mockClient.callTool).toHaveBeenCalledWith( + expect.objectContaining({ name: "find_skills" }), + expect.anything(), + ); + }); + + it("resolves the remote name once per client", async () => { + const mockClient = createMockClient({}); + await handleFindSkills(mockClient, tmpDir, {}); + await handleFindSkills(mockClient, tmpDir, {}); + expect(mockClient.listTools).toHaveBeenCalledTimes(1); + }); }); From e30a5e01fa6395b7ed89102566010307d1f313f8 Mon Sep 17 00:00:00 2001 From: aditya-scio Date: Thu, 17 Sep 2026 14:22:48 -0700 Subject: [PATCH 2/2] Fix --- shared/glean-dev-docs/CHANGELOG.md | 10 ++++++++++ shared/glean/CHANGELOG.md | 10 ++++++++++ shared/glean/mcp/src/tools/find-skills.ts | 11 +++++++++++ shared/glean/mcp/tests/find-skills.test.ts | 14 ++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/shared/glean-dev-docs/CHANGELOG.md b/shared/glean-dev-docs/CHANGELOG.md index 068338f..baf440a 100644 --- a/shared/glean-dev-docs/CHANGELOG.md +++ b/shared/glean-dev-docs/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [3.4.2](https://github.com/gleanwork/agent-plugins/compare/v3.4.1...v3.4.2) (2026-09-03) + +### Bug Fixes + +* sync plugin changelogs after generation ([b7df133](https://github.com/gleanwork/agent-plugins/commit/b7df133796df5a8a25a0cde95d40a2197331f45a)) + +### Documentation + +* **plugins:** update Glean plugin description ([f4a3e23](https://github.com/gleanwork/agent-plugins/commit/f4a3e23000efc50cfdea75dd3c11da06b26beb71)) + ## [3.4.1](https://github.com/gleanwork/agent-plugins/compare/v3.4.0...v3.4.1) (2026-09-02) ### Documentation diff --git a/shared/glean/CHANGELOG.md b/shared/glean/CHANGELOG.md index 068338f..baf440a 100644 --- a/shared/glean/CHANGELOG.md +++ b/shared/glean/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [3.4.2](https://github.com/gleanwork/agent-plugins/compare/v3.4.1...v3.4.2) (2026-09-03) + +### Bug Fixes + +* sync plugin changelogs after generation ([b7df133](https://github.com/gleanwork/agent-plugins/commit/b7df133796df5a8a25a0cde95d40a2197331f45a)) + +### Documentation + +* **plugins:** update Glean plugin description ([f4a3e23](https://github.com/gleanwork/agent-plugins/commit/f4a3e23000efc50cfdea75dd3c11da06b26beb71)) + ## [3.4.1](https://github.com/gleanwork/agent-plugins/compare/v3.4.0...v3.4.1) (2026-09-02) ### Documentation diff --git a/shared/glean/mcp/src/tools/find-skills.ts b/shared/glean/mcp/src/tools/find-skills.ts index 9b8947c..de40842 100644 --- a/shared/glean/mcp/src/tools/find-skills.ts +++ b/shared/glean/mcp/src/tools/find-skills.ts @@ -44,6 +44,17 @@ export async function handleFindSkills( throw new Error(textContent.text || `${remoteName} failed`); } + // ponytail: lazy-disclosure surfaces (/mcp/default, skill-pack servers) + // answer with an XML index and expect read_skill_files; the plugin only + // speaks the gateway/proxy JSON map. Name the misconfiguration instead of + // surfacing "Unexpected token '<'". + if (textContent.text.trimStart().startsWith("<")) { + throw new Error( + `${remoteName} returned a lazy-disclosure XML index instead of the skills JSON map; ` + + "point the plugin at the /mcp/gateway/proxy route (check GLEAN_MCP_SERVER_URL).", + ); + } + const parsed = JSON.parse(textContent.text) as { skills?: SkillsMap }; if (!parsed.skills || typeof parsed.skills !== "object") { console.error( diff --git a/shared/glean/mcp/tests/find-skills.test.ts b/shared/glean/mcp/tests/find-skills.test.ts index e728120..6147409 100644 --- a/shared/glean/mcp/tests/find-skills.test.ts +++ b/shared/glean/mcp/tests/find-skills.test.ts @@ -150,6 +150,20 @@ describe("handleFindSkills", () => { ).rejects.toThrow("backend unavailable"); }); + it("names the route misconfiguration when the remote answers with the lazy XML index", async () => { + const mockClient = { + listTools: listTools("find_skills_and_tools"), + callTool: vi.fn().mockResolvedValue({ + content: [{ type: "text", text: "\n" }], + }), + close: vi.fn(), + } as any; + + await expect(handleFindSkills(mockClient, tmpDir, {})).rejects.toThrow( + /gateway\/proxy/, + ); + }); + it("falls back to legacy find_skills when the host has not been renamed", async () => { const mockClient = createMockClient({}, "find_skills"); await handleFindSkills(mockClient, tmpDir, {});