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:
commit
cc789d3b32
17 changed files with 3315 additions and 0 deletions
85
.claude/commands/openapi.md
Normal file
85
.claude/commands/openapi.md
Normal 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
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
96
CLAUDE.md
Normal file
96
CLAUDE.md
Normal 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
1816
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
package.json
Normal file
37
package.json
Normal 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
91
src/index.ts
Normal 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
90
src/lib/parser.ts
Normal 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
75
src/lib/types.ts
Normal 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
89
src/lib/validator.ts
Normal 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
241
src/tools/generate.ts
Normal 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
40
src/tools/parse.ts
Normal 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
168
src/tools/query.ts
Normal 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
99
src/tools/schema.ts
Normal 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
38
src/tools/validate.ts
Normal 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
146
src/utils/format.ts
Normal 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
181
test/fixtures/petstore.yaml
vendored
Normal 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
18
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue