Skill
Community

webmcp-setup

Install
1
Install the plugin
$
npx claudepluginhub webmcp-org/npm-packages

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

Strategic guidance for adding WebMCP to web applications. Use when the user wants to make their web app AI-accessible, create LLM tools for their UI, or enable browser automation through MCP. Focuses on design principles, tool architecture, and testing workflow.

Tool Access

This skill is limited to using the following tools:

ReadWriteBashGlobGrepmcp__docs__SearchWebMcpDocumentationmcp__chrome-devtools__*
Supporting Assets
View in Repository
CHANGELOG.md
CONTRIBUTING.md
README.md
assets/templates/vanilla-demo.html
examples/COMMON_APPS.md
package.json
references/ADVANCED_PATTERNS.md
references/INSTALLATION.md
references/REACT_SETUP.md
references/TOOL_PATTERNS.md
references/TROUBLESHOOTING.md
scripts/verify-setup.js
Skill Content

WebMCP Setup - Creating an LLM UI

Core Philosophy: WebMCP is about creating a user interface for LLMs. Just as humans use buttons, forms, and navigation, LLMs use tools. Your goal is UI parity - enable everything a human can do, in a way that makes sense for LLMs.

Quick Start

  1. Understand the app - What actions can humans take?
  2. Plan tools - List needed tools by category (read/write/destructive)
  3. Phase 1: Read - Build read-only tools, test with Chrome DevTools MCP
  4. Phase 2: Modify - Build read-write tools, test with Chrome DevTools MCP
  5. Phase 3: Act - Build destructive tools, test with Chrome DevTools MCP
  6. Iterate - Use the tools, find gaps, improve

For installation: See references/INSTALLATION.md or mcp__docs__SearchWebMcpDocumentation("setup guide")

Quick Reference

