Summary
On 2.0.0-beta.4 self-host, an MCP app fails tool discovery with AppEvaluationFailed ("Executor could not load this app's tool definitions") when its account has a large OAuth grant. The real cause is CacheError { reason: "capacity" } from the cache-key size check. cache.forAccount(account) puts the account's full credential fields into the cache scope, and every key is canonicalJson([scope, key]), capped at keyBytes: 8192.
Cloudflare's MCP OAuth (https://mcp.cloudflare.com/mcp) advertises 384 scopes (~7.7 KB). The stored grant is just under the limit, so the short …/current key passes and the first catalog page write (…/<revision>/tool/<name>) goes over.
Reproduction
The app is the generated MCP app for Cloudflare:
export default defineApp({ accounts: { service: provider.many() } }, async ({ accounts, signal, cache }) =>
accountOperations(accounts.service, async (account) => mcpOperations({
url: "https://mcp.cloudflare.com/mcp?codemode=false",
cache: cache.forAccount(account),
accountId: account.id,
headers: { Authorization: "Bearer " + account.fields.access_token },
signal,
}), { signal }),
)
- Connect a Cloudflare account through OAuth (
oauth2({ discover: "https://mcp.cloudflare.com/mcp" })).
- Open the Tools page, or run
tools.search over MCP.
- Result:
app.inspect fails with HostEvaluationFailed, which surfaces as AppEvaluationFailed. No catalog rows are written; only a released lease row appears in the app's executor_cache.
It also fails without ?codemode=false (3 tools), so catalog size is not the cause.
Evidence:
- Wrapping the cache passed to
mcpOperations shows the first write (64 entries, ~64 KB) rejected with CacheError capacity. The stack points at the key-size check in the app-side cache client (vi/Gs in node_modules/apps/chunk-3JO5LYEW.js: if (n.byteLength > St.keyBytes) return yield* new ce({ reason: "capacity" })).
- In a standalone workerd harness running the real
apps/main.js runtime and DO cache, credential fields around 7.7 KB pass and fields around 8.1 KB fail with the same HostEvaluationFailed.
- The same app with
cache (shared scope) instead of cache.forAccount(account) loads all 3,595 tools and calls work.
Expected
- Cache scopes should not embed credential fields. Use the account ID, or a digest of the account and credentials, so key size doesn't depend on grant size and secrets don't enter key material.
- A
CacheError during inspect should keep its identity (or at least reach telemetry/logs). Today cn in chunk-3JO5LYEW.js collapses it into HostEvaluationFailed, and the self-host logs and motel spans show no cause.
Workaround
Pass cache instead of cache.forAccount(account) for MCP apps. The mcp-catalog-v2 prefix already fingerprints url, headers (including the token) and accountId, so entries stay per account.
Environment
ghcr.io/usefulsoftwareco/executor-selfhost:beta, EXECUTOR_BUILD_VERSION=2.0.0-beta.4, workerd 2026-09-01
- Provider: Cloudflare MCP OAuth (384 advertised scopes)
🤖 Generated with Claude Code
Summary
On
2.0.0-beta.4self-host, an MCP app fails tool discovery withAppEvaluationFailed("Executor could not load this app's tool definitions") when its account has a large OAuth grant. The real cause isCacheError { reason: "capacity" }from the cache-key size check.cache.forAccount(account)puts the account's full credentialfieldsinto the cache scope, and every key iscanonicalJson([scope, key]), capped atkeyBytes: 8192.Cloudflare's MCP OAuth (
https://mcp.cloudflare.com/mcp) advertises 384 scopes (~7.7 KB). The stored grant is just under the limit, so the short…/currentkey passes and the first catalog page write (…/<revision>/tool/<name>) goes over.Reproduction
The app is the generated MCP app for Cloudflare:
oauth2({ discover: "https://mcp.cloudflare.com/mcp" })).tools.searchover MCP.app.inspectfails withHostEvaluationFailed, which surfaces asAppEvaluationFailed. No catalog rows are written; only a released lease row appears in the app'sexecutor_cache.It also fails without
?codemode=false(3 tools), so catalog size is not the cause.Evidence:
mcpOperationsshows the firstwrite(64 entries, ~64 KB) rejected withCacheError capacity. The stack points at the key-size check in the app-side cache client (vi/Gsinnode_modules/apps/chunk-3JO5LYEW.js:if (n.byteLength > St.keyBytes) return yield* new ce({ reason: "capacity" })).apps/main.jsruntime and DO cache, credential fields around 7.7 KB pass and fields around 8.1 KB fail with the sameHostEvaluationFailed.cache(shared scope) instead ofcache.forAccount(account)loads all 3,595 tools and calls work.Expected
CacheErrorduringinspectshould keep its identity (or at least reach telemetry/logs). Todaycninchunk-3JO5LYEW.jscollapses it intoHostEvaluationFailed, and the self-host logs and motel spans show no cause.Workaround
Pass
cacheinstead ofcache.forAccount(account)for MCP apps. Themcp-catalog-v2prefix already fingerprintsurl,headers(including the token) andaccountId, so entries stay per account.Environment
ghcr.io/usefulsoftwareco/executor-selfhost:beta,EXECUTOR_BUILD_VERSION=2.0.0-beta.4, workerd2026-09-01🤖 Generated with Claude Code