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}
/>
+