Create MCP tools for any website or web app. Use when the user wants to: - Add AI tools to their React/Vue/Next.js app - Create userscripts for sites they don't control (Notion, GitHub, etc.) - Test MCP tools on their running app (Rails, Django, Laravel) - Add MCP to vanilla JS/HTML apps - Create a distributable site package with tools + documentation Enables autonomous iteration: write code -> inject -> test -> fix -> repeat. Self-verifies via chrome-devtools-mcp. TypeScript files are auto-bundled via esbuild.
Creates and injects MCP tools into web applications for autonomous development and testing cycles.
/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:
references/HELPERS_API.mdreferences/PRODUCTION_TESTING.mdreferences/REACT_INTEGRATION.mdreferences/SELF_TESTING.mdreferences/TOOL_DESIGN.mdreferences/TROUBLESHOOTING.mdreferences/USERSCRIPT_GUIDE.mdreferences/VANILLA_JS.mdCreate MCP tools for any website or web app. Inject, test, iterate.
The universal development loop:
navigate_page to target URLnavigate_page with type="reload" (clears stale state from prior sessions)take_snapshot to understand page structureexecute: function)inject_webmcp_script - polyfill auto-injected if neededdiff_webmcp_tools to see registered toolsmcp__chrome-devtools__webmcp_{domain}_page{idx}_{name}list_console_messages if failuresIMPORTANT: Always reload the page before injecting to avoid stale polyfill issues.
// Minimal tool - no imports needed!
navigator.modelContext.registerTool({
name: 'get_page_title',
description: 'Get the current page title',
inputSchema: { type: 'object', properties: {} },
execute: async () => ({
content: [{ type: 'text', text: document.title }]
})
});
React/Vue/Next.js app? -> See REACT_INTEGRATION.md
External site (Notion, GitHub)? -> See USERSCRIPT_GUIDE.md
Running Rails/Django/Laravel app? -> See PRODUCTION_TESTING.md
Vanilla JS/HTML? -> See VANILLA_JS.md
After injection, tools become first-class MCP tools:
| Your Tool Name | Becomes |
|---|---|
search_pages | webmcp_notion_so_page0_search_pages |
list_orders | webmcp_localhost_3000_page0_list_orders |
The key tool for development iteration:
// Option 1: Inline code (quick prototyping)
inject_webmcp_script({
code: `
navigator.modelContext.registerTool({
name: 'my_tool',
description: 'Description here',
inputSchema: { type: 'object', properties: {} },
execute: async () => ({
content: [{ type: 'text', text: 'Result' }]
})
});
`,
wait_for_tools: true, // Wait for registration (default: true)
timeout: 5000 // Max wait time ms (default: 5000)
})
// Option 2: TypeScript file (production development)
inject_webmcp_script({
file_path: './tools/src/mysite.ts'
// TypeScript auto-bundled via esbuild (~10ms)
// Imports from node_modules resolved automatically
})
Features:
navigator.modelContext)NOTE: Not published yet. The site-package template includes inline helpers until published.
Lightweight tree-shakable helpers for userscripts:
// Helper functions (inlined in template until package published)
function textResponse(text: string): ToolResponse
function jsonResponse(data: unknown, indent = 2): ToolResponse
function errorResponse(message: string): ToolResponse
function getAllElements(selector: string): Element[]
function getText(selectorOrElement: string | Element | null): string | null
function waitForElement(selector: string, timeout = 5000): Promise<Element>
See HELPERS_API.md for full API.
After injection, tools are automatically registered with the Chrome DevTools MCP server
and become immediately callable. The server sends tools/list_changed notifications
to inform clients about new tools.
Tools follow the pattern: webmcp_{sanitized_domain}_page{N}_{tool_name}
Examples:
webmcp_news_ycombinator_com_page0_get_storieswebmcp_old_reddit_com_page0_get_postswebmcp_github_com_page1_search_reposWhen calling tools in Claude Code, use the MCP server prefix:
mcp__chrome-devtools__webmcp_news_ycombinator_com_page0_get_stories({
limit: 10
})
The mcp__chrome-devtools__ prefix routes the call to the correct MCP server.
When using the MCP SDK directly (e.g., in test scripts), call by the tool name only:
await client.callTool({
name: 'webmcp_news_ycombinator_com_page0_get_stories',
arguments: { limit: 10 }
})
No prefix needed - the SDK already knows which server to call.
CRITICAL: Verify every tool before considering done.
diff_webmcp_tools -> Tools appear as webmcp_{domain}_page{idx}_{name}list_console_messages + take_snapshotSee SELF_TESTING.md for detailed protocol.
Categories by safety:
readOnlyHint: true): No side effectsdestructiveHint: true): Permanent actionsTwo-tool pattern for forms:
fill_*_form (read-write) - populate fieldssubmit_*_form (destructive) - commit changesHandler return format:
// Success
return {
content: [{ type: 'text', text: 'Result data' }]
};
// Error
return {
content: [{ type: 'text', text: 'Error message' }],
isError: true
};
See TOOL_DESIGN.md for patterns.
navigator.modelContext.registerTool({
name: 'get_items',
description: 'Get items from the page',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max items (default: 10)' }
}
},
execute: async ({ limit = 10 }) => {
const items = [];
document.querySelectorAll('.item').forEach((el, i) => {
if (i >= limit) return;
items.push({
title: el.querySelector('.title')?.textContent?.trim() || '',
url: el.querySelector('a')?.href || ''
});
});
return {
content: [{ type: 'text', text: JSON.stringify(items, null, 2) }]
};
}
});
// Step 1: Fill form (read-write, reversible)
navigator.modelContext.registerTool({
name: 'fill_contact_form',
description: 'Fill contact form fields. Call submit_contact_form to send.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' },
message: { type: 'string' }
}
},
execute: async ({ name, email, message }) => {
if (name) document.querySelector('#name').value = name;
if (email) document.querySelector('#email').value = email;
if (message) document.querySelector('#message').value = message;
return { content: [{ type: 'text', text: 'Form filled' }] };
}
});
// Step 2: Submit form (destructive)
navigator.modelContext.registerTool({
name: 'submit_contact_form',
description: 'Submit the contact form. Sends the message.',
inputSchema: { type: 'object', properties: {} },
execute: async () => {
document.querySelector('form#contact').submit();
return { content: [{ type: 'text', text: 'Form submitted' }] };
}
});
navigator.modelContext.registerTool({
name: 'navigate_to',
description: 'Navigate to a section',
inputSchema: {
type: 'object',
properties: {
section: { type: 'string', enum: ['home', 'about', 'contact'] }
},
required: ['section']
},
execute: async ({ section }) => {
const urls = { home: '/', about: '/about', contact: '/contact' };
window.location.href = urls[section];
return { content: [{ type: 'text', text: `Navigating to ${section}...` }] };
}
});
Use when:
Don't use when:
Search docs for specifics:
mcp__docs__SearchWebMcpDocumentation("registerTool inputSchema")
mcp__docs__SearchWebMcpDocumentation("tool annotations")
Reference implementations available in examples/:
hackernews.js - Hacker Newsgithub.js - GitHub repositorieswikipedia.js - Wikipedia articlesreddit.js - Reddit posts/commentsyoutube.js - YouTube videostwitter.js - Twitter/Xamazon.js - Amazon productslinkedin.js - LinkedIn profiles/jobsstackoverflow.js - Stack Overflow Q&Anotion.js - Notion pages/databasesgoogle-docs.js - Google DocsFor distributable tools + documentation, use the site package template:
# Copy template
cp -r templates/site-package ./mysite-mcp
# Update placeholders: {{site}}, {{Site}}, {{site_url}}
# Edit: SKILL.md, tools/src/{{site}}.ts, etc.
# Install dependencies
cd mysite-mcp/tools && npm install
# Develop tools
inject_webmcp_script({ file_path: "./mysite-mcp/tools/src/mysite.ts" })
Site package structure:
mysite-mcp/
├── SKILL.md # Main skill (Setup + Workflows)
├── tools/
│ ├── package.json # Dependencies (@webmcp/helpers)
│ └── src/mysite.ts # Tool implementations
├── reference/
│ ├── api.md # Detailed API docs
│ └── workflows.md # Usage examples
└── README.md
The SKILL.md includes a Setup section that tells future agents where to find and inject the tools.
Common issues:
| Issue | Solution |
|---|---|
| Connection timeout / stale polyfill | navigate_page with type="reload", then reinject |
| CSP blocking injection | Use browser extension approach |
| Tools not registering | Check console for errors |
| Stale tools after navigation | Reinject script |
| Selector not finding element | Use take_snapshot to verify page state |
| TypeScript build fails | Check for syntax errors, run pnpm install |
| Imports not resolved | Ensure dependencies in package.json, run install |
handler not recognized | Use execute: instead of handler: in tool definition |
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.