#
tokens: 36814/50000 28/29 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/makenotion/notion-mcp-server?page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── images
│       ├── connections.png
│       ├── integration-access.png
│       ├── integrations-capabilities.png
│       ├── integrations-creation.png
│       └── page-access-edit.png
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── build-cli.js
│   ├── notion-openapi.json
│   └── start-server.ts
├── smithery.yaml
├── 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
│       │   └── polyfill-headers.ts
│       ├── index.ts
│       ├── mcp
│       │   ├── __tests__
│       │   │   └── 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

```

--------------------------------------------------------------------------------
/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 MCP Server

> [!NOTE] 
> 
> We’ve introduced **Notion MCP**, a remote MCP server with the following improvements:
> - Easy installation via standard OAuth. No need to fiddle with JSON or API token anymore.
> - Powerful tools tailored to AI agents. These tools are designed with optimized token consumption in mind.
> 
> Learn more and try it out [here](https://developers.notion.com/docs/mcp)


![notion-mcp-sm](https://github.com/user-attachments/assets/6c07003c-8455-4636-b298-d60ffdf46cd8)

This project implements an [MCP server](https://spec.modelcontextprotocol.io/) for the [Notion API](https://developers.notion.com/reference/intro). 

![mcp-demo](https://github.com/user-attachments/assets/e3ff90a7-7801-48a9-b807-f7dd47f0d3d6)

### Installation

#### 1. Setting up Integration in Notion:
Go to [https://www.notion.so/profile/integrations](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 (for example, you will not be able to delete databases via MCP), 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. Connecting content to integration:
Ensure relevant pages and databases are connected to your integration.

To do this, visit the **Access** tab in your internal integration settings. Edit access and select the pages you'd like to use.
![Integration Access tab](docs/images/integration-access.png)

![Edit integration access](docs/images/page-access-edit.png)

Alternatively, you can grant page access individually. You'll need to visit the target page, and click on the 3 dots, and select "Connect to integration". 

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

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

##### Using npm:

**Cursor & Claude:**

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

**Option 1: Using NOTION_TOKEN (recommended)**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {
        "NOTION_TOKEN": "ntn_****"
      }
    }
  }
}
```

**Option 2: Using OPENAPI_MCP_HEADERS (for advanced use cases)**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {
        "OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ntn_****\", \"Notion-Version\": \"2022-06-28\" }"
      }
    }
  }
}
```

**Zed**

Add the following to your `settings.json`

```json
{
  "context_servers": {
    "some-context-server": {
      "command": {
        "path": "npx",
        "args": ["-y", "@notionhq/notion-mcp-server"],
        "env": {
          "OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ntn_****\", \"Notion-Version\": \"2022-06-28\" }"
        }
      },
      "settings": {}
    }
  }
}
```

##### Using Docker:

There are two options for running the MCP server with Docker:

###### Option 1: Using the official Docker Hub image:

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

**Using NOTION_TOKEN (recommended):**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-e", "NOTION_TOKEN",
        "mcp/notion"
      ],
      "env": {
        "NOTION_TOKEN": "ntn_****"
      }
    }
  }
}
```

**Using OPENAPI_MCP_HEADERS (for advanced use cases):**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-e", "OPENAPI_MCP_HEADERS",
        "mcp/notion"
      ],
      "env": {
        "OPENAPI_MCP_HEADERS": "{\"Authorization\":\"Bearer ntn_****\",\"Notion-Version\":\"2022-06-28\"}"
      }
    }
  }
}
```

This approach:
- Uses the official Docker Hub image
- Properly handles JSON escaping via environment variables
- Provides a more reliable configuration method

###### Option 2: Building the Docker image locally:

You can also build and run the Docker image locally. First, build the Docker image:

```bash
docker compose build
```

Then, add the following to your `.cursor/mcp.json` or `claude_desktop_config.json`:

**Using NOTION_TOKEN (recommended):**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-e",
        "NOTION_TOKEN=ntn_****",
        "notion-mcp-server"
      ]
    }
  }
}
```

