Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Browser lifecycle and scoped loading

Use createWebMcpRegistrationController from @elysra/tungsten-webmcp to connect page or route lifetimes to browser tools. It has no React or Vue dependency. Imports and controller construction are safe during SSR; without a supported backend, replace returns null before invoking the loader.

import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";

const tools = createWebMcpRegistrationController();
await tools.replace(async ({ signal }) => {
  const page = await import("./tool-scopes/pets.js");
  signal.throwIfAborted();
  return page.createScope();
});
// Before changing pages, replace with the next page's loader.
// For a page with no tools:
await tools.clear();
// When permanently retiring this controller:
await tools.dispose();

A scope factory returns { core, manifest, macros?, allowlist? }, with the same options as registerTungstenWebMcp except backend and signal. Optional result limits and streaming/binary policies can also be returned. Put imports of scoped manifests/descriptors and core construction inside the dynamically imported module and its factory. Importing a large catalog at application startup and filtering it inside the loader does not defer its download or construction. Avoid a module that re-exports every group; import the specific group's entry point. Check your bundler's chunk report to verify that scope modules are separate chunks. Dynamic import cancellation prevents registration; the browser may still finish downloading an import already in progress.

Generated WebMCP packages already provide scoped modules. Use the moduleKey listed in their catalogue's groups:

await tools.replace(async ({ signal }) => {
  const group = await import("my-api-webmcp/groups/public_pets");
  signal.throwIfAborted();
  return group.createScope({ baseUrl: new URL("/api", window.location.origin).href });
});

Each module includes its operation dependencies and reachable models. Its default registration exposes reads; add an explicit allowlist to the returned scope when the page needs a mutation or macro.

Keep one core for the entire active scope. Preview tokens and execution must use that same core/session. Return a newly constructed core on a fresh scope load; never construct a new core inside a tool's execution callback. Scope manifests should contain only the tools for that page, and their operations, macro steps, endpoint previews and verification targets must be registered with the core. Default registration exposes read-only operations. Add consequential operations or macros only through an explicit allowlist of manifest tool names; preview and confirmation requirements still apply.

replace immediately aborts the preceding scope's lifetime, cancels in-flight SDK calls, disposes owned tools, then loads the new scope. Concurrent loads do not block a newer route even if an old loader ignores its signal. Only the latest load may register. Superseded loads return null, including rejected obsolete imports; current import and registration failures reject. Browser registration and cleanup are serialized, so a delayed browser registration rolls back before the next scope registers. Cleanup never clears another owner's catalog. A failed legacy cleanup rejects and can be retried with clear() or dispose().

React

Pass a stable loader function, typically a route-to-loader map declared outside the component. This hook cancels the preceding route and supports React's additional development effect cleanup/setup cycle. clear() keeps the controller reusable; use dispose() when permanently retiring an application-owned controller.

import { useEffect, useMemo } from "react";
import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";
import type { WebMcpScopeLoader } from "@elysra/tungsten-webmcp";

export function usePageTools(loadScope: WebMcpScopeLoader) {
  const tools = useMemo(() => createWebMcpRegistrationController(), []);
  useEffect(() => {
    void tools.replace(loadScope).catch(reportToolError);
    return () => { void tools.clear().catch(reportToolError); };
  }, [tools, loadScope]);
}

function reportToolError(error: unknown) {
  console.error("Page tools could not be updated", error);
}

Call usePageTools(routeLoaders[routeName]) from the component that owns the active page. Do not register during render. An unsupported browser leaves the page's normal UI available without constructing its SDK core.

Vue

Watch the route after mount, and clear the same controller on unmount. Keep loaders in a stable map so a watch update represents a route change.

import { onBeforeUnmount, onMounted, watch } from "vue";
import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";
import type { WebMcpScopeLoader } from "@elysra/tungsten-webmcp";

export function usePageTools(
  activeRoute: () => string,
  loaders: Record<string, WebMcpScopeLoader>,
) {
  const tools = createWebMcpRegistrationController();
  let stop: (() => void) | undefined;
  const report = (error: unknown) => console.error("Page tools failed", error);
  onMounted(() => {
    stop = watch(activeRoute, route => {
      const loader = loaders[route];
      if (loader) void tools.replace(loader).catch(report);
      else void tools.clear().catch(report);
    }, { immediate: true });
  });
  onBeforeUnmount(() => {
    stop?.();
    void tools.dispose().catch(report);
  });
}

Authenticated same-origin transport

Use the application's authenticated same-origin API or backend-for-frontend. The browser session cookie remains managed by the browser. Do not embed service API keys or client secrets in a bundle, manifest, tool schema or loader.

// Pass these ClientOptions into the scope's ClientCore or generated client.
const origin = window.location.origin;
const clientOptions = {
  baseUrl: new URL("/api", origin).href,
  fetch: ((input, init) => {
    const url = new URL(input instanceof Request ? input.url : String(input), origin);
    if (url.origin !== origin) throw new Error("Expected a same-origin API URL");
    return window.fetch(input, { ...init, credentials: "same-origin" });
  }) satisfies typeof fetch,
};

Create these options inside the client-only loader or scope factory. Preserve init.signal and the SDK's headers when wrapping fetch so route cancellation, request diagnostics and idempotency continue to work. Have the application add its normal CSRF header for cookie-authenticated mutations; do not expose the CSRF value as a tool parameter. Backend authorization remains authoritative for every request, and an allowlist is a page exposure decision. After sign-out or a change of authenticated principal, clear the scope and load a fresh core for the new session. Cancellation of an in-flight mutation does not prove it had no effect; inspect server state before retrying.