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>
This commit is contained in:
rimskij 2026-01-12 14:33:10 +01:00
commit cc789d3b32
17 changed files with 3315 additions and 0 deletions

View file

@ -0,0 +1,85 @@
# OpenAPI Spec Tools
Parse, validate, and explore OpenAPI/Swagger specifications.
## Arguments
- `$ARGUMENTS` - Subcommand and arguments
## Subcommands
### parse <file>
Parse an OpenAPI spec and show overview (version, paths, schemas, servers).
### validate <file>
Validate a spec against OpenAPI schema. Reports errors and warnings.
### endpoints <file> [--method METHOD] [--path PATTERN] [--tag TAG]
List API endpoints. Optional filters:
- `--method GET|POST|PUT|DELETE|...` - Filter by HTTP method
- `--path /users.*` - Filter by path regex pattern
- `--tag users` - Filter by tag name
### schema <file> <name>
Get details of a component schema by name.
### types <file> [schema1 schema2 ...]
Generate TypeScript interfaces from schemas. Optionally specify schema names.
## Execution
Based on the subcommand in `$ARGUMENTS`, use the appropriate MCP tool:
### If subcommand is "parse":
Use the `mcp__swagger-tools__parse-spec` tool with the file path.
### If subcommand is "validate":
Use the `mcp__swagger-tools__validate-spec` tool with the file path.
### If subcommand is "endpoints":
Use the `mcp__swagger-tools__query-endpoints` tool with:
- `path`: The spec file path
- `method`: Value after `--method` (if provided)
- `pathPattern`: Value after `--path` (if provided)
- `tag`: Value after `--tag` (if provided)
### If subcommand is "schema":
Use the `mcp__swagger-tools__get-schema` tool with:
- `path`: The spec file path
- `schemaName`: The schema name argument
### If subcommand is "types":
Use the `mcp__swagger-tools__generate-types` tool with:
- `path`: The spec file path
- `schemas`: Array of schema names (if any specified after the file)
## Examples
```bash
# Parse and show overview
/openapi parse api.yaml
# Validate spec
/openapi validate openapi.json
# List all GET endpoints
/openapi endpoints api.yaml --method GET
# List endpoints matching /users path
/openapi endpoints api.yaml --path /users
# Get User schema details
/openapi schema api.yaml User
# Generate all TypeScript types
/openapi types api.yaml
# Generate specific schemas only
/openapi types api.yaml User Pet Order
```
## Notes
- Supports OpenAPI 3.0, 3.1, and Swagger 2.0 specifications
- Files can be YAML or JSON format
- References ($ref) are automatically dereferenced

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.env
.DS_Store

96
CLAUDE.md Normal file
View file

@ -0,0 +1,96 @@
# swagger-tools
MCP server for parsing, validating, and querying OpenAPI/Swagger specifications.
## Quick Start
```bash
# Install dependencies
npm install
# Build
npm run build
# Run (for testing)
npm start
```
## MCP Server Configuration
Add to `~/.claude.json` (or project `.mcp.json`):
```json
{
"mcpServers": {
"swagger-tools": {
"command": "node",
"args": ["/absolute/path/to/swagger-tools/dist/index.js"]
}
}
}
```
After configuring, restart Claude Code. The tools will be available as:
- `mcp__swagger-tools__parse-spec`
- `mcp__swagger-tools__validate-spec`
- `mcp__swagger-tools__query-endpoints`
- `mcp__swagger-tools__get-schema`
- `mcp__swagger-tools__generate-types`
## Claude Code Slash Command
Use `/openapi` for convenient access:
```bash
/openapi parse api.yaml # Parse and show overview
/openapi validate api.yaml # Validate spec
/openapi endpoints api.yaml # List all endpoints
/openapi schema api.yaml User # Get schema details
/openapi types api.yaml # Generate TypeScript types
```
## Project Structure
```
src/
├── index.ts # MCP server entry point (stdio)
├── tools/ # MCP tool implementations
│ ├── parse.ts # parse-spec tool
│ ├── validate.ts # validate-spec tool
│ ├── query.ts # query-endpoints tool
│ ├── schema.ts # get-schema tool
│ └── generate.ts # generate-types tool
├── lib/ # Core library
│ ├── parser.ts # OpenAPI parsing wrapper
│ ├── validator.ts # Validation logic
│ └── types.ts # TypeScript types
└── utils/
└── format.ts # Output formatting
.claude/
└── commands/
└── openapi.md # Slash command definition
```
## Development
```bash
# Type check without building
npm run typecheck
# Run with tsx (development)
npm run dev
```
## Supported Formats
- OpenAPI 3.0.x
- OpenAPI 3.1.x
- Swagger 2.0
- YAML and JSON files
## Dependencies
- `@modelcontextprotocol/sdk` - MCP server framework
- `@apidevtools/swagger-parser` - OpenAPI parsing and validation
- `zod` - Schema validation for tool inputs