**Using OPENAPI_MCP_HEADERS (for advanced use cases):**
```javascript
{
  "mcpServers": {
    "notionApi": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-e",
        "OPENAPI_MCP_HEADERS={\"Authorization\": \"Bearer ntn_****\", \"Notion-Version\": \"2022-06-28\"}",
        "notion-mcp-server"
      ]
    }
  }
}
```

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

![Copying your Integration token from the Configuration tab in the developer portal](https://github.com/user-attachments/assets/67b44536-5333-49fa-809c-59581bf5370a)


#### Installing via Smithery

[![smithery badge](https://smithery.ai/badge/@makenotion/notion-mcp-server)](https://smithery.ai/server/@makenotion/notion-mcp-server)

To install Notion API Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@makenotion/notion-mcp-server):

```bash
npx -y @smithery/cli install @makenotion/notion-mcp-server --client claude
```

### Transport Options

The Notion MCP Server supports two transport modes:

#### STDIO Transport (Default)
The default transport mode uses standard input/output for communication. This is the standard MCP transport used by most clients like Claude Desktop.

```bash
# Run with default stdio transport
npx @notionhq/notion-mcp-server

# Or explicitly specify stdio
npx @notionhq/notion-mcp-server --transport stdio
```

#### Streamable HTTP Transport
For web-based applications or clients that prefer HTTP communication, you can use the Streamable HTTP transport:

```bash
# Run with Streamable HTTP transport on port 3000 (default)
npx @notionhq/notion-mcp-server --transport http

# Run on a custom port
npx @notionhq/notion-mcp-server --transport http --port 8080

# Run with a custom authentication token
npx @notionhq/notion-mcp-server --transport http --auth-token "your-secret-token"
```

When using Streamable HTTP transport, the server will be available at `http://0.0.0.0:<port>/mcp`.

##### Authentication
The Streamable HTTP transport requires bearer token authentication for security. You have three options:

**Option 1: Auto-generated token (recommended for development)**
```bash
npx @notionhq/notion-mcp-server --transport http
```
The server will generate a secure random token and display it in the console:
```
Generated auth token: a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab
Use this token in the Authorization header: Bearer a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab
```

**Option 2: Custom token via command line (recommended for production)**
```bash
npx @notionhq/notion-mcp-server --transport http --auth-token "your-secret-token"
```

**Option 3: Custom token via environment variable (recommended for production)**
```bash
AUTH_TOKEN="your-secret-token" npx @notionhq/notion-mcp-server --transport http
```

The command line argument `--auth-token` takes precedence over the `AUTH_TOKEN` environment variable if both are provided.

##### Making HTTP Requests
All requests to the Streamable HTTP transport must include the bearer token in the Authorization header:

```bash
# Example request
curl -H "Authorization: Bearer your-token-here" \
     -H "Content-Type: application/json" \
     -H "mcp-session-id: your-session-id" \
     -d '{"jsonrpc": "2.0", "method": "initialize", "params": {}, "id": 1}' \
     http://localhost:3000/mcp
```

**Note:** Make sure to set either the `NOTION_TOKEN` environment variable (recommended) or the `OPENAPI_MCP_HEADERS` environment variable with your Notion integration token when using either transport mode.

### Examples

1. Using the following instruction
```
Comment "Hello MCP" on page "Getting started"
```

AI will correctly plan two API calls, `v1/search` and `v1/comments`, to achieve the task

2. Similarly, the following instruction will result in a new page named "Notion MCP" added to parent page "Development"
```
Add a page titled "Notion MCP" to page "Development"
```

3. You may also reference content ID directly
```
Get the content of page 1a6b35e6e67f802fa7e1d27686f017f2
```

### Development

Build

```
npm run build
```

Execute

```
npx -y --prefix /path/to/local/notion-mcp-server @notionhq/notion-mcp-server
```

Publish

```
npm publish --access public
```

```

--------------------------------------------------------------------------------
/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"]
}

```

--------------------------------------------------------------------------------
/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);
});

