Skip to main content
Engineering 2026-07-07

Register WebMCP Tools in React with useModelTool

MCP Trail

MCP Trail Team

Developer Relations

Register WebMCP Tools in React with useModelTool

Register WebMCP Tools in React with useModelTool

Short answer: In a React app, the set of actions an agent should be able to take changes as the user navigates — a checkout tool only makes sense while the cart is open. @mcptrail/use-model-tool gives you useModelTool(tool), a hook that registers a WebMCP tool when a component mounts and unregisters it when the component unmounts. Tie tool availability to your component tree and the agent’s toolset stays in sync with what’s actually on screen — no manual add/remove bookkeeping.

New to the underlying browser API? Read What is WebMCP? first. This post is specifically about wiring WebMCP into React the idiomatic way.

Why tool lifecycle should follow your component tree

WebMCP registers tools on the global navigator.modelContext. That global doesn’t know anything about your routes or which panel is open — so if you register every tool at app startup, the agent sees checkout, apply_coupon, and remove_item even on the marketing homepage where none of them apply.

That’s a real problem. An agent offered a tool it can’t meaningfully use will sometimes try it anyway, producing confusing errors or acting on stale state. The fix is to scope tools to the UI they belong to: expose checkout only while the cart is on screen, and pull it the moment the cart closes.

In React, “while this is on screen” is exactly what useEffect cleanup expresses — which is what useModelTool is built on.

useModelTool: register on mount, unregister on unmount

npm install @mcptrail/use-model-tool

Call the hook with a tool definition inside any component. It registers on mount and cleans up on unmount automatically:

import { useModelTool } from "@mcptrail/use-model-tool";

function Cart({ items }: { items: CartItem[] }) {
  useModelTool({
    name: "checkout",
    description: "Purchase everything currently in the cart",
    inputSchema: {
      type: "object",
      properties: { paymentMethod: { type: "string" } },
      required: ["paymentMethod"],
    },
    async execute(args) {
      const order = await placeOrder(items, args.paymentMethod);
      return {
        content: [{ type: "text", text: `Order ${order.id} placed` }],
      };
    },
  });

  return <CartView items={items} />;
}

Because the Cart component only renders while the cart drawer is open, the checkout tool exists only during that window. Close the cart, the component unmounts, and the hook’s cleanup unregisters the tool — the agent no longer sees it. You wrote zero registration plumbing.

Re-registering when inputs change

The hook watches the tool object the same way useEffect watches its dependencies. If the tool’s identity changes across renders, it unregisters the old definition and registers the new one — so a tool whose behavior depends on props or state stays current:

function OrderTracker({ orderId }: { orderId: string }) {
  useModelTool(
    {
      name: "track_order",
      inputSchema: { type: "object", properties: {} },
      async execute() {
        const status = await getStatus(orderId);
        return { content: [{ type: "text", text: status }] };
      },
    },
    [orderId], // re-register when orderId changes
  );

  return <TrackingPanel orderId={orderId} />;
}

Pass a dependency array as the second argument, just like useEffect. When orderId changes, the hook swaps the registration so execute always closes over the current order — no stale closures reaching for last render’s value.

Registering several tools at once with useModelTools

A panel often exposes a handful of related actions. Rather than stacking hook calls, useModelTools takes an array and manages the whole group’s lifecycle together:

import { useModelTools } from "@mcptrail/use-model-tool";

function CartTools({ items }: { items: CartItem[] }) {
  useModelTools([
    {
      name: "apply_coupon",
      inputSchema: {
        type: "object",
        properties: { code: { type: "string" } },
        required: ["code"],
      },
      async execute(args) {
        return { content: [{ type: "text", text: await applyCoupon(args.code) }] };
      },
    },
    {
      name: "remove_item",
      inputSchema: {
        type: "object",
        properties: { itemId: { type: "string" } },
        required: ["itemId"],
      },
      async execute(args) {
        return { content: [{ type: "text", text: await removeItem(args.itemId) }] };
      },
    },
  ]);

  return null;
}

The whole set registers together and tears down together when the component unmounts.

HookRegistersBest for
useModelToolone toola single action tied to a view
useModelToolsan array of toolsa panel that owns several related actions

The hook is MIT-licensed and open source at github.com/ElBartoTn/use-model-tool.

FAQ

What happens during React Strict Mode’s double-mount?

Strict Mode mounts, unmounts, and remounts components in development to surface effect bugs. useModelTool handles this cleanly: the extra unmount unregisters the tool and the remount re-registers it, so you end in the correct state with no duplicate registrations. The behavior mirrors a well-behaved useEffect with cleanup.

Can two components register a tool with the same name?

Give each tool a unique name — WebMCP identifies tools by name, so two live registrations sharing one name will collide. If two views legitimately expose “the same” action, either render only one at a time (so their mount windows don’t overlap) or namespace the names, e.g. cart.checkout versus quickbuy.checkout.

Do I still get type-safe arguments in React?

Yes. useModelTool accepts the same tool shape as the core kit, so you can build definitions with defineTool from @mcptrail/webmcp-kit and keep the inferred execute argument types. See Type-safe WebMCP tools with TypeScript for how that inference works.


Explore features · Open MCP Trail

Share this article