Skip to main content
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.
These practices apply when you own the website and can integrate WebMCP tools directly into your codebase. For userscript development, see Managing Userscripts.

Tool Design Principles

Use Descriptive, Consistent Names

Follow a clear naming convention for all your tools:

Good Names

  • products_search
  • cart_add_item
  • user_get_profile
  • orders_list_recent

Avoid

  • doStuff
  • action1
  • helper
  • processData
Naming pattern: Use domain_verb_noun or verb_noun format:
navigator.modelContext.registerTool({ 
  name: 'search',      // Too generic
  name: 'doAction',    // Unclear purpose
  name: 'helper1'      // Meaningless
}); 

navigator.modelContext.registerTool({ 
  name: 'products_search',  // Clear domain + action
  name: 'cart_add_item',    // Verb + noun pattern
  name: 'orders_get_status' // Descriptive and specific
}); 

Provide Detailed Descriptions

Tool names, descriptions, and input schemas are sent directly to the AI model’s context. This is the ONLY information the model has about your tools. Make these as detailed and informative as possible.
Help AI agents understand when and how to use your tools by including everything they need to know in the description:
navigator.modelContext.registerTool({ 
  name: 'products_search', 
  description: 'Searches for products', // Too vague!
  // Model doesn't know when to use this or what it returns
}); 

navigator.modelContext.registerTool({ 
  name: 'products_search', 
  description: `Search products by name, category, or SKU. // [!code ++]
Returns paginated results with title, price, stock status, and product URLs. // [!code ++]
Use this when users ask about product availability or pricing. // [!code ++]
If searching by price range, use the minPrice and maxPrice parameters. // [!code ++]
Results are sorted by relevance by default.`, 
  inputSchema: { 
    query: z.string().min(1).describe('Product name, category, or SKU to search for'), 
    minPrice: z.number().positive().optional().describe('Minimum price filter in USD'), 
    maxPrice: z.number().positive().optional().describe('Maximum price filter in USD'), 
    limit: z.number().int().min(1).max(100).default(10).describe('Number of results (max 100)') 
  } 
  // ...
}); 
Include in 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
  • If this tool will modify the available tool list

Design Powerful, Consolidated Tools

Create consolidated tools that handle related operations rather than many single-purpose tools:
  • Good: Consolidated Tools
  • Avoid: Too Many Single-Purpose Tools
// ✅ Powerful tool handling related operations
navigator.modelContext.registerTool({
  name: 'cart_manager',
  description: 'Manage all cart operations including add, remove, view, clear, and update quantity. Use action parameter to specify the operation.',
  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)')
  },
  // ...
});
Benefits of consolidated tools:
  • Reduces context consumption (fewer tool definitions)
  • Modern AI models handle complex tools effectively
  • Fewer tools to maintain and document
  • Simpler tool discovery for AI agents
  • More efficient use of available context window

Input Validation

Use Zod for Type-Safe Validation

Parameter descriptions (via .describe()) are sent to the AI model’s context. Be detailed and specific!
Zod provides excellent TypeScript integration and runtime validation. Use .describe() on every parameter to tell the model exactly what it needs to know:
import { z } from 'zod';

navigator.modelContext.registerTool({
  name: 'products_search',
  description: 'Search products by various criteria',
  inputSchema: {
    query: z.string()
      .min(1, 'Search query cannot be empty')
      .max(100, 'Search query too long')
      .describe('Product name, category, or SKU to search for. Examples: "laptop", "running shoes", "SKU-12345"'),

    minPrice: z.number()
      .positive()
      .optional()
      .describe('Minimum price filter in USD. Use with maxPrice to filter by price range.'),

    maxPrice: z.number()
      .positive()
      .optional()
      .describe('Maximum price filter in USD. Use with minPrice to filter by price range.'),

    category: z.enum(['electronics', 'clothing', 'home', 'sports'])
      .optional()
      .describe('Product category filter. Choose from: electronics, clothing, home, or sports.'),

    limit: z.number()
      .int()
      .min(1)
      .max(100)
      .default(10)
      .describe('Number of results to return (1-100, default: 10). Use higher values for broader searches.')
  },
  async execute({ query, minPrice, maxPrice, category, limit }) {
    // TypeScript knows the exact types here
    // ...
  }
});

Validate Business Logic Constraints