```

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

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
# 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/openapi-mcp-server/client/polyfill-headers.ts:
--------------------------------------------------------------------------------

```typescript
/*
* The Headers class was supported in Node.js starting with version 18, which was released on April 19, 2022.
* We need to have a polyfill ready to work for old Node versions.
* See more at https://github.com/makenotion/notion-mcp-server/issues/32
* */
class PolyfillHeaders {
  private headers: Map<string, string[]> = new Map();

  constructor(init?: Record<string, string>) {
    if (init) {
      Object.entries(init).forEach(([key, value]) => {
        this.append(key, value);
      });
    }
  }

  public append(name: string, value: string): void {
    const key = name.toLowerCase();

    if (!this.headers.has(key)) {
      this.headers.set(key, []);
    }

    this.headers.get(key)!.push(value);
  }

  public get(name: string): string | null {
    const key = name.toLowerCase();

    if (!this.headers.has(key)) {
      return null;
    }

    return this.headers.get(key)!.join(', ');
  }
}

const GlobalHeaders = typeof global !== 'undefined' && 'Headers' in global
  ? (global as any).Headers
  : undefined;

export const Headers = (GlobalHeaders || PolyfillHeaders);

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/build/project-config

startCommand:
  type: stdio
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => {
      const env = {};
      if (config.notionToken) {
        env.NOTION_TOKEN = config.notionToken;
      } else if (config.openapiMcpHeaders) {
        env.OPENAPI_MCP_HEADERS = config.openapiMcpHeaders;
      }
      if (config.baseUrl) env.BASE_URL = config.baseUrl;
      return { command: 'notion-mcp-server', args: [], env };
    }
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    anyOf:
      - required: [notionToken]
      - required: [openapiMcpHeaders]
    properties:
      notionToken:
        type: string
        description: Notion integration token (recommended)
      openapiMcpHeaders:
        type: string
        default: "{}"
        description: JSON string for HTTP headers, must include Authorization and
          Notion-Version (alternative to notionToken)
      baseUrl:
        type: string
        description: Optional override for Notion API base URL
  exampleConfig:
    notionToken: 'ntn_abcdef'
    baseUrl: https://api.notion.com