1816
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "swagger-tools",
"version": "0.1.0",
"description": "MCP server for parsing, validating, and querying OpenAPI/Swagger specifications",
"type": "module",
"main": "dist/index.js",
"bin": {
"swagger-tools": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"keywords": [
"openapi",
"swagger",
"mcp",
"claude",
"api"
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}

91
src/index.ts Normal file
View file

@ -0,0 +1,91 @@
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
parseToolName,
parseToolDescription,
parseToolSchema,
parseToolHandler,
} from './tools/parse.js';
import {
validateToolName,
validateToolDescription,
validateToolSchema,
validateToolHandler,
} from './tools/validate.js';
import {
queryToolName,
queryToolDescription,
queryToolSchema,
queryToolHandler,
} from './tools/query.js';
import {
schemaToolName,
schemaToolDescription,
schemaToolSchema,
schemaToolHandler,
} from './tools/schema.js';
import {
generateToolName,
generateToolDescription,
generateToolSchema,
generateToolHandler,
} from './tools/generate.js';
const server = new McpServer({
name: 'swagger-tools',
version: '0.1.0',
});
// Register parse-spec tool
server.tool(
parseToolName,
parseToolDescription,
parseToolSchema,
parseToolHandler
);
// Register validate-spec tool
server.tool(
validateToolName,
validateToolDescription,
validateToolSchema,
validateToolHandler
);
// Register query-endpoints tool
server.tool(
queryToolName,
queryToolDescription,
queryToolSchema,
queryToolHandler
);
// Register get-schema tool
server.tool(
schemaToolName,
schemaToolDescription,
schemaToolSchema,
schemaToolHandler
);
// Register generate-types tool
server.tool(
generateToolName,
generateToolDescription,
generateToolSchema,
generateToolHandler
);
// Start the server with stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);

90
src/lib/parser.ts Normal file
View file

@ -0,0 +1,90 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
import type { ParsedSpec, OpenAPISpec } from './types.js';
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] as const;
export async function parseSpec(specPath: string): Promise<{
spec: OpenAPISpec;
metadata: ParsedSpec;
}> {
const spec = await SwaggerParser.dereference(specPath);
const metadata = extractMetadata(spec);
return { spec, metadata };
}
export async function bundleSpec(specPath: string): Promise<OpenAPISpec> {
return SwaggerParser.bundle(specPath);
}
function extractMetadata(spec: OpenAPISpec): ParsedSpec {
const version = getSpecVersion(spec);
const info = spec.info;
const paths = spec.paths || {};
const pathCount = Object.keys(paths).length;
let operationCount = 0;
const tagsSet = new Set<string>();
for (const pathItem of Object.values(paths)) {
if (!pathItem) continue;
for (const method of HTTP_METHODS) {
const operation = (pathItem as Record<string, unknown>)[method] as OpenAPIV3.OperationObject | undefined;
if (operation) {
operationCount++;
if (operation.tags) {
operation.tags.forEach(tag => tagsSet.add(tag));
}
}
}
}
const schemaCount = getSchemaCount(spec);
const servers = getServers(spec);
return {
version,
title: info.title,
description: info.description,
servers,
pathCount,
schemaCount,
operationCount,
tags: Array.from(tagsSet).sort(),
};
}
function getSpecVersion(spec: OpenAPISpec): string {
if ('openapi' in spec) {
return `OpenAPI ${spec.openapi}`;
}
if ('swagger' in spec) {
return `Swagger ${spec.swagger}`;
}
return 'Unknown';
}
function getSchemaCount(spec: OpenAPISpec): number {
if ('components' in spec && spec.components?.schemas) {
return Object.keys(spec.components.schemas).length;
}
if ('definitions' in spec && spec.definitions) {
return Object.keys(spec.definitions).length;
}
return 0;
}
function getServers(spec: OpenAPISpec): string[] {
if ('servers' in spec && spec.servers) {
return spec.servers.map(s => s.url);
}
if ('host' in spec) {
const swagger2 = spec as { host?: string; basePath?: string; schemes?: string[] };
const scheme = swagger2.schemes?.[0] || 'https';
const host = swagger2.host || 'localhost';
const basePath = swagger2.basePath || '';
return [`${scheme}://${host}${basePath}`];
}
return [];
}

