#
tokens: 48837/50000 29/29 files
lines: off (toggle) GitHub
raw markdown copy
# 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

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
node_modules
Dockerfile
docker-compose.yml
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules/
build/
dist
bin/

.cache
.yarn/cache
.eslintcache

.cursor

.DS_Store

.env
```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/README.md:
--------------------------------------------------------------------------------

```markdown
Note: This is a fork from v1 of https://github.com/snaggle-ai/openapi-mcp-server. The library took a different direction with v2 which is not compatible with our development approach.

Forked to upgrade vulnerable dependencies and easier setup.

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Notion ReadOnly MCP Server

This project implements an optimized read-only MCP server for the Notion API, focusing on performance and efficiency for AI assistants to query and retrieve Notion content.

<a href="https://glama.ai/mcp/servers/@Taewoong1378/notion-readonly-mcp-server">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@Taewoong1378/notion-readonly-mcp-server/badge" alt="Notion ReadOnly Server MCP server" />
</a>

## Key Improvements

- **Read-Only Design**: Focused exclusively on data retrieval operations, ensuring safe access to Notion content.
- **Minimized Tool Set**: Reduced the number of exposed Notion API tools from 15+ to only 6 essential ones for document analysis.
- **Parallel Processing**: Enhanced performance by implementing asynchronous and parallel API requests for retrieving block content, significantly reducing response times.
- **Extended Database Access**: Added support for database, page property, and comment retrieval operations.
- **Optimized for AI Assistants**: Significantly reduced tool count addresses the "Too many tools can degrade performance" issue in AI assistants like Cursor, which limits models to approximately 40 tools.

## Tool Comparison

This read-only implementation exposes far fewer tools compared to the standard Notion API integration, improving performance and compatibility with AI assistants:

![Notion API Tools Comparison](docs/images/notion-api-tools-comparison.png)

The reduced tool set helps stay within the recommended tool limits for optimal AI assistant performance while still providing all essential functionality.

## Installation

### 1. Setting up Integration in Notion:

Go to https://www.notion.so/profile/integrations and create a new **internal** integration or select an existing one.

![Creating a Notion Integration token](docs/images/integrations-creation.png)

While we limit the scope of Notion API's exposed to read-only operations, there is a non-zero risk to workspace data by exposing it to LLMs. Security-conscious users may want to further configure the Integration's _Capabilities_.

For example, you can create a read-only integration token by giving only "Read content" access from the "Configuration" tab:

![Notion Integration Token Capabilities showing Read content checked](docs/images/integrations-capabilities.png)

### 2. Adding MCP config to your client:

#### Using npm:

Add the following to your `.cursor/mcp.json` or `claude_desktop_config.json` (MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "notionApi": {
      "command": "npx",
      "args": ["-y", "notion-readonly-mcp-server"],
      "env": {
        "OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ntn_****\", \"Notion-Version\": \"2022-06-28\" }"
      }
    }
  }
}
```

#### Using Docker:

Add the following to your `.cursor/mcp.json` or `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "notionApi": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-e", "OPENAPI_MCP_HEADERS",
        "taewoong1378/notion-readonly-mcp-server"
      ],
      "env": {
        "OPENAPI_MCP_HEADERS": "{\"Authorization\":\"Bearer ntn_****\",\"Notion-Version\":\"2022-06-28\"}"
      }
    }
  }
}
```

Don't forget to replace `ntn_****` with your integration secret. Find it from your integration configuration tab.

### 3. Connecting content to integration:

Ensure relevant pages and databases are connected to your integration.

To do this, visit the page, click on the 3 dots, and select "Connect to integration".

![Adding Integration Token to Notion Connections](docs/images/connections.png)

## Available Tools

This optimized server exposes only essential read-only Notion API tools:

- `API-retrieve-a-page`: Get page information
- `API-get-block-children`: Get page content blocks (with parallel processing)
- `API-retrieve-a-block`: Get details about a specific block
- `API-retrieve-a-database`: Get database information
- `API-retrieve-a-comment`: Get comments on a page or block
- `API-retrieve-a-page-property`: Get specific property information from a page
- `API-get-one-pager`: **NEW!** Recursively retrieve a full Notion page with all its blocks, databases, and related content in a single call

By limiting to these 7 essential tools (compared to 15+ in the standard implementation), we ensure:

1. Better performance in AI assistants like Cursor and Claude that have tool count limitations
2. Reduced cognitive load for AI models when choosing appropriate tools
3. Faster response times with fewer API options to consider
4. Enhanced security through minimized API surface area

## Automatic Content Exploration

The new `API-get-one-pager` tool provides a powerful way to explore Notion pages without requiring multiple API calls:

- **Recursive retrieval**: Automatically traverses the entire page structure including nested blocks
- **Parallel processing**: Fetches multiple blocks and their children simultaneously for maximum performance
- **Intelligent caching**: Stores retrieved data to minimize redundant API calls
- **Comprehensive content**: Includes pages, blocks, databases, comments, and detailed property information
- **Customizable depth**: Control the level of recursion to balance between detail and performance

### Using One Pager Tool

```
{
  "page_id": "YOUR_PAGE_ID",
  "maxDepth": 5,               // Optional: Maximum recursion depth (default: 5)
  "includeDatabases": true,    // Optional: Include linked databases (default: true)
  "includeComments": true,     // Optional: Include comments (default: true)
  "includeProperties": true    // Optional: Include detailed page properties (default: true)
}
```

This automatic exploration capability is especially useful for AI assistants that need to understand the entire content of a Notion page without making dozens of separate API calls, resulting in much faster and more efficient responses.

## Asynchronous Processing

The server implements advanced parallel processing techniques for handling large Notion documents:

- Multiple requests are batched and processed concurrently
- Pagination is handled automatically for block children
- Results are efficiently aggregated before being returned
- Console logging provides visibility into the process without affecting response format

## Examples

1. Using the following instruction:

```
Get the content of page 1a6b35e6e67f802fa7e1d27686f017f2
```

The AI will retrieve the page details efficiently with parallel processing of block content.

2. Using database information:

```
Get the structure of database 8a6b35e6e67f802fa7e1d27686f017f2
```

## Development

Build:

```
pnpm build
```

Execute:

```
pnpm dev
```

## License

MIT

## AI Assistant Performance Benefits

Modern AI assistants like Cursor and Claude have limitations on the number of tools they can effectively handle:

- Most models may not respect more than 40 tools in total
- Too many tools can degrade overall performance and reasoning capabilities
- Complex tool sets increase response latency and decision-making difficulty

This read-only implementation deliberately reduces the Notion API surface to address these limitations while preserving all essential functionality. The result is:

- Faster and more reliable responses from AI assistants
- Improved accuracy when interacting with Notion content
- Better overall performance through focused API design
```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/auth/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from './types'
export * from './template'

```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
services:
  notion-mcp-server:
    build: .
    stdin_open: true
    tty: true
    restart: unless-stopped

```

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

```typescript
export { OpenAPIToMCPConverter } from './openapi/parser'
export { HttpClient } from './client/http-client'
export type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/auth/types.ts:
--------------------------------------------------------------------------------

```typescript
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'

export interface AuthTemplate {
  url: string
  method: HttpMethod
  headers: Record<string, string>
  body?: string
}

export interface SecurityScheme {
  [key: string]: {
    tokenUrl?: string
    [key: string]: any
  }
}

export interface Server {
  url: string
  description?: string
}

export interface TemplateContext {
  securityScheme?: SecurityScheme
  servers?: Server[]
  args: Record<string, string>
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./build",
    "target": "es2021",
    "lib": ["es2022"],
    "jsx": "react-jsx",
    "module": "es2022",
    "moduleResolution": "Bundler",
    "types": [
      "node"
    ],
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": false,
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": [ "test/**/*.ts", "scripts/**/*.ts", "src/**/*.ts", "examples/**/*.cjs"]
}

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/auth/template.ts:
--------------------------------------------------------------------------------

```typescript
import Mustache from 'mustache'
import { AuthTemplate, TemplateContext } from './types'

export function renderAuthTemplate(template: AuthTemplate, context: TemplateContext): AuthTemplate {
  // Disable HTML escaping for URLs
  Mustache.escape = (text) => text

  // Render URL with template variables
  const renderedUrl = Mustache.render(template.url, context)

  // Create a new template object with rendered values
  const renderedTemplate: AuthTemplate = {
    ...template,
    url: renderedUrl,
    headers: { ...template.headers }, // Create a new headers object to avoid modifying the original
  }

  // Render body if it exists
  if (template.body) {
    renderedTemplate.body = Mustache.render(template.body, context)
  }

  return renderedTemplate
}

```

--------------------------------------------------------------------------------
/scripts/build-cli.js:
--------------------------------------------------------------------------------

```javascript
import * as esbuild from 'esbuild';
import { chmod } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));

async function build() {
  await esbuild.build({
    entryPoints: [join(__dirname, 'start-server.ts')],
    bundle: true,
    minify: true,
    platform: 'node',
    target: 'node18',
    format: 'esm',
    outfile: 'bin/cli.mjs',
    banner: {
      js: "#!/usr/bin/env node\nimport { createRequire } from 'module';const require = createRequire(import.meta.url);" // see https://github.com/evanw/esbuild/pull/2067
    },
    external: ['util'],
  });

  // Make the output file executable
  await chmod('./bin/cli.mjs', 0o755);
}

build().catch((err) => {
  console.error(err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/scripts/start-server.ts:
--------------------------------------------------------------------------------

```typescript
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import path from 'node:path'
import { fileURLToPath } from 'url'

import { initProxy, ValidationError } from '../src/init-server'

export async function startServer(args: string[] = process.argv.slice(2)) {
  const filename = fileURLToPath(import.meta.url)
  const directory = path.dirname(filename)
  const specPath = path.resolve(directory, '../scripts/notion-openapi.json')
  
  const baseUrl = process.env.BASE_URL ?? undefined

  const proxy = await initProxy(specPath, baseUrl)
  await proxy.connect(new StdioServerTransport())

  return proxy.getServer()
}

startServer().catch(error => {
  if (error instanceof ValidationError) {
    console.error('Invalid OpenAPI 3.1 specification:')
    error.errors.forEach(err => console.error(err))
  } else {
    console.error('Error:', error)
  }
  process.exit(1)
})

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# syntax=docker/dockerfile:1

# Use Node.js LTS as the base image
FROM node:20-slim AS builder

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts --omit-dev

# Copy source code
COPY . .

# Build the package
RUN --mount=type=cache,target=/root/.npm npm run build

# Install package globally
RUN --mount=type=cache,target=/root/.npm npm link

# Minimal image for runtime
FROM node:20-slim

# Copy built package from builder stage
COPY scripts/notion-openapi.json /usr/local/scripts/
COPY --from=builder /usr/local/lib/node_modules/@notionhq/notion-mcp-server /usr/local/lib/node_modules/@notionhq/notion-mcp-server
COPY --from=builder /usr/local/bin/notion-mcp-server /usr/local/bin/notion-mcp-server

# Set default environment variables
ENV OPENAPI_MCP_HEADERS="{}"

# Set entrypoint
ENTRYPOINT ["notion-mcp-server"]
```

--------------------------------------------------------------------------------
/src/init-server.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'node:fs'
import path from 'node:path'

import { OpenAPIV3 } from 'openapi-types'
import OpenAPISchemaValidator from 'openapi-schema-validator'

import { MCPProxy } from './openapi-mcp-server/mcp/proxy'

export class ValidationError extends Error {
  constructor(public errors: any[]) {
    super('OpenAPI validation failed')
    this.name = 'ValidationError'
  }
}

async function loadOpenApiSpec(specPath: string, baseUrl: string | undefined): Promise<OpenAPIV3.Document> {
  let rawSpec: string

  try {
    rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
  } catch (error) {
    console.error('Failed to read OpenAPI specification file:', (error as Error).message)
    process.exit(1)
  }

  // Parse and validate the OpenApi Spec
  try {
    const parsed = JSON.parse(rawSpec)

    // Override baseUrl if specified.
    if (baseUrl) {
      parsed.servers[0].url = baseUrl
    }

    return parsed as OpenAPIV3.Document
  } catch (error) {
    if (error instanceof ValidationError) {
      throw error
    }
    console.error('Failed to parse OpenAPI spec:', (error as Error).message)
    process.exit(1)
  }
}

export async function initProxy(specPath: string, baseUrl: string |undefined) {
  const openApiSpec = await loadOpenApiSpec(specPath, baseUrl)
  const proxy = new MCPProxy('Notion API', openApiSpec)

  return proxy
}

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/openapi/file-upload.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types'

/**
 * Identifies file upload parameters in an OpenAPI operation
 * @param operation The OpenAPI operation object to check
 * @returns Array of parameter names that are file uploads
 */
export function isFileUploadParameter(operation: OpenAPIV3.OperationObject): string[] {
  const fileParams: string[] = []

  if (!operation.requestBody) return fileParams

  const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
  const content = requestBody.content || {}

  // Check multipart/form-data content type for file uploads
  const multipartContent = content['multipart/form-data']
  if (!multipartContent?.schema) return fileParams

  const schema = multipartContent.schema as OpenAPIV3.SchemaObject
  if (schema.type !== 'object' || !schema.properties) return fileParams

  // Look for properties with type: string, format: binary which indicates file uploads
  Object.entries(schema.properties).forEach(([propName, prop]) => {
    const schemaProp = prop as OpenAPIV3.SchemaObject
    if (schemaProp.type === 'string' && schemaProp.format === 'binary') {
      fileParams.push(propName)
    }

    // Check for array of files
    if (schemaProp.type === 'array' && schemaProp.items) {
      const itemSchema = schemaProp.items as OpenAPIV3.SchemaObject
      if (itemSchema.type === 'string' && itemSchema.format === 'binary') {
        fileParams.push(propName)
      }
    }
  })

  return fileParams
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "notion-readonly-mcp-server",
  "keywords": [
    "notion",
    "api",
    "mcp",
    "server",
    "read-only",
    "async"
  ],
  "version": "1.0.9",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "build": "tsc -build && node scripts/build-cli.js",
    "dev": "tsx watch scripts/start-server.ts"
  },
  "bin": {
    "notion-mcp-server": "bin/cli.mjs"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.8.0",
    "axios": "^1.8.4",
    "form-data": "^4.0.1",
    "mustache": "^4.2.0",
    "openapi-client-axios": "^7.5.5",
    "openapi-schema-validator": "^12.1.3",
    "openapi-types": "^12.1.3",
    "which": "^5.0.0",
    "zod": "3.24.1"
  },
  "devDependencies": {
    "@anthropic-ai/sdk": "^0.33.1",
    "@types/express": "^5.0.0",
    "@types/js-yaml": "^4.0.9",
    "@types/json-schema": "^7.0.15",
    "@types/mustache": "^4.2.5",
    "@types/node": "^20.17.16",
    "@types/which": "^3.0.4",
    "@vitest/coverage-v8": "3.1.1",
    "body-parser": "^2.2.0",
    "esbuild": "^0.25.2",
    "express": "^4.21.2",
    "multer": "1.4.5-lts.1",
    "openai": "^4.91.1",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2",
    "vitest": "^3.1.1"
  },
  "description": "Optimized read-only MCP server for Notion API with asynchronous processing",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "[email protected]:Taewoong1378/notion-readonly-mcp-server.git"
  },
  "author": "@taewoong1378",
  "bugs": {
    "url": "https://github.com/Taewoong1378/notion-readonly-mcp-server/issues"
  },
  "homepage": "https://github.com/Taewoong1378/notion-readonly-mcp-server#readme"
}

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types'
import { describe, it, expect } from 'vitest'
import { isFileUploadParameter } from '../file-upload'

describe('File Upload Detection', () => {
  it('identifies file upload parameters in request bodies', () => {
    const operation: OpenAPIV3.OperationObject = {
      operationId: 'uploadFile',
      responses: {
        '200': {
          description: 'File uploaded successfully',
        },
      },
      requestBody: {
        content: {
          'multipart/form-data': {
            schema: {
              type: 'object',
              properties: {
                file: {
                  type: 'string',
                  format: 'binary',
                },
                additionalInfo: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    }

    const fileParams = isFileUploadParameter(operation)
    expect(fileParams).toEqual(['file'])
  })

  it('returns empty array for non-file upload operations', () => {
    const operation: OpenAPIV3.OperationObject = {
      operationId: 'createUser',
      responses: {
        '200': {
          description: 'User created successfully',
        },
      },
      requestBody: {
        content: {
          'application/json': {
            schema: {
              type: 'object',
              properties: {
                name: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    }

    const fileParams = isFileUploadParameter(operation)
    expect(fileParams).toEqual([])
  })

  it('identifies array-based file upload parameters', () => {
    const operation: OpenAPIV3.OperationObject = {
      operationId: 'uploadFiles',
      responses: {
        '200': {
          description: 'Files uploaded successfully',
        },
      },
      requestBody: {
        content: {
          'multipart/form-data': {
            schema: {
              type: 'object',
              properties: {
                files: {
                  type: 'array',
                  items: {
                    type: 'string',
                    format: 'binary',
                  },
                },
                description: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    }

    const fileParams = isFileUploadParameter(operation)
    expect(fileParams).toEqual(['files'])
  })
})

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs'
import { OpenAPIV3 } from 'openapi-types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { HttpClient } from '../http-client'

// 모킹 방식 변경
vi.mock('fs', () => {
  return {
    default: {
      createReadStream: vi.fn()
    },
    createReadStream: vi.fn()
  }
})

vi.mock('form-data', () => {
  const FormDataMock = vi.fn().mockImplementation(() => ({
    append: vi.fn(),
    getHeaders: vi.fn().mockReturnValue({ 'content-type': 'multipart/form-data; boundary=---123' })
  }))
  return {
    default: FormDataMock
  }
})

describe('HttpClient File Upload', () => {
  let client: HttpClient
  const mockApiInstance = {
    uploadFile: vi.fn(),
  }

  const baseConfig = {
    baseUrl: 'http://test.com',
    headers: {},
  }

  const mockOpenApiSpec: OpenAPIV3.Document = {
    openapi: '3.0.0',
    info: {
      title: 'Test API',
      version: '1.0.0',
    },
    paths: {
      '/upload': {
        post: {
          operationId: 'uploadFile',
          responses: {
            '200': {
              description: 'File uploaded successfully',
              content: {
                'application/json': {
                  schema: {
                    type: 'object',
                    properties: {
                      success: {
                        type: 'boolean',
                      },
                    },
                  },
                },
              },
            },
          },
          requestBody: {
            content: {
              'multipart/form-data': {
                schema: {
                  type: 'object',
                  properties: {
                    file: {
                      type: 'string',
                      format: 'binary',
                    },
                    description: {
                      type: 'string',
                    },
                  },
                },
              },
            },
          },
        },
      },
    },
  }

  beforeEach(() => {
    vi.clearAllMocks()
    client = new HttpClient(baseConfig, mockOpenApiSpec)
    // @ts-expect-error - Mock the private api property
    client['api'] = Promise.resolve(mockApiInstance)
  })

  it('should handle file uploads with FormData', async () => {
    const mockFileStream = { pipe: vi.fn() }
    
    // 모킹 방식 변경
    vi.mocked(fs.createReadStream).mockReturnValue(mockFileStream as any)

    const uploadPath = mockOpenApiSpec.paths['/upload']
    if (!uploadPath?.post) {
      throw new Error('Upload path not found in spec')
    }
    const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
    const params = {
      file: '/path/to/test.txt',
      description: 'Test file',
    }

    mockApiInstance.uploadFile.mockResolvedValue({
      data: { success: true },
      status: 200,
      headers: {},
    })

    await client.executeOperation(operation, params)

    expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test.txt')
    expect(mockApiInstance.uploadFile).toHaveBeenCalled()
  })

  it('should throw error for invalid file path', async () => {
    vi.mocked(fs.createReadStream).mockImplementation(() => {
      throw new Error('File not found')
    })

    const uploadPath = mockOpenApiSpec.paths['/upload']
    if (!uploadPath?.post) {
      throw new Error('Upload path not found in spec')
    }
    const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
    const params = {
      file: '/nonexistent/file.txt',
      description: 'Test file',
    }

    await expect(client.executeOperation(operation, params)).rejects.toThrow('Failed to read file at /nonexistent/file.txt')
  })

  it('should handle multiple file uploads', async () => {
    const mockFileStream1 = { pipe: vi.fn() }
    const mockFileStream2 = { pipe: vi.fn() }
    
    // createReadStream 모킹을 시퀀스로 설정
    vi.mocked(fs.createReadStream)
      .mockReturnValueOnce(mockFileStream1 as any)
      .mockReturnValueOnce(mockFileStream2 as any)

    const operation: OpenAPIV3.OperationObject = {
      operationId: 'uploadFile',
      responses: {
        '200': {
          description: 'Files uploaded successfully',
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  success: {
                    type: 'boolean',
                  },
                },
              },
            },
          },
        },
      },
      requestBody: {
        content: {
          'multipart/form-data': {
            schema: {
              type: 'object',
              properties: {
                file1: {
                  type: 'string',
                  format: 'binary',
                },
                file2: {
                  type: 'string',
                  format: 'binary',
                },
                description: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    }

    const params = {
      file1: '/path/to/test1.txt',
      file2: '/path/to/test2.txt',
      description: 'Test files',
    }

    mockApiInstance.uploadFile.mockResolvedValue({
      data: { success: true },
      status: 200,
      headers: {},
    })

    await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, params)

    expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test1.txt')
    expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test2.txt')
    expect(mockApiInstance.uploadFile).toHaveBeenCalled()
  })
})

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
import type express from 'express'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { HttpClient } from '../http-client'
//@ts-ignore
import axios from 'axios'
import type { OpenAPIV3 } from 'openapi-types'
import { createPetstoreServer } from '../../../../examples/petstore-server.cjs'

interface Pet {
  id: number
  name: string
  species: string
  age: number
  status: 'available' | 'pending' | 'sold'
}

describe('HttpClient Integration Tests', () => {
  const PORT = 3456
  const BASE_URL = `http://localhost:${PORT}`
  let server: ReturnType<typeof express>
  let openApiSpec: OpenAPIV3.Document
  let client: HttpClient

  beforeAll(async () => {
    // Start the petstore server
    server = createPetstoreServer(PORT) as unknown as express.Express

    // Fetch the OpenAPI spec from the server
    const response = await axios.get(`${BASE_URL}/openapi.json`)
    openApiSpec = response.data

    // Create HTTP client
    client = new HttpClient(
      {
        baseUrl: BASE_URL,
        headers: {
          Accept: 'application/json',
        },
      },
      openApiSpec,
    )
  })

  afterAll(() => {
    //@ts-expect-error
    server.close()
  })

  it('should list all pets', async () => {
    const operation = openApiSpec.paths['/pets']?.get
    if (!operation) throw new Error('Operation not found')

    const response = await client.executeOperation<Pet[]>(operation as OpenAPIV3.OperationObject & { method: string; path: string })

    expect(response.status).toBe(200)
    expect(Array.isArray(response.data)).toBe(true)
    expect(response.data.length).toBeGreaterThan(0)
    expect(response.data[0]).toHaveProperty('name')
    expect(response.data[0]).toHaveProperty('species')
    expect(response.data[0]).toHaveProperty('status')
  })

  it('should filter pets by status', async () => {
    const operation = openApiSpec.paths['/pets']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
    if (!operation) throw new Error('Operation not found')

    const response = await client.executeOperation<Pet[]>(operation, { status: 'available' })

    expect(response.status).toBe(200)
    expect(Array.isArray(response.data)).toBe(true)
    response.data.forEach((pet: Pet) => {
      expect(pet.status).toBe('available')
    })
  })

  it('should get a specific pet by ID', async () => {
    const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
    if (!operation) throw new Error('Operation not found')

    const response = await client.executeOperation<Pet>(operation, { id: 1 })

    expect(response.status).toBe(200)
    expect(response.data).toHaveProperty('id', 1)
    expect(response.data).toHaveProperty('name')
    expect(response.data).toHaveProperty('species')
  })

  it('should create a new pet', async () => {
    const operation = openApiSpec.paths['/pets']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
    if (!operation) throw new Error('Operation not found')

    const newPet = {
      name: 'TestPet',
      species: 'Dog',
      age: 2,
    }

    const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, newPet)

    expect(response.status).toBe(201)
    expect(response.data).toMatchObject({
      ...newPet,
      status: 'available',
    })
    expect(response.data.id).toBeDefined()
  })

  it("should update a pet's status", async () => {
    const operation = openApiSpec.paths['/pets/{id}']?.put
    if (!operation) throw new Error('Operation not found')

    const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
      id: 1,
      status: 'sold',
    })

    expect(response.status).toBe(200)
    expect(response.data).toHaveProperty('id', 1)
    expect(response.data).toHaveProperty('status', 'sold')
  })

  it('should delete a pet', async () => {
    // First create a pet to delete
    const createOperation = openApiSpec.paths['/pets']?.post
    if (!createOperation) throw new Error('Operation not found')

    const createResponse = await client.executeOperation<Pet>(
      createOperation as OpenAPIV3.OperationObject & { method: string; path: string },
      {
        name: 'ToDelete',
        species: 'Cat',
        age: 3,
      },
    )
    const petId = createResponse.data.id

    // Then delete it
    const deleteOperation = openApiSpec.paths['/pets/{id}']?.delete
    if (!deleteOperation) throw new Error('Operation not found')

    const deleteResponse = await client.executeOperation(deleteOperation as OpenAPIV3.OperationObject & { method: string; path: string }, {
      id: petId,
    })

    expect(deleteResponse.status).toBe(204)

    // Verify the pet is deleted
    const getOperation = openApiSpec.paths['/pets/{id}']?.get
    if (!getOperation) throw new Error('Operation not found')

    try {
      await client.executeOperation(getOperation as OpenAPIV3.OperationObject & { method: string; path: string }, { id: petId })
      throw new Error('Should not reach here')
    } catch (error: any) {
      expect(error.message).toContain('404')
    }
  })

  it('should handle errors appropriately', async () => {
    const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
    if (!operation) throw new Error('Operation not found')

    try {
      await client.executeOperation(
        operation as OpenAPIV3.OperationObject & { method: string; path: string },
        { id: 99999 }, // Non-existent ID
      )
      throw new Error('Should not reach here')
    } catch (error: any) {
      expect(error.message).toContain('404')
    }
  })
})

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/client/http-client.ts:
--------------------------------------------------------------------------------

```typescript
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
import OpenAPIClientAxios from 'openapi-client-axios'
import type { AxiosInstance } from 'axios'
import FormData from 'form-data'
import fs from 'fs'
import { isFileUploadParameter } from '../openapi/file-upload'

export type HttpClientConfig = {
  baseUrl: string
  headers?: Record<string, string>
}

export type HttpClientResponse<T = any> = {
  data: T
  status: number
  headers: Headers
}

export class HttpClientError extends Error {
  constructor(
    message: string,
    public status: number,
    public data: any,
    public headers?: Headers,
  ) {
    super(`${status} ${message}`)
    this.name = 'HttpClientError'
  }
}

export class HttpClient {
  private api: Promise<AxiosInstance>
  private client: OpenAPIClientAxios

  constructor(config: HttpClientConfig, openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {
    // @ts-expect-error
    this.client = new (OpenAPIClientAxios.default ?? OpenAPIClientAxios)({
      definition: openApiSpec,
      axiosConfigDefaults: {
        baseURL: config.baseUrl,
        headers: {
          'Content-Type': 'application/json',
          'User-Agent': 'notion-mcp-server',
          ...config.headers,
        },
      },
    })
    this.api = this.client.init()
  }

  private async prepareFileUpload(operation: OpenAPIV3.OperationObject, params: Record<string, any>): Promise<FormData | null> {
    const fileParams = isFileUploadParameter(operation)
    if (fileParams.length === 0) return null

    const formData = new FormData()

    // Handle file uploads
    for (const param of fileParams) {
      const filePath = params[param]
      if (!filePath) {
        throw new Error(`File path must be provided for parameter: ${param}`)
      }
      switch (typeof filePath) {
        case 'string':
          addFile(param, filePath)
          break
        case 'object':
          if(Array.isArray(filePath)) {
            let fileCount = 0
            for(const file of filePath) {
              addFile(param, file)
              fileCount++
            }
            break
          }
          //deliberate fallthrough
        default:
          throw new Error(`Unsupported file type: ${typeof filePath}`)
      }
      function addFile(name: string, filePath: string) {
          try {
            const fileStream = fs.createReadStream(filePath)
            formData.append(name, fileStream)
        } catch (error) {
          throw new Error(`Failed to read file at ${filePath}: ${error}`)
        }
      }
    }

    // Add non-file parameters to form data
    for (const [key, value] of Object.entries(params)) {
      if (!fileParams.includes(key)) {
        formData.append(key, value)
      }
    }

    return formData
  }

  /**
   * Execute an OpenAPI operation
   */
  async executeOperation<T = any>(
    operation: OpenAPIV3.OperationObject & { method: string; path: string },
    params: Record<string, any> = {},
  ): Promise<HttpClientResponse<T>> {
    const api = await this.api
    const operationId = operation.operationId
    if (!operationId) {
      throw new Error('Operation ID is required')
    }

    // Handle file uploads if present
    const formData = await this.prepareFileUpload(operation, params)

    // Separate parameters based on their location
    const urlParameters: Record<string, any> = {}
    const bodyParams: Record<string, any> = formData || { ...params }

    // Extract path and query parameters based on operation definition
    if (operation.parameters) {
      for (const param of operation.parameters) {
        if ('name' in param && param.name && param.in) {
          if (param.in === 'path' || param.in === 'query') {
            if (params[param.name] !== undefined) {
              urlParameters[param.name] = params[param.name]
              if (!formData) {
                delete bodyParams[param.name]
              }
            }
          }
        }
      }
    }

    // Add all parameters as url parameters if there is no requestBody defined
    if (!operation.requestBody && !formData) {
      for (const key in bodyParams) {
        if (bodyParams[key] !== undefined) {
          urlParameters[key] = bodyParams[key]
          delete bodyParams[key]
        }
      }
    }

    const operationFn = (api as any)[operationId]
    if (!operationFn) {
      throw new Error(`Operation ${operationId} not found`)
    }

    try {
      // If we have form data, we need to set the correct headers
      const hasBody = Object.keys(bodyParams).length > 0
      const headers = formData
        ? formData.getHeaders()
        : { ...(hasBody ? { 'Content-Type': 'application/json' } : { 'Content-Type': null }) }
      const requestConfig = {
        headers: {
          ...headers,
        },
      }

      // first argument is url parameters, second is body parameters
      const response = await operationFn(urlParameters, hasBody ? bodyParams : undefined, requestConfig)

      // Convert axios headers to Headers object
      const responseHeaders = new Headers()
      Object.entries(response.headers).forEach(([key, value]) => {
        if (value) responseHeaders.append(key, value.toString())
      })

      return {
        data: response.data,
        status: response.status,
        headers: responseHeaders,
      }
    } catch (error: any) {
      if (error.response) {
        console.error('Error in http client', error)
        const headers = new Headers()
        Object.entries(error.response.headers).forEach(([key, value]) => {
          if (value) headers.append(key, value.toString())
        })

        throw new HttpClientError(error.response.statusText || 'Request failed', error.response.status, error.response.data, headers)
      }
      throw error
    }
  }
}

```

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

```typescript
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { OpenAPIV3 } from 'openapi-types'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { HttpClient } from '../../client/http-client'
import { MCPProxy } from '../proxy'

// Mock the dependencies
vi.mock('../../client/http-client')
vi.mock('@modelcontextprotocol/sdk/server/index.js')

describe('MCPProxy', () => {
  let proxy: MCPProxy
  let mockOpenApiSpec: OpenAPIV3.Document

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks()

    // Setup minimal OpenAPI spec for testing
    mockOpenApiSpec = {
      openapi: '3.0.0',
      servers: [{ url: 'http://localhost:3000' }],
      info: {
        title: 'Test API',
        version: '1.0.0',
      },
      paths: {
        '/test': {
          get: {
            operationId: 'getTest',
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
      },
    }

    proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
  })

  describe('listTools handler', () => {
    it('should return converted tools from OpenAPI spec', async () => {
      const server = (proxy as any).server
      const listToolsHandler = server.setRequestHandler.mock.calls[0].filter((x: unknown) => typeof x === 'function')[0]
      const result = await listToolsHandler()

      expect(result).toHaveProperty('tools')
      expect(Array.isArray(result.tools)).toBe(true)
    })

    it('should truncate tool names exceeding 64 characters', async () => {
      // Setup OpenAPI spec with long tool names
      mockOpenApiSpec.paths = {
        '/test': {
          get: {
            operationId: 'a'.repeat(65),
            responses: {
              '200': {
                description: 'Success'
              }
            }
          }
        }
      }
      proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      const server = (proxy as any).server
      const listToolsHandler = server.setRequestHandler.mock.calls[0].filter((x: unknown) => typeof x === 'function')[0];
      const result = await listToolsHandler()

      expect(result.tools[0].name.length).toBeLessThanOrEqual(64)
    })
  })

  describe('callTool handler', () => {
    it('should execute operation and return formatted response', async () => {
      // Mock HttpClient response
      const mockResponse = {
        data: { message: 'success' },
        status: 200,
        headers: new Headers({
          'content-type': 'application/json',
        }),
      }
      ;(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)

      // Set up the openApiLookup with our test operation
      ;(proxy as any).openApiLookup = {
        'API-getTest': {
          operationId: 'getTest',
          responses: { '200': { description: 'Success' } },
          method: 'get',
          path: '/test',
        },
      }

      const server = (proxy as any).server
      const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
      const callToolHandler = handlers[1]

      const result = await callToolHandler({
        params: {
          name: 'API-getTest',
          arguments: {},
        },
      })

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify({ message: 'success' }),
          },
        ],
      })
    })

    it('should throw error for non-existent operation', async () => {
      const server = (proxy as any).server
      const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
      const callToolHandler = handlers[1]

      await expect(
        callToolHandler({
          params: {
            name: 'nonExistentMethod',
            arguments: {},
          },
        }),
      ).resolves.toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              status: 'error',
              message: 'Method nonExistentMethod not found.',
              code: 404
            }),
          },
        ],
      })
    })

    it('should handle tool names exceeding 64 characters', async () => {
      // Mock HttpClient response
      const mockResponse = {
        data: { message: 'success' },
        status: 200,
        headers: new Headers({
          'content-type': 'application/json'
        })
      };
      (HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);

      // Set up the openApiLookup with a long tool name
      const longToolName = 'a'.repeat(65)
      const truncatedToolName = longToolName.slice(0, 64)
      ;(proxy as any).openApiLookup = {
        [truncatedToolName]: {
          operationId: longToolName,
          responses: { '200': { description: 'Success' } },
          method: 'get',
          path: '/test'
        }
      };

      const server = (proxy as any).server;
      const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function');
      const callToolHandler = handlers[1];

      const result = await callToolHandler({
        params: {
          name: truncatedToolName,
          arguments: {}
        }
      })

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify({ message: 'success' })
          }
        ]
      })
    })
  })

  describe('getContentType', () => {
    it('should return correct content type for different headers', () => {
      const getContentType = (proxy as any).getContentType.bind(proxy)

      expect(getContentType(new Headers({ 'content-type': 'text/plain' }))).toBe('text')
      expect(getContentType(new Headers({ 'content-type': 'application/json' }))).toBe('text')
      expect(getContentType(new Headers({ 'content-type': 'image/jpeg' }))).toBe('image')
      expect(getContentType(new Headers({ 'content-type': 'application/octet-stream' }))).toBe('binary')
      expect(getContentType(new Headers())).toBe('binary')
    })
  })

  describe('parseHeadersFromEnv', () => {
    const originalEnv = process.env

    beforeEach(() => {
      process.env = { ...originalEnv }
    })

    afterEach(() => {
      process.env = originalEnv
    })

    it('should parse valid JSON headers from env', () => {
      process.env.OPENAPI_MCP_HEADERS = JSON.stringify({
        Authorization: 'Bearer token123',
        'X-Custom-Header': 'test',
      })

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {
            Authorization: 'Bearer token123',
            'X-Custom-Header': 'test',
          },
        }),
        expect.anything(),
      )
    })

    it('should return empty object when env var is not set', () => {
      delete process.env.OPENAPI_MCP_HEADERS

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {},
        }),
        expect.anything(),
      )
    })

    it('should return empty object and warn on invalid JSON', () => {
      const consoleSpy = vi.spyOn(console, 'warn')
      process.env.OPENAPI_MCP_HEADERS = 'invalid json'

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {},
        }),
        expect.anything(),
      )
      expect(consoleSpy).toHaveBeenCalledWith('Failed to parse OPENAPI_MCP_HEADERS environment variable:', expect.any(Error))
    })

    it('should return empty object and warn on non-object JSON', () => {
      const consoleSpy = vi.spyOn(console, 'warn')
      process.env.OPENAPI_MCP_HEADERS = '"string"'

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {},
        }),
        expect.anything(),
      )
      expect(consoleSpy).toHaveBeenCalledWith('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', 'string')
    })
  })
  describe('connect', () => {
    it('should connect to transport', async () => {
      const mockTransport = {} as Transport
      await proxy.connect(mockTransport)

      const server = (proxy as any).server
      expect(server.connect).toHaveBeenCalledWith(mockTransport)
    })
  })
})

```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/mcp/__tests__/one-pager.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { HttpClient } from '../../client/http-client'
import { MCPProxy } from '../proxy'

// Mock the dependencies
vi.mock('../../client/http-client')
vi.mock('@modelcontextprotocol/sdk/server/index.js')