PhaseWhat You're BuildingTools to Use
UnderstandingLearn WebMCP patternsmcp__docs__SearchWebMcpDocumentation
PlanningDesign tool architectureThis skill (you're reading it)
ImplementingWrite tool codemcp__docs__SearchWebMcpDocumentation for APIs
TestingDogfood every toolmcp__chrome-devtools__* tools (requires Chrome Dev 145+ for auth testing)
IteratingRefine based on usageChrome DevTools MCP + dogfooding

Success Criteria

Every major UI action has a corresponding tool

  • If a human can do it, the LLM should be able to do it
  • UI parity achieved

Tools are categorized by safety

  • Read-only, read-write, and destructive tools clearly separated
  • Annotations properly set

Forms use two-tool pattern

  • fill_*_form (read-write) + submit_*_form (destructive)
  • User can see what's being submitted

All tools tested with Chrome DevTools MCP

  • Every tool has been called and verified
  • Edge cases tested
  • Return values validated

Tools are powerful, not granular

  • One tool does a complete task
  • Minimizes number of tool calls needed

Tool Design Principles

1. Categorize by Safety

Organize tools into three categories:

Read-Only Tools (readOnlyHint: true)

Purpose: Let the LLM understand the current state

Characteristics:

  • No side effects
  • Safe to call repeatedly
  • Idempotent

Examples:

  • list_todos - Get all todos with filtering
  • get_user_profile - Get current user data
  • search_products - Search product catalog
  • get_cart_contents - See what's in cart

Read-Write Tools (default)

Purpose: Modify UI state in a non-destructive way

Characteristics:

  • Changes what user sees on screen
  • Reversible (user can undo)
  • Does NOT commit/submit/save permanently
  • User sees changes in real-time

Examples:

  • fill_contact_form - Populate form fields (but don't submit)
  • set_search_query - Change search box text (but don't search yet)
  • apply_filters - Update filter selection (but don't reload data yet)
  • navigate_to_page - Change page/tab (reversible with back button)

Destructive Tools (destructiveHint: true)

Purpose: Take permanent, irreversible actions

Characteristics:

  • Commits changes permanently
  • Submits forms, deletes data, makes purchases
  • Requires careful use
  • Should be separate from filling/preparation

Examples:

  • submit_order - Actually place the order
  • delete_item - Permanently remove item
  • send_message - Send email/message
  • create_account - Register new user

2. The Two-Tool Pattern for Forms

CRITICAL PRINCIPLE: Separate filling from submission

Bad Approach (Single Tool)

// ❌ Don't do this
useWebMCP({
  name: 'submit_contact_form',
  destructiveHint: true, // Destructive from the start!
  inputSchema: {
    name: z.string(),
    email: z.string(),
    message: z.string(),
  },
  handler: async ({ name, email, message }) => {
    // Fill AND submit in one go
    setName(name);
    setEmail(email);
    setMessage(message);
    await submitForm(); // User never sees what's being submitted!
    return { success: true };
  },
});

Problems:

  • User doesn't see what's being submitted
  • No chance to review or correct
  • Single atomic action = risky

Good Approach (Two Tools)

// ✅ Tool 1: Fill the form (read-write)
useWebMCP({
  name: 'fill_contact_form',
  description: 'Fill out the contact form fields',
  inputSchema: {
    name: z.string().optional(),
    email: z.string().optional(),
    message: z.string().optional(),
  },
  handler: async ({ name, email, message }) => {
    // Only fill the fields, don't submit
    if (name) setName(name);
    if (email) setEmail(email);
    if (message) setMessage(message);
    return { success: true, filledFields: { name, email, message } };
  },
});

// ✅ Tool 2: Submit the form (destructive)
useWebMCP({
  name: 'submit_contact_form',
  destructiveHint: true,
  description: 'Submit the contact form',
  handler: async () => {
    if (!name || !email) {
      return { success: false, error: 'Name and email required' };
    }
    await submitForm();
    return { success: true, message: 'Form submitted' };
  },
});

Benefits:

  • User sees form get filled on screen
  • Separate tool call = explicit intent
  • Can fill, review, then submit
  • If submission fails, form is already filled

3. UI Parity - Match Human Capabilities

Mental Model: For every major action a human can take in your UI, create a corresponding tool.

Audit Process:

  1. Open your app as a human user
  2. List all major actions you can take
  3. For each action, create a tool

Example Audit - Todo App:

  • Human can: View todos → Tool: list_todos
  • Human can: Add todo → Tools: fill_todo_form, create_todo
  • Human can: Mark complete → Tool: mark_todo_complete
  • Human can: Delete todo → Tool: delete_todo
  • Human can: Filter todos → Tool: set_filter

UI Parity Achieved: LLM can do everything a human can do.

4. Make Tools Powerful, Not Granular

Principle: One tool should accomplish a complete task, not just one tiny piece.

Too Granular (Bad)

// ❌ User needs 3 tool calls to fill a form
useWebMCP({ name: 'set_name', ... });
useWebMCP({ name: 'set_email', ... });
useWebMCP({ name: 'set_message', ... });

Problems: 3 tool calls instead of 1, inefficient, poor UX

Powerful (Good)

// ✅ One tool call fills entire form
useWebMCP({
  name: 'fill_contact_form',
  inputSchema: {
    name: z.string().optional(),
    email: z.string().optional(),
    message: z.string().optional(),
  },
  handler: async ({ name, email, message }) => {
    // Fill all fields at once
    if (name) setName(name);
    if (email) setEmail(email);
    if (message) setMessage(message);
    return { success: true };
  },
});

Benefits: 1 tool call, faster execution, better UX

Advanced Patterns & Best Practices

For detailed patterns that significantly impact tool quality, see:

references/ADVANCED_PATTERNS.md - Covers:

  • Tool naming conventions (domain_verb_noun)
  • Complete annotation system (readOnlyHint, idempotentHint, destructiveHint)
  • Avoiding tool overload (>50 tools)
  • outputSchema vs formatOutput
  • Error handling with onError
  • Confirmation dialogs for destructive actions
  • Performance optimization
  • Tool cleanup and memory management
  • Import order requirements
  • React StrictMode handling
  • Hot Module Replacement (HMR) support

When to read: After understanding core principles, before implementing complex tools.

Always search docs for specifics: mcp__docs__SearchWebMcpDocumentation("your question")

React WebMCP Hooks - Best Practices

When using @mcp-b/react-webmcp hooks, follow these patterns for optimal performance and reliability.

The Three Hooks

useWebMCP - Main hook for registering tools with full control

const tool = useWebMCP({
  name: 'posts_like',
  description: 'Like a post by ID',
  inputSchema: { postId: z.string().uuid() },
  outputSchema: { success: z.boolean(), likeCount: z.number() },
  handler: async ({ postId }) => {
    await api.posts.like(postId);
    return { success: true, likeCount: 42 };
  },
});

useWebMCPContext - Simplified hook for read-only context exposure

useWebMCPContext('context_current_post', 'Get the currently viewed post ID and metadata', () => ({
  postId,
  title: post?.title,
  author: post?.author,
}));

useWebMCPResource - Hook for exposing MCP resources (files, data streams)

const { isRegistered } = useWebMCPResource({
  uri: 'user://{userId}/profile',
  name: 'User Profile',
  description: 'User profile data by ID',
  mimeType: 'application/json',
  read: async (uri, params) => {
    const profile = await fetchUserProfile(params?.userId);
    return { contents: [{ uri: uri.href, text: JSON.stringify(profile) }] };
  },
});

Critical: Always Use Output Schemas

Output schemas are MANDATORY for modern WebMCP integrations. They enable:

  • AI providers to compile schemas to TypeScript
  • Type-safe code generation by AI
  • Structured responses that AI can reason about
  • Better validation and error handling
// ❌ BAD: No output schema (AI gets untyped text)
useWebMCP({
  name: 'get_users',
  description: 'List users',
  handler: async () => {
    return { users: [...] };  // AI receives text blob
  }
});

// ✅ GOOD: With output schema (AI gets typed structure)
const OUTPUT_SCHEMA = {
  users: z.array(z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email()
  })),
  total: z.number()
};

useWebMCP({
  name: 'get_users',
  description: 'List users',
  outputSchema: OUTPUT_SCHEMA,  // AI receives structuredContent
  handler: async () => ({
    users: await api.users.list(),
    total: 10
  })
});

Why this matters:

  • Without outputSchema: AI receives CallToolResult.content[0].text (string)
  • With outputSchema: AI receives CallToolResult.structuredContent (typed object)
  • Modern AI models expect structured data for code generation

Performance: Memoize Schemas

Schemas must be stable references or tools will re-register on every render.

// ❌ BAD: New object every render → re-registers constantly
useWebMCP({
  name: 'get_count',
  outputSchema: { count: z.number() }, // New object!
  handler: async () => ({ count: 10 }),
});

// ✅ GOOD: Memoized schema → stable reference
const OUTPUT_SCHEMA = useMemo(
  () => ({
    count: z.number(),
    items: z.array(z.string()),
  }),
  []
);

useWebMCP({
  name: 'get_count',
  outputSchema: OUTPUT_SCHEMA, // Stable reference
  handler: async () => ({ count: 10, items: [] }),
});

// ✅ BEST: Static schema outside component
const OUTPUT_SCHEMA = {
  count: z.number(),
  items: z.array(z.string()),
};

function MyComponent() {
  useWebMCP({
    name: 'get_count',
    outputSchema: OUTPUT_SCHEMA, // Always stable
    handler: async () => ({ count: 10, items: [] }),
  });
}

What gets memoized:

  • inputSchema - Always memoize or define outside component
  • outputSchema - Always memoize or define outside component
  • annotations - Always memoize or define outside component

What does NOT need memoization:

  • handler - Stored in a ref, changes don't trigger re-registration
  • onSuccess / onError - Stored in refs
  • formatOutput - Stored in a ref

The deps Array

Use the second parameter to control when tools re-register:

function TodoList({ todos }: { todos: Todo[] }) {
  const todoCount = todos.length;
  const todoIds = todos.map((t) => t.id).join(',');

  // Re-register when count or IDs change
  useWebMCP(
    {
      name: 'list_todos',
      description: `List all todos (${todoCount} items)`,
      outputSchema: OUTPUT_SCHEMA,
      handler: async () => ({ todos, count: todoCount }),
    },
    [todoCount, todoIds] // ← deps array
  );
}

Best practices for deps:

  • Use primitive values: [count, id] not [{ count, id }]
  • Derive stable values: items.map(i => i.id).join(',') not [items]
  • Don't include callbacks: Handler changes don't need re-registration
  • Memoize objects/arrays: Use useMemo if you must include them
// ✅ GOOD: Primitive values
useWebMCP({ ... }, [count, userId]);

// ✅ GOOD: Derived primitive
const itemIds = items.map(i => i.id).join(',');
useWebMCP({ ... }, [items.length, itemIds]);

// ❌ BAD: New object every render
useWebMCP({ ... }, [{ count }]);  // Re-registers every render!

// ❌ BAD: Array reference changes
useWebMCP({ ... }, [items]);  // Re-registers when array changes

React Forms: The Two-Tool Pattern

Follow the same two-tool pattern (fill + submit) with React state:

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });

  // Tool 1: Fill form (read-write, user sees changes)
  const FILL_SCHEMA = useMemo(
    () => ({
      name: z.string().optional(),
      email: z.string().email().optional(),
      message: z.string().optional(),
    }),
    []
  );

  useWebMCP({
    name: 'fill_contact_form',
    description: 'Fill out contact form fields',
    inputSchema: FILL_SCHEMA,
    annotations: {
      title: 'Fill Contact Form',
      readOnlyHint: false,
      destructiveHint: false,
    },
    handler: async ({ name, email, message }) => {
      setFormData((prev) => ({
        name: name ?? prev.name,
        email: email ?? prev.email,
        message: message ?? prev.message,
      }));
      return { success: true };
    },
  });

  // Tool 2: Submit form (destructive, permanent action)
  const SUBMIT_OUTPUT_SCHEMA = useMemo(
    () => ({
      success: z.boolean(),
      error: z.string().optional(),
    }),
    []
  );

  useWebMCP({
    name: 'submit_contact_form',
    description: 'Submit the contact form',
    outputSchema: SUBMIT_OUTPUT_SCHEMA,
    annotations: {
      title: 'Submit Contact Form',
      destructiveHint: true,
    },
    handler: async () => {
      if (!formData.name || !formData.email) {
        return { success: false, error: 'Name and email required' };
      }
      await submitForm(formData);
      return { success: true };
    },
  });

  // Tool 3: Read current form state (read-only)
  const READ_OUTPUT_SCHEMA = useMemo(
    () => ({
      formData: z.object({
        name: z.string(),
        email: z.string(),
        message: z.string(),
      }),
      isValid: z.boolean(),
    }),
    []
  );

  useWebMCP(
    {
      name: 'get_form_data',
      description: 'Get current contact form data',
      outputSchema: READ_OUTPUT_SCHEMA,
      annotations: { readOnlyHint: true },
      handler: async () => ({
        formData,
        isValid: !!(formData.name && formData.email),
      }),
    },
    [formData.name, formData.email, formData.message]
  );

  return <form>{/* UI */}</form>;
}

