- Parse and validate OpenAPI 3.x / Swagger 2.0 specs - Query endpoints by method, path pattern, tag, operationId - Get component schema details - Generate TypeScript interfaces from schemas - Support local files and remote URLs Tools: parse-spec, validate-spec, query-endpoints, get-schema, generate-types Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
5 KiB
TypeScript
168 lines
5 KiB
TypeScript
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<string, unknown>;
|
|
}> {
|
|
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<string, object> }).paths || {};
|
|
|
|
for (const [pathName, pathItem] of Object.entries(paths)) {
|
|
if (!pathItem) continue;
|
|
|
|
for (const method of HTTP_METHODS) {
|
|
const operation = (pathItem as Record<string, unknown>)[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;
|
|
}
|