Go beyond type checking to enforce business rules:
inputSchema: {
  productId: z.string().uuid().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')
}

Provide Helpful Error Messages

async execute({ productId, quantity }) {
  // Validate availability
  const product = await getProduct(productId);

  if (!product) {
    return {
      content: [{
        type: "text",
        text: `Product ${productId} not found. Please verify the product ID.`
      }],
      isError: true
    };
  }

  if (product.stock < quantity) {
    return {
      content: [{
        type: "text",
        text: `Only ${product.stock} units available. Requested: ${quantity}.`
      }],
      isError: true
    };
  }

  // Proceed with adding to cart
  // ...
}

Response Format

Use Markdown Instead of JSON

AI models work better with markdown-formatted responses than JSON. Markdown is more readable and easier for models to incorporate into natural language responses.
Return data as markdown strings rather than JSON objects:
  • Good: Markdown
  • Avoid: JSON
// ✅ Markdown formatted response
async execute({ query }) {
  try {
    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
      }]
    };
  } catch (error) {
    return {
      content: [{
        type: "text",
        text: `**Error searching products:** ${error.message}`
      }],
      isError: true
    };
  }
}
Benefits of markdown responses:
  • More natural for AI to read and present to users
  • Better formatting in chat interfaces
  • Easier for models to extract specific information
  • More human-readable in logs and debugging

Include Helpful Context in Responses

Provide information that helps the AI understand and present the results:
async execute({ orderId }) {
  const order = await getOrder(orderId);

  // ✅ Rich markdown with context
  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}
`).join('\n')}

## 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()}*`
    }]
  };
}

## Error Handling

### Handle Errors Gracefully

Always anticipate and handle potential failures:

```javascript
async execute({ userId }) {
  try {
    // Check authentication
    const currentUser = await getCurrentUser();
    if (!currentUser) {
      return {
        content: [{
          type: "text",
          text: "User not authenticated. Please log in first."
        }],
        isError: true
      };
    }

    // Check authorization
    if (currentUser.id !== userId && !currentUser.isAdmin) {
      return {
        content: [{
          type: "text",
          text: "Unauthorized. You can only access your own profile."
        }],
        isError: true
      };
    }

    // Fetch data with timeout
    const profile = await fetchUserProfile(userId, { timeout: 5000 });

    if (!profile) {
      return {
        content: [{
          type: "text",
          text: `User profile ${userId} not found.`
        }],
        isError: true
      };
    }

    // Return as markdown for better readability
    return {
      content: [{
        type: "text",
        text: `# User Profile

**Name:** ${profile.name}
**Email:** ${profile.email}
**Member since:** ${new Date(profile.createdAt).toLocaleDateString()}
**Account type:** ${profile.accountType}`
      }]
    };

  } catch (error) {
    console.error('Error fetching user profile:', error);

    return {
      content: [{
        type: "text",
        text: `Failed to fetch user profile: ${error.message}`
      }],
      isError: true
    };
  }
}

Use Specific Error Messages

Help AI agents understand what went wrong with clear, formatted error messages:
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.'
  );
}

Security Best Practices

Validate User Authentication

Tools run with the user’s session, but always verify:
async execute({ orderId }) {
  const session = await getSession();

  if (!session?.user) {
    return formatError(
      'UNAUTHORIZED',
      'Please log in to view order details.'
    );
  }

  const order = await getOrder(orderId);

  // Verify order belongs to current user
  if (order.userId !== session.user.id && !session.user.isAdmin) {
    return formatError(
      'FORBIDDEN',
      'You do not have permission to view this order.'
    );
  }

  return { content: [{ type: "text", text: JSON.stringify(order) }] };
}

Sanitize Inputs

Never trust input data, even with schema validation:
import DOMPurify from 'dompurify';
import { escapeHtml } from './utils';

async execute({ content }) {
  // Sanitize HTML content
  const sanitized = DOMPurify.sanitize(content);

  // Escape for database
  const escaped = escapeHtml(sanitized);

  // Use parameterized queries
  await db.query(
    'INSERT INTO posts (content) VALUES ($1)',
    [escaped]
  );

  return { content: [{ type: "text", text: "Post created successfully" }] };
}

Rate Limit Tool Calls

Protect your API from abuse:
import rateLimit from './rate-limiter';

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 10 // 10 requests per minute per user
});

navigator.modelContext.registerTool({
  name: 'expensive_operation',
  description: 'Performs a resource-intensive operation',
  async execute(params) {
    const session = await getSession();
    const userId = session?.user?.id || 'anonymous';

    const allowed = await limiter.check(userId);

    if (!allowed) {
      return formatError(
        'RATE_LIMIT',
        'Rate limit exceeded. Maximum 10 requests per minute.',
        { retryAfter: limiter.getResetTime(userId) }
      );
    }

    // Process the request
    // ...
  }
});

Avoid Exposing Sensitive Data

Be careful what data you include in responses:
async execute({ userId }) { 
  const user = await getUser(userId); 
  return { 
    content: [{ 
      type: "text", 
      text: JSON.stringify(user) // ❌ Might include sensitive fields!
    }] 
  }; 
} 

async execute({ userId }) { 
  const user = await getUser(userId); 
  // ✅ Only include safe fields in response
  return { 
    content: [{ 
      type: "text", 
      text: `# User Information // [!code ++]
**Name:** ${user.name}
**Email:** ${user.email}
**Member since:** ${new Date(user.createdAt).toLocaleDateString()}`
    }] 
  }; 
  // ✅ NOT including: password hash, internal IDs, private notes, session tokens
} 

Performance Optimization

Design for Async Operations

All tool execution should be non-blocking:
// ✅ Async with proper error handling, markdown formatted
async execute({ query }) {
  try {
    const [products, categories, suggestions] = await Promise.all([
      searchProducts(query),
      getCategories(),
      getSuggestions(query)
    ]);

    return {
      content: [{
        type: "text",
        text: `# Search Results

## Products
${products.map(p => `- **${p.name}** - $${p.price}`).join('\n')}

## Available Categories
${categories.join(', ')}

## Did you mean?
${suggestions.join(', ')}`
      }]
    };
  } catch (error) {
    return formatError('SERVER_ERROR', error.message);
  }
}

Implement Timeouts

Prevent tools from hanging indefinitely:
const TIMEOUT = 5000; // 5 seconds

async execute({ query }) {
  try {
    const results = await Promise.race([
      performSearch(query),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Timeout')), TIMEOUT)
      )
    ]);

    return {
      content: [{
        type: "text",
        text: `# Search Results for "${query}"

${results.map((r, i) => `${i + 1}. ${r.title}`).join('\n')}`
      }]
    };
  } catch (error) {
    if (error.message === 'Timeout') {
      return formatError(
        'TIMEOUT',
        'Search took too long. Please try a more specific query.'
      );
    }
    throw error;
  }
}

Use Optimistic Updates for Voice Models

Voice models and real-time interactions work best with instant tool responses. Implement optimistic updates by operating on in-app state rather than waiting for async API calls:
  • Good: Optimistic Update
  • Avoid: Blocking on API
// ✅ Update local state immediately, sync in background
navigator.modelContext.registerTool({
  name: 'cart_add_item',
  description: 'Add product to cart',
  inputSchema: {
    productId: z.string(),
    quantity: z.number().positive()
  },
  async execute({ productId, quantity }) {
    // Update in-app state immediately
    const cartState = getCartState();
    const product = cartState.addItem({ productId, quantity });

    // Return success instantly as markdown
    const cartItems = cartState.getItems();
    const markdown = `**Added to cart!**

${product.name} x${quantity}

## Current Cart (${cartItems.length} items)
${cartItems.map(item => `- ${item.name} x${item.quantity} - $${item.price * item.quantity}`).join('\n')}

**Total:** $${cartState.getTotal()}`;

    // Sync to backend in background (don't await)
    syncCartToBackend(cartState).catch(err => {
      console.error('Background sync failed:', err);
      // Handle sync errors appropriately
    });

    return {
      content: [{ type: "text", text: markdown }]
    };
  }
});
Benefits of optimistic updates:
  • Instant tool responses for voice and real-time interactions
  • Better user experience with immediate feedback
  • Voice models can chain multiple operations smoothly
  • Reduced latency in multi-step workflows
Implementation tips:
  • Maintain local application state (Redux, Zustand, React Context)
  • Update state synchronously before returning from tool
  • Queue background sync operations
  • Handle sync failures gracefully with retry logic

Limit Response Size

Prevent sending huge payloads:
const MAX_RESULTS = 100;
const MAX_RESPONSE_SIZE = 1024 * 100; // 100KB

async execute({ query, limit = 10 }) {
  const safLimit = Math.min(limit, MAX_RESULTS);
  const results = await search(query, safLimit);

  // Format as markdown
  const markdown = `# Search Results for "${query}"