75
src/lib/types.ts Normal file
View file

@ -0,0 +1,75 @@
import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
export type OpenAPISpec = OpenAPI.Document;
export type OpenAPIV3Spec = OpenAPIV3.Document | OpenAPIV3_1.Document;
export interface ParsedSpec {
version: string;
title: string;
description?: string;
servers: string[];
pathCount: number;
schemaCount: number;
operationCount: number;
tags: string[];
}
export interface ValidationError {
path: string;
message: string;
severity: 'error' | 'warning';
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
warnings: ValidationError[];
}
export interface EndpointInfo {
method: string;
path: string;
operationId?: string;
summary?: string;
description?: string;
tags: string[];
parameters: ParameterInfo[];
requestBody?: RequestBodyInfo;
responses: ResponseInfo[];
}
export interface ParameterInfo {
name: string;
in: 'query' | 'header' | 'path' | 'cookie';
required: boolean;
description?: string;
schema?: object;
}
export interface RequestBodyInfo {
required: boolean;
description?: string;
contentTypes: string[];
}
export interface ResponseInfo {
statusCode: string;
description?: string;
contentTypes: string[];
}
export interface EndpointFilter {
method?: string;
pathPattern?: string;
tag?: string;
operationId?: string;
}
export interface SchemaInfo {
name: string;
type?: string;
description?: string;
properties?: Record<string, object>;
required?: string[];
schema: object;
}

89
src/lib/validator.ts Normal file
View file

@ -0,0 +1,89 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import type { ValidationResult, ValidationError } from './types.js';
export async function validateSpec(specPath: string): Promise<ValidationResult> {
const errors: ValidationError[] = [];
const warnings: ValidationError[] = [];
try {
// Validate will throw on errors, but also returns the parsed spec
await SwaggerParser.validate(specPath);
return {
valid: true,
errors: [],
warnings: [],
};
} catch (err) {
const error = err as Error & { details?: Array<{ path: string[]; message: string }> };
// swagger-parser throws errors with details array for validation issues
if (error.details && Array.isArray(error.details)) {
for (const detail of error.details) {
errors.push({
path: detail.path?.join('.') || '',
message: detail.message,
severity: 'error',
});
}
} else {
// Single error without details
errors.push({
path: '',
message: error.message,
severity: 'error',
});
}
return {
valid: false,
errors,
warnings,
};
}
}
export async function validateWithWarnings(specPath: string): Promise<ValidationResult> {
const result = await validateSpec(specPath);
// Add additional checks that produce warnings
try {
const spec = await SwaggerParser.dereference(specPath);
// Check for operations without operationId
if (spec.paths) {
for (const [path, pathItem] of Object.entries(spec.paths)) {
if (!pathItem) continue;
for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']) {
const operation = (pathItem as Record<string, unknown>)[method] as { operationId?: string } | undefined;
if (operation && !operation.operationId) {
result.warnings.push({
path: `paths.${path}.${method}`,
message: 'Operation is missing operationId',
severity: 'warning',
});
}
}
}
}
// Check for missing descriptions on schemas
const schemas = ('components' in spec && spec.components?.schemas) ||
('definitions' in spec && (spec as { definitions?: object }).definitions) ||
{};
for (const [name, schema] of Object.entries(schemas)) {
if (schema && typeof schema === 'object' && !('description' in schema)) {
result.warnings.push({
path: `components.schemas.${name}`,
message: 'Schema is missing description',
severity: 'warning',
});
}
}
} catch {
// If we can't parse for warnings, just return validation result
}
return result;
}

