From webmcp-setup
Develops MCP tools for websites and web apps via JS/TS injection and Chrome DevTools testing. For React/Vue/Next.js apps, userscripts (Notion/GitHub), Rails/Django/Laravel testing, vanilla JS/HTML.
How this skill is triggered — by the user, by Claude, or both
Slash command
/webmcp-setup:webmcpThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create MCP tools for any website or web app. Inject, test, iterate.
Create 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 |
npx claudepluginhub webmcp-org/npm-packagesGuides adding WebMCP to web applications for AI accessibility, LLM UI tools, and MCP browser automation. Covers design principles, tool architecture, and testing workflows.
Converts existing web apps to hybrid MCP Apps that render standalone or inline in MCP hosts, using shared UI code, context detection, and server tool registration.
Applies WebMCP patterns for SPA tool lifecycle, error handling, performance optimization, multi-site agents, accessibility, SEO, and production deployment.