diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index 9951e698ea3..5e615873d17 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -416,7 +416,7 @@ Notes: - `list` sorts server names. - `show` without a name prints the full configured MCP server object. -- `status` classifies configured transports without connecting. `--verbose` includes resolved launch, timeout, OAuth, filter, and parallel-call details, including when stored OAuth tokens require additional authorization. +- `status` classifies configured transports without connecting. `--verbose` includes resolved launch, timeout, OAuth, filter, and parallel-call details, including when stored OAuth tokens require additional authorization. Credential-bearing stdio arguments are redacted in text and JSON output. - `doctor` performs static checks without connecting. Add `--probe` when the command should also verify that enabled servers connect. - `probe` connects and reports tool counts, resources/prompts support, list-change support, and diagnostics. - `add` accepts stdio flags such as `--command`, `--arg`, `--env`, and `--cwd`, or HTTP flags such as `--url`, `--transport`, `--header`, `--auth oauth`, TLS, timeout, and tool-selection flags. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index c6e093bc147..1185a0ceaa6 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -327,8 +327,8 @@ Typical workflow: 1. Open **MCP** from the sidebar. 2. Check the summary cards for total, enabled, OAuth, and filtered server counts. 3. Review each server row for transport, enablement, auth, filters, timeouts, and command hints. -4. Add, enable, disable, or remove servers directly on the MCP page. Use the **Plugins** page for one-click connectors and discovery. -5. Edit the scoped `mcp` config section for server definitions, headers, TLS/mTLS paths, OAuth metadata, tool filters, and Codex projection metadata. +4. Add, enable, disable, or remove servers directly on the MCP page. Choose Streamable HTTP, SSE, or stdio explicitly; stdio command lines accept quoted arguments such as paths with spaces. Use the **Plugins** page for one-click connectors and discovery. +5. Edit the scoped `mcp` config section for advanced server fields such as environment variables, working directories, headers, TLS/mTLS paths, OAuth metadata, tool filters, and Codex projection metadata. 6. Use **Save** for a config write, or **Save & Publish** when the running Gateway should apply the changed config. 7. Run `openclaw mcp status --verbose`, `openclaw mcp doctor --probe`, or `openclaw mcp reload` from a terminal for static diagnostics, live proof, or cached-runtime disposal. diff --git a/src/agents/mcp-stdio.ts b/src/agents/mcp-stdio.ts index dc78aee7a1b..06b5f1733fc 100644 --- a/src/agents/mcp-stdio.ts +++ b/src/agents/mcp-stdio.ts @@ -3,6 +3,7 @@ * Accepts OpenClaw and upstream MCP config field names, keeping only * command/args/env/cwd needed to spawn a stdio server. */ +import { redactSensitiveArgv } from "../config/redact-argv.js"; import { isMcpConfigRecord, toMcpEnvRecord, toMcpStringArray } from "./mcp-config-shared.js"; /** Normalized stdio MCP server launch config. */ @@ -53,8 +54,8 @@ export function resolveStdioMcpServerLaunchConfig( /** Describe a stdio MCP launch config for diagnostics. */ export function describeStdioMcpServerLaunchConfig(config: StdioMcpServerLaunchConfig): string { - const args = - Array.isArray(config.args) && config.args.length > 0 ? ` ${config.args.join(" ")}` : ""; + const redactedArgs = Array.isArray(config.args) ? redactSensitiveArgv(config.args) : []; + const args = redactedArgs.length > 0 ? ` ${redactedArgs.join(" ")}` : ""; const cwd = config.cwd ? ` (cwd=${config.cwd})` : ""; return `${config.command}${args}${cwd}`; } diff --git a/src/cli/mcp-cli.test.ts b/src/cli/mcp-cli.test.ts index 21f7e621a07..bf3281893d8 100644 --- a/src/cli/mcp-cli.test.ts +++ b/src/cli/mcp-cli.test.ts @@ -477,6 +477,36 @@ describe("mcp cli", () => { }); }); + it("redacts stdio argv credentials from MCP status output", async () => { + await withTempHome("openclaw-cli-mcp-home-", async () => { + const workspaceDir = await createWorkspace(); + vi.spyOn(process, "cwd").mockReturnValue(workspaceDir); + + await runMcpCommand([ + "mcp", + "set", + "local", + '{"command":"node","args":["server.mjs","--api-key","test-api-key","--token=test-token"]}', + ]); + mockLog.mockClear(); + + await runMcpCommand(["mcp", "status", "--json"]); + const jsonOutput = lastLogLine(); + expect(jsonOutput).not.toContain("test-api-key"); + expect(jsonOutput).not.toContain("test-token"); + expect(JSON.parse(jsonOutput).servers[0].launch).toBe( + "node server.mjs --api-key *** --token=***", + ); + + mockLog.mockClear(); + await runMcpCommand(["mcp", "status", "--verbose"]); + const verboseOutput = mockLog.mock.calls.map(([line]) => String(line)).join("\n"); + expect(verboseOutput).toContain("launch: node server.mjs --api-key *** --token=***"); + expect(verboseOutput).not.toContain("test-api-key"); + expect(verboseOutput).not.toContain("test-token"); + }); + }); + it("includes OAuth credential status in MCP status output", async () => { await withTempHome("openclaw-cli-mcp-home-", async () => { const workspaceDir = await createWorkspace(); diff --git a/src/config/types.mcp.ts b/src/config/types.mcp.ts index 3f90250aa27..50ab7ee4813 100644 --- a/src/config/types.mcp.ts +++ b/src/config/types.mcp.ts @@ -86,9 +86,4 @@ export type McpConfig = { /** Dedicated listener port. Defaults to the Gateway port plus one. */ sandboxPort?: number; }; - /** - * Idle TTL for session-scoped bundled MCP runtimes, in milliseconds. - * - * Defaults to 10 minutes. Set to 0 to disable idle eviction. - */ }; diff --git a/src/mcp/plugin-tools-handlers.ts b/src/mcp/plugin-tools-handlers.ts index 611dbdcab3e..46f3b0e76e6 100644 --- a/src/mcp/plugin-tools-handlers.ts +++ b/src/mcp/plugin-tools-handlers.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "node:crypto"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; // Plugin MCP tool handlers route plugin tool calls through the active runtime. import { @@ -82,7 +83,7 @@ export function createPluginToolsMcpHandlers(tools: AnyAgentTool[]) { }; } try { - const result = await tool.execute(`mcp-${Date.now()}`, params.arguments ?? {}, signal); + const result = await tool.execute(`mcp-${randomUUID()}`, params.arguments ?? {}, signal); const rawContent = result && typeof result === "object" && "content" in result ? (result as { content?: unknown }).content diff --git a/src/mcp/plugin-tools-mcp-client.test.ts b/src/mcp/plugin-tools-mcp-client.test.ts index 795b9cb1569..5ad494b66ff 100644 --- a/src/mcp/plugin-tools-mcp-client.test.ts +++ b/src/mcp/plugin-tools-mcp-client.test.ts @@ -46,7 +46,9 @@ describe("plugin tools MCP client bridge", () => { }); expect(execute).toHaveBeenCalledWith( - expect.stringMatching(/^mcp-\d+$/), + expect.stringMatching( + /^mcp-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, + ), { query: "ORBIT-9 codename", maxResults: 3 }, expect.any(AbortSignal), undefined, diff --git a/src/mcp/plugin-tools-serve.test.ts b/src/mcp/plugin-tools-serve.test.ts index 2bf94b4f2de..1cfc7a94d23 100644 --- a/src/mcp/plugin-tools-serve.test.ts +++ b/src/mcp/plugin-tools-serve.test.ts @@ -215,17 +215,17 @@ describe("plugin tools MCP server", () => { const executeCall = requireFirstMockCall(execute.mock.calls, "plugin tool execute"); const requestId = executeCall[0]; expect(typeof requestId).toBe("string"); - expect((requestId as string).startsWith("mcp-")).toBe(true); - expect(Number.isSafeInteger(Number((requestId as string).slice("mcp-".length)))).toBe(true); + expect(requestId).toMatch( + /^mcp-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, + ); expect(executeCall[1]).toEqual({ query: "remember this" }); expect(executeCall[2]).toBeUndefined(); expect(executeCall[3]).toBeUndefined(); expect(result.content).toEqual([{ type: "text", text: "Stored." }]); }); - it("releases execution tracking after repeated direct MCP calls", async () => { - let now = 1_000; - vi.spyOn(Date, "now").mockImplementation(() => now++); + it("uses unique ids and releases execution tracking after repeated direct MCP calls", async () => { + vi.spyOn(Date, "now").mockReturnValue(1_000); const executeSuccess = vi.fn().mockResolvedValue({ content: "Stored." }); const executeFailure = vi.fn().mockRejectedValue(new Error("unavailable")); const handlers = createPluginToolsMcpHandlers([ @@ -250,8 +250,12 @@ describe("plugin tools MCP server", () => { expect(executeSuccess).toHaveBeenCalledTimes(32); expect(executeFailure).toHaveBeenCalledTimes(32); - for (const [toolCallId] of [...executeSuccess.mock.calls, ...executeFailure.mock.calls]) { - expect(consumeTrackedToolExecutionStarted(String(toolCallId))).toBeUndefined(); + const toolCallIds = [...executeSuccess.mock.calls, ...executeFailure.mock.calls].map( + ([toolCallId]) => String(toolCallId), + ); + expect(new Set(toolCallIds).size).toBe(toolCallIds.length); + for (const toolCallId of toolCallIds) { + expect(consumeTrackedToolExecutionStarted(toolCallId)).toBeUndefined(); } }); diff --git a/ui/src/components/mcp-server-form.ts b/ui/src/components/mcp-server-form.ts index 4d4ea9556bb..40bebf6f678 100644 --- a/ui/src/components/mcp-server-form.ts +++ b/ui/src/components/mcp-server-form.ts @@ -1,8 +1,10 @@ import { html, type TemplateResult } from "lit"; import { t } from "../i18n/index.ts"; +import type { McpServerTransport } from "../lib/config/mcp-servers.ts"; export type McpServerForm = { name: string; + transport: McpServerTransport; target: string; }; @@ -19,9 +21,11 @@ export function renderMcpServerForm(props: { const form = event.currentTarget as HTMLFormElement; const data = new FormData(form); const name = data.get("mcp-name"); + const transport = data.get("mcp-transport"); const target = data.get("mcp-target"); props.onSubmit({ name: typeof name === "string" ? name.trim() : "", + transport: transport === "sse" || transport === "stdio" ? transport : "streamable-http", target: typeof target === "string" ? target.trim() : "", }); }; @@ -40,6 +44,19 @@ export function renderMcpServerForm(props: { ?disabled=${disabled} /> +