Skip to content

Let apps declare read-only resources, not just tools (MCP resources/read) #7

Description

@V3RON

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/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.

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions