Skip to content

feat: Reactive State Streaming via Resource Subscriptions #151

@KushagraAgarwal525

Description

@KushagraAgarwal525

Motivation

WebMCP currently exposes only tools. An agent that needs to track live application state - selections, filters, form state, document contents - has no mechanism other than calling get_* tools on every reasoning step. This polling approach has three concrete costs:

  • Token overhead: every poll is a full tool-call round-trip, consuming input + output tokens even when state is unchanged.
  • Temporal unsoundness: state read at step N may have changed by step N+1 when the agent acts on it. In WebMCP's primary use case - a human and agent working concurrently on the same page — the user is actively mutating state the agent depends on.
  • Unnecessary latency: the agent's action loop stalls waiting for a poll result it could have received proactively.

The underlying issue is architectural: the page owns reactive state but has no way to publish it; the agent must pull what it should be able to subscribe to.

Proposal: Resource Registration and Subscription

Extend ModelContext with a resources surface that mirrors the MCP Resources and Subscriptions primitive. WebMCP already aligns its tools surface with MCP tools; this extends that alignment to resources. The browser acts as the subscription broker, routing notifications/resources/updated to subscribed agents when the page calls notifyResourceUpdated().

Proposed Web IDL

partial interface ModelContext {
  undefined registerResource(ModelContextResource resource);
  undefined unregisterResource(DOMString uri);
  undefined notifyResourceUpdated(DOMString uri);
};

dictionary ModelContextResource {
  required DOMString uri;
  required DOMString name;
  DOMString description;
  DOMString mimeType;
  ModelContextResourceAnnotations annotations;
  required ResourceReadCallback read;
};

dictionary ModelContextResourceAnnotations {
  boolean subscribeHint = false;
};

// Return value follows the same content shape as MCP resources/read:
// { text: DOMString } | { blob: DOMString } — mirrors ResourceContents in MCP schema
callback ResourceReadCallback = Promise<object> ();

MCP Protocol Mapping

The browser bridge translates between the Web IDL surface and MCP protocol messages:

Web API call MCP message emitted by browser
Agent requests resource list resources/list → built from registered resource map
Agent reads a resource resources/read → invokes read callback, returns result
Agent subscribes resources/subscribe → stored in browser subscription table
notifyResourceUpdated(uri) notifications/resources/updated → fanned out to subscribers
unregisterResource(uri) notifications/resources/list_changed

Minimal Example

// Register a resource backed by existing page state
navigator.modelContext.registerResource({
  uri: "https://example.com/app/resources/selection",
  name: "Document Selection",
  description: "The user's current text selection and cursor position.",
  mimeType: "application/json",
  annotations: { subscribeHint: true },
  read: async () => ({ text: JSON.stringify(getSelectionState()) }),
});

// Page signals a state change — no polling, agent receives
// notifications/resources/updated and re-reads only if subscribed
document.addEventListener("selectionchange", () => {
  navigator.modelContext.notifyResourceUpdated(
    "https://example.com/app/resources/selection"
  );
});

Note the use of an https:// URI anchored to the page's own origin — see URI Scheme below.

Relationship to Existing Spec Work

  • MCP Resources spec (modelcontextprotocol.io): This proposal is a direct Web IDL surface for the MCP resources + subscriptions primitive. The browser's role mirrors that of an MCP server implementing resources/list, resources/read, and resources/subscribe.
  • Declarative WebMCP (PR Declarative API Explainer #76): Orthogonal and complementary. A declarative form could expose resources via a dedicated element or <meta> binding in a follow-up.
  • ModelContextClient.requestUserInteraction(): Complementary rather than overlapping - requestUserInteraction pauses the agent to collect user input; resource subscriptions push page-owned state to the agent proactively.

Design Questions

These are the open decisions that need group consensus before the shape of this API can be settled.

1. URI scheme for page-owned resources

app:// and similar custom schemes are not registered and create interoperability risk. Two options:

  • https:// at the page's own origin (e.g., https://example.com/app/resources/cart) - unambiguous origin binding, requires no new scheme, naturally enforces same-origin access.
  • A new web+ scheme (per WHATWG URL) - cleaner namespacing but requires registration and additional spec text.

Recommendation: https:// at the page origin, with the browser enforcing that registerResource URIs must be same-origin. Open to counter-arguments.

2. Read callback return type

Promise<any> is too loose. The return value should be constrained to match MCP ResourceContents - either { text: string } or { blob: string } (base64). Should the Web IDL define a ModelContextResourceContents dictionary, or rely on runtime validation with a thrown TypeError on malformed returns?

3. Subscription lifecycle and agent capability negotiation

MCP capability negotiation requires the server to declare resources: { subscribe: true } during initialization. In WebMCP, registerResource() is called at runtime, potentially after capability negotiation has already occurred. Two sub-questions:

  • Should the browser declare resources capability speculatively at init if the origin has previously called registerResource? Or lazily on first call?
  • When unregisterResource() removes the last resource, should the browser withdraw the capability?

4. Security model for subscriptions

Agent-initiated subscriptions raise questions absent from the current tool invocation model:

  • Does resources/subscribe require the same user-consent gate as tool invocations, or is it lower-friction given it is read-only?
  • readOnlyHint already exists on tools. subscribeHint on resources signals intent - but should the browser enforce read-only semantics on resources, or is this advisory only?
  • Cross-origin agents (e.g., a browser-embedded AI platform from a different origin than the page) - can they subscribe? The current spec has no cross-origin agent model; this proposal should not introduce one implicitly.

5. Notification debouncing and rate limiting

A page calling notifyResourceUpdated() in a tight loop (e.g., on mousemove) could flood an agent with notifications. Should the spec define any normative debouncing behavior on the browser side, or leave this entirely to the page author?

Prior Art and Alternatives Considered

Polling via tools - the status quo, which motivates this issue. See Problem above.

A generic event/push channel (e.g., a sendToAgent(event) API): more flexible but unstructured, no schema, no MCP alignment. Agents would need bespoke handling per page.

WebSockets or Server-Sent Events from a backend: moves state management off the page and requires a server round-trip for state that the page already owns. Contradicts WebMCP's premise of client-side MCP servers.

MCP Sampling (client/sampling): inverts the relationship - the server asks the model to reason, not the model subscribing to state. Different use case.

Resource Subscriptions are the minimal, MCP-aligned, additive primitive that addresses the problem without introducing a new communication model.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions