CedarCopilot wants to merge 0 commits into staging from feat/mcp-surface-fixes
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
A review of Cedar's external MCP surface (775 tool calls) found the dominant cost is repeated reads, not the 8 hard failures , e.g. one session loaded the drafting skill 8Ć in ~2.5 min, and re-fetched the same conversation 4Ć in 39s. This PR addresses both the repetition and the validation failures.
The external MCP server is built fresh per HTTP request (stateless mode , routes/external-mcp.ts sessionIdGenerator: undefined), so a client model that calls the same read tool several times over one conversation issues each as a separate request against a separate server. A closure cache can't span calls; only a process-level cache can , so this uses one, mirroring the existing getUserContext idiom (module-level Map, per-entry TTL, no cross-request invalidation), identity-scoped by the impersonation-effective userId.
load-skill , 5 min TTL keyed by user+skill+resource+scope. Skill instructions are per-user but don't change between calls seconds apart; 5 min self-heals after a playbook edit.conversation-read get , 20 s TTL keyed by user+get-params, successful reads only. The short TTL is a deliberate tradeoff: the surface is multi-instance (ECS) with no cross-instance write-invalidation, so TTL is the only staleness bound , 20 s collapses the sub-minute re-read bursts while keeping live CRM reads at most ~20 s stale. Only get is ever cached, so no write path serves stale data through the cache.mutation.type as an alias for the operation discriminator, and copy a top-level conversationId down into the branch that needs it (normalizeMutationInput via z.preprocess). Fixes 2 of the 8 failures.document write type: defaults to "document" instead of being required.draftCategory: an unrecognized value (e.g. productQuestions) is dropped, not rejected (.catch(undefined)) , it's an optional tag, never load-bearing.All three publish unchanged JSON schemas (preprocess/default/catch unwrap), so connected clients need no reload.
recent-executions rows now carry a truncated failureReason for failed runs, so triage no longer needs an execution-detail drill-in per failure (the report saw 33 such follow-ups).user_request_summary stays required. It's required by design (tool-call-observability.ts) precisely so the model fills it , a miss is a self-correcting validation error, not a real failure. Defaulting it would gut the observability signal.debug.health-report already returns success (DebugOutputSchema), so the reported "add success" item is already satisfied , no change.22 new tests (cache TTL/eviction, normalizeMutationInput + preprocess wiring, truncateFailureReason, document-type default, draftCategory tolerance). All 434 tests across the touched modules pass; typecheck clean.
š¤ Generated with Claude Code
This PR reduces repeated external-MCP reads and makes several tool inputs more tolerant.
The PR should not merge until both external-MCP cache keys distinguish the effective organization and cannot return content authorized for a user's previous tenant.
Cache hits bypass the current organization-aware execution paths, while the keys retain only the
Libra has not measured any production surfaces for this change yet.