Skip to main content

Dynamic Tool Registration

Conditional Tools Based on User State

Register tools based on authentication, permissions, or user roles:
import { useWebMCP } from '@mcp-b/react-webmcp';
import { useAuth } from './auth';
import { z } from 'zod';

function AdminPanel() {
  const { user } = useAuth();

  // Only register admin tools if user is admin
  useWebMCP({
    name: 'delete_user',
    description: 'Delete a user account (admin only)',
    inputSchema: {
      userId: z.string().uuid()
    },
    handler: async ({ userId }) => {
      if (!user?.isAdmin) {
        throw new Error('Unauthorized');
      }
      await api.users.delete(userId);
      return { success: true };
    },
    // Only register if user is admin
    enabled: user?.isAdmin
  });

  return <div>Admin Panel</div>;
}

Page-Specific Tools

Register tools based on the current route - tools appear and disappear as users navigate:
function ProductPage({ productId }) {
  useWebMCP({
    name: 'add_to_cart',
    description: 'Add this product to cart',
    inputSchema: { quantity: z.number().min(1).default(1) },
    handler: async ({ quantity }) => {
      await addToCart(productId, quantity);
      return { success: true };
    }
  });

  return <div>Product Page</div>;
}

Component Lifecycle Tool Scoping

Tools automatically register when mounted and unregister when unmounted:
function CartTools({ cartId, items }) {
  useWebMCP({
    name: 'checkout',
    description: 'Proceed to checkout',
    inputSchema: { paymentMethod: z.enum(['credit', 'debit', 'paypal']) },
    handler: async ({ paymentMethod }) => {
      const order = await checkoutService.processCart(cartId, paymentMethod);
      return { orderId: order.id };
    },
    enabled: items.length > 0 // Only available when cart has items
  });

  return null;
}
This creates a UI for LLMs - instead of showing all tools at once, you progressively reveal capabilities based on context.

Context Engineering Patterns

Context engineering is the practice of giving AI models only the tools and information relevant to their current task. Just as good UI design doesn’t overwhelm users with every possible option, good WebMCP design limits tool availability based on context.
The principle: If you give a model 100 tools when only 5 are relevant, performance suffers. Think of it like giving someone an entire Home Depot when they just need a saw, hammer, and nails.

URL-Based Tool Scoping

Expose different tools based on the current URL path:
function ToolProvider() {
  const { pathname } = useLocation();

  if (pathname === '/') {
    useWebMCP({
      name: 'search_products',
      inputSchema: { query: z.string(), category: z.string().optional() },
      handler: async ({ query, category }) => await searchAPI.search({ query, category })
    });
  }

  if (pathname.startsWith('/product/')) {
    useWebMCP({
      name: 'add_to_cart',
      inputSchema: { quantity: z.number().min(1).default(1) },
      handler: async ({ quantity }) => await cartAPI.add(productId, quantity)
    });
  }

  if (pathname === '/checkout') {
    useWebMCP({
      name: 'complete_checkout',
      inputSchema: { paymentMethod: z.enum(['credit', 'debit', 'paypal']) },
      handler: async ({ paymentMethod }) => await checkoutAPI.complete(paymentMethod)
    });
  }

  return null;
}

Progressive Tool Disclosure

Reveal tools as the user progresses through a workflow: Each stage only shows tools relevant to that step, reducing cognitive load on the AI model.

Role-Based Tool Exposure

Different tools for different user roles:
function RoleBasedTools({ user }) {
  // Customer tools (available to everyone)
  useWebMCP({
    name: 'view_products',
    description: 'View product catalog',
    handler: async () => await productAPI.list()
  });

  // Moderator tools
  if (user?.role === 'moderator' || user?.role === 'admin') {
    useWebMCP({
      name: 'hide_comment',
      description: 'Hide inappropriate comments',
      inputSchema: {
        commentId: z.string()
      },
      handler: async ({ commentId }) => {
        return await moderationAPI.hideComment(commentId);
      }
    });
  }

  // Admin-only tools
  if (user?.role === 'admin') {
    useWebMCP({
      name: 'delete_user',
      description: 'Permanently delete a user account',
      inputSchema: {
        userId: z.string()
      },
      handler: async ({ userId }) => {
        return await adminAPI.deleteUser(userId);
      }
    });

    useWebMCP({
      name: 'update_product_price',
      description: 'Update product pricing',
      inputSchema: {
        productId: z.string(),
        newPrice: z.number().positive()
      },
      handler: async ({ productId, newPrice }) => {
        return await adminAPI.updatePrice(productId, newPrice);
      }
    });
  }

  return null;
}

