diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index cd5194dd98b..bb3bdaacdda 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -174,7 +174,7 @@ OPENCLAW_LIVE_TEST=1 \ This does not ask Gemini to generate a response. It writes the same system settings OpenClaw gives Gemini, then runs `gemini --debug mcp list` to prove a saved `transport: "streamable-http"` server is normalized to Gemini's HTTP MCP -shape and can connect. +shape and can connect to a local streamable-HTTP MCP server. Docker recipe: diff --git a/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts b/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts index 2e312ad8199..0af9491d684 100644 --- a/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts +++ b/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts @@ -1,5 +1,8 @@ import { execFile } from "node:child_process"; +import http from "node:http"; import { promisify } from "node:util"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { describe, expect, it } from "vitest"; import { isLiveTestEnabled } from "../live-test-helpers.js"; import { prepareCliBundleMcpConfig } from "./bundle-mcp.js"; @@ -17,6 +20,42 @@ async function canRunGemini(command: string): Promise { } } +async function startLocalStreamableHttpMcpServer(): Promise<{ + url: string; + close: () => Promise; +}> { + const mcpServer = new McpServer({ name: "openclaw-gemini-live-probe", version: "1.0.0" }); + mcpServer.tool("openclaw_live_probe", "OpenClaw Gemini MCP live probe", async () => ({ + content: [{ type: "text", text: "ok" }], + })); + + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await mcpServer.connect(transport); + const httpServer = http.createServer(async (req, res) => { + if (!req.url?.startsWith("/mcp")) { + res.writeHead(404).end(); + return; + } + await transport.handleRequest(req, res); + }); + + await new Promise((resolve) => { + httpServer.listen(0, "127.0.0.1", resolve); + }); + const address = httpServer.address(); + const port = typeof address === "object" && address ? address.port : 0; + + return { + url: `http://127.0.0.1:${port}/mcp`, + close: async () => { + await transport.close().catch(() => undefined); + await new Promise((resolve, reject) => + httpServer.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + describeLive("Gemini CLI MCP settings smoke", () => { it("connects to an OpenClaw-configured streamable-http server", async () => { const geminiCommand = process.env.OPENCLAW_LIVE_GEMINI_COMMAND ?? "gemini"; @@ -25,10 +64,7 @@ describeLive("Gemini CLI MCP settings smoke", () => { return; } - const inheritedEnv = - typeof process.env.CONTEXT7_API_KEY === "string" && process.env.CONTEXT7_API_KEY - ? { CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY } - : undefined; + const probeServer = await startLocalStreamableHttpMcpServer(); const prepared = await prepareCliBundleMcpConfig({ enabled: true, mode: "gemini-system-settings", @@ -41,15 +77,13 @@ describeLive("Gemini CLI MCP settings smoke", () => { plugins: { enabled: false }, mcp: { servers: { - context7: { + openclawLiveProbe: { transport: "streamable-http", - url: "https://mcp.context7.com/mcp", - ...(inheritedEnv ? { headers: { Authorization: "Bearer ${CONTEXT7_API_KEY}" } } : {}), + url: probeServer.url, }, }, }, }, - env: inheritedEnv, }); try { @@ -62,11 +96,12 @@ describeLive("Gemini CLI MCP settings smoke", () => { maxBuffer: 1024 * 1024, }); const output = `${result.stdout}\n${result.stderr}`; - expect(output).toContain("context7"); + expect(output).toContain("openclawLiveProbe"); expect(output).toMatch(/\(http\)|type:\s*http|http/i); expect(output).not.toContain("transport"); } finally { await prepared.cleanup?.(); + await probeServer.close(); } }, 60_000); });