swagger-tools/docs/analysis/feature-enhancements-analysis.md
rimskij a4fc2df4ea feat: add TypeScript generation options to generate-types tool
Add configurable options for customizing TypeScript output:
- enumAsUnion/enumAsEnum: control enum generation style
- interfacePrefix/interfaceSuffix: naming conventions for interfaces
- indentation: 2 spaces, 4 spaces, or tab

Includes validation for mutually exclusive options and valid
TypeScript identifier prefixes/suffixes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 15:42:39 +01:00

14 KiB

Analysis: Feature Enhancements (Caching, OpenAPI 3.1, Batch, TypeScript Options)

Scope Affected

  • Backend
  • Frontend
  • Database
  • Infrastructure

Summary

Analysis of four proposed feature enhancements for the swagger-tools MCP server: remote spec caching, OpenAPI 3.1 support improvements, batch operations, and TypeScript generation options. The current codebase is well-structured with clear separation of concerns, making these enhancements straightforward to implement.

Files Involved

File Purpose
/Users/rimskij/projects/swagger-tools/src/index.ts MCP server entry point - tool registration
/Users/rimskij/projects/swagger-tools/src/lib/parser.ts Spec parsing with dereference fallback
/Users/rimskij/projects/swagger-tools/src/lib/validator.ts Spec validation wrapper
/Users/rimskij/projects/swagger-tools/src/lib/types.ts TypeScript interfaces for parsed data
/Users/rimskij/projects/swagger-tools/src/tools/parse.ts parse-spec tool handler
/Users/rimskij/projects/swagger-tools/src/tools/validate.ts validate-spec tool handler
/Users/rimskij/projects/swagger-tools/src/tools/query.ts query-endpoints tool handler
/Users/rimskij/projects/swagger-tools/src/tools/schema.ts get-schema tool handler
/Users/rimskij/projects/swagger-tools/src/tools/generate.ts generate-types tool with sanitizeName()
/Users/rimskij/projects/swagger-tools/src/utils/format.ts Human-readable output formatters

Current Architecture

src/
├── index.ts              # Entry: registers 5 tools, starts stdio server
├── lib/
│   ├── parser.ts         # parseSpec() - dereference with fallback
│   ├── validator.ts      # validateSpec(), validateWithWarnings()
│   └── types.ts          # TypeScript interfaces
├── tools/
│   ├── parse.ts          # Calls parseSpec(), returns metadata
│   ├── validate.ts       # Calls validateWithWarnings()
│   ├── query.ts          # Filters endpoints from parsed spec
│   ├── schema.ts         # Looks up schema by name
│   └── generate.ts       # Converts schemas to TypeScript
└── utils/
    └── format.ts         # Markdown formatters

Execution Flow

  1. Entry: MCP client sends JSON-RPC request via stdio
  2. Routing: McpServer routes to registered tool handler
  3. Parsing: Tool handler calls parseSpec() with spec path
  4. Processing: Tool-specific logic (filter, validate, generate)
  5. Response: Returns {content: [...], structuredContent: {...}}

Key Dependencies

  • @apidevtools/swagger-parser v10.1.1 - Supports OpenAPI 3.1 (with workarounds)
  • @modelcontextprotocol/sdk v1.25.2 - MCP protocol
  • openapi-types v12.1.3 - TypeScript types (OpenAPIV3_1 namespace)
  • zod v3.23.0 - Schema validation for tool parameters

Feature 1: Caching for Remote Specs

Current Behavior

Every tool call to parseSpec() fetches and parses the spec fresh:

// src/lib/parser.ts:7-11
export async function parseSpec(specPath: string, options?: { dereference?: boolean }): Promise<{...}>
  // Always fetches/reads from scratch
  spec = await SwaggerParser.dereference(specPath);

Proposed Solution

Add an in-memory LRU cache in src/lib/parser.ts:

// New file: src/lib/cache.ts
interface CacheEntry {
  spec: OpenAPISpec;
  metadata: ParsedSpec;
  dereferenced: boolean;
  timestamp: number;
  etag?: string;
}

class SpecCache {
  private cache: Map<string, CacheEntry>;
  private maxSize: number;
  private ttlMs: number;

  constructor(maxSize = 10, ttlMinutes = 15) {...}
  get(key: string): CacheEntry | undefined {...}
  set(key: string, entry: CacheEntry): void {...}
  invalidate(key?: string): void {...}
}

Files to Modify