Found ${results.length} results:

${results.map((r, i) => `${i + 1}. **${r.title}**\n   ${r.description}`).join('\n\n')}`;

  if (markdown.length > MAX_RESPONSE_SIZE) {
    return formatError(
      'RESPONSE_TOO_LARGE',
      `Response exceeds maximum size. Try reducing limit (current: ${safLimit}).`
    );
  }

  return { content: [{ type: "text", text: markdown }] };
}

Testing & Quality Assurance

Test Tool Registration

Verify tools are properly registered:
import { describe, it, expect, beforeEach } 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)
      })
    );
  });
});

Test Tool Execution

Verify tool handlers work correctly:
describe('Product Search Execution', () => {
  it('should return products for valid query', async () => {
    const result = await executeProductSearch({ query: 'laptop' });

    expect(result.content[0].text).toBeTruthy();
    const data = JSON.parse(result.content[0].text);
    expect(data.success).toBe(true);
    expect(data.products).toBeInstanceOf(Array);
  });

  it('should handle empty query gracefully', async () => {
    const result = await executeProductSearch({ query: '' });

    expect(result.isError).toBe(true);
    const data = JSON.parse(result.content[0].text);
    expect(data.errorCode).toBe('VALIDATION_ERROR');
  });

  it('should handle database errors', async () => {
    // Mock database failure
    vi.spyOn(db, 'searchProducts').mockRejectedValue(
      new Error('Database connection failed')
    );

    const result = await executeProductSearch({ query: 'laptop' });

    expect(result.isError).toBe(true);
  });
});

Test with Real AI Agents

Use the MCP-B Extension to test with actual AI:
1

Install MCP-B Extension

Get it from the Chrome Web Store
2

Load your website

Navigate to your development site where tools are registered
3

Verify tool discovery

Open the extension and confirm your tools appear in the available tools list
4

Test with natural language

Ask the AI agent to use your tools: “Search for laptops under $1000”
5

Verify results

Check that the AI correctly interprets tool responses and presents them to the user

Tool Organization

Organize tools logically in your codebase:
// tools/products.ts
export function registerProductTools() {
  navigator.modelContext.registerTool({
    name: 'products_search',
    // ...
  });

  navigator.modelContext.registerTool({
    name: 'products_get_details',
    // ...
  });
}

// tools/cart.ts
export function registerCartTools() {
  navigator.modelContext.registerTool({
    name: 'cart_add_item',
    // ...
  });

  navigator.modelContext.registerTool({
    name: 'cart_get_contents',
    // ...
  });
}

// tools/index.ts
export function registerAllTools() {
  registerProductTools();
  registerCartTools();
  registerOrderTools();
}

Use Consistent Prefixes

Group tools by domain using name prefixes:
// Product domain
products_search
products_get_details
products_get_reviews

// Cart domain
cart_add_item
cart_remove_item
cart_update_quantity
cart_clear

// Order domain
orders_create
orders_get_status
orders_list_recent
Remember: Tool descriptions go into the model’s context. Reference other tools by name to help the model understand workflows and dependencies.
Make it clear when tools should be called in sequence or when one tool depends on another:
// ✅ References other tools and explains workflow
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.`,
  // ...
});

// ✅ Mentions tool list changes
navigator.modelContext.registerTool({
  name: 'user_login',
  description: `Authenticate user with email and password.

After successful login, additional tools will become available:
- orders_list_recent
- user_get_profile
- user_update_settings

Call this tool first if user wants to access account features.`,
  // ...
});

// ✅ References prerequisite tool
navigator.modelContext.registerTool({
  name: 'order_track_shipment',
  description: `Get real-time tracking information for an order shipment.

Prerequisite: Call orders_get_details first to get the tracking number.
Requires the trackingNumber from that response.`,
  inputSchema: {
    trackingNumber: z.string().describe('Tracking number from orders_get_details')
  }
  // ...
});
When to reference other tools:
  • A tool should be called before this one
  • This tool’s execution will register/unregister other tools
  • Data from another tool is needed as input
  • Multiple tools work together in a common workflow

