diff --git a/CLAUDE.md b/CLAUDE.md index 9ebe866..9e78ef6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,20 +1,63 @@ # Mintlify documentation +## Diataxis Documentation Framework + +This documentation follows the [Diataxis framework](https://diataxis.fr/), which organizes content into four distinct types. **Do not mix content types within a single page.** + +### The Four Content Types + +| Type | Purpose | User Need | Directory | +|------|---------|-----------|-----------| +| **Tutorials** | Learn by doing | "Teach me" | `/tutorials/` | +| **How-to Guides** | Solve specific problems | "Help me do X" | `/how-to/` | +| **Reference** | Look up facts | "What are the details?" | `/packages/`, `/concepts/` | +| **Explanation** | Understand concepts | "Help me understand why" | `/concepts/` | + +### Tutorials (`/tutorials/`) +- Guide users through building a complete project +- Focus on ONE path - don't present alternatives +- Don't explain "why" - just teach through doing +- Include checkpoints where users can verify success +- Example: "Build your first WebMCP tool with React" + +### How-to Guides (`/how-to/`) +- Solve specific problems or tasks +- Assume the reader already knows the basics +- Action-focused: minimal explanation, maximum doing +- Can be read non-linearly +- Example: "How to handle errors in tools" + +### Reference (`/packages/`, `/concepts/*.mdx`) +- Technical descriptions and facts +- Complete, accurate, austere +- Structure mirrors the thing being described +- No tutorials or explanations mixed in +- Example: "registerTool() API parameters" + +### Explanation (`/concepts/why-webmcp.mdx`, `/concepts/architecture.mdx`) +- Help users understand concepts deeply +- Discuss "why" not "how" +- Can include opinions and perspectives +- Designed for reading away from keyboard +- Example: "Why WebMCP vs browser automation" + +### Common Mistakes to Avoid +- **Don't mix types**: A tutorial that stops to explain "why" breaks flow +- **Don't add alternatives to tutorials**: Guide ONE path completely +- **Don't explain in reference docs**: Just state the facts +- **Don't include procedures in explanations**: Save the "how" for tutorials/guides + ## Documentation structure overview -The WebMCP documentation uses a flat + organized directory structure: +The WebMCP documentation uses a Diataxis-organized directory structure: -### Root-level pages -Core documentation pages live in the repository root: -- **Introduction & Getting Started**: `introduction.mdx`, `quickstart.mdx`, `development.mdx` -- **Core Guides**: `best-practices.mdx`, `security.mdx`, `troubleshooting.mdx`, `advanced.mdx` -- **Use Cases**: `building-mcp-ui-apps.mdx`, `connecting-agents.mdx`, `frontend-tools.mdx` -- **Examples & Reference**: `examples.mdx`, `live-tool-examples.mdx`, `changelog.mdx` +### Content type directories +- **`/tutorials/`**: Step-by-step learning content (Diataxis: Tutorials) +- **`/how-to/`**: Task-focused problem-solving guides (Diataxis: How-to Guides) +- **`/concepts/`**: Explanations and reference material (Diataxis: Explanation + Reference) +- **`/packages/`**: NPM package API reference (Diataxis: Reference) -### Organized directories -Specialized content is organized into directories: -- **`/concepts/`**: Core concepts (architecture, tool design, schemas, security, performance, glossary) -- **`/packages/`**: NPM package reference (react-webmcp, transports, smart-dom-reader, etc.) +### Other directories - **`/calling-tools/`**: How agents call WebMCP tools (embedded agent, AI browsers, extension, devtools) - **`/frameworks/`**: Framework-specific integration guides (Vue, Svelte, Angular, Rails, etc.) - **`/extension/`**: Browser extension documentation (agents, userscripts) diff --git a/best-practices.mdx b/best-practices.mdx index 46ca46e..7e14ff3 100644 --- a/best-practices.mdx +++ b/best-practices.mdx @@ -1,14 +1,145 @@ --- -title: Best Practices for Creating Tools -description: Learn best practices for designing and implementing WebMCP tools on websites you control. Tool design principles, naming conventions, security, and optimal AI integration. +title: Best Practices +description: Learn best practices for designing and implementing WebMCP tools. Quick reference to detailed how-to guides. icon: list-check --- -This guide covers the complete tool development lifecycle: from naming and schema design, through implementation and error handling, to testing and monitoring. Unlike userscripts where you work around existing structures, controlling your entire website lets you design tools from the ground up for optimal AI integration. +This page provides a quick overview of WebMCP best practices with links to detailed how-to guides. - -These practices apply when you own the website and can integrate WebMCP tools directly into your codebase. For userscript development, see [Managing Userscripts](/extension/managing-userscripts). - + +**Looking for step-by-step guidance?** See the [How-to Guides](/how-to) for detailed instructions on specific tasks. + + +## Tool Design + +Create tools that AI agents can discover and use effectively. + + + + Naming conventions, descriptions, and consolidating related operations + + + + Use Zod and JSON Schema for robust input validation + + + +### Quick Tips + +- Use `domain_verb_noun` naming pattern (e.g., `cart_add_item`) +- Write detailed descriptions - it's the only info AI has about your tool +- Add `.describe()` to every Zod parameter +- Consolidate related operations into one powerful tool + +## Error Handling + +Return helpful error messages that AI can use to recover or inform users. + + + Authentication checks, authorization, network failures, and validation errors + + +### Quick Tips + +- Always use `isError: true` for failures +- Check authentication before protected operations +- Provide actionable error messages +- Use timeouts for network requests + +## Response Format + +Format responses so AI can present information clearly. + + + Use markdown instead of JSON for better AI comprehension + + +### Quick Tips + +- Return markdown strings, not JSON objects +- Use headers, lists, and formatting +- Include helpful context and metadata + +## Performance + +Make tools respond quickly for voice and real-time interactions. + + + Optimistic updates, caching, and avoiding blocking operations + + +### Quick Tips + +- Update local state before returning (optimistic updates) +- Sync to backend in background +- Limit tool count - consolidate related operations +- Cache expensive operations + +## Testing + +Verify tools work correctly before deploying. + + + Unit testing, React hook testing, and manual testing with MCP-B Extension + + +### Quick Tips + +- Test tool registration configuration +- Test handlers with valid and invalid inputs +- Test error scenarios +- Manual test with MCP-B Extension + +## Security + +Protect your application and users. + + + Authentication, authorization, input sanitization, and threat models + + +### Quick Tips + +- Only expose tools users can already access via UI +- Validate user authentication and authorization +- Sanitize all inputs +- Never expose sensitive data in responses + +## All How-to Guides + + + + Tool naming, descriptions, and schemas + + + + Error responses and recovery patterns + + + + Zod and JSON Schema validation + + + + Markdown formatting for AI + + + + Optimistic updates and caching + + + + Unit and integration testing + + + +--- + +## Detailed Reference (Legacy) + +The sections below contain the original detailed content. For updated guidance, see the linked how-to guides above. + + ## Tool Design Principles @@ -1260,3 +1391,5 @@ ${results.map(r => `- ${r.title}`).join('\n')}` See real-world implementations + + diff --git a/docs.json b/docs.json index 5de3dbd..809b81a 100644 --- a/docs.json +++ b/docs.json @@ -76,10 +76,25 @@ ] }, { - "group": "Guides", + "group": "Tutorials", + "icon": "graduation-cap", "pages": [ - "connecting-agents", - "best-practices", + "tutorials/index", + "tutorials/first-tool-vanilla", + "tutorials/first-tool-react" + ] + }, + { + "group": "How-to Guides", + "icon": "wrench", + "pages": [ + "how-to/index", + "how-to/design-tools", + "how-to/handle-errors", + "how-to/validate-input", + "how-to/format-responses", + "how-to/optimize-performance", + "how-to/test-tools", "development", "advanced", "building-mcp-ui-apps", @@ -108,7 +123,8 @@ ] }, { - "group": "NPM Packages", + "group": "API Reference", + "icon": "book", "pages": [ "packages/global", "packages/transports", @@ -121,7 +137,7 @@ ] }, { - "group": "Reference", + "group": "Resources", "pages": [ "changelog", "troubleshooting", diff --git a/how-to/design-tools.mdx b/how-to/design-tools.mdx new file mode 100644 index 0000000..6f4082a --- /dev/null +++ b/how-to/design-tools.mdx @@ -0,0 +1,196 @@ +--- +title: 'How to Design Effective Tools' +sidebarTitle: 'Design Effective Tools' +description: 'Create WebMCP tools that AI agents can discover and use effectively.' +icon: 'pen-ruler' +--- + +This guide covers how to design tools that AI agents can understand and use effectively. Good tool design makes the difference between an AI that fumbles and one that works smoothly. + +## Use Descriptive Names + +Follow a consistent naming pattern. Use `domain_verb_noun` or `verb_noun` format: + +```javascript +// Good names +navigator.modelContext.registerTool({ + name: 'products_search', // Clear domain + action + name: 'cart_add_item', // Verb + noun pattern + name: 'orders_get_status', // Descriptive and specific +}); + +// Avoid +navigator.modelContext.registerTool({ + name: 'search', // Too generic + name: 'doAction', // Unclear purpose + name: 'helper1', // Meaningless +}); +``` + + + + - `products_search` + - `cart_add_item` + - `user_get_profile` + - `orders_list_recent` + + + + - `doStuff` + - `action1` + - `helper` + - `processData` + + + +## Write Detailed Descriptions + + +Tool names, descriptions, and input schemas are the ONLY information the AI model has about your tools. Make them detailed and informative. + + +Include everything the AI needs to know: + +```javascript +// Vague - AI doesn't know when to use this +navigator.modelContext.registerTool({ + name: 'products_search', + description: 'Searches for products', +}); + +// Better - AI understands exactly how to use it +navigator.modelContext.registerTool({ + name: 'products_search', + description: `Search products by name, category, or SKU. + +Returns paginated results with title, price, stock status, and product URLs. +Use this when users ask about product availability or pricing. + +If searching by price range, use the minPrice and maxPrice parameters. +Results are sorted by relevance by default.`, +}); +``` + +**Include in your descriptions:** +- What the tool does and when to use it +- What data it returns and in what format +- When to use it vs similar tools +- Any important limitations or constraints +- Prerequisites or dependencies on other tools + +## Use Parameter Descriptions + +Add `.describe()` to every Zod parameter: + +```typescript +inputSchema: { + query: z.string() + .min(1) + .describe('Product name, category, or SKU. Examples: "laptop", "running shoes"'), + + minPrice: z.number() + .positive() + .optional() + .describe('Minimum price filter in USD'), + + limit: z.number() + .int() + .min(1) + .max(100) + .default(10) + .describe('Results per page (1-100, default: 10)') +} +``` + +## Consolidate Related Operations + +Prefer one powerful tool over many single-purpose tools: + + + + ```javascript + // One tool handling related operations + navigator.modelContext.registerTool({ + name: 'cart_manager', + description: 'Manage cart operations: add, remove, view, clear, update quantity', + inputSchema: { + action: z.enum(['add', 'remove', 'view', 'clear', 'update']) + .describe('The cart operation to perform'), + productId: z.string().optional() + .describe('Product ID (required for add, remove, update)'), + quantity: z.number().positive().optional() + .describe('Quantity (required for add and update)') + }, + }); + ``` + + + + ```javascript + // Multiple tools consuming unnecessary context + registerTool({ name: 'cart_add_item', ... }); + registerTool({ name: 'cart_remove_item', ... }); + registerTool({ name: 'cart_get_contents', ... }); + registerTool({ name: 'cart_clear', ... }); + registerTool({ name: 'cart_update_quantity', ... }); + ``` + + + +**Benefits of consolidated tools:** +- Reduces context consumption (fewer tool definitions) +- Fewer tools to maintain and document +- Simpler tool discovery for AI agents + +## Reference Related Tools + +Tell the AI when tools work together: + +```typescript +navigator.modelContext.registerTool({ + name: 'cart_checkout', + description: `Proceed to checkout with current cart contents. + +Prerequisites: +- User must be authenticated +- Cart must contain at least one item (call cart_get_contents to verify) +- Shipping address must be set (call user_set_shipping_address if needed) + +Returns a checkout URL where user can complete payment.`, +}); +``` + +## Quick Checklist + + + + - Use `domain_verb_noun` pattern + - Be specific and descriptive + - Avoid generic names like "helper" or "action" + + + + - Explain what, when, and why + - Describe return format + - List prerequisites and dependencies + - Mention related tools + + + + - Add `.describe()` to every parameter + - Include examples in descriptions + - Set sensible defaults + + + +## Related + + + + Define robust input schemas + + + + Return responses AI can use well + + diff --git a/how-to/format-responses.mdx b/how-to/format-responses.mdx new file mode 100644 index 0000000..7b5f4f2 --- /dev/null +++ b/how-to/format-responses.mdx @@ -0,0 +1,201 @@ +--- +title: 'How to Format Responses' +sidebarTitle: 'Format Responses' +description: 'Return markdown responses that AI agents can present clearly to users.' +icon: 'file-lines' +--- + +This guide shows how to format tool responses so AI agents can present information clearly. + +## Use Markdown Instead of JSON + +AI models work better with markdown than raw JSON: + + + + ```javascript + async execute({ query }) { + const results = await searchProducts(query); + + const markdown = `# Search Results for "${query}" + +Found ${results.length} products: + +${results.map((p, i) => ` +${i + 1}. **${p.name}** - $${p.price} + - SKU: ${p.sku} + - Stock: ${p.inStock ? '✓ In Stock' : '✗ Out of Stock'} + - [View Product](${p.url}) +`).join('\n')} + +--- +*Showing ${results.length} of ${results.total} total results*`; + + return { + content: [{ type: "text", text: markdown }] + }; + } + ``` + + + + ```javascript + // JSON is harder for AI to parse and present + async execute({ query }) { + const results = await searchProducts(query); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + data: results, + meta: { total: results.length } + }, null, 2) + }] + }; + } + ``` + + + +**Benefits of markdown:** +- More natural for AI to read and present +- Better formatting in chat interfaces +- More human-readable in logs + +## Structure Your Responses + +Use headers, lists, and formatting: + +```javascript +async execute({ orderId }) { + const order = await getOrder(orderId); + + return { + content: [{ + type: "text", + text: `# Order #${order.id} + +**Status:** ${order.status} +**Placed:** ${new Date(order.createdAt).toLocaleDateString()} +**Total:** $${order.total.toFixed(2)} + +## Items +${order.items.map(item => ` +- **${item.name}** x${item.quantity} - $${(item.price * item.quantity).toFixed(2)} +`).join('')} + +## Shipping +${order.shippingAddress.street} +${order.shippingAddress.city}, ${order.shippingAddress.state} ${order.shippingAddress.zip} + +${order.trackingNumber + ? `**Tracking:** ${order.trackingNumber}` + : '*Tracking number not yet available*'} + +--- +*Order placed on ${new Date(order.createdAt).toLocaleString()}*` + }] + }; +} +``` + +## Format Lists Clearly + +```javascript +// Product list +const markdown = `Found ${products.length} products: + +${products.map((p, i) => `${i + 1}. **${p.name}** - $${p.price}`).join('\n')}`; + +// Task list with status +const markdown = `## Your Tasks + +${tasks.map(t => `- [${t.done ? 'x' : ' '}] ${t.title}`).join('\n')}`; + +// Numbered steps +const markdown = `## How to proceed + +1. Review your cart +2. Enter shipping address +3. Complete payment + +Click **Checkout** when ready.`; +``` + +## Include Helpful Context + +Add metadata that helps the AI understand the response: + +```javascript +async execute({ query, page }) { + const results = await search(query, page); + + return { + content: [{ + type: "text", + text: `# Search Results + +**Query:** "${query}" +**Page:** ${page} of ${results.totalPages} +**Showing:** ${results.items.length} of ${results.totalItems} results + +${results.items.map(item => `- ${item.title}`).join('\n')} + +${page < results.totalPages + ? `*Use page ${page + 1} to see more results*` + : '*This is the last page of results*'}` + }] + }; +} +``` + +## Format Errors as Markdown + +Error messages benefit from formatting too: + +```javascript +return { + content: [{ + type: "text", + text: `**Error:** Product not found + +The product ID \`${productId}\` does not exist in our catalog. + +**What to try:** +- Check the product ID for typos +- Search for the product by name using \`products_search\` +- Browse categories to find similar products` + }], + isError: true +}; +``` + +## Quick Patterns + +```javascript +// Simple success +return { content: [{ type: "text", text: "Task completed successfully." }] }; + +// With count +return { content: [{ type: "text", text: `Added ${count} items to cart.` }] }; + +// With link +return { content: [{ type: "text", text: `View order: [Order #${id}](${url})` }] }; + +// Confirmation +return { content: [{ type: "text", text: `**Deleted:** ${item.name}` }] }; +``` + +## Related + + + + Format error responses + + + + Write effective descriptions + + diff --git a/how-to/handle-errors.mdx b/how-to/handle-errors.mdx new file mode 100644 index 0000000..24a9dae --- /dev/null +++ b/how-to/handle-errors.mdx @@ -0,0 +1,253 @@ +--- +title: 'How to Handle Errors' +sidebarTitle: 'Handle Errors' +description: 'Return helpful error messages and handle failures gracefully in WebMCP tools.' +icon: 'triangle-exclamation' +--- + +This guide shows how to handle errors in your WebMCP tools so AI agents can understand what went wrong and help users recover. + +## Return Error Responses + +Use the `isError: true` flag to signal failures: + +```javascript +async execute({ productId }) { + const product = await getProduct(productId); + + if (!product) { + return { + content: [{ + type: "text", + text: `Product ${productId} not found. Please verify the product ID.` + }], + isError: true + }; + } + + // Success path... +} +``` + +## Check Authentication First + +Always verify the user is logged in before accessing protected resources: + +```javascript +async execute({ userId }) { + const currentUser = await getCurrentUser(); + + if (!currentUser) { + return { + content: [{ + type: "text", + text: "User not authenticated. Please log in first." + }], + isError: true + }; + } + + // Continue with authenticated user... +} +``` + +## Check Authorization + +Verify the user has permission for the requested action: + +```javascript +async execute({ userId }) { + const currentUser = await getCurrentUser(); + + if (currentUser.id !== userId && !currentUser.isAdmin) { + return { + content: [{ + type: "text", + text: "Unauthorized. You can only access your own profile." + }], + isError: true + }; + } + + // Continue with authorized action... +} +``` + +## Handle Network Failures + +Wrap external calls in try-catch: + +```javascript +async execute({ query }) { + try { + const results = await fetch('/api/search', { + method: 'POST', + body: JSON.stringify({ query }), + signal: AbortSignal.timeout(5000) // 5 second timeout + }); + + if (!results.ok) { + throw new Error(`API returned ${results.status}`); + } + + const data = await results.json(); + return { + content: [{ type: "text", text: formatResults(data) }] + }; + + } catch (error) { + console.error('Search failed:', error); + + return { + content: [{ + type: "text", + text: `Search failed: ${error.message}. Please try again.` + }], + isError: true + }; + } +} +``` + +## Use Specific Error Codes + +Create structured error responses for consistency: + +```typescript +enum ErrorCode { + UNAUTHORIZED = 'UNAUTHORIZED', + NOT_FOUND = 'NOT_FOUND', + VALIDATION_ERROR = 'VALIDATION_ERROR', + RATE_LIMIT = 'RATE_LIMIT', + SERVER_ERROR = 'SERVER_ERROR' +} + +function formatError(code: ErrorCode, message: string, details?: string) { + return { + content: [{ + type: "text", + text: `**Error (${code}):** ${message}${details ? `\n\n${details}` : ''}` + }], + isError: true + }; +} + +// Usage +if (rateLimitExceeded) { + return formatError( + ErrorCode.RATE_LIMIT, + 'Too many requests.', + 'Please try again in 60 seconds.' + ); +} +``` + +## Validate Business Rules + +Check constraints beyond just types: + +```javascript +async execute({ productId, quantity }) { + const product = await getProduct(productId); + + if (!product) { + return { + content: [{ + type: "text", + text: `Product ${productId} not found.` + }], + isError: true + }; + } + + if (product.stock < quantity) { + return { + content: [{ + type: "text", + text: `Only ${product.stock} units available. Requested: ${quantity}.` + }], + isError: true + }; + } + + // Proceed with order... +} +``` + +## Complete Example + +Here's a comprehensive error handling pattern: + +```javascript +async execute({ userId }) { + try { + // 1. Check authentication + const currentUser = await getCurrentUser(); + if (!currentUser) { + return { + content: [{ type: "text", text: "Please log in first." }], + isError: true + }; + } + + // 2. Check authorization + if (currentUser.id !== userId && !currentUser.isAdmin) { + return { + content: [{ type: "text", text: "You can only access your own profile." }], + isError: true + }; + } + + // 3. Fetch with timeout + const profile = await fetchWithTimeout( + `/api/users/${userId}`, + { timeout: 5000 } + ); + + // 4. Handle not found + if (!profile) { + return { + content: [{ type: "text", text: `User ${userId} not found.` }], + isError: true + }; + } + + // 5. Return success + return { + content: [{ + type: "text", + text: `# User Profile\n\n**Name:** ${profile.name}\n**Email:** ${profile.email}` + }] + }; + + } catch (error) { + // 6. Catch unexpected errors + console.error('Profile fetch error:', error); + return { + content: [{ type: "text", text: `Failed to fetch profile: ${error.message}` }], + isError: true + }; + } +} +``` + +## Quick Checklist + +- Always use `isError: true` for failures +- Check authentication before protected operations +- Check authorization for user-specific data +- Use timeouts for network requests +- Provide actionable error messages +- Log errors for debugging + +## Related + + + + Prevent errors with input validation + + + + Format error messages as markdown + + diff --git a/how-to/index.mdx b/how-to/index.mdx new file mode 100644 index 0000000..8a9e26f --- /dev/null +++ b/how-to/index.mdx @@ -0,0 +1,76 @@ +--- +title: 'How-to Guides' +description: 'Practical guides for solving specific problems with WebMCP. Task-focused instructions for developers.' +icon: 'wrench' +--- + +How-to guides help you accomplish specific tasks. They assume you have basic WebMCP knowledge and focus on solving real problems. + + +**New to WebMCP?** Start with a [Tutorial](/tutorials) first to learn the basics. + + +## Tool Development + + + + Create tools that AI agents can use effectively. Naming, descriptions, and schemas. + + + + Return helpful error messages and handle failures gracefully. + + + + Use Zod and JSON Schema for robust input validation. + + + + Return markdown responses that AI agents can present well. + + + +## Integration + + + + Protect tools with user authentication and authorization. + + + + Use optimistic updates and avoid blocking operations. + + + + Unit test tool registration and execution. + + + + Log tool calls and track performance metrics. + + + +## Framework-Specific + + + + React hooks and component patterns for WebMCP. + + + + Vue 3 Composition API integration. + + + + Avoid hydration errors in server-rendered apps. + + + +## What's the Difference? + +| Content Type | Purpose | Example | +|-------------|---------|---------| +| [Tutorials](/tutorials) | Learn by building complete projects | "Build a task manager" | +| **How-to Guides** (you are here) | Solve specific problems | "How to handle errors" | +| [Reference](/concepts/tool-registration) | Look up API details | "registerTool() parameters" | +| [Explanation](/concepts/why-webmcp) | Understand concepts deeply | "Why WebMCP exists" | diff --git a/how-to/optimize-performance.mdx b/how-to/optimize-performance.mdx new file mode 100644 index 0000000..ab76ed2 --- /dev/null +++ b/how-to/optimize-performance.mdx @@ -0,0 +1,223 @@ +--- +title: 'How to Optimize Performance' +sidebarTitle: 'Optimize Performance' +description: 'Use optimistic updates and avoid blocking operations for fast tool responses.' +icon: 'gauge-high' +--- + +This guide shows how to make your tools respond quickly, especially important for voice interfaces and real-time interactions. + +## Use Optimistic Updates + +Update local state immediately instead of waiting for API responses: + + + + ```javascript + navigator.modelContext.registerTool({ + name: 'cart_add_item', + description: 'Add product to cart', + inputSchema: { /* ... */ }, + async execute({ productId, quantity }) { + // 1. Update local state immediately + const cartState = getCartState(); + const product = cartState.addItem({ productId, quantity }); + + // 2. Return success instantly + const response = { + content: [{ + type: "text", + text: `**Added to cart:** ${product.name} x${quantity}` + }] + }; + + // 3. Sync to backend in background (don't await) + syncCartToBackend(cartState).catch(err => { + console.error('Background sync failed:', err); + }); + + return response; + } + }); + ``` + + + + ```javascript + // Slow - waits for API before responding + navigator.modelContext.registerTool({ + name: 'cart_add_item', + async execute({ productId, quantity }) { + // Blocks until API responds (could take seconds) + const result = await fetch('/api/cart/add', { + method: 'POST', + body: JSON.stringify({ productId, quantity }) + }); + + const data = await result.json(); + return { + content: [{ type: "text", text: `Added ${data.name}` }] + }; + } + }); + ``` + + + +**Benefits:** +- Instant responses for voice and real-time interactions +- Better user experience +- Voice models can chain multiple operations smoothly + +## Maintain Local State + +Keep application state that tools can update synchronously: + +```javascript +// Using a state manager (Zustand, Redux, etc.) +import { useCartStore } from './stores/cart'; + +function CartTools() { + const cart = useCartStore(); + + useWebMCP({ + name: 'cart_add_item', + handler: async ({ productId, quantity }) => { + // Synchronous state update + cart.addItem(productId, quantity); + + // Return immediately + return `Added ${quantity} items. Cart total: $${cart.total}`; + } + }); +} +``` + +## Use Timeouts + +Don't let slow operations block indefinitely: + +```javascript +async execute({ query }) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const response = await fetch('/api/search', { + method: 'POST', + body: JSON.stringify({ query }), + signal: controller.signal + }); + + clearTimeout(timeout); + return { content: [{ type: "text", text: formatResults(response) }] }; + + } catch (error) { + if (error.name === 'AbortError') { + return { + content: [{ type: "text", text: "Search timed out. Try a simpler query." }], + isError: true + }; + } + throw error; + } +} +``` + +## Lazy Tool Registration + +Only register tools when needed: + +```javascript +// Register tools only when user is authenticated +function AuthenticatedTools({ user }) { + useWebMCP({ + name: 'user_orders', + description: 'Get user order history', + handler: async () => { + const orders = await getOrders(user.id); + return formatOrders(orders); + } + }); + + return null; +} + +// Conditionally render +function App() { + const { user } = useAuth(); + + return ( +
+ {user && } +
+ ); +} +``` + +## Limit Concurrent Tools + +Don't register hundreds of tools: + +```javascript +// Bad: One tool per product +products.forEach(product => { + registerTool({ name: `buy_${product.id}`, ... }); +}); // Could register 1000+ tools! + +// Good: One tool that handles any product +registerTool({ + name: 'buy_product', + inputSchema: { + productId: z.string().describe('Product ID to purchase') + }, + handler: async ({ productId }) => { + // Handle any product + } +}); +``` + +## Cache Expensive Operations + +```javascript +const cache = new Map(); +const CACHE_TTL = 60000; // 1 minute + +async execute({ query }) { + const cacheKey = `search:${query}`; + const cached = cache.get(cacheKey); + + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.response; + } + + const results = await searchProducts(query); + const response = { + content: [{ type: "text", text: formatResults(results) }] + }; + + cache.set(cacheKey, { response, timestamp: Date.now() }); + return response; +} +``` + +## Quick Checklist + +- Update local state before returning +- Sync to backend in background (non-blocking) +- Use timeouts for network requests +- Limit tool count (consolidate related operations) +- Cache expensive operations +- Register tools lazily when possible + +## Related + + + + Complete performance guidelines + + + + Consolidate tools to reduce count + + diff --git a/how-to/test-tools.mdx b/how-to/test-tools.mdx new file mode 100644 index 0000000..d5df0e7 --- /dev/null +++ b/how-to/test-tools.mdx @@ -0,0 +1,248 @@ +--- +title: 'How to Test Your Tools' +sidebarTitle: 'Test Your Tools' +description: 'Unit test tool registration and execution, and test with real AI agents.' +icon: 'flask-vial' +--- + +This guide shows how to test WebMCP tools to ensure they work correctly. + +## Test Tool Registration + +Verify tools register with the correct configuration: + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +describe('Product Search Tool', () => { + beforeEach(() => { + // Mock navigator.modelContext + global.navigator = { + modelContext: { + registerTool: vi.fn() + } + } as any; + }); + + it('should register with correct schema', () => { + registerProductSearchTool(); + + expect(navigator.modelContext.registerTool).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'products_search', + description: expect.any(String), + inputSchema: expect.any(Object) + }) + ); + }); + + it('should have required fields in schema', () => { + registerProductSearchTool(); + + const call = vi.mocked(navigator.modelContext.registerTool).mock.calls[0][0]; + expect(call.inputSchema).toHaveProperty('query'); + }); +}); +``` + +## Test Tool Execution + +Test the handler logic directly: + +```typescript +describe('Product Search Execution', () => { + it('should return products for valid query', async () => { + // Mock the data layer + vi.spyOn(api, 'searchProducts').mockResolvedValue([ + { id: '1', name: 'Laptop', price: 999 } + ]); + + const result = await executeProductSearch({ query: 'laptop' }); + + expect(result.content[0].text).toContain('Laptop'); + expect(result.isError).toBeUndefined(); + }); + + it('should return error for empty query', async () => { + const result = await executeProductSearch({ query: '' }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('empty'); + }); + + it('should handle API errors', async () => { + vi.spyOn(api, 'searchProducts').mockRejectedValue( + new Error('Database connection failed') + ); + + const result = await executeProductSearch({ query: 'laptop' }); + + expect(result.isError).toBe(true); + }); +}); +``` + +## Test React Hooks + +Test `useWebMCP` with React Testing Library: + +```typescript +import { renderHook } from '@testing-library/react'; +import { useWebMCP } from '@mcp-b/react-webmcp'; + +describe('useWebMCP', () => { + beforeEach(() => { + global.navigator = { + modelContext: { + registerTool: vi.fn(() => ({ unregister: vi.fn() })) + } + } as any; + }); + + it('should register tool on mount', () => { + renderHook(() => + useWebMCP({ + name: 'test_tool', + description: 'Test', + inputSchema: {}, + handler: async () => 'result' + }) + ); + + expect(navigator.modelContext.registerTool).toHaveBeenCalledTimes(1); + }); + + it('should unregister on unmount', () => { + const unregister = vi.fn(); + vi.mocked(navigator.modelContext.registerTool).mockReturnValue({ unregister }); + + const { unmount } = renderHook(() => + useWebMCP({ + name: 'test_tool', + description: 'Test', + inputSchema: {}, + handler: async () => 'result' + }) + ); + + unmount(); + expect(unregister).toHaveBeenCalled(); + }); +}); +``` + +## Test Input Validation + +Verify schemas reject invalid input: + +```typescript +import { z } from 'zod'; + +describe('Input Schema', () => { + const schema = z.object({ + query: z.string().min(1).max(100), + limit: z.number().int().min(1).max(100).default(10) + }); + + it('should accept valid input', () => { + expect(() => schema.parse({ query: 'laptop' })).not.toThrow(); + }); + + it('should reject empty query', () => { + expect(() => schema.parse({ query: '' })).toThrow(); + }); + + it('should reject limit over max', () => { + expect(() => schema.parse({ query: 'test', limit: 500 })).toThrow(); + }); + + it('should use default limit', () => { + const result = schema.parse({ query: 'test' }); + expect(result.limit).toBe(10); + }); +}); +``` + +## Test with MCP-B Extension + +Manual testing with real AI: + + + + ```bash + npm run dev + ``` + + + + Navigate to your development URL (e.g., http://localhost:5173) + + + + Click the extension icon and go to the **Tools** tab + + + + Confirm your tools appear in the list with correct names and descriptions + + + + Ask the AI to use your tools: + - "Search for laptops under $1000" + - "Add 2 items to my cart" + - "What's in my cart?" + + + + Check that: + - The AI calls the correct tool + - Your app UI updates appropriately + - The AI presents the response clearly + + + +## Test Error Scenarios + +```typescript +describe('Error Handling', () => { + it('should handle network timeout', async () => { + vi.spyOn(global, 'fetch').mockImplementation(() => + new Promise((_, reject) => + setTimeout(() => reject(new Error('timeout')), 100) + ) + ); + + const result = await executeSearch({ query: 'test' }); + expect(result.isError).toBe(true); + }); + + it('should handle unauthorized access', async () => { + vi.spyOn(auth, 'getCurrentUser').mockResolvedValue(null); + + const result = await executeProtectedTool({ userId: '123' }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('log in'); + }); +}); +``` + +## Quick Checklist + +- Test tool registration configuration +- Test handler with valid inputs +- Test handler with invalid inputs +- Test error scenarios (network, auth, not found) +- Test cleanup on unmount (React) +- Manual test with MCP-B Extension + +## Related + + + + Error patterns to test + + + + Schema patterns to validate + + diff --git a/how-to/validate-input.mdx b/how-to/validate-input.mdx new file mode 100644 index 0000000..42993f6 --- /dev/null +++ b/how-to/validate-input.mdx @@ -0,0 +1,220 @@ +--- +title: 'How to Validate Input' +sidebarTitle: 'Validate Input' +description: 'Use Zod and JSON Schema for robust input validation in WebMCP tools.' +icon: 'shield-check' +--- + +This guide shows how to validate tool inputs to prevent errors and provide helpful feedback to AI agents. + +## Zod Validation (React) + +Use Zod for TypeScript projects with the `@mcp-b/react-webmcp` package: + +```typescript +import { useWebMCP } from '@mcp-b/react-webmcp'; +import { z } from 'zod'; + +useWebMCP({ + name: 'products_search', + description: 'Search products', + inputSchema: { + query: z.string() + .min(1, 'Search query cannot be empty') + .max(100, 'Search query too long') + .describe('Product name, category, or SKU'), + + minPrice: z.number() + .positive() + .optional() + .describe('Minimum price filter in USD'), + + maxPrice: z.number() + .positive() + .optional() + .describe('Maximum price filter in USD'), + + category: z.enum(['electronics', 'clothing', 'home', 'sports']) + .optional() + .describe('Filter by category'), + + limit: z.number() + .int() + .min(1) + .max(100) + .default(10) + .describe('Results per page (1-100, default: 10)') + }, + handler: async ({ query, minPrice, maxPrice, category, limit }) => { + // TypeScript knows exact types here + } +}); +``` + + +Parameter descriptions (via `.describe()`) are sent to the AI model. Be detailed! + + +## JSON Schema Validation (Vanilla JS) + +Use JSON Schema for vanilla JavaScript: + +```javascript +navigator.modelContext.registerTool({ + name: 'products_search', + description: 'Search products', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + minLength: 1, + maxLength: 100, + description: 'Product name, category, or SKU' + }, + minPrice: { + type: 'number', + minimum: 0, + description: 'Minimum price filter in USD' + }, + maxPrice: { + type: 'number', + minimum: 0, + description: 'Maximum price filter in USD' + }, + category: { + type: 'string', + enum: ['electronics', 'clothing', 'home', 'sports'], + description: 'Filter by category' + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 10, + description: 'Results per page (1-100)' + } + }, + required: ['query'] + }, + async execute({ query, minPrice, maxPrice, category, limit = 10 }) { + // ... + } +}); +``` + +## Validate Business Rules + +Go beyond type checking: + +```typescript +inputSchema: { + productId: z.string() + .uuid('Invalid product ID format') + .describe('Product UUID'), + + quantity: z.number() + .int() + .positive() + .max(99, 'Cannot order more than 99 items at once') + .describe('Quantity to add to cart'), + + promoCode: z.string() + .regex(/^[A-Z0-9]{6,12}$/, 'Invalid promo code format') + .optional() + .describe('Optional promotional code (6-12 alphanumeric characters)') +} +``` + +## Common Patterns + +### Optional with Default + +```typescript +// Zod +limit: z.number().int().default(10).describe('Results per page') + +// JSON Schema +limit: { type: 'integer', default: 10, description: 'Results per page' } +``` + +### Enum Choices + +```typescript +// Zod +status: z.enum(['pending', 'shipped', 'delivered']).describe('Order status filter') + +// JSON Schema +status: { + type: 'string', + enum: ['pending', 'shipped', 'delivered'], + description: 'Order status filter' +} +``` + +### Nested Objects + +```typescript +// Zod +address: z.object({ + street: z.string().describe('Street address'), + city: z.string().describe('City name'), + zip: z.string().regex(/^\d{5}$/).describe('5-digit ZIP code') +}).describe('Shipping address') + +// JSON Schema +address: { + type: 'object', + description: 'Shipping address', + properties: { + street: { type: 'string', description: 'Street address' }, + city: { type: 'string', description: 'City name' }, + zip: { type: 'string', pattern: '^\\d{5}$', description: '5-digit ZIP code' } + }, + required: ['street', 'city', 'zip'] +} +``` + +### Arrays + +```typescript +// Zod +productIds: z.array(z.string().uuid()) + .min(1) + .max(10) + .describe('List of product IDs (1-10)') + +// JSON Schema +productIds: { + type: 'array', + items: { type: 'string', format: 'uuid' }, + minItems: 1, + maxItems: 10, + description: 'List of product IDs (1-10)' +} +``` + +## Quick Reference + +| Validation | Zod | JSON Schema | +|------------|-----|-------------| +| Required string | `z.string()` | `{ type: 'string' }` + `required` | +| Optional | `.optional()` | Omit from `required` | +| Default | `.default(value)` | `default: value` | +| Min/max number | `.min(1).max(100)` | `minimum: 1, maximum: 100` | +| String length | `.min(1).max(100)` | `minLength: 1, maxLength: 100` | +| Enum | `z.enum([...])` | `enum: [...]` | +| Regex | `.regex(/pattern/)` | `pattern: "..."` | +| Description | `.describe('...')` | `description: '...'` | + +## Related + + + + Complete schema syntax reference + + + + Handle validation failures gracefully + + diff --git a/tutorials/first-tool-react.mdx b/tutorials/first-tool-react.mdx new file mode 100644 index 0000000..e483694 --- /dev/null +++ b/tutorials/first-tool-react.mdx @@ -0,0 +1,191 @@ +--- +title: 'Tutorial: Your First Tool (React)' +sidebarTitle: 'Your First Tool (React)' +description: 'Build a working WebMCP tool from scratch using React. By the end, AI agents will be able to interact with your application.' +icon: 'graduation-cap' +--- + +In this tutorial, you'll build a simple counter application with an AI-accessible tool. By the end, AI agents will be able to read and modify your counter through natural language. + +**What you'll build:** A React counter app where AI agents can get the current count and increment it. + +**Time:** ~15 minutes + +## Prerequisites + +You need: +- Node.js 18+ installed +- A code editor (VS Code recommended) +- Chrome, Edge, or Brave browser +- Basic React knowledge + +## Step 1: Create Your React App + +Open your terminal and create a new React project: + +```bash +npm create vite@latest my-webmcp-app -- --template react-ts +cd my-webmcp-app +``` + +## Step 2: Install WebMCP Packages + +Install the required packages: + +```bash +npm install @mcp-b/global @mcp-b/react-webmcp zod +``` + +- **@mcp-b/global** - Adds the `navigator.modelContext` API to your browser +- **@mcp-b/react-webmcp** - React hooks for registering tools +- **zod** - Schema validation for tool inputs + +## Step 3: Install the Browser Extension + +Install the MCP-B Extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/mcp-b-extension/daohopfhkdelnpemnhlekblhnikhdhfa). + +This extension allows AI agents to discover and call your tools. + +## Step 4: Create Your Counter Component + +Replace the contents of `src/App.tsx` with: + +```tsx "src/App.tsx" +import { useState } from 'react'; +import '@mcp-b/global'; +import { useWebMCP } from '@mcp-b/react-webmcp'; +import { z } from 'zod'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( +
+

