# Analysis: Feature Enhancements (Caching, OpenAPI 3.1, Batch, TypeScript Options) ## Scope Affected - [x] 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: ```typescript // 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`: ```typescript // New file: src/lib/cache.ts interface CacheEntry { spec: OpenAPISpec; metadata: ParsedSpec; dereferenced: boolean; timestamp: number; etag?: string; } class SpecCache { private cache: Map; 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 ```typescript // 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: ```javascript // 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: ```typescript // 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: ```typescript 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: ```typescript // 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) ```typescript const results = []; for (const path of paths) { results.push(await parseSpec(path)); } ``` **Option B: Parallel Processing** (Faster) ```typescript const results = await Promise.allSettled( paths.map(path => parseSpec(path)) ); ``` ### Recommended Approach Use **Option B** with concurrency limit: ```typescript // src/lib/batch.ts async function batchProcess( items: string[], processor: (item: string) => Promise, concurrency = 5 ): Promise> { const results: Array<...> = []; const executing: Promise[] = []; 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 ```typescript 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 ```typescript 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)**: ```typescript export type PetStatus = 'available' | 'pending' | 'sold'; export interface Pet { id: number; name: string; status?: PetStatus; } ``` **With `enumAsEnum: true, interfacePrefix: 'I'`**: ```typescript 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 --- ## Implementation Order (Recommended) 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