swagger-tools/src/tools/query.ts
rimskij cc789d3b32 feat: initial MCP server for OpenAPI/Swagger parsing
- 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>
2026-01-12 14:33:10 +01:00

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;
}