241
src/tools/generate.ts Normal file
View file

@ -0,0 +1,241 @@
import { z } from 'zod';
import { parseSpec } from '../lib/parser.js';
import { formatTypes } from '../utils/format.js';
export const generateToolName = 'generate-types';
export const generateToolDescription = 'Generate TypeScript interfaces from OpenAPI component schemas. Optionally specify which schemas to generate.';
export const generateToolSchema = {
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
schemas: z.array(z.string()).optional().describe('Specific schema names to generate (all if omitted)'),
};
export async function generateToolHandler({ path, schemas }: {
path: string;
schemas?: string[];
}): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent: Record<string, unknown>;
}> {
try {
const { spec } = await parseSpec(path);
const allSchemas = getSchemas(spec);
const schemasToGenerate = schemas
? Object.fromEntries(
Object.entries(allSchemas).filter(([name]) => schemas.includes(name))
)
: allSchemas;
if (Object.keys(schemasToGenerate).length === 0) {
return {
content: [{ type: 'text', text: 'No schemas found to generate.' }],
structuredContent: {
success: false,
error: 'No schemas found',
availableSchemas: Object.keys(allSchemas),
},
};
}
const types = generateTypeScript(schemasToGenerate);
const text = formatTypes(types);
return {
content: [{ type: 'text', text }],
structuredContent: {
success: true,
types,
generatedCount: Object.keys(schemasToGenerate).length,
},
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: 'text', text: `Error generating types: ${error.message}` }],
structuredContent: {
success: false,
error: error.message,
},
};
}
}
function getSchemas(spec: object): Record<string, object> {
// OpenAPI 3.x
const spec3 = spec as { components?: { schemas?: Record<string, object> } };
if (spec3.components?.schemas) {
return spec3.components.schemas;
}
// Swagger 2.0
const spec2 = spec as { definitions?: Record<string, object> };
if (spec2.definitions) {
return spec2.definitions;
}
return {};
}
function generateTypeScript(schemas: Record<string, object>): string {
const lines: string[] = [];
for (const [name, schema] of Object.entries(schemas)) {
const typeDef = schemaToTypeScript(name, schema);
lines.push(typeDef);
lines.push('');
}
return lines.join('\n');
}
function schemaToTypeScript(name: string, schema: object): string {
const s = schema as {
type?: string;
description?: string;
properties?: Record<string, object>;
required?: string[];
items?: object;
enum?: (string | number)[];
allOf?: object[];
oneOf?: object[];
anyOf?: object[];
$ref?: string;
};
const lines: string[] = [];
// Add JSDoc comment if description exists
if (s.description) {
lines.push(`/** ${s.description} */`);
}
// Handle enums
if (s.enum) {
const values = s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
lines.push(`export type ${name} = ${values};`);
return lines.join('\n');
}
// Handle object types
if (s.type === 'object' || s.properties) {
lines.push(`export interface ${name} {`);
if (s.properties) {
const required = new Set(s.required || []);
for (const [propName, propSchema] of Object.entries(s.properties)) {
const optional = required.has(propName) ? '' : '?';
const propType = schemaToType(propSchema);
const propDesc = (propSchema as { description?: string }).description;
if (propDesc) {
lines.push(` /** ${propDesc} */`);
}
lines.push(` ${propName}${optional}: ${propType};`);
}
}
lines.push('}');
return lines.join('\n');
}
// Handle array types
if (s.type === 'array' && s.items) {
const itemType = schemaToType(s.items);
lines.push(`export type ${name} = ${itemType}[];`);
return lines.join('\n');
}
// Handle simple types
const simpleType = schemaToType(schema);
lines.push(`export type ${name} = ${simpleType};`);
return lines.join('\n');
}
function schemaToType(schema: object): string {
const s = schema as {
type?: string;
format?: string;
$ref?: string;
items?: object;
enum?: (string | number)[];
properties?: Record<string, object>;
required?: string[];
allOf?: object[];
oneOf?: object[];
anyOf?: object[];
additionalProperties?: boolean | object;
};
// Handle $ref (after dereferencing, these should be resolved, but handle just in case)
if (s.$ref) {
const refName = s.$ref.split('/').pop() || 'unknown';
return refName;
}
// Handle enums
if (s.enum) {
return s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
}
// Handle allOf (intersection)
if (s.allOf) {
return s.allOf.map(schemaToType).join(' & ');
}
// Handle oneOf/anyOf (union)
if (s.oneOf || s.anyOf) {
const schemas = s.oneOf || s.anyOf || [];
return schemas.map(schemaToType).join(' | ');
}
// Handle arrays
if (s.type === 'array') {
const itemType = s.items ? schemaToType(s.items) : 'unknown';
return `${itemType}[]`;
}
// Handle objects with additionalProperties
if (s.type === 'object' && s.additionalProperties) {
const valueType = typeof s.additionalProperties === 'object'
? schemaToType(s.additionalProperties)
: 'unknown';
return `Record<string, ${valueType}>`;
}
// Handle inline objects
if (s.type === 'object' && s.properties) {
const required = new Set(s.required || []);
const props = Object.entries(s.properties)
.map(([name, prop]) => {
const optional = required.has(name) ? '' : '?';
return `${name}${optional}: ${schemaToType(prop)}`;
})
.join('; ');
return `{ ${props} }`;
}
// Handle primitive types
switch (s.type) {
case 'string':
if (s.format === 'date' || s.format === 'date-time') {
return 'string'; // Could be Date, but string is safer
}
return 'string';
case 'integer':
case 'number':
return 'number';
case 'boolean':
return 'boolean';
case 'null':
return 'null';
case 'object':
return 'Record<string, unknown>';
default:
return 'unknown';
}
}

