diff --git a/extensions/qa-lab/src/suite-runtime-agent-tools.test.ts b/extensions/qa-lab/src/suite-runtime-agent-tools.test.ts index beefb15ba5ee..b8d43f90a6aa 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-tools.test.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-tools.test.ts @@ -124,7 +124,7 @@ describe("qa suite runtime agent tools helpers", () => { ).resolves.toEqual("done"); }); - it("calls plugin-tools MCP through the resolved node executable", async () => { + it("falls back to the source plugin-tools MCP entry", async () => { listToolsMock.mockResolvedValueOnce({ tools: [{ name: "plugin.echo" }] as never[], }); @@ -180,6 +180,35 @@ describe("qa suite runtime agent tools helpers", () => { expect(closeMock).toHaveBeenCalled(); }); + it("prefers the built plugin-tools MCP entry", async () => { + const builtRepoRoot = await makeTempDir("qa-built-repo-"); + const distEntry = path.join(builtRepoRoot, "dist", "mcp", "plugin-tools-serve.js"); + await fs.mkdir(path.dirname(distEntry), { recursive: true }); + await fs.writeFile(distEntry, "// built MCP entry\n", "utf8"); + listToolsMock.mockResolvedValueOnce({ + tools: [{ name: "plugin.echo" }] as never[], + }); + + await callPluginToolsMcp({ + env: { + gateway: { + tempRoot: gatewayTempRoot, + runtimeEnv: { PATH: "/usr/bin" }, + }, + repoRoot: builtRepoRoot, + } as never, + toolName: "plugin.echo", + args: {}, + }); + + expect(stdioTransportMock).toHaveBeenCalledWith( + expect.objectContaining({ + command: "/usr/bin/node", + args: [distEntry], + }), + ); + }); + it("reports available plugin-tools MCP names when the requested tool is missing", async () => { listToolsMock.mockResolvedValueOnce({ tools: [{ name: "plugin.beta" }, { name: "plugin.alpha" }] as never[], diff --git a/extensions/qa-lab/src/suite-runtime-agent-tools.ts b/extensions/qa-lab/src/suite-runtime-agent-tools.ts index 91b9b0a2b0c6..5a38d89dd9f3 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-tools.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-tools.ts @@ -22,6 +22,20 @@ const requireFromHere = createRequire(import.meta.url); const MCP_STDERR_TAIL_LIMIT = 8_192; const MCP_REQUEST_TIMEOUT_MS = 180_000; +async function resolvePluginToolsMcpArgs(repoRoot: string) { + const distEntry = path.join(repoRoot, "dist", "mcp", "plugin-tools-serve.js"); + try { + await fs.access(distEntry); + return [distEntry]; + } catch { + return [ + "--import", + requireFromHere.resolve("tsx"), + path.join(repoRoot, "src", "mcp", "plugin-tools-serve.ts"), + ]; + } +} + function findSkill(skills: QaSkillStatusEntry[], name: string) { return skills.find((skill) => skill.name === name); } @@ -73,11 +87,7 @@ async function callPluginToolsMcp(params: { const nodeExecPath = await resolveQaNodeExecPath(); const transport = new StdioClientTransport({ command: nodeExecPath, - args: [ - "--import", - requireFromHere.resolve("tsx"), - path.join(params.env.repoRoot, "src/mcp/plugin-tools-serve.ts"), - ], + args: await resolvePluginToolsMcpArgs(params.env.repoRoot), stderr: "pipe", cwd: params.env.repoRoot, env: transportEnv,