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

# Quick Start

> Add AI agent support to your website in minutes with WebMCP. Turn JavaScript functions into AI-accessible tools using React hooks, vanilla JS, or script tags.

export const InteractiveQuickstart = () => {
  const {useState, useEffect, useRef, useMemo} = React;
  const highlightCode = code => {
    const patterns = [{
      regex: /(\/\/.*$)/gm,
      cls: 'text-zinc-500'
    }, {
      regex: /(`[^`]*`|'[^']*'|"[^"]*")/g,
      cls: 'text-green-400'
    }, {
      regex: /\b(import|export|from|async|await|function|const|let|var|return|if|else|try|catch|throw|new|class|extends|type|interface)\b/g,
      cls: 'text-purple-400'
    }, {
      regex: /\b(navigator|window|document|console|Promise|Object|Array|String|Number|Boolean)\b/g,
      cls: 'text-yellow-400'
    }, {
      regex: /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/g,
      cls: 'text-blue-400'
    }, {
      regex: /\b(\d+)\b/g,
      cls: 'text-orange-400'
    }];
    let result = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    patterns.forEach(({regex, cls}) => {
      result = result.replace(regex, `<span class="${cls}">$1</span>`);
    });
    return result;
  };
  const [isPolyfillLoaded, setIsPolyfillLoaded] = useState(false);
  const [isRegistered, setIsRegistered] = useState(false);
  const [isExecuting, setIsExecuting] = useState(false);
  const [showSuccessModal, setShowSuccessModal] = useState(false);
  const [testResult, setTestResult] = useState(null);
  const [registrationError, setRegistrationError] = useState(null);
  const [activeTab, setActiveTab] = useState('react');
  const [copied, setCopied] = useState(false);
  const containerRef = useRef(null);
  const registrationRef = useRef(null);
  const [toolConfig, setToolConfig] = useState({
    name: 'greet_user',
    description: 'Greets the user by name',
    parameters: [{
      name: 'name',
      type: 'string',
      description: 'Name to greet',
      required: true
    }]
  });
  const [testInput, setTestInput] = useState({
    name: 'World'
  });
  useEffect(() => {
    const checkPolyfill = () => {
      if (window.navigator?.modelContext) {
        setIsPolyfillLoaded(true);
      }
    };
    checkPolyfill();
    window.addEventListener('webmcp-loaded', checkPolyfill);
    return () => window.removeEventListener('webmcp-loaded', checkPolyfill);
  }, []);
  const generateCode = framework => {
    const paramName = toolConfig.parameters[0]?.name || 'input';
    const paramDesc = toolConfig.parameters[0]?.description || 'Input parameter';
    if (framework === 'react') {
      const zodSchema = toolConfig.parameters.map(p => `      ${p.name}: z.${p.type}()${p.required ? '' : '.optional()'}`).join(',\n');
      return `import '@mcp-b/global';
import { useWebMCP } from '@mcp-b/react-webmcp';
import { z } from 'zod';

function MyComponent() {
  useWebMCP({
    name: '${toolConfig.name}',
    description: '${toolConfig.description}',
    inputSchema: {
${zodSchema}
    },
    handler: async ({ ${paramName} }) => {
      return \`Hello, \${${paramName}}!\`;
    }
  });

  return <div>My Component</div>;
}`;
    }
    if (framework === 'vanilla') {
      const schemaProps = toolConfig.parameters.map(p => `        ${p.name}: { type: '${p.type}', description: '${p.description}' }`).join(',\n');
      const required = toolConfig.parameters.filter(p => p.required).map(p => `'${p.name}'`).join(', ');
      return `import '@mcp-b/global';

navigator.modelContext.registerTool({
  name: '${toolConfig.name}',
  description: '${toolConfig.description}',
  inputSchema: {
    type: 'object',
    properties: {
${schemaProps}
    },
    required: [${required}]
  },
  execute: async ({ ${paramName} }) => {
    return {
      content: [{ type: 'text', text: \`Hello, \${${paramName}}!\` }]
    };
  }
});`;
    }
    if (framework === 'script') {
      const schemaProps = toolConfig.parameters.map(p => `        ${p.name}: { type: '${p.type}', description: '${p.description}' }`).join(',\n');
      const required = toolConfig.parameters.filter(p => p.required).map(p => `'${p.name}'`).join(', ');
      return `<script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>
<script>
  navigator.modelContext.registerTool({
    name: '${toolConfig.name}',
    description: '${toolConfig.description}',
    inputSchema: {
      type: 'object',
      properties: {
${schemaProps}
      },
      required: [${required}]
    },
    execute: async ({ ${paramName} }) => {
      return {
        content: [{ type: 'text', text: \`Hello, \${${paramName}}!\` }]
      };
    }
  });
</script>`;
    }
  };
  const registerTool = async () => {
    if (!isPolyfillLoaded) {
      setRegistrationError('WebMCP polyfill not loaded');
      return;
    }
    if (registrationRef.current) {
      try {
        registrationRef.current.unregister?.();
      } catch (e) {}
    }
    setRegistrationError(null);
    setIsRegistered(false);
    setTestResult(null);
    try {
      const schema = {
        type: 'object',
        properties: {},
        required: []
      };
      toolConfig.parameters.forEach(p => {
        schema.properties[p.name] = {
          type: p.type,
          description: p.description
        };
        if (p.required) schema.required.push(p.name);
      });
      registrationRef.current = await window.navigator.modelContext.registerTool({
        name: toolConfig.name,
        description: toolConfig.description,
        inputSchema: schema,
        execute: async args => {
          setIsExecuting(true);
          setShowSuccessModal(true);
          const paramName = toolConfig.parameters[0]?.name || 'input';
          const result = `Hello, ${args[paramName] || 'friend'}!`;
          setTestResult({
            success: true,
            result,
            input: args[paramName]
          });
          setTimeout(() => {
            setIsExecuting(false);
            setShowSuccessModal(false);
          }, 3000);
          return {
            content: [{
              type: 'text',
              text: result
            }]
          };
        }
      });
      setIsRegistered(true);
    } catch (error) {
      setRegistrationError(error.message);
    }
  };
  const testTool = async () => {
    if (!isRegistered) return;
    setIsExecuting(true);
    setShowSuccessModal(true);
    setTestResult(null);
    const paramName = toolConfig.parameters[0]?.name || 'input';
    const inputValue = testInput[paramName] || 'friend';
    const result = `Hello, ${inputValue}!`;
    setTimeout(() => {
      setTestResult({
        success: true,
        result,
        input: inputValue
      });
    }, 500);
    setTimeout(() => {
      setIsExecuting(false);
      setShowSuccessModal(false);
    }, 2500);
  };
  const copyCode = () => {
    navigator.clipboard.writeText(generateCode(activeTab));
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  const updateParameter = (index, field, value) => {
    setToolConfig(prev => ({
      ...prev,
      parameters: prev.parameters.map((p, i) => i === index ? {
        ...p,
        [field]: value
      } : p)
    }));
    if (field === 'name') {
      const oldName = toolConfig.parameters[index].name;
      setTestInput(prev => {
        const newInput = {
          ...prev
        };
        if ((oldName in newInput)) {
          newInput[value] = newInput[oldName];
          delete newInput[oldName];
        }
        return newInput;
      });
    }
  };
  return <div ref={containerRef} className={`not-prose rounded-xl border overflow-hidden transition-all duration-300 relative ${isExecuting ? 'border-green-500 ring-2 ring-green-500/20' : 'border-zinc-200 dark:border-white/10'}`}>
      {}
      {showSuccessModal && <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
          <div className="bg-white dark:bg-zinc-800 rounded-2xl shadow-2xl p-8 mx-4 max-w-md w-full transform animate-bounce-in text-center">
            {}
            <div className="w-20 h-20 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
              <svg className="w-10 h-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
                <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" className="animate-draw-check" style={{
    strokeDasharray: 24,
    strokeDashoffset: 24,
    animation: 'drawCheck 0.5s ease forwards 0.2s'
  }} />
              </svg>
            </div>

            <h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-2">Tool Executed!</h3>

            {testResult ? <div className="space-y-3">
                <div className="p-3 rounded-lg bg-zinc-100 dark:bg-zinc-700 text-left">
                  <p className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Input:</p>
                  <p className="font-mono text-sm text-zinc-900 dark:text-zinc-100">
                    {toolConfig.parameters[0]?.name}: "{testResult.input}"
                  </p>
                </div>
                <div className="p-3 rounded-lg bg-green-100 dark:bg-green-900/30 text-left">
                  <p className="text-xs text-green-600 dark:text-green-400 mb-1">Output:</p>
                  <p className="font-mono text-sm text-green-700 dark:text-green-300">
                    {testResult.result}
                  </p>
                </div>
              </div> : <div className="flex items-center justify-center gap-2 text-zinc-600 dark:text-zinc-400">
                <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
                </svg>
                Processing...
              </div>}

            <p className="text-xs text-zinc-500 dark:text-zinc-400 mt-4">
              This is what happens when an AI calls your tool
            </p>
          </div>
        </div>}

      {}
      <style>{`
        @keyframes drawCheck {
          to {
            stroke-dashoffset: 0;
          }
        }
        @keyframes bounce-in {
          0% { transform: scale(0.3); opacity: 0; }
          50% { transform: scale(1.05); }
          70% { transform: scale(0.9); }
          100% { transform: scale(1); opacity: 1; }
        }
        .animate-bounce-in {
          animation: bounce-in 0.5s ease;
        }
        .scrollbar-hide {
          -ms-overflow-style: none;
          scrollbar-width: none;
        }
        .scrollbar-hide::-webkit-scrollbar {
          display: none;
        }
      `}</style>

      {}
      <div className="grid lg:grid-cols-2 divide-y lg:divide-y-0 lg:divide-x divide-zinc-200 dark:divide-white/10">
        {}
        <div className="p-5 space-y-4">
          <div className="flex items-center justify-between">
            <h4 className="font-semibold text-zinc-900 dark:text-white">Configure Your Tool</h4>
            {isPolyfillLoaded ? <span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
                <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
                WebMCP Ready
              </span> : <span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400">
                <span className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />
                Loading...
              </span>}
          </div>

          {}
          <div>
            <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
              Tool Name
            </label>
            <input type="text" value={toolConfig.name} onChange={e => setToolConfig(prev => ({
    ...prev,
    name: e.target.value.replace(/[^a-z0-9_]/gi, '_').toLowerCase()
  }))} className="w-full px-3 py-2 text-sm rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 font-mono focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="my_tool" />
            <p className="mt-1 text-xs text-zinc-500">
              Use snake_case (letters, numbers, underscores)
            </p>
          </div>

          {}
          <div>
            <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
              Description
            </label>
            <input type="text" value={toolConfig.description} onChange={e => setToolConfig(prev => ({
    ...prev,
    description: e.target.value
  }))} className="w-full px-3 py-2 text-sm rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="What does your tool do?" />
            <p className="mt-1 text-xs text-zinc-500">
              AI uses this to decide when to call your tool
            </p>
          </div>

          {}
          <div>
            <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
              Input Parameter
            </label>
            <div className="flex flex-col sm:flex-row gap-2">
              <input type="text" value={toolConfig.parameters[0]?.name || ''} onChange={e => updateParameter(0, 'name', e.target.value.replace(/[^a-z0-9_]/gi, ''))} placeholder="param_name" className="flex-1 px-3 py-2 text-sm rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 font-mono focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
              <select value={toolConfig.parameters[0]?.type || 'string'} onChange={e => updateParameter(0, 'type', e.target.value)} className="w-full sm:w-auto px-3 py-2 text-sm rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
                <option value="string">string</option>
                <option value="number">number</option>
                <option value="boolean">boolean</option>
              </select>
            </div>
          </div>

          {}
          <button onClick={registerTool} disabled={!isPolyfillLoaded || !toolConfig.name} className={`w-full py-2.5 px-4 rounded-md font-medium text-sm transition-all ${isRegistered ? 'bg-green-600 text-white' : 'bg-blue-600 hover:bg-blue-700 text-white disabled:bg-zinc-300 dark:disabled:bg-zinc-700 disabled:cursor-not-allowed'}`}>
            {isRegistered ? '✓ Tool Registered' : 'Register Tool'}
          </button>

          {registrationError && <p className="text-sm text-red-600 dark:text-red-400">{registrationError}</p>}

          {}
          {isRegistered && <div className="pt-4 border-t border-zinc-200 dark:border-zinc-700 space-y-3">
              <h5 className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
                Test Your Tool
              </h5>

              <div className="flex flex-col sm:flex-row gap-2">
                <input type="text" value={testInput[toolConfig.parameters[0]?.name] || ''} onChange={e => setTestInput({
    [toolConfig.parameters[0]?.name]: e.target.value
  })} placeholder={`Enter ${toolConfig.parameters[0]?.name || 'value'}...`} className="flex-1 px-3 py-2 text-sm rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
                <button onClick={testTool} disabled={isExecuting} className="w-full sm:w-auto px-4 py-2 text-sm font-medium rounded-md bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 transition-colors flex items-center justify-center gap-2">
                  {isExecuting ? <>
                      <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
                        <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                        <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
                      </svg>
                      Running
                    </> : <>
                      <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                      </svg>
                      Execute
                    </>}
                </button>
              </div>

              <div className="p-3 rounded-md bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
                <p className="text-xs text-blue-700 dark:text-blue-300">
                  <strong>Try it with AI:</strong> Open the{' '}
                  <a href="https://chromewebstore.google.com/detail/mcp-b-extension/daohopfhkdelnpemnhlekblhnikhdhfa" target="_blank" rel="noopener noreferrer" className="underline hover:no-underline font-medium">
                    MCP-B extension
                  </a>{' '}
                  and ask Claude to use your{' '}
                  <code className="px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-800 font-mono">
                    {toolConfig.name}
                  </code>{' '}
                  tool.
                </p>
              </div>
            </div>}
        </div>

        {}
        <div className="flex flex-col bg-zinc-900 min-h-[300px] sm:min-h-[400px]">
          {}
          <div className="flex flex-col sm:flex-row border-b border-zinc-700">
            <div className="flex overflow-x-auto scrollbar-hide">
              {[{
    id: 'react',
    label: 'React',
    icon: '⚛️'
  }, {
    id: 'vanilla',
    label: 'Vanilla JS',
    icon: '📦'
  }, {
    id: 'script',
    label: 'Script Tag',
    icon: '🏷️'
  }].map(tab => <button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`px-3 sm:px-4 py-2.5 text-sm font-medium transition-colors flex items-center gap-1.5 whitespace-nowrap ${activeTab === tab.id ? 'text-white bg-zinc-800 border-b-2 border-blue-500' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50'}`}>
                  <span className="text-base">{tab.icon}</span>
                  <span>{tab.label}</span>
                </button>)}
            </div>
            <div className="hidden sm:flex flex-1" />
            <button onClick={copyCode} className={`w-full sm:w-auto px-3 py-2 sm:py-1.5 sm:m-1.5 text-xs font-medium sm:rounded transition-all flex items-center justify-center gap-1.5 border-t sm:border-t-0 border-zinc-700 ${copied ? 'bg-green-600 text-white' : 'bg-zinc-800 sm:bg-zinc-700 hover:bg-zinc-600 text-zinc-300'}`}>
              {copied ? <>
                  <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                  </svg>
                  Copied!
                </> : <>
                  <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
                  </svg>
                  Copy Code
                </>}
            </button>
          </div>

          {}
          <div className="flex-1 overflow-auto">
            <pre className="p-4 text-sm leading-relaxed font-mono">
              <code className="text-zinc-100" dangerouslySetInnerHTML={{
    __html: highlightCode(generateCode(activeTab))
  }} />
            </pre>
          </div>

          {}
          <div className="px-4 py-2 border-t border-zinc-700 bg-zinc-800/50">
            <p className="text-xs text-zinc-500">
              {activeTab === 'react' && 'Requires: @mcp-b/global, @mcp-b/react-webmcp, zod'}
              {activeTab === 'vanilla' && 'Requires: @mcp-b/global (npm install)'}
              {activeTab === 'script' && 'No build tools required - just paste into HTML'}
            </p>
          </div>
        </div>
      </div>
    </div>;
};

This guide teaches you three approaches to adding tools to your site, starting with the simplest method and advancing to more powerful patterns. By the end, you'll understand which installation method fits your project.

## Try It Now

Build and test your first WebMCP tool right here - no setup required:

<InteractiveQuickstart />

***

## Prerequisites

Before you begin, ensure you have:

* **Modern browser**: Chrome, Edge, or Brave (latest version)
* **[MCP-B Extension](https://chromewebstore.google.com/detail/mcp-b-extension/daohopfhkdelnpemnhlekblhnikhdhfa)** installed from Chrome Web Store
* **Node.js 18+** (for NPM installation) or basic HTML knowledge (for script tag method)
* **Package manager**: npm, pnpm, or yarn (optional for NPM method)

<Note>
  **Early Incubation**: WebMCP is being incubated by the W3C Web Machine Learning Community Group. Not all MCP-B features may be included in the final WebMCP specification. See the [introduction](/introduction) for details.
</Note>

## Installation

<Tabs>
  <Tab title="Script Tag">
    ```html "index.html" lines icon="code" highlight={3-10} theme={null}
    <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>
    <script>
      // Register tools using the standard API
      navigator.modelContext.registerTool({
        name: "get_page_title",
        description: "Get current page title",
        inputSchema: { type: "object", properties: {} },
        async execute() {
          return { content: [{ type: "text", text: document.title }] };
        }
      });
    </script>
    ```
  </Tab>

  <Tab title="Vanilla JS">
    ```bash icon="node" theme={null}
    pnpm add @mcp-b/global
    ```

    ```javascript "tool-registration.js" lines icon="square-js" highlight={4-12} theme={null}
    import '@mcp-b/global';

    // Register individual tools
    const registration = navigator.modelContext.registerTool({
      name: "get_page_title",
      description: "Get current page title",
      inputSchema: { type: "object", properties: {} },
      async execute() {
        return {
          content: [{ type: "text", text: document.title }]
        };
      }
    });

    // Later, unregister if needed
    // registration.unregister();
    ```
  </Tab>

  <Tab title="React">
    ```bash icon="react" theme={null}
    pnpm add @mcp-b/react-webmcp @mcp-b/global zod
    ```

    ```tsx "MyComponent.tsx" twoslash lines icon="react" highlight={5-17} theme={null}
    import '@mcp-b/global';
    import { useWebMCP } from '@mcp-b/react-webmcp';
    import { z } from 'zod';

    function MyComponent() {
      useWebMCP({
        name: 'get_page_info',
        description: 'Get current page information',
        inputSchema: {
          includeUrl: z.boolean().optional()
        },
        handler: async ({ includeUrl }) => {
          return {
            title: document.title,
            ...(includeUrl && { url: window.location.href })
          };
        }
      });

      return <div>My Component</div>;
    }
    ```
  </Tab>
</Tabs>

## Real-World Example

Wrap your existing application logic as tools:

<Tabs>
  <Tab title="Vanilla JS">
    ```javascript "cart-tool.js" lines icon="square-js" highlight={1-23} theme={null}
    navigator.modelContext.registerTool({
      name: "add_to_cart",
      description: "Add item to shopping cart",
      inputSchema: {
        type: "object",
        properties: {
          productId: { type: "string" },
          quantity: { type: "number", minimum: 1 }
        },
        required: ["productId", "quantity"]
      },
      async execute({ productId, quantity }) {
        await fetch('/api/cart/add', {
          method: 'POST',
          credentials: 'same-origin',
          body: JSON.stringify({ productId, quantity })
        });
        return {
          content: [{ type: "text", text: `Added ${quantity} items` }]
        };
      }
    });
    ```
  </Tab>

  <Tab title="React">
    ```tsx "ProductPage.tsx" twoslash lines icon="react" highlight={5-18} theme={null}
    import { useWebMCP } from '@mcp-b/react-webmcp';
    import { z } from 'zod';

    function ProductPage() {
      useWebMCP({
        name: "add_to_cart",
        description: "Add item to shopping cart",
        inputSchema: {
          productId: z.string(),
          quantity: z.number().min(1)
        },
        handler: async ({ productId, quantity }) => {
          await fetch('/api/cart/add', {
            method: 'POST',
            credentials: 'same-origin',
            body: JSON.stringify({ productId, quantity })
          });
          return { success: true, quantity };
        }
      });

      return <div>Product Page</div>;
    }
    ```
  </Tab>
</Tabs>

## Testing

1. Run dev server: `pnpm dev`
2. Open in Chrome with MCP-B extension
3. Click extension icon → Tools tab
4. Test tools via chat or inspector

## Examples

<CardGroup cols={2}>
  <Card title="webmcp.sh" icon="play" href="https://github.com/WebMCP-org/webmcp-sh">
    Production-ready React app with database, navigation, and graph tools
  </Card>

  <Card title="Vanilla TypeScript" icon="js" href="https://github.com/WebMCP-org/examples/tree/main/vanilla">
    Shopping cart app with dynamic tool registration
  </Card>

  <Card title="React Task Manager" icon="react" href="https://github.com/WebMCP-org/examples/tree/main/react">
    Task management app demonstrating useWebMCP() hook
  </Card>

  <Card title="More Examples" icon="folder" href="/examples">
    Vue, Nuxt, React Flow, and community examples
  </Card>
</CardGroup>

## Quick Tips

* **Security**: Only expose tools users can already access via your UI. Tools inherit user authentication.
* **Naming**: Use descriptive names like `add_to_cart`, `search_products`, `update_profile`
* **Validation**: Always define input schemas - use Zod (React) or JSON Schema (vanilla)
* **Feedback**: Update UI state in handlers so users see what's happening

<Info>
  See [Best Practices](/best-practices) for comprehensive guidance on tool design and security.
</Info>

## Next Steps

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="diagram-project" href="/concepts/overview">
    Understand WebMCP architecture
  </Card>

  <Card title="Examples" icon="code" href="/examples">
    Complete working examples and patterns
  </Card>

  <Card title="React Hooks" icon="react" href="/packages/react-webmcp">
    @mcp-b/react-webmcp package documentation
  </Card>

  <Card title="Security Guide" icon="shield-halved" href="/security">
    Security best practices
  </Card>

  <Card title="NPM Packages" icon="box" href="/packages/global">
    All available packages and APIs
  </Card>

  <Card title="Advanced Patterns" icon="layer-group" href="/advanced">
    Chrome extensions, transports, and more
  </Card>
</CardGroup>