Framework Integration

React with Hooks

Use useWebMCP for component-scoped tools:
import { useWebMCP } from '@mcp-b/react-webmcp';
import { z } from 'zod';

function ProductSearch() {
  const [results, setResults] = useState([]);

  useWebMCP({
    name: 'products_search',
    description: 'Search products',
    inputSchema: {
      query: z.string().min(1)
    },
    async execute({ query }) {
      const data = await searchProducts(query);
      setResults(data); // Update UI

      // Return markdown formatted results
      return {
        content: [{
          type: "text",
          text: `# Search Results

Found ${data.length} products matching "${query}":

${data.map(p => `- **${p.name}** - $${p.price}`).join('\n')}`
        }]
      };
    }
  });

  return (
    <div>
      {/* Component renders search results */}
      {results.map(product => <ProductCard key={product.id} {...product} />)}
    </div>
  );
}

Vue with Composition API

import { onMounted, onUnmounted } from 'vue';

export function useProductSearch() {
  const results = ref([]);
  let registration: ToolRegistration | null = null;

  onMounted(() => {
    registration = navigator.modelContext.registerTool({
      name: 'products_search',
      description: 'Search products',
      inputSchema: {
        query: z.string().min(1)
      },
      async execute({ query }) {
        const data = await searchProducts(query);
        results.value = data;

        // Return markdown formatted results
        return {
          content: [{
            type: "text",
            text: `# Search Results

Found ${data.length} products:

${data.map(p => `- **${p.name}** - $${p.price}`).join('\n')}`
          }]
        };
      }
    });
  });

  onUnmounted(() => {
    registration?.unregister();
  });

  return { results };
}

Vanilla JavaScript

// Register on page load
document.addEventListener('DOMContentLoaded', () => {
  const registration = navigator.modelContext.registerTool({
    name: 'products_search',
    description: 'Search products',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string' }
      }
    },
    async execute({ query }) {
      const results = await searchProducts(query);

      // Update DOM
      document.getElementById('results').innerHTML =
        results.map(p => `<div>${p.name} - $${p.price}</div>`).join('');

      // Return markdown formatted results
      return {
        content: [{
          type: "text",
          text: `# Search Results

Found ${results.length} products:

${results.map(p => `- **${p.name}** - $${p.price}`).join('\n')}`
        }]
      };
    }
  });

  // Cleanup on page unload
  window.addEventListener('beforeunload', () => {
    registration.unregister();
  });
});

Documentation

Write for the Model, Not Developers

JSDoc comments and code documentation do NOT go into the model’s context. Only the tool name, description, and input schema are sent to the AI.
Put all important information in the tool description and parameter descriptions:
// ❌ JSDoc won't help the model
/**
 * @param query - Search query
 * @param maxPrice - Maximum price
 */
navigator.modelContext.registerTool({
  name: 'products_search',
  description: 'Search products',
  // ...
});

// ✅ Everything in the description and schema
navigator.modelContext.registerTool({
  name: 'products_search',
  description: `Search the product catalog using full-text search across names, descriptions, and SKUs.

Returns paginated array of products with name, price, stock status, and URLs.
Useful when users ask "what products do you have" or "find me a laptop under $1000".`,
  inputSchema: {
    query: z.string()
      .min(1)
      .max(100)
      .describe('Search query (1-100 characters). Can be product name, category, or SKU.'),
    category: z.enum(['electronics', 'clothing', 'home'])
      .optional()
      .describe('Optional category filter'),
    maxPrice: z.number()
      .positive()
      .optional()
      .describe('Optional maximum price filter in USD'),
    limit: z.number()
      .int()
      .min(1)
      .max(100)
      .default(10)
      .describe('Results per page (1-100, default: 10)')
  }
});

Create Tool Catalog for Developers

Maintain a reference document for your development team:
# WebMCP Tools Catalog

## Product Tools

### products_search
**Description:** Search products by name, category, or SKU
**Inputs:**
- `query` (string, required): Search terms
- `category` (string, optional): Filter by category
- `limit` (number, optional): Results per page (default: 10)

**Returns:** Array of products with pricing and availability

**Example:** Find laptops under $1000

Use OpenAPI/JSON Schema

Export tool schemas for documentation:
export const toolSchemas = {
  products_search: {
    name: 'products_search',
    description: 'Search products',
    parameters: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'Search query'
        }
      },
      required: ['query']
    }
  }
};

Monitoring & Analytics

Log Tool Usage

Track which tools are being called:
async execute(params) {
  const startTime = Date.now();

  try {
    const results = await performSearch(params.query);

    // Log successful execution
    analytics.track('tool_executed', {
      toolName: 'products_search',
      duration: Date.now() - startTime,
      resultCount: results.length,
      userId: getCurrentUser()?.id
    });

    // Return markdown formatted results
    return {
      content: [{
        type: "text",
        text: `# Search Results

Found ${results.length} products matching "${params.query}":

${results.map((r, i) => `${i + 1}. **${r.name}** - $${r.price}`).join('\n')}`
      }]
    };
  } catch (error) {
    // Log errors
    analytics.track('tool_error', {
      toolName: 'products_search',
      error: error.message,
      duration: Date.now() - startTime
    });

    throw error;
  }
}

Monitor Performance

Track tool execution time:
class ToolMetrics {
  private metrics = new Map<string, number[]>();

  record(toolName: string, duration: number) {
    if (!this.metrics.has(toolName)) {
      this.metrics.set(toolName, []);
    }
    this.metrics.get(toolName)!.push(duration);
  }

  getStats(toolName: string) {
    const durations = this.metrics.get(toolName) || [];
    return {
      count: durations.length,
      avg: durations.reduce((a, b) => a + b, 0) / durations.length,
      max: Math.max(...durations),
      min: Math.min(...durations)
    };
  }
}

const metrics = new ToolMetrics();

async execute(params) {
  const start = Date.now();
  try {
    const result = await performOperation(params);
    return result;
  } finally {
    metrics.record('products_search', Date.now() - start);
  }
}

Version Management

Version Your Tools

Include version info in tool names or metadata:
// Option 1: Include version in name for breaking changes
navigator.modelContext.registerTool({
  name: 'products_search_v2',
  description: 'Search products (v2 - new schema)',
  // ...
});

// Option 2: Include version in response metadata
async execute(params) {
  const results = await performSearch(params);

  return {
    content: [{
      type: "text",
      text: `# Search Results (API v2.0.0)

${results.map(r => `- ${r.title}`).join('\n')}

---
*Using API version 2.0.0*`
    }]
  };
}

Deprecate Gracefully

Warn when tools will be removed:
navigator.modelContext.registerTool({
  name: 'legacy_search',
  description: '⚠️ DEPRECATED: Use products_search instead. This tool will be removed in v3.0.',
  async execute(params) {
    console.warn('legacy_search is deprecated, use products_search');
    const results = await legacySearch(params);

    return {
      content: [{
        type: "text",
        text: `⚠️ **DEPRECATION WARNING**

This tool is deprecated and will be removed in v3.0.
Please use \`products_search\` instead.

---

# Search Results

${results.map(r => `- ${r.title}`).join('\n')}`
      }]
    };
  }
});

Quick Reference

  • Tool name, description, and input schema ONLY
  • JSDoc and code comments are NOT sent to the model
  • Use detailed descriptions and parameter .describe() methods
  • Reference other tools by name in descriptions
  • Mention if tool execution changes the tool list
  • Prefer consolidated tools over many single-purpose tools
  • Reduces context consumption
  • Use domain_verb_noun naming pattern
  • Be specific and descriptive in all metadata
  • Include what the tool does and when to use it
  • Describe return data format
  • List prerequisites and dependencies on other tools
  • Mention if this tool registers/unregisters other tools
  • Use parameter .describe() for detailed parameter info
  • Use optimistic updates - update local state first
  • Return instantly, sync to backend in background
  • Don’t block on async API calls
  • Maintain in-app state for fast operations
  • Return markdown strings instead of JSON objects
  • Markdown is more readable for AI models
  • Better for natural language presentation
  • Include helpful context and formatting
  • Prefer Zod schemas for TypeScript projects
  • Use .describe() on every parameter (goes to model)
  • Validate both types and business logic
  • Provide helpful error messages
  • Validate user authentication
  • Check authorization for protected resources
  • Sanitize all inputs
  • Rate limit tool calls
  • Never expose sensitive data
  • Unit test registration and execution
  • Test error handling
  • Test with real AI agents using MCP-B Extension

Additional Resources