Angular
No adapter needed. Register in a root-level service so it happens once, and use DestroyRef to abort on teardown.
Service
// src/app/webmcp.service.ts
import { Injectable, DestroyRef, inject, PLATFORM_ID } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { Router } from "@angular/router";
import contentstack from "@contentstack/delivery-sdk";
import { createContentstackWebMcp, isWebMcpSupported } from "@timbenniks/contentstack-webmcp";
import { environment } from "../environments/environment";
import { contentTypes } from "./content-types";
@Injectable({ providedIn: "root" })
export class WebMcpService {
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
async register(): Promise<string[]> {
// Guard for SSR / prerendering — there is no document on the server.
if (!this.isBrowser || !isWebMcpSupported()) return [];
const stack = contentstack.stack({
apiKey: environment.contentstackApiKey,
deliveryToken: environment.contentstackDeliveryToken,
environment: environment.contentstackEnvironment,
});
const webmcp = createContentstackWebMcp({ mode: "direct", stack, contentTypes });
const controller = new AbortController();
this.destroyRef.onDestroy(() => controller.abort());
return webmcp.register({
signal: controller.signal,
navigate: (path) => void this.router.navigateByUrl(path),
});
}
}Bootstrap it once
// src/app/app.config.ts
import { ApplicationConfig, provideAppInitializer, inject } from "@angular/core";
import { WebMcpService } from "./webmcp.service";
export const appConfig: ApplicationConfig = {
providers: [provideAppInitializer(() => inject(WebMcpService).register())],
};On Angular 18 or earlier, use the classic token instead:
{
provide: APP_INITIALIZER,
multi: true,
useFactory: (webmcp: WebMcpService) => () => webmcp.register(),
deps: [WebMcpService],
}TIP
router.navigateByUrl keeps navigate_to inside the Angular router, so guards and resolvers still run. location.assign would bypass all of it with a full reload.
Environment files
Angular has no public/private prefix convention — environment.ts is compiled into the bundle, so treat everything in it as public.
// src/environments/environment.ts
export const environment = {
production: true,
contentstackApiKey: "...",
contentstackDeliveryToken: "...", // read-only; direct mode only
contentstackEnvironment: "production",
};WARNING
Never put a management token in environment.ts. Use a read-only delivery token scoped to an environment holding only content you are happy to have public — or switch to proxy mode.
Proxy mode
Angular has no server runtime of its own unless you use Angular SSR, so host the handler wherever you serve the app — see Server runtimes. Then:
const webmcp = createContentstackWebMcp({
mode: "proxy",
proxyBasePath: "/api/contentstack",
contentTypes,
});With Angular SSR (@angular/ssr) the Express server it scaffolds can host the handler directly — see the Express section of Server runtimes.
Zone.js
Tool handlers run outside Angular's change detection. If a tool updates component state, wrap the update in NgZone.run() or use signals, which do not depend on zones.