import { z } from 'zod'; import { parseSpec } from '../lib/parser.js'; import { formatEndpoints } from '../utils/format.js'; import type { EndpointInfo, EndpointFilter, ParameterInfo, ResponseInfo } from '../lib/types.js'; import type { OpenAPIV3 } from 'openapi-types'; export const queryToolName = 'query-endpoints'; export const queryToolDescription = 'Search and filter API endpoints in an OpenAPI spec. Filter by method, path pattern, tag, or operation ID.'; export const queryToolSchema = { path: z.string().describe('Path to the OpenAPI/Swagger spec file'), method: z.string().optional().describe('Filter by HTTP method (GET, POST, etc.)'), pathPattern: z.string().optional().describe('Regex pattern to match path'), tag: z.string().optional().describe('Filter by tag name'), operationId: z.string().optional().describe('Filter by exact operation ID'), }; const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] as const; export async function queryToolHandler(args: { path: string; method?: string; pathPattern?: string; tag?: string; operationId?: string; }): Promise<{ content: Array<{ type: 'text'; text: string }>; structuredContent: Record; }> { try { const { spec } = await parseSpec(args.path); const filter: EndpointFilter = { method: args.method?.toLowerCase(), pathPattern: args.pathPattern, tag: args.tag, operationId: args.operationId, }; const endpoints = extractEndpoints(spec, filter); const text = formatEndpoints(endpoints); return { content: [{ type: 'text', text }], structuredContent: { success: true, count: endpoints.length, endpoints, }, }; } catch (err) { const error = err as Error; return { content: [{ type: 'text', text: `Error querying endpoints: ${error.message}` }], structuredContent: { success: false, error: error.message, }, }; } } function extractEndpoints(spec: object, filter: EndpointFilter): EndpointInfo[] { const endpoints: EndpointInfo[] = []; const paths = (spec as { paths?: Record }).paths || {}; for (const [pathName, pathItem] of Object.entries(paths)) { if (!pathItem) continue; for (const method of HTTP_METHODS) { const operation = (pathItem as Record)[method] as OpenAPIV3.OperationObject | undefined; if (!operation) continue; // Apply filters if (filter.method && method !== filter.method) continue; if (filter.pathPattern) { try { const regex = new RegExp(filter.pathPattern); if (!regex.test(pathName)) continue; } catch { // Invalid regex, skip filter } } if (filter.tag && (!operation.tags || !operation.tags.includes(filter.tag))) continue; if (filter.operationId && operation.operationId !== filter.operationId) continue; // Extract endpoint info const endpoint: EndpointInfo = { method, path: pathName, operationId: operation.operationId, summary: operation.summary, description: operation.description, tags: operation.tags || [], parameters: extractParameters(operation, pathItem as OpenAPIV3.PathItemObject), requestBody: extractRequestBody(operation), responses: extractResponses(operation), }; endpoints.push(endpoint); } } return endpoints; } function extractParameters( operation: OpenAPIV3.OperationObject, pathItem: OpenAPIV3.PathItemObject ): ParameterInfo[] { const params: ParameterInfo[] = []; // Combine path-level and operation-level parameters const allParams = [ ...(pathItem.parameters || []), ...(operation.parameters || []), ] as OpenAPIV3.ParameterObject[]; for (const param of allParams) { if ('$ref' in param) continue; // Skip refs (should be dereferenced) params.push({ name: param.name, in: param.in as 'query' | 'header' | 'path' | 'cookie', required: param.required || false, description: param.description, schema: param.schema as object | undefined, }); } return params; } function extractRequestBody(operation: OpenAPIV3.OperationObject): EndpointInfo['requestBody'] { if (!operation.requestBody) return undefined; const body = operation.requestBody as OpenAPIV3.RequestBodyObject; if ('$ref' in body) return undefined; // Skip refs return { required: body.required || false, description: body.description, contentTypes: Object.keys(body.content || {}), }; } function extractResponses(operation: OpenAPIV3.OperationObject): ResponseInfo[] { const responses: ResponseInfo[] = []; if (!operation.responses) return responses; for (const [statusCode, response] of Object.entries(operation.responses)) { const resp = response as OpenAPIV3.ResponseObject; if ('$ref' in resp) continue; // Skip refs responses.push({ statusCode, description: resp.description, contentTypes: Object.keys(resp.content || {}), }); } return responses; }