WebMCP Counter

+

Count: {count}

+ + +
+ ); +} + +function CounterTools({ + count, + setCount +}: { + count: number; + setCount: React.Dispatch>; +}) { + // Tool 1: Get the current count + useWebMCP({ + name: 'get_count', + description: 'Get the current counter value', + inputSchema: {}, + handler: async () => { + return `The current count is ${count}`; + } + }); + + // Tool 2: Increment the counter + useWebMCP({ + name: 'increment_count', + description: 'Increment the counter by a specified amount', + inputSchema: { + amount: z.number().min(1).max(100).default(1) + .describe('Amount to increment by (1-100)') + }, + handler: async ({ amount }) => { + setCount(c => c + amount); + return `Incremented counter by ${amount}. New count: ${count + amount}`; + } + }); + + return null; // This component only registers tools +} + +export default App; +``` + +Let's understand what this code does: + +1. **State management** - `useState` tracks the counter value +2. **`useWebMCP` hook** - Registers tools that AI agents can call +3. **`get_count` tool** - Returns the current count (read operation) +4. **`increment_count` tool** - Increases the count (write operation) +5. **Input schema** - Defines what parameters the tool accepts using Zod + +## Step 5: Start Your Development Server + +```bash +npm run dev +``` + +Open http://localhost:5173 in your browser. You should see your counter app. + +## Step 6: Test Your Tools + +1. Click the MCP-B extension icon in your browser toolbar +2. Click the **Tools** tab +3. You should see your two tools: `get_count` and `increment_count` + + + Extension showing registered tools + + +Try calling your tools: + +1. In the extension chat, type: "What's the current count?" +2. The AI will call `get_count` and report the value +3. Type: "Increment the counter by 5" +4. The AI will call `increment_count` and your app's UI will update + +**Congratulations!** You've built your first WebMCP tool. AI agents can now interact with your React application. + +## Step 7: Add a Third Tool + +Let's add a tool to reset the counter. Add this inside the `CounterTools` component: + +```tsx +// Tool 3: Reset the counter +useWebMCP({ + name: 'reset_count', + description: 'Reset the counter back to zero', + inputSchema: {}, + handler: async () => { + setCount(0); + return 'Counter has been reset to 0'; + } +}); +``` + +Refresh your browser and check the extension - you'll see the new tool available. + +## What You Learned + +- How to install and set up WebMCP in a React project +- How to use the `useWebMCP` hook to register tools +- How to define input schemas with Zod +- How to test tools using the MCP-B extension + +## Next Steps + + + + Build a more complex app with CRUD operations + + + + Learn to handle errors gracefully in tools + + + + Protect tools with user authentication + + + + Complete API reference for registerTool + + diff --git a/tutorials/first-tool-vanilla.mdx b/tutorials/first-tool-vanilla.mdx new file mode 100644 index 0000000..a84e949 --- /dev/null +++ b/tutorials/first-tool-vanilla.mdx @@ -0,0 +1,202 @@ +--- +title: 'Tutorial: Your First Tool (Vanilla JS)' +sidebarTitle: 'Your First Tool (Vanilla JS)' +description: 'Build a working WebMCP tool from scratch using vanilla JavaScript. No frameworks required.' +icon: 'graduation-cap' +--- + +In this tutorial, you'll build a simple counter page with an AI-accessible tool using plain JavaScript. No frameworks needed. + +**What you'll build:** A counter page where AI agents can read and modify the count. + +**Time:** ~10 minutes + +## Prerequisites + +You need: +- A code editor (VS Code recommended) +- Chrome, Edge, or Brave browser +- Basic JavaScript knowledge + +## Step 1: Create Your HTML File + +Create a new file called `index.html`: + +```html "index.html" + + + + + + WebMCP Counter + + + +

WebMCP Counter

+
0
+ + + + + + + + +``` + +Let's understand what this code does: + +1. **Script tag** - Loads the WebMCP polyfill from unpkg CDN +2. **`navigator.modelContext.registerTool()`** - Registers tools for AI agents +3. **`inputSchema`** - Uses JSON Schema to define parameters +4. **`execute()`** - The function called when an AI invokes the tool + +## Step 2: Install the Browser Extension + +Install the MCP-B Extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/mcp-b-extension/daohopfhkdelnpemnhlekblhnikhdhfa). + +## Step 3: Open Your Page + +Open `index.html` in your browser. You can: +- Double-click the file to open it directly +- Or use a local server: `npx serve .` + +## Step 4: Test Your Tools + +1. Click the MCP-B extension icon in your browser toolbar +2. Click the **Tools** tab +3. You should see `get_count` and `increment_count` + +Try it out: +1. Type: "What's the current count?" +2. Type: "Increment by 10" +3. Watch your page update automatically! + +**Congratulations!** You've built your first WebMCP tool with vanilla JavaScript. + +## Step 5: Add a Reset Tool + +Add this before the closing `` tag: + +```javascript +navigator.modelContext.registerTool({ + name: 'reset_count', + description: 'Reset the counter back to zero', + inputSchema: { + type: 'object', + properties: {} + }, + async execute() { + count = 0; + updateDisplay(); + return { + content: [{ + type: 'text', + text: 'Counter has been reset to 0' + }] + }; + } +}); +``` + +Refresh the page and your new tool is ready. + +## What You Learned + +- How to add WebMCP to any HTML page with a script tag +- How to register tools using `navigator.modelContext.registerTool()` +- How to define input schemas with JSON Schema +- How to return responses from tools + +## Next Steps + + + + Try the React version with hooks + + + + Learn to handle errors gracefully + + + + Set up WebMCP with npm and bundlers + + + + Complete API reference + + diff --git a/tutorials/index.mdx b/tutorials/index.mdx new file mode 100644 index 0000000..a6ff1b2 --- /dev/null +++ b/tutorials/index.mdx @@ -0,0 +1,52 @@ +--- +title: 'Tutorials' +description: 'Learn WebMCP by building complete projects from scratch. Step-by-step guides for beginners.' +icon: 'graduation-cap' +--- + +Tutorials guide you through building complete projects from scratch. Each tutorial focuses on **one path** and takes you from zero to a working result. + + +**New to WebMCP?** Start with [Your First Tool (Vanilla JS)](/tutorials/first-tool-vanilla) - no frameworks required. + + +## Getting Started + + + + Build a counter page with tools using plain JavaScript. No frameworks required. + + **Time:** ~10 minutes + + + + Build a counter app with AI-accessible tools using React and the useWebMCP hook. + + **Time:** ~15 minutes + + + +## Building Real Applications + + + + Build a full task manager with create, complete, and delete operations. + + **Time:** ~30 minutes + + + + Build an e-commerce cart with product search and checkout tools. + + **Time:** ~45 minutes + + + +## What's the Difference? + +| Content Type | Purpose | Example | +|-------------|---------|---------| +| **Tutorials** (you are here) | Learn by building complete projects | "Build a task manager" | +| [How-to Guides](/how-to) | Solve specific problems | "How to handle errors" | +| [Reference](/concepts/tool-registration) | Look up API details | "registerTool() parameters" | +| [Explanation](/concepts/why-webmcp) | Understand concepts deeply | "Why WebMCP exists" |