Skip to content

Commit 138bab3

Browse files
Fix plop template for non-interactive environments
Fixes issue where plop generator fails with readline errors when run from non-TTY environments like AI agents, CI, or piped scripts. ## Changes ### plopfile.cjs - Added comprehensive JSDoc comments explaining both interactive and non-interactive usage - Documented command-line argument syntax: `npx plop create-tool "ToolName" "tool_name_tool"` - Fixed tool registration to include `{ httpRequest }` parameter ### plop-templates/tool.hbs - Fixed execute() method signature to include `accessToken: string` parameter - Added proper `annotations` property (title, readOnlyHint, openWorldHint, etc.) - Replaced invalid syntax with valid TypeScript (throw Error vs invalid ellipsis) - Updated example to use correct API: `mapboxApiEndpoint` (not MAPBOX_API_ENDPOINT) - Added comprehensive TODO comments with proper code structure ### plop-templates/tool.test.hbs - Migrated from Jest to Vitest (describe, it, expect, vi) - Updated imports to use current test utilities (setupHttpRequest, assertHeadersSent) - Fixed import paths to match current structure - Added copyright header - Added MAPBOX_ACCESS_TOKEN env var setup - Added three test cases: custom header, structured content, API errors ### CLAUDE.md - Documented both interactive and non-interactive modes - Added clear examples and usage notes - Emphasized non-interactive mode for AI agents to avoid readline errors ## Testing Verified generator works in non-interactive mode: ```bash npx plop create-tool "Example" "example_tool" ``` Generated files: - ExampleTool.ts with proper structure and annotations - ExampleTool.input.schema.ts - ExampleTool.output.schema.ts - ExampleTool.test.ts with vitest tests - Auto-updates toolRegistry.ts with correct registration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 79f11aa commit 138bab3

File tree

4 files changed

+112
-24
lines changed

4 files changed

+112
-24
lines changed

CLAUDE.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,19 @@ npm run inspect:build # Test with MCP inspector
6464
### Creating a New Tool
6565

6666
```bash
67-
npx plop create-tool # Interactive tool scaffolding
68-
# Provide tool name without suffix (e.g., "Search" creates "SearchTool")
67+
# Interactive mode (requires TTY - use in terminal):
68+
npx plop create-tool
69+
70+
# Non-interactive mode (for AI agents, CI, or scripts):
71+
npx plop create-tool "ToolName" "tool_name_tool"
72+
73+
# Example:
74+
npx plop create-tool "Search" "search_tool"
75+
# Creates SearchTool in src/tools/search-tool/ with schemas and tests
6976
```
7077

78+
**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.
79+
7180
### Pre-commit
7281

7382
- Husky hooks auto-run linting and formatting