describe('MCPProxy - One Pager Functionality', () => {
  let proxy: MCPProxy
  let mockOpenApiSpec: OpenAPIV3.Document

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks()

    // Setup OpenAPI spec for testing
    mockOpenApiSpec = {
      openapi: '3.0.0',
      servers: [{ url: 'http://localhost:3000' }],
      info: {
        title: 'Notion API',
        version: '1.0.0',
      },
      paths: {
        '/v1/pages/{page_id}': {
          get: {
            operationId: 'retrieve-a-page',
            parameters: [
              {
                name: 'page_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
        '/v1/blocks/{block_id}/children': {
          get: {
            operationId: 'get-block-children',
            parameters: [
              {
                name: 'block_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              },
              {
                name: 'page_size',
                in: 'query',
                schema: { type: 'integer' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
        '/v1/blocks/{block_id}': {
          get: {
            operationId: 'retrieve-a-block',
            parameters: [
              {
                name: 'block_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
        '/v1/databases/{database_id}': {
          get: {
            operationId: 'retrieve-a-database',
            parameters: [
              {
                name: 'database_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
        '/v1/comments': {
          get: {
            operationId: 'retrieve-a-comment',
            parameters: [
              {
                name: 'block_id',
                in: 'query',
                required: true,
                schema: { type: 'string' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
        '/v1/pages/{page_id}/properties/{property_id}': {
          get: {
            operationId: 'retrieve-a-page-property',
            parameters: [
              {
                name: 'page_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              },
              {
                name: 'property_id',
                in: 'path',
                required: true,
                schema: { type: 'string' }
              }
            ],
            responses: {
              '200': {
                description: 'Success',
              },
            },
          },
        },
      },
    }

    proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
  })

  describe('handleOnePagerRequest', () => {
    it('should recursively retrieve page content', async () => {
      // Set up mocks for each API response

      // 1. Mock page response
      const mockPageResponse = {
        data: {
          object: 'page',
          id: 'test-page-id',
          properties: {
            title: {
              id: 'title',
              type: 'title',
              title: [{ type: 'text', text: { content: 'Test Page' } }]
            }
          },
          has_children: true
        },
        status: 200,
        headers: new Headers({ 'content-type': 'application/json' }),
      }

      // 2. Mock block children response
      const mockBlocksResponse = {
        data: {
          object: 'list',
          results: [
            {
              object: 'block',
              id: 'block-1',
              type: 'paragraph',
              has_children: false,
              paragraph: {
                rich_text: [{ type: 'text', text: { content: 'Test paragraph' } }]
              }
            },
            {
              object: 'block',
              id: 'block-2',
              type: 'child_database',
              has_children: false,
              child_database: {
                database_id: 'db-1'
              }
            }
          ],
          next_cursor: null,
          has_more: false
        },
        status: 200,
        headers: new Headers({ 'content-type': 'application/json' }),
      }

      // 3. Mock database response
      const mockDatabaseResponse = {
        data: {
          object: 'database',
          id: 'db-1',
          title: [{ type: 'text', text: { content: 'Test Database' } }],
          properties: {
            Name: {
              id: 'title',
              type: 'title',
              title: {}
            }
          }
        },
        status: 200,
        headers: new Headers({ 'content-type': 'application/json' }),
      }

      // 4. Mock comments response
      const mockCommentsResponse = {
        data: {
          object: 'list',
          results: [
            {
              object: 'comment',
              id: 'comment-1',
              rich_text: [{ type: 'text', text: { content: 'Test comment' } }]
            }
          ],
          has_more: false
        },
        status: 200,
        headers: new Headers({ 'content-type': 'application/json' }),
      }

      // Set up the mock API responses
      const executeOperationMock = HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>
      
      executeOperationMock.mockImplementation((operation, params) => {
        if (operation.operationId === 'retrieve-a-page') {
          return Promise.resolve(mockPageResponse)
        } else if (operation.operationId === 'get-block-children') {
          return Promise.resolve(mockBlocksResponse)
        } else if (operation.operationId === 'retrieve-a-database') {
          return Promise.resolve(mockDatabaseResponse)
        } else if (operation.operationId === 'retrieve-a-comment') {
          return Promise.resolve(mockCommentsResponse)
        }
        return Promise.resolve({ data: {}, status: 200, headers: new Headers() })
      })

      // Set up openApiLookup with our test operations
      const openApiLookup = {
        'API-retrieve-a-page': {
          operationId: 'retrieve-a-page',
          method: 'get',
          path: '/v1/pages/{page_id}',
        },
        'API-get-block-children': {
          operationId: 'get-block-children',
          method: 'get',
          path: '/v1/blocks/{block_id}/children',
        },
        'API-retrieve-a-database': {
          operationId: 'retrieve-a-database',
          method: 'get',
          path: '/v1/databases/{database_id}',
        },
        'API-retrieve-a-comment': {
          operationId: 'retrieve-a-comment',
          method: 'get',
          path: '/v1/comments',
        },
      }
      ;(proxy as any).openApiLookup = openApiLookup

      // Get the server request handlers
      const server = (proxy as any).server
      const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
      const callToolHandler = handlers[1]

      // Call the get-one-pager tool
      const result = await callToolHandler({
        params: {
          name: 'API-get-one-pager',
          arguments: {
            page_id: 'test-page-id',
            maxDepth: 2,
            includeDatabases: true,
            includeComments: true
          },
        },
      })

      // Parse the result
      const onePagerData = JSON.parse(result.content[0].text)

      // Verify the structure of the One Pager result
      expect(onePagerData).toHaveProperty('id', 'test-page-id')
      expect(onePagerData).toHaveProperty('content')
      
      // Verify that recursive content was retrieved
      expect(onePagerData.content).toBeInstanceOf(Array)
      expect(onePagerData.content.length).toBeGreaterThan(0)
      
      // Verify that at least one comment was retrieved
      expect(onePagerData).toHaveProperty('comments')
      expect(onePagerData.comments.results.length).toBeGreaterThan(0)
      
      // Verify database information was retrieved
      const databaseBlock = onePagerData.content.find((block: any) => block.type === 'child_database')
      expect(databaseBlock).toBeDefined()
      expect(databaseBlock).toHaveProperty('database')
      expect(databaseBlock.database).toHaveProperty('id', 'db-1')
    })
  })
}) 
```

--------------------------------------------------------------------------------
/examples/petstore-server.cjs:
--------------------------------------------------------------------------------

```
const express = require('express')
const bodyParser = require('body-parser')

// 메모리에 저장할 데이터
let pets = [
  {
    id: 1,
    name: 'Max',
    species: 'Dog',
    age: 3,
    status: 'available'
  },
  {
    id: 2,
    name: 'Whiskers',
    species: 'Cat',
    age: 2,
    status: 'pending'
  },
  {
    id: 3,
    name: 'Goldie',
    species: 'Fish',
    age: 1,
    status: 'sold'
  }
]

// 다음 ID 추적용
let nextId = 4

/**
 * Petstore 서버 생성 함수
 * @param {number} port 서버가 실행될 포트
 * @returns {Express} Express 서버 인스턴스
 */
function createPetstoreServer(port) {
  const app = express()

  // Middleware
  app.use(bodyParser.json())

  // OpenAPI spec 제공
  app.get('/openapi.json', (req, res) => {
    res.json({
      openapi: '3.0.0',
      info: {
        title: 'Petstore API',
        version: '1.0.0',
        description: 'A simple petstore API for testing'
      },
      servers: [
        {
          url: `http://localhost:${port}`
        }
      ],
      paths: {
        '/pets': {
          get: {
            operationId: 'listPets',
            summary: 'List all pets',
            parameters: [
              {
                name: 'status',
                in: 'query',
                required: false,
                schema: {
                  type: 'string',
                  enum: ['available', 'pending', 'sold']
                }
              }
            ],
            responses: {
              '200': {
                description: 'A list of pets',
                content: {
                  'application/json': {
                    schema: {
                      type: 'array',
                      items: {
                        $ref: '#/components/schemas/Pet'
                      }
                    }
                  }
                }
              }
            }
          },
          post: {
            operationId: 'createPet',
            summary: 'Create a pet',
            requestBody: {
              content: {
                'application/json': {
                  schema: {
                    $ref: '#/components/schemas/NewPet'
                  }
                }
              },
              required: true
            },
            responses: {
              '201': {
                description: 'Pet created',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Pet'
                    }
                  }
                }
              }
            }
          }
        },
        '/pets/{id}': {
          get: {
            operationId: 'getPet',
            summary: 'Get a pet by ID',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: {
                  type: 'integer'
                }
              }
            ],
            responses: {
              '200': {
                description: 'A pet',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Pet'
                    }
                  }
                }
              },
              '404': {
                description: 'Pet not found',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Error'
                    }
                  }
                }
              }
            }
          },
          put: {
            operationId: 'updatePet',
            summary: 'Update a pet',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: {
                  type: 'integer'
                }
              }
            ],
            requestBody: {
              content: {
                'application/json': {
                  schema: {
                    $ref: '#/components/schemas/PetUpdate'
                  }
                }
              },
              required: true
            },
            responses: {
              '200': {
                description: 'Pet updated',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Pet'
                    }
                  }
                }
              },
              '404': {
                description: 'Pet not found',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Error'
                    }
                  }
                }
              }
            }
          },
          delete: {
            operationId: 'deletePet',
            summary: 'Delete a pet',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: {
                  type: 'integer'
                }
              }
            ],
            responses: {
              '204': {
                description: 'Pet deleted'
              },
              '404': {
                description: 'Pet not found',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Error'
                    }
                  }
                }
              }
            }
          }
        }
      },
      components: {
        schemas: {
          Pet: {
            type: 'object',
            required: ['id', 'name', 'species', 'status'],
            properties: {
              id: {
                type: 'integer'
              },
              name: {
                type: 'string'
              },
              species: {
                type: 'string'
              },
              age: {
                type: 'integer'
              },
              status: {
                type: 'string',
                enum: ['available', 'pending', 'sold']
              }
            }
          },
          NewPet: {
            type: 'object',
            required: ['name', 'species'],
            properties: {
              name: {
                type: 'string'
              },
              species: {
                type: 'string'
              },
              age: {
                type: 'integer'
              }
            }
          },
          PetUpdate: {
            type: 'object',
            properties: {
              name: {
                type: 'string'
              },
              species: {
                type: 'string'
              },
              age: {
                type: 'integer'
              },
              status: {
                type: 'string',
                enum: ['available', 'pending', 'sold']
              }
            }
          },
          Error: {
            type: 'object',
            required: ['code', 'message'],
            properties: {
              code: {
                type: 'string'
              },
              message: {
                type: 'string'
              }
            }
          }
        }
      }
    })
  })

  // 모든 펫 목록 조회
  app.get('/pets', (req, res) => {
    let result = [...pets]
    
    // 상태별 필터링
    if (req.query.status) {
      result = result.filter(pet => pet.status === req.query.status)
    }
    
    res.json(result)
  })

  // 특정 펫 조회
  app.get('/pets/:id', (req, res) => {
    const id = parseInt(req.params.id)
    const pet = pets.find(p => p.id === id)
    
    if (!pet) {
      return res.status(404).json({
        code: 'RESOURCE_NOT_FOUND',
        message: 'Pet not found',
        petId: id
      })
    }
    
    res.json(pet)
  })

  // 펫 생성
  app.post('/pets', (req, res) => {
    const { name, species, age } = req.body
    
    if (!name || !species) {
      return res.status(400).json({
        code: 'VALIDATION_ERROR',
        message: 'Name and species are required'
      })
    }
    
    const newPet = {
      id: nextId++,
      name,
      species,
      age: age || 0,
      status: 'available'
    }
    
    pets.push(newPet)
    res.status(201).json(newPet)
  })

  // 펫 정보 업데이트
  app.put('/pets/:id', (req, res) => {
    const id = parseInt(req.params.id)
    const petIndex = pets.findIndex(p => p.id === id)
    
    if (petIndex === -1) {
      return res.status(404).json({
        code: 'RESOURCE_NOT_FOUND',
        message: 'Pet not found',
        petId: id
      })
    }
    
    const { name, species, age, status } = req.body
    const updatedPet = {
      ...pets[petIndex],
      name: name !== undefined ? name : pets[petIndex].name,
      species: species !== undefined ? species : pets[petIndex].species,
      age: age !== undefined ? age : pets[petIndex].age,
      status: status !== undefined ? status : pets[petIndex].status
    }
    
    pets[petIndex] = updatedPet
    res.json(updatedPet)
  })

  // 펫 삭제
  app.delete('/pets/:id', (req, res) => {
    const id = parseInt(req.params.id)
    const petIndex = pets.findIndex(p => p.id === id)
    
    if (petIndex === -1) {
      return res.status(404).json({
        code: 'RESOURCE_NOT_FOUND',
        message: 'Pet not found',
        petId: id
      })
    }
    
    pets.splice(petIndex, 1)
    res.status(204).end()
  })

  // 서버 시작
  const server = app.listen(port, () => {
    console.log(`Petstore server running on http://localhost:${port}`)
  })

  return server
}

module.exports = { createPetstoreServer } 
```

--------------------------------------------------------------------------------
/src/openapi-mcp-server/client/__tests__/http-client.test.ts:
--------------------------------------------------------------------------------

```typescript
import { HttpClient, HttpClientError } from '../http-client'
import { OpenAPIV3 } from 'openapi-types'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import OpenAPIClientAxios from 'openapi-client-axios'

// Mock the OpenAPIClientAxios initialization
vi.mock('openapi-client-axios', () => {
  const mockApi = {
    getPet: vi.fn(),
    testOperation: vi.fn(),
    complexOperation: vi.fn(),
  }
  return {
    default: vi.fn().mockImplementation(() => ({
      init: vi.fn().mockResolvedValue(mockApi),
    })),
  }
})

describe('HttpClient', () => {
  let client: HttpClient
  let mockApi: any

  const sampleSpec: OpenAPIV3.Document = {
    openapi: '3.0.0',
    info: { title: 'Test API', version: '1.0.0' },
    paths: {
      '/pets/{petId}': {
        get: {
          operationId: 'getPet',
          parameters: [
            {
              name: 'petId',
              in: 'path',
              required: true,
              schema: { type: 'integer' },
            },
          ],
          responses: {
            '200': {
              description: 'OK',
              content: {
                'application/json': {
                  schema: { type: 'object' },
                },
              },
            },
          },
        },
      },
    },
  }

  const getPetOperation = sampleSpec.paths['/pets/{petId}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
  if (!getPetOperation) {
    throw new Error('Test setup error: getPet operation not found in sample spec')
  }

  beforeEach(async () => {
    // Create a new instance of HttpClient
    client = new HttpClient({ baseUrl: 'https://api.example.com' }, sampleSpec)
    // Await the initialization to ensure mockApi is set correctly
    mockApi = await client['api']
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  it('successfully executes an operation', async () => {
    const mockResponse = {
      data: { id: 1, name: 'Fluffy' },
      status: 200,
      headers: {
        'content-type': 'application/json',
      },
    }

    mockApi.getPet.mockResolvedValueOnce(mockResponse)

    const response = await client.executeOperation(getPetOperation, { petId: 1 })

    // Note GET requests should have a null Content-Type header!
    expect(mockApi.getPet).toHaveBeenCalledWith({ petId: 1 }, undefined, { headers: { 'Content-Type': null } })
    expect(response.data).toEqual(mockResponse.data)
    expect(response.status).toBe(200)
    expect(response.headers).toBeInstanceOf(Headers)
    expect(response.headers.get('content-type')).toBe('application/json')
  })

  it('throws error when operation ID is missing', async () => {
    const operationWithoutId: OpenAPIV3.OperationObject & { method: string; path: string } = {
      method: 'GET',
      path: '/unknown',
      responses: {
        '200': {
          description: 'OK',
        },
      },
    }

    await expect(client.executeOperation(operationWithoutId)).rejects.toThrow('Operation ID is required')
  })

  it('throws error when operation is not found', async () => {
    const operation: OpenAPIV3.OperationObject & { method: string; path: string } = {
      method: 'GET',
      path: '/unknown',
      operationId: 'nonexistentOperation',
      responses: {
        '200': {
          description: 'OK',
        },
      },
    }

    await expect(client.executeOperation(operation)).rejects.toThrow('Operation nonexistentOperation not found')
  })

  it('handles API errors correctly', async () => {
    const error = {
      response: {
        status: 404,
        statusText: 'Not Found',
        data: {
          code: 'RESOURCE_NOT_FOUND',
          message: 'Pet not found',
          petId: 999,
        },
        headers: {
          'content-type': 'application/json',
        },
      },
    }
    mockApi.getPet.mockRejectedValueOnce(error)

    await expect(client.executeOperation(getPetOperation, { petId: 999 })).rejects.toMatchObject({
      status: 404,
      message: '404 Not Found',
      data: {
        code: 'RESOURCE_NOT_FOUND',
        message: 'Pet not found',
        petId: 999,
      },
    })
  })

  it('handles validation errors (400) correctly', async () => {
    const error = {
      response: {
        status: 400,
        statusText: 'Bad Request',
        data: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid input data',
          errors: [
            {
              field: 'age',
              message: 'Age must be a positive number',
            },
            {
              field: 'name',
              message: 'Name is required',
            },
          ],
        },
        headers: {
          'content-type': 'application/json',
        },
      },
    }
    mockApi.getPet.mockRejectedValueOnce(error)

    await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
      status: 400,
      message: '400 Bad Request',
      data: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid input data',
        errors: [
          {
            field: 'age',
            message: 'Age must be a positive number',
          },
          {
            field: 'name',
            message: 'Name is required',
          },
        ],
      },
    })
  })

  it('handles server errors (500) with HTML response', async () => {
    const error = {
      response: {
        status: 500,
        statusText: 'Internal Server Error',
        data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
        headers: {
          'content-type': 'text/html',
        },
      },
    }
    mockApi.getPet.mockRejectedValueOnce(error)

    await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
      status: 500,
      message: '500 Internal Server Error',
      data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
    })
  })

  it('handles rate limit errors (429)', async () => {
    const error = {
      response: {
        status: 429,
        statusText: 'Too Many Requests',
        data: {
          code: 'RATE_LIMIT_EXCEEDED',
          message: 'Rate limit exceeded',
          retryAfter: 60,
        },
        headers: {
          'content-type': 'application/json',
          'retry-after': '60',
        },
      },
    }
    mockApi.getPet.mockRejectedValueOnce(error)

    await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
      status: 429,
      message: '429 Too Many Requests',
      data: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Rate limit exceeded',
        retryAfter: 60,
      },
    })
  })

  it('should send body parameters in request body for POST operations', async () => {
    // Setup mock API with the new operation
    mockApi.testOperation = vi.fn().mockResolvedValue({
      data: {},
      status: 200,
      headers: {},
    })

    const testSpec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/test': {
          post: {
            operationId: 'testOperation',
            requestBody: {
              content: {
                'application/json': {
                  schema: {
                    type: 'object',
                    properties: {
                      foo: { type: 'string' },
                    },
                  },
                },
              },
            },
            responses: {
              '200': {
                description: 'Success response',
                content: {
                  'application/json': {
                    schema: {
                      type: 'object',
                    },
                  },
                },
              },
            },
          },
        },
      },
    }

    const postOperation = testSpec.paths['/test']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
    if (!postOperation) {
      throw new Error('Test setup error: post operation not found')
    }

    const client = new HttpClient({ baseUrl: 'http://test.com' }, testSpec)

    await client.executeOperation(postOperation, { foo: 'bar' })

    expect(mockApi.testOperation).toHaveBeenCalledWith({}, { foo: 'bar' }, { headers: { 'Content-Type': 'application/json' } })
  })

  it('should handle query, path, and body parameters correctly', async () => {
    mockApi.complexOperation = vi.fn().mockResolvedValue({
      data: { success: true },
      status: 200,
      headers: {
        'content-type': 'application/json',
      },
    })

    const complexSpec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/users/{userId}/posts': {
          post: {
            operationId: 'complexOperation',
            parameters: [
              {
                name: 'userId',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
              {
                name: 'include',
                in: 'query',
                required: false,
                schema: { type: 'string' },
              },
            ],
            requestBody: {
              content: {
                'application/json': {
                  schema: {
                    type: 'object',
                    properties: {
                      title: { type: 'string' },
                      content: { type: 'string' },
                    },
                  },
                },
              },
            },
            responses: {
              '200': {
                description: 'Success response',
                content: {
                  'application/json': {
                    schema: {
                      type: 'object',
                      properties: {
                        success: { type: 'boolean' },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    }

    const complexOperation = complexSpec.paths['/users/{userId}/posts']?.post as OpenAPIV3.OperationObject & {
      method: string
      path: string
    }
    if (!complexOperation) {
      throw new Error('Test setup error: complex operation not found')
    }

    const client = new HttpClient({ baseUrl: 'http://test.com' }, complexSpec)

    await client.executeOperation(complexOperation, {
      // Path parameter
      userId: 123,
      // Query parameter
      include: 'comments',
      // Body parameters
      title: 'Test Post',
      content: 'Test Content',
    })

    expect(mockApi.complexOperation).toHaveBeenCalledWith(
      {
        userId: 123,
        include: 'comments',
      },
      {
        title: 'Test Post',
        content: 'Test Content',
      },
      { headers: { 'Content-Type': 'application/json' } },
    )
  })

  const mockOpenApiSpec: OpenAPIV3.Document = {
    openapi: '3.0.0',
    info: { title: 'Test API', version: '1.0.0' },
    paths: {
      '/test': {
        post: {
          operationId: 'testOperation',
          parameters: [
            {
              name: 'queryParam',
              in: 'query',
              schema: { type: 'string' },
            },
            {
              name: 'pathParam',
              in: 'path',
              schema: { type: 'string' },
            },
          ],
          requestBody: {
            content: {
              'application/json': {
                schema: {
                  type: 'object',
                  properties: {
                    bodyParam: { type: 'string' },
                  },
                },
              },
            },
          },
          responses: {
            '200': {
              description: 'Success',
            },
            '400': {
              description: 'Bad Request',
            },
          },
        },
      },
    },
  }

  const mockConfig = {
    baseUrl: 'http://test-api.com',
  }

  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should properly propagate structured error responses', async () => {
    const errorResponse = {
      response: {
        data: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid input',
          details: ['Field x is required'],
        },
        status: 400,
        statusText: 'Bad Request',
        headers: {
          'content-type': 'application/json',
        },
      },
    }

    // Mock axios instance
    const mockAxiosInstance = {
      testOperation: vi.fn().mockRejectedValue(errorResponse),
    }

    // Mock the OpenAPIClientAxios initialization
    const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
      init: () => Promise.resolve(mockAxiosInstance),
    }))

    vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios())

    const client = new HttpClient(mockConfig, mockOpenApiSpec)
    const operation = mockOpenApiSpec.paths['/test']?.post
    if (!operation) {
      throw new Error('Operation not found in mock spec')
    }

    try {
      await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {})
      // Should not reach here
      expect(true).toBe(false)
    } catch (error: any) {
      expect(error.status).toBe(400)
      expect(error.data).toEqual({
        code: 'VALIDATION_ERROR',
        message: 'Invalid input',
        details: ['Field x is required'],
      })
      expect(error.message).toBe('400 Bad Request')
    }
  })

  it('should handle query, path, and body parameters correctly', async () => {
    const mockAxiosInstance = {
      testOperation: vi.fn().mockResolvedValue({
        data: { success: true },
        status: 200,
        headers: { 'content-type': 'application/json' },
      }),
    }

    const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
      init: () => Promise.resolve(mockAxiosInstance),
    }))

    vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios())

    const client = new HttpClient(mockConfig, mockOpenApiSpec)
    const operation = mockOpenApiSpec.paths['/test']?.post
    if (!operation) {
      throw new Error('Operation not found in mock spec')
    }

    const response = await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
      queryParam: 'query1',
      pathParam: 'path1',
      bodyParam: 'body1',
    })

    expect(mockAxiosInstance.testOperation).toHaveBeenCalledWith(
      {
        queryParam: 'query1',
        pathParam: 'path1',
      },
      {
        bodyParam: 'body1',
      },
      { headers: { 'Content-Type': 'application/json' } },
    )

    // Additional check to ensure headers are correctly processed
    expect(response.headers.get('content-type')).toBe('application/json')
  })
})