Feature Flag-Based Tools

Control tool availability with feature flags:
import { useFeatureFlag } from './feature-flags';

function FeatureGatedTools() {
  const hasAIRecommendations = useFeatureFlag('ai-recommendations');
  const hasBetaCheckout = useFeatureFlag('beta-checkout');

  useWebMCP({
    name: 'get_ai_recommendations',
    description: 'Get AI-powered product recommendations',
    handler: async () => {
      return await recommendationAPI.get();
    },
    enabled: hasAIRecommendations
  });

  useWebMCP({
    name: 'beta_one_click_checkout',
    description: 'Complete purchase with one click (beta)',
    handler: async () => {
      return await betaCheckoutAPI.oneClick();
    },
    enabled: hasBetaCheckout
  });

  return null;
}

State Synchronization

React State Integration

Tools can access and update React state:
function Counter() {
  const [count, setCount] = useState(0);

  useWebMCP({
    name: 'increment',
    inputSchema: { amount: z.number().default(1) },
    handler: async ({ amount }) => {
      setCount(prev => prev + amount);
      return { newValue: count + amount };
    }
  });

  return <div>Count: {count}</div>;
}

Context API Integration

Share tools across the component tree using React Context:
function AppProvider({ children }) {
  const [state, setState] = useState({});

  useWebMCP({
    name: 'update_settings',
    inputSchema: { theme: z.enum(['light', 'dark']).optional() },
    handler: async (settings) => {
      setState(prev => ({ ...prev, ...settings }));
      return { success: true };
    }
  });

  return <AppContext.Provider value={{ state }}>{children}</AppContext.Provider>;
}

Advanced Validation

Complex Input Validation

Zod supports sophisticated validation patterns:
useWebMCP({
  name: 'create_event',
  inputSchema: {
    title: z.string().min(1).max(100),
    startDate: z.string().datetime(),
    attendees: z.array(z.string().email()).min(1).max(50),
    recurrence: z.object({
      frequency: z.enum(['daily', 'weekly', 'monthly']),
      interval: z.number().min(1).max(365)
    }).optional()
  },
  handler: async (event) => await api.events.create(event)
});

Custom Validation Logic

Add business logic validation in your handler:
useWebMCP({
  name: 'transfer_funds',
  inputSchema: {
    fromAccount: z.string(),
    toAccount: z.string(),
    amount: z.number().positive()
  },
  handler: async ({ fromAccount, toAccount, amount }) => {
    const balance = await getAccountBalance(fromAccount);
    if (balance < amount) throw new Error(`Insufficient funds`);
    if (fromAccount === toAccount) throw new Error('Same account transfer');

    await api.transfer({ fromAccount, toAccount, amount });
    return { success: true };
  }
});

Error Handling

Handle errors gracefully and provide clear feedback:
useWebMCP({
  name: 'process_order',
  inputSchema: { orderId: z.string() },
  handler: async ({ orderId }) => {
    try {
      return await api.orders.process(orderId);
    } catch (error) {
      if (error.code === 'PAYMENT_FAILED') {
        return { success: false, error: 'Payment failed' };
      }
      throw error;
    }
  },
  onError: (error, input) => analytics.trackError('process_order_failed', { orderId: input.orderId })
});

Performance Optimization

Use debouncing, throttling, and caching for expensive operations:
// Debounce search requests
const debouncedSearch = useMemo(() => debounce(api.search, 300), []);

useWebMCP({
  name: 'search',
  inputSchema: { query: z.string().min(2) },
  handler: async ({ query }) => await debouncedSearch(query)
});

// Cache results
const cache = useRef(new Map());
useWebMCP({
  name: 'get_data',
  inputSchema: { id: z.string() },
  handler: async ({ id }) => {
    if (cache.current.has(id)) return cache.current.get(id);
    const data = await api.get(id);
    cache.current.set(id, data);
    return data;
  }
});

