mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
fix(cache): sort MCP tools deterministically to stabilize prompt cache (#58037)
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
This commit is contained in:
@@ -102,6 +102,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Matrix/media: surface a dedicated `[matrix <kind> attachment too large]` marker for oversized inbound media instead of the generic unavailable marker, and classify size-limit failures with a typed Matrix error. (#60289) Thanks @efe-arv.
|
||||
- WhatsApp/watchdog: reset watchdog timeout after reconnect so quiet channels no longer enter a tight reconnect loop from stale message timestamps carried across connection runs. (#60007) Thanks @MonkeyLeeT.
|
||||
- Agents/fallback: persist selected fallback overrides before retry attempts start, prefer persisted overrides during live-session reconciliation, and keep provider-scoped auth-profile failover from snapping retries back to stale primary selections.
|
||||
- Agents/MCP: sort MCP tools deterministically by name so the tools block in API requests is stable across turns, preventing unnecessary prompt-cache busting from non-deterministic `listTools()` order. (#58037) Thanks @bcherny.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -101,6 +101,11 @@ export async function materializeBundleMcpToolsForRun(params: {
|
||||
});
|
||||
}
|
||||
|
||||
// Sort tools deterministically by name so the tools block in API requests is
|
||||
// stable across turns. MCP's listTools() does not guarantee order, and any
|
||||
// change in the tools array busts the prompt cache at the tools block.
|
||||
tools.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return {
|
||||
tools,
|
||||
dispose: async () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function makeTempDir(prefix: string): Promise<string> {
|
||||
return dir;
|
||||
}
|
||||
|
||||
async function writeExecutable(filePath: string, content: string): Promise<void> {
|
||||
export async function writeExecutable(filePath: string, content: string): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, content, { encoding: "utf-8", mode: 0o755 });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -6,9 +7,14 @@ import {
|
||||
startSseProbeServer,
|
||||
writeBundleProbeMcpServer,
|
||||
writeClaudeBundle,
|
||||
writeExecutable,
|
||||
} from "./pi-bundle-mcp-test-harness.js";
|
||||
import { createBundleMcpToolRuntime } from "./pi-bundle-mcp-tools.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const SDK_SERVER_MCP_PATH = require.resolve("@modelcontextprotocol/sdk/server/mcp.js");
|
||||
const SDK_SERVER_STDIO_PATH = require.resolve("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupBundleMcpHarness();
|
||||
});
|
||||
@@ -154,4 +160,43 @@ describe("createBundleMcpToolRuntime", () => {
|
||||
await sseServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns tools sorted alphabetically for stable prompt-cache keys", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-bundle-mcp-tools-");
|
||||
const serverScriptPath = path.join(workspaceDir, "servers", "multi-tool.mjs");
|
||||
// Register tools in non-alphabetical order; runtime must sort them.
|
||||
await writeExecutable(
|
||||
serverScriptPath,
|
||||
`#!/usr/bin/env node
|
||||
import { McpServer } from ${JSON.stringify(SDK_SERVER_MCP_PATH)};
|
||||
import { StdioServerTransport } from ${JSON.stringify(SDK_SERVER_STDIO_PATH)};
|
||||
const server = new McpServer({ name: "multi", version: "1.0.0" });
|
||||
server.tool("zeta", "z", async () => ({ content: [{ type: "text", text: "z" }] }));
|
||||
server.tool("alpha", "a", async () => ({ content: [{ type: "text", text: "a" }] }));
|
||||
server.tool("mu", "m", async () => ({ content: [{ type: "text", text: "m" }] }));
|
||||
await server.connect(new StdioServerTransport());
|
||||
`,
|
||||
);
|
||||
|
||||
const runtime = await createBundleMcpToolRuntime({
|
||||
workspaceDir,
|
||||
cfg: {
|
||||
mcp: {
|
||||
servers: {
|
||||
multi: { command: "node", args: [serverScriptPath] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
expect(runtime.tools.map((tool) => tool.name)).toEqual([
|
||||
"multi__alpha",
|
||||
"multi__mu",
|
||||
"multi__zeta",
|
||||
]);
|
||||
} finally {
|
||||
await runtime.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user