40
src/tools/parse.ts Normal file
View file

@ -0,0 +1,40 @@
import { z } from 'zod';
import { parseSpec } from '../lib/parser.js';
import { formatMetadata } from '../utils/format.js';
export const parseToolName = 'parse-spec';
export const parseToolDescription = 'Parse and analyze an OpenAPI/Swagger specification file. Returns metadata including version, title, server count, path count, and schema count.';
export const parseToolSchema = {
path: z.string().describe('Path to the OpenAPI/Swagger spec file (YAML or JSON)'),
};
export async function parseToolHandler({ path }: { path: string }): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent: Record<string, unknown>;
}> {
try {
const { spec, metadata } = await parseSpec(path);
const text = formatMetadata(metadata);
return {
content: [{ type: 'text', text }],
structuredContent: {
success: true,
metadata,
spec,
},
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: 'text', text: `Error parsing spec: ${error.message}` }],
structuredContent: {
success: false,
error: error.message,
},
};
}
}

168
src/tools/query.ts Normal file
View file

@ -0,0 +1,168 @@
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;
}

99
src/tools/schema.ts Normal file
View file

@ -0,0 +1,99 @@
import { z } from 'zod';
import { parseSpec } from '../lib/parser.js';
import { formatSchema } from '../utils/format.js';
import type { SchemaInfo } from '../lib/types.js';
export const schemaToolName = 'get-schema';
export const schemaToolDescription = 'Get details of a component schema from an OpenAPI spec. Returns the full schema definition with resolved references.';
export const schemaToolSchema = {
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
schemaName: z.string().describe('Name of the schema in components/schemas (or definitions for Swagger 2.0)'),
};
export async function schemaToolHandler({ path, schemaName }: {
path: string;
schemaName: string;
}): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent: Record<string, unknown>;
}> {
try {
const { spec } = await parseSpec(path);
const schema = findSchema(spec, schemaName);
if (!schema) {
const available = getAvailableSchemas(spec);
return {
content: [{ type: 'text', text: `Schema '${schemaName}' not found. Available schemas: ${available.join(', ')}` }],
structuredContent: {
success: false,
error: `Schema '${schemaName}' not found`,
availableSchemas: available,
},
};
}
const schemaInfo: SchemaInfo = {
name: schemaName,
type: (schema as { type?: string }).type,
description: (schema as { description?: string }).description,
properties: (schema as { properties?: Record<string, object> }).properties,
required: (schema as { required?: string[] }).required,
schema,
};
const text = formatSchema(schemaInfo);
return {
content: [{ type: 'text', text }],
structuredContent: {
success: true,
schema: schemaInfo,
},
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: 'text', text: `Error getting schema: ${error.message}` }],
structuredContent: {
success: false,
error: error.message,
},
};
}
}
function findSchema(spec: object, schemaName: string): object | null {
// OpenAPI 3.x: components.schemas
const spec3 = spec as { components?: { schemas?: Record<string, object> } };
if (spec3.components?.schemas?.[schemaName]) {
return spec3.components.schemas[schemaName];
}
// Swagger 2.0: definitions
const spec2 = spec as { definitions?: Record<string, object> };
if (spec2.definitions?.[schemaName]) {
return spec2.definitions[schemaName];
}
return null;
}
function getAvailableSchemas(spec: object): string[] {
// OpenAPI 3.x
const spec3 = spec as { components?: { schemas?: Record<string, object> } };
if (spec3.components?.schemas) {
return Object.keys(spec3.components.schemas);
}
// Swagger 2.0
const spec2 = spec as { definitions?: Record<string, object> };
if (spec2.definitions) {
return Object.keys(spec2.definitions);
}
return [];
}