File Change
src/lib/cache.ts NEW: LRU cache with TTL
src/lib/parser.ts Import cache, wrap parseSpec
src/tools/parse.ts Add optional noCache parameter
src/tools/validate.ts Add optional noCache parameter
src/tools/query.ts Add optional noCache parameter
src/tools/schema.ts Add optional noCache parameter
src/tools/generate.ts Add optional noCache parameter

Cache Key Strategy

// For remote URLs: normalize URL (remove trailing slash, sort query params)
// For local files: use absolute path + file mtime
function getCacheKey(specPath: string): string {
  if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
    return normalizeUrl(specPath);
  }
  const resolved = path.resolve(specPath);
  const stat = fs.statSync(resolved);
  return `${resolved}:${stat.mtimeMs}`;
}

Risks

Risk Impact Mitigation
Stale cache for rapidly changing specs MEDIUM Add noCache param, short TTL (15 min)
Memory pressure with large specs MEDIUM LRU eviction, max 10 entries
Local file changes not detected LOW Include mtime in cache key

Feature 2: OpenAPI 3.1 Support

Current State

Good news: The underlying @apidevtools/swagger-parser v10.1.1 already supports OpenAPI 3.1:

// node_modules/@apidevtools/swagger-parser/lib/index.js
const supported31Versions = ["3.1.0", "3.1.1"];

Gap: The TypeScript generation in generate.ts doesn't handle 3.1-specific JSON Schema features.

OpenAPI 3.1 Differences from 3.0

Feature 3.0 3.1 Code Impact
Schema dialect OpenAPI Schema JSON Schema 2020-12 Type generation
type field Single string Array allowed schemaToType()
nullable nullable: true type: ['string', 'null'] schemaToType()
const Not supported Supported schemaToType()
exclusiveMinimum boolean number Not TS-relevant
webhooks Not supported Top-level field metadata extraction
$ref siblings Not allowed Allowed with summary/desc Reference handling
pathItems Not in components In components Schema lookup

Proposed Changes

  1. Detect 3.1 version in parser.ts metadata extraction
  2. Handle array types in generate.ts:
    // type: ['string', 'null'] -> string | null
    if (Array.isArray(s.type)) {
      return s.type.map(t => t === 'null' ? 'null' : mapPrimitiveType(t)).join(' | ');
    }
    
  3. Handle const in generate.ts:
    if (s.const !== undefined) {
      return typeof s.const === 'string' ? `'${s.const}'` : String(s.const);
    }
    
  4. Extract webhooks in parser.ts metadata

Files to Modify

File Change
src/lib/parser.ts Add webhook count to metadata
src/lib/types.ts Add webhookCount?: number to ParsedSpec
src/tools/generate.ts Handle 3.1 type arrays, const
src/utils/format.ts Display webhook count

Risks

Risk Impact Mitigation
Array type combinations complex LOW Map each type, join with |
const type inference tricky LOW Use literal types
Existing 3.0 specs break LOW Changes are additive

Feature 3: Batch Operations

Proposed Design

Add a new tool batch-process that accepts multiple specs:

// src/tools/batch.ts
export const batchToolSchema = {
  paths: z.array(z.string()).describe('Paths to OpenAPI specs'),
  operation: z.enum(['parse', 'validate']).describe('Operation to perform'),
};

Implementation Options

Option A: Sequential Processing (Simpler)

const results = [];
for (const path of paths) {
  results.push(await parseSpec(path));
}

Option B: Parallel Processing (Faster)

const results = await Promise.allSettled(
  paths.map(path => parseSpec(path))
);

Use Option B with concurrency limit:

// src/lib/batch.ts
async function batchProcess<T>(
  items: string[],
  processor: (item: string) => Promise<T>,
  concurrency = 5
): Promise<Array<{ path: string; result?: T; error?: string }>> {
  const results: Array<...> = [];
  const executing: Promise<void>[] = [];

  for (const item of items) {
    const p = processor(item)
      .then(result => results.push({ path: item, result }))
      .catch(error => results.push({ path: item, error: error.message }));

    executing.push(p);
    if (executing.length >= concurrency) {
      await Promise.race(executing);
    }
  }
  await Promise.all(executing);
  return results;
}

Files to Modify

File Change
src/lib/batch.ts NEW: Parallel processing utility
src/tools/batch.ts NEW: batch-process tool
src/index.ts Register batch-process tool
src/utils/format.ts Add formatBatchResults()

Output Format

interface BatchResult {
  success: boolean;
  total: number;
  succeeded: number;
  failed: number;
  results: Array<{
    path: string;
    success: boolean;
    data?: ParsedSpec | ValidationResult;
    error?: string;
  }>;
}

Risks

