diff --git a/.changeset/codegen-operationid-fallback.md b/.changeset/codegen-operationid-fallback.md new file mode 100644 index 0000000..b7f39ed --- /dev/null +++ b/.changeset/codegen-operationid-fallback.md @@ -0,0 +1,8 @@ +--- +'@taskade/mcp-openapi-codegen': patch +--- + +Derive a camelCase tool name from an operation's path when the OpenAPI spec omits +`operationId` (and fall back to `summary` for the description). Enables generating +tools from specs like Taskade API v2's flat RPC routes (`POST /promptAgent`). Specs +that provide `operationId` (e.g. Taskade v1) are unaffected. diff --git a/packages/openapi-codegen/src/parser.test.ts b/packages/openapi-codegen/src/parser.test.ts new file mode 100644 index 0000000..6947930 --- /dev/null +++ b/packages/openapi-codegen/src/parser.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; + +import { deriveToolName, parseOpenApi } from './parser'; + +describe('deriveToolName', () => { + it('derives a camelCase name from a flat RPC path (API v2)', () => { + expect(deriveToolName('post', '/promptAgent')).toBe('promptAgent'); + expect(deriveToolName('post', '/subscribeWebhook')).toBe('subscribeWebhook'); + expect(deriveToolName('post', '/listConversations')).toBe('listConversations'); + }); + + it('drops path params and camelCases remaining segments', () => { + expect(deriveToolName('get', '/media/{mediaId}/content')).toBe('mediaContent'); + expect(deriveToolName('get', '/bundles/{spaceId}/export/zip')).toBe('bundlesExportZip'); + }); + + it('falls back to the HTTP method for a root path', () => { + expect(deriveToolName('get', '/')).toBe('get'); + }); +}); + +describe('parseOpenApi name resolution', () => { + it('prefers operationId when present (API v1, unchanged behavior)', () => { + const tools = parseOpenApi({ + '/projects': { + post: { operationId: 'projectCreate', description: 'Create a project', responses: {} }, + }, + } as never); + expect(tools).toHaveLength(1); + expect(tools[0].name).toBe('projectCreate'); + expect(tools[0].description).toBe('Create a project'); + }); + + it('derives the name from the path and uses summary as description when operationId is absent (API v2)', () => { + const tools = parseOpenApi({ + '/promptAgent': { post: { summary: 'Prompt an agent', responses: {} } }, + } as never); + expect(tools).toHaveLength(1); + expect(tools[0].name).toBe('promptAgent'); + expect(tools[0].description).toBe('Prompt an agent'); + }); +}); diff --git a/packages/openapi-codegen/src/parser.ts b/packages/openapi-codegen/src/parser.ts index 6f4bd47..cf06211 100644 --- a/packages/openapi-codegen/src/parser.ts +++ b/packages/openapi-codegen/src/parser.ts @@ -21,6 +21,34 @@ export type ParsedTool = { outputSchema: IJsonSchema; }; +/** + * Derive a stable camelCase tool name from an operation's path when the spec omits + * `operationId` — e.g. Taskade API v2's flat RPC routes (`POST /promptAgent`). Path + * params (`{id}`) are dropped and remaining segments are camelCased + * (`/media/{mediaId}/content` → `mediaContent`). Falls back to the HTTP method for a + * root path. Specs that DO provide `operationId` (e.g. Taskade v1) are unaffected. + */ +export const deriveToolName = (method: string, path: string): string => { + const words = path + .split('/') + .filter((segment) => segment && !segment.startsWith('{')) + .join('-') + .split(/[-_]/) + .filter(Boolean); + + if (words.length === 0) { + return method.toLowerCase(); + } + + return words + .map((word, index) => + index === 0 + ? word.charAt(0).toLowerCase() + word.slice(1) + : word.charAt(0).toUpperCase() + word.slice(1), + ) + .join(''); +}; + export const parseOpenApi = ( paths: OpenAPIV3_1.PathsObject | OpenAPIV3.PathsObject | OpenAPIV2.PathsObject, ): ParsedTool[] => { @@ -129,10 +157,10 @@ export const parseOpenApi = ( } tools.push({ - name: operation.operationId!, + name: operation.operationId ?? deriveToolName(method, path), method: method, path: path, - description: operation.description!, + description: operation.description ?? operation.summary ?? '', inputSchema, queryParamsSchema, pathParamsSchema,