#
tokens: 49285/50000 63/75 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/kadykov/mcp-openapi-schema-explorer?page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   ├── devcontainer.json
│   └── Dockerfile
├── .github
│   ├── dependabot.yml
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .releaserc.json
├── .tokeignore
├── assets
│   ├── logo-400.png
│   └── logo-full.png
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── DOCKERHUB_README.md
├── eslint.config.js
├── jest.config.js
├── justfile
├── LICENSE
├── llms-install.md
├── memory-bank
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   └── techContext.md
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── generate-version.js
├── src
│   ├── config.ts
│   ├── handlers
│   │   ├── component-detail-handler.ts
│   │   ├── component-map-handler.ts
│   │   ├── handler-utils.ts
│   │   ├── operation-handler.ts
│   │   ├── path-item-handler.ts
│   │   └── top-level-field-handler.ts
│   ├── index.ts
│   ├── rendering
│   │   ├── components.ts
│   │   ├── document.ts
│   │   ├── path-item.ts
│   │   ├── paths.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── services
│   │   ├── formatters.ts
│   │   ├── reference-transform.ts
│   │   └── spec-loader.ts
│   ├── types.ts
│   ├── utils
│   │   └── uri-builder.ts
│   └── version.ts
├── test
│   ├── __tests__
│   │   ├── e2e
│   │   │   ├── format.test.ts
│   │   │   ├── resources.test.ts
│   │   │   └── spec-loading.test.ts
│   │   └── unit
│   │       ├── config.test.ts
│   │       ├── handlers
│   │       │   ├── component-detail-handler.test.ts
│   │       │   ├── component-map-handler.test.ts
│   │       │   ├── handler-utils.test.ts
│   │       │   ├── operation-handler.test.ts
│   │       │   ├── path-item-handler.test.ts
│   │       │   └── top-level-field-handler.test.ts
│   │       ├── rendering
│   │       │   ├── components.test.ts
│   │       │   ├── document.test.ts
│   │       │   ├── path-item.test.ts
│   │       │   └── paths.test.ts
│   │       ├── services
│   │       │   ├── formatters.test.ts
│   │       │   ├── reference-transform.test.ts
│   │       │   └── spec-loader.test.ts
│   │       └── utils
│   │           └── uri-builder.test.ts
│   ├── fixtures
│   │   ├── complex-endpoint.json
│   │   ├── empty-api.json
│   │   ├── multi-component-types.json
│   │   ├── paths-test.json
│   │   ├── sample-api.json
│   │   └── sample-v2-api.json
│   ├── setup.ts
│   └── utils
│       ├── console-helpers.ts
│       ├── mcp-test-helpers.ts
│       └── test-types.ts
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/.tokeignore:
--------------------------------------------------------------------------------

```
package-lock.json

```

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
dist/
node_modules/
local-docs/

```

--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------

```json
{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "quoteProps": "as-needed",
  "trailingComma": "es5",
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "lf"
}

```

--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------

```json
{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    [
      "@semantic-release/exec",
      {
        "prepareCmd": "node ./scripts/generate-version.js ${nextRelease.version}"
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["package.json", "package-lock.json", "CHANGELOG.md", "src/version.ts"],
        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
      }
    ],
    [
      "@codedependant/semantic-release-docker",
      {
        "dockerProject": "kadykov",
        "dockerImage": "mcp-openapi-schema-explorer",
        "dockerLogin": false
      }
    ],
    "@semantic-release/github"
  ]
}

```

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

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# Personal files
kadykov-*

# Local documentation and examples
local-docs/

```

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

```markdown
<p align="center">
  <img src="assets/logo-400.png" alt="MCP OpenAPI Schema Explorer Logo" width="200">
</p>

# MCP OpenAPI Schema Explorer

[![npm version](https://badge.fury.io/js/mcp-openapi-schema-explorer.svg)](https://badge.fury.io/js/mcp-openapi-schema-explorer)
[![NPM Downloads](https://img.shields.io/npm/dw/mcp-openapi-schema-explorer)](https://badge.fury.io/js/mcp-openapi-schema-explorer)
[![Docker Pulls](https://img.shields.io/docker/pulls/kadykov/mcp-openapi-schema-explorer.svg)](https://hub.docker.com/r/kadykov/mcp-openapi-schema-explorer)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gh/kadykov/mcp-openapi-schema-explorer/graph/badge.svg?token=LFDOMJ6W4W)](https://codecov.io/gh/kadykov/mcp-openapi-schema-explorer)
[![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/819a3ba3-ad54-4657-9241-648497e57d7b)
[![Lines of code](https://tokei.rs/b1/github/kadykov/mcp-openapi-schema-explorer?category=code)](https://github.com/kadykov/mcp-openapi-schema-explorer)
[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/kadykov/mcp-openapi-schema-explorer)](https://archestra.ai/mcp-catalog/kadykov__mcp-openapi-schema-explorer)

An MCP (Model Context Protocol) server that provides token-efficient access to OpenAPI (v3.0) and Swagger (v2.0) specifications via **MCP Resources**.

## Project Goal

The primary goal of this project is to allow MCP clients (like Cline or Claude Desktop) to explore the structure and details of large OpenAPI specifications without needing to load the entire file into an LLM's context window. It achieves this by exposing parts of the specification through MCP Resources, which are well-suited for read-only data exploration.

This server supports loading specifications from both local file paths and remote HTTP/HTTPS URLs. Swagger v2.0 specifications are automatically converted to OpenAPI v3.0 upon loading.

## Why MCP Resources?

The Model Context Protocol defines both **Resources** and **Tools**.

- **Resources:** Represent data sources (like files, API responses). They are ideal for read-only access and exploration by MCP clients (e.g., browsing API paths in Claude Desktop).
- **Tools:** Represent executable actions or functions, often used by LLMs to perform tasks or interact with external systems.

While other MCP servers exist that provide access to OpenAPI specs via _Tools_, this project specifically focuses on providing access via _Resources_. This makes it particularly useful for direct exploration within MCP client applications.

For more details on MCP clients and their capabilities, see the [MCP Client Documentation](https://modelcontextprotocol.io/clients).

## Installation

For the recommended usage methods (`npx` and Docker, described below), **no separate installation step is required**. Your MCP client will download the package or pull the Docker image automatically based on the configuration you provide.

However, if you prefer or need to install the server explicitly, you have two options:

1.  **Global Installation:** You can install the package globally using npm:

    ```bash
    npm install -g mcp-openapi-schema-explorer
    ```

    See **Method 3** below for how to configure your MCP client to use a globally installed server.

2.  **Local Development/Installation:** You can clone the repository and build it locally:
    ```bash
    git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git
    cd mcp-openapi-schema-explorer
    npm install
    npm run build
    ```
    See **Method 4** below for how to configure your MCP client to run the server from your local build using `node`.

## Adding the Server to your MCP Client

This server is designed to be run by MCP clients (like Claude Desktop, Windsurf, Cline, etc.). To use it, you add a configuration entry to your client's settings file (often a JSON file). This entry tells the client how to execute the server process (e.g., using `npx`, `docker`, or `node`). The server itself doesn't require separate configuration beyond the command-line arguments specified in the client settings entry.

Below are the common methods for adding the server entry to your client's configuration.

### Method 1: npx (Recommended)

Using `npx` is recommended as it avoids global/local installation and ensures the client uses the latest published version.

**Example Client Configuration Entry (npx Method):**

Add the following JSON object to the `mcpServers` section of your MCP client's configuration file. This entry instructs the client on how to run the server using `npx`:

```json
{
  "mcpServers": {
    "My API Spec (npx)": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-openapi-schema-explorer@latest",
        "<path-or-url-to-spec>",
        "--output-format",
        "yaml"
      ],
      "env": {}
    }
  }
}
```

**Configuration Notes:**

- Replace `"My API Spec (npx)"` with a unique name for this server instance in your client.
- Replace `<path-or-url-to-spec>` with the absolute local file path or full remote URL of your specification.
- The `--output-format` is optional (`json`, `yaml`, `json-minified`), defaulting to `json`.
- To explore multiple specifications, add separate entries in `mcpServers`, each with a unique name and pointing to a different spec.

### Method 2: Docker

You can instruct your MCP client to run the server using the official Docker image: `kadykov/mcp-openapi-schema-explorer`.

**Example Client Configuration Entries (Docker Method):**

Add one of the following JSON objects to the `mcpServers` section of your MCP client's configuration file. These entries instruct the client on how to run the server using `docker run`:

- **Remote URL:** Pass the URL directly to `docker run`.

- **Using a Remote URL:**

  ```json
  {
    "mcpServers": {
      "My API Spec (Docker Remote)": {
        "command": "docker",
        "args": [
          "run",
          "--rm",
          "-i",
          "kadykov/mcp-openapi-schema-explorer:latest",
          "<remote-url-to-spec>"
        ],
        "env": {}
      }
    }
  }
  ```

- **Using a Local File:** (Requires mounting the file into the container)
  ```json
  {
    "mcpServers": {
      "My API Spec (Docker Local)": {
        "command": "docker",
        "args": [
          "run",
          "--rm",
          "-i",
          "-v",
          "/full/host/path/to/spec.yaml:/spec/api.yaml",
          "kadykov/mcp-openapi-schema-explorer:latest",
          "/spec/api.yaml",
          "--output-format",
          "yaml"
        ],
        "env": {}
      }
    }
  }
  ```
  **Important:** Replace `/full/host/path/to/spec.yaml` with the correct absolute path on your host machine. The path `/spec/api.yaml` is the corresponding path inside the container.

### Method 3: Global Installation (Less Common)

If you have installed the package globally using `npm install -g`, you can configure your client to run it directly.

```bash
# Run this command once in your terminal
npm install -g mcp-openapi-schema-explorer
```

**Example Client Configuration Entry (Global Install Method):**

Add the following entry to your MCP client's configuration file. This assumes the `mcp-openapi-schema-explorer` command is accessible in the client's execution environment PATH.

```json
{
  "mcpServers": {
    "My API Spec (Global)": {
      "command": "mcp-openapi-schema-explorer",
      "args": ["<path-or-url-to-spec>", "--output-format", "yaml"],
      "env": {}
    }
  }
}
```

- Ensure the `command` (`mcp-openapi-schema-explorer`) is accessible in the PATH environment variable used by your MCP client.

### Method 4: Local Development/Installation

This method is useful if you have cloned the repository locally for development or to run a modified version.

**Setup Steps (Run once in your terminal):**

1.  Clone the repository: `git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git`
2.  Navigate into the directory: `cd mcp-openapi-schema-explorer`
3.  Install dependencies: `npm install`
4.  Build the project: `npm run build` (or `just build`)

**Example Client Configuration Entry (Local Development Method):**

Add the following entry to your MCP client's configuration file. This instructs the client to run the locally built server using `node`.

```json
{
  "mcpServers": {
    "My API Spec (Local Dev)": {
      "command": "node",
      "args": [
        "/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js",
        "<path-or-url-to-spec>",
        "--output-format",
        "yaml"
      ],

      "env": {}
    }
  }
}
```

**Important:** Replace `/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js` with the correct absolute path to the built `index.js` file in your cloned repository.

## Features

- **MCP Resource Access:** Explore OpenAPI specs via intuitive URIs (`openapi://info`, `openapi://paths/...`, `openapi://components/...`).
- **OpenAPI v3.0 & Swagger v2.0 Support:** Loads both formats, automatically converting v2.0 to v3.0.
- **Local & Remote Files:** Load specs from local file paths or HTTP/HTTPS URLs.
- **Token-Efficient:** Designed to minimize token usage for LLMs by providing structured access.
- **Multiple Output Formats:** Get detailed views in JSON (default), YAML, or minified JSON (`--output-format`).
- **Dynamic Server Name:** Server name in MCP clients reflects the `info.title` from the loaded spec.
- **Reference Transformation:** Internal `$ref`s (`#/components/...`) are transformed into clickable MCP URIs.

## Available MCP Resources

This server exposes the following MCP resource templates for exploring the OpenAPI specification.

**Understanding Multi-Value Parameters (`*`)**

Some resource templates include parameters ending with an asterisk (`*`), like `{method*}` or `{name*}`. This indicates that the parameter accepts **multiple comma-separated values**. For example, to request details for both the `GET` and `POST` methods of a path, you would use a URI like `openapi://paths/users/get,post`. This allows fetching details for multiple items in a single request.

**Resource Templates:**

- **`openapi://{field}`**
  - **Description:** Accesses top-level fields of the OpenAPI document (e.g., `info`, `servers`, `tags`) or lists the contents of `paths` or `components`. The specific available fields depend on the loaded specification.
  - **Example:** `openapi://info`
  - **Output:** `text/plain` list for `paths` and `components`; configured format (JSON/YAML/minified JSON) for other fields.
  - **Completions:** Provides dynamic suggestions for `{field}` based on the actual top-level keys found in the loaded spec.

- **`openapi://paths/{path}`**
  - **Description:** Lists the available HTTP methods (operations) for a specific API path.
  - **Parameter:** `{path}` - The API path string. **Must be URL-encoded** (e.g., `/users/{id}` becomes `users%2F%7Bid%7D`).
  - **Example:** `openapi://paths/users%2F%7Bid%7D`
  - **Output:** `text/plain` list of methods.
  - **Completions:** Provides dynamic suggestions for `{path}` based on the paths found in the loaded spec (URL-encoded).

- **`openapi://paths/{path}/{method*}`**
  - **Description:** Gets the detailed specification for one or more operations (HTTP methods) on a specific API path.
  - **Parameters:**
    - `{path}` - The API path string. **Must be URL-encoded**.
    - `{method*}` - One or more HTTP methods (e.g., `get`, `post`, `get,post`). Case-insensitive.
  - **Example (Single):** `openapi://paths/users%2F%7Bid%7D/get`
  - **Example (Multiple):** `openapi://paths/users%2F%7Bid%7D/get,post`
  - **Output:** Configured format (JSON/YAML/minified JSON).
  - **Completions:** Provides dynamic suggestions for `{path}`. Provides static suggestions for `{method*}` (common HTTP verbs like GET, POST, PUT, DELETE, etc.).

- **`openapi://components/{type}`**
  - **Description:** Lists the names of all defined components of a specific type (e.g., `schemas`, `responses`, `parameters`). The specific available types depend on the loaded specification. Also provides a short description for each listed type.
  - **Example:** `openapi://components/schemas`
  - **Output:** `text/plain` list of component names with descriptions.
  - **Completions:** Provides dynamic suggestions for `{type}` based on the component types found in the loaded spec.

- **`openapi://components/{type}/{name*}`**
  - **Description:** Gets the detailed specification for one or more named components of a specific type.
  - **Parameters:**
    - `{type}` - The component type.
    - `{name*}` - One or more component names (e.g., `User`, `Order`, `User,Order`). Case-sensitive.
  - **Example (Single):** `openapi://components/schemas/User`
  - **Example (Multiple):** `openapi://components/schemas/User,Order`
  - **Output:** Configured format (JSON/YAML/minified JSON).
  - **Completions:** Provides dynamic suggestions for `{type}`. Provides dynamic suggestions for `{name*}` _only if_ the loaded spec contains exactly one component type overall (e.g., only `schemas`). This limitation exists because the MCP SDK currently doesn't support providing completions scoped to the selected `{type}`; providing all names across all types could be misleading.

## Contributing

Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on setting up the development environment, running tests, and submitting changes.

## Releases