38
src/tools/validate.ts Normal file
View file

@ -0,0 +1,38 @@
import { z } from 'zod';
import { validateWithWarnings } from '../lib/validator.js';
import { formatValidation } from '../utils/format.js';
export const validateToolName = 'validate-spec';
export const validateToolDescription = 'Validate an OpenAPI/Swagger specification against the schema. Reports errors and warnings.';
export const validateToolSchema = {
path: z.string().describe('Path to the OpenAPI/Swagger spec file (YAML or JSON)'),
};
export async function validateToolHandler({ path }: { path: string }): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent: Record<string, unknown>;
}> {
try {
const result = await validateWithWarnings(path);
const text = formatValidation(result);
return {
content: [{ type: 'text', text }],
structuredContent: {
success: true,
...result,
},
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: 'text', text: `Error validating spec: ${error.message}` }],
structuredContent: {
success: false,
error: error.message,
},
};
}
}

146
src/utils/format.ts Normal file
View file

@ -0,0 +1,146 @@
import type { EndpointInfo, ParsedSpec, ValidationResult, SchemaInfo } from '../lib/types.js';
export function formatMetadata(metadata: ParsedSpec): string {
const lines = [
`# ${metadata.title}`,
`Version: ${metadata.version}`,
];
if (metadata.description) {
lines.push(`Description: ${metadata.description}`);
}
lines.push('');
lines.push(`## Statistics`);
lines.push(`- Paths: ${metadata.pathCount}`);
lines.push(`- Operations: ${metadata.operationCount}`);
lines.push(`- Schemas: ${metadata.schemaCount}`);
if (metadata.servers.length > 0) {
lines.push('');
lines.push(`## Servers`);
metadata.servers.forEach(s => lines.push(`- ${s}`));
}
if (metadata.tags.length > 0) {
lines.push('');
lines.push(`## Tags`);
metadata.tags.forEach(t => lines.push(`- ${t}`));
}
return lines.join('\n');
}
export function formatValidation(result: ValidationResult): string {
const lines: string[] = [];
if (result.valid) {
lines.push('Validation: PASSED');
} else {
lines.push('Validation: FAILED');
}
if (result.errors.length > 0) {
lines.push('');
lines.push('## Errors');
for (const err of result.errors) {
const path = err.path ? `[${err.path}] ` : '';
lines.push(`- ${path}${err.message}`);
}
}
if (result.warnings.length > 0) {
lines.push('');
lines.push('## Warnings');
for (const warn of result.warnings) {
const path = warn.path ? `[${warn.path}] ` : '';
lines.push(`- ${path}${warn.message}`);
}
}
return lines.join('\n');
}
export function formatEndpoints(endpoints: EndpointInfo[]): string {
if (endpoints.length === 0) {
return 'No endpoints found matching criteria.';
}
const lines = [`Found ${endpoints.length} endpoint(s):`, ''];
for (const ep of endpoints) {
lines.push(`## ${ep.method.toUpperCase()} ${ep.path}`);
if (ep.operationId) {
lines.push(`Operation ID: ${ep.operationId}`);
}
if (ep.summary) {
lines.push(`Summary: ${ep.summary}`);
}
if (ep.tags.length > 0) {
lines.push(`Tags: ${ep.tags.join(', ')}`);
}
if (ep.parameters.length > 0) {
lines.push('');
lines.push('### Parameters');
for (const param of ep.parameters) {
const required = param.required ? ' (required)' : '';
lines.push(`- **${param.name}** [${param.in}]${required}`);
if (param.description) {
lines.push(` ${param.description}`);
}
}
}
if (ep.requestBody) {
lines.push('');
lines.push('### Request Body');
const required = ep.requestBody.required ? ' (required)' : '';
lines.push(`Content types${required}: ${ep.requestBody.contentTypes.join(', ')}`);
if (ep.requestBody.description) {
lines.push(ep.requestBody.description);
}
}
if (ep.responses.length > 0) {
lines.push('');
lines.push('### Responses');
for (const resp of ep.responses) {
lines.push(`- **${resp.statusCode}**: ${resp.description || 'No description'}`);
}
}
lines.push('');
}
return lines.join('\n');
}
export function formatSchema(schema: SchemaInfo): string {
const lines = [
`# ${schema.name}`,
];
if (schema.description) {
lines.push(schema.description);
}
if (schema.type) {
lines.push(`Type: ${schema.type}`);
}
lines.push('');
lines.push('## Schema');
lines.push('```json');
lines.push(JSON.stringify(schema.schema, null, 2));
lines.push('```');
return lines.join('\n');
}
export function formatTypes(types: string): string {
return ['## Generated TypeScript Types', '', '```typescript', types, '```'].join('\n');
}

