Skip to content

@cloudflare/codemode: runtime dispatch rejects raw hyphenated tool names — asymmetric with registration-side sanitization #1457

@bkalytta-wq

Description

@bkalytta-wq

Describe the bug

@cloudflare/codemode's DynamicWorkerExecutor sanitizes provider tool names at registration (sanitizeToolName(name) — hyphens/dots become underscores), but the sandbox proxy passes the raw lookup key straight through to the dispatcher. As a result, callers that obtain the tool name from MCP discovery (raw form) and dispatch with bracket notation get Tool "foo-bar" not found, even though the same provider works when addressed via the sanitized identifier. The asymmetry remains after #879 and #1117 — those PRs introduced and internalized sanitization on the registration side, and closed #806, but the runtime dispatch path was not made symmetric.

To Reproduce

Steps to reproduce the behavior:

  1. Register a provider with a hyphenated tool name:
    const fns = { "github-list-issues": async () => [{ id: 1 }] };
  2. Dispatch from sandbox code using the raw discovery form:
    await executor.execute(
      `async () => await codemode["github-list-issues"]({})`,
      [{ name: "codemode", fns }]
    );
  3. See error: Tool "github-list-issues" not found.

The same code works if the lookup key is swapped to the sanitized identifier (codemode.github_list_issues({})), even though the provider key is the raw form in both cases.

Expected behavior

Both shapes should resolve to the same provider function:

  • raw, the form returned by MCP discovery — codemode["github-list-issues"]({})
  • sanitized, the form generated by the type-gen / LLM path — codemode.github_list_issues({})

Screenshots

N/A — programmatic test failure, reproducible via vitest-pool-workers.

Version:

@cloudflare/codemode 0.3.4 (current main, after #1117).

Additional context

Root cause

packages/codemode/src/executor.ts (lines 316–328 on current main):

for (const [name, fn] of Object.entries(provider.fns)) {
  sanitizedFns[sanitizeToolName(name)] = fn;
}

Combined with the proxy template at lines ~256–278 that calls __dispatchers.${p.name}.call(String(toolName), ...) with the raw toolName from the Proxy get trap, the dispatcher only finds the sanitized key.

Suggested fix

Register each tool under both its raw name and its sanitized identifier when they differ. Two added lines, no new dependencies, no behaviour change for existing callers using the sanitized form:

for (const [name, fn] of Object.entries(provider.fns)) {
  const sanitized = sanitizeToolName(name);
  sanitizedFns[sanitized] = fn;
  if (sanitized !== name) sanitizedFns[name] = fn;
}

Three new regression tests added in packages/codemode/src/tests/executor.test.ts covering raw-hyphen dispatch, raw-dot dispatch, and the same provider being reachable under both shapes. The existing should sanitize tool names with hyphens and dots test was also tightened — it now uses a raw provider key, so the test actually exercises the registration-time sanitization that its name advertises.

Impact

Any client that builds codemode dispatch programmatically from MCP tools/list output uses the raw names (hyphens preserved). The current behaviour silently routes only via the sanitized identifier, which works for type-generated AI code but breaks for hand-written / discovery-driven dispatch — including MCP clients that hand discovery results into a codemode portal, and integration tests asserting on the discovery name.

Workaround

Until merged: dispatch only via the sanitized identifier (codemode.github_list_issues({})). Clients that hand raw discovery names into codemode can normalize names client-side: name.replace(/[-.\s]/g, "_").

PR

Follows shortly from bkalytta-wq:fix/codemode-hyphen-dispatch-asymmetry.

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