```

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

```typescript
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
import type { JSONSchema7 as IJsonSchema } from 'json-schema'
import type { ChatCompletionTool } from 'openai/resources/chat/completions'
import type { Tool } from '@anthropic-ai/sdk/resources/messages/messages'

type NewToolMethod = {
  name: string
  description: string
  inputSchema: IJsonSchema & { type: 'object' }
  returnSchema?: IJsonSchema
}

type FunctionParameters = {
  type: 'object'
  properties?: Record<string, unknown>
  required?: string[]
  [key: string]: unknown
}

export class OpenAPIToMCPConverter {
  private schemaCache: Record<string, IJsonSchema> = {}
  private nameCounter: number = 0

  constructor(private openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {}

  /**
   * Resolve a $ref reference to its schema in the openApiSpec.
   * Returns the raw OpenAPI SchemaObject or null if not found.
   */
  private internalResolveRef(ref: string, resolvedRefs: Set<string>): OpenAPIV3.SchemaObject | null {
    if (!ref.startsWith('#/')) {
      return null
    }
    if (resolvedRefs.has(ref)) {
      return null
    }

    const parts = ref.replace(/^#\//, '').split('/')
    let current: any = this.openApiSpec
    for (const part of parts) {
      current = current[part]
      if (!current) return null
    }
    resolvedRefs.add(ref)
    return current as OpenAPIV3.SchemaObject
  }

  /**
   * Convert an OpenAPI schema (or reference) into a JSON Schema object.
   * Uses caching and handles cycles by returning $ref nodes.
   */
  convertOpenApiSchemaToJsonSchema(
    schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
    resolvedRefs: Set<string>,
    resolveRefs: boolean = false,
  ): IJsonSchema {
    if ('$ref' in schema) {
      const ref = schema.$ref
      if (!resolveRefs) {
        if (ref.startsWith('#/components/schemas/')) {
          return {
            $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
            ...('description' in schema ? { description: schema.description as string } : {}),
          }
        }
        console.error(`Attempting to resolve ref ${ref} not found in components collection.`)
        // deliberate fall through
      }
      // Create base schema with $ref and description if present
      const refSchema: IJsonSchema = { $ref: ref }
      if ('description' in schema && schema.description) {
        refSchema.description = schema.description as string
      }

      // If already cached, return immediately with description
      if (this.schemaCache[ref]) {
        return this.schemaCache[ref]
      }

      const resolved = this.internalResolveRef(ref, resolvedRefs)
      if (!resolved) {
        // TODO: need extensive tests for this and we definitely need to handle the case of self references
        console.error(`Failed to resolve ref ${ref}`)
        return {
          $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
          description: 'description' in schema ? ((schema.description as string) ?? '') : '',
        }
      } else {
        const converted = this.convertOpenApiSchemaToJsonSchema(resolved, resolvedRefs, resolveRefs)
        this.schemaCache[ref] = converted

        return converted
      }
    }

    // Handle inline schema
    const result: IJsonSchema = {}

    if (schema.type) {
      result.type = schema.type as IJsonSchema['type']
    }

    // Convert binary format to uri-reference and enhance description
    if (schema.format === 'binary') {
      result.format = 'uri-reference'
      const binaryDesc = 'absolute paths to local files'
      result.description = schema.description ? `${schema.description} (${binaryDesc})` : binaryDesc
    } else {
      if (schema.format) {
        result.format = schema.format
      }
      if (schema.description) {
        result.description = schema.description
      }
    }

    if (schema.enum) {
      result.enum = schema.enum
    }

    if (schema.default !== undefined) {
      result.default = schema.default
    }

    // Handle object properties
    if (schema.type === 'object') {
      result.type = 'object'
      if (schema.properties) {
        result.properties = {}
        for (const [name, propSchema] of Object.entries(schema.properties)) {
          result.properties[name] = this.convertOpenApiSchemaToJsonSchema(propSchema, resolvedRefs, resolveRefs)
        }
      }
      if (schema.required) {
        result.required = schema.required
      }
      if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
        result.additionalProperties = true
      } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
        result.additionalProperties = this.convertOpenApiSchemaToJsonSchema(schema.additionalProperties, resolvedRefs, resolveRefs)
      } else {
        result.additionalProperties = false
      }
    }

    // Handle arrays - ensure binary format conversion happens for array items too
    if (schema.type === 'array' && schema.items) {
      result.type = 'array'
      result.items = this.convertOpenApiSchemaToJsonSchema(schema.items, resolvedRefs, resolveRefs)
    }

    // oneOf, anyOf, allOf
    if (schema.oneOf) {
      result.oneOf = schema.oneOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
    }
    if (schema.anyOf) {
      result.anyOf = schema.anyOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
    }
    if (schema.allOf) {
      result.allOf = schema.allOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
    }

    return result
  }

  convertToMCPTools(): {
    tools: Record<string, { methods: NewToolMethod[] }>
    openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
    zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }>
  } {
    const apiName = 'API'

    const openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }> = {}
    const tools: Record<string, { methods: NewToolMethod[] }> = {
      [apiName]: { methods: [] },
    }
    const zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }> = {}
    for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
      if (!pathItem) continue

      for (const [method, operation] of Object.entries(pathItem)) {
        if (!this.isOperation(method, operation)) continue

        const mcpMethod = this.convertOperationToMCPMethod(operation, method, path)
        if (mcpMethod) {
          const uniqueName = this.ensureUniqueName(mcpMethod.name)
          mcpMethod.name = uniqueName
          tools[apiName]!.methods.push(mcpMethod)
          openApiLookup[apiName + '-' + uniqueName] = { ...operation, method, path }
          zip[apiName + '-' + uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod }
        }
      }
    }

    return { tools, openApiLookup, zip }
  }

  /**
   * Convert the OpenAPI spec to OpenAI's ChatCompletionTool format
   */
  convertToOpenAITools(): ChatCompletionTool[] {
    const tools: ChatCompletionTool[] = []

    for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
      if (!pathItem) continue

      for (const [method, operation] of Object.entries(pathItem)) {
        if (!this.isOperation(method, operation)) continue

        const parameters = this.convertOperationToJsonSchema(operation, method, path)
        const tool: ChatCompletionTool = {
          type: 'function',
          function: {
            name: operation.operationId!,
            description: operation.summary || operation.description || '',
            parameters: parameters as FunctionParameters,
          },
        }
        tools.push(tool)
      }
    }

    return tools
  }

  /**
   * Convert the OpenAPI spec to Anthropic's Tool format
   */
  convertToAnthropicTools(): Tool[] {
    const tools: Tool[] = []

    for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
      if (!pathItem) continue

      for (const [method, operation] of Object.entries(pathItem)) {
        if (!this.isOperation(method, operation)) continue

        const parameters = this.convertOperationToJsonSchema(operation, method, path)
        const tool: Tool = {
          name: operation.operationId!,
          description: operation.summary || operation.description || '',
          input_schema: parameters as Tool['input_schema'],
        }
        tools.push(tool)
      }
    }

    return tools
  }

  private convertComponentsToJsonSchema(): Record<string, IJsonSchema> {
    const components = this.openApiSpec.components || {}
    const schema: Record<string, IJsonSchema> = {}
    for (const [key, value] of Object.entries(components.schemas || {})) {
      schema[key] = this.convertOpenApiSchemaToJsonSchema(value, new Set())
    }
    return schema
  }
  /**
   * Helper method to convert an operation to a JSON Schema for parameters
   */
  private convertOperationToJsonSchema(
    operation: OpenAPIV3.OperationObject,
    method: string,
    path: string,
  ): IJsonSchema & { type: 'object' } {
    const schema: IJsonSchema & { type: 'object' } = {
      type: 'object',
      properties: {},
      required: [],
      $defs: this.convertComponentsToJsonSchema(),
    }

    // Handle parameters (path, query, header, cookie)
    if (operation.parameters) {
      for (const param of operation.parameters) {
        const paramObj = this.resolveParameter(param)
        if (paramObj && paramObj.schema) {
          const paramSchema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set())
          // Merge parameter-level description if available
          if (paramObj.description) {
            paramSchema.description = paramObj.description
          }
          schema.properties![paramObj.name] = paramSchema
          if (paramObj.required) {
            schema.required!.push(paramObj.name)
          }
        }
      }
    }

    // Handle requestBody
    if (operation.requestBody) {
      const bodyObj = this.resolveRequestBody(operation.requestBody)
      if (bodyObj?.content) {
        if (bodyObj.content['application/json']?.schema) {
          const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set())
          if (bodySchema.type === 'object' && bodySchema.properties) {
            for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
              schema.properties![name] = propSchema
            }
            if (bodySchema.required) {
              schema.required!.push(...bodySchema.required)
            }
          }
        }
      }
    }

    return schema
  }

  private isOperation(method: string, operation: any): operation is OpenAPIV3.OperationObject {
    return ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())
  }

  private isParameterObject(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): param is OpenAPIV3.ParameterObject {
    return !('$ref' in param)
  }

  private isRequestBodyObject(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): body is OpenAPIV3.RequestBodyObject {
    return !('$ref' in body)
  }

  private resolveParameter(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ParameterObject | null {
    if (this.isParameterObject(param)) {
      return param
    } else {
      const resolved = this.internalResolveRef(param.$ref, new Set())
      if (resolved && (resolved as OpenAPIV3.ParameterObject).name) {
        return resolved as OpenAPIV3.ParameterObject
      }
    }
    return null
  }

  private resolveRequestBody(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): OpenAPIV3.RequestBodyObject | null {
    if (this.isRequestBodyObject(body)) {
      return body
    } else {
      const resolved = this.internalResolveRef(body.$ref, new Set())
      if (resolved) {
        return resolved as OpenAPIV3.RequestBodyObject
      }
    }
    return null
  }

  private resolveResponse(response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ResponseObject | null {
    if ('$ref' in response) {
      const resolved = this.internalResolveRef(response.$ref, new Set())
      if (resolved) {
        return resolved as OpenAPIV3.ResponseObject
      } else {
        return null
      }
    }
    return response
  }

  private convertOperationToMCPMethod(operation: OpenAPIV3.OperationObject, method: string, path: string): NewToolMethod | null {
    if (!operation.operationId) {
      console.warn(`Operation without operationId at ${method} ${path}`)
      return null
    }

    const methodName = operation.operationId

    const inputSchema: IJsonSchema & { type: 'object' } = {
      $defs: this.convertComponentsToJsonSchema(),
      type: 'object',
      properties: {},
      required: [],
    }

    // Handle parameters (path, query, header, cookie)
    if (operation.parameters) {
      for (const param of operation.parameters) {
        const paramObj = this.resolveParameter(param)
        if (paramObj && paramObj.schema) {
          const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false)
          // Merge parameter-level description if available
          if (paramObj.description) {
            schema.description = paramObj.description
          }
          inputSchema.properties![paramObj.name] = schema
          if (paramObj.required) {
            inputSchema.required!.push(paramObj.name)
          }
        }
      }
    }

    // Handle requestBody
    if (operation.requestBody) {
      const bodyObj = this.resolveRequestBody(operation.requestBody)
      if (bodyObj?.content) {
        // Handle multipart/form-data for file uploads
        // We convert the multipart/form-data schema to a JSON schema and we require
        // that the user passes in a string for each file that points to the local file
        if (bodyObj.content['multipart/form-data']?.schema) {
          const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['multipart/form-data'].schema, new Set(), false)
          if (formSchema.type === 'object' && formSchema.properties) {
            for (const [name, propSchema] of Object.entries(formSchema.properties)) {
              inputSchema.properties![name] = propSchema
            }
            if (formSchema.required) {
              inputSchema.required!.push(...formSchema.required!)
            }
          }
        }
        // Handle application/json
        else if (bodyObj.content['application/json']?.schema) {
          const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set(), false)
          // Merge body schema into the inputSchema's properties
          if (bodySchema.type === 'object' && bodySchema.properties) {
            for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
              inputSchema.properties![name] = propSchema
            }
            if (bodySchema.required) {
              inputSchema.required!.push(...bodySchema.required!)
            }
          } else {
            // If the request body is not an object, just put it under "body"
            inputSchema.properties!['body'] = bodySchema
            inputSchema.required!.push('body')
          }
        }
      }
    }

    // Build description including error responses
    let description = operation.summary || operation.description || ''
    if (operation.responses) {
      const errorResponses = Object.entries(operation.responses)
        .filter(([code]) => code.startsWith('4') || code.startsWith('5'))
        .map(([code, response]) => {
          const responseObj = this.resolveResponse(response)
          let errorDesc = responseObj?.description || ''
          return `${code}: ${errorDesc}`
        })

      if (errorResponses.length > 0) {
        description += '\nError Responses:\n' + errorResponses.join('\n')
      }
    }

    // Extract return type (response schema)
    const returnSchema = this.extractResponseType(operation.responses)

    // Generate Zod schema from input schema
    try {
      // const zodSchemaStr = jsonSchemaToZod(inputSchema, { module: "cjs" })
      // console.log(zodSchemaStr)
      // // Execute the function with the zod instance
      // const zodSchema = eval(zodSchemaStr) as z.ZodType

      return {
        name: methodName,
        description,
        inputSchema,
        ...(returnSchema ? { returnSchema } : {}),
      }
    } catch (error) {
      console.warn(`Failed to generate Zod schema for ${methodName}:`, error)
      // Fallback to a basic object schema
      return {
        name: methodName,
        description,
        inputSchema,
        ...(returnSchema ? { returnSchema } : {}),
      }
    }
  }

  private extractResponseType(responses: OpenAPIV3.ResponsesObject | undefined): IJsonSchema | null {
    // Look for a success response
    const successResponse = responses?.['200'] || responses?.['201'] || responses?.['202'] || responses?.['204']
    if (!successResponse) return null

    const responseObj = this.resolveResponse(successResponse)
    if (!responseObj || !responseObj.content) return null

    if (responseObj.content['application/json']?.schema) {
      const returnSchema = this.convertOpenApiSchemaToJsonSchema(responseObj.content['application/json'].schema, new Set(), false)
      returnSchema['$defs'] = this.convertComponentsToJsonSchema()

      // Preserve the response description if available and not already set
      if (responseObj.description && !returnSchema.description) {
        returnSchema.description = responseObj.description
      }

      return returnSchema
    }

    // If no JSON response, fallback to a generic string or known formats
    if (responseObj.content['image/png'] || responseObj.content['image/jpeg']) {
      return { type: 'string', format: 'binary', description: responseObj.description || '' }
    }

    // Fallback
    return { type: 'string', description: responseObj.description || '' }
  }

  private ensureUniqueName(name: string): string {
    if (name.length <= 64) {
      return name
    }

    const truncatedName = name.slice(0, 64 - 5) // Reserve space for suffix
    const uniqueSuffix = this.generateUniqueSuffix()
    return `${truncatedName}-${uniqueSuffix}`
  }

  private generateUniqueSuffix(): string {
    this.nameCounter += 1
    return this.nameCounter.toString().padStart(4, '0')
  }
}

