You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Everything an agent can see today must be a registered tool. Reading a piece of app state — "what's in the cart", "who's logged in", "which feature flags are on" — costs a tool registration, a schema, a slot in the agent's tool list, and a full tools/call round trip. MCP already has the right primitive for this (resources), and Cordierite already has the plumbing for exactly one of them.
Current state
The React Native public API (packages/react-native/src/public-api.ts) offers registerTool / useCordieriteTool and nothing else for exposing state.
The MCP server (packages/cordierite/src/mcp/server.ts) declares capabilities: { resources: {} } and serves a single hardcoded resource, cordierite://sessions, via ListResourcesRequestSchema / ReadResourceRequestSchema — a sessions.list passthrough. The handler pattern is already there; there is just nothing app-authored behind it.
The wire protocol (docs/PROTOCOL.md §4) has tool_registry_snapshot / tool_registry_delta and no equivalent for resources.
Harness support: resources alone are not enough
Checked before committing to a design, because it changes what we build:
Claude Code — full support, both directions. Resources are @-mentionable (@server:protocol://resource/path), and per the MCP docs, "Claude Code automatically provides tools to list and read MCP resources when servers support them" — so the model can read a resource on its own initiative, not only when a human attaches one.
Codex — no support. Tools only; prompts, resources and sampling are not implemented. There is also a reported failure mode where Codex uses resources/list to decide whether a server is available at all, so a tools-only server can be reported as not installed.
Cursor and several others have historically been tools-only as well.
So a resources-only implementation would be invisible in a large share of the harnesses this project targets.
Proposal
App side. Add registerResource({ uri, name, description, mimeType?, read }) and a useCordieriteResource hook mirroring useCordieriteTool (same disposer semantics, same enabled option, same registration-is-the-allowlist rule). read is an async function returning the content; no input schema, because a resource takes no arguments.
Wire. A resource registry alongside the tool registry: a resource_registry_snapshot / resource_registry_delta pair with the same strict-validation-or-close-the-socket rule the tool registry already follows, re-sent authoritatively after every resume. Reads travel as a resource_read / resource_result / resource_error triple mirroring tool_call / tool_result / tool_error, reusing the existing error types.
Daemon.resources.list / resources.read RPC methods with the usual selector semantics, and a resources_changed event kind.
MCP — expose the same registry twice. Both surfaces read from one registry; this is a mapping decision, not two implementations.
The real resource surface, for clients that support it: extend the two existing handlers so app-declared resources appear alongside cordierite://sessions, namespaced per session the way tools already are (cordierite://<alias>/<name>). Declare resources: { listChanged: true, subscribe: true }, emit notifications/resources/list_changed on resources_changed (the tool-list machinery in server.ts is a direct template), and — combined with App events never reach an agent: add daemon-side retention and a pull surface for app_event #6 — let a postEvent drive notifications/resources/updated so an agent knows a resource it read is now stale.
Two built-in tools, for everything else: cordierite_list_resources() and cordierite_read({ uri }), sitting next to cordierite_connect / cordierite_wait_for_session. This costs two tool slots in total rather than one per resource, so the "don't flood the tool list" motivation for the whole feature survives. Mark them readOnlyHint: true so the policy engine routes them through policy.default rather than policy.destructive.
CLI.cordierite resources [selector] and cordierite read [selector] <name>, for parity with tools / invoke.
Why this matters
Semantics. Reading state is not calling a function. Modelling it as a tool means every read shows up in the agent's tool list, competing for attention with the tools that actually do something.
Token cost. Agents handle a dozen resources far better than a dozen extra tools — resource descriptors are listed, but their schemas don't have to be reasoned over on every turn.
Policy. The policy engine (docs/ARCHITECTURE.md §12) keys on destructiveHint/readOnlyHint annotations. A resource is inherently read-only, so it can bypass the destructive path entirely and be safe to allow in configurations where tool calls are denied.
Open questions
Should resource reads be audited like tools.call is? Probably yes, with the same argsSha256-free record shape (there are no args).
Whether /noop needs registerResource in its parity surface — it does, per src/__tests__/noop-parity.test.ts's contract that both entries implement one shared interface (CordierePublicApi).
Resource templates (parameterized URIs) are a possible follow-up but shouldn't block the static case.
Summary
Everything an agent can see today must be a registered tool. Reading a piece of app state — "what's in the cart", "who's logged in", "which feature flags are on" — costs a tool registration, a schema, a slot in the agent's tool list, and a full
tools/callround trip. MCP already has the right primitive for this (resources), and Cordierite already has the plumbing for exactly one of them.Current state
packages/react-native/src/public-api.ts) offersregisterTool/useCordieriteTooland nothing else for exposing state.packages/cordierite/src/mcp/server.ts) declarescapabilities: { resources: {} }and serves a single hardcoded resource,cordierite://sessions, viaListResourcesRequestSchema/ReadResourceRequestSchema— asessions.listpassthrough. The handler pattern is already there; there is just nothing app-authored behind it.docs/PROTOCOL.md§4) hastool_registry_snapshot/tool_registry_deltaand no equivalent for resources.Harness support: resources alone are not enough
Checked before committing to a design, because it changes what we build:
@-mentionable (@server:protocol://resource/path), and per the MCP docs, "Claude Code automatically provides tools to list and read MCP resources when servers support them" — so the model can read a resource on its own initiative, not only when a human attaches one.resources/listto decide whether a server is available at all, so a tools-only server can be reported as not installed.So a resources-only implementation would be invisible in a large share of the harnesses this project targets.
Proposal
App side. Add
registerResource({ uri, name, description, mimeType?, read })and auseCordieriteResourcehook mirroringuseCordieriteTool(same disposer semantics, sameenabledoption, same registration-is-the-allowlist rule).readis an async function returning the content; no input schema, because a resource takes no arguments.Wire. A resource registry alongside the tool registry: a
resource_registry_snapshot/resource_registry_deltapair with the same strict-validation-or-close-the-socket rule the tool registry already follows, re-sent authoritatively after every resume. Reads travel as aresource_read/resource_result/resource_errortriple mirroringtool_call/tool_result/tool_error, reusing the existing error types.Daemon.
resources.list/resources.readRPC methods with the usual selector semantics, and aresources_changedevent kind.MCP — expose the same registry twice. Both surfaces read from one registry; this is a mapping decision, not two implementations.
cordierite://sessions, namespaced per session the way tools already are (cordierite://<alias>/<name>). Declareresources: { listChanged: true, subscribe: true }, emitnotifications/resources/list_changedonresources_changed(the tool-list machinery inserver.tsis a direct template), and — combined with App events never reach an agent: add daemon-side retention and a pull surface forapp_event#6 — let apostEventdrivenotifications/resources/updatedso an agent knows a resource it read is now stale.cordierite_list_resources()andcordierite_read({ uri }), sitting next tocordierite_connect/cordierite_wait_for_session. This costs two tool slots in total rather than one per resource, so the "don't flood the tool list" motivation for the whole feature survives. Mark themreadOnlyHint: trueso the policy engine routes them throughpolicy.defaultrather thanpolicy.destructive.CLI.
cordierite resources [selector]andcordierite read [selector] <name>, for parity withtools/invoke.Why this matters
docs/ARCHITECTURE.md§12) keys ondestructiveHint/readOnlyHintannotations. A resource is inherently read-only, so it can bypass the destructive path entirely and be safe to allow in configurations where tool calls are denied.Open questions
tools.callis? Probably yes, with the sameargsSha256-free record shape (there are no args)./noopneedsregisterResourcein its parity surface — it does, persrc/__tests__/noop-parity.test.ts's contract that both entries implement one shared interface (CordierePublicApi).