Files
gh-jezweb-claude-skills-ski…/references/tool-patterns.md
2025-11-30 08:25:43 +08:00

8.4 KiB

Common Tool Implementation Patterns

Production-tested patterns for implementing MCP tools in TypeScript.


Pattern 1: External API Wrapper

Wrap external REST APIs as MCP tools.

server.registerTool(
  'fetch-weather',
  {
    description: 'Fetches weather data from OpenWeatherMap API',
    inputSchema: z.object({
      city: z.string().describe('City name'),
      units: z.enum(['metric', 'imperial']).default('metric')
    })
  },
  async ({ city, units }, env) => {
    try {
      const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${env.WEATHER_API_KEY}`
      );

      if (!response.ok) {
        throw new Error(`API error: ${response.statusText}`);
      }

      const data = await response.json();

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(data, null, 2)
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Best Practices:

  • Always validate API keys exist before calling
  • Use proper URL encoding for parameters
  • Handle HTTP errors gracefully
  • Return structured error messages
  • Consider caching responses in KV

Pattern 2: Database Query Tool

Execute SQL queries on D1 database.

server.registerTool(
  'search-users',
  {
    description: 'Searches users in database',
    inputSchema: z.object({
      query: z.string().describe('Search query'),
      limit: z.number().default(10).max(100)
    })
  },
  async ({ query, limit }, env) => {
    if (!env.DB) {
      return {
        content: [{ type: 'text', text: 'Database not configured' }],
        isError: true
      };
    }

    try {
      const result = await env.DB
        .prepare('SELECT id, name, email FROM users WHERE name LIKE ? LIMIT ?')
        .bind(`%${query}%`, limit)
        .all();

      return {
        content: [{
          type: 'text',
          text: `Found ${result.results.length} users:\n${JSON.stringify(result.results, null, 2)}`
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Database error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Security:

  • ⚠️ Never allow raw SQL injection
  • Use parameterized queries only
  • Limit result set size
  • Don't expose sensitive fields (passwords, tokens)

Pattern 3: File Operations (R2)

Read/write files from R2 object storage.

server.registerTool(
  'get-file',
  {
    description: 'Retrieves file from R2 storage',
    inputSchema: z.object({
      key: z.string().describe('File key/path')
    })
  },
  async ({ key }, env) => {
    if (!env.BUCKET) {
      return {
        content: [{ type: 'text', text: 'R2 not configured' }],
        isError: true
      };
    }

    try {
      const object = await env.BUCKET.get(key);

      if (!object) {
        return {
          content: [{ type: 'text', text: `File "${key}" not found` }],
          isError: true
        };
      }

      const text = await object.text();

      return {
        content: [{
          type: 'text',
          text: `File: ${key}\nSize: ${object.size} bytes\n\n${text}`
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `R2 error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Pattern 4: Validation & Transformation

Transform and validate data.

server.registerTool(
  'validate-email',
  {
    description: 'Validates and normalizes email addresses',
    inputSchema: z.object({
      email: z.string().describe('Email to validate')
    })
  },
  async ({ email }) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    const isValid = emailRegex.test(email);
    const normalized = email.toLowerCase().trim();

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          valid: isValid,
          original: email,
          normalized,
          domain: isValid ? normalized.split('@')[1] : null
        }, null, 2)
      }]
    };
  }
);

Pattern 5: Multi-Step Operations

Chain multiple operations together.

server.registerTool(
  'analyze-and-store',
  {
    description: 'Analyzes text and stores result',
    inputSchema: z.object({
      text: z.string(),
      key: z.string()
    })
  },
  async ({ text, key }, env) => {
    try {
      // Step 1: Analyze
      const wordCount = text.split(/\s+/).length;
      const charCount = text.length;

      const analysis = {
        wordCount,
        charCount,
        avgWordLength: (charCount / wordCount).toFixed(2),
        timestamp: new Date().toISOString()
      };

      // Step 2: Store in KV
      await env.CACHE.put(key, JSON.stringify(analysis));

      return {
        content: [{
          type: 'text',
          text: `Analysis complete and stored at "${key}":\n${JSON.stringify(analysis, null, 2)}`
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Pattern 6: Streaming Responses

Handle large responses efficiently.

server.registerTool(
  'fetch-large-file',
  {
    description: 'Fetches and summarizes large file',
    inputSchema: z.object({
      url: z.string().url()
    })
  },
  async ({ url }) => {
    const MAX_SIZE = 100000; // 100KB

    try {
      const response = await fetch(url);
      const reader = response.body?.getReader();

      if (!reader) {
        throw new Error('No response body');
      }

      let text = '';
      let totalSize = 0;

      while (true) {
        const { done, value } = await reader.read();

        if (done || totalSize >= MAX_SIZE) break;

        text += new TextDecoder().decode(value);
        totalSize += value.length;
      }

      return {
        content: [{
          type: 'text',
          text: totalSize >= MAX_SIZE
            ? `File too large. First 100KB:\n${text}`
            : text
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Pattern 7: Caching Tool Responses

Cache expensive operations.

server.registerTool(
  'get-exchange-rate',
  {
    description: 'Gets currency exchange rate (cached)',
    inputSchema: z.object({
      from: z.string().length(3),
      to: z.string().length(3)
    })
  },
  async ({ from, to }, env) => {
    const cacheKey = `exchange:${from}:${to}`;

    // Check cache first
    const cached = await env.CACHE.get(cacheKey);
    if (cached) {
      return {
        content: [{
          type: 'text',
          text: `${from}${to}: ${cached} (cached)`
        }]
      };
    }

    // Fetch fresh data
    try {
      const response = await fetch(
        `https://api.exchangerate-api.com/v4/latest/${from}`
      );
      const data = await response.json();
      const rate = data.rates[to];

      // Cache for 1 hour
      await env.CACHE.put(cacheKey, String(rate), { expirationTtl: 3600 });

      return {
        content: [{
          type: 'text',
          text: `${from}${to}: ${rate} (fresh)`
        }]
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

Error Handling Best Practices

server.registerTool('example', { ... }, async (args, env) => {
  try {
    // Operation
  } catch (error) {
    // Safe error handling
    const message = error instanceof Error
      ? error.message
      : 'Unknown error';

    // Don't leak sensitive data
    const safeMessage = message.replace(/api[_-]?key[s]?[:\s]+[^\s]+/gi, '[REDACTED]');

    return {
      content: [{ type: 'text', text: `Error: ${safeMessage}` }],
      isError: true
    };
  }
});

Tool Response Formats

Text Response

return {
  content: [{ type: 'text', text: 'Result text' }]
};

Multiple Content Blocks

return {
  content: [
    { type: 'text', text: 'Summary' },
    { type: 'text', text: 'Details: ...' }
  ]
};

Error Response

return {
  content: [{ type: 'text', text: 'Error message' }],
  isError: true
};

Last Updated: 2025-10-28