```

--------------------------------------------------------------------------------
/scripts/notion-openapi.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Notion API",
    "version": "1"
  },
  "servers": [
    {
      "url": "https://api.notion.com"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      },
      "basicAuth": {
        "type": "http",
        "scheme": "basic"
      }
    },
    "parameters": {},
    "schemas": {}
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/blocks/{block_id}/children": {
      "get": {
        "summary": "Retrieve block children",
        "description": "",
        "operationId": "get-block-children",
        "parameters": [
          {
            "name": "block_id",
            "in": "path",
            "description": "Identifier for a [block](ref:block)",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "start_cursor",
            "in": "query",
            "description": "If supplied, this endpoint will return a page of results starting after the cursor provided. If not supplied, this endpoint will return the first page of results.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "description": "The number of items from the full list desired in the response. Maximum: 100",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 100
            }
          }
        ],
        "responses": {},
        "deprecated": false,
        "security": []
      }
    },
    "/v1/pages/{page_id}": {
      "get": {
        "summary": "Retrieve a page",
        "description": "",
        "operationId": "retrieve-a-page",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "description": "Identifier for a Notion page",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "filter_properties",
            "in": "query",
            "description": "A list of page property value IDs associated with the page. Use this param to limit the response to a specific page property value or values. To retrieve multiple properties, specify each page property ID. For example: `?filter_properties=iAk8&filter_properties=b7dh`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {},
        "deprecated": false,
        "security": []
      }
    },
    "/v1/blocks/{block_id}": {
      "get": {
        "summary": "Retrieve a block",
        "description": "",
        "operationId": "retrieve-a-block",
        "parameters": [
          {
            "name": "block_id",
            "in": "path",
            "description": "Identifier for a Notion block",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {},
        "deprecated": false,
        "security": []
      }
    },
    "/v1/databases/{database_id}": {
      "get": {
        "summary": "Retrieve a database",
        "description": "",
        "operationId": "retrieve-a-database",
        "parameters": [
          {
            "name": "database_id",
            "in": "path",
            "description": "An identifier for the Notion database.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "200",
            "content": {
              "application/json": {
                "examples": {
                  "Result": {
                    "value": "{\n    \"object\": \"database\",\n    \"id\": \"bc1211ca-e3f1-4939-ae34-5260b16f627c\",\n    \"created_time\": \"2021-07-08T23:50:00.000Z\",\n    \"last_edited_time\": \"2021-07-08T23:50:00.000Z\",\n    \"icon\": {\n        \"type\": \"emoji\",\n        \"emoji\": \"🎉\"\n    },\n    \"cover\": {\n        \"type\": \"external\",\n        \"external\": {\n            \"url\": \"https://website.domain/images/image.png\"\n        }\n    },\n    \"url\": \"https://www.notion.so/bc1211cae3f14939ae34260b16f627c\",\n    \"title\": [\n        {\n            \"type\": \"text\",\n            \"text\": {\n                \"content\": \"Grocery List\",\n                \"link\": null\n            },\n            \"annotations\": {\n                \"bold\": false,\n                \"italic\": false,\n                \"strikethrough\": false,\n                \"underline\": false,\n                \"code\": false,\n                \"color\": \"default\"\n            },\n            \"plain_text\": \"Grocery List\",\n            \"href\": null\n        }\n    ],\n    \"description\": [\n        {\n            \"type\": \"text\",\n            \"text\": {\n                \"content\": \"Grocery list for just kale 🥬\",\n                \"link\": null\n            },\n            \"annotations\": {\n                \"bold\": false,\n                \"italic\": false,\n                \"strikethrough\": false,\n                \"underline\": false,\n                \"code\": false,\n                \"color\": \"default\"\n            },\n            \"plain_text\": \"Grocery list for just kale 🥬\",\n            \"href\": null\n        }\n    ],\n    \"properties\": {\n        \"+1\": {\n            \"id\": \"Wp%3DC\",\n            \"name\": \"+1\",\n            \"type\": \"people\",\n            \"people\": {}\n        },\n        \"In stock\": {\n            \"id\": \"fk%5EY\",\n            \"name\": \"In stock\",\n            \"type\": \"checkbox\",\n            \"checkbox\": {}\n        },\n        \"Price\": {\n            \"id\": \"evWq\",\n            \"name\": \"Price\",\n            \"type\": \"number\",\n            \"number\": {\n                \"format\": \"dollar\"\n            }\n        },\n        \"Description\": {\n            \"id\": \"V}lX\",\n            \"name\": \"Description\",\n            \"type\": \"rich_text\",\n            \"rich_text\": {}\n        },\n        \"Last ordered\": {\n            \"id\": \"eVnV\",\n            \"name\": \"Last ordered\",\n            \"type\": \"date\",\n            \"date\": {}\n        },\n        \"Meals\": {\n            \"id\": \"%7DWA~\",\n            \"name\": \"Meals\",\n            \"type\": \"relation\",\n            \"relation\": {\n                \"database_id\": \"668d797c-76fa-4934-9b05-ad288df2d136\",\n                \"synced_property_name\": \"Related to Grocery List (Meals)\"\n            }\n        },\n        \"Number of meals\": {\n            \"id\": \"Z\\\\Eh\",\n            \"name\": \"Number of meals\",\n            \"type\": \"rollup\",\n            \"rollup\": {\n                \"rollup_property_name\": \"Name\",\n                \"relation_property_name\": \"Meals\",\n                \"rollup_property_id\": \"title\",\n                \"relation_property_id\": \"mxp^\",\n                \"function\": \"count\"\n            }\n        },\n        \"Store availability\": {\n            \"id\": \"s}Kq\",\n            \"name\": \"Store availability\",\n            \"type\": \"multi_select\",\n            \"multi_select\": {\n                \"options\": [\n                    {\n                        \"id\": \"cb79b393-d1c1-4528-b517-c450859de766\",\n                        \"name\": \"Duc Loi Market\",\n                        \"color\": \"blue\"\n                    },\n                    {\n                        \"id\": \"58aae162-75d4-403b-a793-3bc7308e4cd2\",\n                        \"name\": \"Rainbow Grocery\",\n                        \"color\": \"gray\"\n                    },\n                    {\n                        \"id\": \"22d0f199-babc-44ff-bd80-a9eae3e3fcbf\",\n                        \"name\": \"Nijiya Market\",\n                        \"color\": \"purple\"\n                    },\n                    {\n                        \"id\": \"0d069987-ffb0-4347-bde2-8e4068003dbc\",\n                        \"name\": \"Gus's Community Market\",\n                        \"color\": \"yellow\"\n                    }\n                ]\n            }\n        },\n        \"Photo\": {\n            \"id\": \"yfiK\",\n            \"name\": \"Photo\",\n            \"type\": \"files\",\n            \"files\": {}\n        },\n        \"Food group\": {\n            \"id\": \"CM%3EH\",\n            \"name\": \"Food group\",\n            \"type\": \"select\",\n            \"select\": {\n                \"options\": [\n                    {\n                        \"id\": \"6d4523fa-88cb-4ffd-9364-1e39d0f4e566\",\n                        \"name\": \"🥦Vegetable\",\n                        \"color\": \"green\"\n                    },\n                    {\n                        \"id\": \"268d7e75-de8f-4c4b-8b9d-de0f97021833\",\n                        \"name\": \"🍎Fruit\",\n                        \"color\": \"red\"\n                    },\n                    {\n                        \"id\": \"1b234a00-dc97-489c-b987-829264cfdfef\",\n                        \"name\": \"💪Protein\",\n                        \"color\": \"yellow\"\n                    }\n                ]\n            }\n        },\n        \"Name\": {\n            \"id\": \"title\",\n            \"name\": \"Name\",\n            \"type\": \"title\",\n            \"title\": {}\n        }\n    },\n    \"parent\": {\n        \"type\": \"page_id\",\n        \"page_id\": \"98ad959b-2b6a-4774-80ee-00246fb0ea9b\"\n    },\n    \"archived\": false,\n    \"is_inline\": false,\n    \"public_url\": null\n}"
                  }
                }
              }
            }
          }
        },
        "deprecated": false,
        "security": []
      }
    },
    "/v1/comments": {
      "get": {
        "summary": "Retrieve comments",
        "description": "Retrieves a list of un-resolved [Comment objects](ref:comment-object) from a page or block.",
        "operationId": "retrieve-a-comment",
        "parameters": [
          {
            "name": "block_id",
            "in": "query",
            "description": "Identifier for a Notion block or page",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "start_cursor",
            "in": "query",
            "description": "If supplied, this endpoint will return a page of results starting after the cursor provided. If not supplied, this endpoint will return the first page of results.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "description": "The number of items from the full list desired in the response. Maximum: 100",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "200",
            "content": {
              "application/json": {
                "examples": {
                  "OK": {
                    "value": "{\n    \"object\": \"list\",\n    \"results\": [\n        {\n            \"object\": \"comment\",\n            \"id\": \"94cc56ab-9f02-409d-9f99-1037e9fe502f\",\n            \"parent\": {\n                \"type\": \"page_id\",\n                \"page_id\": \"5c6a2821-6bb1-4a7e-b6e1-c50111515c3d\"\n            },\n            \"discussion_id\": \"f1407351-36f5-4c49-a13c-49f8ba11776d\",\n            \"created_time\": \"2022-07-15T16:52:00.000Z\",\n            \"last_edited_time\": \"2022-07-15T19:16:00.000Z\",\n            \"created_by\": {\n                \"object\": \"user\",\n                \"id\": \"9b15170a-9941-4297-8ee6-83fa7649a87a\"\n            },\n            \"rich_text\": [\n                {\n                    \"type\": \"text\",\n                    \"text\": {\n                        \"content\": \"Single comment\",\n                        \"link\": null\n                    },\n                    \"annotations\": {\n                        \"bold\": false,\n                        \"italic\": false,\n                        \"strikethrough\": false,\n                        \"underline\": false,\n                        \"code\": false,\n                        \"color\": \"default\"\n                    },\n                    \"plain_text\": \"Single comment\",\n                    \"href\": null\n                }\n            ]\n        }\n    ],\n    \"next_cursor\": null,\n    \"has_more\": false,\n    \"type\": \"comment\",\n    \"comment\": {}\n}"
                  }
                }
              }
            }
          }
        },
        "deprecated": false,
        "security": []
      }
    },
    "/v1/pages/{page_id}/properties/{property_id}": {
      "get": {
        "summary": "Retrieve a page property item",
        "description": "",
        "operationId": "retrieve-a-page-property",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "description": "Identifier for a Notion page",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "property_id",
            "in": "path",
            "description": "Identifier for a page [property](https://developers.notion.com/reference/page#all-property-values)",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "page_size",
            "in": "query",
            "description": "For paginated properties. The max number of property item objects on a page. The default size is 100",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "start_cursor",
            "in": "query",
            "description": "For paginated properties.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "200",
            "content": {
              "application/json": {
                "examples": {
                  "Number Property Item": {
                    "value": "{\n  \"object\": \"property_item\",\n  \"id\" \"kjPO\",\n  \"type\": \"number\",\n  \"number\": 2\n}"
                  },
                  "Result": {
                    "value": "{\n    \"object\": \"list\",\n    \"results\": [\n        {\n            \"object\": \"property_item\",\n            \"id\" \"kjPO\",\n            \"type\": \"rich_text\",\n            \"rich_text\": {\n                \"type\": \"text\",\n                \"text\": {\n                    \"content\": \"Avocado \",\n                    \"link\": null\n                },\n                \"annotations\": {\n                    \"bold\": false,\n                    \"italic\": false,\n                    \"strikethrough\": false,\n                    \"underline\": false,\n                    \"code\": false,\n                    \"color\": \"default\"\n                },\n                \"plain_text\": \"Avocado \",\n                \"href\": null\n            }\n        },\n        {\n            \"object\": \"property_item\",\n            \"id\" \"ijPO\",\n            \"type\": \"rich_text\",\n            \"rich_text\": {\n                \"type\": \"mention\",\n                \"mention\": {\n                    \"type\": \"page\",\n                    \"page\": {\n                        \"id\": \"41117fd7-69a5-4694-bc07-c1e3a682c857\"\n                    }\n                },\n                \"annotations\": {\n                    \"bold\": false,\n                    \"italic\": false,\n                    \"strikethrough\": false,\n                    \"underline\": false,\n                    \"code\": false,\n                    \"color\": \"default\"\n                },\n                \"plain_text\": \"Lemons\",\n                \"href\": \"http://notion.so/41117fd769a54694bc07c1e3a682c857\"\n            }\n        },\n        {\n            \"object\": \"property_item\",\n            \"id\" \"kjPO\",\n            \"type\": \"rich_text\",\n            \"rich_text\": {\n                \"type\": \"text\",\n                \"text\": {\n                    \"content\": \" Tomato \",\n                    \"link\": null\n                },\n                \"annotations\": {\n                    \"bold\": false,\n                    \"italic\": false,\n                    \"strikethrough\": false,\n                    \"underline\": false,\n                    \"code\": false,\n                    \"color\": \"default\"\n                },\n                \"plain_text\": \" Tomato \",\n                \"href\": null\n            }\n        },\n...\n    ],\n    \"next_cursor\": \"some-next-cursor-value\",\n    \"has_more\": true,\n\t\t\"next_url\": \"http://api.notion.com/v1/pages/0e5235bf86aa4efb93aa772cce7eab71/properties/NVv^?start_cursor=some-next-cursor-value&page_size=25\",\n    \"property_item\": {\n      \"id\": \"NVv^\",\n      \"next_url\": null,\n      \"type\": \"rich_text\",\n      \"rich_text\": {}\n    }\n}"
                  },
                  "Rollup List Property Item": {
                    "value": "{\n    \"object\": \"list\",\n    \"results\": [\n        {\n            \"object\": \"property_item\",\n          \t\"id\": \"dj2l\",\n            \"type\": \"relation\",\n            \"relation\": {\n                \"id\": \"83f92c9d-523d-466e-8c1f-9bc2c25a99fe\"\n            }\n        },\n        {\n            \"object\": \"property_item\",\n          \t\"id\": \"dj2l\",\n            \"type\": \"relation\",\n            \"relation\": {\n                \"id\": \"45cfb825-3463-4891-8932-7e6d8c170630\"\n            }\n        },\n        {\n            \"object\": \"property_item\",\n          \t\"id\": \"dj2l\",\n            \"type\": \"relation\",\n            \"relation\": {\n                \"id\": \"1688be1a-a197-4f2a-9688-e528c4b56d94\"\n            }\n        }\n    ],\n    \"next_cursor\": \"some-next-cursor-value\",\n    \"has_more\": true,\n\t\t\"property_item\": {\n      \"id\": \"y}~p\",\n      \"next_url\": \"http://api.notion.com/v1/pages/0e5235bf86aa4efb93aa772cce7eab71/properties/y%7D~p?start_cursor=1QaTunT5&page_size=25\",\n      \"type\": \"rollup\",\n      \"rollup\": {\n        \"function\": \"sum\",\n        \"type\": \"incomplete\",\n        \"incomplete\": {}\n      }\n    }\n    \"type\": \"property_item\"\n}"
                  }
                }
              }
            }
          }
        },
        "deprecated": false,
        "security": []
      }
    }
  }
}

```

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

```typescript
import { OpenAPIV3 } from 'openapi-types'
import { describe, it, expect } from 'vitest'
import { OpenAPIToMCPConverter } from '../parser'

describe('OpenAPI Multipart Form Parser', () => {
  it('converts single file upload endpoint to tool', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/photo': {
          post: {
            operationId: 'uploadPetPhoto',
            summary: 'Upload a photo for a pet',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['photo'],
                    properties: {
                      photo: {
                        type: 'string',
                        format: 'binary',
                        description: 'The photo to upload',
                      },
                      caption: {
                        type: 'string',
                        description: 'Optional caption for the photo',
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Photo uploaded successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    expect(Object.keys(tools)).toHaveLength(1)

    const [tool] = Object.values(tools)
    expect(tool.methods).toHaveLength(1)
    const [method] = tool.methods
    expect(method.name).toBe('uploadPetPhoto')
    expect(method.description).toContain('Upload a photo for a pet')

    // Check parameters
    expect(method.inputSchema.properties).toEqual({
      id: {
        type: 'integer',
      },
      photo: {
        type: 'string',
        format: 'uri-reference',
        description: expect.stringContaining('The photo to upload (absolute paths to local files)'),
      },
      caption: {
        type: 'string',
        description: expect.stringContaining('Optional caption'),
      },
    })

    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('photo')
    expect(method.inputSchema.required).not.toContain('caption')
  })

  it('converts multiple file upload endpoint to tool', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/documents': {
          post: {
            operationId: 'uploadPetDocuments',
            summary: 'Upload multiple documents for a pet',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['documents'],
                    properties: {
                      documents: {
                        type: 'array',
                        items: {
                          type: 'string',
                          format: 'binary',
                        },
                        description: 'The documents to upload (max 5 files)',
                      },
                      tags: {
                        type: 'array',
                        items: {
                          type: 'string',
                        },
                        description: 'Optional tags for the documents',
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Documents uploaded successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    expect(Object.keys(tools)).toHaveLength(1)

    const [tool] = Object.values(tools)
    expect(tool.methods).toHaveLength(1)
    const [method] = tool.methods
    expect(method.name).toBe('uploadPetDocuments')
    expect(method.description).toContain('Upload multiple documents')

    // Check parameters
    expect(method.inputSchema.properties).toEqual({
      id: {
        type: 'integer',
      },
      documents: {
        type: 'array',
        items: {
          type: 'string',
          format: 'uri-reference',
          description: 'absolute paths to local files',
        },
        description: expect.stringContaining('max 5 files'),
      },
      tags: {
        type: 'array',
        items: {
          type: 'string',
        },
        description: expect.stringContaining('Optional tags'),
      },
    })

    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('documents')
    expect(method.inputSchema.required).not.toContain('tags')
  })

  it('handles complex multipart forms with mixed content', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/profile': {
          post: {
            operationId: 'updatePetProfile',
            summary: 'Update pet profile with images and data',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['avatar', 'details'],
                    properties: {
                      avatar: {
                        type: 'string',
                        format: 'binary',
                        description: 'Profile picture',
                      },
                      gallery: {
                        type: 'array',
                        items: {
                          type: 'string',
                          format: 'binary',
                        },
                        description: 'Additional pet photos',
                      },
                      details: {
                        type: 'object',
                        properties: {
                          name: { type: 'string' },
                          age: { type: 'integer' },
                          breed: { type: 'string' },
                        },
                      },
                      preferences: {
                        type: 'array',
                        items: {
                          type: 'object',
                          properties: {
                            category: { type: 'string' },
                            value: { type: 'string' },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '200': {
                description: 'Profile updated successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    expect(Object.keys(tools)).toHaveLength(1)

    const [tool] = Object.values(tools)
    expect(tool.methods).toHaveLength(1)
    const [method] = tool.methods
    expect(method.name).toBe('updatePetProfile')
    expect(method.description).toContain('Update pet profile')

    // Check parameters
    expect(method.inputSchema.properties).toEqual({
      id: {
        type: 'integer',
      },
      avatar: {
        type: 'string',
        format: 'uri-reference',
        description: expect.stringContaining('Profile picture (absolute paths to local files)'),
      },
      gallery: {
        type: 'array',
        items: {
          type: 'string',
          format: 'uri-reference',
          description: 'absolute paths to local files',
        },
        description: expect.stringContaining('Additional pet photos'),
      },
      details: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          age: { type: 'integer' },
          breed: { type: 'string' },
        },
        additionalProperties: true,
      },
      preferences: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            category: { type: 'string' },
            value: { type: 'string' },
          },
          additionalProperties: true,
        },
      },
    })

    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('avatar')
    expect(method.inputSchema.required).toContain('details')
    expect(method.inputSchema.required).not.toContain('gallery')
    expect(method.inputSchema.required).not.toContain('preferences')
  })

  it('handles optional file uploads in multipart forms', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/metadata': {
          post: {
            operationId: 'updatePetMetadata',
            summary: 'Update pet metadata with optional attachments',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['metadata'],
                    properties: {
                      metadata: {
                        type: 'object',
                        required: ['name'],
                        properties: {
                          name: { type: 'string' },
                          description: { type: 'string' },
                        },
                      },
                      certificate: {
                        type: 'string',
                        format: 'binary',
                        description: 'Optional pet certificate',
                      },
                      vaccinations: {
                        type: 'array',
                        items: {
                          type: 'string',
                          format: 'binary',
                        },
                        description: 'Optional vaccination records',
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '200': {
                description: 'Metadata updated successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    const [tool] = Object.values(tools)
    const [method] = tool.methods

    expect(method.name).toBe('updatePetMetadata')
    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('metadata')
    expect(method.inputSchema.required).not.toContain('certificate')
    expect(method.inputSchema.required).not.toContain('vaccinations')

    expect(method.inputSchema.properties).toEqual({
      id: {
        type: 'integer',
      },
      metadata: {
        type: 'object',
        required: ['name'],
        properties: {
          name: { type: 'string' },
          description: { type: 'string' },
        },
        additionalProperties: true,
      },
      certificate: {
        type: 'string',
        format: 'uri-reference',
        description: expect.stringContaining('Optional pet certificate (absolute paths to local files)'),
      },
      vaccinations: {
        type: 'array',
        items: {
          type: 'string',
          format: 'uri-reference',
          description: 'absolute paths to local files',
        },
        description: expect.stringContaining('Optional vaccination records'),
      },
    })
  })

  it('handles nested objects with file arrays in multipart forms', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/medical-records': {
          post: {
            operationId: 'addMedicalRecord',
            summary: 'Add medical record with attachments',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['record'],
                    properties: {
                      record: {
                        type: 'object',
                        required: ['date', 'type'],
                        properties: {
                          date: { type: 'string', format: 'date' },
                          type: { type: 'string' },
                          notes: { type: 'string' },
                          attachments: {
                            type: 'array',
                            items: {
                              type: 'object',
                              required: ['file', 'type'],
                              properties: {
                                file: {
                                  type: 'string',
                                  format: 'binary',
                                },
                                type: {
                                  type: 'string',
                                  enum: ['xray', 'lab', 'prescription'],
                                },
                                description: { type: 'string' },
                              },
                            },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Medical record added successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    const [tool] = Object.values(tools)
    const [method] = tool.methods

    expect(method.name).toBe('addMedicalRecord')
    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('record')

    // Verify nested structure is preserved
    const recordSchema = method.inputSchema.properties!.record as any
    expect(recordSchema.type).toBe('object')
    expect(recordSchema.required).toContain('date')
    expect(recordSchema.required).toContain('type')

    // Verify nested file array structure
    const attachmentsSchema = recordSchema.properties.attachments
    expect(attachmentsSchema.type).toBe('array')
    expect(attachmentsSchema.items.type).toBe('object')
    expect(attachmentsSchema.items.properties.file.format).toBe('uri-reference')
    expect(attachmentsSchema.items.properties.file.description).toBe('absolute paths to local files')
    expect(attachmentsSchema.items.required).toContain('file')
    expect(attachmentsSchema.items.required).toContain('type')
  })

  it('handles oneOf/anyOf schemas with file uploads', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/pets/{id}/content': {
          post: {
            operationId: 'addPetContent',
            summary: 'Add pet content (photo or document)',
            parameters: [
              {
                name: 'id',
                in: 'path',
                required: true,
                schema: { type: 'integer' },
              },
            ],
            requestBody: {
              required: true,
              content: {
                'multipart/form-data': {
                  schema: {
                    type: 'object',
                    required: ['content'],
                    properties: {
                      content: {
                        oneOf: [
                          {
                            type: 'object',
                            required: ['photo', 'isProfile'],
                            properties: {
                              photo: {
                                type: 'string',
                                format: 'binary',
                              },
                              isProfile: {
                                type: 'boolean',
                              },
                            },
                          },
                          {
                            type: 'object',
                            required: ['document', 'category'],
                            properties: {
                              document: {
                                type: 'string',
                                format: 'binary',
                              },
                              category: {
                                type: 'string',
                                enum: ['medical', 'training', 'adoption'],
                              },
                            },
                          },
                        ],
                      },
                    },
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Content added successfully',
              },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const { tools } = converter.convertToMCPTools()
    const [tool] = Object.values(tools)
    const [method] = tool.methods

    expect(method.name).toBe('addPetContent')
    expect(method.inputSchema.required).toContain('id')
    expect(method.inputSchema.required).toContain('content')

    // Verify oneOf structure is preserved
    const contentSchema = method.inputSchema.properties!.content as any
    expect(contentSchema.oneOf).toHaveLength(2)

    // Check photo option
    const photoOption = contentSchema.oneOf[0]
    expect(photoOption.type).toBe('object')
    expect(photoOption.properties.photo.format).toBe('uri-reference')
    expect(photoOption.properties.photo.description).toBe('absolute paths to local files')
    expect(photoOption.required).toContain('photo')
    expect(photoOption.required).toContain('isProfile')

    // Check document option
    const documentOption = contentSchema.oneOf[1]
    expect(documentOption.type).toBe('object')
    expect(documentOption.properties.document.format).toBe('uri-reference')
    expect(documentOption.properties.document.description).toBe('absolute paths to local files')
    expect(documentOption.required).toContain('document')
    expect(documentOption.required).toContain('category')
  })
})

```

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

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
import { JSONSchema7 as IJsonSchema } from 'json-schema'
import { OpenAPIV3 } from 'openapi-types'
import { HttpClient, HttpClientError } from '../client/http-client'
import { OpenAPIToMCPConverter } from '../openapi/parser'

type PathItemObject = OpenAPIV3.PathItemObject & {
  get?: OpenAPIV3.OperationObject
  put?: OpenAPIV3.OperationObject
  post?: OpenAPIV3.OperationObject
  delete?: OpenAPIV3.OperationObject
  patch?: OpenAPIV3.OperationObject
}

type NewToolDefinition = {
  methods: Array<{
    name: string
    description: string
    inputSchema: IJsonSchema & { type: 'object' }
    returnSchema?: IJsonSchema
  }>
}

// Notion object type definition
interface NotionBlock {
  object: 'block';
  id: string;
  type: string;
  has_children?: boolean;
  [key: string]: any; // Allow additional fields
}

interface NotionPage {
  object: 'page';
  id: string;
  properties: Record<string, any>;
  [key: string]: any; // Allow additional fields
}

interface NotionDatabase {
  object: 'database';
  id: string;
  properties: Record<string, any>;
  [key: string]: any; // Allow additional fields
}

