> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcp-b.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate with your framework

> Register WebMCP tools in React, Vue, Svelte, Angular, Next.js, Astro, or vanilla JS using framework-native lifecycle patterns.

Register WebMCP tools in any frontend framework by following its native lifecycle patterns. For cross-cutting patterns like conditional registration, error handling, and annotations, see [Add Tools to an Existing App](/how-to/add-tools-to-an-existing-app).

## Install the runtime

<Tabs>
  <Tab title="@mcp-b/global (recommended)">
    The full runtime: polyfill, MCP server, transports, prompts, resources, sampling, and elicitation.

    <CodeGroup>
      ```bash npm theme={null}
      npm install @mcp-b/global
      ```

      ```bash pnpm theme={null}
      pnpm add @mcp-b/global
      ```

      ```html "Script tag (no build step)" theme={null}
      <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>
      ```
    </CodeGroup>

    `@mcp-b/global` accesses browser APIs on import. If you use SSR, see [Handle SSR](#handle-ssr) for required guards.
  </Tab>

  <Tab title="@mcp-b/webmcp-polyfill (strict core)">
    Strict-core only: installs `document.modelContext` with `registerTool`, `getTools`, and `executeTool`. No MCP extensions, no transports.

    ```bash theme={null}
    npm install @mcp-b/webmcp-polyfill
    ```

    The polyfill checks for a browser environment internally and is SSR-safe. No client guards needed.

    For details on when to use the polyfill vs the full runtime, see [Choose a Runtime](/how-to/choose-runtime).
  </Tab>
</Tabs>

<Note>
  React users also need a hook package. Install `@mcp-b/react-webmcp` (recommended) or `usewebmcp`
  alongside your chosen runtime. See [Choose a hook package](#choose-a-react-hook-package) below.
</Note>

## Initialize at your entry point

Import `@mcp-b/global` once before any component mounts. The import is a side effect that installs `document.modelContext`.

<Tabs>
  <Tab title="React">
    ```tsx "src/main.tsx" theme={null}
    import '@mcp-b/global';
    import { createRoot } from 'react-dom/client';
    import { App } from './App';

    createRoot(document.getElementById('root')!).render(<App />);
    ```
  </Tab>

  <Tab title="Vue">
    ```typescript "src/main.ts" theme={null}
    import '@mcp-b/global';
    import { createApp } from 'vue';
    import App from './App.vue';

    createApp(App).mount('#app');
    ```
  </Tab>

  <Tab title="Svelte">
    ```typescript "src/main.ts" theme={null}
    import '@mcp-b/global';
    import App from './App.svelte';

    const app = new App({ target: document.getElementById('app')! });
    export default app;
    ```
  </Tab>

  <Tab title="Angular">
    ```typescript "src/main.ts" theme={null}
    import '@mcp-b/global';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { AppComponent } from './app/app.component';

    bootstrapApplication(AppComponent);
    ```
  </Tab>

  <Tab title="Next.js">
    Next.js App Router defaults to Server Components. Import `@mcp-b/global` in a Client Component layout, not the root layout.

    ```tsx "src/app/(dashboard)/layout.tsx" theme={null}
    'use client';

    import '@mcp-b/global';

    export default function DashboardLayout({ children }) {
      return <>{children}</>;
    }
    ```

    The polyfill is idempotent. If tools live in multiple sections, import it in each feature layout.

    <Warning>
      Do not make your root layout a Client Component. This disables SSR for your entire application.
    </Warning>
  </Tab>

  <Tab title="Astro">
    Import inside a `<script>` tag. Astro processes these through its bundler and runs them on the client.

    ```astro "src/pages/index.astro" theme={null}
    <script>
      import '@mcp-b/global';
    </script>
    ```

    Alternatively, load the IIFE in your layout's `<head>`:

    ```astro "src/layouts/Layout.astro" theme={null}
    <head>
      <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>
    </head>
    ```
  </Tab>

  <Tab title="Vanilla JS">
    ```typescript "src/main.ts" theme={null}
    import '@mcp-b/global';
    ```

    Or without a build step:

    ```html "index.html" theme={null}
    <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>
    ```
  </Tab>
</Tabs>

## Register a tool

Each framework has its own lifecycle hooks for mount and unmount. Register tools on mount, unregister on unmount.

<Tabs>
  <Tab title="React">
    <a id="choose-a-react-hook-package" />

    Two hook packages are available:

    | Package                                                     | Use when                                                                                |
    | ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
    | [**@mcp-b/react-webmcp**](/packages/react-webmcp/reference) | You want the full MCP-B surface: Zod schemas, prompts, resources, sampling, elicitation |
    | [**usewebmcp**](/packages/usewebmcp/reference)              | You want strict-core `document.modelContext` tools only                                 |

    Both handle registration on mount and cleanup on unmount automatically.

    ```tsx "src/components/LikeTool.tsx" theme={null}
    import { useWebMCP } from '@mcp-b/react-webmcp';
    import { z } from 'zod';

    export function LikeTool() {
      const likeTool = useWebMCP({
        name: 'posts_like',
        description: 'Like a post by ID. Increments the like count.',
        inputSchema: {
          postId: z.string().uuid().describe('The post ID to like'),
        },
        annotations: {
          title: 'Like Post',
          readOnlyHint: false,
          idempotentHint: true,
        },
        handler: async (input) => {
          await api.posts.like(input.postId);
          return { success: true, postId: input.postId };
        },
      });

      return (
        <div>
          {likeTool.state.isExecuting && <p>Liking...</p>}
          {likeTool.state.error && <p>Error: {likeTool.state.error.message}</p>}
        </div>
      );
    }
    ```
  </Tab>

  <Tab title="Vue">
    Call `registerTool()` in `onMounted` and abort the registration signal in `onUnmounted`. The `execute` function can read and write Vue reactive state through `.value` access.

    ```vue "src/components/CounterTool.vue" theme={null}
    <script setup lang="ts">
    import { ref, onMounted, onUnmounted } from 'vue';

    const count = ref(0);
    let controller: AbortController | undefined;

    onMounted(() => {
      controller = new AbortController();
      document.modelContext.registerTool(
        {
          name: 'increment',
          description: 'Increment the counter by a given amount',
          inputSchema: {
            type: 'object',
            properties: {
              amount: { type: 'number', description: 'Amount to add' },
            },
          },
          async execute({ amount = 1 }) {
            count.value += amount as number;
            return {
              content: [{ type: 'text', text: `Count: ${count.value}` }],
            };
          },
        },
        { signal: controller.signal }
      );
    });

    onUnmounted(() => {
      controller?.abort();
    });
    </script>

    <template>
      <p>Count: {{ count }}</p>
    </template>
    ```
  </Tab>

  <Tab title="Svelte">
    Use `onMount` to register and `onDestroy` to unregister. Svelte 5 runes work in the execute handler.

    ```svelte "src/lib/CounterTool.svelte" theme={null}
    <script lang="ts">
      import { onMount, onDestroy } from 'svelte';

      let count = $state(0);
      let controller: AbortController | undefined;

      onMount(() => {
        controller = new AbortController();
        document.modelContext.registerTool(
          {
            name: 'increment',
            description: 'Increment the counter by a given amount',
            inputSchema: {
              type: 'object',
              properties: {
                amount: { type: 'number', description: 'Amount to add' },
              },
            },
            async execute({ amount = 1 }) {
              count += amount as number;
              return {
                content: [{ type: 'text', text: `Count: ${count}` }],
              };
            },
          },
          { signal: controller.signal }
        );
      });

      onDestroy(() => {
        controller?.abort();
      });
    </script>

    <p>Count: {count}</p>
    ```
  </Tab>

  <Tab title="Angular">
    Use `ngOnInit` to register and `ngOnDestroy` to abort the registration signal.

    ```typescript "src/app/counter.component.ts" theme={null}
    import { Component, OnInit, OnDestroy } from '@angular/core';
    import '@mcp-b/global';

    @Component({
      selector: 'app-counter',
      template: `<p>Count: {{ count }}</p>`,
    })
    export class CounterComponent implements OnInit, OnDestroy {
      count = 0;
      private toolController?: AbortController;

      ngOnInit() {
        if (!('modelContext' in navigator)) return;

        this.toolController = new AbortController();
        document.modelContext.registerTool(
          {
            name: 'increment',
            description: 'Increment the counter by a given amount',
            inputSchema: {
              type: 'object',
              properties: {
                amount: { type: 'number', description: 'Amount to add' },
              },
            },
            execute: async ({ amount = 1 }) => {
              this.count += amount as number;
              return {
                content: [{ type: 'text', text: `Count: ${this.count}` }],
              };
            },
          },
          { signal: this.toolController.signal }
        );
      }

      ngOnDestroy() {
        this.toolController?.abort();
      }
    }
    ```
  </Tab>

  <Tab title="Next.js">
    Mark tool components with `'use client'` and use the same React hooks.

    ```tsx "src/components/dashboard-tools.tsx" theme={null}
    'use client';

    import { useWebMCP } from '@mcp-b/react-webmcp';
    import { z } from 'zod';

    export function DashboardTools() {
      useWebMCP({
        name: 'get_metrics',
        description: 'Get dashboard metrics for a date range',
        inputSchema: {
          startDate: z.string().describe('ISO date string'),
          endDate: z.string().describe('ISO date string'),
        },
        handler: async ({ startDate, endDate }) => {
          const res = await fetch(`/api/metrics?start=${startDate}&end=${endDate}`);
          return await res.json();
        },
      });

      return null;
    }
    ```
  </Tab>

  <Tab title="Astro">
    Register tools inside a `<script>` tag. Astro bundles and deduplicates these scripts automatically.

    ```astro "src/pages/index.astro" theme={null}
    <script>
      import '@mcp-b/global';

      const controller = new AbortController();
      document.modelContext.registerTool(
        {
          name: 'get_page_title',
          description: 'Get the current page title',
          inputSchema: { type: 'object', properties: {} },
          async execute() {
            return {
              content: [{ type: 'text', text: document.title }],
            };
          },
        },
        { signal: controller.signal }
      );
    </script>
    ```

    If you use View Transitions, abort the registration before navigation:

    ```javascript theme={null}
    document.addEventListener('astro:before-preparation', () => {
      controller.abort();
    });
    ```
  </Tab>

  <Tab title="Vanilla JS">
    Call `registerTool()` directly after importing the runtime.

    ```typescript "src/main.ts" theme={null}
    import '@mcp-b/global';

    const controller = new AbortController();

    document.modelContext.registerTool(
      {
        name: 'get-page-title',
        description: 'Get the current page title',
        inputSchema: { type: 'object', properties: {} },
        async execute() {
          return {
            content: [{ type: 'text', text: document.title }],
          };
        },
      },
      { signal: controller.signal }
    );
    ```

    Abort the registration when the tool is no longer needed:

    ```typescript theme={null}
    controller.abort();
    ```
  </Tab>
</Tabs>

## Create a reusable abstraction

React already has dedicated hook packages (`@mcp-b/react-webmcp` and `usewebmcp`), so no custom abstraction is needed. For other frameworks, extract the register/abort lifecycle into a reusable pattern.

<Tabs>
  <Tab title="Vue composable">
    ```typescript "src/composables/useWebMCPTool.ts" theme={null}
    import { onMounted, onUnmounted } from 'vue';

    export function useWebMCPTool(
      tool: Parameters<typeof document.modelContext.registerTool>[0]
    ) {
      let controller: AbortController | undefined;

      onMounted(() => {
        controller = new AbortController();
        document.modelContext.registerTool(tool, { signal: controller.signal });
      });

      onUnmounted(() => {
        controller?.abort();
      });
    }
    ```

    ```vue "src/components/GreetingTool.vue" theme={null}
    <script setup lang="ts">
    import { useWebMCPTool } from '@/composables/useWebMCPTool';

    useWebMCPTool({
      name: 'get_greeting',
      description: 'Get a greeting message',
      inputSchema: { type: 'object', properties: {} },
      async execute() {
        return { content: [{ type: 'text', text: 'Hello from Vue!' }] };
      },
    });
    </script>
    ```
  </Tab>

  <Tab title="Svelte action">
    ```typescript "src/lib/actions/webmcp.ts" theme={null}
    export function webmcpTool(
      node: HTMLElement,
      tool: Parameters<typeof document.modelContext.registerTool>[0]
    ) {
      const controller = new AbortController();
      document.modelContext.registerTool(tool, { signal: controller.signal });

      return {
        destroy() {
          controller.abort();
        },
      };
    }
    ```

    ```svelte "src/routes/+page.svelte" theme={null}
    <script lang="ts">
      import { webmcpTool } from '$lib/actions/webmcp';
    </script>

    <div use:webmcpTool={{
      name: 'get_greeting',
      description: 'Get a greeting message',
      inputSchema: { type: 'object', properties: {} },
      execute: async () => ({
        content: [{ type: 'text', text: 'Hello from Svelte!' }],
      }),
    }}>
      Content here
    </div>
    ```
  </Tab>

  <Tab title="Angular service">
    ```typescript "src/app/webmcp.service.ts" theme={null}
    import { Injectable, OnDestroy } from '@angular/core';
    import '@mcp-b/global';

    @Injectable({ providedIn: 'root' })
    export class WebMCPService implements OnDestroy {
      private controllers = new Map<string, AbortController>();

      registerTool(tool: Parameters<typeof document.modelContext.registerTool>[0]) {
        if (!('modelContext' in navigator)) return;

        this.controllers.get(tool.name)?.abort();

        const controller = new AbortController();
        this.controllers.set(tool.name, controller);
        document.modelContext.registerTool(tool, { signal: controller.signal });
      }

      removeTool(name: string) {
        this.controllers.get(name)?.abort();
        this.controllers.delete(name);
      }

      ngOnDestroy() {
        for (const controller of this.controllers.values()) {
          controller.abort();
        }
        this.controllers.clear();
      }
    }
    ```

    ```typescript "src/app/greeting.component.ts" theme={null}
    import { Component, OnInit, OnDestroy } from '@angular/core';
    import { WebMCPService } from './webmcp.service';

    @Component({
      selector: 'app-greeting',
      template: `<p>Greeting tool registered</p>`,
    })
    export class GreetingComponent implements OnInit, OnDestroy {
      constructor(private webmcp: WebMCPService) {}

      ngOnInit() {
        this.webmcp.registerTool({
          name: 'get_greeting',
          description: 'Get a greeting message',
          inputSchema: { type: 'object', properties: {} },
          execute: async () => ({
            content: [{ type: 'text', text: 'Hello from Angular!' }],
          }),
        });
      }

      ngOnDestroy() {
        this.webmcp.removeTool('get_greeting');
      }
    }
    ```
  </Tab>
</Tabs>

## Handle SSR

`@mcp-b/global` accesses browser APIs on import, so SSR frameworks need client-side guards. If you use `@mcp-b/webmcp-polyfill` instead, it is SSR-safe out of the box and these guards are not needed.

<Tabs>
  <Tab title="Next.js">
    Mark components with `'use client'`. For components that access `window` or `document` directly, use dynamic imports:

    ```tsx theme={null}
    import dynamic from 'next/dynamic';
    const BrowserOnly = dynamic(() => import('./BrowserOnly'), { ssr: false });
    ```
  </Tab>

  <Tab title="Nuxt / Vue SSR">
    In Nuxt, guard with `import.meta.client`:

    ```vue "pages/index.vue" theme={null}
    <script setup lang="ts">
    import { onMounted, onUnmounted } from 'vue';

    let controller: AbortController | undefined;

    onMounted(() => {
      if (!import.meta.client) return;

      import('@mcp-b/global').then(() => {
        controller = new AbortController();
        document.modelContext.registerTool(
          {
            name: 'my_tool',
            description: 'A tool that only runs on the client',
            inputSchema: { type: 'object', properties: {} },
            async execute() {
              return { content: [{ type: 'text', text: 'Done' }] };
            },
          },
          { signal: controller.signal }
        );
      });
    });

    onUnmounted(() => {
      controller?.abort();
    });
    </script>
    ```
  </Tab>

  <Tab title="SvelteKit">
    Guard with `browser` from `$app/environment`:

    ```svelte "src/routes/+page.svelte" theme={null}
    <script lang="ts">
      import { browser } from '$app/environment';
      import { onMount, onDestroy } from 'svelte';
      import '@mcp-b/global';

      let controller: AbortController | undefined;

      onMount(() => {
        if (!browser) return;

        controller = new AbortController();
        document.modelContext.registerTool(
          {
            name: 'my_tool',
            description: 'A tool that only runs on the client',
            inputSchema: { type: 'object', properties: {} },
            async execute() {
              return { content: [{ type: 'text', text: 'Done' }] };
            },
          },
          { signal: controller.signal }
        );
      });

      onDestroy(() => {
        controller?.abort();
      });
    </script>
    ```

    To persist tools across route navigations in SvelteKit, register them in `+layout.svelte` instead of `+page.svelte`.
  </Tab>

  <Tab title="Angular Universal">
    Guard with `isPlatformBrowser`:

    ```typescript "src/app/my.component.ts" theme={null}
    import { Component, OnInit, OnDestroy, PLATFORM_ID, Inject } from '@angular/core';
    import { isPlatformBrowser } from '@angular/common';
    import '@mcp-b/global';

    @Component({ selector: 'app-my', template: '' })
    export class MyComponent implements OnInit, OnDestroy {
      private isBrowser: boolean;
      private controller?: AbortController;

      constructor(@Inject(PLATFORM_ID) platformId: object) {
        this.isBrowser = isPlatformBrowser(platformId);
      }

      ngOnInit() {
        if (!this.isBrowser || !('modelContext' in navigator)) return;

        this.controller = new AbortController();
        document.modelContext.registerTool(
          {
            name: 'my_tool',
            description: 'A tool that only runs on the client',
            inputSchema: { type: 'object', properties: {} },
            execute: async () => ({
              content: [{ type: 'text', text: 'Done' }],
            }),
          },
          { signal: this.controller.signal }
        );
      }

      ngOnDestroy() {
        this.controller?.abort();
      }
    }
    ```
  </Tab>

  <Tab title="Astro">
    Astro renders pages as static HTML by default. Code inside `<script>` tags runs on the client only, so no guard is needed for standard Astro pages. For framework islands, use `client:load` or `client:only`:

    ```astro "src/pages/dashboard.astro" theme={null}
    ---
    import DashboardTools from '../components/DashboardTools';
    ---

    <DashboardTools client:load />
    ```
  </Tab>
</Tabs>

## Verify registration

Open the browser console and run:

```javascript theme={null}
navigator.modelContextTesting?.listTools();
```

This returns an array of all registered tools with their names, descriptions, and input schemas. For a richer inspection experience, see [Debug and Troubleshoot](/how-to/debug-and-troubleshoot).
