diff --git a/CLAUDE.md b/CLAUDE.md index fac4a60..2ca806f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ The codebase organizes into: ## Key Architectural Patterns -**Tool Architecture:** All tools extend `BaseTool`. Tools auto-validate inputs using Zod schemas. Each tool lives in `src/tools/tool-name-tool/` with separate `*.schema.ts` and `*.tool.ts` files. +**Tool Architecture:** All tools extend `BaseTool` or `MapboxApiBasedTool`. Tools auto-validate inputs and outputs using Zod schemas. Each tool lives in `src/tools/tool-name-tool/` with separate `*.input.schema.ts`, `*.output.schema.ts`, and `*.ts` files. **Prompt Architecture:** All prompts extend `BasePrompt` abstract class. Prompts orchestrate multi-step workflows, guiding AI assistants through complex tasks with best practices built-in. Each prompt lives in `src/prompts/` with separate files per prompt (e.g., `CreateAndPreviewStylePrompt.ts`). Prompts use kebab-case naming (e.g., `create-and-preview-style`). @@ -47,10 +47,26 @@ npm install npm test npm run build npm run inspect:build # Interactive MCP inspector -npx plop create-tool # Generate tool scaffold ``` -**New tool creation:** Run `npx plop create-tool` for interactive scaffolding (provide name without "Tool" suffix). Generates three files: `*.schema.ts`, `*.tool.ts`, and `*.test.ts`. +**New tool creation:** + +```bash +# Interactive mode (requires TTY - use in terminal): +npx plop create-tool + +# Non-interactive mode (for AI agents, CI, or scripts): +npx plop create-tool "api-based" "ToolName" +npx plop create-tool "local" "ToolName" + +# Examples: +npx plop create-tool "api-based" "Search" +npx plop create-tool "local" "Validator" +``` + +**Note**: When running from AI agents or non-TTY environments (like Claude Code), always use non-interactive mode with command-line arguments to avoid readline errors. + +Generates three files: `*.input.schema.ts`, `*.output.schema.ts`, and `*.test.ts` in appropriate directories (src/tools/ for implementation, test/tools/ for tests). **Testing workflow:** @@ -61,7 +77,7 @@ npx plop create-tool # Generate tool scaffold ## Important Constraints - **Tool naming:** Tool names (MCP identifiers) must be `snake_case_tool` (e.g., `list_styles_tool`). TypeScript class names follow `PascalCaseTool` convention (e.g., `ListStylesTool`) -- Schema files must be separate from implementation files (`*.schema.ts` vs `*.tool.ts`) +- Schema files must be separate from implementation files: `*.input.schema.ts`, `*.output.schema.ts`, and `*.ts` - Avoid `any` types; add comments explaining unavoidable usage - Never execute real network calls in tests—mock `HttpPipeline` instead - All Mapbox API tools require valid token with specific scopes (most common failure mode) diff --git a/plop-templates/local-tool.hbs b/plop-templates/local-tool.hbs index 998287d..8b3700d 100644 --- a/plop-templates/local-tool.hbs +++ b/plop-templates/local-tool.hbs @@ -1,72 +1,95 @@ +import { z } from 'zod'; import { BaseTool } from '../BaseTool.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { {{pascalCase name}}InputSchema } from './{{pascalCase name}}Tool.input.schema.js'; import { - {{pascalCase name}}Schema, - {{pascalCase name}}Input -} from './{{pascalCase name}}Tool.schema.js'; + {{pascalCase name}}OutputSchema, + type {{pascalCase name}}Output +} from './{{pascalCase name}}Tool.output.schema.js'; /** * {{pascalCase name}}Tool - Local processing tool - * + * * TODO: Provide a detailed description of what this tool does - * + * * This tool performs local data processing without making external API calls. * It's designed for operations like data transformation, file processing, * calculations, or other computational tasks. - * + * * @example * ```typescript * const tool = new {{pascalCase name}}Tool(); - * const result = await tool.run({ + * const result = await tool.run({ * // TODO: Add example input parameters * }); * ``` */ export class {{pascalCase name}}Tool extends BaseTool< - typeof {{pascalCase name}}Schema + typeof {{pascalCase name}}InputSchema, + typeof {{pascalCase name}}OutputSchema > { readonly name = '{{snakeCase name}}_tool'; - readonly description = 'Tool description.'; // TODO: Update with actual description + readonly description = 'TODO: Update with actual description'; + readonly annotations = { + title: '{{pascalCase name}} Tool', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + }; constructor() { - super({ inputSchema: {{pascalCase name}}Schema }); + super({ + inputSchema: {{pascalCase name}}InputSchema, + outputSchema: {{pascalCase name}}OutputSchema + }); } + /** + * Execute the tool logic + * @param input - Validated input from {{pascalCase name}}InputSchema + * @returns CallToolResult with structured output + */ protected async execute( - input: {{pascalCase name}}Input - ): Promise<{ type: 'text'; text: string }> { + input: z.infer + ): Promise { try { // TODO: Implement your tool logic here // This tool doesn't make API calls // You can process data locally, manipulate files, etc. - + // Example implementation: - // const result = await processData(input); - // - // // Validate result if needed - // if (!result) { - // throw new Error('Processing failed: No result generated'); - // } - // + // const result = processData(input); + // + // // Validate result against output schema + // const validatedResult = {{pascalCase name}}OutputSchema.parse(result); + // // return { - // type: 'text', - // text: JSON.stringify(result, null, 2) + // content: [{ type: 'text', text: JSON.stringify(validatedResult, null, 2) }], + // structuredContent: validatedResult, + // isError: false // }; // Placeholder implementation - replace with actual logic return { - type: 'text', - text: JSON.stringify({ - message: 'Tool executed successfully', - input, - timestamp: new Date().toISOString() - }, null, 2) + content: [{ + type: 'text', + text: JSON.stringify({ + message: 'Tool executed successfully', + input, + timestamp: new Date().toISOString() + }, null, 2) + }], + isError: false }; } catch (error) { - // Handle specific error types - if (error instanceof Error) { - throw new Error(`{{pascalCase name}}Tool execution failed: ${error.message}`); - } - throw new Error('{{pascalCase name}}Tool execution failed: Unknown error occurred'); + const errorMessage = error instanceof Error ? error.message : String(error); + this.log('error', `${this.name}: ${errorMessage}`); + + return { + content: [{ type: 'text', text: `Error: ${errorMessage}` }], + isError: true + }; } } -} \ No newline at end of file +} diff --git a/plop-templates/local-tool.test.hbs b/plop-templates/local-tool.test.hbs index a60b1e5..0f75cd8 100644 --- a/plop-templates/local-tool.test.hbs +++ b/plop-templates/local-tool.test.hbs @@ -1,4 +1,8 @@ -import { {{pascalCase name}}Tool } from '../{{kebabCase name}}-tool/{{pascalCase name}}Tool.js'; +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect, afterEach, beforeEach } from 'vitest'; +import { {{pascalCase name}}Tool } from '../../../src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.js'; describe('{{pascalCase name}}Tool', () => { let tool: {{pascalCase name}}Tool; @@ -8,76 +12,72 @@ describe('{{pascalCase name}}Tool', () => { }); afterEach(() => { - jest.restoreAllMocks(); + // Clean up if needed + }); + + it('should have correct tool metadata', () => { + expect(tool.name).toBe('{{snakeCase name}}_tool'); + expect(tool.description).toBeTruthy(); + expect(tool.annotations).toBeDefined(); }); - describe('tool metadata', () => { - it('should have correct name and description', () => { - expect(tool.name).toBe('{{snakeCase name}}_tool'); - expect(tool.description).toBe('Tool description.'); - }); + it('should execute successfully with valid input', async () => { + // TODO: Replace with actual valid input for your tool + const testInput = {}; + + const result = await tool.run(testInput); - it('should have correct input schema', () => { - const { {{pascalCase name}}Schema } = require('./{{pascalCase name}}Tool.schema.js'); - expect({{pascalCase name}}Schema).toBeDefined(); - }); + expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + + // Verify the response structure + const parsedResponse = JSON.parse(result.content[0].text); + expect(parsedResponse).toHaveProperty('message'); + expect(parsedResponse).toHaveProperty('timestamp'); }); - describe('execution', () => { - it('should execute successfully with valid input', async () => { - // TODO: Replace with actual valid input for your tool - const testInput = {}; - const result = await tool.run(testInput); - - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); - - // Verify the response structure - const parsedResponse = JSON.parse(result.content[0].text); - expect(parsedResponse).toHaveProperty('message'); - expect(parsedResponse).toHaveProperty('timestamp'); - }); - - it('should include timestamp in response', async () => { - const testInput = {}; - const result = await tool.run(testInput); - - expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); - expect(parsedResponse.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); - }); - - it('should echo input in response', async () => { - // TODO: Replace with actual test input for your tool - const testInput = {}; - const result = await tool.run(testInput); - - expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); - expect(parsedResponse.input).toEqual(testInput); - }); - - // TODO: Add more specific test cases based on your tool's functionality - // it('should handle specific use case', async () => { - // const testInput = { /* specific input */ }; - // const result = await tool.run(testInput); - // - // expect(result.isError).toBe(false); - // // Add specific assertions for your tool's logic - // }); - - // TODO: Add invalid input tests only if your tool has input validation - // Remove this comment and uncomment the test below if you add input validation - // it('should handle invalid input gracefully', async () => { - // const invalidInput = { /* invalid input that should fail validation */ }; - // const result = await tool.run(invalidInput); - // - // expect(result.isError).toBe(true); - // expect(result.content[0]).toMatchObject({ - // type: 'text', - // text: expect.stringContaining('error') - // }); - // }); + it('should include timestamp in response', async () => { + // TODO: Replace with actual input + const testInput = {}; + + const result = await tool.run(testInput); + + expect(result.isError).toBe(false); + const parsedResponse = JSON.parse(result.content[0].text); + expect(parsedResponse.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); }); -}); \ No newline at end of file + + it('should echo input in response', async () => { + // TODO: Replace with actual test input for your tool + const testInput = {}; + + const result = await tool.run(testInput); + + expect(result.isError).toBe(false); + const parsedResponse = JSON.parse(result.content[0].text); + expect(parsedResponse.input).toEqual(testInput); + }); + + // TODO: Add more specific test cases based on your tool's functionality + // it('should handle specific use case', async () => { + // const testInput = { /* specific input */ }; + // const result = await tool.run(testInput); + // + // expect(result.isError).toBe(false); + // // Add specific assertions for your tool's logic + // }); + + // TODO: Add input validation tests only if your tool has validation + // Remove this comment and uncomment the test below if you add input validation + // it('should handle invalid input gracefully', async () => { + // const invalidInput = { /* invalid input that should fail validation */ }; + // const result = await tool.run(invalidInput); + // + // expect(result.isError).toBe(true); + // expect(result.content[0]).toMatchObject({ + // type: 'text', + // text: expect.stringContaining('error') + // }); + // }); +}); diff --git a/plop-templates/mapbox-api-tool.hbs b/plop-templates/mapbox-api-tool.hbs index 30b7a99..0eecdfe 100644 --- a/plop-templates/mapbox-api-tool.hbs +++ b/plop-templates/mapbox-api-tool.hbs @@ -1,91 +1,110 @@ +import { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; +import type { HttpRequest } from '../../utils/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ToolExecutionContext } from '../../utils/tracing.js'; +import { {{pascalCase name}}InputSchema } from './{{pascalCase name}}Tool.input.schema.js'; import { - {{pascalCase name}}Schema, - {{pascalCase name}}Input -} from './{{pascalCase name}}Tool.schema.js'; + {{pascalCase name}}OutputSchema, + type {{pascalCase name}}Output +} from './{{pascalCase name}}Tool.output.schema.js'; /** * {{pascalCase name}}Tool - Mapbox API integration tool - * + * * TODO: Provide a detailed description of what this tool does - * + * * This tool integrates with Mapbox APIs to provide geospatial functionality. * It handles authentication, request formatting, and response processing * for Mapbox services. - * + * * @requires MAPBOX_ACCESS_TOKEN - Valid Mapbox access token with appropriate scopes - * + * * @example * ```typescript - * const tool = new {{pascalCase name}}Tool(); - * const result = await tool.run({ + * const tool = new {{pascalCase name}}Tool({ httpRequest }); + * const result = await tool.run({ * // TODO: Add example input parameters * }); * ``` - * + * * @see {@link https://docs.mapbox.com/api/} Mapbox API Documentation */ export class {{pascalCase name}}Tool extends MapboxApiBasedTool< - typeof {{pascalCase name}}Schema + typeof {{pascalCase name}}InputSchema, + typeof {{pascalCase name}}OutputSchema > { readonly name = '{{snakeCase name}}_tool'; - readonly description = 'Tool description.'; // TODO: Update with actual description + readonly description = 'TODO: Update with actual description'; + readonly annotations = { + title: '{{pascalCase name}} Tool', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + }; - constructor() { - super({ inputSchema: {{pascalCase name}}Schema }); + constructor({ httpRequest }: { httpRequest: HttpRequest }) { + super({ + inputSchema: {{pascalCase name}}InputSchema, + outputSchema: {{pascalCase name}}OutputSchema, + httpRequest + }); } + /** + * Execute the tool logic + * @param input - Validated input from {{pascalCase name}}InputSchema + * @param accessToken - Mapbox access token + * @param context - Tool execution context for tracing + * @returns CallToolResult with structured output + */ protected async execute( - input: {{pascalCase name}}Input, - accessToken?: string - ): Promise<{ type: 'text'; text: string }> { + input: z.infer, + accessToken: string, + context: ToolExecutionContext + ): Promise { try { // TODO: Implement your Mapbox API call here - + // Example implementation: - // const username = MapboxApiBasedTool.getUserNameFromToken(); - // const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`; - // - // const response = await fetch(url); - // + // const url = `${MapboxApiBasedTool.mapboxApiEndpoint}your-api-endpoint?access_token=${accessToken}`; + // + // const response = await this.httpRequest(url); + // // if (!response.ok) { - // const errorText = await response.text().catch(() => 'Unknown error'); - // throw new Error(`Mapbox API request failed: ${response.status} ${response.statusText}. ${errorText}`); - // } - // - // const data = await response.json(); - // - // // Validate API response if needed - // if (!data || typeof data !== 'object') { - // throw new Error('Invalid response from Mapbox API'); + // return this.handleApiError(response, 'perform operation'); // } - // + // + // const data = (await response.json()) as {{pascalCase name}}Output; + // // return { - // type: 'text', - // text: JSON.stringify(data, null, 2) + // content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], + // structuredContent: data, + // isError: false // }; // Placeholder implementation - replace with actual API call return { - type: 'text', - text: JSON.stringify({ - message: 'Tool executed successfully', - input, - timestamp: new Date().toISOString(), - note: 'Replace this with actual Mapbox API call' - }, null, 2) + content: [{ + type: 'text', + text: JSON.stringify({ + message: 'Tool executed successfully', + input, + timestamp: new Date().toISOString(), + note: 'Replace this with actual Mapbox API call' + }, null, 2) + }], + isError: false }; } catch (error) { - // Handle different error types - if (error instanceof TypeError && error.message.includes('fetch')) { - throw new Error('{{pascalCase name}}Tool: Network error occurred while connecting to Mapbox API'); - } - - if (error instanceof Error) { - throw new Error(`{{pascalCase name}}Tool execution failed: ${error.message}`); - } - - throw new Error('{{pascalCase name}}Tool execution failed: Unknown error occurred'); + const errorMessage = error instanceof Error ? error.message : String(error); + this.log('error', `${this.name}: ${errorMessage}`); + + return { + content: [{ type: 'text', text: `Error: ${errorMessage}` }], + isError: true + }; } } -} \ No newline at end of file +} diff --git a/plop-templates/mapbox-api-tool.test.hbs b/plop-templates/mapbox-api-tool.test.hbs index 0765d96..ac8afd8 100644 --- a/plop-templates/mapbox-api-tool.test.hbs +++ b/plop-templates/mapbox-api-tool.test.hbs @@ -1,114 +1,113 @@ -import { {{pascalCase name}}Tool } from '../{{kebabCase name}}-tool/{{pascalCase name}}Tool.js'; -import { setupFetch, assertHeadersSent } from '../../utils/requestUtils.test-helpers.js'; +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +process.env.MAPBOX_ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.signature'; + +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'; +import { {{pascalCase name}}Tool } from '../../../src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; + +// TODO: Add sample response data matching your OutputSchema +const sampleResponse = { + // Add expected API response structure here + result: 'test data' +}; describe('{{pascalCase name}}Tool', () => { + let mockHttpRequest: ReturnType; let tool: {{pascalCase name}}Tool; beforeEach(() => { - tool = new {{pascalCase name}}Tool(); + // Mock httpRequest function + mockHttpRequest = vi.fn(); + tool = new {{pascalCase name}}Tool({ httpRequest: mockHttpRequest as HttpRequest }); }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); - describe('tool metadata', () => { - it('should have correct name and description', () => { - expect(tool.name).toBe('{{snakeCase name}}_tool'); - expect(tool.description).toBe('Tool description.'); - }); + it('should have correct tool metadata', () => { + expect(tool.name).toBe('{{snakeCase name}}_tool'); + expect(tool.description).toBeTruthy(); + expect(tool.annotations).toBeDefined(); + }); - it('should have correct input schema', () => { - const { {{pascalCase name}}Schema } = require('./{{pascalCase name}}Tool.schema.js'); - expect({{pascalCase name}}Schema).toBeDefined(); + it('should make API request with correct headers and return structured content', async () => { + // Mock successful API response + mockHttpRequest.mockResolvedValue({ + ok: true, + status: 200, + json: async () => sampleResponse, + headers: new Headers() }); + + // TODO: Replace with actual valid input for your tool + const testInput = {}; + + const result = await tool.run(testInput); + + expect(result.isError).toBe(false); + expect(result.content).toBeDefined(); + expect(result.content[0].type).toBe('text'); + + // Verify HTTP request was made + expect(mockHttpRequest).toHaveBeenCalled(); + + // TODO: Add assertions for your specific API endpoint and parameters + // const callUrl = mockHttpRequest.mock.calls[0][0]; + // expect(callUrl).toContain('your-api-endpoint'); }); - describe('API requests', () => { - it('should send custom headers', async () => { - const mockFetch = setupFetch(); - - // TODO: Replace with actual valid input for your tool - const testInput = {}; - await tool.run(testInput); - - assertHeadersSent(mockFetch); + it('should handle API errors gracefully', async () => { + // Mock failed API response + mockHttpRequest.mockResolvedValue({ + ok: false, + status: 400, + statusText: 'Bad Request', + text: async () => JSON.stringify({ message: 'Invalid parameters' }), + json: async () => ({ message: 'Invalid parameters' }), + headers: new Headers({ 'content-type': 'application/json' }) }); - it('should handle successful API response', async () => { - const mockResponseData = { success: true, data: 'test' }; - const mockFetch = setupFetch({ - ok: true, - status: 200, - json: () => Promise.resolve(mockResponseData) - }); - - // TODO: Replace with actual valid input for your tool - const testInput = {}; - const result = await tool.run(testInput); - - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); - - // Verify response structure for placeholder implementation - const parsedResponse = JSON.parse(result.content[0].text); - expect(parsedResponse).toHaveProperty('message'); - expect(parsedResponse).toHaveProperty('timestamp'); - expect(parsedResponse).toHaveProperty('note'); - - // TODO: Remove this assertion when implementing actual API calls - // assertHeadersSent(mockFetch); - }); + // TODO: Replace with actual input + const testInput = {}; - it('should handle API errors gracefully', async () => { - const mockFetch = setupFetch({ - ok: false, - status: 404, - statusText: 'Not Found' - }); - - // TODO: Replace with actual valid input for your tool - const testInput = {}; - const result = await tool.run(testInput); - - expect(result.isError).toBe(true); - expect(result.content[0]).toMatchObject({ - type: 'text', - text: 'Internal error has occurred.' - }); - assertHeadersSent(mockFetch); - }); + const result = await tool.run(testInput); - // TODO: Add more specific test cases based on your tool's API functionality - // it('should handle specific API use case', async () => { - // const mockResponseData = { /* expected response */ }; - // const mockFetch = setupFetch({ - // ok: true, - // status: 200, - // json: () => Promise.resolve(mockResponseData) - // }); - // - // const testInput = { /* specific input */ }; - // const result = await tool.run(testInput); - // - // expect(result.isError).toBe(false); - // // Add specific assertions for your tool's logic - // assertHeadersSent(mockFetch); - // }); + expect(result.isError).toBe(true); + expect(result.content[0]).toMatchObject({ + type: 'text', + text: expect.stringContaining('Invalid parameters') + }); }); - describe('input validation', () => { - // TODO: Add input validation tests if your tool has required parameters - // it('should handle invalid input gracefully', async () => { - // const invalidInput = { /* invalid input that should fail validation */ }; - // const result = await tool.run(invalidInput); - // - // expect(result.isError).toBe(true); - // expect(result.content[0]).toMatchObject({ - // type: 'text', - // text: expect.stringContaining('error') - // }); - // }); + it('should handle network errors gracefully', async () => { + // Mock network error + mockHttpRequest.mockRejectedValue(new TypeError('Failed to fetch')); + + // TODO: Replace with actual input + const testInput = {}; + + const result = await tool.run(testInput); + + expect(result.isError).toBe(true); + expect(result.content[0].type).toBe('text'); }); -}); \ No newline at end of file + + // TODO: Add more specific test cases based on your tool's functionality + // it('should handle specific input parameter', async () => { + // mockHttpRequest.mockResolvedValue({ + // ok: true, + // status: 200, + // json: async () => sampleResponse, + // headers: new Headers() + // }); + // + // const testInput = { /* specific input */ }; + // const result = await tool.run(testInput); + // + // expect(result.isError).toBe(false); + // // Add specific assertions for your tool's logic + // }); +}); diff --git a/plop-templates/tool.schema.hbs b/plop-templates/tool.input.schema.hbs similarity index 95% rename from plop-templates/tool.schema.hbs rename to plop-templates/tool.input.schema.hbs index 231027d..def2fc8 100644 --- a/plop-templates/tool.schema.hbs +++ b/plop-templates/tool.input.schema.hbs @@ -26,10 +26,10 @@ import { z } from 'zod'; * - GeoJSON: z.union([z.string(), z.object({}).passthrough()]).describe('GeoJSON data') {{/if}} */ -export const {{pascalCase name}}Schema = z.object({ +export const {{pascalCase name}}InputSchema = z.object({ // TODO: Add your input parameters here // Remove this comment when you add real parameters - + // Example parameter (remove this and add your own): // name: z.string().min(1).describe('Name of the resource'), // options: z.object({ @@ -40,4 +40,4 @@ export const {{pascalCase name}}Schema = z.object({ /** * Inferred TypeScript type for {{pascalCase name}}Tool input */ -export type {{pascalCase name}}Input = z.infer; \ No newline at end of file +export type {{pascalCase name}}Input = z.infer; \ No newline at end of file diff --git a/plop-templates/tool.output.schema.hbs b/plop-templates/tool.output.schema.hbs new file mode 100644 index 0000000..ecf638e --- /dev/null +++ b/plop-templates/tool.output.schema.hbs @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +/** + * Output schema for {{pascalCase name}}Tool + * + * TODO: Define the appropriate output schema for your tool + * This is a placeholder schema that should be replaced with your actual output structure + * + * Common output patterns: + * - API responses: Define the exact structure returned by the API + * - Processing results: Define computed values, transformed data, etc. + * - Status responses: Include success flags, messages, metadata + * + * Example patterns: + * ```typescript + * export const {{pascalCase name}}OutputSchema = z.object({ + * success: z.boolean().describe('Whether the operation succeeded'), + * data: z.object({ + * // Your actual data fields + * }).describe('The result data'), + * metadata: z.object({ + * timestamp: z.string().describe('ISO 8601 timestamp'), + * // Other metadata fields + * }).optional().describe('Optional metadata') + * }); + * ``` + */ +export const {{pascalCase name}}OutputSchema = z.object({ + // TODO: Replace this placeholder with your actual output schema + result: z.unknown().describe('Tool execution result') +}); + +/** + * Type inference for {{pascalCase name}}Output + */ +export type {{pascalCase name}}Output = z.infer; diff --git a/plopfile.cjs b/plopfile.cjs index a9e9df5..c579313 100644 --- a/plopfile.cjs +++ b/plopfile.cjs @@ -1,3 +1,29 @@ +/** + * Plop generator for creating new MCP tools. + * + * Usage: + * Interactive mode (requires TTY): + * npx plop create-tool + * + * Non-interactive mode (for CI, scripts, or non-TTY environments): + * npx plop create-tool "api-based" "ToolName" + * npx plop create-tool "local" "ToolName" + * + * Examples: + * npx plop create-tool "api-based" "Search" + * npx plop create-tool "local" "Validator" + * + * Tool types: + * - api-based: Makes API calls to Mapbox services + * - local: Local processing only, no API calls + * + * This generates: + * - src/tools/search-tool/SearchTool.ts + * - src/tools/search-tool/SearchTool.schema.ts + * - src/tools/search-tool/SearchTool.test.ts + * - Updates src/index.ts + * - Updates README.md + */ module.exports = function (plop) { // Register handlebars helpers plop.setHelper('eq', function (a, b) { @@ -30,52 +56,61 @@ module.exports = function (plop) { ], actions: function(data) { const actions = []; - + // Choose template based on tool type const toolTemplate = data.toolType === 'api-based' ? 'plop-templates/mapbox-api-tool.hbs' : 'plop-templates/local-tool.hbs'; const testTemplate = data.toolType === 'api-based' ? 'plop-templates/mapbox-api-tool.test.hbs' : 'plop-templates/local-tool.test.hbs'; - const schemaTemplate = 'plop-templates/tool.schema.hbs'; // Unified schema template - + + // Generate input schema actions.push({ type: 'add', - path: 'src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.schema.ts', - templateFile: schemaTemplate, + path: 'src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.input.schema.ts', + templateFile: 'plop-templates/tool.input.schema.hbs', data: { toolType: data.toolType }, // Pass toolType to template }); - + + // Generate output schema + actions.push({ + type: 'add', + path: 'src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.output.schema.ts', + templateFile: 'plop-templates/tool.output.schema.hbs', + }); + + // Generate tool class actions.push({ type: 'add', path: 'src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.ts', templateFile: toolTemplate, }); - + + // Generate test file in separate test directory actions.push({ type: 'add', - path: 'src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.test.ts', + path: 'test/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.test.ts', templateFile: testTemplate, }); - + actions.push({ type: 'append', path: 'src/index.ts', pattern: /(\/\/ INSERT NEW TOOL REGISTRATION HERE)/, template: 'new {{pascalCase name}}Tool().installTo(server);', }); - + actions.push({ type: 'append', path: 'src/index.ts', pattern: /(\/\/ INSERT NEW TOOL IMPORT HERE)/, template: "import { {{pascalCase name}}Tool } from './tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.js';", }); - + actions.push({ type: 'append', path: 'README.md', pattern: /(### Tools)/, template: '\n\n#### {{titleCase name}} tool\n\nDescription goes here...', }); - + console.log('\n🎉 Tool created successfully!'); console.log('\n📝 Next steps:'); console.log('1. Update the input schema in {{pascalCase name}}Tool.schema.ts'); @@ -86,8 +121,8 @@ module.exports = function (plop) { console.log(' npm test -- src/tools/tool-naming-convention.test.ts --updateSnapshot'); console.log('6. Run all tests to ensure everything works:'); console.log(' npm test'); - + return actions; }, }); -}; \ No newline at end of file +};