Context Exposure with useWebMCPContext

Use useWebMCPContext for lightweight read-only data:

function PostDetailPage() {
  const { postId } = useParams();
  const { data: post } = useQuery(['post', postId], () => fetchPost(postId));

  // Expose current context to AI
  useWebMCPContext('context_current_post', 'Get the currently viewed post ID and metadata', () => ({
    postId,
    title: post?.title,
    author: post?.author,
    tags: post?.tags,
    createdAt: post?.createdAt,
  }));

  return <PostContent post={post} />;
}

When to use useWebMCPContext vs useWebMCP:

  • Use useWebMCPContext for simple read-only context (current page, user session)
  • Use useWebMCP when you need outputSchema for structured responses
  • Use useWebMCP when you need execution state tracking or callbacks

Tool Execution State

useWebMCP returns execution state for UI feedback:

function LikeButton({ postId }: { postId: string }) {
  const likeTool = useWebMCP({
    name: 'posts_like',
    description: 'Like a post',
    inputSchema: { postId: z.string() },
    outputSchema: {
      success: z.boolean(),
      likeCount: z.number(),
    },
    handler: async ({ postId }) => {
      const result = await api.posts.like(postId);
      return { success: true, likeCount: result.likes };
    },
    onSuccess: () => {
      toast.success('Post liked!');
    },
    onError: (error) => {
      toast.error(`Failed: ${error.message}`);
    },
  });

  return (
    <button onClick={() => likeTool.execute({ postId })}>
      {likeTool.state.isExecuting && <Spinner />}
      {likeTool.state.lastResult && <span>♥ {likeTool.state.lastResult.likeCount}</span>}
      {likeTool.state.error && <span>Error</span>}
    </button>
  );
}

