Initial commit
This commit is contained in:
412
references/tool-patterns.md
Normal file
412
references/tool-patterns.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# 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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Result text' }]
|
||||
};
|
||||
```
|
||||
|
||||
### Multiple Content Blocks
|
||||
```typescript
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: 'Summary' },
|
||||
{ type: 'text', text: 'Details: ...' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Error Response
|
||||
```typescript
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Error message' }],
|
||||
isError: true
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-10-28
|
||||
Reference in New Issue
Block a user