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.
Guides developers through designing and implementing WebMCP tools for AI-accessible web applications with strategic patterns and testing workflows.
/plugin marketplace add https://www.claudepluginhub.com/api/plugins/webmcp-org-webmcp-setup/marketplace.json/plugin install webmcp-org-webmcp-setup@cpd-webmcp-org-webmcp-setupThis skill is limited to using the following tools:
CHANGELOG.mdCONTRIBUTING.mdREADME.mdassets/templates/vanilla-demo.htmlexamples/COMMON_APPS.mdpackage.jsonreferences/ADVANCED_PATTERNS.mdreferences/INSTALLATION.mdreferences/REACT_SETUP.mdreferences/TOOL_PATTERNS.mdreferences/TROUBLESHOOTING.mdscripts/verify-setup.jsCore 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.
For installation: See references/INSTALLATION.md or mcp__docs__SearchWebMcpDocumentation("setup guide")
| Phase | What You're Building | Tools to Use |
|---|---|---|
| Understanding | Learn WebMCP patterns | mcp__docs__SearchWebMcpDocumentation |
| Planning | Design tool architecture | This skill (you're reading it) |
| Implementing | Write tool code | mcp__docs__SearchWebMcpDocumentation for APIs |
| Testing | Dogfood every tool | mcp__chrome-devtools__* tools (requires Chrome Dev 145+ for auth testing) |
| Iterating | Refine based on usage | Chrome DevTools MCP + dogfooding |
✅ Every major UI action has a corresponding tool
✅ Tools are categorized by safety
✅ Forms use two-tool pattern
fill_*_form (read-write) + submit_*_form (destructive)✅ All tools tested with Chrome DevTools MCP
✅ Tools are powerful, not granular
Organize tools into three categories:
readOnlyHint: true)Purpose: Let the LLM understand the current state
Characteristics:
Examples:
list_todos - Get all todos with filteringget_user_profile - Get current user datasearch_products - Search product catalogget_cart_contents - See what's in cartPurpose: Modify UI state in a non-destructive way
Characteristics:
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)destructiveHint: true)Purpose: Take permanent, irreversible actions
Characteristics:
Examples:
submit_order - Actually place the orderdelete_item - Permanently remove itemsend_message - Send email/messagecreate_account - Register new userCRITICAL PRINCIPLE: Separate filling from submission
// ❌ 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:
// ✅ 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:
Mental Model: For every major action a human can take in your UI, create a corresponding tool.
Audit Process:
Example Audit - Todo App:
list_todosfill_todo_form, create_todomark_todo_completedelete_todoset_filterUI Parity Achieved: LLM can do everything a human can do.
Principle: One tool should accomplish a complete task, not just one tiny piece.
// ❌ 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
// ✅ 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
For detailed patterns that significantly impact tool quality, see:
references/ADVANCED_PATTERNS.md - Covers:
domain_verb_noun)When to read: After understanding core principles, before implementing complex tools.
Always search docs for specifics: mcp__docs__SearchWebMcpDocumentation("your question")
When using @mcp-b/react-webmcp hooks, follow these patterns for optimal performance and reliability.
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) }] };
}
});
Output schemas are MANDATORY for modern WebMCP integrations. They enable:
// ❌ 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:
outputSchema: AI receives CallToolResult.content[0].text (string)outputSchema: AI receives CallToolResult.structuredContent (typed object)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 componentoutputSchema - Always memoize or define outside componentannotations - Always memoize or define outside componentWhat does NOT need memoization:
handler - Stored in a ref, changes don't trigger re-registrationonSuccess / onError - Stored in refsformatOutput - Stored in a refUse 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:
[count, id] not [{ count, id }]items.map(i => i.id).join(',') not [items]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
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>;
}
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:
useWebMCPContext for simple read-only context (current page, user session)useWebMCP when you need outputSchema for structured responsesuseWebMCP when you need execution state tracking or callbacksuseWebMCP 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>
);
}
See the Chrome DevTools MCP setup section below for how to test React tools with auto-connect to preserve authentication.
Key workflow:
npm run dev)outputSchemaWhy this matters for React:
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>;
}
1. StrictMode Double Rendering
2. Hot Module Replacement (HMR)
3. Async State Updates
setState// ❌ 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
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>
);
}
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")
See examples/COMMON_APPS.md for complete tool structures for:
Each pattern shows the full tool hierarchy (read → write → destructive) with specific examples.
Goal: Give the LLM eyes. Let it understand what's on screen.
What to build:
List tools - Get collections of items
list_todos, list_products, list_usersGet tools - Get specific item details
get_todo_by_id, get_product_details, get_user_profileSearch tools - Find specific information
search_products, search_logs, search_messagesStatus tools - Get current application state
get_cart_contents, get_current_filters, get_themeWhy first?:
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)
Goal: Let the LLM interact with the UI without permanent consequences.
What to build:
Fill tools - Populate forms (but don't submit)
fill_contact_form, fill_checkout_form, fill_profile_formSet tools - Change UI state
set_filter, set_search_query, set_theme, set_languageNavigate tools - Move between pages
navigate_to_page, open_modal, switch_tabWhy second?:
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.
Goal: Let the LLM make permanent changes and complete workflows.
What to build:
Submit tools - Actually commit forms
submit_contact_form, submit_order, submit_profile_updateCreate tools - Add new records
create_todo, create_user, create_postDelete tools - Remove items permanently
delete_todo, delete_user, delete_postAction tools - Other permanent state changes
mark_complete, send_message, publish_postWhy last?:
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
MOST IMPORTANT PART: You MUST test every tool with Chrome DevTools MCP.
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:
If you DO test:
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:
npm run dev)Repeat this for every single tool. No exceptions.
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:
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.
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.
You'll discover:
Fix these immediately. Dogfooding gives you this feedback.
To dogfood WebMCP tools effectively, you need Chrome DevTools MCP properly configured. Here's how to set it up for optimal testing workflow:
Auto-Connect Feature Requires Chrome 145+
The auto-connect feature (connects to running Chrome with your cookies/auth) requires:
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
Option 1: Auto-Connect to Running Chrome (Best for Testing)
Use this when:
MCP Config:
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "@mcp-b/chrome-devtools-mcp@latest"]
}
}
}
What it does:
Option 2: Always Launch Fresh Instance (Headless Testing)
Use this when:
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).
For apps requiring authentication:
Start your dev server
npm run dev # Your app runs on localhost:3000
Open Chrome Dev manually
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:
Without auto-connect (old way):
With auto-connect (Chrome 145+):
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.
You have powerful tools at your disposal:
mcp__docs__SearchWebMcpDocumentation)Use this for:
Example queries:
mcp__docs__SearchWebMcpDocumentation("useWebMCP deps array")
mcp__docs__SearchWebMcpDocumentation("outputSchema with Zod")
mcp__docs__SearchWebMcpDocumentation("tool annotations destructiveHint")
mcp__chrome-devtools__*)Use this for:
This is your testing environment. Use it constantly.
Use this for:
Don't use this for:
You're not just adding tools - you're creating an interface for AI. Make it good.
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.