Testing React Tools with Chrome DevTools MCP

See the Chrome DevTools MCP setup section below for how to test React tools with auto-connect to preserve authentication.

Key workflow:

  1. Start your React dev server (npm run dev)
  2. Open Chrome Dev browser
  3. Log into your app (create account, navigate to test page, etc.)
  4. Launch Chrome DevTools MCP with auto-connect (preserves cookies/auth)
  5. Call tools via MCP client to test them
  6. Verify UI updates in browser
  7. Check return values match outputSchema

Why this matters for React:

  • React hooks register tools on component mount
  • Chrome DevTools MCP can call those tools from outside the browser
  • Auto-connect preserves your authenticated session
  • Perfect for testing authenticated React apps

Common React Patterns

Pattern 1: Todo List with Filtering

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

  const filteredTodos = useMemo(() => {
    return todos.filter((t) => {
      if (filter === 'all') return true;
      if (filter === 'active') return !t.completed;
      return t.completed;
    });
  }, [todos, filter]);

  // Phase 1: Read tools
  const LIST_OUTPUT_SCHEMA = useMemo(
    () => ({
      todos: z.array(
        z.object({
          id: z.string(),
          text: z.string(),
          completed: z.boolean(),
        })
      ),
      count: z.number(),
      filter: z.enum(['all', 'active', 'completed']),
    }),
    []
  );

  useWebMCP(
    {
      name: 'list_todos',
      description: `List todos (${filteredTodos.length} of ${todos.length})`,
      outputSchema: LIST_OUTPUT_SCHEMA,
      annotations: { readOnlyHint: true },
      handler: async () => ({
        todos: filteredTodos,
        count: todos.length,
        filter,
      }),
    },
    [filteredTodos.length, todos.length, filter]
  );

  // Phase 2: Write tools
  useWebMCP({
    name: 'set_filter',
    description: 'Change todo filter',
    inputSchema: {
      filter: z.enum(['all', 'active', 'completed']),
    },
    handler: async ({ filter }) => {
      setFilter(filter);
      return { success: true };
    },
  });

  // Phase 3: Destructive tools
  const CREATE_OUTPUT_SCHEMA = useMemo(
    () => ({
      success: z.boolean(),
      todo: z.object({
        id: z.string(),
        text: z.string(),
        completed: z.boolean(),
      }),
    }),
    []
  );

  useWebMCP({
    name: 'create_todo',
    description: 'Create a new todo',
    inputSchema: { text: z.string().min(1) },
    outputSchema: CREATE_OUTPUT_SCHEMA,
    annotations: { destructiveHint: true },
    handler: async ({ text }) => {
      const newTodo = { id: crypto.randomUUID(), text, completed: false };
      setTodos((prev) => [...prev, newTodo]);
      return { success: true, todo: newTodo };
    },
  });

  useWebMCP({
    name: 'toggle_todo',
    description: 'Toggle todo completion status',
    inputSchema: { todoId: z.string() },
    annotations: { destructiveHint: true },
    handler: async ({ todoId }) => {
      setTodos((prev) =>
        prev.map((t) => (t.id === todoId ? { ...t, completed: !t.completed } : t))
      );
      return { success: true };
    },
  });

  return <div>{/* UI */}</div>;
}