interface NotionComment {
  object: 'comment';
  id: string;
  [key: string]: any; // Allow additional fields
}

type NotionObject = NotionBlock | NotionPage | NotionDatabase | NotionComment;

// Recursive exploration options
interface RecursiveExplorationOptions {
  maxDepth?: number;
  includeDatabases?: boolean;
  includeComments?: boolean;
  includeProperties?: boolean;
  maxParallelRequests?: number;
  skipCache?: boolean;
  batchSize?: number;
  timeoutMs?: number;
  runInBackground?: boolean;
}

// import this class, extend and return server
export class MCPProxy {
  private server: Server
  private httpClient: HttpClient
  private tools: Record<string, NewToolDefinition>
  private openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
  private pageCache: Map<string, any> = new Map() // Cache for performance improvement
  private blockCache: Map<string, any> = new Map() // Block cache
  private databaseCache: Map<string, any> = new Map() // Database cache
  private commentCache: Map<string, any> = new Map() // Comment cache
  private propertyCache: Map<string, any> = new Map() // Property cache
  private backgroundProcessingResults: Map<string, any> = new Map()

  constructor(name: string, openApiSpec: OpenAPIV3.Document) {
    this.server = new Server({ name, version: '1.0.0' }, { capabilities: { tools: {} } })
    const baseUrl = openApiSpec.servers?.[0].url
    if (!baseUrl) {
      throw new Error('No base URL found in OpenAPI spec')
    }
    this.httpClient = new HttpClient(
      {
        baseUrl,
        headers: this.parseHeadersFromEnv(),
      },
      openApiSpec,
    )

    // Convert OpenAPI spec to MCP tools
    const converter = new OpenAPIToMCPConverter(openApiSpec)
    const { tools, openApiLookup } = converter.convertToMCPTools()
    this.tools = tools
    this.openApiLookup = openApiLookup

    this.setupHandlers()
  }

  private setupHandlers() {
    // Handle tool listing
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      const tools: Tool[] = []

      // Log available tools
      console.log('One Pager Assistant - Available tools:')

      // Add methods as separate tools to match the MCP format
      Object.entries(this.tools).forEach(([toolName, def]) => {
        def.methods.forEach(method => {
          const toolNameWithMethod = `${toolName}-${method.name}`;
          const truncatedToolName = this.truncateToolName(toolNameWithMethod);
          tools.push({
            name: truncatedToolName,
            description: method.description,
            inputSchema: method.inputSchema as Tool['inputSchema'],
          })
          console.log(`- ${truncatedToolName}: ${method.description}`)
        })
      })

      // Add extended One Pager tool
      const onePagerTool = {
        name: 'API-get-one-pager',
        description: 'Recursively retrieve a full Notion page with all its blocks, databases, and related content',
        inputSchema: {
          type: 'object',
          properties: {
            page_id: {
              type: 'string',
              description: 'Identifier for a Notion page',
            },
            maxDepth: {
              type: 'integer',
              description: 'Maximum recursion depth (default: 5)',
            },
            includeDatabases: {
              type: 'boolean',
              description: 'Whether to include linked databases (default: true)',
            },
            includeComments: {
              type: 'boolean',
              description: 'Whether to include comments (default: true)',
            },
            includeProperties: {
              type: 'boolean',
              description: 'Whether to include detailed page properties (default: true)',
            },
            maxParallelRequests: {
              type: 'integer',
              description: 'Maximum number of parallel requests (default: 15)',
            },
            batchSize: {
              type: 'integer',
              description: 'Batch size for parallel processing (default: 10)',
            },
            timeoutMs: {
              type: 'integer',
              description: 'Timeout in milliseconds (default: 300000)',
            },
            runInBackground: {
              type: 'boolean',
              description: 'Process request in background without timeout (default: true)',
            }
          },
          required: ['page_id'],
        } as Tool['inputSchema'],
      };
      
      tools.push(onePagerTool);
      console.log(`- ${onePagerTool.name}: ${onePagerTool.description}`);
      
      // Add tool to retrieve background processing results
      const backgroundResultTool = {
        name: 'API-get-background-result',
        description: 'Retrieve the result of a background processing request',
        inputSchema: {
          type: 'object',
          properties: {
            page_id: {
              type: 'string',
              description: 'Identifier for the Notion page that was processed in background',
            },
          },
          required: ['page_id'],
        } as Tool['inputSchema'],
      };
      
      tools.push(backgroundResultTool);
      console.log(`- ${backgroundResultTool.name}: ${backgroundResultTool.description}`);

      return { tools }
    })

    // Handle tool calling
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: params } = request.params

      console.log(`One Pager Assistant - Tool call: ${name}`)
      console.log('Parameters:', JSON.stringify(params, null, 2))

      try {
        // Handle extended One Pager tool
        if (name === 'API-get-one-pager') {
          return await this.handleOnePagerRequest(params);
        }
        
        // Handle background result retrieval
        if (name === 'API-get-background-result') {
          const result = this.getBackgroundProcessingResult(params?.page_id as string);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(result),
              },
            ],
          };
        }

        // Find the operation in OpenAPI spec
        const operation = this.findOperation(name)
        if (!operation) {
          const error = `Method ${name} not found.`
          console.error(error)
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  status: 'error',
                  message: error,
                  code: 404
                }),
              },
            ],
          }
        }

        // Optimized parallel processing for API-get-block-children
        if (name === 'API-get-block-children') {
          // Create basic options for logging control
          const blockOptions: RecursiveExplorationOptions = {
            runInBackground: false, // Default to not showing logs for regular API calls
          };
          
          return await this.handleBlockChildrenParallel(operation, params, blockOptions);
        }

        // Other regular API calls
        console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path}`)
        const response = await this.httpClient.executeOperation(operation, params)

        // Log response summary
        console.log('Notion API response code:', response.status)
        if (response.status !== 200) {
          console.error('Response error:', response.data)
        } else {
          console.log('Response success')
        }

        // Update cache with response data
        this.updateCacheFromResponse(name, response.data);

        // Convert response to MCP format
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(response.data),
            },
          ],
        }
      } catch (error) {
        console.error('Tool call error', error)
        
        if (error instanceof HttpClientError) {
          console.error('HttpClientError occurred, returning structured error', error)
          const data = error.data?.response?.data ?? error.data ?? {}
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  status: 'error',
                  code: error.status,
                  message: error.message,
                  details: typeof data === 'object' ? data : { data: data },
                }),
              },
            ],
          }
        }
        
        // Ensure any other errors are also properly formatted as JSON
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                status: 'error',
                message: error instanceof Error ? error.message : String(error),
                code: 500
              }),
            },
          ],
        }
      }
    })
  }

  // Update cache based on API response type
  private updateCacheFromResponse(apiName: string, data: any): void {
    if (!data || typeof data !== 'object') return;

    try {
      // Update appropriate cache based on API response type
      if (apiName === 'API-retrieve-a-page' && data.object === 'page' && data.id) {
        this.pageCache.set(data.id, data);
      } else if (apiName === 'API-retrieve-a-block' && data.object === 'block' && data.id) {
        this.blockCache.set(data.id, data);
      } else if (apiName === 'API-retrieve-a-database' && data.object === 'database' && data.id) {
        this.databaseCache.set(data.id, data);
      } else if (apiName === 'API-retrieve-a-comment' && data.results) {
        // Cache comments from result list
        data.results.forEach((comment: any) => {
          if (comment.object === 'comment' && comment.id) {
            this.commentCache.set(comment.id, comment);
          }
        });
      } else if (apiName === 'API-retrieve-a-page-property' && data.results) {
        // Page property caching - would need params from call context
        // Skip this in current context
        console.log('Page property information has been cached');
      }

      // API-get-block-children handled in handleBlockChildrenParallel
    } catch (error) {
      console.warn('Error updating cache:', error);
    }
  }

  // One Pager request handler
  private async handleOnePagerRequest(params: any) {
    if (params.runInBackground !== false) {
      console.log('Starting One Pager request processing:', params.page_id);
    }
    
    const options: RecursiveExplorationOptions = {
      maxDepth: params.maxDepth || 5,
      includeDatabases: params.includeDatabases !== false,
      includeComments: params.includeComments !== false,
      includeProperties: params.includeProperties !== false,
      maxParallelRequests: params.maxParallelRequests || 15,
      skipCache: params.skipCache || false,
      batchSize: params.batchSize || 10,
      timeoutMs: params.timeoutMs || 300000, // Increased timeout to 5 minutes (300000ms)
      runInBackground: params.runInBackground !== false,
    };
    
    if (options.runInBackground) {
      console.log('Exploration options:', JSON.stringify(options, null, 2));
    }
    
    try {
      const startTime = Date.now();
      
      // Check if we should run in background mode
      if (options.runInBackground) {
        // Return immediately with a background processing message
        // The actual processing will continue in the background
        this.runBackgroundProcessing(params.page_id, options);
        
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                status: 'processing',
                message: `Request processing for page ${params.page_id} started in background`,
                page_id: params.page_id,
                request_time: new Date().toISOString(),
                options: {
                  maxDepth: options.maxDepth,
                  includeDatabases: options.includeDatabases,
                  includeComments: options.includeComments,
                  includeProperties: options.includeProperties,
                  timeoutMs: options.timeoutMs
                }
              }),
            },
          ],
        };
      }
      
      // Foreground processing (standard behavior)
      const pageData = await this.retrievePageRecursively(params.page_id, options);
      
      const duration = Date.now() - startTime;
      if (options.runInBackground) {
        console.log(`One Pager completed in ${duration}ms for page ${params.page_id}`);
      }
      
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              ...pageData,
              _meta: {
                processingTimeMs: duration,
                retrievedAt: new Date().toISOString(),
                options: {
                  maxDepth: options.maxDepth,
                  includeDatabases: options.includeDatabases,
                  includeComments: options.includeComments,
                  includeProperties: options.includeProperties
                }
              }
            }),
          },
        ],
      };
    } catch (error) {
      if (options.runInBackground) {
        console.error('Error in One Pager request:', error);
      }
      const errorResponse = {
        status: 'error',
        message: error instanceof Error ? error.message : String(error),
        code: error instanceof HttpClientError ? error.status : 500,
        details: error instanceof HttpClientError ? error.data : undefined,
        timestamp: new Date().toISOString()
      };
      
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(errorResponse),
          },
        ],
      };
    }
  }

  // New method to run processing in background
  private runBackgroundProcessing(pageId: string, options: RecursiveExplorationOptions): void {
    // Use setTimeout to detach from the current execution context
    setTimeout(async () => {
      try {
        console.log(`Background processing started for page ${pageId}`);
        const startTime = Date.now();
        
        // Execute the recursive page retrieval without time restrictions
        const noTimeoutOptions = { ...options, timeoutMs: 0 }; // 0 means no timeout
        const pageData = await this.retrievePageRecursively(pageId, noTimeoutOptions);
        
        const duration = Date.now() - startTime;
        console.log(`Background processing completed in ${duration}ms for page ${pageId}`);
        
        // Store the result in cache for later retrieval
        this.storeBackgroundProcessingResult(pageId, {
          ...pageData,
          _meta: {
            processingTimeMs: duration,
            retrievedAt: new Date().toISOString(),
            processedInBackground: true,
            options: {
              maxDepth: options.maxDepth,
              includeDatabases: options.includeDatabases,
              includeComments: options.includeComments,
              includeProperties: options.includeProperties
            }
          }
        });
      } catch (error) {
        console.error(`Background processing error for page ${pageId}:`, error);
        // Store error result for later retrieval
        this.storeBackgroundProcessingResult(pageId, {
          status: 'error',
          page_id: pageId,
          message: error instanceof Error ? error.message : String(error),
          timestamp: new Date().toISOString()
        });
      }
    }, 0);
  }
  
  // Store background processing results
  private storeBackgroundProcessingResult(pageId: string, result: any): void {
    this.backgroundProcessingResults.set(pageId, result);
  }
  
  // Add a new tool method to retrieve background processing results
  public getBackgroundProcessingResult(pageId: string): any {
    return this.backgroundProcessingResults.get(pageId) || { 
      status: 'not_found',
      message: `No background processing result found for page ${pageId}`
    };
  }

  // Recursively retrieve page content
  private async retrievePageRecursively(pageId: string, options: RecursiveExplorationOptions, currentDepth: number = 0): Promise<any> {
    if (options.runInBackground) {
      console.log(`Recursive page exploration: ${pageId}, depth: ${currentDepth}/${options.maxDepth || 5}`);
    }
    
    const timeoutPromise = new Promise<never>((_, reject) => {
      if (options.timeoutMs && options.timeoutMs > 0) {
        setTimeout(() => reject(new Error(`Operation timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
      }
    });
    
    try {
      // Check maximum depth
      if (currentDepth >= (options.maxDepth || 5)) {
        if (options.runInBackground) {
          console.log(`Maximum depth reached: ${currentDepth}/${options.maxDepth || 5}`);
        }
        return { id: pageId, note: "Maximum recursion depth reached" };
      }
      
      // 1. Get basic page info (check cache)
      let pageData: any;
      if (!options.skipCache && this.pageCache.has(pageId)) {
        pageData = this.pageCache.get(pageId);
        if (options.runInBackground) {
          console.log(`Page cache hit: ${pageId}`);
        }
      } else {
        // Retrieve page info via API call
        const operation = this.findOperation('API-retrieve-a-page');
        if (!operation) {
          throw new Error('API-retrieve-a-page method not found.');
        }
        
        if (options.runInBackground) {
          console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (pageId: ${pageId})`);
        }
        
        // Only race with timeout if timeoutMs is set
        let response;
        if (options.timeoutMs && options.timeoutMs > 0) {
          response = await Promise.race([
            this.httpClient.executeOperation(operation, { page_id: pageId }),
            timeoutPromise
          ]) as any;
        } else {
          response = await this.httpClient.executeOperation(operation, { page_id: pageId });
        }
        
        if (response.status !== 200) {
          if (options.runInBackground) {
            console.error('Error retrieving page information:', response.data);
          }
          return { 
            id: pageId,
            error: "Failed to retrieve page", 
            status: response.status,
            details: response.data 
          };
        }
        
        pageData = response.data;
        // Only cache successful responses
        this.pageCache.set(pageId, pageData);
      }
      
      // Collection of tasks to be executed in parallel for improved efficiency
      const parallelTasks: Promise<any>[] = [];
      
      // 2. Fetch block content (register async task)
      const blocksPromise = this.retrieveBlocksRecursively(pageId, options, currentDepth + 1);
      parallelTasks.push(blocksPromise);
      
      // 3. Fetch property details (if option enabled)
      let propertiesPromise: Promise<any> = Promise.resolve(null);
      if (options.includeProperties && pageData.properties) {
        propertiesPromise = this.enrichPageProperties(pageId, pageData.properties, options);
        parallelTasks.push(propertiesPromise);
      }
      
      // 4. Fetch comments (if option enabled)
      let commentsPromise: Promise<any> = Promise.resolve(null);
      if (options.includeComments) {
        commentsPromise = this.retrieveComments(pageId, options);
        parallelTasks.push(commentsPromise);
      }
      
      // Execute all tasks in parallel
      if (options.timeoutMs && options.timeoutMs > 0) {
        await Promise.race([Promise.all(parallelTasks), timeoutPromise]);
      } else {
        await Promise.all(parallelTasks);
      }
      
      // Integrate results into the main page data
      const enrichedPageData = { ...pageData };
      
      // Add block content
      const blocksData = await blocksPromise;
      enrichedPageData.content = blocksData;
      
      // Add property details (if option enabled)
      if (options.includeProperties && pageData.properties) {
        const enrichedProperties = await propertiesPromise;
        if (enrichedProperties) {
          enrichedPageData.detailed_properties = enrichedProperties;
        }
      }
      
      // Add comments (if option enabled)
      if (options.includeComments) {
        const comments = await commentsPromise;
        if (comments && comments.results && comments.results.length > 0) {
          enrichedPageData.comments = comments;
        }
      }
      
      return enrichedPageData;
    } catch (error) {
      if (error instanceof Error && error.message.includes('timed out')) {
        if (options.runInBackground) {
          console.error(`Timeout occurred while processing page ${pageId} at depth ${currentDepth}`);
        }
        return { 
          id: pageId, 
          error: "Operation timed out", 
          partial_results: true,
          note: `Processing exceeded timeout limit (${options.timeoutMs}ms)`
        };
      }
      
      if (options.runInBackground) {
        console.error(`Error in retrievePageRecursively for page ${pageId}:`, error);
      }
      return { 
        id: pageId, 
        error: error instanceof Error ? error.message : String(error),
        retrievalFailed: true
      };
    }
  }
  
  // Recursively retrieve block content with improved parallelism
  private async retrieveBlocksRecursively(blockId: string, options: RecursiveExplorationOptions, currentDepth: number): Promise<any[]> {
    if (options.runInBackground) {
      console.log(`Recursive block exploration: ${blockId}, depth: ${currentDepth}/${options.maxDepth || 5}`);
    }
    
    if (currentDepth >= (options.maxDepth || 5)) {
      if (options.runInBackground) {
        console.log(`Maximum depth reached: ${currentDepth}/${options.maxDepth || 5}`);
      }
      return [{ note: "Maximum recursion depth reached" }];
    }
    
    try {
      const operation = this.findOperation('API-get-block-children');
      if (!operation) {
        throw new Error('API-get-block-children method not found.');
      }
      
      const blocksResponse = await this.handleBlockChildrenParallel(operation, { 
        block_id: blockId,
        page_size: 100
      }, options);
      
      const blocksData = JSON.parse(blocksResponse.content[0].text);
      const blocks = blocksData.results || [];
      
      if (blocks.length === 0) {
        return [];
      }
      
      const batchSize = options.batchSize || 10;
      const enrichedBlocks: any[] = [];
      
      // Process blocks in batches for memory optimization and improved parallel execution
      for (let i = 0; i < blocks.length; i += batchSize) {
        const batch = blocks.slice(i, i + batchSize);
        
        // Process each batch in parallel
        const batchResults = await Promise.all(
          batch.map(async (block: any) => {
            this.blockCache.set(block.id, block);
            
            const enrichedBlock = { ...block };
            
            // Collection of async tasks for this block
            const blockTasks: Promise<any>[] = [];
            
            // Process child blocks recursively
            if (block.has_children) {
              blockTasks.push(
                this.retrieveBlocksRecursively(block.id, options, currentDepth + 1)
                  .then(childBlocks => { enrichedBlock.children = childBlocks; })
                  .catch(error => {
                    console.error(`Error retrieving child blocks for ${block.id}:`, error);
                    enrichedBlock.children_error = { message: String(error) };
                    return [];
                  })
              );
            }
            
            // Process database blocks (if option enabled)
            if (options.includeDatabases && 
                (block.type === 'child_database' || block.type === 'linked_database')) {
              const databaseId = block[block.type]?.database_id;
              if (databaseId) {
                blockTasks.push(
                  this.retrieveDatabase(databaseId, options)
                    .then(database => { enrichedBlock.database = database; })
                    .catch(error => {
                      console.error(`Error retrieving database ${databaseId}:`, error);
                      enrichedBlock.database_error = { message: String(error) };
                    })
                );
              }
            }
            
            // Process page blocks or linked pages - optimization
            if (block.type === 'child_page' && currentDepth < (options.maxDepth || 5) - 1) {
              const pageId = block.id;
              blockTasks.push(
                this.retrievePageBasicInfo(pageId, options)
                  .then(pageInfo => { enrichedBlock.page_info = pageInfo; })
                  .catch(error => {
                    console.error(`Error retrieving page info for ${pageId}:`, error);
                    enrichedBlock.page_info_error = { message: String(error) };
                  })
              );
            }
            
            // Wait for all async tasks to complete
            if (blockTasks.length > 0) {
              await Promise.all(blockTasks);
            }
            
            return enrichedBlock;
          })
        );
        
        enrichedBlocks.push(...batchResults);
      }
      
      return enrichedBlocks;
    } catch (error) {
      console.error(`Error in retrieveBlocksRecursively for block ${blockId}:`, error);
      return [{ 
        id: blockId, 
        error: error instanceof Error ? error.message : String(error),
        retrievalFailed: true
      }];
    }
  }
  
  // Lightweight method to fetch only basic page info (without recursive loading)
  private async retrievePageBasicInfo(pageId: string, options: RecursiveExplorationOptions): Promise<any> {
    // Check cache
    if (!options.skipCache && this.pageCache.has(pageId)) {
      const cachedData = this.pageCache.get(pageId);
      return {
        id: cachedData.id,
        title: cachedData.properties?.title || { text: null },
        icon: cachedData.icon,
        cover: cachedData.cover,
        url: cachedData.url,
        fromCache: true
      };
    }
    
    // Get page info via API
    const operation = this.findOperation('API-retrieve-a-page');
    if (!operation) {
      return { id: pageId, note: "API-retrieve-a-page method not found" };
    }
    
    try {
      const response = await this.httpClient.executeOperation(operation, { page_id: pageId });
      
      if (response.status !== 200) {
        return { id: pageId, error: "Failed to retrieve page", status: response.status };
      }
      
      const pageData = response.data;
      this.pageCache.set(pageId, pageData);
      
      return {
        id: pageData.id,
        title: pageData.properties?.title || { text: null },
        icon: pageData.icon,
        cover: pageData.cover,
        url: pageData.url,
        created_time: pageData.created_time,
        last_edited_time: pageData.last_edited_time
      };
    } catch (error) {
      console.error(`Error retrieving basic page info ${pageId}:`, error);
      return { id: pageId, error: error instanceof Error ? error.message : String(error) };
    }
  }

  // Retrieve database information
  private async retrieveDatabase(databaseId: string, options: RecursiveExplorationOptions): Promise<any> {
    console.log(`Retrieving database information: ${databaseId}`);
    
    // Check cache
    if (!options.skipCache && this.databaseCache.has(databaseId)) {
      console.log(`Database cache hit: ${databaseId}`);
      return this.databaseCache.get(databaseId);
    }
    
    // Get database info via API call
    const operation = this.findOperation('API-retrieve-a-database');
    if (!operation) {
      console.warn('API-retrieve-a-database method not found.');
      return { id: databaseId, note: "Database details not available" };
    }
    
    try {
      console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (databaseId: ${databaseId})`);
      const response = await this.httpClient.executeOperation(operation, { database_id: databaseId });
      
      if (response.status !== 200) {
        console.error('Error retrieving database information:', response.data);
        return { id: databaseId, error: "Failed to retrieve database" };
      }
      
      const databaseData = response.data;
      this.databaseCache.set(databaseId, databaseData);
      return databaseData;
    } catch (error) {
      console.error('Error retrieving database:', error);
      return { id: databaseId, error: "Failed to retrieve database" };
    }
  }
  
  // Retrieve comments
  private async retrieveComments(blockId: string, options: RecursiveExplorationOptions): Promise<any> {
    if (options.runInBackground) {
      console.log(`Retrieving comments: ${blockId}`);
    }
    
    // Get comments via API call
    const operation = this.findOperation('API-retrieve-a-comment');
    if (!operation) {
      if (options.runInBackground) {
        console.warn('API-retrieve-a-comment method not found.');
      }
      return Promise.resolve({ results: [] });
    }
    
    try {
      if (options.runInBackground) {
        console.log(`Notion API call: ${operation.method.toUpperCase()} ${operation.path} (blockId: ${blockId})`);
      }
      
      return this.httpClient.executeOperation(operation, { block_id: blockId })
        .then(response => {
          if (response.status !== 200) {
            if (options.runInBackground) {
              console.error('Error retrieving comments:', response.data);
            }
            return { results: [] };
          }
          
          const commentsData = response.data;
          
          // Cache comments
          if (commentsData.results) {
            commentsData.results.forEach((comment: any) => {
              if (comment.id) {
                this.commentCache.set(comment.id, comment);
              }
            });
          }
          
          return commentsData;
        })
        .catch(error => {
          if (options.runInBackground) {
            console.error('Error retrieving comments:', error);
          }
          return { results: [] };
        });
    } catch (error) {
      if (options.runInBackground) {
        console.error('Error retrieving comments:', error);
      }
      return Promise.resolve({ results: [] });
    }
  }
  
  // Enrich page properties with detailed information
  private async enrichPageProperties(pageId: string, properties: any, options: RecursiveExplorationOptions): Promise<any> {
    if (options.runInBackground) {
      console.log(`Enriching page properties: ${pageId}`);
    }
    
    const enrichedProperties = { ...properties };
    const propertyPromises: Promise<void>[] = [];
    
    // Get detailed information for each property
    for (const [propName, propData] of Object.entries(properties)) {
      const propId = (propData as any).id;
      if (!propId) continue;
      
      // Create cache key
      const cacheKey = `${pageId}:${propId}`;
      
      propertyPromises.push(
        (async () => {
          try {
            // Check cache
            if (!options.skipCache && this.propertyCache.has(cacheKey)) {
              enrichedProperties[propName].details = this.propertyCache.get(cacheKey);
            } else {
              // Skip properties with URLs that contain special characters like notion://
              if (propId.includes('notion://') || propId.includes('%3A%2F%2F')) {
                if (options.runInBackground) {
                  console.warn(`Skipping property with special URL format: ${propName} (${propId})`);
                }
                enrichedProperties[propName].details = { 
                  object: 'property_item', 
                  type: 'unsupported',
                  unsupported: { type: 'special_url_format' } 
                };
                return;
              }
              
              // Get property details via API call
              const operation = this.findOperation('API-retrieve-a-page-property');
              if (!operation) {
                if (options.runInBackground) {
                  console.warn('API-retrieve-a-page-property method not found.');
                }
                return;
              }
              
              const response = await this.httpClient.executeOperation(operation, {
                page_id: pageId,
                property_id: propId
              }).catch(error => {
                if (options.runInBackground) {
                  console.warn(`Error retrieving property ${propName} (${propId}): ${error.message}`);
                }
                return { 
                  status: error.status || 500,
                  data: { 
                    object: 'property_item', 
                    type: 'error',
                    error: { message: error.message } 
                  }
                };
              });
              
              if (response.status === 200) {
                enrichedProperties[propName].details = response.data;
                this.propertyCache.set(cacheKey, response.data);
              } else {
                enrichedProperties[propName].details = { 
                  object: 'property_item', 
                  type: 'error',
                  error: { status: response.status, message: JSON.stringify(response.data) } 
                };
              }
            }
          } catch (error) {
            if (options.runInBackground) {
              console.error(`Error retrieving property ${propName}:`, error);
            }
            enrichedProperties[propName].details = { 
              object: 'property_item', 
              type: 'error',
              error: { message: error instanceof Error ? error.message : String(error) } 
            };
          }
        })()
      );
    }
    
    // Get all property information in parallel
    await Promise.all(propertyPromises);
    
    return enrichedProperties;
  }

  // Optimized parallel processing for block children
  private async handleBlockChildrenParallel(
    operation: OpenAPIV3.OperationObject & { method: string; path: string }, 
    params: any,
    options?: RecursiveExplorationOptions
  ) {
    if (options?.runInBackground) {
      console.log(`Starting Notion API parallel processing: ${operation.method.toUpperCase()} ${operation.path}`);
    }
    
    // Get first page
    const initialResponse = await this.httpClient.executeOperation(operation, params);
    
    if (initialResponse.status !== 200) {
      if (options?.runInBackground) {
        console.error('Response error:', initialResponse.data);
      }
      return {
        content: [{ type: 'text', text: JSON.stringify(initialResponse.data) }],
      };
    }
    
    const results = initialResponse.data.results || [];
    let nextCursor = initialResponse.data.next_cursor;
    
    // Array for parallel processing
    const pageRequests = [];
    const maxParallelRequests = 5; // Limit simultaneous requests
    
    if (options?.runInBackground) {
      console.log(`Retrieved ${results.length} blocks from first page`);
    }
    
    // Request subsequent pages in parallel if available
    while (nextCursor) {
      // Clone parameters for next page
      const nextPageParams = { ...params, start_cursor: nextCursor };
      
      // Add page request
      pageRequests.push(
        this.httpClient.executeOperation(operation, nextPageParams)
          .then(response => {
            if (response.status === 200) {
              if (options?.runInBackground) {
                console.log(`Retrieved ${response.data.results?.length || 0} blocks from additional page`);
              }
              return {
                results: response.data.results || [],
                next_cursor: response.data.next_cursor
              };
            }
            return { results: [], next_cursor: null };
          })
          .catch(error => {
            if (options?.runInBackground) {
              console.error('Error retrieving page:', error);
            }
            return { results: [], next_cursor: null };
          })
      );
      
      // Execute parallel requests when batch size reached or no more pages
      if (pageRequests.length >= maxParallelRequests || !nextCursor) {
        if (options?.runInBackground) {
          console.log(`Processing ${pageRequests.length} pages in parallel...`);
        }
        const pageResponses = await Promise.all(pageRequests);
        
        // Merge results
        for (const response of pageResponses) {
          results.push(...response.results);
          // Set next cursor for next batch
          if (response.next_cursor) {
            nextCursor = response.next_cursor;
          } else {
            nextCursor = null;
          }
        }
        
        // Reset request array
        pageRequests.length = 0;
      }
      
      // Exit loop if no more pages
      if (!nextCursor) break;
    }
    
    if (options?.runInBackground) {
      console.log(`Retrieved ${results.length} blocks in total`);
    }
    
    // Return merged response
    const mergedResponse = {
      ...initialResponse.data,
      results,
      has_more: false,
      next_cursor: null
    };
    
    return {
      content: [{ type: 'text', text: JSON.stringify(mergedResponse) }],
    };
  }

  private findOperation(operationId: string): (OpenAPIV3.OperationObject & { method: string; path: string }) | null {
    return this.openApiLookup[operationId] ?? null
  }

  private parseHeadersFromEnv(): Record<string, string> {
    const headersJson = process.env.OPENAPI_MCP_HEADERS
    if (!headersJson) {
      return {}
    }

    try {
      const headers = JSON.parse(headersJson)
      if (typeof headers !== 'object' || headers === null) {
        console.warn('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', typeof headers)
        return {}
      }
      return headers
    } catch (error) {
      console.warn('Failed to parse OPENAPI_MCP_HEADERS environment variable:', error)
      return {}
    }
  }

  private getContentType(headers: Headers): 'text' | 'image' | 'binary' {
    const contentType = headers.get('content-type')
    if (!contentType) return 'binary'

    if (contentType.includes('text') || contentType.includes('json')) {
      return 'text'
    } else if (contentType.includes('image')) {
      return 'image'
    }
    return 'binary'
  }

  private truncateToolName(name: string): string {
    if (name.length <= 64) {
      return name;
    }
    return name.slice(0, 64);
  }

  async connect(transport: Transport) {
    console.log('One Pager Assistant - MCP server started')
    console.log('Providing APIs: retrieve-a-page, get-block-children, retrieve-a-block')
    console.log('New feature: get-one-pager - recursively explore pages automatically')
    console.log('Parallel processing optimization enabled')
    
    // The SDK will handle stdio communication
    await this.server.connect(transport)
  }

  getServer() {
    return this.server
  }
}