plop-templates/tool.hbs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export class {{pascalCase name}}Tool extends MapboxApiBasedTool<
1616
> {
1717
name = '{{toolName}}';
1818
description = 'TODO: Add tool description here.';
19+
annotations = {
20+
title: '{{pascalCase name}} Tool',
21+
readOnlyHint: true,
22+
destructiveHint: false,
23+
idempotentHint: true,
24+
openWorldHint: true
25+
};
1926

2027
constructor({ httpRequest }: { httpRequest: HttpRequest }) {
2128
super({
@@ -28,16 +35,29 @@ export class {{pascalCase name}}Tool extends MapboxApiBasedTool<
2835
/**
2936
* Execute the tool logic
3037
* @param input - Validated input from {{pascalCase name}}InputSchema
38+
* @param accessToken - Mapbox access token
3139
* @returns CallToolResult with structured output
3240
*/
3341
protected async execute(
34-
input: z.infer<typeof {{pascalCase name}}InputSchema>
42+
input: z.infer<typeof {{pascalCase name}}InputSchema>,
43+
accessToken: string
3544
): Promise<any> {
36-
... write your logic here ...
37-
// e.g.:
38-
// const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}...?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
39-
// const response = await fetch(url);
40-
// const data = await response.json();
41-
// return data;
45+
// TODO: Implement your tool logic here
46+
// Example:
47+
// const url = `${MapboxApiBasedTool.mapboxApiEndpoint}your-api-endpoint?access_token=${accessToken}`;
48+
// const response = await this.httpRequest(url);
49+
// if (!response.ok) {
50+
// return {
51+
// content: [{ type: 'text', text: `Error: ${response.statusText}` }],
52+
// isError: true
53+
// };
54+
// }
55+
// const data = (await response.json()) as {{pascalCase name}}Output;
56+
// return {
57+
// content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
58+
// structuredContent: data,
59+
// isError: false
60+
// };
61+
throw new Error('Not implemented');
4262
}
4363
}

plop-templates/tool.test.hbs

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,69 @@
1-
import { {{pascalCase name}}Tool } from '../{{kebabCase name}}-tool/{{pascalCase name}}Tool.js';
2-
import { setupFetch, assertHeadersSent } from '../../utils/requestUtils.test-helpers.js';
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
process.env.MAPBOX_ACCESS_TOKEN =
5+
'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.signature';
6+
7+
import { describe, it, expect, afterEach, vi } from 'vitest';
8+
import {
9+
setupHttpRequest,
10+
assertHeadersSent
11+
} from '../../utils/httpPipelineUtils.js';
12+
import { {{pascalCase name}}Tool } from '../../../src/tools/{{kebabCase name}}-tool/{{pascalCase name}}Tool.js';
13+
14+
// TODO: Add sample response data
15+
const sampleResponse = {
16+
// Add expected API response structure here
17+
};
318

419
describe('{{pascalCase name}}Tool', () => {
520
afterEach(() => {
6-
jest.restoreAllMocks();
21+
vi.restoreAllMocks();
722
});
823

924
it('sends custom header', async () => {
10-
const mockFetch = setupFetch();
11-
await new {{pascalCase name}}Tool().run({...});
12-
assertHeadersSent(mockFetch);
25+
const { httpRequest, mockHttpRequest } = setupHttpRequest({
26+
json: async () => sampleResponse
27+
});
28+
29+
await new {{pascalCase name}}Tool({ httpRequest }).run({
30+
// TODO: Add required input parameters
31+
});
32+
33+
assertHeadersSent(mockHttpRequest);
1334
});
1435

15-
it('handles fetch errors gracefully', async () => {
16-
const mockFetch = setupFetch({
36+
it('returns structured content for valid input', async () => {
37+
const { httpRequest } = setupHttpRequest({
38+
json: async () => sampleResponse
39+
});
40+
41+
const tool = new {{pascalCase name}}Tool({ httpRequest });
42+
const result = await tool.run({
43+
// TODO: Add required input parameters
44+
});
45+
46+
expect(result.isError).toBe(false);
47+
expect(result.structuredContent).toBeDefined();
48+
});
49+
50+
it('handles API errors gracefully', async () => {
51+
const { httpRequest } = setupHttpRequest({
1752
ok: false,
18-
status: 404,
19-
statusText: 'Not Found'
53+
status: 400,
54+
statusText: 'Bad Request',
55+
text: async () => JSON.stringify({ message: 'Invalid parameters' })
2056
});
2157

22-
const result = await new {{pascalCase name}}Tool().run({...});
58+
const tool = new {{pascalCase name}}Tool({ httpRequest });
59+
const result = await tool.run({
60+
// TODO: Add required input parameters
61+
});
2362

2463
expect(result.isError).toBe(true);
2564
expect(result.content[0]).toMatchObject({
2665
type: 'text',
27-
text: 'Request failed with status 404: Not Found'
66+
text: expect.stringContaining('Invalid parameters')
2867
});
29-
assertHeadersSent(mockFetch);
3068
});
3169
});

plopfile.cjs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
/**
2+
* Plop generator for creating new MCP tools.
3+
*
4+
* Usage:
5+
* Interactive mode (requires TTY):
6+
* npx plop create-tool
7+
*
8+
* Non-interactive mode (for CI, scripts, or non-TTY environments):
9+
* npx plop create-tool "ToolName" "tool_name_tool"
10+
*
11+
* Example:
12+
* npx plop create-tool "Search" "search_tool"
13+
*
14+
* This generates:
15+
* - src/tools/search-tool/SearchTool.ts
16+
* - src/tools/search-tool/SearchTool.input.schema.ts
17+
* - src/tools/search-tool/SearchTool.output.schema.ts
18+
* - test/tools/search-tool/SearchTool.test.ts
19+
* - Updates src/tools/toolRegistry.ts
20+
* - Updates README.md
21+
*/
122
module.exports = function (plop) {
223
plop.setGenerator('create-tool', {
324
description: 'Generate a TypeScript class and its test',
@@ -6,7 +27,7 @@ module.exports = function (plop) {
627
type: 'input',
728
name: 'name',
829
message: 'Tool class name without suffix using PascalCase e.g. Search:',
9-
},
30+
},
1031
{
1132
type: 'input',
1233
name: 'toolName',
@@ -44,7 +65,7 @@ module.exports = function (plop) {
4465
type: 'append',
4566
path: 'src/tools/toolRegistry.ts',
4667
pattern: /(\/\/ INSERT NEW TOOL INSTANCE HERE)/,
47-
template: ' new {{pascalCase name}}Tool(),',
68+
template: ' new {{pascalCase name}}Tool({ httpRequest }),',
4869
},
4970
{
5071
type: 'append',

0 commit comments

Comments
 (0)