Pattern 2: Search with Debouncing

function ProductSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Product[]>([]);

  // Debounced search effect
  useEffect(() => {
    const timer = setTimeout(() => {
      if (query) {
        api.products.search(query).then(setResults);
      }
    }, 300);
    return () => clearTimeout(timer);
  }, [query]);

  const SEARCH_OUTPUT_SCHEMA = useMemo(
    () => ({
      products: z.array(
        z.object({
          id: z.string(),
          name: z.string(),
          price: z.number(),
        })
      ),
      total: z.number(),
    }),
    []
  );

  useWebMCP({
    name: 'search_products',
    description: 'Search for products',
    inputSchema: { query: z.string() },
    outputSchema: SEARCH_OUTPUT_SCHEMA,
    handler: async ({ query }) => {
      setQuery(query);
      // Results update via useEffect debouncing
      return { products: results, total: results.length };
    },
  });

  return <div>{/* UI */}</div>;
}

React-Specific Gotchas

1. StrictMode Double Rendering

  • React hooks handle StrictMode automatically
  • Tools only register once even with double render
  • No manual deduplication needed

2. Hot Module Replacement (HMR)

  • Tools automatically unregister on component unmount
  • New tool versions register on remount
  • Works seamlessly with Vite, Webpack, etc.

3. Async State Updates

  • React state updates are async
  • Handler returns immediately after setState
  • Don't wait for state to update before returning
// ❌ BAD: Trying to read updated state
useWebMCP({
  name: 'increment',
  handler: async () => {
    setCount((c) => c + 1);
    return { newCount: count }; // Returns OLD value!
  },
});

// ✅ GOOD: Calculate new value directly
useWebMCP({
  name: 'increment',
  handler: async () => {
    const newCount = count + 1;
    setCount(newCount);
    return { newCount }; // Returns correct value
  },
});

4. Memory Leaks

  • React hooks auto-cleanup on unmount
  • State updates after unmount are prevented
  • No manual cleanup needed

Framework Integration

Next.js App Router

// app/components/Tools.tsx
'use client';
import { useWebMCP } from '@mcp-b/react-webmcp';

export function WebMCPTools() {
  useWebMCP({
    /* ... */
  });
  return null; // Can be invisible component
}

// app/layout.tsx
import { WebMCPTools } from './components/Tools';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.global.js" />
      </head>
      <body>
        <WebMCPTools />
        {children}
      </body>
    </html>
  );
}

Remix

// app/root.tsx
import { useWebMCP } from '@mcp-b/react-webmcp';

export default function App() {
  useWebMCP({
    /* ... */
  });

  return (
    <html>
      <head>
        <script src="https://unpkg.com/@mcp-b/global@latest/dist/index.global.js" />
      </head>
      <body>
        <Outlet />
      </body>
    </html>
  );
}

Quick Checklist for React Tools

Before testing with Chrome DevTools MCP:

Output schemas defined for all tools (use useMemo or static const) ✅ Schemas are memoized or static (not inline objects) ✅ deps array used correctly (primitives, no objects/functions) ✅ Forms use two-tool pattern (fill + submit separated) ✅ Annotations set properly (readOnlyHint, destructiveHint) ✅ State updates are sync (don't wait for async setState) ✅ Error handling in place (try/catch in handlers)

For detailed React setup: mcp__docs__SearchWebMcpDocumentation("react setup")

Common App Patterns

See examples/COMMON_APPS.md for complete tool structures for:

  • Todo List App
  • E-Commerce Site
  • Admin Dashboard
  • Social Media Platform
  • Project Management Tool

Each pattern shows the full tool hierarchy (read → write → destructive) with specific examples.

Implementation Strategy

Phase 1: Read the World (Read-Only Tools)

Goal: Give the LLM eyes. Let it understand what's on screen.

What to build:

  1. List tools - Get collections of items

    • list_todos, list_products, list_users
  2. Get tools - Get specific item details

    • get_todo_by_id, get_product_details, get_user_profile
  3. Search tools - Find specific information

    • search_products, search_logs, search_messages
  4. Status tools - Get current application state

    • get_cart_contents, get_current_filters, get_theme

Why first?:

  • LLM needs context before taking actions
  • Safest to implement and test
  • Builds your confidence with WebMCP
  • No risk of breaking anything

Testing:

# For each read-only tool:
1. Call the tool
2. Verify returned data matches what's on screen
3. Call again - should get same data (idempotent)
4. Try different parameters (filters, IDs)
5. Check edge cases (empty lists, invalid IDs)

Phase 2: Modify UI (Read-Write Tools)

Goal: Let the LLM interact with the UI without permanent consequences.

What to build:

  1. Fill tools - Populate forms (but don't submit)

    • fill_contact_form, fill_checkout_form, fill_profile_form
  2. Set tools - Change UI state

    • set_filter, set_search_query, set_theme, set_language
  3. Navigate tools - Move between pages

    • navigate_to_page, open_modal, switch_tab

Why second?:

  • Gives LLM agency without risk
  • User sees changes in real-time
  • Reversible (user can undo)
  • Builds trust

Testing:

# For each read-write tool:
1. Call the tool with test data
2. Verify changes appear on screen immediately
3. Check that nothing permanent happened
4. Try edge cases (empty values, invalid values)
5. Verify error handling works

Dogfooding: Actually use these tools yourself via Chrome DevTools MCP. If it's tedious or confusing for you, it'll be worse for the LLM.

Phase 3: Take Action (Destructive Tools)

Goal: Let the LLM make permanent changes and complete workflows.

What to build:

  1. Submit tools - Actually commit forms

    • submit_contact_form, submit_order, submit_profile_update
  2. Create tools - Add new records

    • create_todo, create_user, create_post
  3. Delete tools - Remove items permanently

    • delete_todo, delete_user, delete_post
  4. Action tools - Other permanent state changes

    • mark_complete, send_message, publish_post

Why last?:

  • Most risky
  • Requires phases 1-2 to be solid
  • Build confidence first
  • Easier to test when you can inspect state

Testing:

# For each destructive tool:
1. Use Phase 2 tools to set up state (fill forms, etc.)
2. Call the destructive tool
3. Verify action completed successfully
4. Check for confirmation dialogs (if any)
5. Use Phase 1 tools to verify new state
6. Test error cases (invalid IDs, missing data)
7. Test what happens when user cancels/rejects

Critical: Dogfooding with Chrome DevTools MCP

MOST IMPORTANT PART: You MUST test every tool with Chrome DevTools MCP.

Why Dogfooding Matters

You are building an interface. Just like you'd manually test a button to see if it works, you must manually test each tool.

If you don't test:

  • Tools might not work at all
  • Return values might be wrong
  • Edge cases will be broken
  • User experience will be poor

If you DO test:

  • You'll catch bugs immediately
  • You'll see what the LLM experiences
  • You'll find confusing APIs and fix them
  • You'll build intuition for good tool design

Dogfooding Workflow

Prerequisites: Set up Chrome DevTools MCP with Chrome Dev 145+ for best testing experience. See Setting Up Chrome DevTools MCP for Testing below for configuration details.

For EVERY tool you create:

  1. Register the tool in your app code
  2. Start your dev server (npm run dev)
  3. Open Chrome DevTools MCP (if not already running)
  4. Navigate to your app in Chrome DevTools MCP
  5. Call the tool via Chrome DevTools MCP
  6. Verify the behavior in the actual browser
  7. Check the return value from the tool
  8. Try edge cases (empty inputs, invalid IDs, etc.)
  9. Iterate - fix issues and test again

Repeat this for every single tool. No exceptions.

The Build-Test-Iterate Loop

This is TDD (Test-Driven Development) for AI tools. The tight feedback cycle with Chrome DevTools MCP enables rapid iteration:

AI writes tool code
      ↓
Dev server hot-reloads (instant)
      ↓
AI navigates to page via Chrome DevTools MCP
      ↓
AI calls list_webmcp_tools (discovers new tool)
      ↓
AI calls the tool with test inputs
      ↓
Does it work correctly?
  ├─ Yes → Done! Move to next tool
  └─ No → Fix the code, loop back to top

Why this is powerful:

  • Instant feedback: HMR means changes appear immediately
  • Real testing: Tools are called in actual browser context
  • Self-verification: AI can verify its own work
  • Rapid iteration: Fix → test → verify in seconds

Example workflow:

Agent: "I'll create a search_products tool"
1. Agent writes tool code using useWebMCP
2. Vite dev server hot-reloads (< 1 second)
3. Agent: mcp__chrome-devtools__navigate("http://localhost:3000")
4. Agent: mcp__chrome-devtools__list_webmcp_tools
   → Sees "search_products" in the list ✓
5. Agent: mcp__chrome-devtools__call_webmcp_tool("search_products", { query: "laptop" })
   → Returns: { products: [...], count: 5 } ✓
6. Agent verifies results match expectation
7. Tool works! Move on.

If something breaks:

Agent: "The tool returned undefined instead of products array"
1. Agent examines the code
2. Agent: "I see the issue - missing return statement"
3. Agent fixes the code
4. Dev server reloads automatically
5. Agent calls the tool again
6. Now it works! ✓

This build-test-iterate loop is why Chrome DevTools MCP integration is so critical. It turns tool development into an interactive, self-correcting process.

Example Dogfooding Session

Let's say you're building a todo app. Here's what testing looks like:

# You've just added the 'create_todo' tool
# Now test it:

1. Start dev server: npm run dev
2. Chrome DevTools MCP is already connected to localhost:3000
3. Call the tool:
   mcp__chrome-devtools__* → call tool 'create_todo'
   Input: { "text": "Test todo", "priority": "high" }

4. Look at browser → New todo appears on screen ✅
5. Check return value → { success: true, id: "abc123" } ✅
6. Call list_todos → New todo is in the list ✅

7. Try edge case: { "text": "", "priority": "invalid" }
8. Check error handling → Got clear error message ✅

9. Todo works! Move to next tool.

This is NOT optional. Every tool must be dogfooded.

Common Issues Found During Dogfooding

You'll discover:

  • "This tool should return the new todo, not just success:true"
  • "The description doesn't match what the tool actually does"
  • "I need a get_todo_by_id tool to verify the create worked"
  • "This should be two tools - one to fill, one to submit"
  • "The error message is confusing"
  • "This tool is too granular, I need to call it 5 times"

Fix these immediately. Dogfooding gives you this feedback.

Setting Up Chrome DevTools MCP for Testing

To dogfood WebMCP tools effectively, you need Chrome DevTools MCP properly configured. Here's how to set it up for optimal testing workflow:

Chrome Version Requirements

Auto-Connect Feature Requires Chrome 145+

The auto-connect feature (connects to running Chrome with your cookies/auth) requires:

  • Chrome Dev (v145+) - Available NOW, recommended for testing
  • Chrome Canary (v146+) - Bleeding edge, may be unstable
  • Chrome Stable (v143) - Does NOT support auto-connect yet (coming Feb 2026)
  • Chrome Beta (v144) - Does NOT support auto-connect yet

Check your Chrome version:

# Mac
/Applications/Google\ Chrome\ Dev.app/Contents/MacOS/Google\ Chrome\ Dev --version

# Should show: Google Chrome 145.x.x.x dev

Configuration for Testing (Recommended)

Option 1: Auto-Connect to Running Chrome (Best for Testing)

Use this when:

  • ✅ Testing with authenticated sessions (logged-in user)
  • ✅ Need browser cookies/localStorage from your dev session
  • ✅ Want to reuse existing browser profile with extensions
  • ✅ Testing WebMCP tools that require auth

MCP Config:

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "@mcp-b/chrome-devtools-mcp@latest"]
    }
  }
}