This project uses [`semantic-release`](https://github.com/semantic-release/semantic-release) for automated version management and package publishing based on [Conventional Commits](https://www.conventionalcommits.org/).

## Future Plans

(Future plans to be determined)

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to MCP OpenAPI Schema Explorer

Thank you for considering contributing to this project! We welcome improvements and bug fixes.

## Getting Started

- **Devcontainer:** The easiest way to get a consistent development environment is to use the provided [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers) configuration (`.devcontainer/`). If you have Docker and the VS Code Dev Containers extension installed, simply reopen the project folder in a container.
- **Manual Setup:** If you prefer not to use the devcontainer, ensure you have Node.js (v22 or later recommended) and npm installed. Clone the repository and run `npm install` to install dependencies.

## Development Workflow

This project uses [`just`](https://github.com/casey/just) as a command runner for common development tasks. See the `justfile` for all available commands. Key commands include:

- `just install`: Install dependencies (`npm install`).
- `just format`: Format code using Prettier.
- `just lint`: Check code for linting errors using ESLint.
- `just build`: Compile TypeScript code (`npx tsc`).
- `just test`: Run unit and end-to-end tests using Jest.
- `just test-coverage`: Run tests and generate a coverage report.
- `just security`: Run security checks (npm audit, license check).
- `just all`: Run format, lint, build, test-coverage, and security checks sequentially.

Please ensure `just all` passes before submitting a pull request.

## Code Style

- **Formatting:** We use [Prettier](https://prettier.io/) for automatic code formatting. Please run `just format` before committing.
- **Linting:** We use [ESLint](https://eslint.org/) for code analysis. Please run `just lint` to check for issues.

## Commit Messages

This project uses [`semantic-release`](https://github.com/semantic-release/semantic-release) to automate versioning and releases. Therefore, commit messages **must** follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. This allows the release process to automatically determine the version bump (patch, minor, major) and generate changelogs.

Common commit types include:

- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- `chore`: Other changes that don't modify src or test files

Example: `feat: add support for YAML output format`
Example: `fix: correct handling of remote URL loading errors`
Example: `docs: update README with client configuration examples`

## Cline & Memory Bank

This project utilizes [Cline](https://github.com/cline/cline) for AI-assisted development. The `memory-bank/` directory contains documentation specifically for Cline's context. Maintaining this memory bank helps ensure Cline can effectively assist with development tasks.

If you make significant changes to the project's architecture, features, or development process, please consider updating the relevant files in `memory-bank/`. You can learn more about the Cline Memory Bank [here](https://docs.cline.bot/improving-your-prompting-skills/cline-memory-bank).

## Submitting Changes

1.  Fork the repository.
2.  Create a new branch for your feature or fix (`git checkout -b feat/my-new-feature` or `git checkout -b fix/my-bug-fix`).
3.  Make your changes.
4.  Ensure all checks pass (`just all`).
5.  Commit your changes using the Conventional Commits format.
6.  Push your branch to your fork (`git push origin feat/my-new-feature`).
7.  Open a pull request against the `main` branch of the original repository.

Thank you for your contribution!

```

--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:22-alpine3.21

WORKDIR /workspaces/mcp-openapi-schema-explorer

```

--------------------------------------------------------------------------------
/test/fixtures/empty-api.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.0.0",
  "info": {
    "title": "Empty API",
    "version": "1.0.0"
  },
  "paths": {}
}

```

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

```json
{
  "extends": "./tsconfig.json",
  "include": ["src/**/*", "test/**/*"],
  "exclude": ["node_modules", "dist", "local-docs"]
}

```

--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------

```typescript
// Auto-generated by scripts/generate-version.js during semantic-release prepare step
// Do not edit this file manually.

export const VERSION = '1.3.0';

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPI } from 'openapi-types';
import type { TransformContext } from './services/reference-transform.js';

/** Common HTTP methods used in OpenAPI specs */
export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch';

/** Interface for spec loader */
export interface SpecLoaderService {
  getSpec(): Promise<OpenAPI.Document>;
  getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document>;
}

// Re-export transform types
export type { TransformContext };

// Re-export OpenAPI types
export type { OpenAPI };

```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
version: 2
updates:
  - package-ecosystem: 'npm'
    directory: '/'
    schedule:
      interval: 'weekly'
    open-pull-requests-limit: 10
    groups:
      # Group all development dependencies together
      dev-dependencies:
        dependency-type: 'development'

      # Group production dependencies together
      production-dependencies:
        dependency-type: 'production'

  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'weekly'
    open-pull-requests-limit: 10
    groups:
      github-actions:
        patterns:
          - '*'

```

--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------

```json
{
  "name": "MCP OpenAPI Schema Explorer",
  "dockerFile": "Dockerfile", // Updated path
  "features": {
    "ghcr.io/devcontainers/features/common-utils:2": {
      "username": "vscode"
    },
    "ghcr.io/guiyomh/features/just:0": {}
  },
  "remoteUser": "vscode",
  "postCreateCommand": "just install",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-azuretools.vscode-docker",
        "GitHub.vscode-github-actions",
        "saoudrizwan.claude-dev",
        "dbaeumer.vscode-eslint",
        "rvest.vs-code-prettier-eslint",
        "ms-vscode.vscode-typescript-next"
      ]
    }
  }
}

```

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

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "declaration": true,
    "sourceMap": true,
    "rootDir": ".",
    "lib": ["ES2020"],
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "types": ["jest", "node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "local-docs", "test"]
}

```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
/** @type {import('jest').Config} */
export default {
  testPathIgnorePatterns: ['/node_modules/', '/local-docs/', '/dist/'],
  preset: 'ts-jest',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
        tsconfig: 'tsconfig.test.json',
      },
    ],
  },
  setupFilesAfterEnv: ['./test/setup.ts'],
  reporters: [
    [
      'jest-silent-reporter',
      {
        useDots: true,
        showPaths: true,
        showInlineStatus: true,
        showWarnings: true,
      },
    ],
  ],
  verbose: true,
};

```

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

```dockerfile
# Stage 1: Builder
FROM node:22-alpine AS builder

WORKDIR /app

# Copy necessary files for installation and build
COPY package.json package-lock.json ./
COPY tsconfig.json ./
COPY src ./src

# Install all dependencies (including devDependencies needed for build)
RUN npm ci

# Build the project
RUN npm run build

# Stage 2: Release
FROM node:22-alpine AS release

WORKDIR /app

# Copy only necessary files from the builder stage
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/dist ./dist

# Install only production dependencies
RUN npm ci --omit=dev --ignore-scripts

# Set the entrypoint to run the compiled server
# Corrected path based on tsconfig.json ("rootDir": ".", "outDir": "dist")
ENTRYPOINT ["node", "dist/src/index.js"]

```

--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'node:fs/promises';
import path from 'path';

// Extend timeout for E2E tests
jest.setTimeout(30000);

// Clean up any previous test artifacts
beforeAll(async () => {
  // Create required directories if they don't exist
  const dirs = ['dist', 'dist/src', 'test/fixtures'];

  for (const dir of dirs) {
    try {
      await fs.mkdir(dir, { recursive: true });
    } catch (error) {
      // Ignore if directory already exists
      if ((error as { code?: string }).code !== 'EEXIST') {
        throw error;
      }
    }
  }

  // Verify sample OpenAPI spec exists
  const specPath = path.resolve(process.cwd(), 'test/fixtures/sample-api.json');
  try {
    await fs.access(specPath);
  } catch {
    throw new Error(`Sample OpenAPI spec not found at ${specPath}`);
  }
});

// Custom matchers could be added here if needed

```

--------------------------------------------------------------------------------
/test/fixtures/sample-v2-api.json:
--------------------------------------------------------------------------------

```json
{
  "swagger": "2.0",
  "info": {
    "title": "Simple Swagger 2.0 API",
    "version": "1.0.0",
    "description": "A simple API definition in Swagger 2.0 format for testing conversion."
  },
  "host": "localhost:3000",
  "basePath": "/v2",
  "schemes": ["http"],
  "paths": {
    "/ping": {
      "get": {
        "summary": "Check service health",
        "description": "Returns a simple pong message.",
        "produces": ["application/json"],
        "responses": {
          "200": {
            "description": "Successful response",
            "schema": {
              "$ref": "#/definitions/Pong"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Pong": {
      "type": "object",
      "properties": {
        "message": {
          "type": "string",
          "example": "pong"
        }
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Configuration management for the OpenAPI Explorer MCP server
 */

import { OutputFormat } from './services/formatters.js';

/** Server configuration */
export interface ServerConfig {
  /** Path to OpenAPI specification file */
  specPath: string;
  /** Output format for responses */
  outputFormat: OutputFormat;
}

/** Load server configuration from command line arguments */
export function loadConfig(specPath?: string, options?: { outputFormat?: string }): ServerConfig {
  if (!specPath) {
    throw new Error(
      'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec> [--output-format json|yaml]'
    );
  }

  const format = options?.outputFormat || 'json';
  if (format !== 'json' && format !== 'yaml' && format !== 'json-minified') {
    throw new Error('Invalid output format. Supported formats: json, yaml, json-minified');
  }

  return {
    specPath,
    // Cast is safe here due to the validation above
    outputFormat: format as OutputFormat,
  };
}

```

--------------------------------------------------------------------------------
/test/fixtures/multi-component-types.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.0.0",
  "info": {
    "title": "Multi Component Type Test API",
    "version": "1.0.0"
  },
  "paths": {
    "/ping": {
      "get": {
        "summary": "Ping",
        "operationId": "ping",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pong"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/TraceId"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Pong": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          }
        }
      }
    },
    "parameters": {
      "TraceId": {
        "name": "X-Trace-ID",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/test/fixtures/paths-test.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Path Testing API",
    "version": "1.0.0"
  },
  "paths": {
    "/project/tasks/{taskId}": {
      "get": {
        "summary": "Get task details",
        "parameters": [
          {
            "name": "taskId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Task details"
          }
        }
      }
    },
    "/article/{articleId}/comment/{commentId}": {
      "get": {
        "summary": "Get comment on article",
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment details"
          }
        }
      }
    },
    "/sub/sub/sub/sub/folded/entrypoint": {
      "post": {
        "summary": "Deeply nested endpoint",
        "responses": {
          "201": {
            "description": "Created"
          }
        }
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/test/__tests__/unit/config.test.ts:
--------------------------------------------------------------------------------

```typescript
import { loadConfig } from '../../../src/config.js';

describe('Config', () => {
  describe('loadConfig', () => {
    it('returns valid configuration with default format when only path is provided', () => {
      const config = loadConfig('/path/to/spec.json');
      expect(config).toEqual({
        specPath: '/path/to/spec.json',
        outputFormat: 'json',
      });
    });

    it('returns valid configuration when path and format are provided', () => {
      const config = loadConfig('/path/to/spec.json', { outputFormat: 'yaml' });
      expect(config).toEqual({
        specPath: '/path/to/spec.json',
        outputFormat: 'yaml',
      });
    });

    it('throws error when invalid format is provided', () => {
      expect(() => loadConfig('/path/to/spec.json', { outputFormat: 'invalid' })).toThrow(
        'Invalid output format. Supported formats: json, yaml'
      );
    });

    it('throws error when path is not provided', () => {
      expect(() => loadConfig()).toThrow(
        'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec>'
      );
    });

    it('throws error when path is empty string', () => {
      expect(() => loadConfig('')).toThrow(
        'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec>'
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/test/utils/test-types.ts:
--------------------------------------------------------------------------------

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

export interface EndpointSuccessResponse {
  method: string;
  path: string;
  parameters?: OpenAPIV3.ParameterObject[];
  requestBody?: OpenAPIV3.RequestBodyObject;
  responses: { [key: string]: OpenAPIV3.ResponseObject };
}

export interface EndpointErrorResponse {
  method: string;
  path: string;
  error: string;
}

export type EndpointResponse = EndpointSuccessResponse | EndpointErrorResponse;

export function isEndpointErrorResponse(obj: unknown): obj is EndpointErrorResponse {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof (obj as EndpointErrorResponse).error === 'string'
  );
}

export interface ResourceContent {
  uri: string;
  mimeType: string;
  text: string;
}

export interface ResourceResponse {
  contents: ResourceContent[];
}

// Types for Schema Resource E2E tests
export type SchemaSuccessResponse = OpenAPIV3.SchemaObject; // Use type alias

export interface SchemaErrorResponse {
  name: string;
  error: string;
}

export type SchemaResponse = SchemaSuccessResponse | SchemaErrorResponse;

export function isSchemaErrorResponse(obj: unknown): obj is SchemaErrorResponse {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof (obj as SchemaErrorResponse).name === 'string' && // Check for name property
    typeof (obj as SchemaErrorResponse).error === 'string'
  );
}

```

--------------------------------------------------------------------------------
/test/fixtures/sample-api.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Sample API",
    "version": "1.0.0"
  },
  "paths": {
    "/users": {
      "get": {
        "summary": "List users",
        "operationId": "listUsers",
        "responses": {
          "200": {
            "description": "List of users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserList"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "required": ["id", "email"],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["active", "inactive"]
          }
        }
      },
      "UserList": {
        "type": "object",
        "required": ["users"],
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/User"
            }
          },
          "total": {
            "type": "integer",
            "format": "int32"
          }
        }
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/services/formatters.ts:
--------------------------------------------------------------------------------

```typescript
import { dump as yamlDump } from 'js-yaml';

/**
 * Supported output formats
 */
export type OutputFormat = 'json' | 'yaml' | 'json-minified';

/**
 * Interface for formatters that handle different output formats
 */
export interface IFormatter {
  format(data: unknown): string;
  getMimeType(): string;
}

/**
 * JSON formatter with pretty printing
 */
export class JsonFormatter implements IFormatter {
  format(data: unknown): string {
    return JSON.stringify(data, null, 2);
  }

  getMimeType(): string {
    return 'application/json';
  }
}

/**
 * Formats data as minified JSON.
 */
export class MinifiedJsonFormatter implements IFormatter {
  format(data: unknown): string {
    return JSON.stringify(data);
  }

  getMimeType(): string {
    return 'application/json';
  }
}

/**
 * YAML formatter using js-yaml library
 */
export class YamlFormatter implements IFormatter {
  format(data: unknown): string {
    return yamlDump(data, {
      indent: 2,
      lineWidth: -1, // Don't wrap long lines
      noRefs: true, // Don't use references
    });
  }

  getMimeType(): string {
    return 'text/yaml';
  }
}

/**
 * Creates a formatter instance based on format name
 */
export function createFormatter(format: OutputFormat): IFormatter {
  switch (format) {
    case 'json':
      return new JsonFormatter();
    case 'yaml':
      return new YamlFormatter();
    case 'json-minified':
      return new MinifiedJsonFormatter();
  }
}

```

--------------------------------------------------------------------------------
/memory-bank/projectbrief.md:
--------------------------------------------------------------------------------

```markdown
# OpenAPI Schema Explorer MCP Server

## Project Overview

Building an MCP server that allows exploration of OpenAPI specification files in a selective, token-efficient manner.

## Core Requirements (✓)

1. Allow loading and exploring OpenAPI spec files without consuming excessive LLM tokens
   - Token-efficient plain text listings
   - JSON format for detailed views
   - Error handling without excessive details
2. Expose key parts of OpenAPI specs through MCP resources
   - Endpoint details with full operation info
   - Multiple values support for batch operations
   - Resource completion support (✓)
3. Support local OpenAPI specification files
   - OpenAPI v3.0 support
   - Local file loading
   - Error handling for invalid specs
4. Provide test coverage with Jest
   - Full unit test coverage
   - E2E test coverage
   - Type-safe test implementation

## Future Extensions (Out of Scope)

- Remote OpenAPI specs
- Different specification formats
- Search functionality

## Technical Constraints (✓)

- Built with TypeScript MCP SDK
- Published to npm
- Comprehensive test coverage
- Optimized for testability and extensibility

## Project Boundaries

- Initial focus on local OpenAPI spec files only
- Focus on most important parts: endpoints and type definitions
- Real-time spec updates are out of scope (server restart required for updates)

## Next Optimizations

- YAML output format for improved token efficiency
- $ref resolution using URI links
- Parameter validation implementation
- Enhanced documentation support

```

--------------------------------------------------------------------------------
/test/utils/mcp-test-helpers.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// import path from 'path';

// Export the interface
export interface McpTestContext {
  client: Client;
  transport: StdioClientTransport;
  cleanup: () => Promise<void>;
}

interface StartServerOptions {
  outputFormat?: 'json' | 'yaml' | 'json-minified';
}

/**
 * Start MCP server with test configuration
 */
export async function startMcpServer(
  specPath: string,
  options: StartServerOptions = {}
): Promise<McpTestContext> {
  let transport: StdioClientTransport | undefined;
  let client: Client | undefined;

  try {
    // Initialize transport with spec path as argument
    transport = new StdioClientTransport({
      command: 'node',
      args: [
        'dist/src/index.js',
        // path.resolve(specPath),
        specPath,
        ...(options.outputFormat ? ['--output-format', options.outputFormat] : []),
      ],
      stderr: 'inherit', // Pass through server errors normally - they're part of E2E testing
    });

    // Initialize client
    client = new Client({
      name: 'test-client',
      version: '1.0.0',
    });

    await client.connect(transport);

    // Create cleanup function
    const cleanup = async (): Promise<void> => {
      if (transport) {
        await transport.close();
      }
    };

    return {
      client,
      transport,
      cleanup,
    };
  } catch (error) {
    // Clean up on error
    if (transport) {
      await transport.close();
    }
    throw error;
  }
}

```

--------------------------------------------------------------------------------
/scripts/generate-version.js:
--------------------------------------------------------------------------------

```javascript
// scripts/generate-version.js
// Purpose: Writes the release version provided by semantic-release to src/version.ts
// Called by: @semantic-release/exec during the 'prepare' step

import fs from 'fs'; // Use import
import path from 'path'; // Use import
import { fileURLToPath } from 'url'; // Needed to convert import.meta.url

// Get version from the command line argument passed by semantic-release exec
// process is globally available in ESM
const version = process.argv[2];

if (!version) {
  console.error('Error: No version argument provided to generate-version.js!');
  process.exit(1);
}

// Basic check for semantic version format (adjust regex if needed)
if (!/^\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/.test(version)) {
  console.error(`Error: Invalid version format received: "${version}"`);
  process.exit(1);
}

const content = `// Auto-generated by scripts/generate-version.js during semantic-release prepare step
// Do not edit this file manually.

export const VERSION = '${version}';
`;

// Derive the directory path in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Construct the absolute path to src/version.ts
const filePath = path.join(__dirname, '..', 'src', 'version.ts');
const fileDir = path.dirname(filePath);

try {
  // Ensure the src directory exists (though it should)
  if (!fs.existsSync(fileDir)) {
    fs.mkdirSync(fileDir, { recursive: true });
  }
  // Write the version file
  fs.writeFileSync(filePath, content, { encoding: 'utf-8' });
  console.log(`Successfully wrote version ${version} to ${filePath}`);
} catch (error) {
  console.error(`Error writing version file to ${filePath}:`, error);
  process.exit(1);
}

```

--------------------------------------------------------------------------------
/test/utils/console-helpers.ts:
--------------------------------------------------------------------------------

```typescript
import { jest } from '@jest/globals';

// Define a type for the console.error function signature using unknown
type ConsoleErrorType = (message?: unknown, ...optionalParams: unknown[]) => void;

/**
 * Temporarily suppresses console.error messages that match the expected pattern
 * during the execution of a provided function (typically an async test operation).
 * Unexpected console.error messages will still be logged.
 *
 * @param expectedMessage The exact string or a RegExp to match against the console.error message.
 * @param fnToRun The async function to execute while suppression is active.
 * @returns The result of the fnToRun function.
 */
export async function suppressExpectedConsoleError<T>(
  expectedMessage: string | RegExp,
  fnToRun: () => T | Promise<T>
): Promise<T> {
  // Use the defined type for the original function
  const originalConsoleError: ConsoleErrorType = console.error;

  // Use unknown in the mock implementation signature
  const consoleErrorSpy = jest
    .spyOn(console, 'error')
    .mockImplementation((message?: unknown, ...args: unknown[]) => {
      const messageStr = String(message); // String conversion handles unknown
      const shouldSuppress =
        expectedMessage instanceof RegExp
          ? expectedMessage.test(messageStr)
          : messageStr === expectedMessage;

      if (!shouldSuppress) {
        // Call the original implementation for unexpected errors
        // We still need to handle the potential type mismatch for the spread
        // Using Function.prototype.apply is a safer way to call with dynamic args
        Function.prototype.apply.call(originalConsoleError, console, [message, ...args]);
      }
      // If it matches, do nothing (suppress)
    });

  try {
    // Execute the function that is expected to trigger the error log
    return await fnToRun();
  } finally {
    // Restore the original console.error
    consoleErrorSpy.mockRestore();
  }
}

```

--------------------------------------------------------------------------------
/memory-bank/productContext.md:
--------------------------------------------------------------------------------

```markdown
# Product Context

## Problem Statement

When working with large OpenAPI specifications, loading the entire spec into an LLM's context:

1. Consumes excessive tokens due to fully resolved references
2. May confuse the LLM with too much information
3. Makes it difficult to focus on specific parts of the API
4. Duplicates schema information across multiple endpoints

## Solution

An MCP server that:

1. Loads OpenAPI v3.0 and Swagger v2.0 specs from local files or remote URLs.
2. Automatically converts Swagger v2.0 specs to OpenAPI v3.0.
3. Transforms internal references (`#/components/...`) to token-efficient MCP URIs.
4. Provides selective access to specific parts of the spec via MCP resources.
5. Returns information in token-efficient formats (text lists, JSON/YAML details).
6. Makes it easy for LLMs to explore API structures without loading the entire spec.

## User Experience Goals

1. Easy installation/usage via npm (`npx`) or Docker.
2. Simple configuration via a single command-line argument (path or URL).
3. Intuitive resource URIs for exploring API parts.
4. Clear and consistent response formats.

## Usage Workflow

1. User configures the server in their MCP client using either:
   - `npx mcp-openapi-schema-explorer <path-or-url-to-spec> [options]` (Recommended for most users)
   - `docker run kadykov/mcp-openapi-schema-explorer:latest <path-or-url-to-spec> [options]` (Requires Docker, local files need volume mounting)
   - Global installation (`npm i -g ...` then `mcp-openapi-schema-explorer ...`) (Less common)
2. Server loads the spec (from file or URL), converts v2.0 to v3.0 if necessary, and transforms internal references to MCP URIs.
3. LLM explores API structure through exposed resources:
   - List paths, components, methods.
   - View details for info, operations, components, etc.
   - Follow transformed reference URIs (`openapi://components/...`) to view component details without loading the whole spec initially.
4. Server restarts required if the source specification file/URL content changes.

```

--------------------------------------------------------------------------------
/src/rendering/types.ts:
--------------------------------------------------------------------------------

```typescript
import { IFormatter } from '../services/formatters';
// We don't need ResourceContents/ResourceContent here anymore

/**
 * Intermediate result structure returned by render methods.
 * Contains the core data needed to build the final ResourceContent.
 */
export interface RenderResultItem {
  /** The raw data object to be formatted. */
  data: unknown;
  /** The suffix to append to the base URI (e.g., 'info', 'paths/users', 'components/schemas/User'). */
  uriSuffix: string;
  /** Optional flag indicating an error for this specific item. */
  isError?: boolean;
  /** Optional error message if isError is true. */
  errorText?: string;
  /** Optional flag to indicate this should be rendered as a list (text/plain). */
  renderAsList?: boolean;
}

/**
 * Context required for rendering OpenAPI specification objects.
 */
export interface RenderContext {
  /** Formatter instance for handling output (JSON/YAML). */
  formatter: IFormatter;
  /** Base URI for generating resource links (e.g., "openapi://"). */
  baseUri: string;
}

/**
 * Represents an OpenAPI specification object that can be rendered
 * in different formats (list or detail).
 */
export interface RenderableSpecObject {
  /**
   * Generates data for a token-efficient list representation.
   * @param context - The rendering context.
   * @returns An array of RenderResultItem.
   */
  renderList(context: RenderContext): RenderResultItem[];

  /**
   * Generates data for a detailed representation.
   * @param context - The rendering context.
   * @returns An array of RenderResultItem.
   */
  renderDetail(context: RenderContext): RenderResultItem[];
}

/**
 * Type guard to check if an object implements RenderableSpecObject.
 * @param obj - The object to check.
 * @returns True if the object implements RenderableSpecObject, false otherwise.
 */
export function isRenderableSpecObject(obj: unknown): obj is RenderableSpecObject {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof (obj as RenderableSpecObject).renderList === 'function' &&
    typeof (obj as RenderableSpecObject).renderDetail === 'function'
  );
}

```

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

```json
{
  "name": "mcp-openapi-schema-explorer",
  "version": "1.3.0",
  "description": "MCP OpenAPI schema explorer",
  "type": "module",
  "main": "dist/src/index.js",
  "types": "dist/src/index.d.ts",
  "bin": {
    "mcp-openapi-schema-explorer": "dist/src/index.js"
  },
  "scripts": {
    "build": "rm -rf dist && mkdir -p dist && npx tsc && chmod +x dist/src/index.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint . --ext .ts",
    "lint:fix": "eslint . --ext .ts --fix",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
    "type-check": "tsc --noEmit",
    "prepare": "husky",
    "prepublishOnly": "npm run build"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kadykov/mcp-openapi-schema-explorer.git"
  },
  "keywords": [
    "mcp",
    "openapi"
  ],
  "author": "",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/kadykov/mcp-openapi-schema-explorer/issues"
  },
  "homepage": "https://github.com/kadykov/mcp-openapi-schema-explorer#readme",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.10.1",
    "js-yaml": "^4.1.0",
    "openapi-types": "^12.1.3",
    "swagger2openapi": "7.0.8",
    "zod": "^4.0.5"
  },
  "devDependencies": {
    "@codedependant/semantic-release-docker": "^5.1.0",
    "@eslint/js": "^9.24.0",
    "@semantic-release/changelog": "^6.0.3",
    "@semantic-release/commit-analyzer": "^13.0.1",
    "@semantic-release/exec": "^7.0.3",
    "@semantic-release/git": "^10.0.1",
    "@semantic-release/github": "^12.0.0",
    "@semantic-release/npm": "^13.1.1",
    "@semantic-release/release-notes-generator": "^14.0.3",
    "@types/jest": "^30.0.0",
    "@types/js-yaml": "^4.0.9",
    "@types/node": "^24.0.3",
    "@types/node-fetch": "^2.6.12",
    "@types/swagger2openapi": "^7.0.4",
    "@typescript-eslint/eslint-plugin": "^8.29.0",
    "@typescript-eslint/parser": "^8.29.0",
    "axios": "^1.8.4",
    "eslint": "^9.24.0",
    "eslint-plugin-security": "^3.0.1",
    "globals": "^16.0.0",
    "husky": "^9.1.7",
    "jest": "^30.0.2",
    "jest-silent-reporter": "^0.6.0",
    "license-checker": "^25.0.1",
    "msw": "^2.7.4",
    "openapi-typescript": "^7.6.1",
    "prettier": "^3.5.3",
    "semantic-release": "^25.0.1",
    "ts-jest": "^29.3.1",
    "typescript": "^5.8.3"
  }
}

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import eslintJs from '@eslint/js';
import globals from 'globals';
import security from 'eslint-plugin-security';

export default [
  {
    ignores: ['dist/**', 'node_modules/**', 'local-docs/**'],
  },
  eslintJs.configs.recommended,
  {
    files: ['src/**/*.ts'],
    languageOptions: {
      parser: tsparser,
      parserOptions: {
        project: './tsconfig.json',
        ecmaVersion: 2020,
        sourceType: 'module',
      },
      globals: {
        ...globals.node,
      },
    },
    plugins: {
      '@typescript-eslint': tseslint,
      security: security,
    },
    rules: {
      ...tseslint.configs['recommended'].rules,
      ...tseslint.configs['recommended-requiring-type-checking'].rules,
      '@typescript-eslint/explicit-function-return-type': 'error',
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/naming-convention': [
        'error',
        {
          selector: 'interface',
          format: ['PascalCase'],
        },
      ],
      'no-console': ['error', { allow: ['warn', 'error'] }],
      ...security.configs.recommended.rules,
    },
  },
  {
    files: ['test/**/*.ts'],
    languageOptions: {
      parser: tsparser,
      parserOptions: {
        project: './tsconfig.test.json',
        ecmaVersion: 2020,
        sourceType: 'module',
      },
      globals: {
        ...globals.node,
        ...globals.jest,
      },
    },
    plugins: {
      '@typescript-eslint': tseslint,
      security: security,
    },
    rules: {
      ...tseslint.configs['recommended'].rules,
      ...tseslint.configs['recommended-requiring-type-checking'].rules,
      '@typescript-eslint/explicit-function-return-type': 'error',
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      'no-console': 'off', // Allow console in tests
    },
  },
  // Configuration for scripts (like generate-version.js)
  {
    files: ['scripts/**/*.js'],
    languageOptions: {
      globals: {
        ...globals.node, // Enable Node.js global variables
      },
      ecmaVersion: 2022, // Use a recent version supporting top-level await etc.
      sourceType: 'module', // Treat .js files in scripts/ as ES Modules
    },
    rules: {
      // Add any specific rules for scripts if needed, e.g., allow console
      'no-console': 'off',
    },
  },
];

```

--------------------------------------------------------------------------------
/src/services/spec-loader.ts:
--------------------------------------------------------------------------------

```typescript
import * as swagger2openapi from 'swagger2openapi';
import { OpenAPI } from 'openapi-types';
import { ReferenceTransformService, TransformContext } from './reference-transform.js';

/**
 * Service for loading and transforming OpenAPI specifications
 */
export class SpecLoaderService {
  private specData: OpenAPI.Document | null = null;

  constructor(
    private specPath: string,
    private referenceTransform: ReferenceTransformService
  ) {}

  /**
   * Load, potentially convert (from v2), and parse the OpenAPI specification.
   */
  async loadSpec(): Promise<OpenAPI.Document> {
    const options = {
      patch: true, // Fix minor errors in the spec
      warnOnly: true, // Add warnings for non-patchable errors instead of throwing
      origin: this.specPath, // Helps with resolving relative references if needed
      source: this.specPath,
    };

    try {
      let result;
      // Check if specPath is a URL
      if (this.specPath.startsWith('http://') || this.specPath.startsWith('https://')) {
        result = await swagger2openapi.convertUrl(this.specPath, options);
      } else {
        result = await swagger2openapi.convertFile(this.specPath, options);
      }

      // swagger2openapi returns the result in result.openapi
      if (!result || !result.openapi) {
        throw new Error('Conversion or parsing failed to produce an OpenAPI document.');
      }

      // TODO: Check result.options?.warnings for potential issues?

      this.specData = result.openapi as OpenAPI.Document; // Assuming result.openapi is compatible
      return this.specData;
    } catch (error) {
      // Improve error message clarity
      let message = `Failed to load/convert OpenAPI spec from ${this.specPath}: `;
      if (error instanceof Error) {
        message += error.message;
        // Include stack trace if available and helpful?
        // console.error(error.stack);
      } else {
        message += String(error);
      }
      throw new Error(message);
    }
  }

  /**
   * Get the loaded specification
   */
  async getSpec(): Promise<OpenAPI.Document> {
    if (!this.specData) {
      await this.loadSpec();
    }
    return this.specData!;
  }

  /**
   * Get transformed specification with MCP resource references
   */
  async getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document> {
    const spec = await this.getSpec();
    return this.referenceTransform.transformDocument(spec, context);
  }
}

/**
 * Create and initialize a new SpecLoaderService instance
 */
export async function createSpecLoader(
  specPath: string,
  referenceTransform: ReferenceTransformService
): Promise<SpecLoaderService> {
  const loader = new SpecLoaderService(specPath, referenceTransform);
  await loader.loadSpec();
  return loader;
}

```

--------------------------------------------------------------------------------
/test/__tests__/unit/services/formatters.test.ts:
--------------------------------------------------------------------------------

```typescript
import { dump as yamlDump } from 'js-yaml';
import {
  JsonFormatter,
  YamlFormatter,
  MinifiedJsonFormatter,
  createFormatter,
} from '../../../../src/services/formatters.js';

describe('Formatters', () => {
  const testData = {
    method: 'GET',
    path: '/test',
    summary: 'Test endpoint',
    parameters: [
      {
        name: 'id',
        in: 'path',
        required: true,
        schema: { type: 'string' },
      },
    ],
  };

  describe('JsonFormatter', () => {
    const formatter = new JsonFormatter();

    it('should format data as JSON with proper indentation', () => {
      const result = formatter.format(testData);
      expect(result).toBe(JSON.stringify(testData, null, 2));
    });

    it('should return application/json mime type', () => {
      expect(formatter.getMimeType()).toBe('application/json');
    });

    it('should handle empty objects', () => {
      expect(formatter.format({})).toBe('{}');
    });

    it('should handle null values', () => {
      expect(formatter.format(null)).toBe('null');
    });
  });

  describe('YamlFormatter', () => {
    const formatter = new YamlFormatter();

    it('should format data as YAML', () => {
      const result = formatter.format(testData);
      expect(result).toBe(
        yamlDump(testData, {
          indent: 2,
          lineWidth: -1,
          noRefs: true,
        })
      );
    });

    it('should return text/yaml mime type', () => {
      expect(formatter.getMimeType()).toBe('text/yaml');
    });

    it('should handle empty objects', () => {
      expect(formatter.format({})).toBe('{}\n');
    });

    it('should handle null values', () => {
      expect(formatter.format(null)).toBe('null\n');
    });
  });

  describe('MinifiedJsonFormatter', () => {
    const formatter = new MinifiedJsonFormatter();

    it('should format data as minified JSON', () => {
      const result = formatter.format(testData);
      expect(result).toBe(JSON.stringify(testData));
    });

    it('should return application/json mime type', () => {
      expect(formatter.getMimeType()).toBe('application/json');
    });

    it('should handle empty objects', () => {
      expect(formatter.format({})).toBe('{}');
    });

    it('should handle null values', () => {
      expect(formatter.format(null)).toBe('null');
    });
  });

  describe('createFormatter', () => {
    it('should create JsonFormatter for json format', () => {
      const formatter = createFormatter('json');
      expect(formatter).toBeInstanceOf(JsonFormatter);
    });

    it('should create YamlFormatter for yaml format', () => {
      const formatter = createFormatter('yaml');
      expect(formatter).toBeInstanceOf(YamlFormatter);
    });

    it('should create MinifiedJsonFormatter for json-minified format', () => {
      const formatter = createFormatter('json-minified');
      expect(formatter).toBeInstanceOf(MinifiedJsonFormatter);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/handlers/component-map-handler.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ReadResourceTemplateCallback,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { SpecLoaderService } from '../types.js';
import { IFormatter } from '../services/formatters.js';
import {
  RenderableComponentMap,
  ComponentType,
  VALID_COMPONENT_TYPES,
} from '../rendering/components.js';
import { RenderContext, RenderResultItem } from '../rendering/types.js';
import { createErrorResult } from '../rendering/utils.js';
// Import shared handler utils
import {
  formatResults,
  isOpenAPIV3,
  FormattedResultItem,
  getValidatedComponentMap, // Import the helper
} from './handler-utils.js'; // Already has .js

const BASE_URI = 'openapi://';

// Removed duplicated FormattedResultItem type - now imported from handler-utils
// Removed duplicated formatResults function - now imported from handler-utils
// Removed duplicated isOpenAPIV3 function - now imported from handler-utils

/**
 * Handles requests for listing component names of a specific type.
 * Corresponds to the `openapi://components/{type}` template.
 */
export class ComponentMapHandler {
  constructor(
    private specLoader: SpecLoaderService,
    private formatter: IFormatter // Needed for context
  ) {}

  getTemplate(): ResourceTemplate {
    // TODO: Add completion logic if needed
    return new ResourceTemplate(`${BASE_URI}components/{type}`, {
      list: undefined,
      complete: undefined,
    });
  }

  handleRequest: ReadResourceTemplateCallback = async (
    uri: URL,
    variables: Variables
  ): Promise<{ contents: FormattedResultItem[] }> => {
    const type = variables.type as string;
    const mapUriSuffix = `components/${type}`;
    const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
    let resultItems: RenderResultItem[];

    try {
      if (!VALID_COMPONENT_TYPES.includes(type as ComponentType)) {
        throw new Error(`Invalid component type: ${type}`);
      }
      const componentType = type as ComponentType;

      const spec = await this.specLoader.getTransformedSpec({
        resourceType: 'schema', // Use 'schema' for now
        format: 'openapi',
      });

      // Use imported type guard
      if (!isOpenAPIV3(spec)) {
        throw new Error('Only OpenAPI v3 specifications are supported');
      }

      // --- Use helper to get validated component map ---
      const componentMapObj = getValidatedComponentMap(spec, componentType);

      // Instantiate RenderableComponentMap with the validated map
      const renderableMap = new RenderableComponentMap(
        componentMapObj, // componentMapObj retrieved safely via helper
        componentType,
        mapUriSuffix
      );
      resultItems = renderableMap.renderList(context);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error handling request ${uri.href}: ${message}`);
      resultItems = createErrorResult(mapUriSuffix, message);
    }

    // Use imported formatResults
    const contents = formatResults(context, resultItems);
    return { contents };
  };
}

```

--------------------------------------------------------------------------------
/src/rendering/paths.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js

/**
 * Wraps an OpenAPIV3.PathsObject to make it renderable.
 * Handles rendering the list of all paths and methods.
 */
export class RenderablePaths implements RenderableSpecObject {
  constructor(private paths: OpenAPIV3.PathsObject | undefined) {}

  /**
   * Renders a token-efficient list of all paths and their methods.
   * Corresponds to the `openapi://paths` URI.
   */
  renderList(context: RenderContext): RenderResultItem[] {
    if (!this.paths || Object.keys(this.paths).length === 0) {
      return [
        {
          uriSuffix: 'paths',
          data: 'No paths found in the specification.',
          renderAsList: true,
        },
      ];
    }

    // Generate hint first and prepend "Hint: "
    const hintText = `Use '${context.baseUri}paths/{encoded_path}' to list methods for a specific path, or '${context.baseUri}paths/{encoded_path}/{method}' to view details for a specific operation.`;
    let outputLines: string[] = [`Hint: ${hintText}`, '']; // Start with hint and a blank line

    const pathEntries = Object.entries(this.paths).sort(([pathA], [pathB]) =>
      pathA.localeCompare(pathB)
    );

    for (const [path, pathItem] of pathEntries) {
      if (!pathItem) continue;

      // Create a list of valid, sorted, uppercase methods for the current path
      const methods: string[] = [];
      for (const key in pathItem) {
        const lowerKey = key.toLowerCase();
        if (Object.values(OpenAPIV3.HttpMethods).includes(lowerKey as OpenAPIV3.HttpMethods)) {
          // Check if it's a valid operation object before adding the method
          const operation = pathItem[key as keyof OpenAPIV3.PathItemObject];
          if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
            methods.push(lowerKey.toUpperCase());
          }
        }
      }
      methods.sort(); // Sort methods alphabetically

      // Format the line: METHODS /path
      const methodsString = methods.length > 0 ? methods.join(' ') : '(No methods)';
      outputLines.push(`${methodsString} ${path}`);
    }

    return [
      {
        uriSuffix: 'paths',
        data: outputLines.join('\n'), // Join lines into a single string
        renderAsList: true, // This result is always plain text
      },
    ];
  }

  /**
   * Renders the detail view. For the Paths object level, this isn't
   * typically used directly. Details are requested per path or operation.
   */
  renderDetail(context: RenderContext): RenderResultItem[] {
    // Delegate to renderList as the primary view for the collection of paths
    return this.renderList(context);
  }

  /**
   * Gets the PathItemObject for a specific path.
   * @param path - The decoded path string.
   * @returns The PathItemObject or undefined if not found.
   */
  getPathItem(path: string): OpenAPIV3.PathItemObject | undefined {
    // Use Map for safe access
    if (!this.paths) {
      return undefined;
    }
    const pathsMap = new Map(Object.entries(this.paths));
    return pathsMap.get(path); // Map.get returns ValueType | undefined
  }
}

```

--------------------------------------------------------------------------------
/src/services/reference-transform.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { buildComponentDetailUri } from '../utils/uri-builder.js'; // Added .js extension

export interface TransformContext {
  resourceType: 'endpoint' | 'schema';
  format: 'openapi' | 'asyncapi' | 'graphql';
  path?: string;
  method?: string;
}

export interface ReferenceObject {
  $ref: string;
}

export interface TransformedReference {
  $ref: string;
}

export interface ReferenceTransform<T> {
  transformRefs(document: T, context: TransformContext): T;
}

export class ReferenceTransformService {
  private transformers = new Map<string, ReferenceTransform<unknown>>();

  registerTransformer<T>(format: string, transformer: ReferenceTransform<T>): void {
    this.transformers.set(format, transformer as ReferenceTransform<unknown>);
  }

  transformDocument<T>(document: T, context: TransformContext): T {
    const transformer = this.transformers.get(context.format) as ReferenceTransform<T>;
    if (!transformer) {
      throw new Error(`No transformer registered for format: ${context.format}`);
    }
    return transformer.transformRefs(document, context);
  }
}

export class OpenAPITransformer implements ReferenceTransform<OpenAPIV3.Document> {
  // Handle nested objects recursively
  private transformObject(obj: unknown, _context: TransformContext): unknown {
    if (!obj || typeof obj !== 'object') {
      return obj;
    }

    // Handle arrays
    if (Array.isArray(obj)) {
      return obj.map(item => this.transformObject(item, _context));
    }

    // Handle references
    if (this.isReferenceObject(obj)) {
      return this.transformReference(obj.$ref);
    }

    // Recursively transform object properties
    const result: Record<string, unknown> = {};
    if (typeof obj === 'object') {
      for (const [key, value] of Object.entries(obj)) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          Object.defineProperty(result, key, {
            value: this.transformObject(value, _context),
            enumerable: true,
            writable: true,
            configurable: true,
          });
        }
      }
    }
    return result;
  }

  private isReferenceObject(obj: unknown): obj is ReferenceObject {
    return typeof obj === 'object' && obj !== null && '$ref' in obj;
  }

  private transformReference(ref: string): TransformedReference {
    // Handle only internal references for now
    if (!ref.startsWith('#/')) {
      return { $ref: ref }; // Keep external refs as-is
    }

    // Example ref: #/components/schemas/MySchema
    const parts = ref.split('/');
    // Check if it's an internal component reference
    if (parts[0] === '#' && parts[1] === 'components' && parts.length === 4) {
      const componentType = parts[2];
      const componentName = parts[3];

      // Use the centralized builder to create the correct URI
      const newUri = buildComponentDetailUri(componentType, componentName);
      return {
        $ref: newUri,
      };
    }

    // Keep other internal references (#/paths/...) and external references as-is
    return { $ref: ref };
  }

  transformRefs(document: OpenAPIV3.Document, context: TransformContext): OpenAPIV3.Document {
    const transformed = this.transformObject(document, context);
    return transformed as OpenAPIV3.Document;
  }
}

```

--------------------------------------------------------------------------------
/src/handlers/path-item-handler.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ReadResourceTemplateCallback,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { SpecLoaderService } from '../types.js';
import { IFormatter } from '../services/formatters.js';
import { RenderablePathItem } from '../rendering/path-item.js';
import { RenderContext, RenderResultItem } from '../rendering/types.js';
import { createErrorResult } from '../rendering/utils.js';
import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
// Import shared handler utils
import {
  formatResults,
  isOpenAPIV3,
  FormattedResultItem,
  getValidatedPathItem, // Import the helper
} from './handler-utils.js'; // Already has .js

const BASE_URI = 'openapi://';

// Removed duplicated FormattedResultItem type - now imported from handler-utils
// Removed duplicated formatResults function - now imported from handler-utils
// Removed duplicated isOpenAPIV3 function - now imported from handler-utils

/**
 * Handles requests for listing methods for a specific path.
 * Corresponds to the `openapi://paths/{path}` template.
 */
export class PathItemHandler {
  constructor(
    private specLoader: SpecLoaderService,
    private formatter: IFormatter // Although unused in list view, needed for context
  ) {}

  getTemplate(): ResourceTemplate {
    // TODO: Add completion logic if needed
    return new ResourceTemplate(`${BASE_URI}paths/{path}`, {
      list: undefined,
      complete: undefined,
    });
  }

  handleRequest: ReadResourceTemplateCallback = async (
    uri: URL,
    variables: Variables
  ): Promise<{ contents: FormattedResultItem[] }> => {
    const encodedPath = variables.path as string;
    // Decode the path received from the URI variable
    const decodedPath = decodeURIComponent(encodedPath || '');
    // Use the builder to create the suffix, which will re-encode the path correctly
    const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
    const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
    let resultItems: RenderResultItem[];

    try {
      const spec = await this.specLoader.getTransformedSpec({
        resourceType: 'schema', // Use 'schema' for now
        format: 'openapi',
      });

      // Use imported type guard
      if (!isOpenAPIV3(spec)) {
        throw new Error('Only OpenAPI v3 specifications are supported');
      }

      // --- Use helper to get validated path item ---
      const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
      const pathItemObj = getValidatedPathItem(spec, lookupPath);

      // Instantiate RenderablePathItem with the validated pathItemObj
      const renderablePathItem = new RenderablePathItem(
        pathItemObj, // pathItemObj retrieved safely via helper
        lookupPath, // Pass the raw, decoded path
        pathUriSuffix // Pass the correctly built suffix
      );
      resultItems = renderablePathItem.renderList(context);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error handling request ${uri.href}: ${message}`);
      resultItems = createErrorResult(pathUriSuffix, message);
    }

    // Use imported formatResults
    const contents = formatResults(context, resultItems);
    return { contents };
  };
}

```

--------------------------------------------------------------------------------
/test/fixtures/complex-endpoint.json:
--------------------------------------------------------------------------------

```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Complex Endpoint Test API",
    "version": "1.0.0"
  },
  "paths": {
    "/api/v1/organizations/{orgId}/projects/{projectId}/tasks": {
      "get": {
        "operationId": "getProjectTasks",
        "summary": "Get Tasks",
        "description": "Retrieve a list of tasks for a specific project.",
        "parameters": [
          {
            "name": "orgId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["active", "completed"]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["created", "updated", "priority"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of tasks",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskList"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProjectTask",
        "summary": "Create Task",
        "description": "Create a new task within a project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTaskRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Task created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Task": {
        "type": "object",
        "required": ["id", "title", "status"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["active", "completed"]
          },
          "priority": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          }
        }
      },
      "TaskList": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          },
          "totalCount": {
            "type": "integer"
          }
        }
      },
      "CreateTaskRequest": {
        "type": "object",
        "required": ["title"],
        "properties": {
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["active", "completed"],
            "default": "active"
          },
          "priority": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "default": 3
          }
        }
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/memory-bank/activeContext.md:
--------------------------------------------------------------------------------

```markdown
# Active Context

## Current Focus

Successfully upgraded @modelcontextprotocol/sdk from 1.10.1 to 1.11.0 and addressed breaking changes in test mocks.

## Implementation Status

- @modelcontextprotocol/sdk updated from 1.10.1 to 1.11.0
- Updated all test mocks to include new `RequestHandlerExtra` property (`requestId`).
- Corrected import path for `RequestId` to `@modelcontextprotocol/sdk/types.js`.
- Modified test files:
  - component-map-handler.test.ts
  - component-detail-handler.test.ts
  - operation-handler.test.ts
  - path-item-handler.test.ts
  - top-level-field-handler.test.ts
- All tests passing successfully
- Server now loads OpenAPI v3.0 and Swagger v2.0 specs from local files or remote URLs
- Swagger v2.0 specs are automatically converted to v3.0
- Internal references are transformed to MCP URIs
- Added `json-minified` output format option
- Server name is now dynamically set based on the loaded spec's `info.title`
- Automated versioning and release process implemented using `semantic-release`
- CI workflow adapted for Node 22, uses `just` for checks, and includes a `release` job
- Docker support added with automated Docker Hub publishing
- Dependencies correctly categorized
- Resource completion logic implemented
- Dynamic server name implemented
- Minified JSON output format added
- Remote spec loading and Swagger v2.0 conversion support added
- Core resource exploration functionality remains operational
- Unit tests updated for latest SDK version
- E2E tests cover all main functionality

## Recent Changes

### SDK Update to v1.11.0 & Test Fixes (✓)

1. **Dependency Update:**

   - Updated @modelcontextprotocol/sdk from 1.10.1 to 1.11.0 in `package.json`.
   - Identified breaking change in `RequestHandlerExtra` type requiring a new `requestId` property.

2. **Test Suite Updates:**
   - Added the `requestId` property to `mockExtra` objects in all handler unit tests:
     - `test/__tests__/unit/handlers/top-level-field-handler.test.ts`
     - `test/__tests__/unit/handlers/component-map-handler.test.ts`
     - `test/__tests__/unit/handlers/path-item-handler.test.ts`
     - `test/__tests__/unit/handlers/operation-handler.test.ts`
     - `test/__tests__/unit/handlers/component-detail-handler.test.ts`
   - Corrected the import path for `RequestId` to `import { RequestId } from '@modelcontextprotocol/sdk/types.js';` in these files. This resolved previous TypeScript import errors and an ESLint warning regarding unsafe assignment also disappeared.
   - Confirmed all test fixes by running `just build && just test` successfully.

## Next Actions

1. **Continue with Previous Plans:**

   - Complete README updates with release process details
   - Clean up any remaining TODOs in codebase
   - Address minor ESLint warnings

2. **Documentation:**

   - Document the SDK upgrade in CHANGELOG.md
   - Update dependencies section in relevant documentation

3. **Testing:**

   - Monitor for any new breaking changes in future SDK updates
   - Consider adding test utilities to simplify mock creation

4. **Code Cleanup:**
   - Refactor duplicated mock setup code in tests
   - Consider creating shared test fixtures for common mocks

## Future Considerations

1. **SDK Integration:**

   - Stay updated with MCP SDK releases
   - Plan for future breaking changes
   - Consider automated dependency update checks

2. **Testing Infrastructure:**

   - Improve test mock reusability
   - Add test coverage for edge cases
   - Consider adding integration tests

3. **Previous Future Considerations:**
   - Implement reference traversal/resolution service
   - Enhance support for all component types

```

--------------------------------------------------------------------------------
/src/handlers/top-level-field-handler.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ReadResourceTemplateCallback,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
// ResourceContents is the base type for a single item, not the array type needed here.
// We'll define the array structure inline based on TextResourceContentsSchema.

import { SpecLoaderService } from '../types.js';
import { IFormatter } from '../services/formatters.js';
import { RenderableDocument } from '../rendering/document.js';
import { RenderablePaths } from '../rendering/paths.js';
import { RenderableComponents } from '../rendering/components.js';
import { RenderContext, RenderResultItem } from '../rendering/types.js';
import { createErrorResult } from '../rendering/utils.js';
// Import shared handler utils
import { formatResults, isOpenAPIV3, FormattedResultItem } from './handler-utils.js'; // Already has .js

const BASE_URI = 'openapi://';

// Removed duplicated FormattedResultItem type - now imported from handler-utils
// Removed duplicated formatResults function - now imported from handler-utils

/**
 * Handles requests for top-level OpenAPI fields (info, servers, paths list, components list).
 * Corresponds to the `openapi://{field}` template.
 */
export class TopLevelFieldHandler {
  constructor(
    private specLoader: SpecLoaderService,
    private formatter: IFormatter
  ) {}

  getTemplate(): ResourceTemplate {
    // TODO: Add completion logic if needed
    return new ResourceTemplate(`${BASE_URI}{field}`, {
      list: undefined,
      complete: undefined,
    });
  }

  handleRequest: ReadResourceTemplateCallback = async (
    uri: URL,
    variables: Variables
    // matchedTemplate is not needed if we only handle one template
  ): Promise<{ contents: FormattedResultItem[] }> => {
    // Return type uses the defined array structure
    const field = variables.field as string;
    const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
    let resultItems: RenderResultItem[];

    try {
      const spec = await this.specLoader.getTransformedSpec({
        // Use 'schema' as placeholder resourceType for transformation context
        resourceType: 'schema',
        format: 'openapi',
      });

      // Use imported type guard
      if (!isOpenAPIV3(spec)) {
        throw new Error('Only OpenAPI v3 specifications are supported');
      }

      const renderableDoc = new RenderableDocument(spec);

      // Route based on the field name
      if (field === 'paths') {
        const pathsObj = renderableDoc.getPathsObject();
        resultItems = new RenderablePaths(pathsObj).renderList(context);
      } else if (field === 'components') {
        const componentsObj = renderableDoc.getComponentsObject();
        resultItems = new RenderableComponents(componentsObj).renderList(context);
      } else {
        // Handle other top-level fields (info, servers, tags, etc.)
        const fieldObject = renderableDoc.getTopLevelField(field);
        resultItems = renderableDoc.renderTopLevelFieldDetail(context, fieldObject, field);
      }
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error handling request ${uri.href}: ${message}`);
      resultItems = createErrorResult(field, message); // Use field as uriSuffix for error
    }

    // Format results into the final structure
    const contents: FormattedResultItem[] = formatResults(context, resultItems);
    // Return the object with the correctly typed contents array
    // Use imported formatResults
    return { contents };
  };

  // Removed duplicated isOpenAPIV3 type guard - now imported from handler-utils
} // Ensure class closing brace is present

```

--------------------------------------------------------------------------------
/test/__tests__/unit/rendering/paths.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderablePaths } from '../../../../src/rendering/paths';
import { RenderContext } from '../../../../src/rendering/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';

// Mock Formatter & Context
const mockFormatter: IFormatter = new JsonFormatter();
const mockContext: RenderContext = {
  formatter: mockFormatter,
  baseUri: 'openapi://',
};

// Sample Paths Object Fixture
const samplePaths: OpenAPIV3.PathsObject = {
  '/users': {
    get: {
      summary: 'List Users',
      responses: { '200': { description: 'OK' } },
    },
    post: {
      summary: 'Create User',
      responses: { '201': { description: 'Created' } },
    },
  },
  '/users/{userId}': {
    get: {
      summary: 'Get User by ID',
      responses: { '200': { description: 'OK' } },
    },
    delete: {
      // No summary
      responses: { '204': { description: 'No Content' } },
    },
  },
  // Removed /ping path with custom operation to avoid type errors
};

const emptyPaths: OpenAPIV3.PathsObject = {};

describe('RenderablePaths', () => {
  describe('renderList', () => {
    it('should render a list of paths and methods correctly', () => {
      const renderablePaths = new RenderablePaths(samplePaths);
      const result = renderablePaths.renderList(mockContext);

      expect(result).toHaveLength(1);
      expect(result[0].uriSuffix).toBe('paths');
      expect(result[0].renderAsList).toBe(true);
      expect(result[0].isError).toBeUndefined();

      // Define expected output lines based on the new format
      const expectedLineUsers = 'GET POST /users'; // Methods sorted alphabetically and uppercased
      const expectedLineUserDetail = 'DELETE GET /users/{userId}'; // Methods sorted alphabetically and uppercased

      // Check essential parts instead of exact match
      expect(result[0].data).toContain('Hint:');
      expect(result[0].data).toContain('openapi://paths/{encoded_path}');
      expect(result[0].data).toContain('openapi://paths/{encoded_path}/{method}');
      expect(result[0].data).toContain(expectedLineUsers);
      expect(result[0].data).toContain(expectedLineUserDetail);
    });

    it('should handle empty paths object', () => {
      const renderablePaths = new RenderablePaths(emptyPaths);
      const result = renderablePaths.renderList(mockContext);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'paths',
        data: 'No paths found in the specification.',
        renderAsList: true,
      });
    });

    it('should handle undefined paths object', () => {
      const renderablePaths = new RenderablePaths(undefined);
      const result = renderablePaths.renderList(mockContext);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'paths',
        data: 'No paths found in the specification.',
        renderAsList: true,
      });
    });
  });

  describe('renderDetail', () => {
    it('should delegate to renderList', () => {
      const renderablePaths = new RenderablePaths(samplePaths);
      const listResult = renderablePaths.renderList(mockContext);
      const detailResult = renderablePaths.renderDetail(mockContext);
      // Check if the output is the same as renderList
      expect(detailResult).toEqual(listResult);
    });
  });

  describe('getPathItem', () => {
    it('should return the correct PathItemObject', () => {
      const renderablePaths = new RenderablePaths(samplePaths);
      expect(renderablePaths.getPathItem('/users')).toBe(samplePaths['/users']);
      expect(renderablePaths.getPathItem('/users/{userId}')).toBe(samplePaths['/users/{userId}']);
    });

    it('should return undefined for non-existent path', () => {
      const renderablePaths = new RenderablePaths(samplePaths);
      expect(renderablePaths.getPathItem('/nonexistent')).toBeUndefined();
    });

    it('should return undefined if paths object is undefined', () => {
      const renderablePaths = new RenderablePaths(undefined);
      expect(renderablePaths.getPathItem('/users')).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

permissions: # Add default permissions, release job will override if needed
  contents: read

on:
  push:
    branches: [main]
    tags:
      - 'v*'
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22 # Match Dockerfile
          cache: 'npm'

      - name: Setup Just
        uses: extractions/setup-just@v3

      - name: Install dependencies
        run: npm ci

      - name: Run all checks (format, lint, build, test)
        run: just all # Uses justfile for consistency

      - name: Upload coverage reports artifact
        uses: actions/upload-artifact@v5
        with:
          name: coverage-report-${{ github.run_id }} # Unique name per run
          path: coverage/
        if: always() # Upload even if previous steps fail

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
        # fail_ci_if_error: true # Optional: fail CI if upload fails

  security:
    runs-on: ubuntu-latest
    permissions:
      contents: read # Needed for checkout and CodeQL
      security-events: write # Needed for CodeQL alert uploads
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22 # Match Dockerfile and test job
          cache: 'npm'

      - name: Setup Just
        uses: extractions/setup-just@v3

      - name: Install dependencies
        run: npm ci

      - name: Run Security Checks (Audit, Licenses)
        run: just security # Uses justfile, includes npm audit and license-checker
        continue-on-error: true # Allow workflow to continue even if npm audit finds vulnerabilities

      # Static code analysis with CodeQL (Keep separate as it's not in justfile)
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v4
        # Auto-detect languages: javascript, typescript
        # queries: +security-extended # Optional: run more queries

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v4

  release:
    name: Release
    runs-on: ubuntu-latest
    needs: [test, security] # Run after test and security checks pass
    # Run only on pushes to main, not on tags (semantic-release creates tags)
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    permissions:
      contents: write # Allow tagging, committing package.json/changelog/version.ts
      issues: write # Allow commenting on issues/PRs
      pull-requests: write # Allow commenting on issues/PRs
      id-token: write # Needed for provenance publishing to npm (alternative to NPM_TOKEN)
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22 # Match Dockerfile and other jobs
          cache: 'npm'

      - name: Install all dependencies
        run: npm ci --include=dev

      # Docker setup steps (Still needed for the environment where the action runs)
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Semantic Release
        uses: cycjimmy/semantic-release-action@v5
        with:
          # Add the docker plugin to extra_plugins
          extra_plugins: |
            @semantic-release/changelog
            @semantic-release/exec
            @semantic-release/git
            @codedependant/semantic-release-docker
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} # Use dedicated release token if needed
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          # Docker login is handled by the login-action step above

```

--------------------------------------------------------------------------------
/src/handlers/component-detail-handler.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ReadResourceTemplateCallback,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { SpecLoaderService } from '../types.js';
import { IFormatter } from '../services/formatters.js';
import {
  RenderableComponentMap,
  ComponentType,
  VALID_COMPONENT_TYPES,
} from '../rendering/components.js';
import { RenderContext, RenderResultItem } from '../rendering/types.js';
import { createErrorResult } from '../rendering/utils.js';
// Import shared handler utils
import {
  formatResults,
  isOpenAPIV3,
  FormattedResultItem,
  getValidatedComponentMap, // Import helper
  getValidatedComponentDetails, // Import helper
} from './handler-utils.js'; // Already has .js

const BASE_URI = 'openapi://';

// Removed duplicated FormattedResultItem type - now imported from handler-utils
// Removed duplicated formatResults function - now imported from handler-utils
// Removed duplicated isOpenAPIV3 function - now imported from handler-utils

/**
 * Handles requests for specific component details.
 * Corresponds to the `openapi://components/{type}/{name*}` template.
 */
export class ComponentDetailHandler {
  constructor(
    private specLoader: SpecLoaderService,
    private formatter: IFormatter
  ) {}

  getTemplate(): ResourceTemplate {
    // TODO: Add completion logic if needed
    return new ResourceTemplate(`${BASE_URI}components/{type}/{name*}`, {
      list: undefined,
      complete: undefined,
    });
  }

  handleRequest: ReadResourceTemplateCallback = async (
    uri: URL,
    variables: Variables
  ): Promise<{ contents: FormattedResultItem[] }> => {
    const type = variables.type as string;
    // Correct variable access key: 'name', not 'name*'
    const nameVar = variables['name']; // Can be string or string[]
    const mapUriSuffix = `components/${type}`;
    const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
    let resultItems: RenderResultItem[];

    try {
      if (!VALID_COMPONENT_TYPES.includes(type as ComponentType)) {
        throw new Error(`Invalid component type: ${type}`);
      }
      const componentType = type as ComponentType;

      // Normalize names: Handle string for single value, array for multiple.
      let names: string[] = [];
      if (Array.isArray(nameVar)) {
        names = nameVar.map(n => String(n).trim()); // Ensure elements are strings
      } else if (typeof nameVar === 'string') {
        names = [nameVar.trim()]; // Treat as single item array
      }
      names = names.filter(n => n.length > 0); // Remove empty strings

      if (names.length === 0) {
        throw new Error('No valid component name specified.');
      }

      const spec = await this.specLoader.getTransformedSpec({
        resourceType: 'schema', // Use 'schema' for now
        format: 'openapi',
      });

      // Use imported type guard
      if (!isOpenAPIV3(spec)) {
        throw new Error('Only OpenAPI v3 specifications are supported');
      }

      // --- Use helper to get validated component map ---
      const componentMapObj = getValidatedComponentMap(spec, componentType);

      // --- Create Map and use helper to get validated component names/details ---
      // Create the Map from the validated object
      const detailsMap = new Map(Object.entries(componentMapObj));
      // Pass the Map to the helper
      const validDetails = getValidatedComponentDetails(detailsMap, names, componentType);
      const validNames = validDetails.map(detail => detail.name); // Extract names

      // Instantiate RenderableComponentMap with the validated map object
      const renderableMap = new RenderableComponentMap(
        componentMapObj, // componentMapObj retrieved safely via helper
        componentType,
        mapUriSuffix
      );
      // Pass the validated names to the rendering function
      resultItems = renderableMap.renderComponentDetail(context, validNames);
    } catch (error: unknown) {
      // Catch errors from helpers (e.g., type/name not found) or rendering
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error handling request ${uri.href}: ${message}`);
      // Create a single error item representing the overall request failure
      resultItems = createErrorResult(
        uri.href.substring(BASE_URI.length), // Use request URI suffix
        message
      );
    }

    // Use imported formatResults
    const contents = formatResults(context, resultItems);
    return { contents };
  };
}

```

--------------------------------------------------------------------------------
/src/handlers/operation-handler.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ReadResourceTemplateCallback,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { SpecLoaderService } from '../types.js';
import { IFormatter } from '../services/formatters.js';
import { RenderablePathItem } from '../rendering/path-item.js';
import { RenderContext, RenderResultItem } from '../rendering/types.js';
import { createErrorResult } from '../rendering/utils.js';
import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
// Import shared handler utils
import {
  formatResults,
  isOpenAPIV3,
  FormattedResultItem,
  getValidatedPathItem, // Import new helper
  getValidatedOperations, // Import new helper
} from './handler-utils.js'; // Already has .js

const BASE_URI = 'openapi://';

// Removed duplicated FormattedResultItem type - now imported from handler-utils
// Removed duplicated formatResults function - now imported from handler-utils
// Removed duplicated isOpenAPIV3 function - now imported from handler-utils

/**
 * Handles requests for specific operation details within a path.
 * Corresponds to the `openapi://paths/{path}/{method*}` template.
 */
export class OperationHandler {
  constructor(
    private specLoader: SpecLoaderService,
    private formatter: IFormatter
  ) {}

  getTemplate(): ResourceTemplate {
    // TODO: Add completion logic if needed
    return new ResourceTemplate(`${BASE_URI}paths/{path}/{method*}`, {
      list: undefined,
      complete: undefined,
    });
  }

  handleRequest: ReadResourceTemplateCallback = async (
    uri: URL,
    variables: Variables
  ): Promise<{ contents: FormattedResultItem[] }> => {
    const encodedPath = variables.path as string;
    // Correct variable access key: 'method', not 'method*'
    const methodVar = variables['method']; // Can be string or string[]
    // Decode the path received from the URI variable
    const decodedPath = decodeURIComponent(encodedPath || '');
    // Use the builder to create the suffix, which will re-encode the path correctly
    const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
    const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
    let resultItems: RenderResultItem[];

    try {
      // Normalize methods: Handle string for single value, array for multiple.
      let methods: string[] = [];
      if (Array.isArray(methodVar)) {
        methods = methodVar.map(m => String(m).trim().toLowerCase()); // Ensure elements are strings
      } else if (typeof methodVar === 'string') {
        methods = [methodVar.trim().toLowerCase()]; // Treat as single item array
      }
      methods = methods.filter(m => m.length > 0); // Remove empty strings

      if (methods.length === 0) {
        throw new Error('No valid HTTP method specified.');
      }

      const spec = await this.specLoader.getTransformedSpec({
        resourceType: 'schema', // Use 'schema' for now
        format: 'openapi',
      });

      // Use imported type guard
      if (!isOpenAPIV3(spec)) {
        throw new Error('Only OpenAPI v3 specifications are supported');
      }

      // --- Use helper to get validated path item ---
      const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
      const pathItemObj = getValidatedPathItem(spec, lookupPath);

      // --- Use helper to get validated requested methods ---
      const validMethods = getValidatedOperations(pathItemObj, methods, lookupPath);

      // Instantiate RenderablePathItem with the validated pathItemObj
      const renderablePathItem = new RenderablePathItem(
        pathItemObj, // pathItemObj retrieved safely via helper
        lookupPath, // Pass the raw, decoded path
        pathUriSuffix // Pass the correctly built suffix
      );

      // Use the validated methods returned by the helper
      resultItems = renderablePathItem.renderOperationDetail(context, validMethods);
    } catch (error: unknown) {
      // Catch errors from helpers (e.g., path/method not found) or rendering
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error handling request ${uri.href}: ${message}`);
      // Create a single error item representing the overall request failure
      resultItems = createErrorResult(
        uri.href.substring(BASE_URI.length), // Use request URI suffix
        message
      );
    }

    // Use imported formatResults
    const contents = formatResults(context, resultItems);
    return { contents };
  };
}

```

--------------------------------------------------------------------------------
/DOCKERHUB_README.md:
--------------------------------------------------------------------------------

```markdown
# MCP OpenAPI Schema Explorer

[![Docker Pulls](https://img.shields.io/docker/pulls/kadykov/mcp-openapi-schema-explorer.svg)](https://hub.docker.com/r/kadykov/mcp-openapi-schema-explorer)
[![GitHub Repo](https://img.shields.io/badge/GitHub-kadykov/mcp--openapi--schema--explorer-blue?logo=github)](https://github.com/kadykov/mcp-openapi-schema-explorer)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

This Docker image runs the **MCP OpenAPI Schema Explorer**, an MCP (Model Context Protocol) server that provides token-efficient access to OpenAPI (v3.0) and Swagger (v2.0) specifications via **MCP Resources**.

It allows MCP clients (like Cline or Claude Desktop) to explore the structure and details of large OpenAPI specifications without needing to load the entire file into an LLM's context window.

**Source Code & Full Documentation:** [https://github.com/kadykov/mcp-openapi-schema-explorer](https://github.com/kadykov/mcp-openapi-schema-explorer)

## Features

- **MCP Resource Access:** Explore OpenAPI specs via intuitive URIs (`openapi://info`, `openapi://paths/...`, `openapi://components/...`).
- **OpenAPI v3.0 & Swagger v2.0 Support:** Loads both formats, automatically converting v2.0 to v3.0.
- **Local & Remote Files:** Load specs from local file paths (via volume mount) or HTTP/HTTPS URLs.
- **Token-Efficient:** Designed to minimize token usage for LLMs.
- **Multiple Output Formats:** Get detailed views in JSON (default), YAML, or minified JSON (`--output-format`).
- **Dynamic Server Name:** Server name in MCP clients reflects the `info.title` from the loaded spec.
- **Reference Transformation:** Internal `$ref`s (`#/components/...`) are transformed into clickable MCP URIs.

## How to Run

Pull the image:

```bash
docker pull kadykov/mcp-openapi-schema-explorer:latest
```

The container expects the path or URL to the OpenAPI specification as a command-line argument.

### Using a Remote Specification URL

Pass the URL directly to `docker run`:

```bash
docker run --rm -i kadykov/mcp-openapi-schema-explorer:latest https://petstore3.swagger.io/api/v3/openapi.json
```

### Using a Local Specification File

Mount your local file into the container using the `-v` flag and provide the path _inside the container_ as the argument:

```bash
# Example: Mount local file ./my-spec.yaml to /spec/api.yaml inside the container
docker run --rm -i -v "$(pwd)/my-spec.yaml:/spec/api.yaml" kadykov/mcp-openapi-schema-explorer:latest /spec/api.yaml
```

_(Note: Replace `$(pwd)/my-spec.yaml` with the actual absolute path to your local file on the host machine)_

### Specifying Output Format

Use the `--output-format` flag (optional, defaults to `json`):

```bash
# Using YAML output with a remote URL
docker run --rm -i kadykov/mcp-openapi-schema-explorer:latest https://petstore3.swagger.io/api/v3/openapi.json --output-format yaml

# Using minified JSON with a local file
docker run --rm -i -v "$(pwd)/my-spec.yaml:/spec/api.yaml" kadykov/mcp-openapi-schema-explorer:latest /spec/api.yaml --output-format json-minified
```

Supported formats: `json`, `yaml`, `json-minified`.

## Tags

- `latest`: Points to the most recent stable release.
- Specific version tags (e.g., `1.2.1`) are available corresponding to the npm package versions.

## Usage with MCP Clients

You can configure your MCP client (like Cline or Claude Desktop) to run this Docker image as an MCP server.

### Example: Remote URL Specification

```json
// Example: ~/.config/cline/mcp_config.json
{
  "mcpServers": {
    "My API Spec (Docker Remote)": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i", // Required for MCP communication
        "kadykov/mcp-openapi-schema-explorer:latest",
        "https://petstore3.swagger.io/api/v3/openapi.json"
        // Optional: Add "--output-format", "yaml" here if needed
      ],
      "env": {}
    }
  }
}
```

### Example: Local File Specification

```json
// Example: ~/.config/cline/mcp_config.json
{
  "mcpServers": {
    "My API Spec (Docker Local)": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i", // Required for MCP communication
        "-v",
        "/full/path/to/your/local/openapi.yaml:/spec/api.yaml", // Host path : Container path
        "kadykov/mcp-openapi-schema-explorer:latest",
        "/spec/api.yaml", // Path inside the container
        "--output-format",
        "yaml" // Optional format
      ],
      "env": {}
    }
  }
}
```

_(Remember to replace `/full/path/to/your/local/openapi.yaml` with the correct absolute path on your host machine)_

## Support

For issues or questions, please refer to the [GitHub repository](https://github.com/kadykov/mcp-openapi-schema-explorer) or open an [issue](https://github.com/kadykov/mcp-openapi-schema-explorer/issues).

```

--------------------------------------------------------------------------------
/test/__tests__/unit/utils/uri-builder.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  buildComponentDetailUri,
  buildComponentMapUri,
  buildOperationUri,
  buildPathItemUri,
  buildTopLevelFieldUri,
  buildComponentDetailUriSuffix,
  buildComponentMapUriSuffix,
  buildOperationUriSuffix,
  buildPathItemUriSuffix,
  buildTopLevelFieldUriSuffix,
} from '../../../../src/utils/uri-builder';

describe('URI Builder Utilities', () => {
  // --- Full URI Builders ---

  test('buildComponentDetailUri builds correct URI', () => {
    expect(buildComponentDetailUri('schemas', 'MySchema')).toBe(
      'openapi://components/schemas/MySchema'
    );
    expect(buildComponentDetailUri('responses', 'NotFound')).toBe(
      'openapi://components/responses/NotFound'
    );
    // Test with characters that might need encoding if rules change (but currently don't)
    expect(buildComponentDetailUri('parameters', 'user-id')).toBe(
      'openapi://components/parameters/user-id'
    );
  });

  test('buildComponentMapUri builds correct URI', () => {
    expect(buildComponentMapUri('schemas')).toBe('openapi://components/schemas');
    expect(buildComponentMapUri('parameters')).toBe('openapi://components/parameters');
  });

  test('buildOperationUri builds correct URI and encodes path (no leading slash)', () => {
    expect(buildOperationUri('/users', 'get')).toBe('openapi://paths/users/get'); // No leading slash encoded
    expect(buildOperationUri('/users/{userId}', 'post')).toBe(
      'openapi://paths/users%2F%7BuserId%7D/post' // Path encoded, no leading %2F
    );
    expect(buildOperationUri('/pets/{petId}/uploadImage', 'post')).toBe(
      'openapi://paths/pets%2F%7BpetId%7D%2FuploadImage/post' // Path encoded, no leading %2F
    );
    expect(buildOperationUri('users', 'get')).toBe('openapi://paths/users/get'); // Handles no leading slash input
    expect(buildOperationUri('users/{userId}', 'post')).toBe(
      'openapi://paths/users%2F%7BuserId%7D/post' // Handles no leading slash input
    );
    expect(buildOperationUri('/users', 'GET')).toBe('openapi://paths/users/get'); // Method lowercased
  });

  test('buildPathItemUri builds correct URI and encodes path (no leading slash)', () => {
    expect(buildPathItemUri('/users')).toBe('openapi://paths/users'); // No leading slash encoded
    expect(buildPathItemUri('/users/{userId}')).toBe('openapi://paths/users%2F%7BuserId%7D'); // Path encoded, no leading %2F
    expect(buildPathItemUri('/pets/{petId}/uploadImage')).toBe(
      'openapi://paths/pets%2F%7BpetId%7D%2FuploadImage' // Path encoded, no leading %2F
    );
    expect(buildPathItemUri('users')).toBe('openapi://paths/users'); // Handles no leading slash input
    expect(buildPathItemUri('users/{userId}')).toBe('openapi://paths/users%2F%7BuserId%7D'); // Handles no leading slash input
  });

  test('buildTopLevelFieldUri builds correct URI', () => {
    expect(buildTopLevelFieldUri('info')).toBe('openapi://info');
    expect(buildTopLevelFieldUri('paths')).toBe('openapi://paths');
    expect(buildTopLevelFieldUri('components')).toBe('openapi://components');
  });

  // --- URI Suffix Builders ---

  test('buildComponentDetailUriSuffix builds correct suffix', () => {
    expect(buildComponentDetailUriSuffix('schemas', 'MySchema')).toBe(
      'components/schemas/MySchema'
    );
    expect(buildComponentDetailUriSuffix('responses', 'NotFound')).toBe(
      'components/responses/NotFound'
    );
  });

  test('buildComponentMapUriSuffix builds correct suffix', () => {
    expect(buildComponentMapUriSuffix('schemas')).toBe('components/schemas');
    expect(buildComponentMapUriSuffix('parameters')).toBe('components/parameters');
  });

  test('buildOperationUriSuffix builds correct suffix and encodes path (no leading slash)', () => {
    expect(buildOperationUriSuffix('/users', 'get')).toBe('paths/users/get'); // No leading slash encoded
    expect(buildOperationUriSuffix('/users/{userId}', 'post')).toBe(
      'paths/users%2F%7BuserId%7D/post' // Path encoded, no leading %2F
    );
    expect(buildOperationUriSuffix('users/{userId}', 'post')).toBe(
      'paths/users%2F%7BuserId%7D/post' // Handles no leading slash input
    );
    expect(buildOperationUriSuffix('/users', 'GET')).toBe('paths/users/get'); // Method lowercased
  });

  test('buildPathItemUriSuffix builds correct suffix and encodes path (no leading slash)', () => {
    expect(buildPathItemUriSuffix('/users')).toBe('paths/users'); // No leading slash encoded
    expect(buildPathItemUriSuffix('/users/{userId}')).toBe('paths/users%2F%7BuserId%7D'); // Path encoded, no leading %2F
    expect(buildPathItemUriSuffix('users/{userId}')).toBe('paths/users%2F%7BuserId%7D'); // Handles no leading slash input
  });

  test('buildTopLevelFieldUriSuffix builds correct suffix', () => {
    expect(buildTopLevelFieldUriSuffix('info')).toBe('info');
    expect(buildTopLevelFieldUriSuffix('paths')).toBe('paths');
  });
});

```

--------------------------------------------------------------------------------
/src/utils/uri-builder.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Utility functions for building standardized MCP URIs for this server.
 */

const BASE_URI_SCHEME = 'openapi://';

/**
 * Encodes a string component for safe inclusion in a URI path segment.
 * Uses standard encodeURIComponent.
 * Encodes a path string for safe inclusion in a URI.
 * This specifically targets path strings which might contain characters
 * like '{', '}', etc., that need encoding when forming the URI path part.
 * Uses standard encodeURIComponent.
 * Encodes a path string for safe inclusion in a URI path segment.
 * This is necessary because the path segment comes from the user potentially
 * containing characters that need encoding (like '{', '}').
 * Uses standard encodeURIComponent.
 * @param path The path string to encode.
 * @returns The encoded path string, with leading slashes removed before encoding.
 */
export function encodeUriPathComponent(path: string): string {
  // Added export
  // Remove leading slashes before encoding
  const pathWithoutLeadingSlash = path.replace(/^\/+/, '');
  return encodeURIComponent(pathWithoutLeadingSlash);
}

// --- Full URI Builders ---

/**
 * Builds the URI for accessing a specific component's details.
 * Example: openapi://components/schemas/MySchema
 * @param type The component type (e.g., 'schemas', 'responses').
 * @param name The component name.
 * @returns The full component detail URI.
 */
export function buildComponentDetailUri(type: string, name: string): string {
  // Per user instruction, do not encode type or name here.
  return `${BASE_URI_SCHEME}components/${type}/${name}`;
}

/**
 * Builds the URI for listing components of a specific type.
 * Example: openapi://components/schemas
 * @param type The component type (e.g., 'schemas', 'responses').
 * @returns The full component map URI.
 */
export function buildComponentMapUri(type: string): string {
  // Per user instruction, do not encode type here.
  return `${BASE_URI_SCHEME}components/${type}`;
}

/**
 * Builds the URI for accessing a specific operation's details.
 * Example: openapi://paths/users/{userId}/GET
 * @param path The API path (e.g., '/users/{userId}').
 * @param method The HTTP method (e.g., 'GET', 'POST').
 * @returns The full operation detail URI.
 */
export function buildOperationUri(path: string, method: string): string {
  // Encode only the path component. Assume 'path' is raw/decoded.
  // Method is assumed to be safe or handled by SDK/client.
  return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`; // Standardize method to lowercase
}

/**
 * Builds the URI for listing methods available at a specific path.
 * Example: openapi://paths/users/{userId}
 * @param path The API path (e.g., '/users/{userId}').
 * @returns The full path item URI.
 */
export function buildPathItemUri(path: string): string {
  // Encode only the path component. Assume 'path' is raw/decoded.
  return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}`;
}

/**
 * Builds the URI for accessing a top-level field (like 'info' or 'servers')
 * or triggering a list view ('paths', 'components').
 * Example: openapi://info, openapi://paths
 * @param field The top-level field name.
 * @returns The full top-level field URI.
 */
export function buildTopLevelFieldUri(field: string): string {
  // Per user instruction, do not encode field here.
  return `${BASE_URI_SCHEME}${field}`;
}

// --- URI Suffix Builders (for RenderResultItem) ---

/**
 * Builds the URI suffix for a specific component's details.
 * Example: components/schemas/MySchema
 */
export function buildComponentDetailUriSuffix(type: string, name: string): string {
  // Per user instruction, do not encode type or name here.
  return `components/${type}/${name}`;
}

/**
 * Builds the URI suffix for listing components of a specific type.
 * Example: components/schemas
 */
export function buildComponentMapUriSuffix(type: string): string {
  // Per user instruction, do not encode type here.
  return `components/${type}`;
}

/**
 * Builds the URI suffix for a specific operation's details.
 * Example: paths/users/{userId}/get
 */
export function buildOperationUriSuffix(path: string, method: string): string {
  // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
  return `paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`;
}

/**
 * Builds the URI suffix for listing methods available at a specific path.
 * Example: paths/users/{userId}
 */
export function buildPathItemUriSuffix(path: string): string {
  // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
  return `paths/${encodeUriPathComponent(path)}`;
}

/**
 * Builds the URI suffix for a top-level field.
 * Example: info, paths
 */
export function buildTopLevelFieldUriSuffix(field: string): string {
  // Per user instruction, do not encode field here.
  return field;
}

```

--------------------------------------------------------------------------------
/memory-bank/techContext.md:
--------------------------------------------------------------------------------

```markdown
# Technical Context

## Development Stack

- TypeScript for implementation
- MCP SDK for server functionality
- Jest for testing
- npm for package distribution

## Key Dependencies

- `@modelcontextprotocol/sdk`: Core MCP functionality
- `swagger2openapi`: OpenAPI/Swagger spec loading, parsing, and v2->v3 conversion (Runtime dependency)
- `js-yaml`: YAML parsing (Runtime dependency)
- `zod`: Schema validation (Runtime dependency)
- `openapi-types`: OpenAPI type definitions (devDependency)
- `typescript`: TypeScript compiler (devDependency)
- `@types/*`: Various type definitions (devDependencies)
- `jest`: Testing framework (devDependency)
- `eslint`: Code linting (devDependency)
- `prettier`: Code formatting (devDependency)
- `semantic-release` & plugins (`@semantic-release/*`, `@codedependant/semantic-release-docker`): Automated releases (devDependencies)
- `just`: Task runner (Used locally, installed via action in CI)

## Technical Requirements

1. Must follow MCP protocol specifications.
2. Must handle large OpenAPI/Swagger specs efficiently.
3. Must provide type-safe reference handling (transforming internal refs to MCP URIs).
4. Must support loading specs from local file paths and remote HTTP/HTTPS URLs.
5. Must support OpenAPI v3.0 and Swagger v2.0 formats (with v2.0 being converted to v3.0).
6. Must be easily testable and maintainable.

## Development Environment

- TypeScript setup with strict type checking
- Jest testing framework with coverage
- ESLint for code quality
- Prettier for code formatting
- `just` task runner (`justfile`) for common development tasks (build, test, lint, etc.)
- Conventional Commits standard for commit messages (required for `semantic-release`)
- Test fixtures and helpers

## Code Organization

- Services layer:
  - `SpecLoaderService`: Uses `swagger2openapi` to load specs from files/URLs and handle v2->v3 conversion.
  - `ReferenceTransformService`: Transforms internal `#/components/...` refs to MCP URIs.
  - `Formatters`: Handle JSON/YAML output.
- Handlers layer for resource endpoints.
- Rendering layer for generating resource content.
- Utilities (e.g., URI builder).
- Strong typing with generics.
- Comprehensive test coverage.

## Testing Infrastructure

- Unit tests:
  - `SpecLoaderService` (mocking `swagger2openapi`).
  - `ReferenceTransformService`.
  - Rendering classes.
  - Handlers (mocking services).
- End-to-end tests:
  - Verify resource access for local v3, local v2, and remote v3 specs.
  - Test multi-value parameters.
  - Cover success and error scenarios.
  - Verify resource completion logic using `client.complete()`.
- Type-safe test utilities (`mcp-test-helpers`).
- Test fixtures (including v2.0 and v3.0 examples).
- Coverage reporting via Jest and upload to Codecov via GitHub Actions.
- CI Integration (`.github/workflows/ci.yml`):
  - Runs checks (`just all`, `just security`, CodeQL) on pushes/PRs to `main`.
  - Uses Node 22 environment.
  - Includes automated release job using `cycjimmy/semantic-release-action@v4`.

## Response Formats

1. Base Formats

   - JSON format (default format)
   - YAML format support
   - URI-based reference links
   - Token-efficient structure
   - OpenAPI v3 type compliance

2. Format Service

   - Pluggable formatter architecture
   - Format-specific MIME types (`application/json`, `text/yaml`)
   - Type-safe formatter interface (`IFormatter`)
   - Consistent error formatting (`text/plain`)
   - CLI-configurable output format (`--output-format`)

3. Implementation
   - Format-specific serialization
   - Shared type system
   - Error response handling
   - Multiple operation support
   - Reference transformation

## Deployment / Release Process

- Automated publishing to npm **and Docker Hub** (`kadykov/mcp-openapi-schema-explorer`) via `semantic-release` triggered by pushes to `main` branch in GitHub Actions.
- Uses `cycjimmy/semantic-release-action@v4` in the CI workflow.
- Relies on Conventional Commits to determine version bumps.
- Uses `@codedependant/semantic-release-docker` plugin for Docker build and push.
- Creates version tags (e.g., `v1.2.3`) and GitHub Releases automatically.
- Requires `NPM_TOKEN`, `DOCKERHUB_USERNAME`, and `DOCKERHUB_TOKEN` secrets/variables configured in GitHub repository.
- `CHANGELOG.md` is automatically generated and updated.
- Server version is dynamically set at runtime based on the release version.

## Configuration

- Command-line argument based configuration (`src/config.ts`).
- Single required argument: `<path-or-url-to-spec>`.
- Optional argument: `--output-format <json|yaml|json-minified>`.
- Required argument validation.
- TypeScript type safety (`ServerConfig` interface).
- Error handling for missing/invalid arguments.

## Error Handling

- Descriptive error messages
- Type-safe error handling
- Consistent error format
- Proper error propagation

## Future Extensions

- AsyncAPI format support
- GraphQL schema support
- External reference resolution
- Enhanced schema resources
- Reference validation

```

--------------------------------------------------------------------------------
/src/rendering/utils.ts:
--------------------------------------------------------------------------------

```typescript
// NOTE: This block replaces the previous import block to ensure types/interfaces are defined correctly.
import { OpenAPIV3 } from 'openapi-types';
import { RenderContext, RenderResultItem } from './types.js'; // Add .js
import {
  buildComponentDetailUriSuffix,
  buildComponentMapUriSuffix,
  buildOperationUriSuffix,
  // buildPathItemUriSuffix, // Not currently used by generateListHint
} from '../utils/uri-builder.js'; // Added .js extension

// Define possible types for list items to guide hint generation
type ListItemType = 'componentType' | 'componentName' | 'pathMethod';

// Define context needed for generating the correct detail URI suffix
interface HintContext {
  itemType: ListItemType;
  firstItemExample?: string; // Example value from the first item in the list
  // For componentName hints, the parent component type is needed
  parentComponentType?: string;
  // For pathMethod hints, the parent path is needed
  parentPath?: string;
}

/**
 * Safely retrieves the summary from an Operation object.
 * Handles cases where the operation might be undefined or lack a summary.
 *
 * @param operation - The Operation object or undefined.
 * @returns The operation summary or operationId string, truncated if necessary, or null if neither is available.
 */
export function getOperationSummary(
  operation: OpenAPIV3.OperationObject | undefined
): string | null {
  // Return summary or operationId without truncation
  return operation?.summary || operation?.operationId || null;
}

/**
 * Helper to generate a standard hint text for list views, using the centralized URI builders.
 * @param renderContext - The rendering context containing the base URI.
 * @param hintContext - Context about the type of items being listed and their parent context.
 * @returns The hint string.
 */
export function generateListHint(renderContext: RenderContext, hintContext: HintContext): string {
  let detailUriSuffixPattern: string;
  let itemTypeName: string; // User-friendly name for the item type in the hint text
  let exampleUriSuffix: string | undefined; // To hold the generated example URI

  switch (hintContext.itemType) {
    case 'componentType':
      // Listing component types (e.g., schemas, responses) at openapi://components
      // Hint should point to openapi://components/{type}
      detailUriSuffixPattern = buildComponentMapUriSuffix('{type}'); // Use placeholder
      itemTypeName = 'component type';
      if (hintContext.firstItemExample) {
        exampleUriSuffix = buildComponentMapUriSuffix(hintContext.firstItemExample);
      }
      break;
    case 'componentName':
      // Listing component names (e.g., MySchema, User) at openapi://components/{type}
      // Hint should point to openapi://components/{type}/{name}
      if (!hintContext.parentComponentType) {
        console.warn('generateListHint called for componentName without parentComponentType');
        return ''; // Avoid generating a broken hint
      }
      // Use the actual parent type and a placeholder for the name
      detailUriSuffixPattern = buildComponentDetailUriSuffix(
        hintContext.parentComponentType,
        '{name}'
      );
      itemTypeName = hintContext.parentComponentType.slice(0, -1); // e.g., 'schema' from 'schemas'
      if (hintContext.firstItemExample) {
        exampleUriSuffix = buildComponentDetailUriSuffix(
          hintContext.parentComponentType,
          hintContext.firstItemExample
        );
      }
      break;
    case 'pathMethod':
      // Listing methods (e.g., get, post) at openapi://paths/{path}
      // Hint should point to openapi://paths/{path}/{method}
      if (!hintContext.parentPath) {
        console.warn('generateListHint called for pathMethod without parentPath');
        return ''; // Avoid generating a broken hint
      }
      // Use the actual parent path and a placeholder for the method
      detailUriSuffixPattern = buildOperationUriSuffix(hintContext.parentPath, '{method}');
      itemTypeName = 'operation'; // Or 'method'? 'operation' seems clearer
      if (hintContext.firstItemExample) {
        // Ensure the example method is valid if needed, though usually it's just 'get', 'post' etc.
        exampleUriSuffix = buildOperationUriSuffix(
          hintContext.parentPath,
          hintContext.firstItemExample
        );
      }
      break;
    default:
      // Explicitly cast to string to avoid potential 'never' type issue in template literal
      console.warn(`Unknown itemType in generateListHint: ${String(hintContext.itemType)}`);
      return ''; // Avoid generating a hint if context is unknown
  }

  // Construct the full hint URI pattern using the base URI
  const fullHintPattern = `${renderContext.baseUri}${detailUriSuffixPattern}`;
  const fullExampleUri = exampleUriSuffix
    ? `${renderContext.baseUri}${exampleUriSuffix}`
    : undefined;

  let hintText = `\nHint: Use '${fullHintPattern}' to view details for a specific ${itemTypeName}.`;
  if (fullExampleUri) {
    hintText += ` (e.g., ${fullExampleUri})`;
  }

  return hintText;
}

/**
 * Helper to generate a standard error item for RenderResultItem arrays.
 * @param uriSuffix - The URI suffix for the error context.
 * @param message - The error message.
 * @returns A RenderResultItem array containing the error.
 */
export function createErrorResult(uriSuffix: string, message: string): RenderResultItem[] {
  return [
    {
      uriSuffix: uriSuffix,
      data: null,
      isError: true,
      errorText: message,
      renderAsList: true, // Errors are typically plain text
    },
  ];
}

```

--------------------------------------------------------------------------------
/test/__tests__/unit/rendering/document.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableDocument } from '../../../../src/rendering/document';
import { RenderContext } from '../../../../src/rendering/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';

// Mock Formatter
const mockFormatter: IFormatter = new JsonFormatter(); // Use JSON for predictable output

const mockContext: RenderContext = {
  formatter: mockFormatter,
  baseUri: 'openapi://',
};

// Sample OpenAPI Document Fixture
const sampleDoc: OpenAPIV3.Document = {
  openapi: '3.0.0',
  info: {
    title: 'Test API',
    version: '1.0.0',
  },
  paths: {
    '/test': {
      get: {
        summary: 'Test GET',
        responses: {
          '200': { description: 'OK' },
        },
      },
    },
  },
  components: {
    schemas: {
      TestSchema: { type: 'string' },
    },
  },
  servers: [{ url: 'http://localhost:3000' }],
};

describe('RenderableDocument', () => {
  let renderableDoc: RenderableDocument;

  beforeEach(() => {
    renderableDoc = new RenderableDocument(sampleDoc);
  });

  it('should instantiate correctly', () => {
    expect(renderableDoc).toBeInstanceOf(RenderableDocument);
  });

  // Test the internal detail rendering method
  describe('renderTopLevelFieldDetail', () => {
    it('should render detail for a valid top-level field (info)', () => {
      const fieldObject = renderableDoc.getTopLevelField('info');
      const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'info');

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'info',
        data: sampleDoc.info, // Expect raw data
        isError: undefined, // Should default to false implicitly
        renderAsList: undefined, // Should default to false implicitly
      });
    });

    it('should render detail for another valid field (servers)', () => {
      const fieldObject = renderableDoc.getTopLevelField('servers');
      const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'servers');

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'servers',
        data: sampleDoc.servers,
        isError: undefined,
        renderAsList: undefined,
      });
    });

    it('should return error for non-existent field', () => {
      const fieldObject = renderableDoc.getTopLevelField('nonexistent');
      const result = renderableDoc.renderTopLevelFieldDetail(
        mockContext,
        fieldObject, // Will be undefined
        'nonexistent'
      );

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'nonexistent',
        data: null,
        isError: true,
        errorText: 'Error: Field "nonexistent" not found in the OpenAPI document.',
        renderAsList: true,
      });
    });

    it('should return error when trying to render "paths" via detail method', () => {
      const fieldObject = renderableDoc.getTopLevelField('paths');
      const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'paths');

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'paths',
        data: null,
        isError: true,
        errorText: `Error: Field "paths" should be accessed via its list view (${mockContext.baseUri}paths). Use the list view first.`,
        renderAsList: true,
      });
    });

    it('should return error when trying to render "components" via detail method', () => {
      const fieldObject = renderableDoc.getTopLevelField('components');
      const result = renderableDoc.renderTopLevelFieldDetail(
        mockContext,
        fieldObject,
        'components'
      );

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'components',
        data: null,
        isError: true,
        errorText: `Error: Field "components" should be accessed via its list view (${mockContext.baseUri}components). Use the list view first.`,
        renderAsList: true,
      });
    });
  });

  // Test the interface methods (which currently return errors)
  describe('Interface Methods', () => {
    it('renderList should return error', () => {
      const result = renderableDoc.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        uriSuffix: 'error',
        isError: true,
        errorText: expect.stringContaining(
          'List rendering is only supported for specific fields'
        ) as string,
        renderAsList: true,
      });
    });

    it('renderDetail should return error', () => {
      const result = renderableDoc.renderDetail(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        uriSuffix: 'error',
        isError: true,
        errorText: expect.stringContaining(
          'Detail rendering requires specifying a top-level field'
        ) as string,
        renderAsList: true,
      });
    });
  });

  // Test helper methods
  describe('Helper Methods', () => {
    it('getPathsObject should return paths', () => {
      expect(renderableDoc.getPathsObject()).toBe(sampleDoc.paths);
    });
    it('getComponentsObject should return components', () => {
      expect(renderableDoc.getComponentsObject()).toBe(sampleDoc.components);
    });
    it('getTopLevelField should return correct field', () => {
      expect(renderableDoc.getTopLevelField('info')).toBe(sampleDoc.info);
      expect(renderableDoc.getTopLevelField('servers')).toBe(sampleDoc.servers);
      expect(renderableDoc.getTopLevelField('nonexistent')).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/rendering/document.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
// No longer need ResourceContents here

// Placeholder for other renderable objects we'll create
// import { RenderablePaths } from './paths.js'; // Add .js
// import { RenderableComponents } from './components.js'; // Add .js

/**
 * Wraps an OpenAPIV3.Document to make it renderable.
 * Handles rendering for top-level fields like 'info', 'servers', etc.
 * Delegates list rendering for 'paths' and 'components' to respective objects.
 */
export class RenderableDocument implements RenderableSpecObject {
  // TODO: Add RenderablePaths and RenderableComponents instances
  // private renderablePaths: RenderablePaths;
  // private renderableComponents: RenderableComponents;

  constructor(private document: OpenAPIV3.Document) {
    // Initialize renderable wrappers for paths and components here
    // this.renderablePaths = new RenderablePaths(document.paths);
    // this.renderableComponents = new RenderableComponents(document.components);
  }

  /**
   * Renders a list view. For the document level, this is intended
   * to be called only when the requested field is 'paths' or 'components'.
   * The actual routing/delegation will happen in the handler based on the field.
   */
  renderList(_context: RenderContext): RenderResultItem[] {
    // Prefix context with _
    // This method should ideally not be called directly on the document
    // without specifying 'paths' or 'components' as the field.
    // The handler for openapi://{field} will delegate to the appropriate
    // sub-object's renderList.
    // Returning an error result item.
    return [
      {
        uriSuffix: 'error',
        data: null, // No specific data for this error
        isError: true,
        errorText:
          'Error: List rendering is only supported for specific fields like "paths" or "components" at the top level.',
        renderAsList: true, // Errors often shown as plain text
      },
    ];
  }

  /**
   * Renders the detail view. For the document level, this should not be called
   * directly without specifying a field. The handler should call
   * `renderTopLevelFieldDetail` instead.
   */
  renderDetail(_context: RenderContext): RenderResultItem[] {
    // Prefix context with _
    // This method implementation fulfills the interface requirement,
    // but direct detail rendering of the whole document isn't meaningful here.
    return [
      {
        uriSuffix: 'error',
        data: null,
        isError: true,
        errorText:
          'Error: Detail rendering requires specifying a top-level field (e.g., "info", "servers").',
        renderAsList: true, // Errors often shown as plain text
      },
    ];
  }

  /**
   * Renders the detail view for a *specific* top-level field (e.g., 'info', 'servers').
   * This is called by the handler after identifying the field.
   *
   * @param context - The rendering context.
   * @param fieldObject - The actual top-level field object to render (e.g., document.info).
   * @param fieldName - The name of the field being rendered (e.g., 'info').
   * @returns An array of RenderResultItem representing the detail view.
   */
  renderTopLevelFieldDetail(
    context: RenderContext,
    fieldObject: unknown,
    fieldName: string
  ): RenderResultItem[] {
    // Ensure fieldObject is provided (handler should validate fieldName exists)
    if (fieldObject === undefined || fieldObject === null) {
      return [
        {
          uriSuffix: fieldName,
          data: null,
          isError: true,
          errorText: `Error: Field "${fieldName}" not found in the OpenAPI document.`,
          renderAsList: true,
        },
      ];
    }

    // Avoid rendering structural fields that have dedicated list views
    if (fieldName === 'paths' || fieldName === 'components') {
      return [
        {
          uriSuffix: fieldName,
          data: null,
          isError: true,
          errorText: `Error: Field "${fieldName}" should be accessed via its list view (${context.baseUri}${fieldName}). Use the list view first.`,
          renderAsList: true,
        },
      ];
    }

    try {
      // For successful detail rendering, return the data object itself.
      // The handler will format it using the context.formatter.
      return [
        {
          uriSuffix: fieldName,
          data: fieldObject, // Pass the raw data
          // isError defaults to false
          // renderAsList defaults to false (meaning use detail formatter)
        },
      ];
    } catch (error: unknown) {
      // Handle potential errors during data access or initial checks
      // Formatting errors will be caught by the handler later
      return [
        {
          uriSuffix: fieldName,
          data: null,
          isError: true,
          errorText: `Error preparing field "${fieldName}" for rendering: ${
            error instanceof Error ? error.message : String(error)
          }`,
          renderAsList: true,
        },
      ];
    }
  } // End of renderTopLevelFieldDetail

  // --- Helper methods to access specific parts ---

  getPathsObject(): OpenAPIV3.PathsObject | undefined {
    return this.document.paths;
  }

  getComponentsObject(): OpenAPIV3.ComponentsObject | undefined {
    return this.document.components;
  }

  getTopLevelField(fieldName: string): unknown {
    // Define allowed top-level OpenAPI document properties
    const allowedFields: Array<keyof OpenAPIV3.Document> = [
      'openapi',
      'info',
      'servers',
      'paths',
      'components',
      'security',
      'tags',
      'externalDocs',
    ];

    // Only allow access to documented OpenAPI properties
    if (allowedFields.includes(fieldName as keyof OpenAPIV3.Document)) {
      return this.document[fieldName as keyof OpenAPIV3.Document];
    }
    return undefined;
  }
} // End of RenderableDocument class

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/path-item-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { PathItemHandler } from '../../../../src/handlers/path-item-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';

// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
  getSpec: jest.fn(),
  getTransformedSpec: mockGetTransformedSpec,
};

const mockFormatter: IFormatter = new JsonFormatter(); // Needed for context

// Sample Data
const samplePathItem: OpenAPIV3.PathItemObject = {
  get: { summary: 'Get Item', responses: { '200': { description: 'OK' } } },
  post: { summary: 'Create Item', responses: { '201': { description: 'Created' } } },
};
const sampleSpec: OpenAPIV3.Document = {
  openapi: '3.0.3',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {
    '/items': samplePathItem,
    '/empty': {}, // Path with no methods
  },
  components: {},
};

const encodedPathItems = encodeURIComponent('items');
const encodedPathEmpty = encodeURIComponent('empty');
const encodedPathNonExistent = encodeURIComponent('nonexistent');

describe('PathItemHandler', () => {
  let handler: PathItemHandler;

  beforeEach(() => {
    handler = new PathItemHandler(mockSpecLoader, mockFormatter);
    mockGetTransformedSpec.mockReset();
    mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
  });

  it('should return the correct template', () => {
    const template = handler.getTemplate();
    expect(template).toBeInstanceOf(ResourceTemplate);
    expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}');
  });

  describe('handleRequest (List Methods)', () => {
    const mockExtra = {
      signal: new AbortController().signal,
      sendNotification: jest.fn(),
      sendRequest: jest.fn(),
      requestId: 'test-request-id' as RequestId,
    };

    it('should list methods for a valid path', async () => {
      const variables: Variables = { path: encodedPathItems };
      const uri = new URL(`openapi://paths/${encodedPathItems}`);

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(mockGetTransformedSpec).toHaveBeenCalledWith({
        resourceType: 'schema',
        format: 'openapi',
      });
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toMatchObject({
        uri: `openapi://paths/${encodedPathItems}`,
        mimeType: 'text/plain',
        isError: false,
      });
      // Check for hint first, then methods
      expect(result.contents[0].text).toContain("Hint: Use 'openapi://paths/items/{method}'");
      expect(result.contents[0].text).toContain('GET: Get Item');
      expect(result.contents[0].text).toContain('POST: Create Item');
      // Ensure the old "Methods for..." header is not present if hint is first
      expect(result.contents[0].text).not.toContain('Methods for items:');
    });

    it('should handle path with no methods', async () => {
      const variables: Variables = { path: encodedPathEmpty };
      const uri = new URL(`openapi://paths/${encodedPathEmpty}`);

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathEmpty}`,
        mimeType: 'text/plain',
        text: 'No standard HTTP methods found for path: empty',
        isError: false, // Not an error, just no methods
      });
    });

    it('should return error for non-existent path', async () => {
      const variables: Variables = { path: encodedPathNonExistent };
      const uri = new URL(`openapi://paths/${encodedPathNonExistent}`);
      const expectedLogMessage = /Path "\/nonexistent" not found/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedPathItem
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathNonExistent}`,
        mimeType: 'text/plain',
        text: 'Path "/nonexistent" not found in the specification.',
        isError: true,
      });
    });

    it('should handle spec loading errors', async () => {
      const error = new Error('Spec load failed');
      mockGetTransformedSpec.mockRejectedValue(error);
      const variables: Variables = { path: encodedPathItems };
      const uri = new URL(`openapi://paths/${encodedPathItems}`);
      const expectedLogMessage = /Spec load failed/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}`,
        mimeType: 'text/plain',
        text: 'Spec load failed',
        isError: true,
      });
    });

    it('should handle non-OpenAPI v3 spec', async () => {
      const invalidSpec = { swagger: '2.0', info: {} };
      mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
      const variables: Variables = { path: encodedPathItems };
      const uri = new URL(`openapi://paths/${encodedPathItems}`);
      const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}`,
        mimeType: 'text/plain',
        text: 'Only OpenAPI v3 specifications are supported',
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/rendering/path-item.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
import { getOperationSummary, createErrorResult, generateListHint } from './utils.js'; // Add .js

/**
 * Wraps an OpenAPIV3.PathItemObject to make it renderable.
 * Handles rendering the list of methods for a specific path and
 * the details of specific operations (methods).
 */
export class RenderablePathItem implements RenderableSpecObject {
  constructor(
    private pathItem: OpenAPIV3.PathItemObject | undefined,
    private path: string, // The raw, decoded path string e.g., "/users/{userId}"
    private pathUriSuffix: string // Built using buildPathItemUriSuffix(path) e.g., 'paths/users%7BuserId%7D'
  ) {}

  /**
   * Renders a token-efficient list of methods available for this path.
   * Corresponds to the `openapi://paths/{path}` URI.
   */
  renderList(context: RenderContext): RenderResultItem[] {
    if (!this.pathItem) {
      return createErrorResult(this.pathUriSuffix, 'Path item not found.');
    }

    // Correctly check if the lowercase key is one of the enum values
    const methods = Object.keys(this.pathItem).filter(key =>
      Object.values(OpenAPIV3.HttpMethods).includes(key.toLowerCase() as OpenAPIV3.HttpMethods)
    ) as OpenAPIV3.HttpMethods[];

    // Check if methods array is empty *after* filtering
    if (methods.length === 0) {
      // Return a specific non-error message indicating no methods were found
      return [
        {
          uriSuffix: this.pathUriSuffix,
          data: `No standard HTTP methods found for path: ${decodeURIComponent(
            this.pathUriSuffix.substring('paths/'.length) // Get original path for display
          )}`,
          renderAsList: true,
          // isError is implicitly false here
        },
      ];
    }

    // Sort methods first to get the correct example
    methods.sort();
    const firstMethodExample = methods.length > 0 ? methods[0] : undefined;

    // Generate hint using the new structure, providing the first *sorted* method as an example
    const hint = generateListHint(context, {
      itemType: 'pathMethod',
      parentPath: this.path, // Use the stored raw path
      firstItemExample: firstMethodExample,
    });
    // Hint includes leading newline, so start output with it directly
    let outputLines: string[] = [hint.trim(), '']; // Trim leading newline from hint for first line

    // Iterate over the already sorted methods
    methods.forEach(method => {
      const operation = this.getOperation(method);
      // Use summary or operationId (via getOperationSummary)
      const summaryText = getOperationSummary(operation);
      // Format as METHOD: Summary or just METHOD if no summary/opId
      outputLines.push(`${method.toUpperCase()}${summaryText ? `: ${summaryText}` : ''}`);
    });

    return [
      {
        uriSuffix: this.pathUriSuffix,
        data: outputLines.join('\n'), // Join lines into a single string
        renderAsList: true,
      },
    ];
  }

  /**
   * Renders the detail view for one or more specific operations (methods)
   * Renders the detail view. For a PathItem, this usually means listing
   * the methods, similar to renderList. The handler should call
   * `renderOperationDetail` for specific method details.
   */
  renderDetail(context: RenderContext): RenderResultItem[] {
    // Delegate to renderList as the primary view for a path item itself.
    return this.renderList(context);
  }

  /**
   * Renders the detail view for one or more specific operations (methods)
   * within this path item.
   * Corresponds to the `openapi://paths/{path}/{method*}` URI.
   * This is called by the handler after identifying the method(s).
   *
   * @param context - The rendering context.
   * @param methods - Array of method names (e.g., ['get', 'post']).
   * @returns An array of RenderResultItem representing the operation details.
   */
  renderOperationDetail(
    _context: RenderContext, // Context might be needed later
    methods: string[]
  ): RenderResultItem[] {
    if (!this.pathItem) {
      // Create error results for all requested methods if path item is missing
      return methods.map(method => ({
        uriSuffix: `${this.pathUriSuffix}/${method}`,
        data: null,
        isError: true,
        errorText: 'Path item not found.',
        renderAsList: true,
      }));
    }

    const results: RenderResultItem[] = [];

    for (const method of methods) {
      const operation = this.getOperation(method);
      const operationUriSuffix = `${this.pathUriSuffix}/${method}`;

      if (!operation) {
        results.push({
          uriSuffix: operationUriSuffix,
          data: null,
          isError: true,
          errorText: `Method "${method.toUpperCase()}" not found for path.`,
          renderAsList: true,
        });
      } else {
        // Return the raw operation object; handler will format it
        results.push({
          uriSuffix: operationUriSuffix,
          data: operation,
          // isError: false (default)
          // renderAsList: false (default)
        });
      }
    }
    return results;
  }

  /**
   * Gets the OperationObject for a specific HTTP method within this path item.
   * Performs case-insensitive lookup.
   * @param method - The HTTP method string (e.g., 'get', 'POST').
   * @returns The OperationObject or undefined if not found.
   */
  getOperation(method: string): OpenAPIV3.OperationObject | undefined {
    if (!this.pathItem) {
      return undefined;
    }
    const lowerMethod = method.toLowerCase();

    // Check if the key is a standard HTTP method defined in the enum
    if (Object.values(OpenAPIV3.HttpMethods).includes(lowerMethod as OpenAPIV3.HttpMethods)) {
      const operation = this.pathItem[lowerMethod as keyof OpenAPIV3.PathItemObject];
      // Basic check to ensure it looks like an operation object
      if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
        // The check above narrows the type sufficiently, assertion is redundant
        return operation;
      }
    }
    return undefined; // Not a valid method or not an operation object
  }
}

```

--------------------------------------------------------------------------------
/test/__tests__/e2e/spec-loading.test.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ReadResourceResult, TextResourceContents } from '@modelcontextprotocol/sdk/types.js';
import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers';
import path from 'path';

// Helper function to parse JSON safely
function parseJsonSafely(text: string | undefined): unknown {
  if (text === undefined) {
    throw new Error('Received undefined text for JSON parsing');
  }
  try {
    return JSON.parse(text);
  } catch (e) {
    console.error('Failed to parse JSON:', text);
    throw new Error(`Invalid JSON received: ${e instanceof Error ? e.message : String(e)}`);
  }
}

// Type guard to check if content is TextResourceContents
function hasTextContent(
  content: ReadResourceResult['contents'][0]
): content is TextResourceContents {
  return typeof (content as TextResourceContents).text === 'string';
}

describe('E2E Tests for Spec Loading Scenarios', () => {
  let testContext: McpTestContext | null = null; // Allow null for cleanup
  let client: Client | null = null; // Allow null

  // Helper to setup client for tests, allowing different spec paths
  async function setup(specPathOrUrl: string): Promise<void> {
    // Cleanup previous context if exists
    if (testContext) {
      await testContext.cleanup();
      testContext = null;
      client = null;
    }
    try {
      testContext = await startMcpServer(specPathOrUrl, { outputFormat: 'json' });
      client = testContext.client;
    } catch (error) {
      // Explicitly convert error to string for logging
      const errorMsg = error instanceof Error ? error.message : String(error);
      console.warn(`Skipping tests for ${specPathOrUrl} due to setup error: ${errorMsg}`);
      testContext = null; // Ensure cleanup doesn't run on failed setup
      client = null; // Ensure tests are skipped
    }
  }

  afterEach(async () => {
    await testContext?.cleanup();
    testContext = null;
    client = null;
  });

  // Helper to read resource and perform basic checks
  async function readResourceAndCheck(uri: string): Promise<ReadResourceResult['contents'][0]> {
    if (!client) throw new Error('Client not initialized, skipping test.');
    const result = await client.readResource({ uri });
    expect(result.contents).toHaveLength(1);
    const content = result.contents[0];
    expect(content.uri).toBe(uri);
    return content;
  }

  // Helper to read resource and check for text/plain list content
  async function checkTextListResponse(uri: string, expectedSubstrings: string[]): Promise<string> {
    const content = await readResourceAndCheck(uri);
    expect(content.mimeType).toBe('text/plain');
    expect(content.isError).toBeFalsy();
    if (!hasTextContent(content)) throw new Error('Expected text content');
    for (const sub of expectedSubstrings) {
      expect(content.text).toContain(sub);
    }
    return content.text;
  }

  // Helper to read resource and check for JSON detail content
  async function checkJsonDetailResponse(uri: string, expectedObject: object): Promise<unknown> {
    const content = await readResourceAndCheck(uri);
    expect(content.mimeType).toBe('application/json');
    expect(content.isError).toBeFalsy();
    if (!hasTextContent(content)) throw new Error('Expected text content');
    const data = parseJsonSafely(content.text);
    expect(data).toMatchObject(expectedObject);
    return data;
  }

  // --- Tests for Local Swagger v2.0 Spec ---
  describe('Local Swagger v2.0 Spec (sample-v2-api.json)', () => {
    const v2SpecPath = path.resolve(__dirname, '../../fixtures/sample-v2-api.json');

    beforeAll(async () => await setup(v2SpecPath)); // Use beforeAll for this block

    it('should retrieve the converted "info" field', async () => {
      if (!client) return; // Skip if setup failed
      await checkJsonDetailResponse('openapi://info', {
        title: 'Simple Swagger 2.0 API',
        version: '1.0.0',
      });
    });

    it('should retrieve the converted "paths" list', async () => {
      if (!client) return; // Skip if setup failed
      await checkTextListResponse('openapi://paths', [
        'Hint:',
        'GET /v2/ping', // Note the basePath is included
      ]);
    });

    it('should retrieve the converted "components" list', async () => {
      if (!client) return; // Skip if setup failed
      await checkTextListResponse('openapi://components', [
        'Available Component Types:',
        '- schemas',
        "Hint: Use 'openapi://components/{type}'",
      ]);
    });

    it('should get details for converted schema Pong', async () => {
      if (!client) return; // Skip if setup failed
      await checkJsonDetailResponse('openapi://components/schemas/Pong', {
        type: 'object',
        properties: { message: { type: 'string', example: 'pong' } },
      });
    });
  });

  // --- Tests for Remote OpenAPI v3.0 Spec (Petstore) ---
  // Increase timeout for remote fetch
  jest.setTimeout(20000); // 20 seconds

  describe('Remote OpenAPI v3.0 Spec (Petstore)', () => {
    const petstoreUrl = 'https://petstore3.swagger.io/api/v3/openapi.json';

    beforeAll(async () => await setup(petstoreUrl)); // Use beforeAll for this block

    it('should retrieve the "info" field from Petstore', async () => {
      if (!client) return; // Skip if setup failed
      await checkJsonDetailResponse('openapi://info', {
        title: 'Swagger Petstore - OpenAPI 3.0',
        // version might change, so don't assert exact value
      });
    });

    it('should retrieve the "paths" list from Petstore', async () => {
      if (!client) return; // Skip if setup failed
      // Check for a known path
      await checkTextListResponse('openapi://paths', ['/pet/{petId}']);
    });

    it('should retrieve the "components" list from Petstore', async () => {
      if (!client) return; // Skip if setup failed
      // Check for known component types
      await checkTextListResponse('openapi://components', [
        '- schemas',
        '- requestBodies',
        '- securitySchemes',
      ]);
    });

    it('should get details for schema Pet from Petstore', async () => {
      if (!client) return; // Skip if setup failed
      await checkJsonDetailResponse('openapi://components/schemas/Pet', {
        required: ['name', 'photoUrls'],
        type: 'object',
        // Check a known property
        properties: { id: { type: 'integer', format: 'int64' } },
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# [1.3.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.2.1...v1.3.0) (2025-08-12)

### Bug Fixes

- Use more generic instructions ([1372aae](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/1372aaea824f2b9eb5d4c3569acc4f38c82550fd))

### Features

- add brief instructions so LLMs can better understand how to use the server ([c55c4ec](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/c55c4ec029a7603746bf506340d8e3ffd54a6532))

## [1.2.1](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.2.0...v1.2.1) (2025-04-13)

### Bug Fixes

- update Node.js setup to match Dockerfile version and include dev dependencies ([8658705](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/86587059268ad4c18d219729b39e4e4f990e05e9))

# [1.2.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.1.0...v1.2.0) (2025-04-13)

### Bug Fixes

- remove husky.sh sourcing from pre-commit hook ([2cf9455](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2cf9455f1432cb0c6cbda71d61cad9f2f87031ab))
- update Docker Hub login to use secrets for credentials ([ab2136b](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ab2136bd8c052d7287ef1fd6d2768a9fd93148c8))

### Features

- implement Docker support with multi-stage builds and CI integration ([910dc02](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/910dc021b3e203574dee93198ce5896a9e8aa16d))

# [1.1.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.2...v1.1.0) (2025-04-13)

### Features

- enhance component and path item rendering with descriptions and examples in hints ([6989159](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/698915972338b4a16419c9cea3e2377b7701f50b))

## [1.0.2](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.1...v1.0.2) (2025-04-13)

### Bug Fixes

- update CI workflow to use RELEASE_TOKEN and disable credential persistence ([e7b18f9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e7b18f9055b95f0e2c6e2a356cb87482db6205da))

## [1.0.1](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.0...v1.0.1) (2025-04-12)

### Bug Fixes

- add openapi-types dependency to package.json and package-lock.json ([d348fb9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/d348fb92a30cdb9d213ee92f1779258f43bbbcd9))

# 1.0.0 (2025-04-12)

### Bug Fixes

- add codecov badge to README for improved visibility of test coverage ([ed7bf93](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ed7bf93de6c6efbf3a890551b67321b0d003c3cf))

### Features

- add CI workflow and dependabot configuration for automated updates ([2d0b22e](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2d0b22ea20afd58297b2169d3761db32b4c92606))
- Add configuration management for OpenAPI Explorer ([b9f4771](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b9f47712e754983d292bd6d53c82fa7e344b45a6))
- add CONTRIBUTING.md and enhance README with detailed project information ([1f4b2d5](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/1f4b2d59d7a19e54556cf8933fc4e4952d8f438c))
- Add end-to-end tests for OpenAPI resource handling ([d1ba7ab](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/d1ba7ab5db84717ed6c326d0c7d625906572be2c))
- Add pre-commit hook to format staged files with Prettier ([af58250](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/af582509fadbffd52afcd36d6113a1965a2bfcef))
- Add SchemaListHandler and implement schema listing resource with error handling ([873bbee](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/873bbee9cee5233e97202458a6b261e6ac58b651))
- Add support for minified JSON output format and related enhancements ([f0cb5b8](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f0cb5b80eeb73d2656b1d8fb37ab8fe21dacf12a))
- Enhance endpoint features and add endpoint list handler with improved error handling ([32082ac](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/32082acd3f187bb0611a2adbbfb107f0c153aae2))
- Enhance OpenAPI resource handling with new templates and completion tests ([45e4938](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/45e4938b226dc6e1baeb506b8c23c615fef78065))
- Enhance output formatting with JSON and YAML support, including formatter implementations and configuration updates ([e63fafe](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e63fafe82abb36a56bbb976ff3098f2d4d6a7d6c))
- Implement dynamic server name based on OpenAPI spec title ([aaa691f](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/aaa691fa2c545a433e09fb3f1faa0d31d4e8624d))
- Implement EndpointListHandler and add endpoint list resource to server ([b81a606](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b81a60645eeec9b2e9bd7eb46914cdf3178f9457))
- Implement Map-based validation helpers to enhance security and error handling ([a4394c9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/a4394c9846482d53436019a0498ca5d91fddefdf))
- Implement resource completion logic and add related tests ([de8f297](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/de8f29785882a6bd68d4fcaf38de971de4bad222))
- Implement SchemaHandler and add schema resource support with error handling ([2fae461](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2fae461e5de51b7610135922b4a4c9a55cd5b126))
- initialize MCP OpenAPI schema explorer project ([fd64242](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/fd642421274172e5ca330c9b85015f597f4a96c1))
- Introduce suppressExpectedConsoleError utility to manage console.error during tests ([ef088c2](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ef088c2f98bacd0dd7ae3f4aa75e44ba52a41712))
- Update dependencies to include swagger2openapi and @types/js-yaml ([8acb951](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/8acb951eb88843c72f8eb7d6d7feff681b56ff84))
- update descriptions in API methods to include URL-encoding notes ([b71dbdf](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b71dbdfd8c5f0c02d9a47f99143416787f76bf50))
- Update endpoint URI template to support wildcard parameters ([ce1281f](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ce1281f16f81a0fd7a74b20fe6bb92e7ed19e158))
- Update EndpointHandler to return detailed operation responses for GET and POST methods ([af55400](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/af554008c35c9be5bdbf53e51b791e90d135e283))
- Update license compliance check to include Python-2.0 ([e00c5e2](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e00c5e23cca6070d6833017b567d7c5402276f45))
- Update MCP inspector command to support YAML output format ([f7fb551](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f7fb551cc3a9d7e84fb47100cf8e0430c2634070))
- update release job to match Node.js version and include dev dependencies ([f3aeb87](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f3aeb87dcd8bed9920fe2eccdcd8f253b310f761))

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/top-level-field-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { TopLevelFieldHandler } from '../../../../src/handlers/top-level-field-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';

// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
  getSpec: jest.fn(), // Not used by this handler directly
  getTransformedSpec: mockGetTransformedSpec,
};

const mockFormatter: IFormatter = new JsonFormatter(); // Use real formatter for structure check

// Sample Data
const sampleSpec: OpenAPIV3.Document = {
  openapi: '3.0.3',
  info: { title: 'Test API', version: '1.1.0' },
  paths: { '/test': { get: { responses: { '200': { description: 'OK' } } } } },
  components: { schemas: { Test: { type: 'string' } } },
  servers: [{ url: 'http://example.com' }],
};

describe('TopLevelFieldHandler', () => {
  let handler: TopLevelFieldHandler;

  beforeEach(() => {
    handler = new TopLevelFieldHandler(mockSpecLoader, mockFormatter);
    mockGetTransformedSpec.mockReset(); // Reset mock before each test
  });

  it('should return the correct template', () => {
    const template = handler.getTemplate();
    expect(template).toBeInstanceOf(ResourceTemplate);
    // Compare against the string representation of the UriTemplate object
    expect(template.uriTemplate.toString()).toBe('openapi://{field}');
  });

  describe('handleRequest', () => {
    const mockExtra = {
      signal: new AbortController().signal,
      sendNotification: jest.fn(),
      sendRequest: jest.fn(),
      requestId: 'test-request-id' as RequestId,
    };

    it('should handle request for "info" field', async () => {
      mockGetTransformedSpec.mockResolvedValue(sampleSpec);
      const variables: Variables = { field: 'info' };
      const uri = new URL('openapi://info');

      // Pass the mock extra object as the third argument
      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(mockGetTransformedSpec).toHaveBeenCalledWith({
        resourceType: 'schema',
        format: 'openapi',
      });
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://info',
        mimeType: 'application/json',
        text: JSON.stringify(sampleSpec.info, null, 2),
        isError: false,
      });
    });

    it('should handle request for "servers" field', async () => {
      mockGetTransformedSpec.mockResolvedValue(sampleSpec);
      const variables: Variables = { field: 'servers' };
      const uri = new URL('openapi://servers');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://servers',
        mimeType: 'application/json',
        text: JSON.stringify(sampleSpec.servers, null, 2),
        isError: false,
      });
    });

    it('should handle request for "paths" field (list view)', async () => {
      mockGetTransformedSpec.mockResolvedValue(sampleSpec);
      const variables: Variables = { field: 'paths' };
      const uri = new URL('openapi://paths');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].uri).toBe('openapi://paths');
      expect(result.contents[0].mimeType).toBe('text/plain');
      expect(result.contents[0].isError).toBe(false);
      expect(result.contents[0].text).toContain('GET /test'); // Check content format
      // Check that the hint contains the essential URI patterns
      expect(result.contents[0].text).toContain('Hint:');
      expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}');
      expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}/{method}');
    });

    it('should handle request for "components" field (list view)', async () => {
      mockGetTransformedSpec.mockResolvedValue(sampleSpec);
      const variables: Variables = { field: 'components' };
      const uri = new URL('openapi://components');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].uri).toBe('openapi://components');
      expect(result.contents[0].mimeType).toBe('text/plain');
      expect(result.contents[0].isError).toBe(false);
      expect(result.contents[0].text).toContain('- schemas'); // Check content format
      expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/{type}'");
    });

    it('should return error for non-existent field', async () => {
      mockGetTransformedSpec.mockResolvedValue(sampleSpec);
      const variables: Variables = { field: 'nonexistent' };
      const uri = new URL('openapi://nonexistent');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://nonexistent',
        mimeType: 'text/plain',
        text: 'Error: Field "nonexistent" not found in the OpenAPI document.',
        isError: true,
      });
    });

    it('should handle spec loading errors', async () => {
      const error = new Error('Failed to load spec');
      mockGetTransformedSpec.mockRejectedValue(error);
      const variables: Variables = { field: 'info' };
      const uri = new URL('openapi://info');
      // Match the core error message using RegExp
      const expectedLogMessage = /Failed to load spec/;

      // Use the helper, letting TypeScript infer the return type
      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://info',
        mimeType: 'text/plain',
        text: 'Failed to load spec',
        isError: true,
      });
    });

    it('should handle non-OpenAPI v3 spec', async () => {
      const invalidSpec = { swagger: '2.0', info: {} }; // Not OpenAPI v3
      mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
      const variables: Variables = { field: 'info' };
      const uri = new URL('openapi://info');
      // Match the core error message using RegExp
      const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;

      // Use the helper, letting TypeScript infer the return type
      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://info',
        mimeType: 'text/plain',
        text: 'Only OpenAPI v3 specifications are supported',
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/component-map-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { ComponentMapHandler } from '../../../../src/handlers/component-map-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';

// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
  getSpec: jest.fn(),
  getTransformedSpec: mockGetTransformedSpec,
};

const mockFormatter: IFormatter = new JsonFormatter(); // Needed for context

// Sample Data
const sampleSpec: OpenAPIV3.Document = {
  openapi: '3.0.3',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {},
  components: {
    schemas: {
      User: { type: 'object', properties: { name: { type: 'string' } } },
      Error: { type: 'object', properties: { message: { type: 'string' } } },
    },
    parameters: {
      limitParam: { name: 'limit', in: 'query', schema: { type: 'integer' } },
    },
    examples: {}, // Empty type
  },
};

describe('ComponentMapHandler', () => {
  let handler: ComponentMapHandler;

  beforeEach(() => {
    handler = new ComponentMapHandler(mockSpecLoader, mockFormatter);
    mockGetTransformedSpec.mockReset();
    mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
  });

  it('should return the correct template', () => {
    const template = handler.getTemplate();
    expect(template).toBeInstanceOf(ResourceTemplate);
    expect(template.uriTemplate.toString()).toBe('openapi://components/{type}');
  });

  describe('handleRequest (List Component Names)', () => {
    const mockExtra = {
      signal: new AbortController().signal,
      sendNotification: jest.fn(),
      sendRequest: jest.fn(),
      requestId: 'test-request-id' as RequestId,
    };

    it('should list names for a valid component type (schemas)', async () => {
      const variables: Variables = { type: 'schemas' };
      const uri = new URL('openapi://components/schemas');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(mockGetTransformedSpec).toHaveBeenCalledWith({
        resourceType: 'schema',
        format: 'openapi',
      });
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toMatchObject({
        uri: 'openapi://components/schemas',
        mimeType: 'text/plain',
        isError: false,
      });
      expect(result.contents[0].text).toContain('Available schemas:');
      expect(result.contents[0].text).toMatch(/-\sError\n/); // Sorted
      expect(result.contents[0].text).toMatch(/-\sUser\n/);
      expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/schemas/{name}'");
    });

    it('should list names for another valid type (parameters)', async () => {
      const variables: Variables = { type: 'parameters' };
      const uri = new URL('openapi://components/parameters');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toMatchObject({
        uri: 'openapi://components/parameters',
        mimeType: 'text/plain',
        isError: false,
      });
      expect(result.contents[0].text).toContain('Available parameters:');
      expect(result.contents[0].text).toMatch(/-\slimitParam\n/);
      expect(result.contents[0].text).toContain(
        "Hint: Use 'openapi://components/parameters/{name}'"
      );
    });

    it('should handle component type with no components defined (examples)', async () => {
      const variables: Variables = { type: 'examples' };
      const uri = new URL('openapi://components/examples');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/examples',
        mimeType: 'text/plain',
        text: 'No components of type "examples" found.',
        isError: true, // Treat as error because map exists but is empty
      });
    });

    it('should handle component type not present in spec (securitySchemes)', async () => {
      const variables: Variables = { type: 'securitySchemes' };
      const uri = new URL('openapi://components/securitySchemes');
      const expectedLogMessage = /Component type "securitySchemes" not found/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedComponentMap
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/securitySchemes',
        mimeType: 'text/plain',
        text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters, examples',
        isError: true,
      });
    });

    it('should return error for invalid component type', async () => {
      const variables: Variables = { type: 'invalidType' };
      const uri = new URL('openapi://components/invalidType');
      const expectedLogMessage = /Invalid component type: invalidType/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/invalidType',
        mimeType: 'text/plain',
        text: 'Invalid component type: invalidType',
        isError: true,
      });
      expect(mockGetTransformedSpec).not.toHaveBeenCalled(); // Should fail before loading spec
    });

    it('should handle spec loading errors', async () => {
      const error = new Error('Spec load failed');
      mockGetTransformedSpec.mockRejectedValue(error);
      const variables: Variables = { type: 'schemas' };
      const uri = new URL('openapi://components/schemas');
      const expectedLogMessage = /Spec load failed/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas',
        mimeType: 'text/plain',
        text: 'Spec load failed',
        isError: true,
      });
    });

    it('should handle non-OpenAPI v3 spec', async () => {
      const invalidSpec = { swagger: '2.0', info: {} };
      mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
      const variables: Variables = { type: 'schemas' };
      const uri = new URL('openapi://components/schemas');
      const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas',
        mimeType: 'text/plain',
        text: 'Only OpenAPI v3 specifications are supported',
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/rendering/path-item.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderablePathItem } from '../../../../src/rendering/path-item';
import { RenderContext } from '../../../../src/rendering/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';

// Mock Formatter & Context
const mockFormatter: IFormatter = new JsonFormatter();
const mockContext: RenderContext = {
  formatter: mockFormatter,
  baseUri: 'openapi://',
};

// Sample PathItem Object Fixture
const samplePathItem: OpenAPIV3.PathItemObject = {
  get: {
    summary: 'Get Item',
    responses: { '200': { description: 'OK' } },
  },
  post: {
    summary: 'Create Item',
    responses: { '201': { description: 'Created' } },
  },
  delete: {
    // No summary
    responses: { '204': { description: 'No Content' } },
  },
  parameters: [
    // Example path-level parameter
    { name: 'commonParam', in: 'query', schema: { type: 'string' } },
  ],
};

// Define both the raw path and the expected suffix (built using the builder logic)
const rawPath = '/items';
const pathUriSuffix = 'paths/items'; // Builder removes leading '/' and encodes, but '/items' has no special chars

describe('RenderablePathItem', () => {
  describe('renderList (List Methods)', () => {
    it('should render a list of methods correctly', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const result = renderable.renderList(mockContext);

      expect(result).toHaveLength(1);
      expect(result[0].uriSuffix).toBe(pathUriSuffix);
      expect(result[0].renderAsList).toBe(true);
      expect(result[0].isError).toBeUndefined();

      // Define expected output lines based on the new format and builder logic
      // generateListHint uses buildOperationUriSuffix which encodes the path
      // Since rawPath is '/items', encoded is 'items'.
      // The first sorted method is 'delete'.
      const expectedHint =
        "Hint: Use 'openapi://paths/items/{method}' to view details for a specific operation. (e.g., openapi://paths/items/delete)";
      const expectedLineDelete = 'DELETE'; // No summary/opId
      const expectedLineGet = 'GET: Get Item'; // Summary exists
      const expectedLinePost = 'POST: Create Item'; // Summary exists
      const expectedOutput = `${expectedHint}\n\n${expectedLineDelete}\n${expectedLineGet}\n${expectedLinePost}`;

      // Check the full output string
      expect(result[0].data).toBe(expectedOutput);
    });

    it('should handle path item with no standard methods', () => {
      const noMethodsPathItem: OpenAPIV3.PathItemObject = {
        parameters: samplePathItem.parameters,
      };
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(noMethodsPathItem, rawPath, pathUriSuffix);
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: pathUriSuffix,
        data: 'No standard HTTP methods found for path: items',
        renderAsList: true,
      });
    });

    it('should return error if path item is undefined', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        uriSuffix: pathUriSuffix,
        isError: true,
        errorText: 'Path item not found.',
        renderAsList: true,
      });
    });
  });

  describe('renderOperationDetail (Get Operation Detail)', () => {
    it('should return detail for a single valid method', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const result = renderable.renderOperationDetail(mockContext, ['get']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: `${pathUriSuffix}/get`,
        data: samplePathItem.get, // Expect raw operation object
      });
    });

    it('should return details for multiple valid methods', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const result = renderable.renderOperationDetail(mockContext, ['post', 'delete']);
      expect(result).toHaveLength(2);
      expect(result).toContainEqual({
        uriSuffix: `${pathUriSuffix}/post`,
        data: samplePathItem.post,
      });
      expect(result).toContainEqual({
        uriSuffix: `${pathUriSuffix}/delete`,
        data: samplePathItem.delete,
      });
    });

    it('should return error for non-existent method', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const result = renderable.renderOperationDetail(mockContext, ['put']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: `${pathUriSuffix}/put`,
        data: null,
        isError: true,
        errorText: 'Method "PUT" not found for path.',
        renderAsList: true,
      });
    });

    it('should handle mix of valid and invalid methods', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const result = renderable.renderOperationDetail(mockContext, ['get', 'patch']);
      expect(result).toHaveLength(2);
      expect(result).toContainEqual({
        uriSuffix: `${pathUriSuffix}/get`,
        data: samplePathItem.get,
      });
      expect(result).toContainEqual({
        uriSuffix: `${pathUriSuffix}/patch`,
        data: null,
        isError: true,
        errorText: 'Method "PATCH" not found for path.',
        renderAsList: true,
      });
    });

    it('should return error if path item is undefined', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
      const result = renderable.renderOperationDetail(mockContext, ['get']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: `${pathUriSuffix}/get`,
        data: null,
        isError: true,
        errorText: 'Path item not found.',
        renderAsList: true,
      });
    });
  });

  describe('renderDetail (Interface Method)', () => {
    it('should delegate to renderList', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      const listResult = renderable.renderList(mockContext);
      const detailResult = renderable.renderDetail(mockContext);
      expect(detailResult).toEqual(listResult);
    });
  });

  describe('getOperation', () => {
    it('should return correct operation object (case-insensitive)', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      expect(renderable.getOperation('get')).toBe(samplePathItem.get);
      expect(renderable.getOperation('POST')).toBe(samplePathItem.post);
      expect(renderable.getOperation('Delete')).toBe(samplePathItem.delete);
    });

    it('should return undefined for non-existent method', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
      expect(renderable.getOperation('put')).toBeUndefined();
    });

    it('should return undefined if path item is undefined', () => {
      // Provide all 3 arguments to constructor
      const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
      expect(renderable.getOperation('get')).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/operation-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { OperationHandler } from '../../../../src/handlers/operation-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';

// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
  getSpec: jest.fn(),
  getTransformedSpec: mockGetTransformedSpec,
};

const mockFormatter: IFormatter = new JsonFormatter();

// Sample Data
const getOperation: OpenAPIV3.OperationObject = {
  summary: 'Get Item',
  responses: { '200': { description: 'OK' } },
};
const postOperation: OpenAPIV3.OperationObject = {
  summary: 'Create Item',
  responses: { '201': { description: 'Created' } },
};
const sampleSpec: OpenAPIV3.Document = {
  openapi: '3.0.3',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {
    '/items': {
      get: getOperation,
      post: postOperation,
    },
    '/items/{id}': {
      get: { summary: 'Get Single Item', responses: { '200': { description: 'OK' } } },
    },
  },
  components: {},
};

const encodedPathItems = encodeURIComponent('items');
const encodedPathNonExistent = encodeURIComponent('nonexistent');

describe('OperationHandler', () => {
  let handler: OperationHandler;

  beforeEach(() => {
    handler = new OperationHandler(mockSpecLoader, mockFormatter);
    mockGetTransformedSpec.mockReset();
    mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
  });

  it('should return the correct template', () => {
    const template = handler.getTemplate();
    expect(template).toBeInstanceOf(ResourceTemplate);
    expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}/{method*}');
  });

  describe('handleRequest', () => {
    const mockExtra = {
      signal: new AbortController().signal,
      sendNotification: jest.fn(),
      sendRequest: jest.fn(),
      requestId: 'test-request-id' as RequestId,
    };

    it('should return detail for a single valid method', async () => {
      const variables: Variables = { path: encodedPathItems, method: 'get' }; // Use 'method' key
      const uri = new URL(`openapi://paths/${encodedPathItems}/get`);

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(mockGetTransformedSpec).toHaveBeenCalledWith({
        resourceType: 'schema',
        format: 'openapi',
      });
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}/get`,
        mimeType: 'application/json',
        text: JSON.stringify(getOperation, null, 2),
        isError: false,
      });
    });

    it('should return details for multiple valid methods (array input)', async () => {
      const variables: Variables = { path: encodedPathItems, method: ['get', 'post'] }; // Use 'method' key with array
      const uri = new URL(`openapi://paths/${encodedPathItems}/get,post`); // URI might not reflect array input

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(2);
      expect(result.contents).toContainEqual({
        uri: `openapi://paths/${encodedPathItems}/get`,
        mimeType: 'application/json',
        text: JSON.stringify(getOperation, null, 2),
        isError: false,
      });
      expect(result.contents).toContainEqual({
        uri: `openapi://paths/${encodedPathItems}/post`,
        mimeType: 'application/json',
        text: JSON.stringify(postOperation, null, 2),
        isError: false,
      });
    });

    it('should return error for non-existent path', async () => {
      const variables: Variables = { path: encodedPathNonExistent, method: 'get' };
      const uri = new URL(`openapi://paths/${encodedPathNonExistent}/get`);
      const expectedLogMessage = /Path "\/nonexistent" not found/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedPathItem
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathNonExistent}/get`,
        mimeType: 'text/plain',
        text: 'Path "/nonexistent" not found in the specification.',
        isError: true,
      });
    });

    it('should return error for non-existent method', async () => {
      const variables: Variables = { path: encodedPathItems, method: 'put' };
      const uri = new URL(`openapi://paths/${encodedPathItems}/put`);
      const expectedLogMessage = /None of the requested methods \(put\) are valid/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedOperations
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}/put`,
        mimeType: 'text/plain',
        text: 'None of the requested methods (put) are valid for path "/items". Available methods: get, post',
        isError: true,
      });
    });

    // Remove test for mix of valid/invalid methods, as getValidatedOperations throws now
    // it('should handle mix of valid and invalid methods', async () => { ... });

    it('should handle empty method array', async () => {
      const variables: Variables = { path: encodedPathItems, method: [] };
      const uri = new URL(`openapi://paths/${encodedPathItems}/`);
      const expectedLogMessage = /No valid HTTP method specified/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}/`,
        mimeType: 'text/plain',
        text: 'No valid HTTP method specified.',
        isError: true,
      });
    });

    it('should handle spec loading errors', async () => {
      const error = new Error('Spec load failed');
      mockGetTransformedSpec.mockRejectedValue(error);
      const variables: Variables = { path: encodedPathItems, method: 'get' };
      const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
      const expectedLogMessage = /Spec load failed/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}/get`,
        mimeType: 'text/plain',
        text: 'Spec load failed',
        isError: true,
      });
    });

    it('should handle non-OpenAPI v3 spec', async () => {
      const invalidSpec = { swagger: '2.0', info: {} };
      mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
      const variables: Variables = { path: encodedPathItems, method: 'get' };
      const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
      const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: `openapi://paths/${encodedPathItems}/get`,
        mimeType: 'text/plain',
        text: 'Only OpenAPI v3 specifications are supported',
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/llms-install.md:
--------------------------------------------------------------------------------

```markdown
# MCP OpenAPI Schema Explorer Usage Guide

This guide explains how to add the MCP OpenAPI Schema Explorer server to your MCP client (e.g., Claude Desktop, Windsurf, Cline). This involves adding a configuration entry to your client's settings file that tells the client how to run the server process. The server itself doesn't require separate configuration beyond the command-line arguments specified in the client settings.

## Prerequisites

1.  Node.js (Latest LTS version recommended) OR Docker installed.
2.  Access to an OpenAPI v3.0 or Swagger v2.0 specification file, either via a local file path or a remote HTTP/HTTPS URL.
3.  An MCP client application (e.g., Claude Desktop, Windsurf, Cline, etc.).

## Installation

For the recommended usage methods (`npx` and Docker, described below), **no separate installation step is required**. Your MCP client will download the package or pull the Docker image automatically based on the configuration you provide in its settings.

If you prefer to install the server explicitly:

- **Global Install:** Run `npm install -g mcp-openapi-schema-explorer`. See **Usage Method 3** for how to configure your client to use this.
- **Local Install (for Development):** Clone the repository (`git clone ...`), install dependencies (`npm install`), and build (`npm run build`). See **Usage Method 4** for how to configure your client to use this.

## Usage Method 1: npx (Recommended)

This is the recommended method as it avoids global/local installation and ensures you use the latest published version.

### Client Configuration Entry (npx Method)

Add the following JSON object to the `mcpServers` section of your MCP client's configuration file (e.g., `claude_desktop_config.json`). This entry instructs the client on how to run the server using `npx`:

```json
{
  "mcpServers": {
    "My API Spec (npx)": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-openapi-schema-explorer@latest",
        "<path-or-url-to-spec>",
        "--output-format",
        "yaml"
      ],
      "env": {}
    }
  }
}
```

**Configuration Details:**

1.  **Replace `"My API Spec (npx)"`:** Choose a descriptive name for this server instance.
2.  **Replace `<path-or-url-to-spec>`:** Provide the **required** absolute local file path (e.g., `/path/to/your/api.yaml`) or the full remote URL (e.g., `https://petstore3.swagger.io/api/v3/openapi.json`).
3.  **(Optional)** Adjust the `--output-format` value (`yaml`, `json`, `json-minified`). Defaults to `json`.

## Usage Method 2: Docker

You can instruct your MCP client to run the server using the official Docker image: `kadykov/mcp-openapi-schema-explorer`.

### Client Configuration Entry (Docker Method)

- **Using a Remote URL:**

  ```json
  {
    "mcpServers": {
      "My API Spec (Docker Remote)": {
        "command": "docker",
        "args": [
          "run",
          "--rm",
          "-i",
          "kadykov/mcp-openapi-schema-explorer:latest",
          "<remote-url-to-spec>"
        ],
        "env": {}
      }
    }
  }
  ```

- **Using a Local File:** (Requires mounting the file into the container)
  ```json
  {
    "mcpServers": {
      "My API Spec (Docker Local)": {
        "command": "docker",
        "args": [
          "run",
          "--rm",
          "-i",
          "-v",
          "/full/host/path/to/spec.yaml:/spec/api.yaml",
          "kadykov/mcp-openapi-schema-explorer:latest",
          "/spec/api.yaml",
          "--output-format",
          "yaml"
        ],
        "env": {}
      }
    }
  }
  ```
  **Important:** Replace `/full/host/path/to/spec.yaml` with the correct absolute path on your host machine. The path `/spec/api.yaml` is the corresponding path inside the container.

## Usage Method 3: Global Installation (Less Common)

You can install the package globally, although `npx` is generally preferred.

```bash
# Run this command once in your terminal
npm install -g mcp-openapi-schema-explorer
```

### Client Configuration Entry (Global Install Method)

Add the following entry to your MCP client's configuration file. This assumes the `mcp-openapi-schema-explorer` command is accessible in the client's execution environment PATH.

```json
{
  "mcpServers": {
    "My API Spec (Global)": {
      "command": "mcp-openapi-schema-explorer",
      "args": ["<path-or-url-to-spec>", "--output-format", "yaml"],
      "env": {}
    }
  }
}
```

- **`command`:** Use the globally installed command name. You might need the full path if it's not in your system's PATH environment variable accessible by the MCP client.

## Usage Method 4: Local Development/Installation

This method is useful for development or running a locally modified version of the server.

### Setup Steps (Run once in your terminal)

1.  Clone the repository: `git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git`
2.  Navigate into the directory: `cd mcp-openapi-schema-explorer`
3.  Install dependencies: `npm install`
4.  Build the project: `npm run build` (or `just build`)

### Client Configuration Entry (Local Development Method)

Add the following entry to your MCP client's configuration file. This instructs the client to run the locally built server using `node`.

```json
{
  "mcpServers": {
    "My API Spec (Local Dev)": {
      "command": "node",
      "args": [
        "/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js",
        "<path-or-url-to-spec>",
        "--output-format",
        "yaml"
      ],

      "env": {}
    }
  }
}
```

**Important:** Replace `/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js` with the correct absolute path to the built `index.js` file in your cloned repository.

## Verification

After adding the server entry to your MCP client's configuration:

1.  The server should appear in the list of available MCP servers within your client (e.g., named "My API Spec (npx)" or whatever key you used). The server name might dynamically update based on the spec's `info.title` (e.g., "Schema Explorer for Petstore API").
2.  Test the connection by accessing a basic resource, for example (using your chosen server name):
    ```
    /mcp "My API Spec (npx)" access openapi://info
    ```

## Troubleshooting

Common issues and solutions:

1.  **Server Fails to Start:**
    - Verify the `<path-or-url-to-spec>` is correct, accessible, and properly quoted in the JSON configuration.
    - Ensure the specification file is a valid OpenAPI v3.0 or Swagger v2.0 document (JSON or YAML).
    - Check Node.js version (LTS recommended) if using `npx`, global, or local install.
    - Check Docker installation and permissions if using Docker.
    - For remote URLs, check network connectivity.
    - For Docker with local files, ensure the volume mount path (`-v` flag) is correct and the host path exists.
    - For Local Development, ensure the path to `dist/src/index.js` is correct and the project has been built (`npm run build`).
2.  **Resources Not Loading or Errors:**
    - Double-check the resource URI syntax (e.g., `openapi://paths`, `openapi://components/schemas/MySchema`). Remember that path segments in URIs need URL encoding (e.g., `/users/{id}` becomes `users%2F%7Bid%7D`).
    - Ensure the requested path, method, or component exists in the specification.

## Environment Variables

No environment variables are required for the server to operate.

## Additional Notes

- The server automatically handles loading specs from local files or remote URLs.
- Swagger v2.0 specifications are automatically converted to OpenAPI v3.0 internally.
- Internal references (`#/components/...`) are transformed into clickable MCP URIs (`openapi://components/...`).
- The server name displayed in the client might be dynamically generated from the specification's title.

## Support

If you encounter any issues:

1.  Check the project's main README for more details: [https://github.com/kadykov/mcp-openapi-schema-explorer#readme](https://github.com/kadykov/mcp-openapi-schema-explorer#readme)
2.  Submit an issue on GitHub: [https://github.com/kadykov/mcp-openapi-schema-explorer/issues](https://github.com/kadykov/mcp-openapi-schema-explorer/issues)

---

This guide provides instructions for adding the server to your MCP client using various execution methods. Refer to the main project README for comprehensive documentation on features and resource usage.

```
Page 1/2FirstPrevNextLast