Skip to content

Resources

A resource is a named piece of app state the agent can read - and optionally subscribe to for push updates. Resources complement actions: actions cause changes, resources expose what changed.

Agent reads once, then subscribes. When the app's value changes, the SDK pushes a resources/updated notification. AGENT MCP GATEWAY RESOURCE HANDLER 1 resources/read tesseron://shop/route 2 resources/read 3 { value: '/cart' } 4 read result 5 resources/subscribe 6 resources/subscribe register emit() callback 7 resources/updated { value: '/checkout' } 8 notifications/resources/updated
Agent reads once, then subscribes. When the app's value changes, the SDK pushes a resources/updated notification.
tesseron.resource('currentRoute')
.describe('The URL path the user is currently viewing')
.read(() => window.location.pathname)
.subscribe((emit) => {
const onChange = () => emit(window.location.pathname);
window.addEventListener('popstate', onChange);
return () => window.removeEventListener('popstate', onChange);
});
  • .read() is a one-shot getter. Called on every resources/read the agent issues.
  • .subscribe() is optional. It registers an emitter; return an unsubscribe function so the SDK can clean up when the agent unsubscribes or the session closes.

Resources are exposed to the agent with the URI tesseron://<app_id>/<resource_name>. For app.id = "shop" and resource = "currentRoute", the agent sees tesseron://shop/currentRoute.

Reading from clients that don't speak MCP resources

Section titled “Reading from clients that don't speak MCP resources”

Some MCP clients don't surface resources/read to their model. The MCP gateway ships a meta-tool fallback:

  • tesseron__read_resource ({ app_id, name }) - returns the resource's current value as a tool-call result. Prefer this over the generic ReadMcpResourceTool because the agent doesn't have to know how the MCP server is namespaced on the client (e.g. plugin:tesseron:tesseron in Claude Code plugin installs vs. tesseron in a raw config).

tesseron__list_actions enumerates every claimed session's resources and includes both the preferred tesseron__read_resource args and the ReadMcpResourceTool fallback.

{ "jsonrpc": "2.0", "id": 14, "method": "resources/read", "params": { "name": "currentRoute" } }

Response:

{ "jsonrpc": "2.0", "id": 14, "result": { "value": "/checkout" } }
{ "jsonrpc": "2.0", "id": 15, "method": "resources/subscribe", "params": { "name": "currentRoute", "subscriptionId": "sub_1" } }

Response is empty - the SDK just acknowledges and now holds the emitter callback.

Each time the emitter fires:

{
"jsonrpc": "2.0",
"method": "resources/updated",
"params": { "subscriptionId": "sub_1", "value": "/cart" }
}

The gateway forwards this as MCP notifications/resources/updated to the agent.

{ "jsonrpc": "2.0", "id": 16, "method": "resources/unsubscribe", "params": { "subscriptionId": "sub_1" } }

The SDK calls the unsubscribe function returned by your .subscribe() handler.

List changed (app → gateway, notification)

Section titled “List changed (app → gateway, notification)”

If your app registers or removes resources after the initial tesseron/hello, the SDK emits resources/list_changed with the new manifest. The gateway forwards this as MCP notifications/resources/list_changed so agents can refetch the list. actions/list_changed follows the same pattern for dynamic action sets.

tesseron.resource('filterState').read(() => ({
search: state.search,
onlyDone: state.onlyDone,
}));

Perfect for letting the agent reason about "what's the user currently looking at" before proposing actions.

Don't emit on every keystroke - the agent can't meaningfully react at that rate.

tesseron.resource('search')
.read(() => state.search)
.subscribe((emit) => {
let timer: ReturnType<typeof setTimeout> | null = null;
const onChange = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => emit(state.search), 250);
};
state.on('change', onChange);
return () => { if (timer) clearTimeout(timer); state.off('change', onChange); };
});

If the value is expensive to produce, remember that .read() runs every time the agent fetches. Cache inside the handler, or use .subscribe() as the source of truth and cache the latest emitted value in-memory.

Subscriptions require agentCapabilities.subscriptions. Reads do not. If the agent can't subscribe, it will only call resources/read and your .subscribe() handler is never invoked.

Next: the full error catalog and capability negotiation.