fix: support draft 2020 mcp tool schemas

This commit is contained in:
Peter Steinberger
2026-04-25 03:51:24 +01:00
parent 9fbfedf12a
commit 37c2450124
3 changed files with 78 additions and 1 deletions

View File

@@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai
- Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago.
- Browser/sandbox: pass the resolved `browser.ssrfPolicy` into sandbox browser bridges and refresh cached bridges when the effective policy changes, so sandboxed browser navigation honors private-network opt-ins. Fixes #45153 and #57055. Thanks @jzakirov, @zuoanCo, and @kybrcore.
- Browser/proxy: keep Gateway/provider proxy environment variables from proxying the OpenClaw-managed browser, so `HTTP_PROXY` and `HTTPS_PROXY` no longer block ordinary browser navigation. Fixes #71358. Thanks @Sanjays2402.
- Agents/MCP: validate draft-2020-12 MCP tool output schemas with a draft-aware bundle-MCP client validator, so external MCP servers no longer fail catalog/tool execution with missing schema refs. Fixes #68772 and #70196. Thanks @mwiesen.
- Dashboard/Windows: open Control UI and OAuth URLs through the system URL handler without `cmd.exe` parsing or PATH-based `rundll32` lookup, and reject non-HTTP browser-open inputs. Fixes #71098. Thanks @Sanjays2402.
- Config/doctor: reject legacy `secretref-env:<ENV_VAR>` marker strings on SecretRef credential paths and migrate valid markers to structured env SecretRefs with `openclaw doctor --fix`. Fixes #51794. Thanks @halointellicore.
- Providers/OpenAI: separate API-key and Codex sign-in onboarding groups, and avoid replaying stale OpenAI Responses reasoning blocks after a model route switch.

View File

@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createBundleMcpJsonSchemaValidator } from "./pi-bundle-mcp-runtime.js";
import { cleanupBundleMcpHarness } from "./pi-bundle-mcp-test-harness.js";
import {
__testing,
@@ -69,6 +70,25 @@ afterEach(async () => {
});
describe("session MCP runtime", () => {
it("accepts draft-2020-12 tool output schemas from external MCP catalogs", () => {
const validator = createBundleMcpJsonSchemaValidator().getValidator<{ url: string }>({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
additionalProperties: false,
});
expect(validator({ url: "https://example.com" })).toEqual({
valid: true,
data: { url: "https://example.com" },
errorMessage: undefined,
});
expect(validator({ url: 42 }).valid).toBe(false);
});
it("keeps colliding sanitized tool definitions stable across catalog order changes", async () => {
const catalogA = [
{ toolName: "alpha?", description: "question" },

View File

@@ -1,8 +1,16 @@
import crypto from "node:crypto";
import { createRequire } from "node:module";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv-provider.js";
import type {
JsonSchemaType,
JsonSchemaValidator,
jsonSchemaValidator,
} from "@modelcontextprotocol/sdk/validation/types.js";
import type { ErrorObject, ValidateFunction } from "ajv";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { logWarn } from "../logger.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
@@ -34,7 +42,53 @@ type CreateSessionMcpRuntime = (
params: Parameters<typeof createSessionMcpRuntime>[0] & { configFingerprint?: string },
) => SessionMcpRuntime;
const require = createRequire(import.meta.url);
const SESSION_MCP_RUNTIME_MANAGER_KEY = Symbol.for("openclaw.sessionMcpRuntimeManager");
const DRAFT_2020_12_SCHEMA = "https://json-schema.org/draft/2020-12/schema";
type Ajv2020Like = {
compile: (schema: JsonSchemaType) => ValidateFunction;
errorsText: (errors?: ErrorObject[] | null) => string;
};
function isDraft202012Schema(schema: JsonSchemaType): boolean {
return (schema as { $schema?: unknown }).$schema === DRAFT_2020_12_SCHEMA;
}
export function createBundleMcpJsonSchemaValidator(): jsonSchemaValidator {
const defaultValidator = new AjvJsonSchemaValidator();
const Ajv2020Ctor = require("ajv/dist/2020") as new (opts?: object) => Ajv2020Like;
const ajv2020 = new Ajv2020Ctor({
strict: false,
validateFormats: false,
validateSchema: false,
allErrors: true,
});
return {
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
if (!isDraft202012Schema(schema)) {
return defaultValidator.getValidator<T>(schema);
}
const ajvValidator = ajv2020.compile(schema);
return (input: unknown) => {
const valid = ajvValidator(input);
if (valid) {
return {
valid: true,
data: input as T,
errorMessage: undefined,
};
}
return {
valid: false,
data: undefined,
errorMessage: ajv2020.errorsText(ajvValidator.errors),
};
};
},
};
}
function connectWithTimeout(
client: Client,
@@ -178,7 +232,9 @@ export function createSessionMcpRuntime(params: {
name: "openclaw-bundle-mcp",
version: "0.0.0",
},
{},
{
jsonSchemaValidator: createBundleMcpJsonSchemaValidator(),
},
);
const session: BundleMcpSession = {
serverName,