Multi-Tab Tool Collection

The MCP-B Extension collects and maintains tools from all open tabs simultaneously, making them available to agents regardless of which tab is currently active.
Key behavior: Unlike traditional tab-scoped approaches, the extension aggregates tools from every open tab, giving agents access to your entire browsing context at once.

How Tool Collection Works

When you have multiple tabs open with WebMCP servers:

Tool Routing

When an agent calls a tool:
1

Agent requests tool

Agent calls a tool (e.g., github_com_create_issue)
2

Extension identifies source

Extension determines which tab owns that tool
3

Extension routes call

If the tab is open, the tool is executed immediately. If the tab was closed, the extension may need to reopen it.
4

Results returned

Tool execution results are returned to the agent

Design Implications

Since all tools from all tabs are visible to the agent:
Use descriptive, namespaced tool names to avoid confusion:
// Good - clear which site/feature
useWebMCP({
  name: 'github_create_issue',
  description: 'Create a new GitHub issue',
  // ...
});

// Avoid - ambiguous
useWebMCP({
  name: 'create',
  description: 'Create something',
  // ...
});
Since agents see all tools at once, make descriptions specific:
// Good - describes what and where
useWebMCP({
  name: 'add_to_cart',
  description: 'Add the currently viewed product from shop.example.com to your shopping cart',
  // ...
});

// Avoid - lacks context
useWebMCP({
  name: 'add_to_cart',
  description: 'Add item to cart',
  // ...
});
Tools appear and disappear as you open/close tabs:
// This tool is available whenever the product page tab is open
function ProductPage() {
  useWebMCP({
    name: 'get_current_product',
    description: 'Get details about the currently viewed product',
    handler: async () => {
      return await fetchProductDetails(productId);
    }
  });

  return <div>Product Details</div>;
}

// When user closes this tab, get_current_product disappears from the toolkit
Agents can naturally compose tools from different tabs:
// Tab 1: Calendar app
useWebMCP({
  name: 'calendar_add_event',
  description: 'Add event to calendar',
  inputSchema: {
    title: z.string(),
    date: z.string().datetime()
  },
  handler: async ({ title, date }) => {
    return await calendar.addEvent({ title, date });
  }
});

// Tab 2: Email app
useWebMCP({
  name: 'email_get_unread',
  description: 'Get unread emails',
  handler: async () => {
    return await email.getUnread();
  }
});

// Agent can: "Get my unread emails and add any meeting invites to my calendar"
// It sees and can use tools from both tabs

Managing Tool Overload

When many tabs are open, agents see many tools. Use context engineering patterns to help:
// Add metadata to help agents understand when to use tools
useWebMCP({
  name: 'shop_checkout',
  description: 'Complete checkout on shop.example.com. Only use this after items have been added to cart and user has confirmed.',
  handler: async () => {
    return await processCheckout();
  }
});

// Or conditionally register tools based on state
function CartPage() {
  const { items } = useCart();

  // Only show checkout tool when cart has items
  useWebMCP({
    name: 'shop_checkout',
    description: 'Complete the checkout process',
    handler: async () => {
      return await processCheckout();
    },
    enabled: items.length > 0
  });

  return <div>Cart</div>;
}
See Context Engineering Patterns for more strategies to limit tool availability based on application state.

Multi-Step Workflows

Stateful Multi-Step Operations

function CheckoutFlow() {
  const [checkoutState, setCheckoutState] = useState({
    step: 'cart',
    cart: [],
    shipping: null,
    payment: null
  });

  useWebMCP({
    name: 'checkout_next_step',
    description: 'Proceed to next checkout step',
    inputSchema: {
      data: z.record(z.any())
    },
    handler: async ({ data }) => {
      const { step } = checkoutState;

      if (step === 'cart') {
        setCheckoutState(prev => ({
          ...prev,
          step: 'shipping',
          shipping: data
        }));
        return { nextStep: 'shipping' };
      }

      if (step === 'shipping') {
        setCheckoutState(prev => ({
          ...prev,
          step: 'payment',
          payment: data
        }));
        return { nextStep: 'payment' };
      }

      if (step === 'payment') {
        const order = await processOrder(checkoutState);
        setCheckoutState({ step: 'complete', orderId: order.id });
        return { complete: true, orderId: order.id };
      }
    }
  });

  useWebMCP({
    name: 'get_checkout_state',
    description: 'Get current checkout progress',
    handler: async () => checkoutState
  });

  return <div>Checkout Flow</div>;
}