181
test/fixtures/petstore.yaml vendored Normal file
View file

@ -0,0 +1,181 @@
openapi: 3.0.3
info:
title: Pet Store API
description: A sample Pet Store API for testing swagger-tools
version: 1.0.0
servers:
- url: https://api.petstore.example.com/v1
description: Production server
- url: https://staging.petstore.example.com/v1
description: Staging server
tags:
- name: pets
description: Pet operations
- name: store
description: Store operations
paths:
/pets:
get:
operationId: listPets
summary: List all pets
tags:
- pets
parameters:
- name: limit
in: query
description: Maximum number of pets to return
required: false
schema:
type: integer
maximum: 100
- name: status
in: query
description: Filter by status
schema:
$ref: '#/components/schemas/PetStatus'
responses:
'200':
description: A list of pets
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Bad request
post:
operationId: createPet
summary: Create a new pet
tags:
- pets
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
responses:
'201':
description: Pet created
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid input
/pets/{petId}:
get:
operationId: getPet
summary: Get a pet by ID
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The pet ID
schema:
type: integer
format: int64
responses:
'200':
description: Pet details
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'404':
description: Pet not found
delete:
operationId: deletePet
summary: Delete a pet
tags:
- pets
parameters:
- name: petId
in: path
required: true
schema:
type: integer
format: int64
responses:
'204':
description: Pet deleted
'404':
description: Pet not found
/store/inventory:
get:
operationId: getInventory
summary: Get store inventory
tags:
- store
responses:
'200':
description: Inventory counts by status
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
components:
schemas:
Pet:
type: object
description: A pet in the store
required:
- id
- name
properties:
id:
type: integer
format: int64
description: Unique identifier
name:
type: string
description: Pet name
status:
$ref: '#/components/schemas/PetStatus'
tags:
type: array
items:
$ref: '#/components/schemas/Tag'
NewPet:
type: object
description: Data for creating a new pet
required:
- name
properties:
name:
type: string
description: Pet name
status:
$ref: '#/components/schemas/PetStatus'
PetStatus:
type: string
description: Pet availability status
enum:
- available
- pending
- sold
Tag:
type: object
description: A tag for categorizing pets
properties:
id:
type: integer
format: int64
name:
type: string
Error:
type: object
description: Error response
required:
- code
- message
properties:
code:
type: integer
message:
type: string

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}