```

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

```typescript
import { OpenAPIToMCPConverter } from '../parser'
import { OpenAPIV3 } from 'openapi-types'
import { describe, expect, it } from 'vitest'
import { JSONSchema7 as IJsonSchema } from 'json-schema'

interface ToolMethod {
  name: string
  description: string
  inputSchema: any
  returnSchema?: any
}

interface Tool {
  methods: ToolMethod[]
}

interface Tools {
  [key: string]: Tool
}

// Helper function to verify tool method structure without checking the exact Zod schema
function verifyToolMethod(actual: ToolMethod, expected: any, toolName: string) {
  expect(actual.name).toBe(expected.name)
  expect(actual.description).toBe(expected.description)
  expect(actual.inputSchema, `inputSchema ${actual.name} ${toolName}`).toEqual(expected.inputSchema)
  if (expected.returnSchema) {
    expect(actual.returnSchema, `returnSchema ${actual.name} ${toolName}`).toEqual(expected.returnSchema)
  }
}

// Helper function to verify tools structure
function verifyTools(actual: Tools, expected: any) {
  expect(Object.keys(actual)).toEqual(Object.keys(expected))
  for (const [key, value] of Object.entries(actual)) {
    expect(value.methods.length).toBe(expected[key].methods.length)
    value.methods.forEach((method: ToolMethod, index: number) => {
      verifyToolMethod(method, expected[key].methods[index], key)
    })
  }
}

// A helper function to derive a type from a possibly complex schema.
// If no explicit type is found, we assume 'object' for testing purposes.
function getTypeFromSchema(schema: IJsonSchema): string {
  if (schema.type) {
    return Array.isArray(schema.type) ? schema.type[0] : schema.type
  } else if (schema.$ref) {
    // If there's a $ref, we treat it as an object reference.
    return 'object'
  } else if (schema.oneOf || schema.anyOf || schema.allOf) {
    // Complex schema combos - assume object for these tests.
    return 'object'
  }
  return 'object'
}

// Updated helper function to get parameters from inputSchema
// Now handles $ref by treating it as an object reference without expecting properties.
function getParamsFromSchema(method: { inputSchema: IJsonSchema }) {
  return Object.entries(method.inputSchema.properties || {}).map(([name, prop]) => {
    if (typeof prop === 'boolean') {
      throw new Error(`Boolean schema not supported for parameter ${name}`)
    }

    // If there's a $ref, treat it as an object reference.
    const schemaType = getTypeFromSchema(prop)
    return {
      name,
      type: schemaType,
      description: prop.description,
      optional: !(method.inputSchema.required || []).includes(name),
    }
  })
}

// Updated helper function to get return type from returnSchema
// No longer requires that the schema be fully expanded. If we have a $ref, just note it as 'object'.
function getReturnType(method: { returnSchema?: IJsonSchema }) {
  if (!method.returnSchema) return null
  const schema = method.returnSchema
  return {
    type: getTypeFromSchema(schema),
    description: schema.description,
  }
}

