#
tokens: 26107/50000 2/29 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/taewoong1378/notion-readonly-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── images
│       ├── connections.png
│       ├── integrations-capabilities.png
│       ├── integrations-creation.png
│       └── notion-api-tools-comparison.png
├── examples
│   └── petstore-server.cjs
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── scripts
│   ├── build-cli.js
│   ├── notion-openapi.json
│   └── start-server.ts
├── src
│   ├── init-server.ts
│   └── openapi-mcp-server
│       ├── auth
│       │   ├── index.ts
│       │   ├── template.ts
│       │   └── types.ts
│       ├── client
│       │   ├── __tests__
│       │   │   ├── http-client-upload.test.ts
│       │   │   ├── http-client.integration.test.ts
│       │   │   └── http-client.test.ts
│       │   └── http-client.ts
│       ├── index.ts
│       ├── mcp
│       │   ├── __tests__
│       │   │   ├── one-pager.test.ts
│       │   │   └── proxy.test.ts
│       │   └── proxy.ts
│       ├── openapi
│       │   ├── __tests__
│       │   │   ├── file-upload.test.ts
│       │   │   ├── parser-multipart.test.ts
│       │   │   └── parser.test.ts
│       │   ├── file-upload.ts
│       │   └── parser.ts
│       └── README.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/openapi-mcp-server/mcp/proxy.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'
   2 | import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
   3 | import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
   4 | import { JSONSchema7 as IJsonSchema } from 'json-schema'
   5 | import { OpenAPIV3 } from 'openapi-types'
   6 | import { HttpClient, HttpClientError } from '../client/http-client'
   7 | import { OpenAPIToMCPConverter } from '../openapi/parser'
   8 | 
   9 | type PathItemObject = OpenAPIV3.PathItemObject & {
  10 |   get?: OpenAPIV3.OperationObject
  11 |   put?: OpenAPIV3.OperationObject
  12 |   post?: OpenAPIV3.OperationObject
  13 |   delete?: OpenAPIV3.OperationObject
  14 |   patch?: OpenAPIV3.OperationObject
  15 | }
  16 | 
  17 | type NewToolDefinition = {
  18 |   methods: Array<{
  19 |     name: string
  20 |     description: string
  21 |     inputSchema: IJsonSchema & { type: 'object' }
  22 |     returnSchema?: IJsonSchema
  23 |   }>
  24 | }
  25 | 
  26 | // Notion object type definition
  27 | interface NotionBlock {
  28 |   object: 'block';
  29 |   id: string;
  30 |   type: string;
  31 |   has_children?: boolean;
  32 |   [key: string]: any; // Allow additional fields
  33 | }
  34 | 
  35 | interface NotionPage {
  36 |   object: 'page';
  37 |   id: string;
  38 |   properties: Record<string, any>;
  39 |   [key: string]: any; // Allow additional fields
  40 | }
  41 | 
  42 | interface NotionDatabase {
  43 |   object: 'database';
  44 |   id: string;
  45 |   properties: Record<string, any>;
  46 |   [key: string]: any; // Allow additional fields
  47 | }
  48 | 
  49 | interface NotionComment {
  50 |   object: 'comment';
  51 |   id: string;
  52 |   [key: string]: any; // Allow additional fields
  53 | }
  54 | 
  55 | type NotionObject = NotionBlock | NotionPage | NotionDatabase | NotionComment;
  56 | 
  57 | // Recursive exploration options
  58 | interface RecursiveExplorationOptions {
  59 |   maxDepth?: number;
  60 |   includeDatabases?: boolean;
  61 |   includeComments?: boolean;
  62 |   includeProperties?: boolean;
  63 |   maxParallelRequests?: number;
  64 |   skipCache?: boolean;
  65 |   batchSize?: number;
  66 |   timeoutMs?: number;
  67 |   runInBackground?: boolean;
  68 | }
  69 | 
  70 | // import this class, extend and return server
  71 | export class MCPProxy {
  72 |   private server: Server
  73 |   private httpClient: HttpClient
  74 |   private tools: Record<string, NewToolDefinition>
  75 |   private openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
  76 |   private pageCache: Map<string, any> = new Map() // Cache for performance improvement
  77 |   private blockCache: Map<string, any> = new Map() // Block cache
  78 |   private databaseCache: Map<string, any> = new Map() // Database cache
  79 |   private commentCache: Map<string, any> = new Map() // Comment cache
  80 |   private propertyCache: Map<string, any> = new Map() // Property cache
  81 |   private backgroundProcessingResults: Map<string, any> = new Map()
  82 | 
  83 |   constructor(name: string, openApiSpec: OpenAPIV3.Document) {
  84 |     this.server = new Server({ name, version: '1.0.0' }, { capabilities: { tools: {} } })
  85 |     const baseUrl = openApiSpec.servers?.[0].url
  86 |     if (!baseUrl) {
  87 |       throw new Error('No base URL found in OpenAPI spec')
  88 |     }
  89 |     this.httpClient = new HttpClient(
  90 |       {
  91 |         baseUrl,
  92 |         headers: this.parseHeadersFromEnv(),
  93 |       },
  94 |       openApiSpec,
  95 |     )
  96 | 
  97 |     // Convert OpenAPI spec to MCP tools
  98 |     const converter = new OpenAPIToMCPConverter(openApiSpec)
  99 |     const { tools, openApiLookup } = converter.convertToMCPTools()
 100 |     this.tools = tools
 101 |     this.openApiLookup = openApiLookup
 102 | 
 103 |     this.setupHandlers()
 104 |   }
 105 | 
 106 |   private setupHandlers() {
 107 |     // Handle tool listing
 108 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => {
 109 |       const tools: Tool[] = []
 110 | 
 111 |       // Log available tools
 112 |       console.log('One Pager Assistant - Available tools:')
 113 | 
 114 |       // Add methods as separate tools to match the MCP format
 115 |       Object.entries(this.tools).forEach(([toolName, def]) => {
 116 |         def.methods.forEach(method => {
 117 |           const toolNameWithMethod = `${toolName}-${method.name}`;
 118 |           const truncatedToolName = this.truncateToolName(toolNameWithMethod);
 119 |           tools.push({
 120 |             name: truncatedToolName,
 121 |             description: method.description,
 122 |             inputSchema: method.inputSchema as Tool['inputSchema'],
 123 |           })
 124 |           console.log(`- ${truncatedToolName}: ${method.description}`)
 125 |         })
 126 |       })
 127 | 
 128 |       // Add extended One Pager tool
 129 |       const onePagerTool = {
 130 |         name: 'API-get-one-pager',
 131 |         description: 'Recursively retrieve a full Notion page with all its blocks, databases, and related content',
 132 |         inputSchema: {
 133 |           type: 'object',
 134 |           properties: {
 135 |             page_id: {
 136 |               type: 'string',
 137 |               description: 'Identifier for a Notion page',
 138 |             },
 139 |             maxDepth: {
 140 |               type: 'integer',
 141 |               description: 'Maximum recursion depth (default: 5)',
 142 |             },
 143 |             includeDatabases: {
 144 |               type: 'boolean',
 145 |               description: 'Whether to include linked databases (default: true)',
 146 |             },
 147 |             includeComments: {
 148 |               type: 'boolean',
 149 |               description: 'Whether to include comments (default: true)',
 150 |             },
 151 |             includeProperties: {
 152 |               type: 'boolean',
 153 |               description: 'Whether to include detailed page properties (default: true)',
 154 |             },
 155 |             maxParallelRequests: {
 156 |               type: 'integer',
 157 |               description: 'Maximum number of parallel requests (default: 15)',
 158 |             },
 159 |             batchSize: {
 160 |               type: 'integer',
 161 |               description: 'Batch size for parallel processing (default: 10)',
 162 |             },
 163 |             timeoutMs: {
 164 |               type: 'integer',
 165 |               description: 'Timeout in milliseconds (default: 300000)',
 166 |             },
 167 |             runInBackground: {
 168 |               type: 'boolean',
 169 |               description: 'Process request in background without timeout (default: true)',
 170 |             }
 171 |           },
 172 |           required: ['page_id'],
 173 |         } as Tool['inputSchema'],
 174 |       };
 175 |       
 176 |       tools.push(onePagerTool);
 177 |       console.log(`- ${onePagerTool.name}: ${onePagerTool.description}`);
 178 |       
 179 |       // Add tool to retrieve background processing results
 180 |       const backgroundResultTool = {
 181 |         name: 'API-get-background-result',
 182 |         description: 'Retrieve the result of a background processing request',
 183 |         inputSchema: {
 184 |           type: 'object',
 185 |           properties: {
 186 |             page_id: {
 187 |               type: 'string',
 188 |               description: 'Identifier for the Notion page that was processed in background',
 189 |             },
 190 |           },
 191 |           required: ['page_id'],
 192 |         } as Tool['inputSchema'],
 193 |       };
 194 |       
 195 |       tools.push(backgroundResultTool);
 196 |       console.log(`- ${backgroundResultTool.name}: ${backgroundResultTool.description}`);
 197 | 
 198 |       return { tools }
 199 |     })
 200 | 
 201 |     // Handle tool calling
 202 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 203 |       const { name, arguments: params } = request.params
 204 | 
 205 |       console.log(`One Pager Assistant - Tool call: ${name}`)
 206 |       console.log('Parameters:', JSON.stringify(params, null, 2))
 207 | 
 208 |       try {
 209 |         // Handle extended One Pager tool
 210 |         if (name === 'API-get-one-pager') {
 211 |           return await this.handleOnePagerRequest(params);
 212 |         }
 213 |         
 214 |         // Handle background result retrieval
 215 |         if (name === 'API-get-background-result') {
 216 |           const result = this.getBackgroundProcessingResult(params?.page_id as string);
 217 |           return {
 218 |             content: [
 219 |               {
 220 |                 type: 'text',
 221 |                 text: JSON.stringify(result),
 222 |               },
 223 |             ],
 224 |           };
 225 |         }
 226 | 
 227 |         // Find the operation in OpenAPI spec
 228 |         const operation = this.findOperation(name)
 229 |         if (!operation) {
 230 |           const error = `Method ${name} not found.`
 231 |           console.error(error)
 232 |           return {
 233 |             content: [
 234 |               {
 235 |                 type: 'text',
 236 |                 text: JSON.stringify({
 237 |                   status: 'error',
 238 |                   message: error,
 239 |                   code: 404
 240 |                 }),
 241 |               },
 242 |             ],
 243 |           }
 244 |         }
 245 | 
 246 |         // Optimized parallel processing for API-get-block-children
 247 |         if (name === 'API-get-block-children') {
 248 |           // Create basic options for logging control
 249 |           const blockOptions: RecursiveExplorationOptions = {
 250 |             runInBackground: false, // Default to not showing logs for regular API calls
 251 |           };
 252 |           
 253 |           return await this.handleBlockChildrenParallel(operation, params, blockOptions);
 254 |         }
 255 | 
 256 |         // Other regular API calls
 257 |         console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path}`)
 258 |         const response = await this.httpClient.executeOperation(operation, params)
 259 | 
 260 |         // Log response summary
 261 |         console.log('Notion API response code:', response.status)
 262 |         if (response.status !== 200) {
 263 |           console.error('Response error:', response.data)
 264 |         } else {
 265 |           console.log('Response success')
 266 |         }
 267 | 
 268 |         // Update cache with response data
 269 |         this.updateCacheFromResponse(name, response.data);
 270 | 
 271 |         // Convert response to MCP format
 272 |         return {
 273 |           content: [
 274 |             {
 275 |               type: 'text',
 276 |               text: JSON.stringify(response.data),
 277 |             },
 278 |           ],
 279 |         }
 280 |       } catch (error) {
 281 |         console.error('Tool call error', error)
 282 |         
 283 |         if (error instanceof HttpClientError) {
 284 |           console.error('HttpClientError occurred, returning structured error', error)
 285 |           const data = error.data?.response?.data ?? error.data ?? {}
 286 |           return {
 287 |             content: [
 288 |               {
 289 |                 type: 'text',
 290 |                 text: JSON.stringify({
 291 |                   status: 'error',
 292 |                   code: error.status,
 293 |                   message: error.message,
 294 |                   details: typeof data === 'object' ? data : { data: data },
 295 |                 }),
 296 |               },
 297 |             ],
 298 |           }
 299 |         }
 300 |         
 301 |         // Ensure any other errors are also properly formatted as JSON
 302 |         return {
 303 |           content: [
 304 |             {
 305 |               type: 'text',
 306 |               text: JSON.stringify({
 307 |                 status: 'error',
 308 |                 message: error instanceof Error ? error.message : String(error),
 309 |                 code: 500
 310 |               }),
 311 |             },
 312 |           ],
 313 |         }
 314 |       }
 315 |     })
 316 |   }
 317 | 
 318 |   // Update cache based on API response type
 319 |   private updateCacheFromResponse(apiName: string, data: any): void {
 320 |     if (!data || typeof data !== 'object') return;
 321 | 
 322 |     try {
 323 |       // Update appropriate cache based on API response type
 324 |       if (apiName === 'API-retrieve-a-page' && data.object === 'page' && data.id) {
 325 |         this.pageCache.set(data.id, data);
 326 |       } else if (apiName === 'API-retrieve-a-block' && data.object === 'block' && data.id) {
 327 |         this.blockCache.set(data.id, data);
 328 |       } else if (apiName === 'API-retrieve-a-database' && data.object === 'database' && data.id) {
 329 |         this.databaseCache.set(data.id, data);
 330 |       } else if (apiName === 'API-retrieve-a-comment' && data.results) {
 331 |         // Cache comments from result list
 332 |         data.results.forEach((comment: any) => {
 333 |           if (comment.object === 'comment' && comment.id) {
 334 |             this.commentCache.set(comment.id, comment);
 335 |           }
 336 |         });
 337 |       } else if (apiName === 'API-retrieve-a-page-property' && data.results) {
 338 |         // Page property caching - would need params from call context
 339 |         // Skip this in current context
 340 |         console.log('Page property information has been cached');
 341 |       }
 342 | 
 343 |       // API-get-block-children handled in handleBlockChildrenParallel
 344 |     } catch (error) {
 345 |       console.warn('Error updating cache:', error);
 346 |     }
 347 |   }
 348 | 
 349 |   // One Pager request handler
 350 |   private async handleOnePagerRequest(params: any) {
 351 |     if (params.runInBackground !== false) {
 352 |       console.log('Starting One Pager request processing:', params.page_id);
 353 |     }
 354 |     
 355 |     const options: RecursiveExplorationOptions = {
 356 |       maxDepth: params.maxDepth || 5,
 357 |       includeDatabases: params.includeDatabases !== false,
 358 |       includeComments: params.includeComments !== false,
 359 |       includeProperties: params.includeProperties !== false,
 360 |       maxParallelRequests: params.maxParallelRequests || 15,
 361 |       skipCache: params.skipCache || false,
 362 |       batchSize: params.batchSize || 10,
 363 |       timeoutMs: params.timeoutMs || 300000, // Increased timeout to 5 minutes (300000ms)
 364 |       runInBackground: params.runInBackground !== false,
 365 |     };
 366 |     
 367 |     if (options.runInBackground) {
 368 |       console.log('Exploration options:', JSON.stringify(options, null, 2));
 369 |     }
 370 |     
 371 |     try {
 372 |       const startTime = Date.now();
 373 |       
 374 |       // Check if we should run in background mode
 375 |       if (options.runInBackground) {
 376 |         // Return immediately with a background processing message
 377 |         // The actual processing will continue in the background
 378 |         this.runBackgroundProcessing(params.page_id, options);
 379 |         
 380 |         return {
 381 |           content: [
 382 |             {
 383 |               type: 'text',
 384 |               text: JSON.stringify({
 385 |                 status: 'processing',
 386 |                 message: `Request processing for page ${params.page_id} started in background`,
 387 |                 page_id: params.page_id,
 388 |                 request_time: new Date().toISOString(),
 389 |                 options: {
 390 |                   maxDepth: options.maxDepth,
 391 |                   includeDatabases: options.includeDatabases,
 392 |                   includeComments: options.includeComments,
 393 |                   includeProperties: options.includeProperties,
 394 |                   timeoutMs: options.timeoutMs
 395 |                 }
 396 |               }),
 397 |             },
 398 |           ],
 399 |         };
 400 |       }
 401 |       
 402 |       // Foreground processing (standard behavior)
 403 |       const pageData = await this.retrievePageRecursively(params.page_id, options);
 404 |       
 405 |       const duration = Date.now() - startTime;
 406 |       if (options.runInBackground) {
 407 |         console.log(`One Pager completed in ${duration}ms for page ${params.page_id}`);
 408 |       }
 409 |       
 410 |       return {
 411 |         content: [
 412 |           {
 413 |             type: 'text',
 414 |             text: JSON.stringify({
 415 |               ...pageData,
 416 |               _meta: {
 417 |                 processingTimeMs: duration,
 418 |                 retrievedAt: new Date().toISOString(),
 419 |                 options: {
 420 |                   maxDepth: options.maxDepth,
 421 |                   includeDatabases: options.includeDatabases,
 422 |                   includeComments: options.includeComments,
 423 |                   includeProperties: options.includeProperties
 424 |                 }
 425 |               }
 426 |             }),
 427 |           },
 428 |         ],
 429 |       };
 430 |     } catch (error) {
 431 |       if (options.runInBackground) {
 432 |         console.error('Error in One Pager request:', error);
 433 |       }
 434 |       const errorResponse = {
 435 |         status: 'error',
 436 |         message: error instanceof Error ? error.message : String(error),
 437 |         code: error instanceof HttpClientError ? error.status : 500,
 438 |         details: error instanceof HttpClientError ? error.data : undefined,
 439 |         timestamp: new Date().toISOString()
 440 |       };
 441 |       
 442 |       return {
 443 |         content: [
 444 |           {
 445 |             type: 'text',
 446 |             text: JSON.stringify(errorResponse),
 447 |           },
 448 |         ],
 449 |       };
 450 |     }
 451 |   }
 452 | 
 453 |   // New method to run processing in background
 454 |   private runBackgroundProcessing(pageId: string, options: RecursiveExplorationOptions): void {
 455 |     // Use setTimeout to detach from the current execution context
 456 |     setTimeout(async () => {
 457 |       try {
 458 |         console.log(`Background processing started for page ${pageId}`);
 459 |         const startTime = Date.now();
 460 |         
 461 |         // Execute the recursive page retrieval without time restrictions
 462 |         const noTimeoutOptions = { ...options, timeoutMs: 0 }; // 0 means no timeout
 463 |         const pageData = await this.retrievePageRecursively(pageId, noTimeoutOptions);
 464 |         
 465 |         const duration = Date.now() - startTime;
 466 |         console.log(`Background processing completed in ${duration}ms for page ${pageId}`);
 467 |         
 468 |         // Store the result in cache for later retrieval
 469 |         this.storeBackgroundProcessingResult(pageId, {
 470 |           ...pageData,
 471 |           _meta: {
 472 |             processingTimeMs: duration,
 473 |             retrievedAt: new Date().toISOString(),
 474 |             processedInBackground: true,
 475 |             options: {
 476 |               maxDepth: options.maxDepth,
 477 |               includeDatabases: options.includeDatabases,
 478 |               includeComments: options.includeComments,
 479 |               includeProperties: options.includeProperties
 480 |             }
 481 |           }
 482 |         });
 483 |       } catch (error) {
 484 |         console.error(`Background processing error for page ${pageId}:`, error);
 485 |         // Store error result for later retrieval
 486 |         this.storeBackgroundProcessingResult(pageId, {
 487 |           status: 'error',
 488 |           page_id: pageId,
 489 |           message: error instanceof Error ? error.message : String(error),
 490 |           timestamp: new Date().toISOString()
 491 |         });
 492 |       }
 493 |     }, 0);
 494 |   }
 495 |   
 496 |   // Store background processing results
 497 |   private storeBackgroundProcessingResult(pageId: string, result: any): void {
 498 |     this.backgroundProcessingResults.set(pageId, result);
 499 |   }
 500 |   
 501 |   // Add a new tool method to retrieve background processing results
 502 |   public getBackgroundProcessingResult(pageId: string): any {
 503 |     return this.backgroundProcessingResults.get(pageId) || { 
 504 |       status: 'not_found',
 505 |       message: `No background processing result found for page ${pageId}`
 506 |     };
 507 |   }
 508 | 
 509 |   // Recursively retrieve page content
 510 |   private async retrievePageRecursively(pageId: string, options: RecursiveExplorationOptions, currentDepth: number = 0): Promise<any> {
 511 |     if (options.runInBackground) {
 512 |       console.log(`Recursive page exploration: ${pageId}, depth: ${currentDepth}/${options.maxDepth || 5}`);
 513 |     }
 514 |     
 515 |     const timeoutPromise = new Promise<never>((_, reject) => {
 516 |       if (options.timeoutMs && options.timeoutMs > 0) {
 517 |         setTimeout(() => reject(new Error(`Operation timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
 518 |       }
 519 |     });
 520 |     
 521 |     try {
 522 |       // Check maximum depth
 523 |       if (currentDepth >= (options.maxDepth || 5)) {
 524 |         if (options.runInBackground) {
 525 |           console.log(`Maximum depth reached: ${currentDepth}/${options.maxDepth || 5}`);
 526 |         }
 527 |         return { id: pageId, note: "Maximum recursion depth reached" };
 528 |       }
 529 |       
 530 |       // 1. Get basic page info (check cache)
 531 |       let pageData: any;
 532 |       if (!options.skipCache && this.pageCache.has(pageId)) {
 533 |         pageData = this.pageCache.get(pageId);
 534 |         if (options.runInBackground) {
 535 |           console.log(`Page cache hit: ${pageId}`);
 536 |         }
 537 |       } else {
 538 |         // Retrieve page info via API call
 539 |         const operation = this.findOperation('API-retrieve-a-page');
 540 |         if (!operation) {
 541 |           throw new Error('API-retrieve-a-page method not found.');
 542 |         }
 543 |         
 544 |         if (options.runInBackground) {
 545 |           console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (pageId: ${pageId})`);
 546 |         }
 547 |         
 548 |         // Only race with timeout if timeoutMs is set
 549 |         let response;
 550 |         if (options.timeoutMs && options.timeoutMs > 0) {
 551 |           response = await Promise.race([
 552 |             this.httpClient.executeOperation(operation, { page_id: pageId }),
 553 |             timeoutPromise
 554 |           ]) as any;
 555 |         } else {
 556 |           response = await this.httpClient.executeOperation(operation, { page_id: pageId });
 557 |         }
 558 |         
 559 |         if (response.status !== 200) {
 560 |           if (options.runInBackground) {
 561 |             console.error('Error retrieving page information:', response.data);
 562 |           }
 563 |           return { 
 564 |             id: pageId,
 565 |             error: "Failed to retrieve page", 
 566 |             status: response.status,
 567 |             details: response.data 
 568 |           };
 569 |         }
 570 |         
 571 |         pageData = response.data;
 572 |         // Only cache successful responses
 573 |         this.pageCache.set(pageId, pageData);
 574 |       }
 575 |       
 576 |       // Collection of tasks to be executed in parallel for improved efficiency
 577 |       const parallelTasks: Promise<any>[] = [];
 578 |       
 579 |       // 2. Fetch block content (register async task)
 580 |       const blocksPromise = this.retrieveBlocksRecursively(pageId, options, currentDepth + 1);
 581 |       parallelTasks.push(blocksPromise);
 582 |       
 583 |       // 3. Fetch property details (if option enabled)
 584 |       let propertiesPromise: Promise<any> = Promise.resolve(null);
 585 |       if (options.includeProperties && pageData.properties) {
 586 |         propertiesPromise = this.enrichPageProperties(pageId, pageData.properties, options);
 587 |         parallelTasks.push(propertiesPromise);
 588 |       }
 589 |       
 590 |       // 4. Fetch comments (if option enabled)
 591 |       let commentsPromise: Promise<any> = Promise.resolve(null);
 592 |       if (options.includeComments) {
 593 |         commentsPromise = this.retrieveComments(pageId, options);
 594 |         parallelTasks.push(commentsPromise);
 595 |       }
 596 |       
 597 |       // Execute all tasks in parallel
 598 |       if (options.timeoutMs && options.timeoutMs > 0) {
 599 |         await Promise.race([Promise.all(parallelTasks), timeoutPromise]);
 600 |       } else {
 601 |         await Promise.all(parallelTasks);
 602 |       }
 603 |       
 604 |       // Integrate results into the main page data
 605 |       const enrichedPageData = { ...pageData };
 606 |       
 607 |       // Add block content
 608 |       const blocksData = await blocksPromise;
 609 |       enrichedPageData.content = blocksData;
 610 |       
 611 |       // Add property details (if option enabled)
 612 |       if (options.includeProperties && pageData.properties) {
 613 |         const enrichedProperties = await propertiesPromise;
 614 |         if (enrichedProperties) {
 615 |           enrichedPageData.detailed_properties = enrichedProperties;
 616 |         }
 617 |       }
 618 |       
 619 |       // Add comments (if option enabled)
 620 |       if (options.includeComments) {
 621 |         const comments = await commentsPromise;
 622 |         if (comments && comments.results && comments.results.length > 0) {
 623 |           enrichedPageData.comments = comments;
 624 |         }
 625 |       }
 626 |       
 627 |       return enrichedPageData;
 628 |     } catch (error) {
 629 |       if (error instanceof Error && error.message.includes('timed out')) {
 630 |         if (options.runInBackground) {
 631 |           console.error(`Timeout occurred while processing page ${pageId} at depth ${currentDepth}`);
 632 |         }
 633 |         return { 
 634 |           id: pageId, 
 635 |           error: "Operation timed out", 
 636 |           partial_results: true,
 637 |           note: `Processing exceeded timeout limit (${options.timeoutMs}ms)`
 638 |         };
 639 |       }
 640 |       
 641 |       if (options.runInBackground) {
 642 |         console.error(`Error in retrievePageRecursively for page ${pageId}:`, error);
 643 |       }
 644 |       return { 
 645 |         id: pageId, 
 646 |         error: error instanceof Error ? error.message : String(error),
 647 |         retrievalFailed: true
 648 |       };
 649 |     }
 650 |   }
 651 |   
 652 |   // Recursively retrieve block content with improved parallelism
 653 |   private async retrieveBlocksRecursively(blockId: string, options: RecursiveExplorationOptions, currentDepth: number): Promise<any[]> {
 654 |     if (options.runInBackground) {
 655 |       console.log(`Recursive block exploration: ${blockId}, depth: ${currentDepth}/${options.maxDepth || 5}`);
 656 |     }
 657 |     
 658 |     if (currentDepth >= (options.maxDepth || 5)) {
 659 |       if (options.runInBackground) {
 660 |         console.log(`Maximum depth reached: ${currentDepth}/${options.maxDepth || 5}`);
 661 |       }
 662 |       return [{ note: "Maximum recursion depth reached" }];
 663 |     }
 664 |     
 665 |     try {
 666 |       const operation = this.findOperation('API-get-block-children');
 667 |       if (!operation) {
 668 |         throw new Error('API-get-block-children method not found.');
 669 |       }
 670 |       
 671 |       const blocksResponse = await this.handleBlockChildrenParallel(operation, { 
 672 |         block_id: blockId,
 673 |         page_size: 100
 674 |       }, options);
 675 |       
 676 |       const blocksData = JSON.parse(blocksResponse.content[0].text);
 677 |       const blocks = blocksData.results || [];
 678 |       
 679 |       if (blocks.length === 0) {
 680 |         return [];
 681 |       }
 682 |       
 683 |       const batchSize = options.batchSize || 10;
 684 |       const enrichedBlocks: any[] = [];
 685 |       
 686 |       // Process blocks in batches for memory optimization and improved parallel execution
 687 |       for (let i = 0; i < blocks.length; i += batchSize) {
 688 |         const batch = blocks.slice(i, i + batchSize);
 689 |         
 690 |         // Process each batch in parallel
 691 |         const batchResults = await Promise.all(
 692 |           batch.map(async (block: any) => {
 693 |             this.blockCache.set(block.id, block);
 694 |             
 695 |             const enrichedBlock = { ...block };
 696 |             
 697 |             // Collection of async tasks for this block
 698 |             const blockTasks: Promise<any>[] = [];
 699 |             
 700 |             // Process child blocks recursively
 701 |             if (block.has_children) {
 702 |               blockTasks.push(
 703 |                 this.retrieveBlocksRecursively(block.id, options, currentDepth + 1)
 704 |                   .then(childBlocks => { enrichedBlock.children = childBlocks; })
 705 |                   .catch(error => {
 706 |                     console.error(`Error retrieving child blocks for ${block.id}:`, error);
 707 |                     enrichedBlock.children_error = { message: String(error) };
 708 |                     return [];
 709 |                   })
 710 |               );
 711 |             }
 712 |             
 713 |             // Process database blocks (if option enabled)
 714 |             if (options.includeDatabases && 
 715 |                 (block.type === 'child_database' || block.type === 'linked_database')) {
 716 |               const databaseId = block[block.type]?.database_id;
 717 |               if (databaseId) {
 718 |                 blockTasks.push(
 719 |                   this.retrieveDatabase(databaseId, options)
 720 |                     .then(database => { enrichedBlock.database = database; })
 721 |                     .catch(error => {
 722 |                       console.error(`Error retrieving database ${databaseId}:`, error);
 723 |                       enrichedBlock.database_error = { message: String(error) };
 724 |                     })
 725 |                 );
 726 |               }
 727 |             }
 728 |             
 729 |             // Process page blocks or linked pages - optimization
 730 |             if (block.type === 'child_page' && currentDepth < (options.maxDepth || 5) - 1) {
 731 |               const pageId = block.id;
 732 |               blockTasks.push(
 733 |                 this.retrievePageBasicInfo(pageId, options)
 734 |                   .then(pageInfo => { enrichedBlock.page_info = pageInfo; })
 735 |                   .catch(error => {
 736 |                     console.error(`Error retrieving page info for ${pageId}:`, error);
 737 |                     enrichedBlock.page_info_error = { message: String(error) };
 738 |                   })
 739 |               );
 740 |             }
 741 |             
 742 |             // Wait for all async tasks to complete
 743 |             if (blockTasks.length > 0) {
 744 |               await Promise.all(blockTasks);
 745 |             }
 746 |             
 747 |             return enrichedBlock;
 748 |           })
 749 |         );
 750 |         
 751 |         enrichedBlocks.push(...batchResults);
 752 |       }
 753 |       
 754 |       return enrichedBlocks;
 755 |     } catch (error) {
 756 |       console.error(`Error in retrieveBlocksRecursively for block ${blockId}:`, error);
 757 |       return [{ 
 758 |         id: blockId, 
 759 |         error: error instanceof Error ? error.message : String(error),
 760 |         retrievalFailed: true
 761 |       }];
 762 |     }
 763 |   }
 764 |   
 765 |   // Lightweight method to fetch only basic page info (without recursive loading)
 766 |   private async retrievePageBasicInfo(pageId: string, options: RecursiveExplorationOptions): Promise<any> {
 767 |     // Check cache
 768 |     if (!options.skipCache && this.pageCache.has(pageId)) {
 769 |       const cachedData = this.pageCache.get(pageId);
 770 |       return {
 771 |         id: cachedData.id,
 772 |         title: cachedData.properties?.title || { text: null },
 773 |         icon: cachedData.icon,
 774 |         cover: cachedData.cover,
 775 |         url: cachedData.url,
 776 |         fromCache: true
 777 |       };
 778 |     }
 779 |     
 780 |     // Get page info via API
 781 |     const operation = this.findOperation('API-retrieve-a-page');
 782 |     if (!operation) {
 783 |       return { id: pageId, note: "API-retrieve-a-page method not found" };
 784 |     }
 785 |     
 786 |     try {
 787 |       const response = await this.httpClient.executeOperation(operation, { page_id: pageId });
 788 |       
 789 |       if (response.status !== 200) {
 790 |         return { id: pageId, error: "Failed to retrieve page", status: response.status };
 791 |       }
 792 |       
 793 |       const pageData = response.data;
 794 |       this.pageCache.set(pageId, pageData);
 795 |       
 796 |       return {
 797 |         id: pageData.id,
 798 |         title: pageData.properties?.title || { text: null },
 799 |         icon: pageData.icon,
 800 |         cover: pageData.cover,
 801 |         url: pageData.url,
 802 |         created_time: pageData.created_time,
 803 |         last_edited_time: pageData.last_edited_time
 804 |       };
 805 |     } catch (error) {
 806 |       console.error(`Error retrieving basic page info ${pageId}:`, error);
 807 |       return { id: pageId, error: error instanceof Error ? error.message : String(error) };
 808 |     }
 809 |   }
 810 | 
 811 |   // Retrieve database information
 812 |   private async retrieveDatabase(databaseId: string, options: RecursiveExplorationOptions): Promise<any> {
 813 |     console.log(`Retrieving database information: ${databaseId}`);
 814 |     
 815 |     // Check cache
 816 |     if (!options.skipCache && this.databaseCache.has(databaseId)) {
 817 |       console.log(`Database cache hit: ${databaseId}`);
 818 |       return this.databaseCache.get(databaseId);
 819 |     }
 820 |     
 821 |     // Get database info via API call
 822 |     const operation = this.findOperation('API-retrieve-a-database');
 823 |     if (!operation) {
 824 |       console.warn('API-retrieve-a-database method not found.');
 825 |       return { id: databaseId, note: "Database details not available" };
 826 |     }
 827 |     
 828 |     try {
 829 |       console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (databaseId: ${databaseId})`);
 830 |       const response = await this.httpClient.executeOperation(operation, { database_id: databaseId });
 831 |       
 832 |       if (response.status !== 200) {
 833 |         console.error('Error retrieving database information:', response.data);
 834 |         return { id: databaseId, error: "Failed to retrieve database" };
 835 |       }
 836 |       
 837 |       const databaseData = response.data;
 838 |       this.databaseCache.set(databaseId, databaseData);
 839 |       return databaseData;
 840 |     } catch (error) {
 841 |       console.error('Error retrieving database:', error);
 842 |       return { id: databaseId, error: "Failed to retrieve database" };
 843 |     }
 844 |   }
 845 |   
 846 |   // Retrieve comments
 847 |   private async retrieveComments(blockId: string, options: RecursiveExplorationOptions): Promise<any> {
 848 |     if (options.runInBackground) {
 849 |       console.log(`Retrieving comments: ${blockId}`);
 850 |     }
 851 |     
 852 |     // Get comments via API call
 853 |     const operation = this.findOperation('API-retrieve-a-comment');
 854 |     if (!operation) {
 855 |       if (options.runInBackground) {
 856 |         console.warn('API-retrieve-a-comment method not found.');
 857 |       }
 858 |       return Promise.resolve({ results: [] });
 859 |     }
 860 |     
 861 |     try {
 862 |       if (options.runInBackground) {
 863 |         console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (blockId: ${blockId})`);
 864 |       }
 865 |       
 866 |       return this.httpClient.executeOperation(operation, { block_id: blockId })
 867 |         .then(response => {
 868 |           if (response.status !== 200) {
 869 |             if (options.runInBackground) {
 870 |               console.error('Error retrieving comments:', response.data);
 871 |             }
 872 |             return { results: [] };
 873 |           }
 874 |           
 875 |           const commentsData = response.data;
 876 |           
 877 |           // Cache comments
 878 |           if (commentsData.results) {
 879 |             commentsData.results.forEach((comment: any) => {
 880 |               if (comment.id) {
 881 |                 this.commentCache.set(comment.id, comment);
 882 |               }
 883 |             });
 884 |           }
 885 |           
 886 |           return commentsData;
 887 |         })
 888 |         .catch(error => {
 889 |           if (options.runInBackground) {
 890 |             console.error('Error retrieving comments:', error);
 891 |           }
 892 |           return { results: [] };
 893 |         });
 894 |     } catch (error) {
 895 |       if (options.runInBackground) {
 896 |         console.error('Error retrieving comments:', error);
 897 |       }
 898 |       return Promise.resolve({ results: [] });
 899 |     }
 900 |   }
 901 |   
 902 |   // Enrich page properties with detailed information
 903 |   private async enrichPageProperties(pageId: string, properties: any, options: RecursiveExplorationOptions): Promise<any> {
 904 |     if (options.runInBackground) {
 905 |       console.log(`Enriching page properties: ${pageId}`);
 906 |     }
 907 |     
 908 |     const enrichedProperties = { ...properties };
 909 |     const propertyPromises: Promise<void>[] = [];
 910 |     
 911 |     // Get detailed information for each property
 912 |     for (const [propName, propData] of Object.entries(properties)) {
 913 |       const propId = (propData as any).id;
 914 |       if (!propId) continue;
 915 |       
 916 |       // Create cache key
 917 |       const cacheKey = `${pageId}:${propId}`;
 918 |       
 919 |       propertyPromises.push(
 920 |         (async () => {
 921 |           try {
 922 |             // Check cache
 923 |             if (!options.skipCache && this.propertyCache.has(cacheKey)) {
 924 |               enrichedProperties[propName].details = this.propertyCache.get(cacheKey);
 925 |             } else {
 926 |               // Skip properties with URLs that contain special characters like notion://
 927 |               if (propId.includes('notion://') || propId.includes('%3A%2F%2F')) {
 928 |                 if (options.runInBackground) {
 929 |                   console.warn(`Skipping property with special URL format: ${propName} (${propId})`);
 930 |                 }
 931 |                 enrichedProperties[propName].details = { 
 932 |                   object: 'property_item', 
 933 |                   type: 'unsupported',
 934 |                   unsupported: { type: 'special_url_format' } 
 935 |                 };
 936 |                 return;
 937 |               }
 938 |               
 939 |               // Get property details via API call
 940 |               const operation = this.findOperation('API-retrieve-a-page-property');
 941 |               if (!operation) {
 942 |                 if (options.runInBackground) {
 943 |                   console.warn('API-retrieve-a-page-property method not found.');
 944 |                 }
 945 |                 return;
 946 |               }
 947 |               
 948 |               const response = await this.httpClient.executeOperation(operation, {
 949 |                 page_id: pageId,
 950 |                 property_id: propId
 951 |               }).catch(error => {
 952 |                 if (options.runInBackground) {
 953 |                   console.warn(`Error retrieving property ${propName} (${propId}): ${error.message}`);
 954 |                 }
 955 |                 return { 
 956 |                   status: error.status || 500,
 957 |                   data: { 
 958 |                     object: 'property_item', 
 959 |                     type: 'error',
 960 |                     error: { message: error.message } 
 961 |                   }
 962 |                 };
 963 |               });
 964 |               
 965 |               if (response.status === 200) {
 966 |                 enrichedProperties[propName].details = response.data;
 967 |                 this.propertyCache.set(cacheKey, response.data);
 968 |               } else {
 969 |                 enrichedProperties[propName].details = { 
 970 |                   object: 'property_item', 
 971 |                   type: 'error',
 972 |                   error: { status: response.status, message: JSON.stringify(response.data) } 
 973 |                 };
 974 |               }
 975 |             }
 976 |           } catch (error) {
 977 |             if (options.runInBackground) {
 978 |               console.error(`Error retrieving property ${propName}:`, error);
 979 |             }
 980 |             enrichedProperties[propName].details = { 
 981 |               object: 'property_item', 
 982 |               type: 'error',
 983 |               error: { message: error instanceof Error ? error.message : String(error) } 
 984 |             };
 985 |           }
 986 |         })()
 987 |       );
 988 |     }
 989 |     
 990 |     // Get all property information in parallel
 991 |     await Promise.all(propertyPromises);
 992 |     
 993 |     return enrichedProperties;
 994 |   }
 995 | 
 996 |   // Optimized parallel processing for block children
 997 |   private async handleBlockChildrenParallel(
 998 |     operation: OpenAPIV3.OperationObject & { method: string; path: string }, 
 999 |     params: any,
1000 |     options?: RecursiveExplorationOptions
1001 |   ) {
1002 |     if (options?.runInBackground) {
1003 |       console.log(`Starting Notion API parallel processing: ${operation.method.toUpperCase()} ${operation.path}`);
1004 |     }
1005 |     
1006 |     // Get first page
1007 |     const initialResponse = await this.httpClient.executeOperation(operation, params);
1008 |     
1009 |     if (initialResponse.status !== 200) {
1010 |       if (options?.runInBackground) {
1011 |         console.error('Response error:', initialResponse.data);
1012 |       }
1013 |       return {
1014 |         content: [{ type: 'text', text: JSON.stringify(initialResponse.data) }],
1015 |       };
1016 |     }
1017 |     
1018 |     const results = initialResponse.data.results || [];
1019 |     let nextCursor = initialResponse.data.next_cursor;
1020 |     
1021 |     // Array for parallel processing
1022 |     const pageRequests = [];
1023 |     const maxParallelRequests = 5; // Limit simultaneous requests
1024 |     
1025 |     if (options?.runInBackground) {
1026 |       console.log(`Retrieved ${results.length} blocks from first page`);
1027 |     }
1028 |     
1029 |     // Request subsequent pages in parallel if available
1030 |     while (nextCursor) {
1031 |       // Clone parameters for next page
1032 |       const nextPageParams = { ...params, start_cursor: nextCursor };
1033 |       
1034 |       // Add page request
1035 |       pageRequests.push(
1036 |         this.httpClient.executeOperation(operation, nextPageParams)
1037 |           .then(response => {
1038 |             if (response.status === 200) {
1039 |               if (options?.runInBackground) {
1040 |                 console.log(`Retrieved ${response.data.results?.length || 0} blocks from additional page`);
1041 |               }
1042 |               return {
1043 |                 results: response.data.results || [],
1044 |                 next_cursor: response.data.next_cursor
1045 |               };
1046 |             }
1047 |             return { results: [], next_cursor: null };
1048 |           })
1049 |           .catch(error => {
1050 |             if (options?.runInBackground) {
1051 |               console.error('Error retrieving page:', error);
1052 |             }
1053 |             return { results: [], next_cursor: null };
1054 |           })
1055 |       );
1056 |       
1057 |       // Execute parallel requests when batch size reached or no more pages
1058 |       if (pageRequests.length >= maxParallelRequests || !nextCursor) {
1059 |         if (options?.runInBackground) {
1060 |           console.log(`Processing ${pageRequests.length} pages in parallel...`);
1061 |         }
1062 |         const pageResponses = await Promise.all(pageRequests);
1063 |         
1064 |         // Merge results
1065 |         for (const response of pageResponses) {
1066 |           results.push(...response.results);
1067 |           // Set next cursor for next batch
1068 |           if (response.next_cursor) {
1069 |             nextCursor = response.next_cursor;
1070 |           } else {
1071 |             nextCursor = null;
1072 |           }
1073 |         }
1074 |         
1075 |         // Reset request array
1076 |         pageRequests.length = 0;
1077 |       }
1078 |       
1079 |       // Exit loop if no more pages
1080 |       if (!nextCursor) break;
1081 |     }
1082 |     
1083 |     if (options?.runInBackground) {
1084 |       console.log(`Retrieved ${results.length} blocks in total`);
1085 |     }
1086 |     
1087 |     // Return merged response
1088 |     const mergedResponse = {
1089 |       ...initialResponse.data,
1090 |       results,
1091 |       has_more: false,
1092 |       next_cursor: null
1093 |     };
1094 |     
1095 |     return {
1096 |       content: [{ type: 'text', text: JSON.stringify(mergedResponse) }],
1097 |     };
1098 |   }
1099 | 
1100 |   private findOperation(operationId: string): (OpenAPIV3.OperationObject & { method: string; path: string }) | null {
1101 |     return this.openApiLookup[operationId] ?? null
1102 |   }
1103 | 
1104 |   private parseHeadersFromEnv(): Record<string, string> {
1105 |     const headersJson = process.env.OPENAPI_MCP_HEADERS
1106 |     if (!headersJson) {
1107 |       return {}
1108 |     }
1109 | 
1110 |     try {
1111 |       const headers = JSON.parse(headersJson)
1112 |       if (typeof headers !== 'object' || headers === null) {
1113 |         console.warn('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', typeof headers)
1114 |         return {}
1115 |       }
1116 |       return headers
1117 |     } catch (error) {
1118 |       console.warn('Failed to parse OPENAPI_MCP_HEADERS environment variable:', error)
1119 |       return {}
1120 |     }
1121 |   }
1122 | 
1123 |   private getContentType(headers: Headers): 'text' | 'image' | 'binary' {
1124 |     const contentType = headers.get('content-type')
1125 |     if (!contentType) return 'binary'
1126 | 
1127 |     if (contentType.includes('text') || contentType.includes('json')) {
1128 |       return 'text'
1129 |     } else if (contentType.includes('image')) {
1130 |       return 'image'
1131 |     }
1132 |     return 'binary'
1133 |   }
1134 | 
1135 |   private truncateToolName(name: string): string {
1136 |     if (name.length <= 64) {
1137 |       return name;
1138 |     }
1139 |     return name.slice(0, 64);
1140 |   }
1141 | 
1142 |   async connect(transport: Transport) {
1143 |     console.log('One Pager Assistant - MCP server started')
1144 |     console.log('Providing APIs: retrieve-a-page, get-block-children, retrieve-a-block')
1145 |     console.log('New feature: get-one-pager - recursively explore pages automatically')
1146 |     console.log('Parallel processing optimization enabled')
1147 |     
1148 |     // The SDK will handle stdio communication
1149 |     await this.server.connect(transport)
1150 |   }
1151 | 
1152 |   getServer() {
1153 |     return this.server
1154 |   }
1155 | }
1156 | 
```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/openapi/__tests__/parser.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { OpenAPIToMCPConverter } from '../parser'
   2 | import { OpenAPIV3 } from 'openapi-types'
   3 | import { describe, expect, it } from 'vitest'
   4 | import { JSONSchema7 as IJsonSchema } from 'json-schema'
   5 | 
   6 | interface ToolMethod {
   7 |   name: string
   8 |   description: string
   9 |   inputSchema: any
  10 |   returnSchema?: any
  11 | }
  12 | 
  13 | interface Tool {
  14 |   methods: ToolMethod[]
  15 | }
  16 | 
  17 | interface Tools {
  18 |   [key: string]: Tool
  19 | }
  20 | 
  21 | // Helper function to verify tool method structure without checking the exact Zod schema
  22 | function verifyToolMethod(actual: ToolMethod, expected: any, toolName: string) {
  23 |   expect(actual.name).toBe(expected.name)
  24 |   expect(actual.description).toBe(expected.description)
  25 |   expect(actual.inputSchema, `inputSchema ${actual.name} ${toolName}`).toEqual(expected.inputSchema)
  26 |   if (expected.returnSchema) {
  27 |     expect(actual.returnSchema, `returnSchema ${actual.name} ${toolName}`).toEqual(expected.returnSchema)
  28 |   }
  29 | }
  30 | 
  31 | // Helper function to verify tools structure
  32 | function verifyTools(actual: Tools, expected: any) {
  33 |   expect(Object.keys(actual)).toEqual(Object.keys(expected))
  34 |   for (const [key, value] of Object.entries(actual)) {
  35 |     expect(value.methods.length).toBe(expected[key].methods.length)
  36 |     value.methods.forEach((method: ToolMethod, index: number) => {
  37 |       verifyToolMethod(method, expected[key].methods[index], key)
  38 |     })
  39 |   }
  40 | }
  41 | 
  42 | // A helper function to derive a type from a possibly complex schema.
  43 | // If no explicit type is found, we assume 'object' for testing purposes.
  44 | function getTypeFromSchema(schema: IJsonSchema): string {
  45 |   if (schema.type) {
  46 |     return Array.isArray(schema.type) ? schema.type[0] : schema.type
  47 |   } else if (schema.$ref) {
  48 |     // If there's a $ref, we treat it as an object reference.
  49 |     return 'object'
  50 |   } else if (schema.oneOf || schema.anyOf || schema.allOf) {
  51 |     // Complex schema combos - assume object for these tests.
  52 |     return 'object'
  53 |   }
  54 |   return 'object'
  55 | }
  56 | 
  57 | // Updated helper function to get parameters from inputSchema
  58 | // Now handles $ref by treating it as an object reference without expecting properties.
  59 | function getParamsFromSchema(method: { inputSchema: IJsonSchema }) {
  60 |   return Object.entries(method.inputSchema.properties || {}).map(([name, prop]) => {
  61 |     if (typeof prop === 'boolean') {
  62 |       throw new Error(`Boolean schema not supported for parameter ${name}`)
  63 |     }
  64 | 
  65 |     // If there's a $ref, treat it as an object reference.
  66 |     const schemaType = getTypeFromSchema(prop)
  67 |     return {
  68 |       name,
  69 |       type: schemaType,
  70 |       description: prop.description,
  71 |       optional: !(method.inputSchema.required || []).includes(name),
  72 |     }
  73 |   })
  74 | }
  75 | 
  76 | // Updated helper function to get return type from returnSchema
  77 | // No longer requires that the schema be fully expanded. If we have a $ref, just note it as 'object'.
  78 | function getReturnType(method: { returnSchema?: IJsonSchema }) {
  79 |   if (!method.returnSchema) return null
  80 |   const schema = method.returnSchema
  81 |   return {
  82 |     type: getTypeFromSchema(schema),
  83 |     description: schema.description,
  84 |   }
  85 | }
  86 | 
  87 | describe('OpenAPIToMCPConverter', () => {
  88 |   describe('Simple API Conversion', () => {
  89 |     const sampleSpec: OpenAPIV3.Document = {
  90 |       openapi: '3.0.0',
  91 |       info: {
  92 |         title: 'Test API',
  93 |         version: '1.0.0',
  94 |       },
  95 |       paths: {
  96 |         '/pets/{petId}': {
  97 |           get: {
  98 |             operationId: 'getPet',
  99 |             summary: 'Get a pet by ID',
 100 |             parameters: [
 101 |               {
 102 |                 name: 'petId',
 103 |                 in: 'path',
 104 |                 required: true,
 105 |                 description: 'The ID of the pet',
 106 |                 schema: {
 107 |                   type: 'integer',
 108 |                 },
 109 |               },
 110 |             ],
 111 |             responses: {
 112 |               '200': {
 113 |                 description: 'Pet found',
 114 |                 content: {
 115 |                   'application/json': {
 116 |                     schema: {
 117 |                       type: 'object',
 118 |                       properties: {
 119 |                         id: { type: 'integer' },
 120 |                         name: { type: 'string' },
 121 |                       },
 122 |                     },
 123 |                   },
 124 |                 },
 125 |               },
 126 |             },
 127 |           },
 128 |         },
 129 |       },
 130 |     }
 131 | 
 132 |     it('converts simple OpenAPI paths to MCP tools', () => {
 133 |       const converter = new OpenAPIToMCPConverter(sampleSpec)
 134 |       const { tools, openApiLookup } = converter.convertToMCPTools()
 135 | 
 136 |       expect(tools).toHaveProperty('API')
 137 |       expect(tools.API.methods).toHaveLength(1)
 138 |       expect(Object.keys(openApiLookup)).toHaveLength(1)
 139 | 
 140 |       const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
 141 |       expect(getPetMethod).toBeDefined()
 142 | 
 143 |       const params = getParamsFromSchema(getPetMethod!)
 144 |       expect(params).toContainEqual({
 145 |         name: 'petId',
 146 |         type: 'integer',
 147 |         description: 'The ID of the pet',
 148 |         optional: false,
 149 |       })
 150 |     })
 151 | 
 152 |     it('truncates tool names exceeding 64 characters', () => {
 153 |       const longOperationId = 'a'.repeat(65)
 154 |       const specWithLongName: OpenAPIV3.Document = {
 155 |         openapi: '3.0.0',
 156 |         info: {
 157 |           title: 'Test API',
 158 |           version: '1.0.0'
 159 |         },
 160 |         paths: {
 161 |           '/pets/{petId}': {
 162 |             get: {
 163 |               operationId: longOperationId,
 164 |               summary: 'Get a pet by ID',
 165 |               parameters: [
 166 |                 {
 167 |                   name: 'petId',
 168 |                   in: 'path',
 169 |                   required: true,
 170 |                   description: 'The ID of the pet',
 171 |                   schema: {
 172 |                     type: 'integer'
 173 |                   }
 174 |                 }
 175 |               ],
 176 |               responses: {
 177 |                 '200': {
 178 |                   description: 'Pet found',
 179 |                   content: {
 180 |                     'application/json': {
 181 |                       schema: {
 182 |                         type: 'object',
 183 |                         properties: {
 184 |                           id: { type: 'integer' },
 185 |                           name: { type: 'string' }
 186 |                         }
 187 |                       }
 188 |                     }
 189 |                   }
 190 |                 }
 191 |               }
 192 |             }
 193 |           }
 194 |         }
 195 |       }
 196 | 
 197 |       const converter = new OpenAPIToMCPConverter(specWithLongName)
 198 |       const { tools } = converter.convertToMCPTools()
 199 | 
 200 |       const longNameMethod = tools.API.methods.find(m => m.name.startsWith('a'.repeat(59)))
 201 |       expect(longNameMethod).toBeDefined()
 202 |       expect(longNameMethod!.name.length).toBeLessThanOrEqual(64)
 203 |     })
 204 |   })
 205 | 
 206 |   describe('Complex API Conversion', () => {
 207 |     const complexSpec: OpenAPIV3.Document = {
 208 |       openapi: '3.0.0',
 209 |       info: { title: 'Complex API', version: '1.0.0' },
 210 |       components: {
 211 |         schemas: {
 212 |           Error: {
 213 |             type: 'object',
 214 |             required: ['code', 'message'],
 215 |             properties: {
 216 |               code: { type: 'integer' },
 217 |               message: { type: 'string' },
 218 |             },
 219 |           },
 220 |           Pet: {
 221 |             type: 'object',
 222 |             required: ['id', 'name'],
 223 |             properties: {
 224 |               id: { type: 'integer', description: 'The ID of the pet' },
 225 |               name: { type: 'string', description: 'The name of the pet' },
 226 |               category: { $ref: '#/components/schemas/Category', description: 'The category of the pet' },
 227 |               tags: {
 228 |                 type: 'array',
 229 |                 description: 'The tags of the pet',
 230 |                 items: { $ref: '#/components/schemas/Tag' },
 231 |               },
 232 |               status: {
 233 |                 type: 'string',
 234 |                 description: 'The status of the pet',
 235 |                 enum: ['available', 'pending', 'sold'],
 236 |               },
 237 |             },
 238 |           },
 239 |           Category: {
 240 |             type: 'object',
 241 |             required: ['id', 'name'],
 242 |             properties: {
 243 |               id: { type: 'integer' },
 244 |               name: { type: 'string' },
 245 |               subcategories: {
 246 |                 type: 'array',
 247 |                 items: { $ref: '#/components/schemas/Category' },
 248 |               },
 249 |             },
 250 |           },
 251 |           Tag: {
 252 |             type: 'object',
 253 |             required: ['id', 'name'],
 254 |             properties: {
 255 |               id: { type: 'integer' },
 256 |               name: { type: 'string' },
 257 |             },
 258 |           },
 259 |         },
 260 |         parameters: {
 261 |           PetId: {
 262 |             name: 'petId',
 263 |             in: 'path',
 264 |             required: true,
 265 |             description: 'ID of pet to fetch',
 266 |             schema: { type: 'integer' },
 267 |           },
 268 |           QueryLimit: {
 269 |             name: 'limit',
 270 |             in: 'query',
 271 |             description: 'Maximum number of results to return',
 272 |             schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
 273 |           },
 274 |         },
 275 |         responses: {
 276 |           NotFound: {
 277 |             description: 'The specified resource was not found',
 278 |             content: {
 279 |               'application/json': {
 280 |                 schema: { $ref: '#/components/schemas/Error' },
 281 |               },
 282 |             },
 283 |           },
 284 |         },
 285 |       },
 286 |       paths: {
 287 |         '/pets': {
 288 |           get: {
 289 |             operationId: 'listPets',
 290 |             summary: 'List all pets',
 291 |             parameters: [{ $ref: '#/components/parameters/QueryLimit' }],
 292 |             responses: {
 293 |               '200': {
 294 |                 description: 'A list of pets',
 295 |                 content: {
 296 |                   'application/json': {
 297 |                     schema: {
 298 |                       type: 'array',
 299 |                       items: { $ref: '#/components/schemas/Pet' },
 300 |                     },
 301 |                   },
 302 |                 },
 303 |               },
 304 |             },
 305 |           },
 306 |           post: {
 307 |             operationId: 'createPet',
 308 |             summary: 'Create a pet',
 309 |             requestBody: {
 310 |               required: true,
 311 |               content: {
 312 |                 'application/json': {
 313 |                   schema: { $ref: '#/components/schemas/Pet' },
 314 |                 },
 315 |               },
 316 |             },
 317 |             responses: {
 318 |               '201': {
 319 |                 description: 'Pet created',
 320 |                 content: {
 321 |                   'application/json': {
 322 |                     schema: { $ref: '#/components/schemas/Pet' },
 323 |                   },
 324 |                 },
 325 |               },
 326 |             },
 327 |           },
 328 |         },
 329 |         '/pets/{petId}': {
 330 |           get: {
 331 |             operationId: 'getPet',
 332 |             summary: 'Get a pet by ID',
 333 |             parameters: [{ $ref: '#/components/parameters/PetId' }],
 334 |             responses: {
 335 |               '200': {
 336 |                 description: 'Pet found',
 337 |                 content: {
 338 |                   'application/json': {
 339 |                     schema: { $ref: '#/components/schemas/Pet' },
 340 |                   },
 341 |                 },
 342 |               },
 343 |               '404': {
 344 |                 $ref: '#/components/responses/NotFound',
 345 |               },
 346 |             },
 347 |           },
 348 |           put: {
 349 |             operationId: 'updatePet',
 350 |             summary: 'Update a pet',
 351 |             parameters: [{ $ref: '#/components/parameters/PetId' }],
 352 |             requestBody: {
 353 |               required: true,
 354 |               content: {
 355 |                 'application/json': {
 356 |                   schema: { $ref: '#/components/schemas/Pet' },
 357 |                 },
 358 |               },
 359 |             },
 360 |             responses: {
 361 |               '200': {
 362 |                 description: 'Pet updated',
 363 |                 content: {
 364 |                   'application/json': {
 365 |                     schema: { $ref: '#/components/schemas/Pet' },
 366 |                   },
 367 |                 },
 368 |               },
 369 |               '404': {
 370 |                 $ref: '#/components/responses/NotFound',
 371 |               },
 372 |             },
 373 |           },
 374 |         },
 375 |       },
 376 |     }
 377 | 
 378 |     it('converts operations with referenced parameters', () => {
 379 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 380 |       const { tools } = converter.convertToMCPTools()
 381 | 
 382 |       const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
 383 |       expect(getPetMethod).toBeDefined()
 384 |       const params = getParamsFromSchema(getPetMethod!)
 385 |       expect(params).toContainEqual({
 386 |         name: 'petId',
 387 |         type: 'integer',
 388 |         description: 'ID of pet to fetch',
 389 |         optional: false,
 390 |       })
 391 |     })
 392 | 
 393 |     it('converts operations with query parameters', () => {
 394 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 395 |       const { tools } = converter.convertToMCPTools()
 396 | 
 397 |       const listPetsMethod = tools.API.methods.find((m) => m.name === 'listPets')
 398 |       expect(listPetsMethod).toBeDefined()
 399 | 
 400 |       const params = getParamsFromSchema(listPetsMethod!)
 401 |       expect(params).toContainEqual({
 402 |         name: 'limit',
 403 |         type: 'integer',
 404 |         description: 'Maximum number of results to return',
 405 |         optional: true,
 406 |       })
 407 |     })
 408 | 
 409 |     it('converts operations with array responses', () => {
 410 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 411 |       const { tools } = converter.convertToMCPTools()
 412 | 
 413 |       const listPetsMethod = tools.API.methods.find((m) => m.name === 'listPets')
 414 |       expect(listPetsMethod).toBeDefined()
 415 | 
 416 |       const returnType = getReturnType(listPetsMethod!)
 417 |       // Now we only check type since description might not be carried through
 418 |       // if we are not expanding schemas.
 419 |       expect(returnType).toMatchObject({
 420 |         type: 'array',
 421 |       })
 422 |     })
 423 | 
 424 |     it('converts operations with request bodies using $ref', () => {
 425 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 426 |       const { tools } = converter.convertToMCPTools()
 427 | 
 428 |       const createPetMethod = tools.API.methods.find((m) => m.name === 'createPet')
 429 |       expect(createPetMethod).toBeDefined()
 430 | 
 431 |       const params = getParamsFromSchema(createPetMethod!)
 432 |       // Now that we are preserving $ref, the request body won't be expanded into multiple parameters.
 433 |       // Instead, we'll have a single "body" parameter referencing Pet.
 434 |       expect(params).toEqual(
 435 |         expect.arrayContaining([
 436 |           expect.objectContaining({
 437 |             name: 'body',
 438 |             type: 'object', // Because it's a $ref
 439 |             optional: false,
 440 |           }),
 441 |         ]),
 442 |       )
 443 |     })
 444 | 
 445 |     it('converts operations with referenced error responses', () => {
 446 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 447 |       const { tools } = converter.convertToMCPTools()
 448 | 
 449 |       const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
 450 |       expect(getPetMethod).toBeDefined()
 451 | 
 452 |       // We just check that the description includes the error references now.
 453 |       expect(getPetMethod?.description).toContain('404: The specified resource was not found')
 454 |     })
 455 | 
 456 |     it('handles recursive schema references without expanding them', () => {
 457 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 458 |       const { tools } = converter.convertToMCPTools()
 459 | 
 460 |       const createPetMethod = tools.API.methods.find((m) => m.name === 'createPet')
 461 |       expect(createPetMethod).toBeDefined()
 462 | 
 463 |       const params = getParamsFromSchema(createPetMethod!)
 464 |       // Since "category" would be inside Pet, and we're not expanding,
 465 |       // we won't see 'category' directly. We only have 'body' as a reference.
 466 |       // Thus, the test no longer checks for a direct 'category' param.
 467 |       expect(params.find((p) => p.name === 'body')).toBeDefined()
 468 |     })
 469 | 
 470 |     it('converts all operations correctly respecting $ref usage', () => {
 471 |       const converter = new OpenAPIToMCPConverter(complexSpec)
 472 |       const { tools } = converter.convertToMCPTools()
 473 | 
 474 |       expect(tools.API.methods).toHaveLength(4)
 475 | 
 476 |       const methodNames = tools.API.methods.map((m) => m.name)
 477 |       expect(methodNames).toEqual(expect.arrayContaining(['listPets', 'createPet', 'getPet', 'updatePet']))
 478 | 
 479 |       tools.API.methods.forEach((method) => {
 480 |         expect(method).toHaveProperty('name')
 481 |         expect(method).toHaveProperty('description')
 482 |         expect(method).toHaveProperty('inputSchema')
 483 |         expect(method).toHaveProperty('returnSchema')
 484 | 
 485 |         // For 'get' operations, we just check the return type is recognized correctly.
 486 |         if (method.name.startsWith('get')) {
 487 |           const returnType = getReturnType(method)
 488 |           // With $ref usage, we can't guarantee description or direct expansion.
 489 |           expect(returnType?.type).toBe('object')
 490 |         }
 491 |       })
 492 |     })
 493 |   })
 494 | 
 495 |   describe('Complex Schema Conversion', () => {
 496 |     // A similar approach for the nested spec
 497 |     // Just as in the previous tests, we no longer test for direct property expansion.
 498 |     // We only confirm that parameters and return types are recognized and that references are preserved.
 499 | 
 500 |     const nestedSpec: OpenAPIV3.Document = {
 501 |       openapi: '3.0.0',
 502 |       info: { title: 'Nested API', version: '1.0.0' },
 503 |       components: {
 504 |         schemas: {
 505 |           Organization: {
 506 |             type: 'object',
 507 |             required: ['id', 'name'],
 508 |             properties: {
 509 |               id: { type: 'integer' },
 510 |               name: { type: 'string' },
 511 |               departments: {
 512 |                 type: 'array',
 513 |                 items: { $ref: '#/components/schemas/Department' },
 514 |               },
 515 |               metadata: { $ref: '#/components/schemas/Metadata' },
 516 |             },
 517 |           },
 518 |           Department: {
 519 |             type: 'object',
 520 |             required: ['id', 'name'],
 521 |             properties: {
 522 |               id: { type: 'integer' },
 523 |               name: { type: 'string' },
 524 |               employees: {
 525 |                 type: 'array',
 526 |                 items: { $ref: '#/components/schemas/Employee' },
 527 |               },
 528 |               subDepartments: {
 529 |                 type: 'array',
 530 |                 items: { $ref: '#/components/schemas/Department' },
 531 |               },
 532 |               metadata: { $ref: '#/components/schemas/Metadata' },
 533 |             },
 534 |           },
 535 |           Employee: {
 536 |             type: 'object',
 537 |             required: ['id', 'name'],
 538 |             properties: {
 539 |               id: { type: 'integer' },
 540 |               name: { type: 'string' },
 541 |               role: { $ref: '#/components/schemas/Role' },
 542 |               skills: {
 543 |                 type: 'array',
 544 |                 items: { $ref: '#/components/schemas/Skill' },
 545 |               },
 546 |               metadata: { $ref: '#/components/schemas/Metadata' },
 547 |             },
 548 |           },
 549 |           Role: {
 550 |             type: 'object',
 551 |             required: ['id', 'name'],
 552 |             properties: {
 553 |               id: { type: 'integer' },
 554 |               name: { type: 'string' },
 555 |               permissions: {
 556 |                 type: 'array',
 557 |                 items: { $ref: '#/components/schemas/Permission' },
 558 |               },
 559 |             },
 560 |           },
 561 |           Permission: {
 562 |             type: 'object',
 563 |             required: ['id', 'name'],
 564 |             properties: {
 565 |               id: { type: 'integer' },
 566 |               name: { type: 'string' },
 567 |               scope: { type: 'string' },
 568 |             },
 569 |           },
 570 |           Skill: {
 571 |             type: 'object',
 572 |             required: ['id', 'name'],
 573 |             properties: {
 574 |               id: { type: 'integer' },
 575 |               name: { type: 'string' },
 576 |               level: {
 577 |                 type: 'string',
 578 |                 enum: ['beginner', 'intermediate', 'expert'],
 579 |               },
 580 |             },
 581 |           },
 582 |           Metadata: {
 583 |             type: 'object',
 584 |             properties: {
 585 |               createdAt: { type: 'string', format: 'date-time' },
 586 |               updatedAt: { type: 'string', format: 'date-time' },
 587 |               tags: {
 588 |                 type: 'array',
 589 |                 items: { type: 'string' },
 590 |               },
 591 |               customFields: {
 592 |                 type: 'object',
 593 |                 additionalProperties: true,
 594 |               },
 595 |             },
 596 |           },
 597 |         },
 598 |         parameters: {
 599 |           OrgId: {
 600 |             name: 'orgId',
 601 |             in: 'path',
 602 |             required: true,
 603 |             description: 'Organization ID',
 604 |             schema: { type: 'integer' },
 605 |           },
 606 |           DeptId: {
 607 |             name: 'deptId',
 608 |             in: 'path',
 609 |             required: true,
 610 |             description: 'Department ID',
 611 |             schema: { type: 'integer' },
 612 |           },
 613 |           IncludeMetadata: {
 614 |             name: 'includeMetadata',
 615 |             in: 'query',
 616 |             description: 'Include metadata in response',
 617 |             schema: { type: 'boolean', default: false },
 618 |           },
 619 |           Depth: {
 620 |             name: 'depth',
 621 |             in: 'query',
 622 |             description: 'Depth of nested objects to return',
 623 |             schema: { type: 'integer', minimum: 1, maximum: 5, default: 1 },
 624 |           },
 625 |         },
 626 |       },
 627 |       paths: {
 628 |         '/organizations/{orgId}': {
 629 |           get: {
 630 |             operationId: 'getOrganization',
 631 |             summary: 'Get organization details',
 632 |             parameters: [
 633 |               { $ref: '#/components/parameters/OrgId' },
 634 |               { $ref: '#/components/parameters/IncludeMetadata' },
 635 |               { $ref: '#/components/parameters/Depth' },
 636 |             ],
 637 |             responses: {
 638 |               '200': {
 639 |                 description: 'Organization details',
 640 |                 content: {
 641 |                   'application/json': {
 642 |                     schema: { $ref: '#/components/schemas/Organization' },
 643 |                   },
 644 |                 },
 645 |               },
 646 |             },
 647 |           },
 648 |         },
 649 |         '/organizations/{orgId}/departments/{deptId}': {
 650 |           get: {
 651 |             operationId: 'getDepartment',
 652 |             summary: 'Get department details',
 653 |             parameters: [
 654 |               { $ref: '#/components/parameters/OrgId' },
 655 |               { $ref: '#/components/parameters/DeptId' },
 656 |               { $ref: '#/components/parameters/IncludeMetadata' },
 657 |               { $ref: '#/components/parameters/Depth' },
 658 |             ],
 659 |             responses: {
 660 |               '200': {
 661 |                 description: 'Department details',
 662 |                 content: {
 663 |                   'application/json': {
 664 |                     schema: { $ref: '#/components/schemas/Department' },
 665 |                   },
 666 |                 },
 667 |               },
 668 |             },
 669 |           },
 670 |           put: {
 671 |             operationId: 'updateDepartment',
 672 |             summary: 'Update department details',
 673 |             parameters: [{ $ref: '#/components/parameters/OrgId' }, { $ref: '#/components/parameters/DeptId' }],
 674 |             requestBody: {
 675 |               required: true,
 676 |               content: {
 677 |                 'application/json': {
 678 |                   schema: { $ref: '#/components/schemas/Department' },
 679 |                 },
 680 |               },
 681 |             },
 682 |             responses: {
 683 |               '200': {
 684 |                 description: 'Department updated',
 685 |                 content: {
 686 |                   'application/json': {
 687 |                     schema: { $ref: '#/components/schemas/Department' },
 688 |                   },
 689 |                 },
 690 |               },
 691 |             },
 692 |           },
 693 |         },
 694 |       },
 695 |     }
 696 | 
 697 |     it('handles deeply nested object references', () => {
 698 |       const converter = new OpenAPIToMCPConverter(nestedSpec)
 699 |       const { tools } = converter.convertToMCPTools()
 700 | 
 701 |       const getOrgMethod = tools.API.methods.find((m) => m.name === 'getOrganization')
 702 |       expect(getOrgMethod).toBeDefined()
 703 | 
 704 |       const params = getParamsFromSchema(getOrgMethod!)
 705 |       expect(params).toEqual(
 706 |         expect.arrayContaining([
 707 |           expect.objectContaining({
 708 |             name: 'orgId',
 709 |             type: 'integer',
 710 |             description: 'Organization ID',
 711 |             optional: false,
 712 |           }),
 713 |           expect.objectContaining({
 714 |             name: 'includeMetadata',
 715 |             type: 'boolean',
 716 |             description: 'Include metadata in response',
 717 |             optional: true,
 718 |           }),
 719 |           expect.objectContaining({
 720 |             name: 'depth',
 721 |             type: 'integer',
 722 |             description: 'Depth of nested objects to return',
 723 |             optional: true,
 724 |           }),
 725 |         ]),
 726 |       )
 727 |     })
 728 | 
 729 |     it('handles recursive array references without requiring expansion', () => {
 730 |       const converter = new OpenAPIToMCPConverter(nestedSpec)
 731 |       const { tools } = converter.convertToMCPTools()
 732 | 
 733 |       const updateDeptMethod = tools.API.methods.find((m) => m.name === 'updateDepartment')
 734 |       expect(updateDeptMethod).toBeDefined()
 735 | 
 736 |       const params = getParamsFromSchema(updateDeptMethod!)
 737 |       // With $ref usage, we have a body parameter referencing Department.
 738 |       // The subDepartments array is inside Department, so we won't see it expanded here.
 739 |       // Instead, we just confirm 'body' is present.
 740 |       const bodyParam = params.find((p) => p.name === 'body')
 741 |       expect(bodyParam).toBeDefined()
 742 |       expect(bodyParam?.type).toBe('object')
 743 |     })
 744 | 
 745 |     it('handles complex nested object hierarchies without expansion', () => {
 746 |       const converter = new OpenAPIToMCPConverter(nestedSpec)
 747 |       const { tools } = converter.convertToMCPTools()
 748 | 
 749 |       const getDeptMethod = tools.API.methods.find((m) => m.name === 'getDepartment')
 750 |       expect(getDeptMethod).toBeDefined()
 751 | 
 752 |       const params = getParamsFromSchema(getDeptMethod!)
 753 |       // Just checking top-level params:
 754 |       expect(params).toEqual(
 755 |         expect.arrayContaining([
 756 |           expect.objectContaining({
 757 |             name: 'orgId',
 758 |             type: 'integer',
 759 |             optional: false,
 760 |           }),
 761 |           expect.objectContaining({
 762 |             name: 'deptId',
 763 |             type: 'integer',
 764 |             optional: false,
 765 |           }),
 766 |           expect.objectContaining({
 767 |             name: 'includeMetadata',
 768 |             type: 'boolean',
 769 |             optional: true,
 770 |           }),
 771 |           expect.objectContaining({
 772 |             name: 'depth',
 773 |             type: 'integer',
 774 |             optional: true,
 775 |           }),
 776 |         ]),
 777 |       )
 778 |     })
 779 | 
 780 |     it('handles schema with mixed primitive and reference types without expansion', () => {
 781 |       const converter = new OpenAPIToMCPConverter(nestedSpec)
 782 |       const { tools } = converter.convertToMCPTools()
 783 | 
 784 |       const updateDeptMethod = tools.API.methods.find((m) => m.name === 'updateDepartment')
 785 |       expect(updateDeptMethod).toBeDefined()
 786 | 
 787 |       const params = getParamsFromSchema(updateDeptMethod!)
 788 |       // Since we are not expanding, we won't see metadata fields directly.
 789 |       // We just confirm 'body' referencing Department is there.
 790 |       expect(params.find((p) => p.name === 'body')).toBeDefined()
 791 |     })
 792 | 
 793 |     it('converts all operations with complex schemas correctly respecting $ref', () => {
 794 |       const converter = new OpenAPIToMCPConverter(nestedSpec)
 795 |       const { tools } = converter.convertToMCPTools()
 796 | 
 797 |       expect(tools.API.methods).toHaveLength(3)
 798 | 
 799 |       const methodNames = tools.API.methods.map((m) => m.name)
 800 |       expect(methodNames).toEqual(expect.arrayContaining(['getOrganization', 'getDepartment', 'updateDepartment']))
 801 | 
 802 |       tools.API.methods.forEach((method) => {
 803 |         expect(method).toHaveProperty('name')
 804 |         expect(method).toHaveProperty('description')
 805 |         expect(method).toHaveProperty('inputSchema')
 806 |         expect(method).toHaveProperty('returnSchema')
 807 | 
 808 |         // If it's a GET operation, check that return type is recognized.
 809 |         if (method.name.startsWith('get')) {
 810 |           const returnType = getReturnType(method)
 811 |           // Without expansion, just check type is recognized as object.
 812 |           expect(returnType).toMatchObject({
 813 |             type: 'object',
 814 |           })
 815 |         }
 816 |       })
 817 |     })
 818 |   })
 819 | 
 820 |   it('preserves description on $ref nodes', () => {
 821 |     const spec: OpenAPIV3.Document = {
 822 |       openapi: '3.0.0',
 823 |       info: { title: 'Test API', version: '1.0.0' },
 824 |       paths: {},
 825 |       components: {
 826 |         schemas: {
 827 |           TestSchema: {
 828 |             type: 'object',
 829 |             properties: {
 830 |               name: { type: 'string' },
 831 |             },
 832 |           },
 833 |         },
 834 |       },
 835 |     }
 836 | 
 837 |     const converter = new OpenAPIToMCPConverter(spec)
 838 |     const result = converter.convertOpenApiSchemaToJsonSchema(
 839 |       {
 840 |         $ref: '#/components/schemas/TestSchema',
 841 |         description: 'A schema description',
 842 |       },
 843 |       new Set(),
 844 |     )
 845 | 
 846 |     expect(result).toEqual({
 847 |       $ref: '#/$defs/TestSchema',
 848 |       description: 'A schema description',
 849 |     })
 850 |   })
 851 | })
 852 | 
 853 | // Additional complex test scenarios as a table test
 854 | describe('OpenAPIToMCPConverter - Additional Complex Tests', () => {
 855 |   interface TestCase {
 856 |     name: string
 857 |     input: OpenAPIV3.Document
 858 |     expected: {
 859 |       tools: Record<
 860 |         string,
 861 |         {
 862 |           methods: Array<{
 863 |             name: string
 864 |             description: string
 865 |             inputSchema: IJsonSchema & { type: 'object' }
 866 |             returnSchema?: IJsonSchema
 867 |           }>
 868 |         }
 869 |       >
 870 |       openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
 871 |     }
 872 |   }
 873 | 
 874 |   const cases: TestCase[] = [
 875 |     {
 876 |       name: 'Cyclic References with Full Descriptions',
 877 |       input: {
 878 |         openapi: '3.0.0',
 879 |         info: {
 880 |           title: 'Cyclic Test API',
 881 |           version: '1.0.0',
 882 |         },
 883 |         paths: {
 884 |           '/ab': {
 885 |             get: {
 886 |               operationId: 'getAB',
 887 |               summary: 'Get an A-B object',
 888 |               responses: {
 889 |                 '200': {
 890 |                   description: 'Returns an A object',
 891 |                   content: {
 892 |                     'application/json': {
 893 |                       schema: { $ref: '#/components/schemas/A' },
 894 |                     },
 895 |                   },
 896 |                 },
 897 |               },
 898 |             },
 899 |             post: {
 900 |               operationId: 'createAB',
 901 |               summary: 'Create an A-B object',
 902 |               requestBody: {
 903 |                 required: true,
 904 |                 content: {
 905 |                   'application/json': {
 906 |                     schema: {
 907 |                       $ref: '#/components/schemas/A',
 908 |                       description: 'A schema description',
 909 |                     },
 910 |                   },
 911 |                 },
 912 |               },
 913 |               responses: {
 914 |                 '201': {
 915 |                   description: 'Created A object',
 916 |                   content: {
 917 |                     'application/json': {
 918 |                       schema: { $ref: '#/components/schemas/A' },
 919 |                     },
 920 |                   },
 921 |                 },
 922 |               },
 923 |             },
 924 |           },
 925 |         },
 926 |         components: {
 927 |           schemas: {
 928 |             A: {
 929 |               type: 'object',
 930 |               description: 'A schema description',
 931 |               required: ['name', 'b'],
 932 |               properties: {
 933 |                 name: {
 934 |                   type: 'string',
 935 |                   description: 'Name of A',
 936 |                 },
 937 |                 b: {
 938 |                   $ref: '#/components/schemas/B',
 939 |                   description: 'B property in A',
 940 |                 },
 941 |               },
 942 |             },
 943 |             B: {
 944 |               type: 'object',
 945 |               description: 'B schema description',
 946 |               required: ['title', 'a'],
 947 |               properties: {
 948 |                 title: {
 949 |                   type: 'string',
 950 |                   description: 'Title of B',
 951 |                 },
 952 |                 a: {
 953 |                   $ref: '#/components/schemas/A',
 954 |                   description: 'A property in B',
 955 |                 },
 956 |               },
 957 |             },
 958 |           },
 959 |         },
 960 |       } as OpenAPIV3.Document,
 961 |       expected: {
 962 |         tools: {
 963 |           API: {
 964 |             methods: [
 965 |               {
 966 |                 name: 'getAB',
 967 |                 description: 'Get an A-B object',
 968 |                 // Error responses might not be listed here since none are defined.
 969 |                 // Just end the description with no Error Responses section.
 970 |                 inputSchema: {
 971 |                   type: 'object',
 972 |                   properties: {},
 973 |                   required: [],
 974 |                   $defs: {
 975 |                     A: {
 976 |                       type: 'object',
 977 |                       description: 'A schema description',
 978 |                       additionalProperties: true,
 979 |                       properties: {
 980 |                         name: {
 981 |                           type: 'string',
 982 |                           description: 'Name of A',
 983 |                         },
 984 |                         b: {
 985 |                           description: 'B property in A',
 986 |                           $ref: '#/$defs/B',
 987 |                         },
 988 |                       },
 989 |                       required: ['name', 'b'],
 990 |                     },
 991 |                     B: {
 992 |                       type: 'object',
 993 |                       description: 'B schema description',
 994 |                       additionalProperties: true,
 995 |                       properties: {
 996 |                         title: {
 997 |                           type: 'string',
 998 |                           description: 'Title of B',
 999 |                         },
1000 |                         a: {
1001 |                           description: 'A property in B',
1002 |                           $ref: '#/$defs/A',
1003 |                         },
1004 |                       },
1005 |                       required: ['title', 'a'],
1006 |                     },
1007 |                   },
1008 |                 },
1009 |                 returnSchema: {
1010 |                   $ref: '#/$defs/A',
1011 |                   description: 'Returns an A object',
1012 |                   $defs: {
1013 |                     A: {
1014 |                       type: 'object',
1015 |                       description: 'A schema description',
1016 |                       additionalProperties: true,
1017 |                       properties: {
1018 |                         name: {
1019 |                           type: 'string',
1020 |                           description: 'Name of A',
1021 |                         },
1022 |                         b: {
1023 |                           description: 'B property in A',
1024 |                           $ref: '#/$defs/B',
1025 |                         },
1026 |                       },
1027 |                       required: ['name', 'b'],
1028 |                     },
1029 |                     B: {
1030 |                       type: 'object',
1031 |                       description: 'B schema description',
1032 |                       additionalProperties: true,
1033 |                       properties: {
1034 |                         title: {
1035 |                           type: 'string',
1036 |                           description: 'Title of B',
1037 |                         },
1038 |                         a: {
1039 |                           description: 'A property in B',
1040 |                           $ref: '#/$defs/A',
1041 |                         },
1042 |                       },
1043 |                       required: ['title', 'a'],
1044 |                     },
1045 |                   },
1046 |                 },
1047 |               },
1048 |               {
1049 |                 name: 'createAB',
1050 |                 description: 'Create an A-B object',
1051 |                 inputSchema: {
1052 |                   type: 'object',
1053 |                   properties: {
1054 |                     // The requestBody references A. We keep it as a single body field with a $ref.
1055 |                     body: {
1056 |                       $ref: '#/$defs/A',
1057 |                       description: 'A schema description',
1058 |                     },
1059 |                   },
1060 |                   required: ['body'],
1061 | 
1062 |                   $defs: {
1063 |                     A: {
1064 |                       type: 'object',
1065 |                       description: 'A schema description',
1066 |                       additionalProperties: true,
1067 |                       properties: {
1068 |                         name: {
1069 |                           type: 'string',
1070 |                           description: 'Name of A',
1071 |                         },
1072 |                         b: {
1073 |                           description: 'B property in A',
1074 |                           $ref: '#/$defs/B',
1075 |                         },
1076 |                       },
1077 |                       required: ['name', 'b'],
1078 |                     },
1079 |                     B: {
1080 |                       type: 'object',
1081 |                       description: 'B schema description',
1082 |                       additionalProperties: true,
1083 |                       properties: {
1084 |                         title: {
1085 |                           type: 'string',
1086 |                           description: 'Title of B',
1087 |                         },
1088 |                         a: {
1089 |                           description: 'A property in B',
1090 |                           $ref: '#/$defs/A',
1091 |                         },
1092 |                       },
1093 |                       required: ['title', 'a'],
1094 |                     },
1095 |                   },
1096 |                 },
1097 |                 returnSchema: {
1098 |                   $ref: '#/$defs/A',
1099 |                   description: 'Created A object',
1100 | 
1101 |                   $defs: {
1102 |                     A: {
1103 |                       type: 'object',
1104 |                       description: 'A schema description',
1105 |                       additionalProperties: true,
1106 |                       properties: {
1107 |                         name: {
1108 |                           type: 'string',
1109 |                           description: 'Name of A',
1110 |                         },
1111 |                         b: {
1112 |                           description: 'B property in A',
1113 |                           $ref: '#/$defs/B',
1114 |                         },
1115 |                       },
1116 |                       required: ['name', 'b'],
1117 |                     },
1118 |                     B: {
1119 |                       type: 'object',
1120 |                       description: 'B schema description',
1121 |                       additionalProperties: true,
1122 |                       properties: {
1123 |                         title: {
1124 |                           type: 'string',
1125 |                           description: 'Title of B',
1126 |                         },
1127 |                         a: {
1128 |                           description: 'A property in B',
1129 |                           $ref: '#/$defs/A',
1130 |                         },
1131 |                       },
1132 |                       required: ['title', 'a'],
1133 |                     },
1134 |                   },
1135 |                 },
1136 |               },
1137 |             ],
1138 |           },
1139 |         },
1140 |         openApiLookup: {
1141 |           'API-getAB': {
1142 |             operationId: 'getAB',
1143 |             summary: 'Get an A-B object',
1144 |             responses: {
1145 |               '200': {
1146 |                 description: 'Returns an A object',
1147 |                 content: {
1148 |                   'application/json': {
1149 |                     schema: { $ref: '#/components/schemas/A' },
1150 |                   },
1151 |                 },
1152 |               },
1153 |             },
1154 |             method: 'get',
1155 |             path: '/ab',
1156 |           },
1157 |           'API-createAB': {
1158 |             operationId: 'createAB',
1159 |             summary: 'Create an A-B object',
1160 |             requestBody: {
1161 |               required: true,
1162 |               content: {
1163 |                 'application/json': {
1164 |                   schema: {
1165 |                     $ref: '#/components/schemas/A',
1166 |                     description: 'A schema description',
1167 |                   },
1168 |                 },
1169 |               },
1170 |             },
1171 |             responses: {
1172 |               '201': {
1173 |                 description: 'Created A object',
1174 |                 content: {
1175 |                   'application/json': {
1176 |                     schema: { $ref: '#/components/schemas/A' },
1177 |                   },
1178 |                 },
1179 |               },
1180 |             },
1181 |             method: 'post',
1182 |             path: '/ab',
1183 |           },
1184 |         },
1185 |       },
1186 |     },
1187 |     {
1188 |       name: 'allOf/oneOf References with Full Descriptions',
1189 |       input: {
1190 |         openapi: '3.0.0',
1191 |         info: { title: 'Composed Schema API', version: '1.0.0' },
1192 |         paths: {
1193 |           '/composed': {
1194 |             get: {
1195 |               operationId: 'getComposed',
1196 |               summary: 'Get a composed resource',
1197 |               responses: {
1198 |                 '200': {
1199 |                   description: 'A composed object',
1200 |                   content: {
1201 |                     'application/json': {
1202 |                       schema: { $ref: '#/components/schemas/C' },
1203 |                     },
1204 |                   },
1205 |                 },
1206 |               },
1207 |             },
1208 |           },
1209 |         },
1210 |         components: {
1211 |           schemas: {
1212 |             Base: {
1213 |               type: 'object',
1214 |               description: 'Base schema description',
1215 |               properties: {
1216 |                 baseName: {
1217 |                   type: 'string',
1218 |                   description: 'Name in the base schema',
1219 |                 },
1220 |               },
1221 |             },
1222 |             D: {
1223 |               type: 'object',
1224 |               description: 'D schema description',
1225 |               properties: {
1226 |                 dProp: {
1227 |                   type: 'integer',
1228 |                   description: 'D property integer',
1229 |                 },
1230 |               },
1231 |             },
1232 |             E: {
1233 |               type: 'object',
1234 |               description: 'E schema description',
1235 |               properties: {
1236 |                 choice: {
1237 |                   description: 'One of these choices',
1238 |                   oneOf: [
1239 |                     {
1240 |                       $ref: '#/components/schemas/F',
1241 |                     },
1242 |                     {
1243 |                       $ref: '#/components/schemas/G',
1244 |                     },
1245 |                   ],
1246 |                 },
1247 |               },
1248 |             },
1249 |             F: {
1250 |               type: 'object',
1251 |               description: 'F schema description',
1252 |               properties: {
1253 |                 fVal: {
1254 |                   type: 'boolean',
1255 |                   description: 'Boolean in F',
1256 |                 },
1257 |               },
1258 |             },
1259 |             G: {
1260 |               type: 'object',
1261 |               description: 'G schema description',
1262 |               properties: {
1263 |                 gVal: {
1264 |                   type: 'string',
1265 |                   description: 'String in G',
1266 |                 },
1267 |               },
1268 |             },
1269 |             C: {
1270 |               description: 'C schema description',
1271 |               allOf: [{ $ref: '#/components/schemas/Base' }, { $ref: '#/components/schemas/D' }, { $ref: '#/components/schemas/E' }],
1272 |             },
1273 |           },
1274 |         },
1275 |       } as OpenAPIV3.Document,
1276 |       expected: {
1277 |         tools: {
1278 |           API: {
1279 |             methods: [
1280 |               {
1281 |                 name: 'getComposed',
1282 |                 description: 'Get a composed resource',
1283 |                 inputSchema: {
1284 |                   type: 'object',
1285 |                   properties: {},
1286 |                   required: [],
1287 |                   $defs: {
1288 |                     Base: {
1289 |                       type: 'object',
1290 |                       description: 'Base schema description',
1291 |                       additionalProperties: true,
1292 |                       properties: {
1293 |                         baseName: {
1294 |                           type: 'string',
1295 |                           description: 'Name in the base schema',
1296 |                         },
1297 |                       },
1298 |                     },
1299 |                     C: {
1300 |                       description: 'C schema description',
1301 |                       allOf: [{ $ref: '#/$defs/Base' }, { $ref: '#/$defs/D' }, { $ref: '#/$defs/E' }],
1302 |                     },
1303 |                     D: {
1304 |                       type: 'object',
1305 |                       additionalProperties: true,
1306 |                       description: 'D schema description',
1307 |                       properties: {
1308 |                         dProp: {
1309 |                           type: 'integer',
1310 |                           description: 'D property integer',
1311 |                         },
1312 |                       },
1313 |                     },
1314 |                     E: {
1315 |                       type: 'object',
1316 |                       additionalProperties: true,
1317 |                       description: 'E schema description',
1318 |                       properties: {
1319 |                         choice: {
1320 |                           description: 'One of these choices',
1321 |                           oneOf: [{ $ref: '#/$defs/F' }, { $ref: '#/$defs/G' }],
1322 |                         },
1323 |                       },
1324 |                     },
1325 |                     F: {
1326 |                       type: 'object',
1327 |                       additionalProperties: true,
1328 |                       description: 'F schema description',
1329 |                       properties: {
1330 |                         fVal: {
1331 |                           type: 'boolean',
1332 |                           description: 'Boolean in F',
1333 |                         },
1334 |                       },
1335 |                     },
1336 |                     G: {
1337 |                       type: 'object',
1338 |                       additionalProperties: true,
1339 |                       description: 'G schema description',
1340 |                       properties: {
1341 |                         gVal: {
1342 |                           type: 'string',
1343 |                           description: 'String in G',
1344 |                         },
1345 |                       },
1346 |                     },
1347 |                   },
1348 |                 },
1349 |                 returnSchema: {
1350 |                   $ref: '#/$defs/C',
1351 |                   description: 'A composed object',
1352 |                   $defs: {
1353 |                     Base: {
1354 |                       type: 'object',
1355 |                       description: 'Base schema description',
1356 |                       additionalProperties: true,
1357 |                       properties: {
1358 |                         baseName: {
1359 |                           type: 'string',
1360 |                           description: 'Name in the base schema',
1361 |                         },
1362 |                       },
1363 |                     },
1364 |                     C: {
1365 |                       description: 'C schema description',
1366 |                       allOf: [{ $ref: '#/$defs/Base' }, { $ref: '#/$defs/D' }, { $ref: '#/$defs/E' }],
1367 |                     },
1368 |                     D: {
1369 |                       type: 'object',
1370 |                       additionalProperties: true,
1371 |                       description: 'D schema description',
1372 |                       properties: {
1373 |                         dProp: {
1374 |                           type: 'integer',
1375 |                           description: 'D property integer',
1376 |                         },
1377 |                       },
1378 |                     },
1379 |                     E: {
1380 |                       type: 'object',
1381 |                       additionalProperties: true,
1382 |                       description: 'E schema description',
1383 |                       properties: {
1384 |                         choice: {
1385 |                           description: 'One of these choices',
1386 |                           oneOf: [{ $ref: '#/$defs/F' }, { $ref: '#/$defs/G' }],
1387 |                         },
1388 |                       },
1389 |                     },
1390 |                     F: {
1391 |                       type: 'object',
1392 |                       additionalProperties: true,
1393 |                       description: 'F schema description',
1394 |                       properties: {
1395 |                         fVal: {
1396 |                           type: 'boolean',
1397 |                           description: 'Boolean in F',
1398 |                         },
1399 |                       },
1400 |                     },
1401 |                     G: {
1402 |                       type: 'object',
1403 |                       additionalProperties: true,
1404 |                       description: 'G schema description',
1405 |                       properties: {
1406 |                         gVal: {
1407 |                           type: 'string',
1408 |                           description: 'String in G',
1409 |                         },
1410 |                       },
1411 |                     },
1412 |                   },
1413 |                 },
1414 |               },
1415 |             ],
1416 |           },
1417 |         },
1418 |         openApiLookup: {
1419 |           'API-getComposed': {
1420 |             operationId: 'getComposed',
1421 |             summary: 'Get a composed resource',
1422 |             responses: {
1423 |               '200': {
1424 |                 description: 'A composed object',
1425 |                 content: {
1426 |                   'application/json': {
1427 |                     schema: { $ref: '#/components/schemas/C' },
1428 |                   },
1429 |                 },
1430 |               },
1431 |             },
1432 |             method: 'get',
1433 |             path: '/composed',
1434 |           },
1435 |         },
1436 |       },
1437 |     },
1438 |   ]
1439 | 
1440 |   it.each(cases)('$name', ({ input, expected }) => {
1441 |     const converter = new OpenAPIToMCPConverter(input)
1442 |     const { tools, openApiLookup } = converter.convertToMCPTools()
1443 | 
1444 |     // Use the custom verification instead of direct equality
1445 |     verifyTools(tools, expected.tools)
1446 |     expect(openApiLookup).toEqual(expected.openApiLookup)
1447 |   })
1448 | })
1449 | 
```
Page 2/2FirstPrevNextLast