Cross-Site Tool Composition

One of the most powerful features of the MCP-B Extension is the ability to compose tools from different websites. Tools from one site can use data from another site, enabling complex multi-site workflows.
Key advantage: Each site exposes its existing functionality as MCP tools, and the extension handles routing calls between them. The user maintains separate authentication contexts for each site.

How Cross-Site Calls Work

Example: Cross-Site Tool Composition

Site A exposes cart data:
useWebMCP({
  name: 'get_current_cart',
  handler: async () => ({ items: cart.items, total: cart.total })
});
Site B offers price comparison:
useWebMCP({
  name: 'compare_prices',
  inputSchema: { productName: z.string() },
  handler: async ({ productName }) => {
    const response = await fetch('/api/products/search', {
      method: 'POST',
      credentials: 'same-origin', // Uses existing session
      body: JSON.stringify({ query: productName })
    });
    return await response.json();
  }
});
How it works: Agent can call tools from both sites. The extension routes each tool call to the correct tab, preserving separate authentication contexts for each site. Tools are thin wrappers around existing APIs - no special auth needed.

Example: Content Aggregation

Agents can fetch content from one site and post to another:
// News site
useWebMCP({
  name: 'get_top_stories',
  handler: async () => ({ stories: await fetch('/api/stories/top').then(r => r.json()) })
});

// Social site
useWebMCP({
  name: 'post_to_feed',
  inputSchema: { title: z.string(), url: z.string().url() },
  handler: async ({ title, url }) => await fetch('/api/posts', {
    method: 'POST',
    credentials: 'same-origin',
    body: JSON.stringify({ title, url })
  }).then(r => r.json())
});
Agents can fetch stories from the news site and share them on social media.

Best Practices for Cross-Site Tools

Include the site domain or purpose in tool names to avoid collisions:
// Good
name: 'github_create_issue'
name: 'jira_create_ticket'

// Avoid
name: 'create_issue' // Which site?
Make it easy for tools on other sites to consume your data:
// Good - structured, predictable
return {
  items: [...],
  total: 99.99,
  currency: 'USD'
};

// Avoid - unstructured text
return "You have 3 items totaling $99.99";
Each tool should do one thing well, making them easier to compose:
// Good - focused tools
useWebMCP({ name: 'get_cart', ... });
useWebMCP({ name: 'add_to_cart', ... });
useWebMCP({ name: 'checkout', ... });

// Avoid - one tool that does everything
useWebMCP({ name: 'cart_manager', ... });
Use clear descriptions and schemas so other sites know what to expect:
useWebMCP({
  name: 'get_cart',
  description: 'Get cart contents. Returns {items: Array<{name, price, sku, quantity}>, total: number}',
  handler: async () => { ... }
});

Security Considerations

When building tools that will be composed with other sites:
  • Never expose sensitive data in tool responses
  • Validate all inputs from external sources
  • Don’t trust data from other sites without validation
  • Be aware that AI may pass data between sites
  • Review security best practices for multi-site scenarios

Read-Only Context Tools

Exposing Application State

import { useWebMCPContext } from '@mcp-b/react-webmcp';

function AppLayout() {
  const { pathname } = useLocation();
  const { user } = useAuth();

  // Lightweight read-only context
  useWebMCPContext(
    'context_current_route',
    'Get the current page route and user info',
    () => ({
      path: pathname,
      user: user ? {
        id: user.id,
        name: user.name,
        role: user.role
      } : null,
      timestamp: new Date().toISOString()
    })
  );

  return <div>App Layout</div>;
}

Security Best Practices

Always sanitize inputs, implement rate limiting, and validate permissions. See the Security Guide for comprehensive patterns including:
  • Input sanitization with DOMPurify
  • Rate limiting implementations
  • Permission checks and auth validation
  • Handling sensitive data securely

Next Steps