What it does:

  1. Tries to connect to running Chrome Dev with your profile
  2. Falls back to launching new Chrome Dev if not running
  3. Preserves cookies, auth tokens, localStorage
  4. Perfect for testing authenticated apps

Option 2: Always Launch Fresh Instance (Headless Testing)

Use this when:

  • Testing without auth requirements
  • CI/CD pipelines
  • Clean slate needed for each test

MCP Config:

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "@mcp-b/chrome-devtools-mcp@latest", "--no-auto-connect", "--isolated"]
    }
  }
}

Option 3: Chrome Stable (No Auto-Connect)

If you don't have Chrome Dev/Canary installed:

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "@mcp-b/chrome-devtools-mcp@latest", "--channel=stable", "--no-auto-connect"]
    }
  }
}

Note: This launches fresh Chrome Stable each time (no preserved auth).

Typical Testing Workflow

For apps requiring authentication:

  1. Start your dev server

    npm run dev  # Your app runs on localhost:3000
    
  2. Open Chrome Dev manually

    • Navigate to localhost:3000
    • Log in to your app
    • Chrome DevTools MCP auto-connects to this session
  3. Test tools with your auth session

    # In your MCP client (Claude, Cursor, etc.)
    "List the WebMCP tools on localhost:3000"
    → Uses your logged-in session
    → Tools see your cookies/auth
    → Can test authenticated endpoints
    

For apps without auth:

  • Just use default config
  • Chrome DevTools MCP will launch Chrome Dev automatically
  • Navigate to your app and test

Why Auto-Connect Matters for Testing

Without auto-connect (old way):

  • Chrome DevTools MCP launches new browser instance
  • No cookies, no auth, no browser state
  • Can't test authenticated features
  • Have to log in manually every time

With auto-connect (Chrome 145+):

  • Uses your existing Chrome Dev session
  • Preserves cookies, localStorage, auth tokens
  • Test authenticated tools immediately
  • Reuses browser profile with extensions

