Skip to content

openApiMcpServer does not forward jsonSchemaValidator to the underlying McpServer; AJV-by-default breaks elicit accept-action under Workers runtime #1491

@victor-bajanov

Description

@victor-bajanov

Summary

@cloudflare/codemode's openApiMcpServer constructs new McpServer({ name, version }) with no options pass-through. The underlying @modelcontextprotocol/sdk Server then defaults to AjvJsonSchemaValidator, which calls new Function(...) to compile validators at runtime. The Cloudflare Workers runtime forbids new Function and eval ("Code generation from strings disallowed"). Any MCP server-initiated request that needs response validation against a requestedSchema — for example Server.elicitInput validating a user's accept-action payload — therefore throws the above error under Workers.

The MCP SDK already ships CfWorkerJsonSchemaValidator (using @cfworker/json-schema, an interpretive validator with no code generation) and accepts a jsonSchemaValidator option on the Server constructor. But openApiMcpServer consumers cannot opt in, because the option isn't part of the openApiMcpServer parameter surface.

As a result, using openApiMcpServer plus elicitation hits a runtime error the moment a user clicks "accept" on an elicit prompt. Decline works as it doesn't invoke any schema validation.

Affected versions

  • @cloudflare/codemode@0.3.4 — confirmed in production.
  • @modelcontextprotocol/sdk@1.29.0AjvJsonSchemaValidator is the default; CfWorkerJsonSchemaValidator is exported at @modelcontextprotocol/sdk/validation/cfworker but is not opt-in by default.
  • Workers runtime: any version that enforces the standard Code generation from strings disallowed security policy.

Prior relevant issues / PRs

  • cloudflare/agents issue Elicitation feature fails on Cloudflare Workers due to AJV code generation (EvalError: Code generation from strings disallowed) #424 ("Elicitation feature fails on Cloudflare Workers due to AJV code generation") — closed in tandem with the MCP SDK shipping CfWorkerJsonSchemaValidator as a callable opt-in. The closure resolves the SDK side; this issue is the follow-up for the codemode-specific surface gap (the option isn't reachable from openApiMcpServer callers).
  • modelcontextprotocol/typescript-sdk PR #1012 introduced CfWorkerJsonSchemaValidator and the jsonSchemaValidator constructor option as explicitly opt-in only ("AJV remains the default; users can opt in by declaring a jsonSchemaValidator").
  • Dependency-resolution-style workarounds suggested in some threads (overrides, resolutions) did not work for me as they introduced various dependency version mismatches I won't go into - in any case pinning dependency versions is not a durable solution.

Reproduction

Any openApiMcpServer({ spec, executor, request }) setup where the request callback can elicit user consent, deployed to Workers, with a client that clicks accept on the resulting elicit prompt. Workers logs include the AJV-generated validator source in the failure:

(error) Error compiling schema, function code: const schema0 = scope.schema[0];return function validate0(data, …){…}
McpError: MCP error -32603: Error validating elicitation response: Code generation from strings disallowed for this context
    at Server2.elicitInput (...sdk/server/index.js:...)

The "function code" preamble is the AJV-generated validator source the runtime refused to compile.

Runnable harness (attached)

elicit-als-context-codemode-pattern.tar.gz

elicit-als-context-codemode-pattern.tar.gz is attached. It reproduces both this validator-default bug and the companion ALS-context bug (opened as #1490) from the same harness via independent env toggles. Steps:

tar xzf elicit-als-context-codemode-pattern.tar.gz
cd elicit-als-context/codemode-pattern
pnpm install --ignore-workspace

# Trigger this validator bug:
WRAP=1 TRIGGER_VALIDATION=1 node verify.mjs

# Apply the workaround and observe the full happy path:
WRAP=1 TRIGGER_VALIDATION=1 VALIDATOR=cfworker node verify.mjs

Toggle reference (full matrix in the tarball's README):

WRAP TRIGGER_VALIDATION VALIDATOR Outcome
1 1 unset AJV codegen error (this bug)
1 1 cfworker tool-success — full accept happy path

TRIGGER_VALIDATION=1 makes the harness's mock client return {action:"accept", content:{confirm:"yes"}} rather than {action:"decline"} — i.e., it gives the SDK something to validate. The SDK currently requires result.action === 'accept' && result.content to enter validation, so decline skips the codepath entirely.

VALIDATOR=cfworker flips the harness host code to swap _jsonSchemaValidator post-construction, mirroring the workaround documented below.

Each invocation exits 0 if the observed behavior matches expectation. The tarball also ships per-scenario expected-output-*.txt captures for diffing.

(Trivia: unsetting WRAP will trigger "Agent was not found in send" from #1490)

Root cause

@cloudflare/codemode/dist/mcp.js (in published 0.3.4):

function openApiMcpServer(options) {
  const { executor, request: requestFn, name = "openapi", version = "1.0.0", /* ... */ } = options;
  const spec = options.spec;
  const server = new McpServer({
    name,
    version
    // ← no `jsonSchemaValidator` is passed through; no surface for the caller
    //   to provide one either.
  });
  // ...
}

@modelcontextprotocol/sdk/dist/esm/server/index.js:

this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();

Default is AJV; AJV uses new Function internally; Workers forbids new Function → error at validate time.

Workaround

After openApiMcpServer returns, replace the inner Server's _jsonSchemaValidator field directly:

import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";

const server = openApiMcpServer({ /* ... */ });
(server.server as unknown as { _jsonSchemaValidator: unknown })._jsonSchemaValidator =
  new CfWorkerJsonSchemaValidator();

This works but couples to a _-prefixed internal field of Server; any SDK change that renames the field will break this workaround.

Suggested fix

Forward jsonSchemaValidator (and any other ServerOptions we want to expose) through openApiMcpServer's parameter surface so callers can opt in to the SDK's already-shipped option:

function openApiMcpServer(options) {
  const { executor, request: requestFn, name = "openapi", version = "1.0.0",
          jsonSchemaValidator, /* ... */ } = options;
  const spec = options.spec;
  const server = new McpServer({ name, version, jsonSchemaValidator });
  // ...
}

Doesn't change defaults for any existing user so fully backwards-compatible, and gives Workers consumers a clean opt-in path. However, it is just a suggestion, I am very much new to this codebase and most of this investigation was done by Claude on my behalf.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmcpIssues related to MCP functionality

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions