Skip to main content
Glama
CREATING_TOOLS.md8.17 kB
# Creating Custom Tools This guide explains how to create custom tools for your MCP server. ## Tool Structure Every tool must export a default object with the following properties: ```javascript export default { name: 'tool_name', // Unique identifier (snake_case) description: 'What it does', // Human-readable description inputSchema: {}, // JSON Schema for parameters handler: async (args) => {} // Implementation function } ``` ## Basic Example ```javascript // custom-tools/hello-world.js export default { name: 'hello_world', description: 'Returns a greeting message', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name to greet' } }, required: ['name'] }, handler: async (args) => { return { message: `Hello, ${args.name}!`, timestamp: new Date().toISOString() }; } }; ``` ## Input Schema The input schema follows JSON Schema specification: ### Basic Types ```javascript inputSchema: { type: 'object', properties: { stringParam: { type: 'string' }, numberParam: { type: 'number' }, booleanParam: { type: 'boolean' }, arrayParam: { type: 'array', items: { type: 'string' } }, objectParam: { type: 'object', properties: { nested: { type: 'string' } } } } } ``` ### Validation ```javascript inputSchema: { type: 'object', properties: { email: { type: 'string', format: 'email' }, age: { type: 'number', minimum: 0, maximum: 150 }, status: { type: 'string', enum: ['active', 'inactive', 'pending'] } }, required: ['email', 'status'] } ``` ## Handler Function The handler function receives the validated arguments and returns the result: ### Async Operations ```javascript handler: async (args) => { // Perform async operations const data = await fetchData(args.id); const processed = await processData(data); return { success: true, result: processed }; } ``` ### Error Handling ```javascript handler: async (args) => { try { const result = await riskyOperation(args); return { success: true, result }; } catch (error) { // Return error info (will be handled by server) return { success: false, error: error.message, code: error.code || 'UNKNOWN_ERROR' }; } } ``` ### File Operations ```javascript import fs from 'fs-extra'; import path from 'path'; export default { name: 'read_file', description: 'Read contents of a file', inputSchema: { type: 'object', properties: { filename: { type: 'string' } }, required: ['filename'] }, handler: async (args) => { const filepath = path.join(process.cwd(), 'data', args.filename); // Check if file exists if (!await fs.pathExists(filepath)) { throw new Error(`File not found: ${args.filename}`); } // Read file const content = await fs.readFile(filepath, 'utf-8'); return { filename: args.filename, content: content, size: content.length }; } }; ``` ## Advanced Examples ### Database Operations ```javascript import Database from 'better-sqlite3'; export default { name: 'query_database', description: 'Execute SQL query', inputSchema: { type: 'object', properties: { query: { type: 'string' }, params: { type: 'array', items: { type: ['string', 'number', 'boolean', 'null'] } } }, required: ['query'] }, handler: async (args) => { const db = new Database('data/app.db'); try { const stmt = db.prepare(args.query); const results = stmt.all(...(args.params || [])); return { success: true, rows: results, count: results.length }; } finally { db.close(); } } }; ``` ### External API Calls ```javascript import fetch from 'node-fetch'; export default { name: 'fetch_weather', description: 'Get weather information', inputSchema: { type: 'object', properties: { city: { type: 'string' }, units: { type: 'string', enum: ['metric', 'imperial'], default: 'metric' } }, required: ['city'] }, handler: async (args) => { const apiKey = process.env.WEATHER_API_KEY; const url = `https://api.openweathermap.org/data/2.5/weather?q=${args.city}&units=${args.units}&appid=${apiKey}`; const response = await fetch(url); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to fetch weather'); } return { city: data.name, temperature: data.main.temp, description: data.weather[0].description, humidity: data.main.humidity, wind_speed: data.wind.speed }; } }; ``` ### Long-Running Operations ```javascript export default { name: 'process_batch', description: 'Process a batch of items', inputSchema: { type: 'object', properties: { items: { type: 'array', items: { type: 'string' } }, parallel: { type: 'boolean', default: false } }, required: ['items'] }, handler: async (args) => { const results = []; const errors = []; const processItem = async (item) => { try { // Simulate processing await new Promise(resolve => setTimeout(resolve, 100)); return { item, status: 'processed' }; } catch (error) { return { item, status: 'failed', error: error.message }; } }; if (args.parallel) { // Process in parallel const promises = args.items.map(processItem); const allResults = await Promise.allSettled(promises); allResults.forEach((result, index) => { if (result.status === 'fulfilled') { results.push(result.value); } else { errors.push({ item: args.items[index], error: result.reason.message }); } }); } else { // Process sequentially for (const item of args.items) { const result = await processItem(item); if (result.status === 'processed') { results.push(result); } else { errors.push(result); } } } return { processed: results.length, failed: errors.length, results, errors }; } }; ``` ## Best Practices 1. **Naming Convention**: Use snake_case for tool names 2. **Descriptions**: Write clear, concise descriptions 3. **Error Handling**: Always handle errors gracefully 4. **Validation**: Use input schema to validate parameters 5. **Return Values**: Return consistent, well-structured data 6. **Logging**: Use console.error for debug logging (respects debug flag) 7. **Resources**: Clean up resources (close files, databases, etc.) 8. **Timeouts**: Implement timeouts for long-running operations 9. **Documentation**: Document complex tools with examples 10. **Testing**: Write tests for your custom tools ## Testing Your Tools ```javascript // tests/custom-tool.test.js import tool from '../custom-tools/my-tool.js'; describe('my_tool', () => { it('should return expected result', async () => { const result = await tool.handler({ param1: 'test' }); expect(result.success).toBe(true); expect(result.result).toBeDefined(); }); it('should handle errors', async () => { const result = await tool.handler({ param1: null // Invalid input }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); ``` ## Loading Custom Tools Place your tool files in the directory specified by `customToolsDir` in your configuration (default: `./custom-tools`). The server will automatically: 1. Scan the directory for `.js` files 2. Import each file 3. Register tools that export a default object 4. Log any loading errors Tools are loaded at server startup. Restart the server to load new tools.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/iptracej-education/mcp-server-template'

If you have feedback or need assistance with the MCP directory API, please join our Discord server