Example: Testing a Todo App with Auth

# Your workflow:
1. Open Chrome Dev, navigate to localhost:3000
2. Log in to your todo app
3. Add a todo manually (now you have data)

# Now test tools:
4. "List all WebMCP tools on this page"
   → list_webmcp_tools shows your todos tools
5. "Call the list_todos tool"
   → Returns todos from your logged-in session
6. "Create a new todo with text 'Test from AI'"
   → create_todo works with your auth session
7. Verify todo appears on screen

Without auto-connect, step 2 wouldn't work - you'd have to re-authenticate every time.

Using Available Resources

You have powerful tools at your disposal:

WebMCP Docs MCP (mcp__docs__SearchWebMcpDocumentation)

Use this for:

  • API syntax: "How do I use outputSchema in useWebMCP?"
  • Best practices: "WebMCP tool naming conventions"
  • Examples: "WebMCP form filling example"
  • Troubleshooting: "Why is my tool not re-registering?"

Example queries:

mcp__docs__SearchWebMcpDocumentation("useWebMCP deps array")
mcp__docs__SearchWebMcpDocumentation("outputSchema with Zod")
mcp__docs__SearchWebMcpDocumentation("tool annotations destructiveHint")

Chrome DevTools MCP (mcp__chrome-devtools__*)

Use this for:

  • Testing tools: Call them and verify behavior
  • Inspecting state: Read the page to see what's there
  • Debugging: Take screenshots, check console logs
  • Verification: Make sure tools work end-to-end

This is your testing environment. Use it constantly.

This Skill (Strategic Guidance)

Use this for:

  • Tool design principles
  • Implementation phases
  • Testing workflow
  • Strategic decisions

Don't use this for:

  • Specific API syntax (use WebMCP Docs MCP)
  • Debugging (use Chrome DevTools MCP)
  • Implementation details (use WebMCP Docs MCP)

Writing Skill Content (<script type="agent-context">)

Skills provide domain knowledge that tools can't structurally express — workflow guidance, lookup tables, inference rules, and constraints. Add skill content to your HTML using <script type="agent-context"> tags.

Reference Tool Names Directly

Skills should reference tool names explicitly. This helps models map domain knowledge to the correct tool calls:

<script type="agent-context">
  ---
  name: my-app
  description: Manage tasks in my app.
  tools:
    - create_task
    - list_tasks
    - update_task
  ---
  Create and manage tasks. Use `create_task` for new items
  and `list_tasks` to browse existing ones.

  Resources:
  - [workflow](references/workflow) — Step-by-step tool sequence for common tasks.
</script>

Skill Content Guidelines

  • tools array in frontmatter — List every tool the skill relates to. This creates an explicit coupling between the skill and its tools.
  • Reference tools in resource content — When describing workflows, parameters, or mappings, name the specific tool (e.g., "Pass the emoji to add_topping" not "Add the emoji as a topping").
  • Don't duplicate tool descriptions — The tool already describes what it does and how to call it. The skill describes when to use it, why, and domain knowledge the tool can't carry (lookup tables, business rules, workflow order).
  • Resources for progressive disclosure — Put detailed reference data (code tables, conversion tables, constraint lists) in separate <script type="agent-context/reference"> tags. This lets smaller models fetch knowledge on demand instead of loading everything upfront.

Example: Tool Reference in a Resource

<script type="agent-context/reference" data-skill="my-app" data-name="workflow">
  ## Build Order

  1. `set_size` — Choose the size first
  2. `set_style` — Pick a style
  3. `add_item` — Add items on top
  4. `share` — Share when done

  ## Size Inference

  When the user mentions a group size, infer the right value for `set_size`:

  | Group size | Value       |
  |------------|-------------|
  | 1-2 people | Small       |
  | 3-4 people | Medium      |
  | 5+ people  | Large       |
</script>

Workflow Summary

  1. Understand the app - What can humans do?
  2. Plan tools - List all needed tools by category
  3. Phase 1: Read - Build read-only tools
    • Test each with Chrome DevTools MCP
  4. Phase 2: Modify - Build read-write tools
    • Test each with Chrome DevTools MCP
    • Dogfood the experience
  5. Phase 3: Act - Build destructive tools
    • Test each with Chrome DevTools MCP
    • Extra careful validation
  6. Iterate - Use the tools, find gaps, improve

Remember

  • UI Parity: LLMs should be able to do everything humans can
  • Safety First: Categorize tools by read-only/read-write/destructive
  • Two-Tool Pattern: Separate filling from submission
  • Powerful Tools: One tool per complete task
  • Dogfood Everything: Test every tool with Chrome DevTools MCP
  • Iterate: The first version won't be perfect

You're not just adding tools - you're creating an interface for AI. Make it good.

Stats
Stars41
Forks6
Last CommitMar 15, 2026

Similar Skills