different-ai/openwork · error · ExternalMcpDiagnosticError

MCP_CATALOG_CURSOR_SIZE_LIMIT

MCP_CATALOG_CURSOR_SIZE_LIMIT

Error message

MCP_CATALOG_CURSOR_SIZE_LIMIT

What it means

Thrown when a tools/list page returns a nextCursor whose serialized UTF-8 size exceeds EXTERNAL_MCP_CURSOR_LIMIT_BYTES (16 KiB). Cursors are re-sent by the gateway on subsequent calls and counted against the catalog byte budget, so oversized cursors are rejected.

Source

Thrown at ee/apps/den-api/src/capability-sources/external-mcp-client.ts:715

      })
      if (seenToolNames.has(tool.name)) {
        throw catalogDiagnosticError({
          tracker: input.diagnostic,
          code: "MCP_CATALOG_DUPLICATE_TOOL",
          operatorAction: "Ensure every tools/list page uses a unique, stable tool name.",
        })
      }
      seenToolNames.add(tool.name)
      tools.push(tool)
    }
    if (!result.nextCursor) {
      input.diagnostic.passed("MCP_TOOL_DISCOVERY", "catalog_ready")
      return tools
    }
    if (serializedStringBytes(result.nextCursor) > EXTERNAL_MCP_CURSOR_LIMIT_BYTES) {
      throw catalogDiagnosticError({
        tracker: input.diagnostic,
        code: "MCP_CATALOG_CURSOR_SIZE_LIMIT",
        operatorAction: `Reduce each serialized tools/list cursor below ${EXTERNAL_MCP_CURSOR_LIMIT_BYTES} UTF-8 bytes.`,
      })
    }
    const cursorMeasurement = measureSerializedJson(
      result.nextCursor,
      EXTERNAL_MCP_CATALOG_LIMIT_BYTES - catalogBytes,
      1,
    )
    if (!cursorMeasurement.ok) {
      throw catalogDiagnosticError({
        tracker: input.diagnostic,
        code: "MCP_CATALOG_BYTE_LIMIT",
        operatorAction: `Reduce the complete serialized tool catalog below ${EXTERNAL_MCP_CATALOG_LIMIT_BYTES} bytes.`,
      })
    }
    catalogBytes += cursorMeasurement.bytes
    if (seenCursors.has(result.nextCursor)) {
      throw catalogDiagnosticError({

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Fix the provider to issue compact opaque cursors (e.g. an offset or short token)
  2. Move pagination state server-side and return a short handle as the cursor
  3. Reduce per-page page size so cursor state stays small
  4. If you cannot change the provider, use a scoped server with a small, simple result set

Example fix

// before
cursor = base64(JSON.stringify({ allRemainingToolIds }))
// after
cursor = "p:3" // server-side page pointer
Defensive patterns

Strategy: validation

Validate before calling

if (nextCursor !== undefined && new TextEncoder().encode(nextCursor).byteLength > 16384) {
  throw new Error("nextCursor exceeds 16KB UTF-8 limit")
}

Type guard

function hasCompactCursor(page: { nextCursor?: string }): boolean {
  return page.nextCursor === undefined || new TextEncoder().encode(page.nextCursor).byteLength <= 16384
}

Try / catch

try {
  await connectExternalMcp(...)
} catch (err) {
  if (err instanceof Error && err.message.includes("MCP_CATALOG_CURSOR_SIZE_LIMIT")) {
    console.error("Provider cursors too large; return short opaque tokens instead")
  } else throw err
}

Prevention

When it happens

Trigger: After a successful page fetch (when result.nextCursor exists), serializedStringBytes(result.nextCursor) > 16384 bytes.

Common situations: Providers embedding the entire remaining result set or a giant opaque state blob in the cursor; base64 of full response snapshots; ORM cursors serializing huge filter state.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/ef0a2688452d9e1e. Report an issue: GitHub.