Resources
What the agent can SEE — live state slices pushed to the agent as they change
Tools let the agent act; resources let it see. A resource is a live slice of your app's state — the page the visitor is on, the cart contents — that the agent receives the moment it changes, without calling anything:
import { registerResource } from '@napster-corp/edge-mcp';
registerResource({
uri: 'state://cart',
name: 'cart',
description: 'The current shopping cart',
mimeType: 'application/json',
get: () => cartStore.getCurrent(),
subscribe: (onChange) => cartStore.subscribe(onChange), // return an unsubscribe fn
});With this, the agent always knows what's in the cart — when the user adds an item by hand, the agent sees it and can react ("I see you added the OLED — want a wall mount to go with it?").
The consumer surface
Installed on document.modelContext by the package's import side effect:
| Member | Purpose |
|---|---|
getResources() | List registered resources |
readResource(uri) | Read one resource's current value |
subscribeResource(uri, handler) | Subscribe to changes |
resourceupdated event | CustomEvent with detail = { uri, value } |
resourcelistchanged event | Fired when the resource list changes |
Resources are a Napster extension: WebMCP hasn't formalized resources yet, so this channel is consumed by the Napster agent over its own path and is not interoperable with third-party WebMCP agents — unlike the tool surface, which is fully standard today. When the standard lands resources, this API is designed to converge with it.
The resource descriptor
| Parameter | Type | Required | Description |
|---|---|---|---|
uri | string | Yes | Stable identity, mirroring MCP — state://cart. Registering the same uri again replaces the previous registration. |
name | string | Yes | Short logical name — cart. |
description | string | No | What the value represents. The agent reads this to decide what the state means. |
mimeType | string | No | MIME type of the value — application/json. |
get | () => T | Promise<T> | Yes | Returns the current value. Called on every read and every push, so keep it cheap and side-effect-free. |
subscribe | (onChange) => () => void | No | The push source. Call onChange when your state changes, and return an unsubscribe function. Omit it to make the resource pull-only. |
Don't pass the new value through onChange — it takes no arguments. get is the value, subscribe is only the when: on every onChange, the extension re-reads get() itself.
registerResource returns an unregister function. Calling it tears down the push subscription and removes the resource from the list.
When something earns a resource
Register a resource only for state that changes without the agent acting — the user navigating, clicking, typing; other tabs; server pushes. If the only way a value changes is through one of the agent's own tools, the tool's return value already tells it — a resource there is pure noise. This is the resource gate; apply it before every registerResource.
Typical resources that pass the gate: the current page (pushed on navigation), the cart (user can modify it by hand), a form's draft state. Typical rejects: a search-results list only the agent's search tool produces.
Behavior and guarantees
How the extension treats your get and subscribe. These rules decide what you'll see when you debug a resource that seems to update too often — or not at all — and what you can assert in a test.
Reads are always live
readResource(uri) calls your get() at the moment of the read and returns what it returns. Nothing is cached: no stored copy, no TTL, no staleness window. Two reads in a row call get() twice.
That makes get() a sound place to instrument reads in a test — every read goes through it. One caveat if you count calls: pushes also call get() (that's how the extension learns the new value), so a naive counter registers pushes as well as reads.
Identical consecutive pushes are filtered
When your subscribe callback fires, the extension re-reads get() and compares the result against the last value it dispatched for that URI. If they serialize identically, the push is dropped — so a store that fires several times per operation (pending → value → pending) doesn't spam the agent with a value that never changed.
The comparison is against the immediately previous dispatched value only — it is not a history:
| Values your resource reads | Pushes the agent receives |
|---|---|
| A → A → A | 1 |
| A → B → A | 3 |
| A → B → B → C | 3 |
So "three user actions must produce three updates" only holds if each action genuinely changes the value. When an expected push doesn't arrive, turn on debug mode — a filtered push logs push suppressed (value unchanged).
If a value can't be serialized (circular references, BigInt), the extension fails open: it dispatches the push and drops the stored baseline, so a comparison it can't make never silences a real change.
subscribe must return an unsubscribe function
The extension calls it to disconnect from your store — on unregister, and before a re-registration. If your subscribe returns nothing, the subscription can never be torn down and it leaks; you'll get a console warning saying so. Return the teardown, or omit subscribe entirely to make the resource pull-only.
Re-registering a URI starts fresh
Registering the same uri again replaces the descriptor: the previous subscribe's teardown runs first, and the comparison baseline is cleared. The first push after a re-registration therefore always dispatches, even if the value is unchanged — worth knowing when a hot reload re-runs your registrations.