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.