```

--------------------------------------------------------------------------------
/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": "@notionhq/notion-mcp-server",
  "keywords": [
    "notion",
    "api",
    "mcp",
    "server"
  ],
  "version": "1.9.0",
  "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.13.3",
    "axios": "^1.8.4",
    "express": "^4.21.2",
    "form-data": "^4.0.1",
    "mustache": "^4.2.0",
    "node-fetch": "^3.3.2",
    "openapi-client-axios": "^7.5.5",
    "openapi-schema-validator": "^12.1.3",
    "openapi-types": "^12.1.3",
    "which": "^5.0.0",
    "yargs": "^17.7.2",
    "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",
    "esbuild": "^0.25.2",
    "multer": "1.4.5-lts.1",
    "openai": "^4.91.1",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2",
    "vitest": "^3.1.1"
  },
  "description": "Official MCP server for Notion API",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "[email protected]:makenotion/notion-mcp-server.git"
  },
  "author": "@notionhq",
  "bugs": {
    "url": "https://github.com/makenotion/notion-mcp-server/issues"
  },
  "homepage": "https://github.com/makenotion/notion-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.integration.test.ts:
--------------------------------------------------------------------------------

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

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 { Headers } from './polyfill-headers'
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/proxy.ts:
--------------------------------------------------------------------------------

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

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
  }>
}

// 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 }>

  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[] = []

      // 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'],
          })
        })
      })

      return { tools }
    })

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

      // Find the operation in OpenAPI spec
      const operation = this.findOperation(name)
      if (!operation) {
        throw new Error(`Method ${name} not found`)
      }

      try {
        // Execute the operation
        const response = await this.httpClient.executeOperation(operation, params)

        // Convert response to MCP format
        return {
          content: [
            {
              type: 'text', // currently this is the only type that seems to be used by mcp server
              text: JSON.stringify(response.data), // TODO: pass through the http status code text?
            },
          ],
        }
      } catch (error) {
        console.error('Error in tool call', error)
        if (error instanceof HttpClientError) {
          console.error('HttpClientError encountered, returning structured error', error)
          const data = error.data?.response?.data ?? error.data ?? {}
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  status: 'error', // TODO: get this from http status code?
                  ...(typeof data === 'object' ? data : { data: data }),
                }),
              },
            ],
          }
        }
        throw error
      }
    })
  }

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

  private parseHeadersFromEnv(): Record<string, string> {
    // First try OPENAPI_MCP_HEADERS (existing behavior)
    const headersJson = process.env.OPENAPI_MCP_HEADERS
    if (headersJson) {
      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)
        } else if (Object.keys(headers).length > 0) {
          // Only use OPENAPI_MCP_HEADERS if it contains actual headers
          return headers
        }
        // If OPENAPI_MCP_HEADERS is empty object, fall through to try NOTION_TOKEN
      } catch (error) {
        console.warn('Failed to parse OPENAPI_MCP_HEADERS environment variable:', error)
        // Fall through to try NOTION_TOKEN
      }
    }

    // Alternative: try NOTION_TOKEN
    const notionToken = process.env.NOTION_TOKEN
    if (notionToken) {
      return {
        'Authorization': `Bearer ${notionToken}`,
        'Notion-Version': '2022-06-28'
      }
    }

    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) {
    // The SDK will handle stdio communication
    await this.server.connect(transport)
  }

  getServer() {
    return this.server
  }
}

```

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

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

vi.mock('fs')
vi.mock('form-data')

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 mockFormData = new FormData()
    const mockFileStream = { pipe: vi.fn() }
    const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }

    vi.mocked(fs.createReadStream).mockReturnValue(mockFileStream as any)
    vi.mocked(FormData.prototype.append).mockImplementation(() => {})
    vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)

    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(FormData.prototype.append).toHaveBeenCalledWith('file', mockFileStream)
    expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test file')
    expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
  })

  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 mockFormData = new FormData()
    const mockFileStream1 = { pipe: vi.fn() }
    const mockFileStream2 = { pipe: vi.fn() }
    const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }

    vi.mocked(fs.createReadStream)
      .mockReturnValueOnce(mockFileStream1 as any)
      .mockReturnValueOnce(mockFileStream2 as any)
    vi.mocked(FormData.prototype.append).mockImplementation(() => {})
    vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)

    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(FormData.prototype.append).toHaveBeenCalledWith('file1', mockFileStream1)
    expect(FormData.prototype.append).toHaveBeenCalledWith('file2', mockFileStream2)
    expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test files')
    expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
  })
})

```

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

```typescript
import path from 'node:path'
import { fileURLToPath } from 'url'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
import { randomUUID, randomBytes } from 'node:crypto'
import express from 'express'

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