Risk Impact Mitigation
Resource exhaustion with many specs HIGH Concurrency limit (5)
One failure stops all LOW Promise.allSettled continues
Output too large MEDIUM Summarize in text, full in structured

Feature 4: TypeScript Generation Options

Current Behavior

generate-types produces a fixed output format:

  • export interface for objects
  • export type for enums, arrays, primitives
  • JSDoc comments for descriptions
  • Properties marked optional with ?

Proposed Options

export const generateToolSchema = {
  path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
  schemas: z.array(z.string()).optional(),
  options: z.object({
    // Output style
    interfacePrefix: z.string().optional().describe('Prefix for interfaces (e.g., "I")'),
    interfaceSuffix: z.string().optional().describe('Suffix for interfaces (e.g., "Type")'),
    enumAsUnion: z.boolean().optional().describe('Generate enums as union types (default: true)'),
    enumAsEnum: z.boolean().optional().describe('Generate enums as TypeScript enums'),

    // Formatting
    indentation: z.enum(['2', '4', 'tab']).optional().describe('Indentation style'),
    semicolons: z.boolean().optional().describe('Add trailing semicolons'),

    // Optionality
    allOptional: z.boolean().optional().describe('Make all properties optional'),
    allRequired: z.boolean().optional().describe('Make all properties required'),

    // Advanced
    readonlyProperties: z.boolean().optional().describe('Mark properties as readonly'),
    exportDefault: z.boolean().optional().describe('Use default exports'),
    includeRefs: z.boolean().optional().describe('Include $ref comments'),
  }).optional(),
};

Implementation Priority

Phase 1 (High value, low effort):

  • enumAsUnion vs enumAsEnum
  • interfacePrefix / interfaceSuffix

Phase 2 (Medium value):

  • indentation options
  • allOptional / allRequired

Phase 3 (Lower priority):

  • readonlyProperties
  • includeRefs

Files to Modify

File Change
src/tools/generate.ts Add options parameter, conditional generation
src/lib/types.ts Add TypeScriptOptions interface

Example Output Variations

Default (current):

export type PetStatus = 'available' | 'pending' | 'sold';
export interface Pet {
  id: number;
  name: string;
  status?: PetStatus;
}

With enumAsEnum: true, interfacePrefix: 'I':

export enum PetStatus {
  Available = 'available',
  Pending = 'pending',
  Sold = 'sold'
}
export interface IPet {
  id: number;
  name: string;
  status?: PetStatus;
}

Risks

Risk Impact Mitigation
Option combinations create invalid TS LOW Validate combinations
Too many options confuse users MEDIUM Good defaults, clear docs
Breaking existing behavior LOW All options optional with current defaults

Dependencies

Upstream (What calls this)

  • MCP Clients (Claude Code, etc.) call tools via JSON-RPC
  • All tools use parseSpec() from lib/parser.ts

Downstream (What this calls)

  • @apidevtools/swagger-parser for spec parsing/validation
  • Node.js fs module for local file access
  • Node.js https/http for remote fetch (via swagger-parser)

Edge Cases

Caching

  • Spec URL returns 304 Not Modified - should reuse cache
  • Local file deleted between cache set and get - invalidate on error
  • Concurrent requests for same URL - deduplicate in-flight requests

OpenAPI 3.1

  • Mixed array types: type: ['string', 'integer', 'null']
  • const with complex objects: const: {foo: 'bar'}
  • $ref with sibling description - merge into output

Batch Operations

  • Empty paths array - return immediately with success
  • Mix of local and remote paths - process in parallel
  • Single path failure mid-batch - continue, report in results

TypeScript Options

  • Invalid combination: enumAsUnion + enumAsEnum both true
  • Schema name collision after prefix/suffix added
  • Circular references with option changes

Testing Strategy

  1. Caching

    • Unit test cache hit/miss
    • Test TTL expiration
    • Test LRU eviction
    • Test local file mtime detection
  2. OpenAPI 3.1

    • Add 3.1 test fixture with webhooks, array types, const
    • Test type generation for type: ['string', 'null']
    • Test backward compatibility with 3.0 spec
  3. Batch Operations

    • Test with 0, 1, many specs
    • Test mix of success and failure
    • Test concurrency limit respected
  4. TypeScript Options

    • Snapshot tests for each option combination
    • Test prefix/suffix application
    • Test enum generation modes

  1. TypeScript Options - Most user-visible, low risk
  2. Caching - Performance win, moderate complexity
  3. OpenAPI 3.1 - Already mostly working, polish needed
  4. Batch Operations - New tool, can be done independently

Next Steps

  • /implement - Proceed with implementation (specify which feature)
  • /create work - Create detailed task documentation for each feature