describe('OpenAPIToMCPConverter', () => {
  describe('Simple API Conversion', () => {
    const sampleSpec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: {
        title: 'Test API',
        version: '1.0.0',
      },
      paths: {
        '/pets/{petId}': {
          get: {
            operationId: 'getPet',
            summary: 'Get a pet by ID',
            parameters: [
              {
                name: 'petId',
                in: 'path',
                required: true,
                description: 'The ID of the pet',
                schema: {
                  type: 'integer',
                },
              },
            ],
            responses: {
              '200': {
                description: 'Pet found',
                content: {
                  'application/json': {
                    schema: {
                      type: 'object',
                      properties: {
                        id: { type: 'integer' },
                        name: { type: 'string' },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    }

    it('converts simple OpenAPI paths to MCP tools', () => {
      const converter = new OpenAPIToMCPConverter(sampleSpec)
      const { tools, openApiLookup } = converter.convertToMCPTools()

      expect(tools).toHaveProperty('API')
      expect(tools.API.methods).toHaveLength(1)
      expect(Object.keys(openApiLookup)).toHaveLength(1)

      const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
      expect(getPetMethod).toBeDefined()

      const params = getParamsFromSchema(getPetMethod!)
      expect(params).toContainEqual({
        name: 'petId',
        type: 'integer',
        description: 'The ID of the pet',
        optional: false,
      })
    })

    it('truncates tool names exceeding 64 characters', () => {
      const longOperationId = 'a'.repeat(65)
      const specWithLongName: OpenAPIV3.Document = {
        openapi: '3.0.0',
        info: {
          title: 'Test API',
          version: '1.0.0'
        },
        paths: {
          '/pets/{petId}': {
            get: {
              operationId: longOperationId,
              summary: 'Get a pet by ID',
              parameters: [
                {
                  name: 'petId',
                  in: 'path',
                  required: true,
                  description: 'The ID of the pet',
                  schema: {
                    type: 'integer'
                  }
                }
              ],
              responses: {
                '200': {
                  description: 'Pet found',
                  content: {
                    'application/json': {
                      schema: {
                        type: 'object',
                        properties: {
                          id: { type: 'integer' },
                          name: { type: 'string' }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }

      const converter = new OpenAPIToMCPConverter(specWithLongName)
      const { tools } = converter.convertToMCPTools()

      const longNameMethod = tools.API.methods.find(m => m.name.startsWith('a'.repeat(59)))
      expect(longNameMethod).toBeDefined()
      expect(longNameMethod!.name.length).toBeLessThanOrEqual(64)
    })
  })

  describe('Complex API Conversion', () => {
    const complexSpec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Complex API', version: '1.0.0' },
      components: {
        schemas: {
          Error: {
            type: 'object',
            required: ['code', 'message'],
            properties: {
              code: { type: 'integer' },
              message: { type: 'string' },
            },
          },
          Pet: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer', description: 'The ID of the pet' },
              name: { type: 'string', description: 'The name of the pet' },
              category: { $ref: '#/components/schemas/Category', description: 'The category of the pet' },
              tags: {
                type: 'array',
                description: 'The tags of the pet',
                items: { $ref: '#/components/schemas/Tag' },
              },
              status: {
                type: 'string',
                description: 'The status of the pet',
                enum: ['available', 'pending', 'sold'],
              },
            },
          },
          Category: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              subcategories: {
                type: 'array',
                items: { $ref: '#/components/schemas/Category' },
              },
            },
          },
          Tag: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
            },
          },
        },
        parameters: {
          PetId: {
            name: 'petId',
            in: 'path',
            required: true,
            description: 'ID of pet to fetch',
            schema: { type: 'integer' },
          },
          QueryLimit: {
            name: 'limit',
            in: 'query',
            description: 'Maximum number of results to return',
            schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
          },
        },
        responses: {
          NotFound: {
            description: 'The specified resource was not found',
            content: {
              'application/json': {
                schema: { $ref: '#/components/schemas/Error' },
              },
            },
          },
        },
      },
      paths: {
        '/pets': {
          get: {
            operationId: 'listPets',
            summary: 'List all pets',
            parameters: [{ $ref: '#/components/parameters/QueryLimit' }],
            responses: {
              '200': {
                description: 'A list of pets',
                content: {
                  'application/json': {
                    schema: {
                      type: 'array',
                      items: { $ref: '#/components/schemas/Pet' },
                    },
                  },
                },
              },
            },
          },
          post: {
            operationId: 'createPet',
            summary: 'Create a pet',
            requestBody: {
              required: true,
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/Pet' },
                },
              },
            },
            responses: {
              '201': {
                description: 'Pet created',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Pet' },
                  },
                },
              },
            },
          },
        },
        '/pets/{petId}': {
          get: {
            operationId: 'getPet',
            summary: 'Get a pet by ID',
            parameters: [{ $ref: '#/components/parameters/PetId' }],
            responses: {
              '200': {
                description: 'Pet found',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Pet' },
                  },
                },
              },
              '404': {
                $ref: '#/components/responses/NotFound',
              },
            },
          },
          put: {
            operationId: 'updatePet',
            summary: 'Update a pet',
            parameters: [{ $ref: '#/components/parameters/PetId' }],
            requestBody: {
              required: true,
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/Pet' },
                },
              },
            },
            responses: {
              '200': {
                description: 'Pet updated',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Pet' },
                  },
                },
              },
              '404': {
                $ref: '#/components/responses/NotFound',
              },
            },
          },
        },
      },
    }

    it('converts operations with referenced parameters', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
      expect(getPetMethod).toBeDefined()
      const params = getParamsFromSchema(getPetMethod!)
      expect(params).toContainEqual({
        name: 'petId',
        type: 'integer',
        description: 'ID of pet to fetch',
        optional: false,
      })
    })

    it('converts operations with query parameters', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const listPetsMethod = tools.API.methods.find((m) => m.name === 'listPets')
      expect(listPetsMethod).toBeDefined()

      const params = getParamsFromSchema(listPetsMethod!)
      expect(params).toContainEqual({
        name: 'limit',
        type: 'integer',
        description: 'Maximum number of results to return',
        optional: true,
      })
    })

    it('converts operations with array responses', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const listPetsMethod = tools.API.methods.find((m) => m.name === 'listPets')
      expect(listPetsMethod).toBeDefined()

      const returnType = getReturnType(listPetsMethod!)
      // Now we only check type since description might not be carried through
      // if we are not expanding schemas.
      expect(returnType).toMatchObject({
        type: 'array',
      })
    })

    it('converts operations with request bodies using $ref', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const createPetMethod = tools.API.methods.find((m) => m.name === 'createPet')
      expect(createPetMethod).toBeDefined()

      const params = getParamsFromSchema(createPetMethod!)
      // Now that we are preserving $ref, the request body won't be expanded into multiple parameters.
      // Instead, we'll have a single "body" parameter referencing Pet.
      expect(params).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            name: 'body',
            type: 'object', // Because it's a $ref
            optional: false,
          }),
        ]),
      )
    })

    it('converts operations with referenced error responses', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const getPetMethod = tools.API.methods.find((m) => m.name === 'getPet')
      expect(getPetMethod).toBeDefined()

      // We just check that the description includes the error references now.
      expect(getPetMethod?.description).toContain('404: The specified resource was not found')
    })

    it('handles recursive schema references without expanding them', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      const createPetMethod = tools.API.methods.find((m) => m.name === 'createPet')
      expect(createPetMethod).toBeDefined()

      const params = getParamsFromSchema(createPetMethod!)
      // Since "category" would be inside Pet, and we're not expanding,
      // we won't see 'category' directly. We only have 'body' as a reference.
      // Thus, the test no longer checks for a direct 'category' param.
      expect(params.find((p) => p.name === 'body')).toBeDefined()
    })

    it('converts all operations correctly respecting $ref usage', () => {
      const converter = new OpenAPIToMCPConverter(complexSpec)
      const { tools } = converter.convertToMCPTools()

      expect(tools.API.methods).toHaveLength(4)

      const methodNames = tools.API.methods.map((m) => m.name)
      expect(methodNames).toEqual(expect.arrayContaining(['listPets', 'createPet', 'getPet', 'updatePet']))

      tools.API.methods.forEach((method) => {
        expect(method).toHaveProperty('name')
        expect(method).toHaveProperty('description')
        expect(method).toHaveProperty('inputSchema')
        expect(method).toHaveProperty('returnSchema')

        // For 'get' operations, we just check the return type is recognized correctly.
        if (method.name.startsWith('get')) {
          const returnType = getReturnType(method)
          // With $ref usage, we can't guarantee description or direct expansion.
          expect(returnType?.type).toBe('object')
        }
      })
    })
  })

  describe('Complex Schema Conversion', () => {
    // A similar approach for the nested spec
    // Just as in the previous tests, we no longer test for direct property expansion.
    // We only confirm that parameters and return types are recognized and that references are preserved.

    const nestedSpec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Nested API', version: '1.0.0' },
      components: {
        schemas: {
          Organization: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              departments: {
                type: 'array',
                items: { $ref: '#/components/schemas/Department' },
              },
              metadata: { $ref: '#/components/schemas/Metadata' },
            },
          },
          Department: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              employees: {
                type: 'array',
                items: { $ref: '#/components/schemas/Employee' },
              },
              subDepartments: {
                type: 'array',
                items: { $ref: '#/components/schemas/Department' },
              },
              metadata: { $ref: '#/components/schemas/Metadata' },
            },
          },
          Employee: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              role: { $ref: '#/components/schemas/Role' },
              skills: {
                type: 'array',
                items: { $ref: '#/components/schemas/Skill' },
              },
              metadata: { $ref: '#/components/schemas/Metadata' },
            },
          },
          Role: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              permissions: {
                type: 'array',
                items: { $ref: '#/components/schemas/Permission' },
              },
            },
          },
          Permission: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              scope: { type: 'string' },
            },
          },
          Skill: {
            type: 'object',
            required: ['id', 'name'],
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              level: {
                type: 'string',
                enum: ['beginner', 'intermediate', 'expert'],
              },
            },
          },
          Metadata: {
            type: 'object',
            properties: {
              createdAt: { type: 'string', format: 'date-time' },
              updatedAt: { type: 'string', format: 'date-time' },
              tags: {
                type: 'array',
                items: { type: 'string' },
              },
              customFields: {
                type: 'object',
                additionalProperties: true,
              },
            },
          },
        },
        parameters: {
          OrgId: {
            name: 'orgId',
            in: 'path',
            required: true,
            description: 'Organization ID',
            schema: { type: 'integer' },
          },
          DeptId: {
            name: 'deptId',
            in: 'path',
            required: true,
            description: 'Department ID',
            schema: { type: 'integer' },
          },
          IncludeMetadata: {
            name: 'includeMetadata',
            in: 'query',
            description: 'Include metadata in response',
            schema: { type: 'boolean', default: false },
          },
          Depth: {
            name: 'depth',
            in: 'query',
            description: 'Depth of nested objects to return',
            schema: { type: 'integer', minimum: 1, maximum: 5, default: 1 },
          },
        },
      },
      paths: {
        '/organizations/{orgId}': {
          get: {
            operationId: 'getOrganization',
            summary: 'Get organization details',
            parameters: [
              { $ref: '#/components/parameters/OrgId' },
              { $ref: '#/components/parameters/IncludeMetadata' },
              { $ref: '#/components/parameters/Depth' },
            ],
            responses: {
              '200': {
                description: 'Organization details',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Organization' },
                  },
                },
              },
            },
          },
        },
        '/organizations/{orgId}/departments/{deptId}': {
          get: {
            operationId: 'getDepartment',
            summary: 'Get department details',
            parameters: [
              { $ref: '#/components/parameters/OrgId' },
              { $ref: '#/components/parameters/DeptId' },
              { $ref: '#/components/parameters/IncludeMetadata' },
              { $ref: '#/components/parameters/Depth' },
            ],
            responses: {
              '200': {
                description: 'Department details',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Department' },
                  },
                },
              },
            },
          },
          put: {
            operationId: 'updateDepartment',
            summary: 'Update department details',
            parameters: [{ $ref: '#/components/parameters/OrgId' }, { $ref: '#/components/parameters/DeptId' }],
            requestBody: {
              required: true,
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/Department' },
                },
              },
            },
            responses: {
              '200': {
                description: 'Department updated',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/Department' },
                  },
                },
              },
            },
          },
        },
      },
    }

    it('handles deeply nested object references', () => {
      const converter = new OpenAPIToMCPConverter(nestedSpec)
      const { tools } = converter.convertToMCPTools()

      const getOrgMethod = tools.API.methods.find((m) => m.name === 'getOrganization')
      expect(getOrgMethod).toBeDefined()

      const params = getParamsFromSchema(getOrgMethod!)
      expect(params).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            name: 'orgId',
            type: 'integer',
            description: 'Organization ID',
            optional: false,
          }),
          expect.objectContaining({
            name: 'includeMetadata',
            type: 'boolean',
            description: 'Include metadata in response',
            optional: true,
          }),
          expect.objectContaining({
            name: 'depth',
            type: 'integer',
            description: 'Depth of nested objects to return',
            optional: true,
          }),
        ]),
      )
    })

    it('handles recursive array references without requiring expansion', () => {
      const converter = new OpenAPIToMCPConverter(nestedSpec)
      const { tools } = converter.convertToMCPTools()

      const updateDeptMethod = tools.API.methods.find((m) => m.name === 'updateDepartment')
      expect(updateDeptMethod).toBeDefined()

      const params = getParamsFromSchema(updateDeptMethod!)
      // With $ref usage, we have a body parameter referencing Department.
      // The subDepartments array is inside Department, so we won't see it expanded here.
      // Instead, we just confirm 'body' is present.
      const bodyParam = params.find((p) => p.name === 'body')
      expect(bodyParam).toBeDefined()
      expect(bodyParam?.type).toBe('object')
    })

    it('handles complex nested object hierarchies without expansion', () => {
      const converter = new OpenAPIToMCPConverter(nestedSpec)
      const { tools } = converter.convertToMCPTools()

      const getDeptMethod = tools.API.methods.find((m) => m.name === 'getDepartment')
      expect(getDeptMethod).toBeDefined()

      const params = getParamsFromSchema(getDeptMethod!)
      // Just checking top-level params:
      expect(params).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            name: 'orgId',
            type: 'integer',
            optional: false,
          }),
          expect.objectContaining({
            name: 'deptId',
            type: 'integer',
            optional: false,
          }),
          expect.objectContaining({
            name: 'includeMetadata',
            type: 'boolean',
            optional: true,
          }),
          expect.objectContaining({
            name: 'depth',
            type: 'integer',
            optional: true,
          }),
        ]),
      )
    })

    it('handles schema with mixed primitive and reference types without expansion', () => {
      const converter = new OpenAPIToMCPConverter(nestedSpec)
      const { tools } = converter.convertToMCPTools()

      const updateDeptMethod = tools.API.methods.find((m) => m.name === 'updateDepartment')
      expect(updateDeptMethod).toBeDefined()

      const params = getParamsFromSchema(updateDeptMethod!)
      // Since we are not expanding, we won't see metadata fields directly.
      // We just confirm 'body' referencing Department is there.
      expect(params.find((p) => p.name === 'body')).toBeDefined()
    })

    it('converts all operations with complex schemas correctly respecting $ref', () => {
      const converter = new OpenAPIToMCPConverter(nestedSpec)
      const { tools } = converter.convertToMCPTools()

      expect(tools.API.methods).toHaveLength(3)

      const methodNames = tools.API.methods.map((m) => m.name)
      expect(methodNames).toEqual(expect.arrayContaining(['getOrganization', 'getDepartment', 'updateDepartment']))

      tools.API.methods.forEach((method) => {
        expect(method).toHaveProperty('name')
        expect(method).toHaveProperty('description')
        expect(method).toHaveProperty('inputSchema')
        expect(method).toHaveProperty('returnSchema')

        // If it's a GET operation, check that return type is recognized.
        if (method.name.startsWith('get')) {
          const returnType = getReturnType(method)
          // Without expansion, just check type is recognized as object.
          expect(returnType).toMatchObject({
            type: 'object',
          })
        }
      })
    })
  })

  it('preserves description on $ref nodes', () => {
    const spec: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        schemas: {
          TestSchema: {
            type: 'object',
            properties: {
              name: { type: 'string' },
            },
          },
        },
      },
    }

    const converter = new OpenAPIToMCPConverter(spec)
    const result = converter.convertOpenApiSchemaToJsonSchema(
      {
        $ref: '#/components/schemas/TestSchema',
        description: 'A schema description',
      },
      new Set(),
    )

    expect(result).toEqual({
      $ref: '#/$defs/TestSchema',
      description: 'A schema description',
    })
  })
})

// Additional complex test scenarios as a table test
describe('OpenAPIToMCPConverter - Additional Complex Tests', () => {
  interface TestCase {
    name: string
    input: OpenAPIV3.Document
    expected: {
      tools: Record<
        string,
        {
          methods: Array<{
            name: string
            description: string
            inputSchema: IJsonSchema & { type: 'object' }
            returnSchema?: IJsonSchema
          }>
        }
      >
      openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
    }
  }

  const cases: TestCase[] = [
    {
      name: 'Cyclic References with Full Descriptions',
      input: {
        openapi: '3.0.0',
        info: {
          title: 'Cyclic Test API',
          version: '1.0.0',
        },
        paths: {
          '/ab': {
            get: {
              operationId: 'getAB',
              summary: 'Get an A-B object',
              responses: {
                '200': {
                  description: 'Returns an A object',
                  content: {
                    'application/json': {
                      schema: { $ref: '#/components/schemas/A' },
                    },
                  },
                },
              },
            },
            post: {
              operationId: 'createAB',
              summary: 'Create an A-B object',
              requestBody: {
                required: true,
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/A',
                      description: 'A schema description',
                    },
                  },
                },
              },
              responses: {
                '201': {
                  description: 'Created A object',
                  content: {
                    'application/json': {
                      schema: { $ref: '#/components/schemas/A' },
                    },
                  },
                },
              },
            },
          },
        },
        components: {
          schemas: {
            A: {
              type: 'object',
              description: 'A schema description',
              required: ['name', 'b'],
              properties: {
                name: {
                  type: 'string',
                  description: 'Name of A',
                },
                b: {
                  $ref: '#/components/schemas/B',
                  description: 'B property in A',
                },
              },
            },
            B: {
              type: 'object',
              description: 'B schema description',
              required: ['title', 'a'],
              properties: {
                title: {
                  type: 'string',
                  description: 'Title of B',
                },
                a: {
                  $ref: '#/components/schemas/A',
                  description: 'A property in B',
                },
              },
            },
          },
        },
      } as OpenAPIV3.Document,
      expected: {
        tools: {
          API: {
            methods: [
              {
                name: 'getAB',
                description: 'Get an A-B object',
                // Error responses might not be listed here since none are defined.
                // Just end the description with no Error Responses section.
                inputSchema: {
                  type: 'object',
                  properties: {},
                  required: [],
                  $defs: {
                    A: {
                      type: 'object',
                      description: 'A schema description',
                      additionalProperties: true,
                      properties: {
                        name: {
                          type: 'string',
                          description: 'Name of A',
                        },
                        b: {
                          description: 'B property in A',
                          $ref: '#/$defs/B',
                        },
                      },
                      required: ['name', 'b'],
                    },
                    B: {
                      type: 'object',
                      description: 'B schema description',
                      additionalProperties: true,
                      properties: {
                        title: {
                          type: 'string',
                          description: 'Title of B',
                        },
                        a: {
                          description: 'A property in B',
                          $ref: '#/$defs/A',
                        },
                      },
                      required: ['title', 'a'],
                    },
                  },
                },
                returnSchema: {
                  $ref: '#/$defs/A',
                  description: 'Returns an A object',
                  $defs: {
                    A: {
                      type: 'object',
                      description: 'A schema description',
                      additionalProperties: true,
                      properties: {
                        name: {
                          type: 'string',
                          description: 'Name of A',
                        },
                        b: {
                          description: 'B property in A',
                          $ref: '#/$defs/B',
                        },
                      },
                      required: ['name', 'b'],
                    },
                    B: {
                      type: 'object',
                      description: 'B schema description',
                      additionalProperties: true,
                      properties: {
                        title: {
                          type: 'string',
                          description: 'Title of B',
                        },
                        a: {
                          description: 'A property in B',
                          $ref: '#/$defs/A',
                        },
                      },
                      required: ['title', 'a'],
                    },
                  },
                },
              },
              {
                name: 'createAB',
                description: 'Create an A-B object',
                inputSchema: {
                  type: 'object',
                  properties: {
                    // The requestBody references A. We keep it as a single body field with a $ref.
                    body: {
                      $ref: '#/$defs/A',
                      description: 'A schema description',
                    },
                  },
                  required: ['body'],

                  $defs: {
                    A: {
                      type: 'object',
                      description: 'A schema description',
                      additionalProperties: true,
                      properties: {
                        name: {
                          type: 'string',
                          description: 'Name of A',
                        },
                        b: {
                          description: 'B property in A',
                          $ref: '#/$defs/B',
                        },
                      },
                      required: ['name', 'b'],
                    },
                    B: {
                      type: 'object',
                      description: 'B schema description',
                      additionalProperties: true,
                      properties: {
                        title: {
                          type: 'string',
                          description: 'Title of B',
                        },
                        a: {
                          description: 'A property in B',
                          $ref: '#/$defs/A',
                        },
                      },
                      required: ['title', 'a'],
                    },
                  },
                },
                returnSchema: {
                  $ref: '#/$defs/A',
                  description: 'Created A object',

                  $defs: {
                    A: {
                      type: 'object',
                      description: 'A schema description',
                      additionalProperties: true,
                      properties: {
                        name: {
                          type: 'string',
                          description: 'Name of A',
                        },
                        b: {
                          description: 'B property in A',
                          $ref: '#/$defs/B',
                        },
                      },
                      required: ['name', 'b'],
                    },
                    B: {
                      type: 'object',
                      description: 'B schema description',
                      additionalProperties: true,
                      properties: {
                        title: {
                          type: 'string',
                          description: 'Title of B',
                        },
                        a: {
                          description: 'A property in B',
                          $ref: '#/$defs/A',
                        },
                      },
                      required: ['title', 'a'],
                    },
                  },
                },
              },
            ],
          },
        },
        openApiLookup: {
          'API-getAB': {
            operationId: 'getAB',
            summary: 'Get an A-B object',
            responses: {
              '200': {
                description: 'Returns an A object',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/A' },
                  },
                },
              },
            },
            method: 'get',
            path: '/ab',
          },
          'API-createAB': {
            operationId: 'createAB',
            summary: 'Create an A-B object',
            requestBody: {
              required: true,
              content: {
                'application/json': {
                  schema: {
                    $ref: '#/components/schemas/A',
                    description: 'A schema description',
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Created A object',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/A' },
                  },
                },
              },
            },
            method: 'post',
            path: '/ab',
          },
        },
      },
    },
    {
      name: 'allOf/oneOf References with Full Descriptions',
      input: {
        openapi: '3.0.0',
        info: { title: 'Composed Schema API', version: '1.0.0' },
        paths: {
          '/composed': {
            get: {
              operationId: 'getComposed',
              summary: 'Get a composed resource',
              responses: {
                '200': {
                  description: 'A composed object',
                  content: {
                    'application/json': {
                      schema: { $ref: '#/components/schemas/C' },
                    },
                  },
                },
              },
            },
          },
        },
        components: {
          schemas: {
            Base: {
              type: 'object',
              description: 'Base schema description',
              properties: {
                baseName: {
                  type: 'string',
                  description: 'Name in the base schema',
                },
              },
            },
            D: {
              type: 'object',
              description: 'D schema description',
              properties: {
                dProp: {
                  type: 'integer',
                  description: 'D property integer',
                },
              },
            },
            E: {
              type: 'object',
              description: 'E schema description',
              properties: {
                choice: {
                  description: 'One of these choices',
                  oneOf: [
                    {
                      $ref: '#/components/schemas/F',
                    },
                    {
                      $ref: '#/components/schemas/G',
                    },
                  ],
                },
              },
            },
            F: {
              type: 'object',
              description: 'F schema description',
              properties: {
                fVal: {
                  type: 'boolean',
                  description: 'Boolean in F',
                },
              },
            },
            G: {
              type: 'object',
              description: 'G schema description',
              properties: {
                gVal: {
                  type: 'string',
                  description: 'String in G',
                },
              },
            },
            C: {
              description: 'C schema description',
              allOf: [{ $ref: '#/components/schemas/Base' }, { $ref: '#/components/schemas/D' }, { $ref: '#/components/schemas/E' }],
            },
          },
        },
      } as OpenAPIV3.Document,
      expected: {
        tools: {
          API: {
            methods: [
              {
                name: 'getComposed',
                description: 'Get a composed resource',
                inputSchema: {
                  type: 'object',
                  properties: {},
                  required: [],
                  $defs: {
                    Base: {
                      type: 'object',
                      description: 'Base schema description',
                      additionalProperties: true,
                      properties: {
                        baseName: {
                          type: 'string',
                          description: 'Name in the base schema',
                        },
                      },
                    },
                    C: {
                      description: 'C schema description',
                      allOf: [{ $ref: '#/$defs/Base' }, { $ref: '#/$defs/D' }, { $ref: '#/$defs/E' }],
                    },
                    D: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'D schema description',
                      properties: {
                        dProp: {
                          type: 'integer',
                          description: 'D property integer',
                        },
                      },
                    },
                    E: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'E schema description',
                      properties: {
                        choice: {
                          description: 'One of these choices',
                          oneOf: [{ $ref: '#/$defs/F' }, { $ref: '#/$defs/G' }],
                        },
                      },
                    },
                    F: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'F schema description',
                      properties: {
                        fVal: {
                          type: 'boolean',
                          description: 'Boolean in F',
                        },
                      },
                    },
                    G: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'G schema description',
                      properties: {
                        gVal: {
                          type: 'string',
                          description: 'String in G',
                        },
                      },
                    },
                  },
                },
                returnSchema: {
                  $ref: '#/$defs/C',
                  description: 'A composed object',
                  $defs: {
                    Base: {
                      type: 'object',
                      description: 'Base schema description',
                      additionalProperties: true,
                      properties: {
                        baseName: {
                          type: 'string',
                          description: 'Name in the base schema',
                        },
                      },
                    },
                    C: {
                      description: 'C schema description',
                      allOf: [{ $ref: '#/$defs/Base' }, { $ref: '#/$defs/D' }, { $ref: '#/$defs/E' }],
                    },
                    D: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'D schema description',
                      properties: {
                        dProp: {
                          type: 'integer',
                          description: 'D property integer',
                        },
                      },
                    },
                    E: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'E schema description',
                      properties: {
                        choice: {
                          description: 'One of these choices',
                          oneOf: [{ $ref: '#/$defs/F' }, { $ref: '#/$defs/G' }],
                        },
                      },
                    },
                    F: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'F schema description',
                      properties: {
                        fVal: {
                          type: 'boolean',
                          description: 'Boolean in F',
                        },
                      },
                    },
                    G: {
                      type: 'object',
                      additionalProperties: true,
                      description: 'G schema description',
                      properties: {
                        gVal: {
                          type: 'string',
                          description: 'String in G',
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
        },
        openApiLookup: {
          'API-getComposed': {
            operationId: 'getComposed',
            summary: 'Get a composed resource',
            responses: {
              '200': {
                description: 'A composed object',
                content: {
                  'application/json': {
                    schema: { $ref: '#/components/schemas/C' },
                  },
                },
              },
            },
            method: 'get',
            path: '/composed',
          },
        },
      },
    },
  ]

  it.each(cases)('$name', ({ input, expected }) => {
    const converter = new OpenAPIToMCPConverter(input)
    const { tools, openApiLookup } = converter.convertToMCPTools()

    // Use the custom verification instead of direct equality
    verifyTools(tools, expected.tools)
    expect(openApiLookup).toEqual(expected.openApiLookup)
  })
})

```