export async function startServer(args: string[] = process.argv) {
  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

  // Parse command line arguments manually (similar to slack-mcp approach)
  function parseArgs() {
    const args = process.argv.slice(2);
    let transport = 'stdio'; // default
    let port = 3000;
    let authToken: string | undefined;

    for (let i = 0; i < args.length; i++) {
      if (args[i] === '--transport' && i + 1 < args.length) {
        transport = args[i + 1];
        i++; // skip next argument
      } else if (args[i] === '--port' && i + 1 < args.length) {
        port = parseInt(args[i + 1], 10);
        i++; // skip next argument
      } else if (args[i] === '--auth-token' && i + 1 < args.length) {
        authToken = args[i + 1];
        i++; // skip next argument
      } else if (args[i] === '--help' || args[i] === '-h') {
        console.log(`
Usage: notion-mcp-server [options]

Options:
  --transport <type>     Transport type: 'stdio' or 'http' (default: stdio)
  --port <number>        Port for HTTP server when using Streamable HTTP transport (default: 3000)
  --auth-token <token>   Bearer token for HTTP transport authentication (optional)
  --help, -h             Show this help message

Environment Variables:
  NOTION_TOKEN           Notion integration token (recommended)
  OPENAPI_MCP_HEADERS    JSON string with Notion API headers (alternative)
  AUTH_TOKEN             Bearer token for HTTP transport authentication (alternative to --auth-token)

Examples:
  notion-mcp-server                                    # Use stdio transport (default)
  notion-mcp-server --transport stdio                  # Use stdio transport explicitly
  notion-mcp-server --transport http                   # Use Streamable HTTP transport on port 3000
  notion-mcp-server --transport http --port 8080       # Use Streamable HTTP transport on port 8080
  notion-mcp-server --transport http --auth-token mytoken # Use Streamable HTTP transport with custom auth token
  AUTH_TOKEN=mytoken notion-mcp-server --transport http # Use Streamable HTTP transport with auth token from env var
`);
        process.exit(0);
      }
      // Ignore unrecognized arguments (like command name passed by Docker)
    }

    return { transport: transport.toLowerCase(), port, authToken };
  }

  const options = parseArgs()
  const transport = options.transport

  if (transport === 'stdio') {
    // Use stdio transport (default)
    const proxy = await initProxy(specPath, baseUrl)
    await proxy.connect(new StdioServerTransport())
    return proxy.getServer()
  } else if (transport === 'http') {
    // Use Streamable HTTP transport
    const app = express()
    app.use(express.json())

    // Generate or use provided auth token (from CLI arg or env var)
    const authToken = options.authToken || process.env.AUTH_TOKEN || randomBytes(32).toString('hex')
    if (!options.authToken && !process.env.AUTH_TOKEN) {
      console.log(`Generated auth token: ${authToken}`)
      console.log(`Use this token in the Authorization header: Bearer ${authToken}`)
    }

    // Authorization middleware
    const authenticateToken = (req: express.Request, res: express.Response, next: express.NextFunction): void => {
      const authHeader = req.headers['authorization']
      const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN

      if (!token) {
        res.status(401).json({
          jsonrpc: '2.0',
          error: {
            code: -32001,
            message: 'Unauthorized: Missing bearer token',
          },
          id: null,
        })
        return
      }

      if (token !== authToken) {
        res.status(403).json({
          jsonrpc: '2.0',
          error: {
            code: -32002,
            message: 'Forbidden: Invalid bearer token',
          },
          id: null,
        })
        return
      }

      next()
    }

    // Health endpoint (no authentication required)
    app.get('/health', (req, res) => {
      res.status(200).json({
        status: 'healthy',
        timestamp: new Date().toISOString(),
        transport: 'http',
        port: options.port
      })
    })

    // Apply authentication to all /mcp routes
    app.use('/mcp', authenticateToken)

    // Map to store transports by session ID
    const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}

    // Handle POST requests for client-to-server communication
    app.post('/mcp', async (req, res) => {
      try {
        // Check for existing session ID
        const sessionId = req.headers['mcp-session-id'] as string | undefined
        let transport: StreamableHTTPServerTransport

        if (sessionId && transports[sessionId]) {
          // Reuse existing transport
          transport = transports[sessionId]
        } else if (!sessionId && isInitializeRequest(req.body)) {
          // New initialization request
          transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: () => randomUUID(),
            onsessioninitialized: (sessionId) => {
              // Store the transport by session ID
              transports[sessionId] = transport
            }
          })

          // Clean up transport when closed
          transport.onclose = () => {
            if (transport.sessionId) {
              delete transports[transport.sessionId]
            }
          }

          const proxy = await initProxy(specPath, baseUrl)
          await proxy.connect(transport)
        } else {
          // Invalid request
          res.status(400).json({
            jsonrpc: '2.0',
            error: {
              code: -32000,
              message: 'Bad Request: No valid session ID provided',
            },
            id: null,
          })
          return
        }

        // Handle the request
        await transport.handleRequest(req, res, req.body)
      } catch (error) {
        console.error('Error handling MCP request:', error)
        if (!res.headersSent) {
          res.status(500).json({
            jsonrpc: '2.0',
            error: {
              code: -32603,
              message: 'Internal server error',
            },
            id: null,
          })
        }
      }
    })

    // Handle GET requests for server-to-client notifications via Streamable HTTP
    app.get('/mcp', async (req, res) => {
      const sessionId = req.headers['mcp-session-id'] as string | undefined
      if (!sessionId || !transports[sessionId]) {
        res.status(400).send('Invalid or missing session ID')
        return
      }
      
      const transport = transports[sessionId]
      await transport.handleRequest(req, res)
    })

    // Handle DELETE requests for session termination
    app.delete('/mcp', async (req, res) => {
      const sessionId = req.headers['mcp-session-id'] as string | undefined
      if (!sessionId || !transports[sessionId]) {
        res.status(400).send('Invalid or missing session ID')
        return
      }
      
      const transport = transports[sessionId]
      await transport.handleRequest(req, res)
    })

    const port = options.port
    app.listen(port, '0.0.0.0', () => {
      console.log(`MCP Server listening on port ${port}`)
      console.log(`Endpoint: http://0.0.0.0:${port}/mcp`)
      console.log(`Health check: http://0.0.0.0:${port}/health`)
      console.log(`Authentication: Bearer token required`)
      if (options.authToken) {
        console.log(`Using provided auth token`)
      }
    })

    // Return a dummy server for compatibility
    return { close: () => {} }
  } else {
    throw new Error(`Unsupported transport: ${transport}. Use 'stdio' or 'http'.`)
  }
}

