SDKsEdge MCP

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:

MemberPurpose
getResources()List registered resources
readResource(uri)Read one resource's current value
subscribeResource(uri, handler)Subscribe to changes
resourceupdated eventCustomEvent with detail = { uri, value }
resourcelistchanged eventFired 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

ParameterTypeRequiredDescription
uristringYesStable identity, mirroring MCP — state://cart. Registering the same uri again replaces the previous registration.
namestringYesShort logical name — cart.
descriptionstringNoWhat the value represents. The agent reads this to decide what the state means.
mimeTypestringNoMIME type of the value — application/json.
get() => T | Promise<T>YesReturns the current value. Called on every read and every push, so keep it cheap and side-effect-free.
subscribe(onChange) => () => voidNoThe 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 readsPushes the agent receives
A → A → A1
A → B → A3
A → B → B → C3

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.

Next steps

On this page