startServer(process.argv).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)
})

```

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

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

// 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: {},
          },
        }),
      ).rejects.toThrow('Method nonExistentMethod not found')
    })

    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')
    })

    it('should use NOTION_TOKEN when OPENAPI_MCP_HEADERS is not set', () => {
      delete process.env.OPENAPI_MCP_HEADERS
      process.env.NOTION_TOKEN = 'ntn_test_token_123'

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {
            'Authorization': 'Bearer ntn_test_token_123',
            'Notion-Version': '2022-06-28'
          },
        }),
        expect.anything(),
      )
    })

    it('should prioritize OPENAPI_MCP_HEADERS over NOTION_TOKEN when both are set', () => {
      process.env.OPENAPI_MCP_HEADERS = JSON.stringify({
        Authorization: 'Bearer custom_token',
        'Custom-Header': 'custom_value',
      })
      process.env.NOTION_TOKEN = 'ntn_test_token_123'

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

    it('should return empty object when neither OPENAPI_MCP_HEADERS nor NOTION_TOKEN are set', () => {
      delete process.env.OPENAPI_MCP_HEADERS
      delete process.env.NOTION_TOKEN

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

    it('should use NOTION_TOKEN when OPENAPI_MCP_HEADERS is empty object', () => {
      process.env.OPENAPI_MCP_HEADERS = '{}'
      process.env.NOTION_TOKEN = 'ntn_test_token_123'

      const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
      expect(HttpClient).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: {
            'Authorization': 'Bearer ntn_test_token_123',
            'Notion-Version': '2022-06-28'
          },
        }),
        expect.anything(),
      )
    })
  })
  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/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
          mcpMethod.description = this.getDescription(operation.summary || operation.description || '')
          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: this.getDescription(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: this.getDescription(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')
  }

  private getDescription(description: string): string {
    return "Notion | " + description
  }
}

```

--------------------------------------------------------------------------------
/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/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)
  })
})

```
Page 1/2FirstPrevNextLast