#
tokens: 48169/50000 56/75 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/kadykov/mcp-openapi-schema-explorer?lines=true&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:
--------------------------------------------------------------------------------

```
1 | package-lock.json
2 | 
```

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

```
1 | dist/
2 | node_modules/
3 | local-docs/
4 | 
```

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

```json
 1 | {
 2 |   "printWidth": 100,
 3 |   "tabWidth": 2,
 4 |   "useTabs": false,
 5 |   "semi": true,
 6 |   "singleQuote": true,
 7 |   "quoteProps": "as-needed",
 8 |   "trailingComma": "es5",
 9 |   "bracketSpacing": true,
10 |   "arrowParens": "avoid",
11 |   "endOfLine": "lf"
12 | }
13 | 
```

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

```json
 1 | {
 2 |   "branches": ["main"],
 3 |   "plugins": [
 4 |     "@semantic-release/commit-analyzer",
 5 |     "@semantic-release/release-notes-generator",
 6 |     "@semantic-release/changelog",
 7 |     "@semantic-release/npm",
 8 |     [
 9 |       "@semantic-release/exec",
10 |       {
11 |         "prepareCmd": "node ./scripts/generate-version.js ${nextRelease.version}"
12 |       }
13 |     ],
14 |     [
15 |       "@semantic-release/git",
16 |       {
17 |         "assets": ["package.json", "package-lock.json", "CHANGELOG.md", "src/version.ts"],
18 |         "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
19 |       }
20 |     ],
21 |     [
22 |       "@codedependant/semantic-release-docker",
23 |       {
24 |         "dockerProject": "kadykov",
25 |         "dockerImage": "mcp-openapi-schema-explorer",
26 |         "dockerLogin": false
27 |       }
28 |     ],
29 |     "@semantic-release/github"
30 |   ]
31 | }
32 | 
```

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

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # vitepress build output
108 | **/.vitepress/dist
109 | 
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 | 
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 | 
116 | # Serverless directories
117 | .serverless/
118 | 
119 | # FuseBox cache
120 | .fusebox/
121 | 
122 | # DynamoDB Local files
123 | .dynamodb/
124 | 
125 | # TernJS port file
126 | .tern-port
127 | 
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 | 
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | 
138 | # Personal files
139 | kadykov-*
140 | 
141 | # Local documentation and examples
142 | local-docs/
143 | 
```

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

```markdown
  1 | <p align="center">
  2 |   <img src="assets/logo-400.png" alt="MCP OpenAPI Schema Explorer Logo" width="200">
  3 | </p>
  4 | 
  5 | # MCP OpenAPI Schema Explorer
  6 | 
  7 | [![npm version](https://badge.fury.io/js/mcp-openapi-schema-explorer.svg)](https://badge.fury.io/js/mcp-openapi-schema-explorer)
  8 | [![NPM Downloads](https://img.shields.io/npm/dw/mcp-openapi-schema-explorer)](https://badge.fury.io/js/mcp-openapi-schema-explorer)
  9 | [![Docker Pulls](https://img.shields.io/docker/pulls/kadykov/mcp-openapi-schema-explorer.svg)](https://hub.docker.com/r/kadykov/mcp-openapi-schema-explorer)
 10 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 11 | [![codecov](https://codecov.io/gh/kadykov/mcp-openapi-schema-explorer/graph/badge.svg?token=LFDOMJ6W4W)](https://codecov.io/gh/kadykov/mcp-openapi-schema-explorer)
 12 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/819a3ba3-ad54-4657-9241-648497e57d7b)
 13 | [![Lines of code](https://tokei.rs/b1/github/kadykov/mcp-openapi-schema-explorer?category=code)](https://github.com/kadykov/mcp-openapi-schema-explorer)
 14 | [![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)
 15 | 
 16 | An MCP (Model Context Protocol) server that provides token-efficient access to OpenAPI (v3.0) and Swagger (v2.0) specifications via **MCP Resources**.
 17 | 
 18 | ## Project Goal
 19 | 
 20 | 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.
 21 | 
 22 | 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.
 23 | 
 24 | ## Why MCP Resources?
 25 | 
 26 | The Model Context Protocol defines both **Resources** and **Tools**.
 27 | 
 28 | - **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).
 29 | - **Tools:** Represent executable actions or functions, often used by LLMs to perform tasks or interact with external systems.
 30 | 
 31 | 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.
 32 | 
 33 | For more details on MCP clients and their capabilities, see the [MCP Client Documentation](https://modelcontextprotocol.io/clients).
 34 | 
 35 | ## Installation
 36 | 
 37 | 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.
 38 | 
 39 | However, if you prefer or need to install the server explicitly, you have two options:
 40 | 
 41 | 1.  **Global Installation:** You can install the package globally using npm:
 42 | 
 43 |     ```bash
 44 |     npm install -g mcp-openapi-schema-explorer
 45 |     ```
 46 | 
 47 |     See **Method 3** below for how to configure your MCP client to use a globally installed server.
 48 | 
 49 | 2.  **Local Development/Installation:** You can clone the repository and build it locally:
 50 |     ```bash
 51 |     git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git
 52 |     cd mcp-openapi-schema-explorer
 53 |     npm install
 54 |     npm run build
 55 |     ```
 56 |     See **Method 4** below for how to configure your MCP client to run the server from your local build using `node`.
 57 | 
 58 | ## Adding the Server to your MCP Client
 59 | 
 60 | 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.
 61 | 
 62 | Below are the common methods for adding the server entry to your client's configuration.
 63 | 
 64 | ### Method 1: npx (Recommended)
 65 | 
 66 | Using `npx` is recommended as it avoids global/local installation and ensures the client uses the latest published version.
 67 | 
 68 | **Example Client Configuration Entry (npx Method):**
 69 | 
 70 | 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`:
 71 | 
 72 | ```json
 73 | {
 74 |   "mcpServers": {
 75 |     "My API Spec (npx)": {
 76 |       "command": "npx",
 77 |       "args": [
 78 |         "-y",
 79 |         "mcp-openapi-schema-explorer@latest",
 80 |         "<path-or-url-to-spec>",
 81 |         "--output-format",
 82 |         "yaml"
 83 |       ],
 84 |       "env": {}
 85 |     }
 86 |   }
 87 | }
 88 | ```
 89 | 
 90 | **Configuration Notes:**
 91 | 
 92 | - Replace `"My API Spec (npx)"` with a unique name for this server instance in your client.
 93 | - Replace `<path-or-url-to-spec>` with the absolute local file path or full remote URL of your specification.
 94 | - The `--output-format` is optional (`json`, `yaml`, `json-minified`), defaulting to `json`.
 95 | - To explore multiple specifications, add separate entries in `mcpServers`, each with a unique name and pointing to a different spec.
 96 | 
 97 | ### Method 2: Docker
 98 | 
 99 | You can instruct your MCP client to run the server using the official Docker image: `kadykov/mcp-openapi-schema-explorer`.
100 | 
101 | **Example Client Configuration Entries (Docker Method):**
102 | 
103 | 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`:
104 | 
105 | - **Remote URL:** Pass the URL directly to `docker run`.
106 | 
107 | - **Using a Remote URL:**
108 | 
109 |   ```json
110 |   {
111 |     "mcpServers": {
112 |       "My API Spec (Docker Remote)": {
113 |         "command": "docker",
114 |         "args": [
115 |           "run",
116 |           "--rm",
117 |           "-i",
118 |           "kadykov/mcp-openapi-schema-explorer:latest",
119 |           "<remote-url-to-spec>"
120 |         ],
121 |         "env": {}
122 |       }
123 |     }
124 |   }
125 |   ```
126 | 
127 | - **Using a Local File:** (Requires mounting the file into the container)
128 |   ```json
129 |   {
130 |     "mcpServers": {
131 |       "My API Spec (Docker Local)": {
132 |         "command": "docker",
133 |         "args": [
134 |           "run",
135 |           "--rm",
136 |           "-i",
137 |           "-v",
138 |           "/full/host/path/to/spec.yaml:/spec/api.yaml",
139 |           "kadykov/mcp-openapi-schema-explorer:latest",
140 |           "/spec/api.yaml",
141 |           "--output-format",
142 |           "yaml"
143 |         ],
144 |         "env": {}
145 |       }
146 |     }
147 |   }
148 |   ```
149 |   **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.
150 | 
151 | ### Method 3: Global Installation (Less Common)
152 | 
153 | If you have installed the package globally using `npm install -g`, you can configure your client to run it directly.
154 | 
155 | ```bash
156 | # Run this command once in your terminal
157 | npm install -g mcp-openapi-schema-explorer
158 | ```
159 | 
160 | **Example Client Configuration Entry (Global Install Method):**
161 | 
162 | 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.
163 | 
164 | ```json
165 | {
166 |   "mcpServers": {
167 |     "My API Spec (Global)": {
168 |       "command": "mcp-openapi-schema-explorer",
169 |       "args": ["<path-or-url-to-spec>", "--output-format", "yaml"],
170 |       "env": {}
171 |     }
172 |   }
173 | }
174 | ```
175 | 
176 | - Ensure the `command` (`mcp-openapi-schema-explorer`) is accessible in the PATH environment variable used by your MCP client.
177 | 
178 | ### Method 4: Local Development/Installation
179 | 
180 | This method is useful if you have cloned the repository locally for development or to run a modified version.
181 | 
182 | **Setup Steps (Run once in your terminal):**
183 | 
184 | 1.  Clone the repository: `git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git`
185 | 2.  Navigate into the directory: `cd mcp-openapi-schema-explorer`
186 | 3.  Install dependencies: `npm install`
187 | 4.  Build the project: `npm run build` (or `just build`)
188 | 
189 | **Example Client Configuration Entry (Local Development Method):**
190 | 
191 | Add the following entry to your MCP client's configuration file. This instructs the client to run the locally built server using `node`.
192 | 
193 | ```json
194 | {
195 |   "mcpServers": {
196 |     "My API Spec (Local Dev)": {
197 |       "command": "node",
198 |       "args": [
199 |         "/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js",
200 |         "<path-or-url-to-spec>",
201 |         "--output-format",
202 |         "yaml"
203 |       ],
204 | 
205 |       "env": {}
206 |     }
207 |   }
208 | }
209 | ```
210 | 
211 | **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.
212 | 
213 | ## Features
214 | 
215 | - **MCP Resource Access:** Explore OpenAPI specs via intuitive URIs (`openapi://info`, `openapi://paths/...`, `openapi://components/...`).
216 | - **OpenAPI v3.0 & Swagger v2.0 Support:** Loads both formats, automatically converting v2.0 to v3.0.
217 | - **Local & Remote Files:** Load specs from local file paths or HTTP/HTTPS URLs.
218 | - **Token-Efficient:** Designed to minimize token usage for LLMs by providing structured access.
219 | - **Multiple Output Formats:** Get detailed views in JSON (default), YAML, or minified JSON (`--output-format`).
220 | - **Dynamic Server Name:** Server name in MCP clients reflects the `info.title` from the loaded spec.
221 | - **Reference Transformation:** Internal `$ref`s (`#/components/...`) are transformed into clickable MCP URIs.
222 | 
223 | ## Available MCP Resources
224 | 
225 | This server exposes the following MCP resource templates for exploring the OpenAPI specification.
226 | 
227 | **Understanding Multi-Value Parameters (`*`)**
228 | 
229 | 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.
230 | 
231 | **Resource Templates:**
232 | 
233 | - **`openapi://{field}`**
234 |   - **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.
235 |   - **Example:** `openapi://info`
236 |   - **Output:** `text/plain` list for `paths` and `components`; configured format (JSON/YAML/minified JSON) for other fields.
237 |   - **Completions:** Provides dynamic suggestions for `{field}` based on the actual top-level keys found in the loaded spec.
238 | 
239 | - **`openapi://paths/{path}`**
240 |   - **Description:** Lists the available HTTP methods (operations) for a specific API path.
241 |   - **Parameter:** `{path}` - The API path string. **Must be URL-encoded** (e.g., `/users/{id}` becomes `users%2F%7Bid%7D`).
242 |   - **Example:** `openapi://paths/users%2F%7Bid%7D`
243 |   - **Output:** `text/plain` list of methods.
244 |   - **Completions:** Provides dynamic suggestions for `{path}` based on the paths found in the loaded spec (URL-encoded).
245 | 
246 | - **`openapi://paths/{path}/{method*}`**
247 |   - **Description:** Gets the detailed specification for one or more operations (HTTP methods) on a specific API path.
248 |   - **Parameters:**
249 |     - `{path}` - The API path string. **Must be URL-encoded**.
250 |     - `{method*}` - One or more HTTP methods (e.g., `get`, `post`, `get,post`). Case-insensitive.
251 |   - **Example (Single):** `openapi://paths/users%2F%7Bid%7D/get`
252 |   - **Example (Multiple):** `openapi://paths/users%2F%7Bid%7D/get,post`
253 |   - **Output:** Configured format (JSON/YAML/minified JSON).
254 |   - **Completions:** Provides dynamic suggestions for `{path}`. Provides static suggestions for `{method*}` (common HTTP verbs like GET, POST, PUT, DELETE, etc.).
255 | 
256 | - **`openapi://components/{type}`**
257 |   - **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.
258 |   - **Example:** `openapi://components/schemas`
259 |   - **Output:** `text/plain` list of component names with descriptions.
260 |   - **Completions:** Provides dynamic suggestions for `{type}` based on the component types found in the loaded spec.
261 | 
262 | - **`openapi://components/{type}/{name*}`**
263 |   - **Description:** Gets the detailed specification for one or more named components of a specific type.
264 |   - **Parameters:**
265 |     - `{type}` - The component type.
266 |     - `{name*}` - One or more component names (e.g., `User`, `Order`, `User,Order`). Case-sensitive.
267 |   - **Example (Single):** `openapi://components/schemas/User`
268 |   - **Example (Multiple):** `openapi://components/schemas/User,Order`
269 |   - **Output:** Configured format (JSON/YAML/minified JSON).
270 |   - **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.
271 | 
272 | ## Contributing
273 | 
274 | Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on setting up the development environment, running tests, and submitting changes.
275 | 
276 | ## Releases
277 | 
278 | 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/).
279 | 
280 | ## Future Plans
281 | 
282 | (Future plans to be determined)
283 | 
```

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

```markdown
 1 | # Contributing to MCP OpenAPI Schema Explorer
 2 | 
 3 | Thank you for considering contributing to this project! We welcome improvements and bug fixes.
 4 | 
 5 | ## Getting Started
 6 | 
 7 | - **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.
 8 | - **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.
 9 | 
10 | ## Development Workflow
11 | 
12 | 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:
13 | 
14 | - `just install`: Install dependencies (`npm install`).
15 | - `just format`: Format code using Prettier.
16 | - `just lint`: Check code for linting errors using ESLint.
17 | - `just build`: Compile TypeScript code (`npx tsc`).
18 | - `just test`: Run unit and end-to-end tests using Jest.
19 | - `just test-coverage`: Run tests and generate a coverage report.
20 | - `just security`: Run security checks (npm audit, license check).
21 | - `just all`: Run format, lint, build, test-coverage, and security checks sequentially.
22 | 
23 | Please ensure `just all` passes before submitting a pull request.
24 | 
25 | ## Code Style
26 | 
27 | - **Formatting:** We use [Prettier](https://prettier.io/) for automatic code formatting. Please run `just format` before committing.
28 | - **Linting:** We use [ESLint](https://eslint.org/) for code analysis. Please run `just lint` to check for issues.
29 | 
30 | ## Commit Messages
31 | 
32 | 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.
33 | 
34 | Common commit types include:
35 | 
36 | - `feat`: A new feature
37 | - `fix`: A bug fix
38 | - `docs`: Documentation only changes
39 | - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
40 | - `refactor`: A code change that neither fixes a bug nor adds a feature
41 | - `perf`: A code change that improves performance
42 | - `test`: Adding missing tests or correcting existing tests
43 | - `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
44 | - `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
45 | - `chore`: Other changes that don't modify src or test files
46 | 
47 | Example: `feat: add support for YAML output format`
48 | Example: `fix: correct handling of remote URL loading errors`
49 | Example: `docs: update README with client configuration examples`
50 | 
51 | ## Cline & Memory Bank
52 | 
53 | 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.
54 | 
55 | 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).
56 | 
57 | ## Submitting Changes
58 | 
59 | 1.  Fork the repository.
60 | 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`).
61 | 3.  Make your changes.
62 | 4.  Ensure all checks pass (`just all`).
63 | 5.  Commit your changes using the Conventional Commits format.
64 | 6.  Push your branch to your fork (`git push origin feat/my-new-feature`).
65 | 7.  Open a pull request against the `main` branch of the original repository.
66 | 
67 | Thank you for your contribution!
68 | 
```

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

```dockerfile
1 | FROM node:22-alpine3.21
2 | 
3 | WORKDIR /workspaces/mcp-openapi-schema-explorer
4 | 
```

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

```json
1 | {
2 |   "openapi": "3.0.0",
3 |   "info": {
4 |     "title": "Empty API",
5 |     "version": "1.0.0"
6 |   },
7 |   "paths": {}
8 | }
9 | 
```

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

```json
1 | {
2 |   "extends": "./tsconfig.json",
3 |   "include": ["src/**/*", "test/**/*"],
4 |   "exclude": ["node_modules", "dist", "local-docs"]
5 | }
6 | 
```

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

```typescript
1 | // Auto-generated by scripts/generate-version.js during semantic-release prepare step
2 | // Do not edit this file manually.
3 | 
4 | export const VERSION = '1.3.0';
5 | 
```

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

```typescript
 1 | import { OpenAPI } from 'openapi-types';
 2 | import type { TransformContext } from './services/reference-transform.js';
 3 | 
 4 | /** Common HTTP methods used in OpenAPI specs */
 5 | export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch';
 6 | 
 7 | /** Interface for spec loader */
 8 | export interface SpecLoaderService {
 9 |   getSpec(): Promise<OpenAPI.Document>;
10 |   getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document>;
11 | }
12 | 
13 | // Re-export transform types
14 | export type { TransformContext };
15 | 
16 | // Re-export OpenAPI types
17 | export type { OpenAPI };
18 | 
```

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

```yaml
 1 | version: 2
 2 | updates:
 3 |   - package-ecosystem: 'npm'
 4 |     directory: '/'
 5 |     schedule:
 6 |       interval: 'weekly'
 7 |     open-pull-requests-limit: 10
 8 |     groups:
 9 |       # Group all development dependencies together
10 |       dev-dependencies:
11 |         dependency-type: 'development'
12 | 
13 |       # Group production dependencies together
14 |       production-dependencies:
15 |         dependency-type: 'production'
16 | 
17 |   - package-ecosystem: 'github-actions'
18 |     directory: '/'
19 |     schedule:
20 |       interval: 'weekly'
21 |     open-pull-requests-limit: 10
22 |     groups:
23 |       github-actions:
24 |         patterns:
25 |           - '*'
26 | 
```

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

```json
 1 | {
 2 |   "name": "MCP OpenAPI Schema Explorer",
 3 |   "dockerFile": "Dockerfile", // Updated path
 4 |   "features": {
 5 |     "ghcr.io/devcontainers/features/common-utils:2": {
 6 |       "username": "vscode"
 7 |     },
 8 |     "ghcr.io/guiyomh/features/just:0": {}
 9 |   },
10 |   "remoteUser": "vscode",
11 |   "postCreateCommand": "just install",
12 |   "customizations": {
13 |     "vscode": {
14 |       "extensions": [
15 |         "ms-azuretools.vscode-docker",
16 |         "GitHub.vscode-github-actions",
17 |         "saoudrizwan.claude-dev",
18 |         "dbaeumer.vscode-eslint",
19 |         "rvest.vs-code-prettier-eslint",
20 |         "ms-vscode.vscode-typescript-next"
21 |       ]
22 |     }
23 |   }
24 | }
25 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "node",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "outDir": "dist",
11 |     "declaration": true,
12 |     "sourceMap": true,
13 |     "rootDir": ".",
14 |     "lib": ["ES2020"],
15 |     "noImplicitAny": true,
16 |     "noImplicitThis": true,
17 |     "noUnusedLocals": true,
18 |     "noUnusedParameters": true,
19 |     "noImplicitReturns": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "types": ["jest", "node"]
22 |   },
23 |   "include": ["src/**/*"],
24 |   "exclude": ["node_modules", "dist", "local-docs", "test"]
25 | }
26 | 
```

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

```javascript
 1 | /** @type {import('jest').Config} */
 2 | export default {
 3 |   testPathIgnorePatterns: ['/node_modules/', '/local-docs/', '/dist/'],
 4 |   preset: 'ts-jest',
 5 |   testEnvironment: 'node',
 6 |   extensionsToTreatAsEsm: ['.ts'],
 7 |   moduleNameMapper: {
 8 |     '^(\\.{1,2}/.*)\\.js$': '$1',
 9 |   },
10 |   transform: {
11 |     '^.+\\.tsx?$': [
12 |       'ts-jest',
13 |       {
14 |         useESM: true,
15 |         tsconfig: 'tsconfig.test.json',
16 |       },
17 |     ],
18 |   },
19 |   setupFilesAfterEnv: ['./test/setup.ts'],
20 |   reporters: [
21 |     [
22 |       'jest-silent-reporter',
23 |       {
24 |         useDots: true,
25 |         showPaths: true,
26 |         showInlineStatus: true,
27 |         showWarnings: true,
28 |       },
29 |     ],
30 |   ],
31 |   verbose: true,
32 | };
33 | 
```

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

```dockerfile
 1 | # Stage 1: Builder
 2 | FROM node:22-alpine AS builder
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy necessary files for installation and build
 7 | COPY package.json package-lock.json ./
 8 | COPY tsconfig.json ./
 9 | COPY src ./src
10 | 
11 | # Install all dependencies (including devDependencies needed for build)
12 | RUN npm ci
13 | 
14 | # Build the project
15 | RUN npm run build
16 | 
17 | # Stage 2: Release
18 | FROM node:22-alpine AS release
19 | 
20 | WORKDIR /app
21 | 
22 | # Copy only necessary files from the builder stage
23 | COPY --from=builder /app/package.json ./package.json
24 | COPY --from=builder /app/package-lock.json ./package-lock.json
25 | COPY --from=builder /app/dist ./dist
26 | 
27 | # Install only production dependencies
28 | RUN npm ci --omit=dev --ignore-scripts
29 | 
30 | # Set the entrypoint to run the compiled server
31 | # Corrected path based on tsconfig.json ("rootDir": ".", "outDir": "dist")
32 | ENTRYPOINT ["node", "dist/src/index.js"]
33 | 
```

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

```typescript
 1 | import fs from 'node:fs/promises';
 2 | import path from 'path';
 3 | 
 4 | // Extend timeout for E2E tests
 5 | jest.setTimeout(30000);
 6 | 
 7 | // Clean up any previous test artifacts
 8 | beforeAll(async () => {
 9 |   // Create required directories if they don't exist
10 |   const dirs = ['dist', 'dist/src', 'test/fixtures'];
11 | 
12 |   for (const dir of dirs) {
13 |     try {
14 |       await fs.mkdir(dir, { recursive: true });
15 |     } catch (error) {
16 |       // Ignore if directory already exists
17 |       if ((error as { code?: string }).code !== 'EEXIST') {
18 |         throw error;
19 |       }
20 |     }
21 |   }
22 | 
23 |   // Verify sample OpenAPI spec exists
24 |   const specPath = path.resolve(process.cwd(), 'test/fixtures/sample-api.json');
25 |   try {
26 |     await fs.access(specPath);
27 |   } catch {
28 |     throw new Error(`Sample OpenAPI spec not found at ${specPath}`);
29 |   }
30 | });
31 | 
32 | // Custom matchers could be added here if needed
33 | 
```

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

```json
 1 | {
 2 |   "swagger": "2.0",
 3 |   "info": {
 4 |     "title": "Simple Swagger 2.0 API",
 5 |     "version": "1.0.0",
 6 |     "description": "A simple API definition in Swagger 2.0 format for testing conversion."
 7 |   },
 8 |   "host": "localhost:3000",
 9 |   "basePath": "/v2",
10 |   "schemes": ["http"],
11 |   "paths": {
12 |     "/ping": {
13 |       "get": {
14 |         "summary": "Check service health",
15 |         "description": "Returns a simple pong message.",
16 |         "produces": ["application/json"],
17 |         "responses": {
18 |           "200": {
19 |             "description": "Successful response",
20 |             "schema": {
21 |               "$ref": "#/definitions/Pong"
22 |             }
23 |           }
24 |         }
25 |       }
26 |     }
27 |   },
28 |   "definitions": {
29 |     "Pong": {
30 |       "type": "object",
31 |       "properties": {
32 |         "message": {
33 |           "type": "string",
34 |           "example": "pong"
35 |         }
36 |       }
37 |     }
38 |   }
39 | }
40 | 
```

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

```typescript
 1 | /**
 2 |  * Configuration management for the OpenAPI Explorer MCP server
 3 |  */
 4 | 
 5 | import { OutputFormat } from './services/formatters.js';
 6 | 
 7 | /** Server configuration */
 8 | export interface ServerConfig {
 9 |   /** Path to OpenAPI specification file */
10 |   specPath: string;
11 |   /** Output format for responses */
12 |   outputFormat: OutputFormat;
13 | }
14 | 
15 | /** Load server configuration from command line arguments */
16 | export function loadConfig(specPath?: string, options?: { outputFormat?: string }): ServerConfig {
17 |   if (!specPath) {
18 |     throw new Error(
19 |       'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec> [--output-format json|yaml]'
20 |     );
21 |   }
22 | 
23 |   const format = options?.outputFormat || 'json';
24 |   if (format !== 'json' && format !== 'yaml' && format !== 'json-minified') {
25 |     throw new Error('Invalid output format. Supported formats: json, yaml, json-minified');
26 |   }
27 | 
28 |   return {
29 |     specPath,
30 |     // Cast is safe here due to the validation above
31 |     outputFormat: format as OutputFormat,
32 |   };
33 | }
34 | 
```

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

```json
 1 | {
 2 |   "openapi": "3.0.0",
 3 |   "info": {
 4 |     "title": "Multi Component Type Test API",
 5 |     "version": "1.0.0"
 6 |   },
 7 |   "paths": {
 8 |     "/ping": {
 9 |       "get": {
10 |         "summary": "Ping",
11 |         "operationId": "ping",
12 |         "responses": {
13 |           "200": {
14 |             "description": "OK",
15 |             "content": {
16 |               "application/json": {
17 |                 "schema": {
18 |                   "$ref": "#/components/schemas/Pong"
19 |                 }
20 |               }
21 |             }
22 |           }
23 |         },
24 |         "parameters": [
25 |           {
26 |             "$ref": "#/components/parameters/TraceId"
27 |           }
28 |         ]
29 |       }
30 |     }
31 |   },
32 |   "components": {
33 |     "schemas": {
34 |       "Pong": {
35 |         "type": "object",
36 |         "properties": {
37 |           "message": {
38 |             "type": "string"
39 |           }
40 |         }
41 |       }
42 |     },
43 |     "parameters": {
44 |       "TraceId": {
45 |         "name": "X-Trace-ID",
46 |         "in": "header",
47 |         "required": false,
48 |         "schema": {
49 |           "type": "string",
50 |           "format": "uuid"
51 |         }
52 |       }
53 |     }
54 |   }
55 | }
56 | 
```

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

```json
 1 | {
 2 |   "openapi": "3.0.3",
 3 |   "info": {
 4 |     "title": "Path Testing API",
 5 |     "version": "1.0.0"
 6 |   },
 7 |   "paths": {
 8 |     "/project/tasks/{taskId}": {
 9 |       "get": {
10 |         "summary": "Get task details",
11 |         "parameters": [
12 |           {
13 |             "name": "taskId",
14 |             "in": "path",
15 |             "required": true,
16 |             "schema": { "type": "string" }
17 |           }
18 |         ],
19 |         "responses": {
20 |           "200": {
21 |             "description": "Task details"
22 |           }
23 |         }
24 |       }
25 |     },
26 |     "/article/{articleId}/comment/{commentId}": {
27 |       "get": {
28 |         "summary": "Get comment on article",
29 |         "parameters": [
30 |           {
31 |             "name": "articleId",
32 |             "in": "path",
33 |             "required": true,
34 |             "schema": { "type": "string" }
35 |           },
36 |           {
37 |             "name": "commentId",
38 |             "in": "path",
39 |             "required": true,
40 |             "schema": { "type": "string" }
41 |           }
42 |         ],
43 |         "responses": {
44 |           "200": {
45 |             "description": "Comment details"
46 |           }
47 |         }
48 |       }
49 |     },
50 |     "/sub/sub/sub/sub/folded/entrypoint": {
51 |       "post": {
52 |         "summary": "Deeply nested endpoint",
53 |         "responses": {
54 |           "201": {
55 |             "description": "Created"
56 |           }
57 |         }
58 |       }
59 |     }
60 |   }
61 | }
62 | 
```

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

```typescript
 1 | import { loadConfig } from '../../../src/config.js';
 2 | 
 3 | describe('Config', () => {
 4 |   describe('loadConfig', () => {
 5 |     it('returns valid configuration with default format when only path is provided', () => {
 6 |       const config = loadConfig('/path/to/spec.json');
 7 |       expect(config).toEqual({
 8 |         specPath: '/path/to/spec.json',
 9 |         outputFormat: 'json',
10 |       });
11 |     });
12 | 
13 |     it('returns valid configuration when path and format are provided', () => {
14 |       const config = loadConfig('/path/to/spec.json', { outputFormat: 'yaml' });
15 |       expect(config).toEqual({
16 |         specPath: '/path/to/spec.json',
17 |         outputFormat: 'yaml',
18 |       });
19 |     });
20 | 
21 |     it('throws error when invalid format is provided', () => {
22 |       expect(() => loadConfig('/path/to/spec.json', { outputFormat: 'invalid' })).toThrow(
23 |         'Invalid output format. Supported formats: json, yaml'
24 |       );
25 |     });
26 | 
27 |     it('throws error when path is not provided', () => {
28 |       expect(() => loadConfig()).toThrow(
29 |         'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec>'
30 |       );
31 |     });
32 | 
33 |     it('throws error when path is empty string', () => {
34 |       expect(() => loadConfig('')).toThrow(
35 |         'OpenAPI spec path is required. Usage: npx mcp-openapi-schema-explorer <path-to-spec>'
36 |       );
37 |     });
38 |   });
39 | });
40 | 
```

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

```typescript
 1 | import { OpenAPIV3 } from 'openapi-types';
 2 | 
 3 | export interface EndpointSuccessResponse {
 4 |   method: string;
 5 |   path: string;
 6 |   parameters?: OpenAPIV3.ParameterObject[];
 7 |   requestBody?: OpenAPIV3.RequestBodyObject;
 8 |   responses: { [key: string]: OpenAPIV3.ResponseObject };
 9 | }
10 | 
11 | export interface EndpointErrorResponse {
12 |   method: string;
13 |   path: string;
14 |   error: string;
15 | }
16 | 
17 | export type EndpointResponse = EndpointSuccessResponse | EndpointErrorResponse;
18 | 
19 | export function isEndpointErrorResponse(obj: unknown): obj is EndpointErrorResponse {
20 |   return (
21 |     typeof obj === 'object' &&
22 |     obj !== null &&
23 |     typeof (obj as EndpointErrorResponse).error === 'string'
24 |   );
25 | }
26 | 
27 | export interface ResourceContent {
28 |   uri: string;
29 |   mimeType: string;
30 |   text: string;
31 | }
32 | 
33 | export interface ResourceResponse {
34 |   contents: ResourceContent[];
35 | }
36 | 
37 | // Types for Schema Resource E2E tests
38 | export type SchemaSuccessResponse = OpenAPIV3.SchemaObject; // Use type alias
39 | 
40 | export interface SchemaErrorResponse {
41 |   name: string;
42 |   error: string;
43 | }
44 | 
45 | export type SchemaResponse = SchemaSuccessResponse | SchemaErrorResponse;
46 | 
47 | export function isSchemaErrorResponse(obj: unknown): obj is SchemaErrorResponse {
48 |   return (
49 |     typeof obj === 'object' &&
50 |     obj !== null &&
51 |     typeof (obj as SchemaErrorResponse).name === 'string' && // Check for name property
52 |     typeof (obj as SchemaErrorResponse).error === 'string'
53 |   );
54 | }
55 | 
```

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

```json
 1 | {
 2 |   "openapi": "3.0.3",
 3 |   "info": {
 4 |     "title": "Sample API",
 5 |     "version": "1.0.0"
 6 |   },
 7 |   "paths": {
 8 |     "/users": {
 9 |       "get": {
10 |         "summary": "List users",
11 |         "operationId": "listUsers",
12 |         "responses": {
13 |           "200": {
14 |             "description": "List of users",
15 |             "content": {
16 |               "application/json": {
17 |                 "schema": {
18 |                   "$ref": "#/components/schemas/UserList"
19 |                 }
20 |               }
21 |             }
22 |           }
23 |         }
24 |       }
25 |     }
26 |   },
27 |   "components": {
28 |     "schemas": {
29 |       "User": {
30 |         "type": "object",
31 |         "required": ["id", "email"],
32 |         "properties": {
33 |           "id": {
34 |             "type": "integer",
35 |             "format": "int64"
36 |           },
37 |           "email": {
38 |             "type": "string",
39 |             "format": "email"
40 |           },
41 |           "name": {
42 |             "type": "string"
43 |           },
44 |           "status": {
45 |             "type": "string",
46 |             "enum": ["active", "inactive"]
47 |           }
48 |         }
49 |       },
50 |       "UserList": {
51 |         "type": "object",
52 |         "required": ["users"],
53 |         "properties": {
54 |           "users": {
55 |             "type": "array",
56 |             "items": {
57 |               "$ref": "#/components/schemas/User"
58 |             }
59 |           },
60 |           "total": {
61 |             "type": "integer",
62 |             "format": "int32"
63 |           }
64 |         }
65 |       }
66 |     }
67 |   }
68 | }
69 | 
```

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

```typescript
 1 | import { dump as yamlDump } from 'js-yaml';
 2 | 
 3 | /**
 4 |  * Supported output formats
 5 |  */
 6 | export type OutputFormat = 'json' | 'yaml' | 'json-minified';
 7 | 
 8 | /**
 9 |  * Interface for formatters that handle different output formats
10 |  */
11 | export interface IFormatter {
12 |   format(data: unknown): string;
13 |   getMimeType(): string;
14 | }
15 | 
16 | /**
17 |  * JSON formatter with pretty printing
18 |  */
19 | export class JsonFormatter implements IFormatter {
20 |   format(data: unknown): string {
21 |     return JSON.stringify(data, null, 2);
22 |   }
23 | 
24 |   getMimeType(): string {
25 |     return 'application/json';
26 |   }
27 | }
28 | 
29 | /**
30 |  * Formats data as minified JSON.
31 |  */
32 | export class MinifiedJsonFormatter implements IFormatter {
33 |   format(data: unknown): string {
34 |     return JSON.stringify(data);
35 |   }
36 | 
37 |   getMimeType(): string {
38 |     return 'application/json';
39 |   }
40 | }
41 | 
42 | /**
43 |  * YAML formatter using js-yaml library
44 |  */
45 | export class YamlFormatter implements IFormatter {
46 |   format(data: unknown): string {
47 |     return yamlDump(data, {
48 |       indent: 2,
49 |       lineWidth: -1, // Don't wrap long lines
50 |       noRefs: true, // Don't use references
51 |     });
52 |   }
53 | 
54 |   getMimeType(): string {
55 |     return 'text/yaml';
56 |   }
57 | }
58 | 
59 | /**
60 |  * Creates a formatter instance based on format name
61 |  */
62 | export function createFormatter(format: OutputFormat): IFormatter {
63 |   switch (format) {
64 |     case 'json':
65 |       return new JsonFormatter();
66 |     case 'yaml':
67 |       return new YamlFormatter();
68 |     case 'json-minified':
69 |       return new MinifiedJsonFormatter();
70 |   }
71 | }
72 | 
```

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

```markdown
 1 | # OpenAPI Schema Explorer MCP Server
 2 | 
 3 | ## Project Overview
 4 | 
 5 | Building an MCP server that allows exploration of OpenAPI specification files in a selective, token-efficient manner.
 6 | 
 7 | ## Core Requirements (✓)
 8 | 
 9 | 1. Allow loading and exploring OpenAPI spec files without consuming excessive LLM tokens
10 |    - Token-efficient plain text listings
11 |    - JSON format for detailed views
12 |    - Error handling without excessive details
13 | 2. Expose key parts of OpenAPI specs through MCP resources
14 |    - Endpoint details with full operation info
15 |    - Multiple values support for batch operations
16 |    - Resource completion support (✓)
17 | 3. Support local OpenAPI specification files
18 |    - OpenAPI v3.0 support
19 |    - Local file loading
20 |    - Error handling for invalid specs
21 | 4. Provide test coverage with Jest
22 |    - Full unit test coverage
23 |    - E2E test coverage
24 |    - Type-safe test implementation
25 | 
26 | ## Future Extensions (Out of Scope)
27 | 
28 | - Remote OpenAPI specs
29 | - Different specification formats
30 | - Search functionality
31 | 
32 | ## Technical Constraints (✓)
33 | 
34 | - Built with TypeScript MCP SDK
35 | - Published to npm
36 | - Comprehensive test coverage
37 | - Optimized for testability and extensibility
38 | 
39 | ## Project Boundaries
40 | 
41 | - Initial focus on local OpenAPI spec files only
42 | - Focus on most important parts: endpoints and type definitions
43 | - Real-time spec updates are out of scope (server restart required for updates)
44 | 
45 | ## Next Optimizations
46 | 
47 | - YAML output format for improved token efficiency
48 | - $ref resolution using URI links
49 | - Parameter validation implementation
50 | - Enhanced documentation support
51 | 
```

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

```typescript
 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 3 | // import path from 'path';
 4 | 
 5 | // Export the interface
 6 | export interface McpTestContext {
 7 |   client: Client;
 8 |   transport: StdioClientTransport;
 9 |   cleanup: () => Promise<void>;
10 | }
11 | 
12 | interface StartServerOptions {
13 |   outputFormat?: 'json' | 'yaml' | 'json-minified';
14 | }
15 | 
16 | /**
17 |  * Start MCP server with test configuration
18 |  */
19 | export async function startMcpServer(
20 |   specPath: string,
21 |   options: StartServerOptions = {}
22 | ): Promise<McpTestContext> {
23 |   let transport: StdioClientTransport | undefined;
24 |   let client: Client | undefined;
25 | 
26 |   try {
27 |     // Initialize transport with spec path as argument
28 |     transport = new StdioClientTransport({
29 |       command: 'node',
30 |       args: [
31 |         'dist/src/index.js',
32 |         // path.resolve(specPath),
33 |         specPath,
34 |         ...(options.outputFormat ? ['--output-format', options.outputFormat] : []),
35 |       ],
36 |       stderr: 'inherit', // Pass through server errors normally - they're part of E2E testing
37 |     });
38 | 
39 |     // Initialize client
40 |     client = new Client({
41 |       name: 'test-client',
42 |       version: '1.0.0',
43 |     });
44 | 
45 |     await client.connect(transport);
46 | 
47 |     // Create cleanup function
48 |     const cleanup = async (): Promise<void> => {
49 |       if (transport) {
50 |         await transport.close();
51 |       }
52 |     };
53 | 
54 |     return {
55 |       client,
56 |       transport,
57 |       cleanup,
58 |     };
59 |   } catch (error) {
60 |     // Clean up on error
61 |     if (transport) {
62 |       await transport.close();
63 |     }
64 |     throw error;
65 |   }
66 | }
67 | 
```

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

```javascript
 1 | // scripts/generate-version.js
 2 | // Purpose: Writes the release version provided by semantic-release to src/version.ts
 3 | // Called by: @semantic-release/exec during the 'prepare' step
 4 | 
 5 | import fs from 'fs'; // Use import
 6 | import path from 'path'; // Use import
 7 | import { fileURLToPath } from 'url'; // Needed to convert import.meta.url
 8 | 
 9 | // Get version from the command line argument passed by semantic-release exec
10 | // process is globally available in ESM
11 | const version = process.argv[2];
12 | 
13 | if (!version) {
14 |   console.error('Error: No version argument provided to generate-version.js!');
15 |   process.exit(1);
16 | }
17 | 
18 | // Basic check for semantic version format (adjust regex if needed)
19 | if (!/^\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/.test(version)) {
20 |   console.error(`Error: Invalid version format received: "${version}"`);
21 |   process.exit(1);
22 | }
23 | 
24 | const content = `// Auto-generated by scripts/generate-version.js during semantic-release prepare step
25 | // Do not edit this file manually.
26 | 
27 | export const VERSION = '${version}';
28 | `;
29 | 
30 | // Derive the directory path in ESM
31 | const __filename = fileURLToPath(import.meta.url);
32 | const __dirname = path.dirname(__filename);
33 | 
34 | // Construct the absolute path to src/version.ts
35 | const filePath = path.join(__dirname, '..', 'src', 'version.ts');
36 | const fileDir = path.dirname(filePath);
37 | 
38 | try {
39 |   // Ensure the src directory exists (though it should)
40 |   if (!fs.existsSync(fileDir)) {
41 |     fs.mkdirSync(fileDir, { recursive: true });
42 |   }
43 |   // Write the version file
44 |   fs.writeFileSync(filePath, content, { encoding: 'utf-8' });
45 |   console.log(`Successfully wrote version ${version} to ${filePath}`);
46 | } catch (error) {
47 |   console.error(`Error writing version file to ${filePath}:`, error);
48 |   process.exit(1);
49 | }
50 | 
```

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

```typescript
 1 | import { jest } from '@jest/globals';
 2 | 
 3 | // Define a type for the console.error function signature using unknown
 4 | type ConsoleErrorType = (message?: unknown, ...optionalParams: unknown[]) => void;
 5 | 
 6 | /**
 7 |  * Temporarily suppresses console.error messages that match the expected pattern
 8 |  * during the execution of a provided function (typically an async test operation).
 9 |  * Unexpected console.error messages will still be logged.
10 |  *
11 |  * @param expectedMessage The exact string or a RegExp to match against the console.error message.
12 |  * @param fnToRun The async function to execute while suppression is active.
13 |  * @returns The result of the fnToRun function.
14 |  */
15 | export async function suppressExpectedConsoleError<T>(
16 |   expectedMessage: string | RegExp,
17 |   fnToRun: () => T | Promise<T>
18 | ): Promise<T> {
19 |   // Use the defined type for the original function
20 |   const originalConsoleError: ConsoleErrorType = console.error;
21 | 
22 |   // Use unknown in the mock implementation signature
23 |   const consoleErrorSpy = jest
24 |     .spyOn(console, 'error')
25 |     .mockImplementation((message?: unknown, ...args: unknown[]) => {
26 |       const messageStr = String(message); // String conversion handles unknown
27 |       const shouldSuppress =
28 |         expectedMessage instanceof RegExp
29 |           ? expectedMessage.test(messageStr)
30 |           : messageStr === expectedMessage;
31 | 
32 |       if (!shouldSuppress) {
33 |         // Call the original implementation for unexpected errors
34 |         // We still need to handle the potential type mismatch for the spread
35 |         // Using Function.prototype.apply is a safer way to call with dynamic args
36 |         Function.prototype.apply.call(originalConsoleError, console, [message, ...args]);
37 |       }
38 |       // If it matches, do nothing (suppress)
39 |     });
40 | 
41 |   try {
42 |     // Execute the function that is expected to trigger the error log
43 |     return await fnToRun();
44 |   } finally {
45 |     // Restore the original console.error
46 |     consoleErrorSpy.mockRestore();
47 |   }
48 | }
49 | 
```

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

```markdown
 1 | # Product Context
 2 | 
 3 | ## Problem Statement
 4 | 
 5 | When working with large OpenAPI specifications, loading the entire spec into an LLM's context:
 6 | 
 7 | 1. Consumes excessive tokens due to fully resolved references
 8 | 2. May confuse the LLM with too much information
 9 | 3. Makes it difficult to focus on specific parts of the API
10 | 4. Duplicates schema information across multiple endpoints
11 | 
12 | ## Solution
13 | 
14 | An MCP server that:
15 | 
16 | 1. Loads OpenAPI v3.0 and Swagger v2.0 specs from local files or remote URLs.
17 | 2. Automatically converts Swagger v2.0 specs to OpenAPI v3.0.
18 | 3. Transforms internal references (`#/components/...`) to token-efficient MCP URIs.
19 | 4. Provides selective access to specific parts of the spec via MCP resources.
20 | 5. Returns information in token-efficient formats (text lists, JSON/YAML details).
21 | 6. Makes it easy for LLMs to explore API structures without loading the entire spec.
22 | 
23 | ## User Experience Goals
24 | 
25 | 1. Easy installation/usage via npm (`npx`) or Docker.
26 | 2. Simple configuration via a single command-line argument (path or URL).
27 | 3. Intuitive resource URIs for exploring API parts.
28 | 4. Clear and consistent response formats.
29 | 
30 | ## Usage Workflow
31 | 
32 | 1. User configures the server in their MCP client using either:
33 |    - `npx mcp-openapi-schema-explorer <path-or-url-to-spec> [options]` (Recommended for most users)
34 |    - `docker run kadykov/mcp-openapi-schema-explorer:latest <path-or-url-to-spec> [options]` (Requires Docker, local files need volume mounting)
35 |    - Global installation (`npm i -g ...` then `mcp-openapi-schema-explorer ...`) (Less common)
36 | 2. Server loads the spec (from file or URL), converts v2.0 to v3.0 if necessary, and transforms internal references to MCP URIs.
37 | 3. LLM explores API structure through exposed resources:
38 |    - List paths, components, methods.
39 |    - View details for info, operations, components, etc.
40 |    - Follow transformed reference URIs (`openapi://components/...`) to view component details without loading the whole spec initially.
41 | 4. Server restarts required if the source specification file/URL content changes.
42 | 
```

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

```typescript
 1 | import { IFormatter } from '../services/formatters';
 2 | // We don't need ResourceContents/ResourceContent here anymore
 3 | 
 4 | /**
 5 |  * Intermediate result structure returned by render methods.
 6 |  * Contains the core data needed to build the final ResourceContent.
 7 |  */
 8 | export interface RenderResultItem {
 9 |   /** The raw data object to be formatted. */
10 |   data: unknown;
11 |   /** The suffix to append to the base URI (e.g., 'info', 'paths/users', 'components/schemas/User'). */
12 |   uriSuffix: string;
13 |   /** Optional flag indicating an error for this specific item. */
14 |   isError?: boolean;
15 |   /** Optional error message if isError is true. */
16 |   errorText?: string;
17 |   /** Optional flag to indicate this should be rendered as a list (text/plain). */
18 |   renderAsList?: boolean;
19 | }
20 | 
21 | /**
22 |  * Context required for rendering OpenAPI specification objects.
23 |  */
24 | export interface RenderContext {
25 |   /** Formatter instance for handling output (JSON/YAML). */
26 |   formatter: IFormatter;
27 |   /** Base URI for generating resource links (e.g., "openapi://"). */
28 |   baseUri: string;
29 | }
30 | 
31 | /**
32 |  * Represents an OpenAPI specification object that can be rendered
33 |  * in different formats (list or detail).
34 |  */
35 | export interface RenderableSpecObject {
36 |   /**
37 |    * Generates data for a token-efficient list representation.
38 |    * @param context - The rendering context.
39 |    * @returns An array of RenderResultItem.
40 |    */
41 |   renderList(context: RenderContext): RenderResultItem[];
42 | 
43 |   /**
44 |    * Generates data for a detailed representation.
45 |    * @param context - The rendering context.
46 |    * @returns An array of RenderResultItem.
47 |    */
48 |   renderDetail(context: RenderContext): RenderResultItem[];
49 | }
50 | 
51 | /**
52 |  * Type guard to check if an object implements RenderableSpecObject.
53 |  * @param obj - The object to check.
54 |  * @returns True if the object implements RenderableSpecObject, false otherwise.
55 |  */
56 | export function isRenderableSpecObject(obj: unknown): obj is RenderableSpecObject {
57 |   return (
58 |     typeof obj === 'object' &&
59 |     obj !== null &&
60 |     typeof (obj as RenderableSpecObject).renderList === 'function' &&
61 |     typeof (obj as RenderableSpecObject).renderDetail === 'function'
62 |   );
63 | }
64 | 
```

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

```json
 1 | {
 2 |   "name": "mcp-openapi-schema-explorer",
 3 |   "version": "1.3.0",
 4 |   "description": "MCP OpenAPI schema explorer",
 5 |   "type": "module",
 6 |   "main": "dist/src/index.js",
 7 |   "types": "dist/src/index.d.ts",
 8 |   "bin": {
 9 |     "mcp-openapi-schema-explorer": "dist/src/index.js"
10 |   },
11 |   "scripts": {
12 |     "build": "rm -rf dist && mkdir -p dist && npx tsc && chmod +x dist/src/index.js",
13 |     "test": "jest",
14 |     "test:watch": "jest --watch",
15 |     "test:coverage": "jest --coverage",
16 |     "lint": "eslint . --ext .ts",
17 |     "lint:fix": "eslint . --ext .ts --fix",
18 |     "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
19 |     "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
20 |     "type-check": "tsc --noEmit",
21 |     "prepare": "husky",
22 |     "prepublishOnly": "npm run build"
23 |   },
24 |   "repository": {
25 |     "type": "git",
26 |     "url": "git+https://github.com/kadykov/mcp-openapi-schema-explorer.git"
27 |   },
28 |   "keywords": [
29 |     "mcp",
30 |     "openapi"
31 |   ],
32 |   "author": "",
33 |   "license": "MIT",
34 |   "bugs": {
35 |     "url": "https://github.com/kadykov/mcp-openapi-schema-explorer/issues"
36 |   },
37 |   "homepage": "https://github.com/kadykov/mcp-openapi-schema-explorer#readme",
38 |   "dependencies": {
39 |     "@modelcontextprotocol/sdk": "^1.10.1",
40 |     "js-yaml": "^4.1.0",
41 |     "openapi-types": "^12.1.3",
42 |     "swagger2openapi": "7.0.8",
43 |     "zod": "^4.0.5"
44 |   },
45 |   "devDependencies": {
46 |     "@codedependant/semantic-release-docker": "^5.1.0",
47 |     "@eslint/js": "^9.24.0",
48 |     "@semantic-release/changelog": "^6.0.3",
49 |     "@semantic-release/commit-analyzer": "^13.0.1",
50 |     "@semantic-release/exec": "^7.0.3",
51 |     "@semantic-release/git": "^10.0.1",
52 |     "@semantic-release/github": "^12.0.0",
53 |     "@semantic-release/npm": "^13.1.1",
54 |     "@semantic-release/release-notes-generator": "^14.0.3",
55 |     "@types/jest": "^30.0.0",
56 |     "@types/js-yaml": "^4.0.9",
57 |     "@types/node": "^24.0.3",
58 |     "@types/node-fetch": "^2.6.12",
59 |     "@types/swagger2openapi": "^7.0.4",
60 |     "@typescript-eslint/eslint-plugin": "^8.29.0",
61 |     "@typescript-eslint/parser": "^8.29.0",
62 |     "axios": "^1.8.4",
63 |     "eslint": "^9.24.0",
64 |     "eslint-plugin-security": "^3.0.1",
65 |     "globals": "^16.0.0",
66 |     "husky": "^9.1.7",
67 |     "jest": "^30.0.2",
68 |     "jest-silent-reporter": "^0.6.0",
69 |     "license-checker": "^25.0.1",
70 |     "msw": "^2.7.4",
71 |     "openapi-typescript": "^7.6.1",
72 |     "prettier": "^3.5.3",
73 |     "semantic-release": "^25.0.1",
74 |     "ts-jest": "^29.3.1",
75 |     "typescript": "^5.8.3"
76 |   }
77 | }
78 | 
```

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

```javascript
 1 | import tseslint from '@typescript-eslint/eslint-plugin';
 2 | import tsparser from '@typescript-eslint/parser';
 3 | import eslintJs from '@eslint/js';
 4 | import globals from 'globals';
 5 | import security from 'eslint-plugin-security';
 6 | 
 7 | export default [
 8 |   {
 9 |     ignores: ['dist/**', 'node_modules/**', 'local-docs/**'],
10 |   },
11 |   eslintJs.configs.recommended,
12 |   {
13 |     files: ['src/**/*.ts'],
14 |     languageOptions: {
15 |       parser: tsparser,
16 |       parserOptions: {
17 |         project: './tsconfig.json',
18 |         ecmaVersion: 2020,
19 |         sourceType: 'module',
20 |       },
21 |       globals: {
22 |         ...globals.node,
23 |       },
24 |     },
25 |     plugins: {
26 |       '@typescript-eslint': tseslint,
27 |       security: security,
28 |     },
29 |     rules: {
30 |       ...tseslint.configs['recommended'].rules,
31 |       ...tseslint.configs['recommended-requiring-type-checking'].rules,
32 |       '@typescript-eslint/explicit-function-return-type': 'error',
33 |       '@typescript-eslint/no-explicit-any': 'error',
34 |       '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
35 |       '@typescript-eslint/naming-convention': [
36 |         'error',
37 |         {
38 |           selector: 'interface',
39 |           format: ['PascalCase'],
40 |         },
41 |       ],
42 |       'no-console': ['error', { allow: ['warn', 'error'] }],
43 |       ...security.configs.recommended.rules,
44 |     },
45 |   },
46 |   {
47 |     files: ['test/**/*.ts'],
48 |     languageOptions: {
49 |       parser: tsparser,
50 |       parserOptions: {
51 |         project: './tsconfig.test.json',
52 |         ecmaVersion: 2020,
53 |         sourceType: 'module',
54 |       },
55 |       globals: {
56 |         ...globals.node,
57 |         ...globals.jest,
58 |       },
59 |     },
60 |     plugins: {
61 |       '@typescript-eslint': tseslint,
62 |       security: security,
63 |     },
64 |     rules: {
65 |       ...tseslint.configs['recommended'].rules,
66 |       ...tseslint.configs['recommended-requiring-type-checking'].rules,
67 |       '@typescript-eslint/explicit-function-return-type': 'error',
68 |       '@typescript-eslint/no-explicit-any': 'error',
69 |       '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
70 |       'no-console': 'off', // Allow console in tests
71 |     },
72 |   },
73 |   // Configuration for scripts (like generate-version.js)
74 |   {
75 |     files: ['scripts/**/*.js'],
76 |     languageOptions: {
77 |       globals: {
78 |         ...globals.node, // Enable Node.js global variables
79 |       },
80 |       ecmaVersion: 2022, // Use a recent version supporting top-level await etc.
81 |       sourceType: 'module', // Treat .js files in scripts/ as ES Modules
82 |     },
83 |     rules: {
84 |       // Add any specific rules for scripts if needed, e.g., allow console
85 |       'no-console': 'off',
86 |     },
87 |   },
88 | ];
89 | 
```

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

```typescript
 1 | import * as swagger2openapi from 'swagger2openapi';
 2 | import { OpenAPI } from 'openapi-types';
 3 | import { ReferenceTransformService, TransformContext } from './reference-transform.js';
 4 | 
 5 | /**
 6 |  * Service for loading and transforming OpenAPI specifications
 7 |  */
 8 | export class SpecLoaderService {
 9 |   private specData: OpenAPI.Document | null = null;
10 | 
11 |   constructor(
12 |     private specPath: string,
13 |     private referenceTransform: ReferenceTransformService
14 |   ) {}
15 | 
16 |   /**
17 |    * Load, potentially convert (from v2), and parse the OpenAPI specification.
18 |    */
19 |   async loadSpec(): Promise<OpenAPI.Document> {
20 |     const options = {
21 |       patch: true, // Fix minor errors in the spec
22 |       warnOnly: true, // Add warnings for non-patchable errors instead of throwing
23 |       origin: this.specPath, // Helps with resolving relative references if needed
24 |       source: this.specPath,
25 |     };
26 | 
27 |     try {
28 |       let result;
29 |       // Check if specPath is a URL
30 |       if (this.specPath.startsWith('http://') || this.specPath.startsWith('https://')) {
31 |         result = await swagger2openapi.convertUrl(this.specPath, options);
32 |       } else {
33 |         result = await swagger2openapi.convertFile(this.specPath, options);
34 |       }
35 | 
36 |       // swagger2openapi returns the result in result.openapi
37 |       if (!result || !result.openapi) {
38 |         throw new Error('Conversion or parsing failed to produce an OpenAPI document.');
39 |       }
40 | 
41 |       // TODO: Check result.options?.warnings for potential issues?
42 | 
43 |       this.specData = result.openapi as OpenAPI.Document; // Assuming result.openapi is compatible
44 |       return this.specData;
45 |     } catch (error) {
46 |       // Improve error message clarity
47 |       let message = `Failed to load/convert OpenAPI spec from ${this.specPath}: `;
48 |       if (error instanceof Error) {
49 |         message += error.message;
50 |         // Include stack trace if available and helpful?
51 |         // console.error(error.stack);
52 |       } else {
53 |         message += String(error);
54 |       }
55 |       throw new Error(message);
56 |     }
57 |   }
58 | 
59 |   /**
60 |    * Get the loaded specification
61 |    */
62 |   async getSpec(): Promise<OpenAPI.Document> {
63 |     if (!this.specData) {
64 |       await this.loadSpec();
65 |     }
66 |     return this.specData!;
67 |   }
68 | 
69 |   /**
70 |    * Get transformed specification with MCP resource references
71 |    */
72 |   async getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document> {
73 |     const spec = await this.getSpec();
74 |     return this.referenceTransform.transformDocument(spec, context);
75 |   }
76 | }
77 | 
78 | /**
79 |  * Create and initialize a new SpecLoaderService instance
80 |  */
81 | export async function createSpecLoader(
82 |   specPath: string,
83 |   referenceTransform: ReferenceTransformService
84 | ): Promise<SpecLoaderService> {
85 |   const loader = new SpecLoaderService(specPath, referenceTransform);
86 |   await loader.loadSpec();
87 |   return loader;
88 | }
89 | 
```

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

```typescript
  1 | import { dump as yamlDump } from 'js-yaml';
  2 | import {
  3 |   JsonFormatter,
  4 |   YamlFormatter,
  5 |   MinifiedJsonFormatter,
  6 |   createFormatter,
  7 | } from '../../../../src/services/formatters.js';
  8 | 
  9 | describe('Formatters', () => {
 10 |   const testData = {
 11 |     method: 'GET',
 12 |     path: '/test',
 13 |     summary: 'Test endpoint',
 14 |     parameters: [
 15 |       {
 16 |         name: 'id',
 17 |         in: 'path',
 18 |         required: true,
 19 |         schema: { type: 'string' },
 20 |       },
 21 |     ],
 22 |   };
 23 | 
 24 |   describe('JsonFormatter', () => {
 25 |     const formatter = new JsonFormatter();
 26 | 
 27 |     it('should format data as JSON with proper indentation', () => {
 28 |       const result = formatter.format(testData);
 29 |       expect(result).toBe(JSON.stringify(testData, null, 2));
 30 |     });
 31 | 
 32 |     it('should return application/json mime type', () => {
 33 |       expect(formatter.getMimeType()).toBe('application/json');
 34 |     });
 35 | 
 36 |     it('should handle empty objects', () => {
 37 |       expect(formatter.format({})).toBe('{}');
 38 |     });
 39 | 
 40 |     it('should handle null values', () => {
 41 |       expect(formatter.format(null)).toBe('null');
 42 |     });
 43 |   });
 44 | 
 45 |   describe('YamlFormatter', () => {
 46 |     const formatter = new YamlFormatter();
 47 | 
 48 |     it('should format data as YAML', () => {
 49 |       const result = formatter.format(testData);
 50 |       expect(result).toBe(
 51 |         yamlDump(testData, {
 52 |           indent: 2,
 53 |           lineWidth: -1,
 54 |           noRefs: true,
 55 |         })
 56 |       );
 57 |     });
 58 | 
 59 |     it('should return text/yaml mime type', () => {
 60 |       expect(formatter.getMimeType()).toBe('text/yaml');
 61 |     });
 62 | 
 63 |     it('should handle empty objects', () => {
 64 |       expect(formatter.format({})).toBe('{}\n');
 65 |     });
 66 | 
 67 |     it('should handle null values', () => {
 68 |       expect(formatter.format(null)).toBe('null\n');
 69 |     });
 70 |   });
 71 | 
 72 |   describe('MinifiedJsonFormatter', () => {
 73 |     const formatter = new MinifiedJsonFormatter();
 74 | 
 75 |     it('should format data as minified JSON', () => {
 76 |       const result = formatter.format(testData);
 77 |       expect(result).toBe(JSON.stringify(testData));
 78 |     });
 79 | 
 80 |     it('should return application/json mime type', () => {
 81 |       expect(formatter.getMimeType()).toBe('application/json');
 82 |     });
 83 | 
 84 |     it('should handle empty objects', () => {
 85 |       expect(formatter.format({})).toBe('{}');
 86 |     });
 87 | 
 88 |     it('should handle null values', () => {
 89 |       expect(formatter.format(null)).toBe('null');
 90 |     });
 91 |   });
 92 | 
 93 |   describe('createFormatter', () => {
 94 |     it('should create JsonFormatter for json format', () => {
 95 |       const formatter = createFormatter('json');
 96 |       expect(formatter).toBeInstanceOf(JsonFormatter);
 97 |     });
 98 | 
 99 |     it('should create YamlFormatter for yaml format', () => {
100 |       const formatter = createFormatter('yaml');
101 |       expect(formatter).toBeInstanceOf(YamlFormatter);
102 |     });
103 | 
104 |     it('should create MinifiedJsonFormatter for json-minified format', () => {
105 |       const formatter = createFormatter('json-minified');
106 |       expect(formatter).toBeInstanceOf(MinifiedJsonFormatter);
107 |     });
108 |   });
109 | });
110 | 
```

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

```typescript
 1 | import {
 2 |   ReadResourceTemplateCallback,
 3 |   ResourceTemplate,
 4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
 5 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
 6 | import { SpecLoaderService } from '../types.js';
 7 | import { IFormatter } from '../services/formatters.js';
 8 | import {
 9 |   RenderableComponentMap,
10 |   ComponentType,
11 |   VALID_COMPONENT_TYPES,
12 | } from '../rendering/components.js';
13 | import { RenderContext, RenderResultItem } from '../rendering/types.js';
14 | import { createErrorResult } from '../rendering/utils.js';
15 | // Import shared handler utils
16 | import {
17 |   formatResults,
18 |   isOpenAPIV3,
19 |   FormattedResultItem,
20 |   getValidatedComponentMap, // Import the helper
21 | } from './handler-utils.js'; // Already has .js
22 | 
23 | const BASE_URI = 'openapi://';
24 | 
25 | // Removed duplicated FormattedResultItem type - now imported from handler-utils
26 | // Removed duplicated formatResults function - now imported from handler-utils
27 | // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
28 | 
29 | /**
30 |  * Handles requests for listing component names of a specific type.
31 |  * Corresponds to the `openapi://components/{type}` template.
32 |  */
33 | export class ComponentMapHandler {
34 |   constructor(
35 |     private specLoader: SpecLoaderService,
36 |     private formatter: IFormatter // Needed for context
37 |   ) {}
38 | 
39 |   getTemplate(): ResourceTemplate {
40 |     // TODO: Add completion logic if needed
41 |     return new ResourceTemplate(`${BASE_URI}components/{type}`, {
42 |       list: undefined,
43 |       complete: undefined,
44 |     });
45 |   }
46 | 
47 |   handleRequest: ReadResourceTemplateCallback = async (
48 |     uri: URL,
49 |     variables: Variables
50 |   ): Promise<{ contents: FormattedResultItem[] }> => {
51 |     const type = variables.type as string;
52 |     const mapUriSuffix = `components/${type}`;
53 |     const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
54 |     let resultItems: RenderResultItem[];
55 | 
56 |     try {
57 |       if (!VALID_COMPONENT_TYPES.includes(type as ComponentType)) {
58 |         throw new Error(`Invalid component type: ${type}`);
59 |       }
60 |       const componentType = type as ComponentType;
61 | 
62 |       const spec = await this.specLoader.getTransformedSpec({
63 |         resourceType: 'schema', // Use 'schema' for now
64 |         format: 'openapi',
65 |       });
66 | 
67 |       // Use imported type guard
68 |       if (!isOpenAPIV3(spec)) {
69 |         throw new Error('Only OpenAPI v3 specifications are supported');
70 |       }
71 | 
72 |       // --- Use helper to get validated component map ---
73 |       const componentMapObj = getValidatedComponentMap(spec, componentType);
74 | 
75 |       // Instantiate RenderableComponentMap with the validated map
76 |       const renderableMap = new RenderableComponentMap(
77 |         componentMapObj, // componentMapObj retrieved safely via helper
78 |         componentType,
79 |         mapUriSuffix
80 |       );
81 |       resultItems = renderableMap.renderList(context);
82 |     } catch (error: unknown) {
83 |       const message = error instanceof Error ? error.message : String(error);
84 |       console.error(`Error handling request ${uri.href}: ${message}`);
85 |       resultItems = createErrorResult(mapUriSuffix, message);
86 |     }
87 | 
88 |     // Use imported formatResults
89 |     const contents = formatResults(context, resultItems);
90 |     return { contents };
91 |   };
92 | }
93 | 
```

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

```typescript
 1 | import { OpenAPIV3 } from 'openapi-types';
 2 | import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
 3 | 
 4 | /**
 5 |  * Wraps an OpenAPIV3.PathsObject to make it renderable.
 6 |  * Handles rendering the list of all paths and methods.
 7 |  */
 8 | export class RenderablePaths implements RenderableSpecObject {
 9 |   constructor(private paths: OpenAPIV3.PathsObject | undefined) {}
10 | 
11 |   /**
12 |    * Renders a token-efficient list of all paths and their methods.
13 |    * Corresponds to the `openapi://paths` URI.
14 |    */
15 |   renderList(context: RenderContext): RenderResultItem[] {
16 |     if (!this.paths || Object.keys(this.paths).length === 0) {
17 |       return [
18 |         {
19 |           uriSuffix: 'paths',
20 |           data: 'No paths found in the specification.',
21 |           renderAsList: true,
22 |         },
23 |       ];
24 |     }
25 | 
26 |     // Generate hint first and prepend "Hint: "
27 |     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.`;
28 |     let outputLines: string[] = [`Hint: ${hintText}`, '']; // Start with hint and a blank line
29 | 
30 |     const pathEntries = Object.entries(this.paths).sort(([pathA], [pathB]) =>
31 |       pathA.localeCompare(pathB)
32 |     );
33 | 
34 |     for (const [path, pathItem] of pathEntries) {
35 |       if (!pathItem) continue;
36 | 
37 |       // Create a list of valid, sorted, uppercase methods for the current path
38 |       const methods: string[] = [];
39 |       for (const key in pathItem) {
40 |         const lowerKey = key.toLowerCase();
41 |         if (Object.values(OpenAPIV3.HttpMethods).includes(lowerKey as OpenAPIV3.HttpMethods)) {
42 |           // Check if it's a valid operation object before adding the method
43 |           const operation = pathItem[key as keyof OpenAPIV3.PathItemObject];
44 |           if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
45 |             methods.push(lowerKey.toUpperCase());
46 |           }
47 |         }
48 |       }
49 |       methods.sort(); // Sort methods alphabetically
50 | 
51 |       // Format the line: METHODS /path
52 |       const methodsString = methods.length > 0 ? methods.join(' ') : '(No methods)';
53 |       outputLines.push(`${methodsString} ${path}`);
54 |     }
55 | 
56 |     return [
57 |       {
58 |         uriSuffix: 'paths',
59 |         data: outputLines.join('\n'), // Join lines into a single string
60 |         renderAsList: true, // This result is always plain text
61 |       },
62 |     ];
63 |   }
64 | 
65 |   /**
66 |    * Renders the detail view. For the Paths object level, this isn't
67 |    * typically used directly. Details are requested per path or operation.
68 |    */
69 |   renderDetail(context: RenderContext): RenderResultItem[] {
70 |     // Delegate to renderList as the primary view for the collection of paths
71 |     return this.renderList(context);
72 |   }
73 | 
74 |   /**
75 |    * Gets the PathItemObject for a specific path.
76 |    * @param path - The decoded path string.
77 |    * @returns The PathItemObject or undefined if not found.
78 |    */
79 |   getPathItem(path: string): OpenAPIV3.PathItemObject | undefined {
80 |     // Use Map for safe access
81 |     if (!this.paths) {
82 |       return undefined;
83 |     }
84 |     const pathsMap = new Map(Object.entries(this.paths));
85 |     return pathsMap.get(path); // Map.get returns ValueType | undefined
86 |   }
87 | }
88 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { buildComponentDetailUri } from '../utils/uri-builder.js'; // Added .js extension
  3 | 
  4 | export interface TransformContext {
  5 |   resourceType: 'endpoint' | 'schema';
  6 |   format: 'openapi' | 'asyncapi' | 'graphql';
  7 |   path?: string;
  8 |   method?: string;
  9 | }
 10 | 
 11 | export interface ReferenceObject {
 12 |   $ref: string;
 13 | }
 14 | 
 15 | export interface TransformedReference {
 16 |   $ref: string;
 17 | }
 18 | 
 19 | export interface ReferenceTransform<T> {
 20 |   transformRefs(document: T, context: TransformContext): T;
 21 | }
 22 | 
 23 | export class ReferenceTransformService {
 24 |   private transformers = new Map<string, ReferenceTransform<unknown>>();
 25 | 
 26 |   registerTransformer<T>(format: string, transformer: ReferenceTransform<T>): void {
 27 |     this.transformers.set(format, transformer as ReferenceTransform<unknown>);
 28 |   }
 29 | 
 30 |   transformDocument<T>(document: T, context: TransformContext): T {
 31 |     const transformer = this.transformers.get(context.format) as ReferenceTransform<T>;
 32 |     if (!transformer) {
 33 |       throw new Error(`No transformer registered for format: ${context.format}`);
 34 |     }
 35 |     return transformer.transformRefs(document, context);
 36 |   }
 37 | }
 38 | 
 39 | export class OpenAPITransformer implements ReferenceTransform<OpenAPIV3.Document> {
 40 |   // Handle nested objects recursively
 41 |   private transformObject(obj: unknown, _context: TransformContext): unknown {
 42 |     if (!obj || typeof obj !== 'object') {
 43 |       return obj;
 44 |     }
 45 | 
 46 |     // Handle arrays
 47 |     if (Array.isArray(obj)) {
 48 |       return obj.map(item => this.transformObject(item, _context));
 49 |     }
 50 | 
 51 |     // Handle references
 52 |     if (this.isReferenceObject(obj)) {
 53 |       return this.transformReference(obj.$ref);
 54 |     }
 55 | 
 56 |     // Recursively transform object properties
 57 |     const result: Record<string, unknown> = {};
 58 |     if (typeof obj === 'object') {
 59 |       for (const [key, value] of Object.entries(obj)) {
 60 |         if (Object.prototype.hasOwnProperty.call(obj, key)) {
 61 |           Object.defineProperty(result, key, {
 62 |             value: this.transformObject(value, _context),
 63 |             enumerable: true,
 64 |             writable: true,
 65 |             configurable: true,
 66 |           });
 67 |         }
 68 |       }
 69 |     }
 70 |     return result;
 71 |   }
 72 | 
 73 |   private isReferenceObject(obj: unknown): obj is ReferenceObject {
 74 |     return typeof obj === 'object' && obj !== null && '$ref' in obj;
 75 |   }
 76 | 
 77 |   private transformReference(ref: string): TransformedReference {
 78 |     // Handle only internal references for now
 79 |     if (!ref.startsWith('#/')) {
 80 |       return { $ref: ref }; // Keep external refs as-is
 81 |     }
 82 | 
 83 |     // Example ref: #/components/schemas/MySchema
 84 |     const parts = ref.split('/');
 85 |     // Check if it's an internal component reference
 86 |     if (parts[0] === '#' && parts[1] === 'components' && parts.length === 4) {
 87 |       const componentType = parts[2];
 88 |       const componentName = parts[3];
 89 | 
 90 |       // Use the centralized builder to create the correct URI
 91 |       const newUri = buildComponentDetailUri(componentType, componentName);
 92 |       return {
 93 |         $ref: newUri,
 94 |       };
 95 |     }
 96 | 
 97 |     // Keep other internal references (#/paths/...) and external references as-is
 98 |     return { $ref: ref };
 99 |   }
100 | 
101 |   transformRefs(document: OpenAPIV3.Document, context: TransformContext): OpenAPIV3.Document {
102 |     const transformed = this.transformObject(document, context);
103 |     return transformed as OpenAPIV3.Document;
104 |   }
105 | }
106 | 
```

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

```typescript
 1 | import {
 2 |   ReadResourceTemplateCallback,
 3 |   ResourceTemplate,
 4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
 5 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
 6 | import { SpecLoaderService } from '../types.js';
 7 | import { IFormatter } from '../services/formatters.js';
 8 | import { RenderablePathItem } from '../rendering/path-item.js';
 9 | import { RenderContext, RenderResultItem } from '../rendering/types.js';
10 | import { createErrorResult } from '../rendering/utils.js';
11 | import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
12 | // Import shared handler utils
13 | import {
14 |   formatResults,
15 |   isOpenAPIV3,
16 |   FormattedResultItem,
17 |   getValidatedPathItem, // Import the helper
18 | } from './handler-utils.js'; // Already has .js
19 | 
20 | const BASE_URI = 'openapi://';
21 | 
22 | // Removed duplicated FormattedResultItem type - now imported from handler-utils
23 | // Removed duplicated formatResults function - now imported from handler-utils
24 | // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
25 | 
26 | /**
27 |  * Handles requests for listing methods for a specific path.
28 |  * Corresponds to the `openapi://paths/{path}` template.
29 |  */
30 | export class PathItemHandler {
31 |   constructor(
32 |     private specLoader: SpecLoaderService,
33 |     private formatter: IFormatter // Although unused in list view, needed for context
34 |   ) {}
35 | 
36 |   getTemplate(): ResourceTemplate {
37 |     // TODO: Add completion logic if needed
38 |     return new ResourceTemplate(`${BASE_URI}paths/{path}`, {
39 |       list: undefined,
40 |       complete: undefined,
41 |     });
42 |   }
43 | 
44 |   handleRequest: ReadResourceTemplateCallback = async (
45 |     uri: URL,
46 |     variables: Variables
47 |   ): Promise<{ contents: FormattedResultItem[] }> => {
48 |     const encodedPath = variables.path as string;
49 |     // Decode the path received from the URI variable
50 |     const decodedPath = decodeURIComponent(encodedPath || '');
51 |     // Use the builder to create the suffix, which will re-encode the path correctly
52 |     const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
53 |     const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
54 |     let resultItems: RenderResultItem[];
55 | 
56 |     try {
57 |       const spec = await this.specLoader.getTransformedSpec({
58 |         resourceType: 'schema', // Use 'schema' for now
59 |         format: 'openapi',
60 |       });
61 | 
62 |       // Use imported type guard
63 |       if (!isOpenAPIV3(spec)) {
64 |         throw new Error('Only OpenAPI v3 specifications are supported');
65 |       }
66 | 
67 |       // --- Use helper to get validated path item ---
68 |       const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
69 |       const pathItemObj = getValidatedPathItem(spec, lookupPath);
70 | 
71 |       // Instantiate RenderablePathItem with the validated pathItemObj
72 |       const renderablePathItem = new RenderablePathItem(
73 |         pathItemObj, // pathItemObj retrieved safely via helper
74 |         lookupPath, // Pass the raw, decoded path
75 |         pathUriSuffix // Pass the correctly built suffix
76 |       );
77 |       resultItems = renderablePathItem.renderList(context);
78 |     } catch (error: unknown) {
79 |       const message = error instanceof Error ? error.message : String(error);
80 |       console.error(`Error handling request ${uri.href}: ${message}`);
81 |       resultItems = createErrorResult(pathUriSuffix, message);
82 |     }
83 | 
84 |     // Use imported formatResults
85 |     const contents = formatResults(context, resultItems);
86 |     return { contents };
87 |   };
88 | }
89 | 
```

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

```json
  1 | {
  2 |   "openapi": "3.0.3",
  3 |   "info": {
  4 |     "title": "Complex Endpoint Test API",
  5 |     "version": "1.0.0"
  6 |   },
  7 |   "paths": {
  8 |     "/api/v1/organizations/{orgId}/projects/{projectId}/tasks": {
  9 |       "get": {
 10 |         "operationId": "getProjectTasks",
 11 |         "summary": "Get Tasks",
 12 |         "description": "Retrieve a list of tasks for a specific project.",
 13 |         "parameters": [
 14 |           {
 15 |             "name": "orgId",
 16 |             "in": "path",
 17 |             "required": true,
 18 |             "schema": { "type": "string" }
 19 |           },
 20 |           {
 21 |             "name": "projectId",
 22 |             "in": "path",
 23 |             "required": true,
 24 |             "schema": { "type": "string" }
 25 |           },
 26 |           {
 27 |             "name": "status",
 28 |             "in": "query",
 29 |             "schema": {
 30 |               "type": "string",
 31 |               "enum": ["active", "completed"]
 32 |             }
 33 |           },
 34 |           {
 35 |             "name": "sort",
 36 |             "in": "query",
 37 |             "schema": {
 38 |               "type": "string",
 39 |               "enum": ["created", "updated", "priority"]
 40 |             }
 41 |           }
 42 |         ],
 43 |         "responses": {
 44 |           "200": {
 45 |             "description": "List of tasks",
 46 |             "content": {
 47 |               "application/json": {
 48 |                 "schema": {
 49 |                   "$ref": "#/components/schemas/TaskList"
 50 |                 }
 51 |               }
 52 |             }
 53 |           }
 54 |         }
 55 |       },
 56 |       "post": {
 57 |         "operationId": "createProjectTask",
 58 |         "summary": "Create Task",
 59 |         "description": "Create a new task within a project.",
 60 |         "requestBody": {
 61 |           "required": true,
 62 |           "content": {
 63 |             "application/json": {
 64 |               "schema": {
 65 |                 "$ref": "#/components/schemas/CreateTaskRequest"
 66 |               }
 67 |             }
 68 |           }
 69 |         },
 70 |         "responses": {
 71 |           "201": {
 72 |             "description": "Task created",
 73 |             "content": {
 74 |               "application/json": {
 75 |                 "schema": {
 76 |                   "$ref": "#/components/schemas/Task"
 77 |                 }
 78 |               }
 79 |             }
 80 |           }
 81 |         }
 82 |       }
 83 |     }
 84 |   },
 85 |   "components": {
 86 |     "schemas": {
 87 |       "Task": {
 88 |         "type": "object",
 89 |         "required": ["id", "title", "status"],
 90 |         "properties": {
 91 |           "id": {
 92 |             "type": "string",
 93 |             "format": "uuid"
 94 |           },
 95 |           "title": {
 96 |             "type": "string"
 97 |           },
 98 |           "status": {
 99 |             "type": "string",
100 |             "enum": ["active", "completed"]
101 |           },
102 |           "priority": {
103 |             "type": "integer",
104 |             "minimum": 1,
105 |             "maximum": 5
106 |           }
107 |         }
108 |       },
109 |       "TaskList": {
110 |         "type": "object",
111 |         "required": ["items"],
112 |         "properties": {
113 |           "items": {
114 |             "type": "array",
115 |             "items": {
116 |               "$ref": "#/components/schemas/Task"
117 |             }
118 |           },
119 |           "totalCount": {
120 |             "type": "integer"
121 |           }
122 |         }
123 |       },
124 |       "CreateTaskRequest": {
125 |         "type": "object",
126 |         "required": ["title"],
127 |         "properties": {
128 |           "title": {
129 |             "type": "string"
130 |           },
131 |           "status": {
132 |             "type": "string",
133 |             "enum": ["active", "completed"],
134 |             "default": "active"
135 |           },
136 |           "priority": {
137 |             "type": "integer",
138 |             "minimum": 1,
139 |             "maximum": 5,
140 |             "default": 3
141 |           }
142 |         }
143 |       }
144 |     }
145 |   }
146 | }
147 | 
```

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

```markdown
 1 | # Active Context
 2 | 
 3 | ## Current Focus
 4 | 
 5 | Successfully upgraded @modelcontextprotocol/sdk from 1.10.1 to 1.11.0 and addressed breaking changes in test mocks.
 6 | 
 7 | ## Implementation Status
 8 | 
 9 | - @modelcontextprotocol/sdk updated from 1.10.1 to 1.11.0
10 | - Updated all test mocks to include new `RequestHandlerExtra` property (`requestId`).
11 | - Corrected import path for `RequestId` to `@modelcontextprotocol/sdk/types.js`.
12 | - Modified test files:
13 |   - component-map-handler.test.ts
14 |   - component-detail-handler.test.ts
15 |   - operation-handler.test.ts
16 |   - path-item-handler.test.ts
17 |   - top-level-field-handler.test.ts
18 | - All tests passing successfully
19 | - Server now loads OpenAPI v3.0 and Swagger v2.0 specs from local files or remote URLs
20 | - Swagger v2.0 specs are automatically converted to v3.0
21 | - Internal references are transformed to MCP URIs
22 | - Added `json-minified` output format option
23 | - Server name is now dynamically set based on the loaded spec's `info.title`
24 | - Automated versioning and release process implemented using `semantic-release`
25 | - CI workflow adapted for Node 22, uses `just` for checks, and includes a `release` job
26 | - Docker support added with automated Docker Hub publishing
27 | - Dependencies correctly categorized
28 | - Resource completion logic implemented
29 | - Dynamic server name implemented
30 | - Minified JSON output format added
31 | - Remote spec loading and Swagger v2.0 conversion support added
32 | - Core resource exploration functionality remains operational
33 | - Unit tests updated for latest SDK version
34 | - E2E tests cover all main functionality
35 | 
36 | ## Recent Changes
37 | 
38 | ### SDK Update to v1.11.0 & Test Fixes (✓)
39 | 
40 | 1. **Dependency Update:**
41 | 
42 |    - Updated @modelcontextprotocol/sdk from 1.10.1 to 1.11.0 in `package.json`.
43 |    - Identified breaking change in `RequestHandlerExtra` type requiring a new `requestId` property.
44 | 
45 | 2. **Test Suite Updates:**
46 |    - Added the `requestId` property to `mockExtra` objects in all handler unit tests:
47 |      - `test/__tests__/unit/handlers/top-level-field-handler.test.ts`
48 |      - `test/__tests__/unit/handlers/component-map-handler.test.ts`
49 |      - `test/__tests__/unit/handlers/path-item-handler.test.ts`
50 |      - `test/__tests__/unit/handlers/operation-handler.test.ts`
51 |      - `test/__tests__/unit/handlers/component-detail-handler.test.ts`
52 |    - 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.
53 |    - Confirmed all test fixes by running `just build && just test` successfully.
54 | 
55 | ## Next Actions
56 | 
57 | 1. **Continue with Previous Plans:**
58 | 
59 |    - Complete README updates with release process details
60 |    - Clean up any remaining TODOs in codebase
61 |    - Address minor ESLint warnings
62 | 
63 | 2. **Documentation:**
64 | 
65 |    - Document the SDK upgrade in CHANGELOG.md
66 |    - Update dependencies section in relevant documentation
67 | 
68 | 3. **Testing:**
69 | 
70 |    - Monitor for any new breaking changes in future SDK updates
71 |    - Consider adding test utilities to simplify mock creation
72 | 
73 | 4. **Code Cleanup:**
74 |    - Refactor duplicated mock setup code in tests
75 |    - Consider creating shared test fixtures for common mocks
76 | 
77 | ## Future Considerations
78 | 
79 | 1. **SDK Integration:**
80 | 
81 |    - Stay updated with MCP SDK releases
82 |    - Plan for future breaking changes
83 |    - Consider automated dependency update checks
84 | 
85 | 2. **Testing Infrastructure:**
86 | 
87 |    - Improve test mock reusability
88 |    - Add test coverage for edge cases
89 |    - Consider adding integration tests
90 | 
91 | 3. **Previous Future Considerations:**
92 |    - Implement reference traversal/resolution service
93 |    - Enhance support for all component types
94 | 
```

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

```typescript
 1 | import {
 2 |   ReadResourceTemplateCallback,
 3 |   ResourceTemplate,
 4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
 5 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
 6 | // ResourceContents is the base type for a single item, not the array type needed here.
 7 | // We'll define the array structure inline based on TextResourceContentsSchema.
 8 | 
 9 | import { SpecLoaderService } from '../types.js';
10 | import { IFormatter } from '../services/formatters.js';
11 | import { RenderableDocument } from '../rendering/document.js';
12 | import { RenderablePaths } from '../rendering/paths.js';
13 | import { RenderableComponents } from '../rendering/components.js';
14 | import { RenderContext, RenderResultItem } from '../rendering/types.js';
15 | import { createErrorResult } from '../rendering/utils.js';
16 | // Import shared handler utils
17 | import { formatResults, isOpenAPIV3, FormattedResultItem } from './handler-utils.js'; // Already has .js
18 | 
19 | const BASE_URI = 'openapi://';
20 | 
21 | // Removed duplicated FormattedResultItem type - now imported from handler-utils
22 | // Removed duplicated formatResults function - now imported from handler-utils
23 | 
24 | /**
25 |  * Handles requests for top-level OpenAPI fields (info, servers, paths list, components list).
26 |  * Corresponds to the `openapi://{field}` template.
27 |  */
28 | export class TopLevelFieldHandler {
29 |   constructor(
30 |     private specLoader: SpecLoaderService,
31 |     private formatter: IFormatter
32 |   ) {}
33 | 
34 |   getTemplate(): ResourceTemplate {
35 |     // TODO: Add completion logic if needed
36 |     return new ResourceTemplate(`${BASE_URI}{field}`, {
37 |       list: undefined,
38 |       complete: undefined,
39 |     });
40 |   }
41 | 
42 |   handleRequest: ReadResourceTemplateCallback = async (
43 |     uri: URL,
44 |     variables: Variables
45 |     // matchedTemplate is not needed if we only handle one template
46 |   ): Promise<{ contents: FormattedResultItem[] }> => {
47 |     // Return type uses the defined array structure
48 |     const field = variables.field as string;
49 |     const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
50 |     let resultItems: RenderResultItem[];
51 | 
52 |     try {
53 |       const spec = await this.specLoader.getTransformedSpec({
54 |         // Use 'schema' as placeholder resourceType for transformation context
55 |         resourceType: 'schema',
56 |         format: 'openapi',
57 |       });
58 | 
59 |       // Use imported type guard
60 |       if (!isOpenAPIV3(spec)) {
61 |         throw new Error('Only OpenAPI v3 specifications are supported');
62 |       }
63 | 
64 |       const renderableDoc = new RenderableDocument(spec);
65 | 
66 |       // Route based on the field name
67 |       if (field === 'paths') {
68 |         const pathsObj = renderableDoc.getPathsObject();
69 |         resultItems = new RenderablePaths(pathsObj).renderList(context);
70 |       } else if (field === 'components') {
71 |         const componentsObj = renderableDoc.getComponentsObject();
72 |         resultItems = new RenderableComponents(componentsObj).renderList(context);
73 |       } else {
74 |         // Handle other top-level fields (info, servers, tags, etc.)
75 |         const fieldObject = renderableDoc.getTopLevelField(field);
76 |         resultItems = renderableDoc.renderTopLevelFieldDetail(context, fieldObject, field);
77 |       }
78 |     } catch (error: unknown) {
79 |       const message = error instanceof Error ? error.message : String(error);
80 |       console.error(`Error handling request ${uri.href}: ${message}`);
81 |       resultItems = createErrorResult(field, message); // Use field as uriSuffix for error
82 |     }
83 | 
84 |     // Format results into the final structure
85 |     const contents: FormattedResultItem[] = formatResults(context, resultItems);
86 |     // Return the object with the correctly typed contents array
87 |     // Use imported formatResults
88 |     return { contents };
89 |   };
90 | 
91 |   // Removed duplicated isOpenAPIV3 type guard - now imported from handler-utils
92 | } // Ensure class closing brace is present
93 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderablePaths } from '../../../../src/rendering/paths';
  3 | import { RenderContext } from '../../../../src/rendering/types';
  4 | import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
  5 | 
  6 | // Mock Formatter & Context
  7 | const mockFormatter: IFormatter = new JsonFormatter();
  8 | const mockContext: RenderContext = {
  9 |   formatter: mockFormatter,
 10 |   baseUri: 'openapi://',
 11 | };
 12 | 
 13 | // Sample Paths Object Fixture
 14 | const samplePaths: OpenAPIV3.PathsObject = {
 15 |   '/users': {
 16 |     get: {
 17 |       summary: 'List Users',
 18 |       responses: { '200': { description: 'OK' } },
 19 |     },
 20 |     post: {
 21 |       summary: 'Create User',
 22 |       responses: { '201': { description: 'Created' } },
 23 |     },
 24 |   },
 25 |   '/users/{userId}': {
 26 |     get: {
 27 |       summary: 'Get User by ID',
 28 |       responses: { '200': { description: 'OK' } },
 29 |     },
 30 |     delete: {
 31 |       // No summary
 32 |       responses: { '204': { description: 'No Content' } },
 33 |     },
 34 |   },
 35 |   // Removed /ping path with custom operation to avoid type errors
 36 | };
 37 | 
 38 | const emptyPaths: OpenAPIV3.PathsObject = {};
 39 | 
 40 | describe('RenderablePaths', () => {
 41 |   describe('renderList', () => {
 42 |     it('should render a list of paths and methods correctly', () => {
 43 |       const renderablePaths = new RenderablePaths(samplePaths);
 44 |       const result = renderablePaths.renderList(mockContext);
 45 | 
 46 |       expect(result).toHaveLength(1);
 47 |       expect(result[0].uriSuffix).toBe('paths');
 48 |       expect(result[0].renderAsList).toBe(true);
 49 |       expect(result[0].isError).toBeUndefined();
 50 | 
 51 |       // Define expected output lines based on the new format
 52 |       const expectedLineUsers = 'GET POST /users'; // Methods sorted alphabetically and uppercased
 53 |       const expectedLineUserDetail = 'DELETE GET /users/{userId}'; // Methods sorted alphabetically and uppercased
 54 | 
 55 |       // Check essential parts instead of exact match
 56 |       expect(result[0].data).toContain('Hint:');
 57 |       expect(result[0].data).toContain('openapi://paths/{encoded_path}');
 58 |       expect(result[0].data).toContain('openapi://paths/{encoded_path}/{method}');
 59 |       expect(result[0].data).toContain(expectedLineUsers);
 60 |       expect(result[0].data).toContain(expectedLineUserDetail);
 61 |     });
 62 | 
 63 |     it('should handle empty paths object', () => {
 64 |       const renderablePaths = new RenderablePaths(emptyPaths);
 65 |       const result = renderablePaths.renderList(mockContext);
 66 | 
 67 |       expect(result).toHaveLength(1);
 68 |       expect(result[0]).toEqual({
 69 |         uriSuffix: 'paths',
 70 |         data: 'No paths found in the specification.',
 71 |         renderAsList: true,
 72 |       });
 73 |     });
 74 | 
 75 |     it('should handle undefined paths object', () => {
 76 |       const renderablePaths = new RenderablePaths(undefined);
 77 |       const result = renderablePaths.renderList(mockContext);
 78 | 
 79 |       expect(result).toHaveLength(1);
 80 |       expect(result[0]).toEqual({
 81 |         uriSuffix: 'paths',
 82 |         data: 'No paths found in the specification.',
 83 |         renderAsList: true,
 84 |       });
 85 |     });
 86 |   });
 87 | 
 88 |   describe('renderDetail', () => {
 89 |     it('should delegate to renderList', () => {
 90 |       const renderablePaths = new RenderablePaths(samplePaths);
 91 |       const listResult = renderablePaths.renderList(mockContext);
 92 |       const detailResult = renderablePaths.renderDetail(mockContext);
 93 |       // Check if the output is the same as renderList
 94 |       expect(detailResult).toEqual(listResult);
 95 |     });
 96 |   });
 97 | 
 98 |   describe('getPathItem', () => {
 99 |     it('should return the correct PathItemObject', () => {
100 |       const renderablePaths = new RenderablePaths(samplePaths);
101 |       expect(renderablePaths.getPathItem('/users')).toBe(samplePaths['/users']);
102 |       expect(renderablePaths.getPathItem('/users/{userId}')).toBe(samplePaths['/users/{userId}']);
103 |     });
104 | 
105 |     it('should return undefined for non-existent path', () => {
106 |       const renderablePaths = new RenderablePaths(samplePaths);
107 |       expect(renderablePaths.getPathItem('/nonexistent')).toBeUndefined();
108 |     });
109 | 
110 |     it('should return undefined if paths object is undefined', () => {
111 |       const renderablePaths = new RenderablePaths(undefined);
112 |       expect(renderablePaths.getPathItem('/users')).toBeUndefined();
113 |     });
114 |   });
115 | });
116 | 
```

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

```yaml
  1 | name: CI
  2 | 
  3 | permissions: # Add default permissions, release job will override if needed
  4 |   contents: read
  5 | 
  6 | on:
  7 |   push:
  8 |     branches: [main]
  9 |     tags:
 10 |       - 'v*'
 11 |   pull_request:
 12 |     branches: [main]
 13 | 
 14 | jobs:
 15 |   test:
 16 |     runs-on: ubuntu-latest
 17 |     steps:
 18 |       - name: Checkout code
 19 |         uses: actions/checkout@v5
 20 | 
 21 |       - name: Setup Node.js
 22 |         uses: actions/setup-node@v6
 23 |         with:
 24 |           node-version: 22 # Match Dockerfile
 25 |           cache: 'npm'
 26 | 
 27 |       - name: Setup Just
 28 |         uses: extractions/setup-just@v3
 29 | 
 30 |       - name: Install dependencies
 31 |         run: npm ci
 32 | 
 33 |       - name: Run all checks (format, lint, build, test)
 34 |         run: just all # Uses justfile for consistency
 35 | 
 36 |       - name: Upload coverage reports artifact
 37 |         uses: actions/upload-artifact@v5
 38 |         with:
 39 |           name: coverage-report-${{ github.run_id }} # Unique name per run
 40 |           path: coverage/
 41 |         if: always() # Upload even if previous steps fail
 42 | 
 43 |       - name: Upload coverage to Codecov
 44 |         uses: codecov/codecov-action@v5
 45 |         with:
 46 |           token: ${{ secrets.CODECOV_TOKEN }}
 47 |         # fail_ci_if_error: true # Optional: fail CI if upload fails
 48 | 
 49 |   security:
 50 |     runs-on: ubuntu-latest
 51 |     permissions:
 52 |       contents: read # Needed for checkout and CodeQL
 53 |       security-events: write # Needed for CodeQL alert uploads
 54 |     steps:
 55 |       - name: Checkout code
 56 |         uses: actions/checkout@v5
 57 | 
 58 |       - name: Setup Node.js
 59 |         uses: actions/setup-node@v6
 60 |         with:
 61 |           node-version: 22 # Match Dockerfile and test job
 62 |           cache: 'npm'
 63 | 
 64 |       - name: Setup Just
 65 |         uses: extractions/setup-just@v3
 66 | 
 67 |       - name: Install dependencies
 68 |         run: npm ci
 69 | 
 70 |       - name: Run Security Checks (Audit, Licenses)
 71 |         run: just security # Uses justfile, includes npm audit and license-checker
 72 |         continue-on-error: true # Allow workflow to continue even if npm audit finds vulnerabilities
 73 | 
 74 |       # Static code analysis with CodeQL (Keep separate as it's not in justfile)
 75 |       - name: Initialize CodeQL
 76 |         uses: github/codeql-action/init@v4
 77 |         # Auto-detect languages: javascript, typescript
 78 |         # queries: +security-extended # Optional: run more queries
 79 | 
 80 |       - name: Perform CodeQL Analysis
 81 |         uses: github/codeql-action/analyze@v4
 82 | 
 83 |   release:
 84 |     name: Release
 85 |     runs-on: ubuntu-latest
 86 |     needs: [test, security] # Run after test and security checks pass
 87 |     # Run only on pushes to main, not on tags (semantic-release creates tags)
 88 |     if: github.ref == 'refs/heads/main' && github.event_name == 'push'
 89 |     permissions:
 90 |       contents: write # Allow tagging, committing package.json/changelog/version.ts
 91 |       issues: write # Allow commenting on issues/PRs
 92 |       pull-requests: write # Allow commenting on issues/PRs
 93 |       id-token: write # Needed for provenance publishing to npm (alternative to NPM_TOKEN)
 94 |     steps:
 95 |       - name: Checkout
 96 |         uses: actions/checkout@v5
 97 |         with:
 98 |           persist-credentials: false
 99 | 
100 |       - name: Setup Node.js
101 |         uses: actions/setup-node@v6
102 |         with:
103 |           node-version: 22 # Match Dockerfile and other jobs
104 |           cache: 'npm'
105 | 
106 |       - name: Install all dependencies
107 |         run: npm ci --include=dev
108 | 
109 |       # Docker setup steps (Still needed for the environment where the action runs)
110 |       - name: Set up QEMU
111 |         uses: docker/setup-qemu-action@v3
112 |       - name: Set up Docker Buildx
113 |         uses: docker/setup-buildx-action@v3
114 |       - name: Log in to Docker Hub
115 |         uses: docker/login-action@v3
116 |         with:
117 |           username: ${{ secrets.DOCKERHUB_USERNAME }}
118 |           password: ${{ secrets.DOCKERHUB_TOKEN }}
119 | 
120 |       - name: Semantic Release
121 |         uses: cycjimmy/semantic-release-action@v5
122 |         with:
123 |           # Add the docker plugin to extra_plugins
124 |           extra_plugins: |
125 |             @semantic-release/changelog
126 |             @semantic-release/exec
127 |             @semantic-release/git
128 |             @codedependant/semantic-release-docker
129 |         env:
130 |           GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} # Use dedicated release token if needed
131 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
132 |           # Docker login is handled by the login-action step above
133 | 
```

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

```typescript
  1 | import {
  2 |   ReadResourceTemplateCallback,
  3 |   ResourceTemplate,
  4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
  5 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
  6 | import { SpecLoaderService } from '../types.js';
  7 | import { IFormatter } from '../services/formatters.js';
  8 | import {
  9 |   RenderableComponentMap,
 10 |   ComponentType,
 11 |   VALID_COMPONENT_TYPES,
 12 | } from '../rendering/components.js';
 13 | import { RenderContext, RenderResultItem } from '../rendering/types.js';
 14 | import { createErrorResult } from '../rendering/utils.js';
 15 | // Import shared handler utils
 16 | import {
 17 |   formatResults,
 18 |   isOpenAPIV3,
 19 |   FormattedResultItem,
 20 |   getValidatedComponentMap, // Import helper
 21 |   getValidatedComponentDetails, // Import helper
 22 | } from './handler-utils.js'; // Already has .js
 23 | 
 24 | const BASE_URI = 'openapi://';
 25 | 
 26 | // Removed duplicated FormattedResultItem type - now imported from handler-utils
 27 | // Removed duplicated formatResults function - now imported from handler-utils
 28 | // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
 29 | 
 30 | /**
 31 |  * Handles requests for specific component details.
 32 |  * Corresponds to the `openapi://components/{type}/{name*}` template.
 33 |  */
 34 | export class ComponentDetailHandler {
 35 |   constructor(
 36 |     private specLoader: SpecLoaderService,
 37 |     private formatter: IFormatter
 38 |   ) {}
 39 | 
 40 |   getTemplate(): ResourceTemplate {
 41 |     // TODO: Add completion logic if needed
 42 |     return new ResourceTemplate(`${BASE_URI}components/{type}/{name*}`, {
 43 |       list: undefined,
 44 |       complete: undefined,
 45 |     });
 46 |   }
 47 | 
 48 |   handleRequest: ReadResourceTemplateCallback = async (
 49 |     uri: URL,
 50 |     variables: Variables
 51 |   ): Promise<{ contents: FormattedResultItem[] }> => {
 52 |     const type = variables.type as string;
 53 |     // Correct variable access key: 'name', not 'name*'
 54 |     const nameVar = variables['name']; // Can be string or string[]
 55 |     const mapUriSuffix = `components/${type}`;
 56 |     const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
 57 |     let resultItems: RenderResultItem[];
 58 | 
 59 |     try {
 60 |       if (!VALID_COMPONENT_TYPES.includes(type as ComponentType)) {
 61 |         throw new Error(`Invalid component type: ${type}`);
 62 |       }
 63 |       const componentType = type as ComponentType;
 64 | 
 65 |       // Normalize names: Handle string for single value, array for multiple.
 66 |       let names: string[] = [];
 67 |       if (Array.isArray(nameVar)) {
 68 |         names = nameVar.map(n => String(n).trim()); // Ensure elements are strings
 69 |       } else if (typeof nameVar === 'string') {
 70 |         names = [nameVar.trim()]; // Treat as single item array
 71 |       }
 72 |       names = names.filter(n => n.length > 0); // Remove empty strings
 73 | 
 74 |       if (names.length === 0) {
 75 |         throw new Error('No valid component name specified.');
 76 |       }
 77 | 
 78 |       const spec = await this.specLoader.getTransformedSpec({
 79 |         resourceType: 'schema', // Use 'schema' for now
 80 |         format: 'openapi',
 81 |       });
 82 | 
 83 |       // Use imported type guard
 84 |       if (!isOpenAPIV3(spec)) {
 85 |         throw new Error('Only OpenAPI v3 specifications are supported');
 86 |       }
 87 | 
 88 |       // --- Use helper to get validated component map ---
 89 |       const componentMapObj = getValidatedComponentMap(spec, componentType);
 90 | 
 91 |       // --- Create Map and use helper to get validated component names/details ---
 92 |       // Create the Map from the validated object
 93 |       const detailsMap = new Map(Object.entries(componentMapObj));
 94 |       // Pass the Map to the helper
 95 |       const validDetails = getValidatedComponentDetails(detailsMap, names, componentType);
 96 |       const validNames = validDetails.map(detail => detail.name); // Extract names
 97 | 
 98 |       // Instantiate RenderableComponentMap with the validated map object
 99 |       const renderableMap = new RenderableComponentMap(
100 |         componentMapObj, // componentMapObj retrieved safely via helper
101 |         componentType,
102 |         mapUriSuffix
103 |       );
104 |       // Pass the validated names to the rendering function
105 |       resultItems = renderableMap.renderComponentDetail(context, validNames);
106 |     } catch (error: unknown) {
107 |       // Catch errors from helpers (e.g., type/name not found) or rendering
108 |       const message = error instanceof Error ? error.message : String(error);
109 |       console.error(`Error handling request ${uri.href}: ${message}`);
110 |       // Create a single error item representing the overall request failure
111 |       resultItems = createErrorResult(
112 |         uri.href.substring(BASE_URI.length), // Use request URI suffix
113 |         message
114 |       );
115 |     }
116 | 
117 |     // Use imported formatResults
118 |     const contents = formatResults(context, resultItems);
119 |     return { contents };
120 |   };
121 | }
122 | 
```

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

```typescript
  1 | import {
  2 |   ReadResourceTemplateCallback,
  3 |   ResourceTemplate,
  4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
  5 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
  6 | import { SpecLoaderService } from '../types.js';
  7 | import { IFormatter } from '../services/formatters.js';
  8 | import { RenderablePathItem } from '../rendering/path-item.js';
  9 | import { RenderContext, RenderResultItem } from '../rendering/types.js';
 10 | import { createErrorResult } from '../rendering/utils.js';
 11 | import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
 12 | // Import shared handler utils
 13 | import {
 14 |   formatResults,
 15 |   isOpenAPIV3,
 16 |   FormattedResultItem,
 17 |   getValidatedPathItem, // Import new helper
 18 |   getValidatedOperations, // Import new helper
 19 | } from './handler-utils.js'; // Already has .js
 20 | 
 21 | const BASE_URI = 'openapi://';
 22 | 
 23 | // Removed duplicated FormattedResultItem type - now imported from handler-utils
 24 | // Removed duplicated formatResults function - now imported from handler-utils
 25 | // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
 26 | 
 27 | /**
 28 |  * Handles requests for specific operation details within a path.
 29 |  * Corresponds to the `openapi://paths/{path}/{method*}` template.
 30 |  */
 31 | export class OperationHandler {
 32 |   constructor(
 33 |     private specLoader: SpecLoaderService,
 34 |     private formatter: IFormatter
 35 |   ) {}
 36 | 
 37 |   getTemplate(): ResourceTemplate {
 38 |     // TODO: Add completion logic if needed
 39 |     return new ResourceTemplate(`${BASE_URI}paths/{path}/{method*}`, {
 40 |       list: undefined,
 41 |       complete: undefined,
 42 |     });
 43 |   }
 44 | 
 45 |   handleRequest: ReadResourceTemplateCallback = async (
 46 |     uri: URL,
 47 |     variables: Variables
 48 |   ): Promise<{ contents: FormattedResultItem[] }> => {
 49 |     const encodedPath = variables.path as string;
 50 |     // Correct variable access key: 'method', not 'method*'
 51 |     const methodVar = variables['method']; // Can be string or string[]
 52 |     // Decode the path received from the URI variable
 53 |     const decodedPath = decodeURIComponent(encodedPath || '');
 54 |     // Use the builder to create the suffix, which will re-encode the path correctly
 55 |     const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
 56 |     const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
 57 |     let resultItems: RenderResultItem[];
 58 | 
 59 |     try {
 60 |       // Normalize methods: Handle string for single value, array for multiple.
 61 |       let methods: string[] = [];
 62 |       if (Array.isArray(methodVar)) {
 63 |         methods = methodVar.map(m => String(m).trim().toLowerCase()); // Ensure elements are strings
 64 |       } else if (typeof methodVar === 'string') {
 65 |         methods = [methodVar.trim().toLowerCase()]; // Treat as single item array
 66 |       }
 67 |       methods = methods.filter(m => m.length > 0); // Remove empty strings
 68 | 
 69 |       if (methods.length === 0) {
 70 |         throw new Error('No valid HTTP method specified.');
 71 |       }
 72 | 
 73 |       const spec = await this.specLoader.getTransformedSpec({
 74 |         resourceType: 'schema', // Use 'schema' for now
 75 |         format: 'openapi',
 76 |       });
 77 | 
 78 |       // Use imported type guard
 79 |       if (!isOpenAPIV3(spec)) {
 80 |         throw new Error('Only OpenAPI v3 specifications are supported');
 81 |       }
 82 | 
 83 |       // --- Use helper to get validated path item ---
 84 |       const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
 85 |       const pathItemObj = getValidatedPathItem(spec, lookupPath);
 86 | 
 87 |       // --- Use helper to get validated requested methods ---
 88 |       const validMethods = getValidatedOperations(pathItemObj, methods, lookupPath);
 89 | 
 90 |       // Instantiate RenderablePathItem with the validated pathItemObj
 91 |       const renderablePathItem = new RenderablePathItem(
 92 |         pathItemObj, // pathItemObj retrieved safely via helper
 93 |         lookupPath, // Pass the raw, decoded path
 94 |         pathUriSuffix // Pass the correctly built suffix
 95 |       );
 96 | 
 97 |       // Use the validated methods returned by the helper
 98 |       resultItems = renderablePathItem.renderOperationDetail(context, validMethods);
 99 |     } catch (error: unknown) {
100 |       // Catch errors from helpers (e.g., path/method not found) or rendering
101 |       const message = error instanceof Error ? error.message : String(error);
102 |       console.error(`Error handling request ${uri.href}: ${message}`);
103 |       // Create a single error item representing the overall request failure
104 |       resultItems = createErrorResult(
105 |         uri.href.substring(BASE_URI.length), // Use request URI suffix
106 |         message
107 |       );
108 |     }
109 | 
110 |     // Use imported formatResults
111 |     const contents = formatResults(context, resultItems);
112 |     return { contents };
113 |   };
114 | }
115 | 
```

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

```markdown
  1 | # MCP OpenAPI Schema Explorer
  2 | 
  3 | [![Docker Pulls](https://img.shields.io/docker/pulls/kadykov/mcp-openapi-schema-explorer.svg)](https://hub.docker.com/r/kadykov/mcp-openapi-schema-explorer)
  4 | [![GitHub Repo](https://img.shields.io/badge/GitHub-kadykov/mcp--openapi--schema--explorer-blue?logo=github)](https://github.com/kadykov/mcp-openapi-schema-explorer)
  5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  6 | 
  7 | 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**.
  8 | 
  9 | 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.
 10 | 
 11 | **Source Code & Full Documentation:** [https://github.com/kadykov/mcp-openapi-schema-explorer](https://github.com/kadykov/mcp-openapi-schema-explorer)
 12 | 
 13 | ## Features
 14 | 
 15 | - **MCP Resource Access:** Explore OpenAPI specs via intuitive URIs (`openapi://info`, `openapi://paths/...`, `openapi://components/...`).
 16 | - **OpenAPI v3.0 & Swagger v2.0 Support:** Loads both formats, automatically converting v2.0 to v3.0.
 17 | - **Local & Remote Files:** Load specs from local file paths (via volume mount) or HTTP/HTTPS URLs.
 18 | - **Token-Efficient:** Designed to minimize token usage for LLMs.
 19 | - **Multiple Output Formats:** Get detailed views in JSON (default), YAML, or minified JSON (`--output-format`).
 20 | - **Dynamic Server Name:** Server name in MCP clients reflects the `info.title` from the loaded spec.
 21 | - **Reference Transformation:** Internal `$ref`s (`#/components/...`) are transformed into clickable MCP URIs.
 22 | 
 23 | ## How to Run
 24 | 
 25 | Pull the image:
 26 | 
 27 | ```bash
 28 | docker pull kadykov/mcp-openapi-schema-explorer:latest
 29 | ```
 30 | 
 31 | The container expects the path or URL to the OpenAPI specification as a command-line argument.
 32 | 
 33 | ### Using a Remote Specification URL
 34 | 
 35 | Pass the URL directly to `docker run`:
 36 | 
 37 | ```bash
 38 | docker run --rm -i kadykov/mcp-openapi-schema-explorer:latest https://petstore3.swagger.io/api/v3/openapi.json
 39 | ```
 40 | 
 41 | ### Using a Local Specification File
 42 | 
 43 | Mount your local file into the container using the `-v` flag and provide the path _inside the container_ as the argument:
 44 | 
 45 | ```bash
 46 | # Example: Mount local file ./my-spec.yaml to /spec/api.yaml inside the container
 47 | docker run --rm -i -v "$(pwd)/my-spec.yaml:/spec/api.yaml" kadykov/mcp-openapi-schema-explorer:latest /spec/api.yaml
 48 | ```
 49 | 
 50 | _(Note: Replace `$(pwd)/my-spec.yaml` with the actual absolute path to your local file on the host machine)_
 51 | 
 52 | ### Specifying Output Format
 53 | 
 54 | Use the `--output-format` flag (optional, defaults to `json`):
 55 | 
 56 | ```bash
 57 | # Using YAML output with a remote URL
 58 | docker run --rm -i kadykov/mcp-openapi-schema-explorer:latest https://petstore3.swagger.io/api/v3/openapi.json --output-format yaml
 59 | 
 60 | # Using minified JSON with a local file
 61 | 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
 62 | ```
 63 | 
 64 | Supported formats: `json`, `yaml`, `json-minified`.
 65 | 
 66 | ## Tags
 67 | 
 68 | - `latest`: Points to the most recent stable release.
 69 | - Specific version tags (e.g., `1.2.1`) are available corresponding to the npm package versions.
 70 | 
 71 | ## Usage with MCP Clients
 72 | 
 73 | You can configure your MCP client (like Cline or Claude Desktop) to run this Docker image as an MCP server.
 74 | 
 75 | ### Example: Remote URL Specification
 76 | 
 77 | ```json
 78 | // Example: ~/.config/cline/mcp_config.json
 79 | {
 80 |   "mcpServers": {
 81 |     "My API Spec (Docker Remote)": {
 82 |       "command": "docker",
 83 |       "args": [
 84 |         "run",
 85 |         "--rm",
 86 |         "-i", // Required for MCP communication
 87 |         "kadykov/mcp-openapi-schema-explorer:latest",
 88 |         "https://petstore3.swagger.io/api/v3/openapi.json"
 89 |         // Optional: Add "--output-format", "yaml" here if needed
 90 |       ],
 91 |       "env": {}
 92 |     }
 93 |   }
 94 | }
 95 | ```
 96 | 
 97 | ### Example: Local File Specification
 98 | 
 99 | ```json
100 | // Example: ~/.config/cline/mcp_config.json
101 | {
102 |   "mcpServers": {
103 |     "My API Spec (Docker Local)": {
104 |       "command": "docker",
105 |       "args": [
106 |         "run",
107 |         "--rm",
108 |         "-i", // Required for MCP communication
109 |         "-v",
110 |         "/full/path/to/your/local/openapi.yaml:/spec/api.yaml", // Host path : Container path
111 |         "kadykov/mcp-openapi-schema-explorer:latest",
112 |         "/spec/api.yaml", // Path inside the container
113 |         "--output-format",
114 |         "yaml" // Optional format
115 |       ],
116 |       "env": {}
117 |     }
118 |   }
119 | }
120 | ```
121 | 
122 | _(Remember to replace `/full/path/to/your/local/openapi.yaml` with the correct absolute path on your host machine)_
123 | 
124 | ## Support
125 | 
126 | 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).
127 | 
```

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

```typescript
  1 | import {
  2 |   buildComponentDetailUri,
  3 |   buildComponentMapUri,
  4 |   buildOperationUri,
  5 |   buildPathItemUri,
  6 |   buildTopLevelFieldUri,
  7 |   buildComponentDetailUriSuffix,
  8 |   buildComponentMapUriSuffix,
  9 |   buildOperationUriSuffix,
 10 |   buildPathItemUriSuffix,
 11 |   buildTopLevelFieldUriSuffix,
 12 | } from '../../../../src/utils/uri-builder';
 13 | 
 14 | describe('URI Builder Utilities', () => {
 15 |   // --- Full URI Builders ---
 16 | 
 17 |   test('buildComponentDetailUri builds correct URI', () => {
 18 |     expect(buildComponentDetailUri('schemas', 'MySchema')).toBe(
 19 |       'openapi://components/schemas/MySchema'
 20 |     );
 21 |     expect(buildComponentDetailUri('responses', 'NotFound')).toBe(
 22 |       'openapi://components/responses/NotFound'
 23 |     );
 24 |     // Test with characters that might need encoding if rules change (but currently don't)
 25 |     expect(buildComponentDetailUri('parameters', 'user-id')).toBe(
 26 |       'openapi://components/parameters/user-id'
 27 |     );
 28 |   });
 29 | 
 30 |   test('buildComponentMapUri builds correct URI', () => {
 31 |     expect(buildComponentMapUri('schemas')).toBe('openapi://components/schemas');
 32 |     expect(buildComponentMapUri('parameters')).toBe('openapi://components/parameters');
 33 |   });
 34 | 
 35 |   test('buildOperationUri builds correct URI and encodes path (no leading slash)', () => {
 36 |     expect(buildOperationUri('/users', 'get')).toBe('openapi://paths/users/get'); // No leading slash encoded
 37 |     expect(buildOperationUri('/users/{userId}', 'post')).toBe(
 38 |       'openapi://paths/users%2F%7BuserId%7D/post' // Path encoded, no leading %2F
 39 |     );
 40 |     expect(buildOperationUri('/pets/{petId}/uploadImage', 'post')).toBe(
 41 |       'openapi://paths/pets%2F%7BpetId%7D%2FuploadImage/post' // Path encoded, no leading %2F
 42 |     );
 43 |     expect(buildOperationUri('users', 'get')).toBe('openapi://paths/users/get'); // Handles no leading slash input
 44 |     expect(buildOperationUri('users/{userId}', 'post')).toBe(
 45 |       'openapi://paths/users%2F%7BuserId%7D/post' // Handles no leading slash input
 46 |     );
 47 |     expect(buildOperationUri('/users', 'GET')).toBe('openapi://paths/users/get'); // Method lowercased
 48 |   });
 49 | 
 50 |   test('buildPathItemUri builds correct URI and encodes path (no leading slash)', () => {
 51 |     expect(buildPathItemUri('/users')).toBe('openapi://paths/users'); // No leading slash encoded
 52 |     expect(buildPathItemUri('/users/{userId}')).toBe('openapi://paths/users%2F%7BuserId%7D'); // Path encoded, no leading %2F
 53 |     expect(buildPathItemUri('/pets/{petId}/uploadImage')).toBe(
 54 |       'openapi://paths/pets%2F%7BpetId%7D%2FuploadImage' // Path encoded, no leading %2F
 55 |     );
 56 |     expect(buildPathItemUri('users')).toBe('openapi://paths/users'); // Handles no leading slash input
 57 |     expect(buildPathItemUri('users/{userId}')).toBe('openapi://paths/users%2F%7BuserId%7D'); // Handles no leading slash input
 58 |   });
 59 | 
 60 |   test('buildTopLevelFieldUri builds correct URI', () => {
 61 |     expect(buildTopLevelFieldUri('info')).toBe('openapi://info');
 62 |     expect(buildTopLevelFieldUri('paths')).toBe('openapi://paths');
 63 |     expect(buildTopLevelFieldUri('components')).toBe('openapi://components');
 64 |   });
 65 | 
 66 |   // --- URI Suffix Builders ---
 67 | 
 68 |   test('buildComponentDetailUriSuffix builds correct suffix', () => {
 69 |     expect(buildComponentDetailUriSuffix('schemas', 'MySchema')).toBe(
 70 |       'components/schemas/MySchema'
 71 |     );
 72 |     expect(buildComponentDetailUriSuffix('responses', 'NotFound')).toBe(
 73 |       'components/responses/NotFound'
 74 |     );
 75 |   });
 76 | 
 77 |   test('buildComponentMapUriSuffix builds correct suffix', () => {
 78 |     expect(buildComponentMapUriSuffix('schemas')).toBe('components/schemas');
 79 |     expect(buildComponentMapUriSuffix('parameters')).toBe('components/parameters');
 80 |   });
 81 | 
 82 |   test('buildOperationUriSuffix builds correct suffix and encodes path (no leading slash)', () => {
 83 |     expect(buildOperationUriSuffix('/users', 'get')).toBe('paths/users/get'); // No leading slash encoded
 84 |     expect(buildOperationUriSuffix('/users/{userId}', 'post')).toBe(
 85 |       'paths/users%2F%7BuserId%7D/post' // Path encoded, no leading %2F
 86 |     );
 87 |     expect(buildOperationUriSuffix('users/{userId}', 'post')).toBe(
 88 |       'paths/users%2F%7BuserId%7D/post' // Handles no leading slash input
 89 |     );
 90 |     expect(buildOperationUriSuffix('/users', 'GET')).toBe('paths/users/get'); // Method lowercased
 91 |   });
 92 | 
 93 |   test('buildPathItemUriSuffix builds correct suffix and encodes path (no leading slash)', () => {
 94 |     expect(buildPathItemUriSuffix('/users')).toBe('paths/users'); // No leading slash encoded
 95 |     expect(buildPathItemUriSuffix('/users/{userId}')).toBe('paths/users%2F%7BuserId%7D'); // Path encoded, no leading %2F
 96 |     expect(buildPathItemUriSuffix('users/{userId}')).toBe('paths/users%2F%7BuserId%7D'); // Handles no leading slash input
 97 |   });
 98 | 
 99 |   test('buildTopLevelFieldUriSuffix builds correct suffix', () => {
100 |     expect(buildTopLevelFieldUriSuffix('info')).toBe('info');
101 |     expect(buildTopLevelFieldUriSuffix('paths')).toBe('paths');
102 |   });
103 | });
104 | 
```

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

```typescript
  1 | /**
  2 |  * Utility functions for building standardized MCP URIs for this server.
  3 |  */
  4 | 
  5 | const BASE_URI_SCHEME = 'openapi://';
  6 | 
  7 | /**
  8 |  * Encodes a string component for safe inclusion in a URI path segment.
  9 |  * Uses standard encodeURIComponent.
 10 |  * Encodes a path string for safe inclusion in a URI.
 11 |  * This specifically targets path strings which might contain characters
 12 |  * like '{', '}', etc., that need encoding when forming the URI path part.
 13 |  * Uses standard encodeURIComponent.
 14 |  * Encodes a path string for safe inclusion in a URI path segment.
 15 |  * This is necessary because the path segment comes from the user potentially
 16 |  * containing characters that need encoding (like '{', '}').
 17 |  * Uses standard encodeURIComponent.
 18 |  * @param path The path string to encode.
 19 |  * @returns The encoded path string, with leading slashes removed before encoding.
 20 |  */
 21 | export function encodeUriPathComponent(path: string): string {
 22 |   // Added export
 23 |   // Remove leading slashes before encoding
 24 |   const pathWithoutLeadingSlash = path.replace(/^\/+/, '');
 25 |   return encodeURIComponent(pathWithoutLeadingSlash);
 26 | }
 27 | 
 28 | // --- Full URI Builders ---
 29 | 
 30 | /**
 31 |  * Builds the URI for accessing a specific component's details.
 32 |  * Example: openapi://components/schemas/MySchema
 33 |  * @param type The component type (e.g., 'schemas', 'responses').
 34 |  * @param name The component name.
 35 |  * @returns The full component detail URI.
 36 |  */
 37 | export function buildComponentDetailUri(type: string, name: string): string {
 38 |   // Per user instruction, do not encode type or name here.
 39 |   return `${BASE_URI_SCHEME}components/${type}/${name}`;
 40 | }
 41 | 
 42 | /**
 43 |  * Builds the URI for listing components of a specific type.
 44 |  * Example: openapi://components/schemas
 45 |  * @param type The component type (e.g., 'schemas', 'responses').
 46 |  * @returns The full component map URI.
 47 |  */
 48 | export function buildComponentMapUri(type: string): string {
 49 |   // Per user instruction, do not encode type here.
 50 |   return `${BASE_URI_SCHEME}components/${type}`;
 51 | }
 52 | 
 53 | /**
 54 |  * Builds the URI for accessing a specific operation's details.
 55 |  * Example: openapi://paths/users/{userId}/GET
 56 |  * @param path The API path (e.g., '/users/{userId}').
 57 |  * @param method The HTTP method (e.g., 'GET', 'POST').
 58 |  * @returns The full operation detail URI.
 59 |  */
 60 | export function buildOperationUri(path: string, method: string): string {
 61 |   // Encode only the path component. Assume 'path' is raw/decoded.
 62 |   // Method is assumed to be safe or handled by SDK/client.
 63 |   return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`; // Standardize method to lowercase
 64 | }
 65 | 
 66 | /**
 67 |  * Builds the URI for listing methods available at a specific path.
 68 |  * Example: openapi://paths/users/{userId}
 69 |  * @param path The API path (e.g., '/users/{userId}').
 70 |  * @returns The full path item URI.
 71 |  */
 72 | export function buildPathItemUri(path: string): string {
 73 |   // Encode only the path component. Assume 'path' is raw/decoded.
 74 |   return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}`;
 75 | }
 76 | 
 77 | /**
 78 |  * Builds the URI for accessing a top-level field (like 'info' or 'servers')
 79 |  * or triggering a list view ('paths', 'components').
 80 |  * Example: openapi://info, openapi://paths
 81 |  * @param field The top-level field name.
 82 |  * @returns The full top-level field URI.
 83 |  */
 84 | export function buildTopLevelFieldUri(field: string): string {
 85 |   // Per user instruction, do not encode field here.
 86 |   return `${BASE_URI_SCHEME}${field}`;
 87 | }
 88 | 
 89 | // --- URI Suffix Builders (for RenderResultItem) ---
 90 | 
 91 | /**
 92 |  * Builds the URI suffix for a specific component's details.
 93 |  * Example: components/schemas/MySchema
 94 |  */
 95 | export function buildComponentDetailUriSuffix(type: string, name: string): string {
 96 |   // Per user instruction, do not encode type or name here.
 97 |   return `components/${type}/${name}`;
 98 | }
 99 | 
100 | /**
101 |  * Builds the URI suffix for listing components of a specific type.
102 |  * Example: components/schemas
103 |  */
104 | export function buildComponentMapUriSuffix(type: string): string {
105 |   // Per user instruction, do not encode type here.
106 |   return `components/${type}`;
107 | }
108 | 
109 | /**
110 |  * Builds the URI suffix for a specific operation's details.
111 |  * Example: paths/users/{userId}/get
112 |  */
113 | export function buildOperationUriSuffix(path: string, method: string): string {
114 |   // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
115 |   return `paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`;
116 | }
117 | 
118 | /**
119 |  * Builds the URI suffix for listing methods available at a specific path.
120 |  * Example: paths/users/{userId}
121 |  */
122 | export function buildPathItemUriSuffix(path: string): string {
123 |   // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
124 |   return `paths/${encodeUriPathComponent(path)}`;
125 | }
126 | 
127 | /**
128 |  * Builds the URI suffix for a top-level field.
129 |  * Example: info, paths
130 |  */
131 | export function buildTopLevelFieldUriSuffix(field: string): string {
132 |   // Per user instruction, do not encode field here.
133 |   return field;
134 | }
135 | 
```

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

```markdown
  1 | # Technical Context
  2 | 
  3 | ## Development Stack
  4 | 
  5 | - TypeScript for implementation
  6 | - MCP SDK for server functionality
  7 | - Jest for testing
  8 | - npm for package distribution
  9 | 
 10 | ## Key Dependencies
 11 | 
 12 | - `@modelcontextprotocol/sdk`: Core MCP functionality
 13 | - `swagger2openapi`: OpenAPI/Swagger spec loading, parsing, and v2->v3 conversion (Runtime dependency)
 14 | - `js-yaml`: YAML parsing (Runtime dependency)
 15 | - `zod`: Schema validation (Runtime dependency)
 16 | - `openapi-types`: OpenAPI type definitions (devDependency)
 17 | - `typescript`: TypeScript compiler (devDependency)
 18 | - `@types/*`: Various type definitions (devDependencies)
 19 | - `jest`: Testing framework (devDependency)
 20 | - `eslint`: Code linting (devDependency)
 21 | - `prettier`: Code formatting (devDependency)
 22 | - `semantic-release` & plugins (`@semantic-release/*`, `@codedependant/semantic-release-docker`): Automated releases (devDependencies)
 23 | - `just`: Task runner (Used locally, installed via action in CI)
 24 | 
 25 | ## Technical Requirements
 26 | 
 27 | 1. Must follow MCP protocol specifications.
 28 | 2. Must handle large OpenAPI/Swagger specs efficiently.
 29 | 3. Must provide type-safe reference handling (transforming internal refs to MCP URIs).
 30 | 4. Must support loading specs from local file paths and remote HTTP/HTTPS URLs.
 31 | 5. Must support OpenAPI v3.0 and Swagger v2.0 formats (with v2.0 being converted to v3.0).
 32 | 6. Must be easily testable and maintainable.
 33 | 
 34 | ## Development Environment
 35 | 
 36 | - TypeScript setup with strict type checking
 37 | - Jest testing framework with coverage
 38 | - ESLint for code quality
 39 | - Prettier for code formatting
 40 | - `just` task runner (`justfile`) for common development tasks (build, test, lint, etc.)
 41 | - Conventional Commits standard for commit messages (required for `semantic-release`)
 42 | - Test fixtures and helpers
 43 | 
 44 | ## Code Organization
 45 | 
 46 | - Services layer:
 47 |   - `SpecLoaderService`: Uses `swagger2openapi` to load specs from files/URLs and handle v2->v3 conversion.
 48 |   - `ReferenceTransformService`: Transforms internal `#/components/...` refs to MCP URIs.
 49 |   - `Formatters`: Handle JSON/YAML output.
 50 | - Handlers layer for resource endpoints.
 51 | - Rendering layer for generating resource content.
 52 | - Utilities (e.g., URI builder).
 53 | - Strong typing with generics.
 54 | - Comprehensive test coverage.
 55 | 
 56 | ## Testing Infrastructure
 57 | 
 58 | - Unit tests:
 59 |   - `SpecLoaderService` (mocking `swagger2openapi`).
 60 |   - `ReferenceTransformService`.
 61 |   - Rendering classes.
 62 |   - Handlers (mocking services).
 63 | - End-to-end tests:
 64 |   - Verify resource access for local v3, local v2, and remote v3 specs.
 65 |   - Test multi-value parameters.
 66 |   - Cover success and error scenarios.
 67 |   - Verify resource completion logic using `client.complete()`.
 68 | - Type-safe test utilities (`mcp-test-helpers`).
 69 | - Test fixtures (including v2.0 and v3.0 examples).
 70 | - Coverage reporting via Jest and upload to Codecov via GitHub Actions.
 71 | - CI Integration (`.github/workflows/ci.yml`):
 72 |   - Runs checks (`just all`, `just security`, CodeQL) on pushes/PRs to `main`.
 73 |   - Uses Node 22 environment.
 74 |   - Includes automated release job using `cycjimmy/semantic-release-action@v4`.
 75 | 
 76 | ## Response Formats
 77 | 
 78 | 1. Base Formats
 79 | 
 80 |    - JSON format (default format)
 81 |    - YAML format support
 82 |    - URI-based reference links
 83 |    - Token-efficient structure
 84 |    - OpenAPI v3 type compliance
 85 | 
 86 | 2. Format Service
 87 | 
 88 |    - Pluggable formatter architecture
 89 |    - Format-specific MIME types (`application/json`, `text/yaml`)
 90 |    - Type-safe formatter interface (`IFormatter`)
 91 |    - Consistent error formatting (`text/plain`)
 92 |    - CLI-configurable output format (`--output-format`)
 93 | 
 94 | 3. Implementation
 95 |    - Format-specific serialization
 96 |    - Shared type system
 97 |    - Error response handling
 98 |    - Multiple operation support
 99 |    - Reference transformation
100 | 
101 | ## Deployment / Release Process
102 | 
103 | - Automated publishing to npm **and Docker Hub** (`kadykov/mcp-openapi-schema-explorer`) via `semantic-release` triggered by pushes to `main` branch in GitHub Actions.
104 | - Uses `cycjimmy/semantic-release-action@v4` in the CI workflow.
105 | - Relies on Conventional Commits to determine version bumps.
106 | - Uses `@codedependant/semantic-release-docker` plugin for Docker build and push.
107 | - Creates version tags (e.g., `v1.2.3`) and GitHub Releases automatically.
108 | - Requires `NPM_TOKEN`, `DOCKERHUB_USERNAME`, and `DOCKERHUB_TOKEN` secrets/variables configured in GitHub repository.
109 | - `CHANGELOG.md` is automatically generated and updated.
110 | - Server version is dynamically set at runtime based on the release version.
111 | 
112 | ## Configuration
113 | 
114 | - Command-line argument based configuration (`src/config.ts`).
115 | - Single required argument: `<path-or-url-to-spec>`.
116 | - Optional argument: `--output-format <json|yaml|json-minified>`.
117 | - Required argument validation.
118 | - TypeScript type safety (`ServerConfig` interface).
119 | - Error handling for missing/invalid arguments.
120 | 
121 | ## Error Handling
122 | 
123 | - Descriptive error messages
124 | - Type-safe error handling
125 | - Consistent error format
126 | - Proper error propagation
127 | 
128 | ## Future Extensions
129 | 
130 | - AsyncAPI format support
131 | - GraphQL schema support
132 | - External reference resolution
133 | - Enhanced schema resources
134 | - Reference validation
135 | 
```

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

```typescript
  1 | // NOTE: This block replaces the previous import block to ensure types/interfaces are defined correctly.
  2 | import { OpenAPIV3 } from 'openapi-types';
  3 | import { RenderContext, RenderResultItem } from './types.js'; // Add .js
  4 | import {
  5 |   buildComponentDetailUriSuffix,
  6 |   buildComponentMapUriSuffix,
  7 |   buildOperationUriSuffix,
  8 |   // buildPathItemUriSuffix, // Not currently used by generateListHint
  9 | } from '../utils/uri-builder.js'; // Added .js extension
 10 | 
 11 | // Define possible types for list items to guide hint generation
 12 | type ListItemType = 'componentType' | 'componentName' | 'pathMethod';
 13 | 
 14 | // Define context needed for generating the correct detail URI suffix
 15 | interface HintContext {
 16 |   itemType: ListItemType;
 17 |   firstItemExample?: string; // Example value from the first item in the list
 18 |   // For componentName hints, the parent component type is needed
 19 |   parentComponentType?: string;
 20 |   // For pathMethod hints, the parent path is needed
 21 |   parentPath?: string;
 22 | }
 23 | 
 24 | /**
 25 |  * Safely retrieves the summary from an Operation object.
 26 |  * Handles cases where the operation might be undefined or lack a summary.
 27 |  *
 28 |  * @param operation - The Operation object or undefined.
 29 |  * @returns The operation summary or operationId string, truncated if necessary, or null if neither is available.
 30 |  */
 31 | export function getOperationSummary(
 32 |   operation: OpenAPIV3.OperationObject | undefined
 33 | ): string | null {
 34 |   // Return summary or operationId without truncation
 35 |   return operation?.summary || operation?.operationId || null;
 36 | }
 37 | 
 38 | /**
 39 |  * Helper to generate a standard hint text for list views, using the centralized URI builders.
 40 |  * @param renderContext - The rendering context containing the base URI.
 41 |  * @param hintContext - Context about the type of items being listed and their parent context.
 42 |  * @returns The hint string.
 43 |  */
 44 | export function generateListHint(renderContext: RenderContext, hintContext: HintContext): string {
 45 |   let detailUriSuffixPattern: string;
 46 |   let itemTypeName: string; // User-friendly name for the item type in the hint text
 47 |   let exampleUriSuffix: string | undefined; // To hold the generated example URI
 48 | 
 49 |   switch (hintContext.itemType) {
 50 |     case 'componentType':
 51 |       // Listing component types (e.g., schemas, responses) at openapi://components
 52 |       // Hint should point to openapi://components/{type}
 53 |       detailUriSuffixPattern = buildComponentMapUriSuffix('{type}'); // Use placeholder
 54 |       itemTypeName = 'component type';
 55 |       if (hintContext.firstItemExample) {
 56 |         exampleUriSuffix = buildComponentMapUriSuffix(hintContext.firstItemExample);
 57 |       }
 58 |       break;
 59 |     case 'componentName':
 60 |       // Listing component names (e.g., MySchema, User) at openapi://components/{type}
 61 |       // Hint should point to openapi://components/{type}/{name}
 62 |       if (!hintContext.parentComponentType) {
 63 |         console.warn('generateListHint called for componentName without parentComponentType');
 64 |         return ''; // Avoid generating a broken hint
 65 |       }
 66 |       // Use the actual parent type and a placeholder for the name
 67 |       detailUriSuffixPattern = buildComponentDetailUriSuffix(
 68 |         hintContext.parentComponentType,
 69 |         '{name}'
 70 |       );
 71 |       itemTypeName = hintContext.parentComponentType.slice(0, -1); // e.g., 'schema' from 'schemas'
 72 |       if (hintContext.firstItemExample) {
 73 |         exampleUriSuffix = buildComponentDetailUriSuffix(
 74 |           hintContext.parentComponentType,
 75 |           hintContext.firstItemExample
 76 |         );
 77 |       }
 78 |       break;
 79 |     case 'pathMethod':
 80 |       // Listing methods (e.g., get, post) at openapi://paths/{path}
 81 |       // Hint should point to openapi://paths/{path}/{method}
 82 |       if (!hintContext.parentPath) {
 83 |         console.warn('generateListHint called for pathMethod without parentPath');
 84 |         return ''; // Avoid generating a broken hint
 85 |       }
 86 |       // Use the actual parent path and a placeholder for the method
 87 |       detailUriSuffixPattern = buildOperationUriSuffix(hintContext.parentPath, '{method}');
 88 |       itemTypeName = 'operation'; // Or 'method'? 'operation' seems clearer
 89 |       if (hintContext.firstItemExample) {
 90 |         // Ensure the example method is valid if needed, though usually it's just 'get', 'post' etc.
 91 |         exampleUriSuffix = buildOperationUriSuffix(
 92 |           hintContext.parentPath,
 93 |           hintContext.firstItemExample
 94 |         );
 95 |       }
 96 |       break;
 97 |     default:
 98 |       // Explicitly cast to string to avoid potential 'never' type issue in template literal
 99 |       console.warn(`Unknown itemType in generateListHint: ${String(hintContext.itemType)}`);
100 |       return ''; // Avoid generating a hint if context is unknown
101 |   }
102 | 
103 |   // Construct the full hint URI pattern using the base URI
104 |   const fullHintPattern = `${renderContext.baseUri}${detailUriSuffixPattern}`;
105 |   const fullExampleUri = exampleUriSuffix
106 |     ? `${renderContext.baseUri}${exampleUriSuffix}`
107 |     : undefined;
108 | 
109 |   let hintText = `\nHint: Use '${fullHintPattern}' to view details for a specific ${itemTypeName}.`;
110 |   if (fullExampleUri) {
111 |     hintText += ` (e.g., ${fullExampleUri})`;
112 |   }
113 | 
114 |   return hintText;
115 | }
116 | 
117 | /**
118 |  * Helper to generate a standard error item for RenderResultItem arrays.
119 |  * @param uriSuffix - The URI suffix for the error context.
120 |  * @param message - The error message.
121 |  * @returns A RenderResultItem array containing the error.
122 |  */
123 | export function createErrorResult(uriSuffix: string, message: string): RenderResultItem[] {
124 |   return [
125 |     {
126 |       uriSuffix: uriSuffix,
127 |       data: null,
128 |       isError: true,
129 |       errorText: message,
130 |       renderAsList: true, // Errors are typically plain text
131 |     },
132 |   ];
133 | }
134 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderableDocument } from '../../../../src/rendering/document';
  3 | import { RenderContext } from '../../../../src/rendering/types';
  4 | import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
  5 | 
  6 | // Mock Formatter
  7 | const mockFormatter: IFormatter = new JsonFormatter(); // Use JSON for predictable output
  8 | 
  9 | const mockContext: RenderContext = {
 10 |   formatter: mockFormatter,
 11 |   baseUri: 'openapi://',
 12 | };
 13 | 
 14 | // Sample OpenAPI Document Fixture
 15 | const sampleDoc: OpenAPIV3.Document = {
 16 |   openapi: '3.0.0',
 17 |   info: {
 18 |     title: 'Test API',
 19 |     version: '1.0.0',
 20 |   },
 21 |   paths: {
 22 |     '/test': {
 23 |       get: {
 24 |         summary: 'Test GET',
 25 |         responses: {
 26 |           '200': { description: 'OK' },
 27 |         },
 28 |       },
 29 |     },
 30 |   },
 31 |   components: {
 32 |     schemas: {
 33 |       TestSchema: { type: 'string' },
 34 |     },
 35 |   },
 36 |   servers: [{ url: 'http://localhost:3000' }],
 37 | };
 38 | 
 39 | describe('RenderableDocument', () => {
 40 |   let renderableDoc: RenderableDocument;
 41 | 
 42 |   beforeEach(() => {
 43 |     renderableDoc = new RenderableDocument(sampleDoc);
 44 |   });
 45 | 
 46 |   it('should instantiate correctly', () => {
 47 |     expect(renderableDoc).toBeInstanceOf(RenderableDocument);
 48 |   });
 49 | 
 50 |   // Test the internal detail rendering method
 51 |   describe('renderTopLevelFieldDetail', () => {
 52 |     it('should render detail for a valid top-level field (info)', () => {
 53 |       const fieldObject = renderableDoc.getTopLevelField('info');
 54 |       const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'info');
 55 | 
 56 |       expect(result).toHaveLength(1);
 57 |       expect(result[0]).toEqual({
 58 |         uriSuffix: 'info',
 59 |         data: sampleDoc.info, // Expect raw data
 60 |         isError: undefined, // Should default to false implicitly
 61 |         renderAsList: undefined, // Should default to false implicitly
 62 |       });
 63 |     });
 64 | 
 65 |     it('should render detail for another valid field (servers)', () => {
 66 |       const fieldObject = renderableDoc.getTopLevelField('servers');
 67 |       const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'servers');
 68 | 
 69 |       expect(result).toHaveLength(1);
 70 |       expect(result[0]).toEqual({
 71 |         uriSuffix: 'servers',
 72 |         data: sampleDoc.servers,
 73 |         isError: undefined,
 74 |         renderAsList: undefined,
 75 |       });
 76 |     });
 77 | 
 78 |     it('should return error for non-existent field', () => {
 79 |       const fieldObject = renderableDoc.getTopLevelField('nonexistent');
 80 |       const result = renderableDoc.renderTopLevelFieldDetail(
 81 |         mockContext,
 82 |         fieldObject, // Will be undefined
 83 |         'nonexistent'
 84 |       );
 85 | 
 86 |       expect(result).toHaveLength(1);
 87 |       expect(result[0]).toEqual({
 88 |         uriSuffix: 'nonexistent',
 89 |         data: null,
 90 |         isError: true,
 91 |         errorText: 'Error: Field "nonexistent" not found in the OpenAPI document.',
 92 |         renderAsList: true,
 93 |       });
 94 |     });
 95 | 
 96 |     it('should return error when trying to render "paths" via detail method', () => {
 97 |       const fieldObject = renderableDoc.getTopLevelField('paths');
 98 |       const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'paths');
 99 | 
100 |       expect(result).toHaveLength(1);
101 |       expect(result[0]).toEqual({
102 |         uriSuffix: 'paths',
103 |         data: null,
104 |         isError: true,
105 |         errorText: `Error: Field "paths" should be accessed via its list view (${mockContext.baseUri}paths). Use the list view first.`,
106 |         renderAsList: true,
107 |       });
108 |     });
109 | 
110 |     it('should return error when trying to render "components" via detail method', () => {
111 |       const fieldObject = renderableDoc.getTopLevelField('components');
112 |       const result = renderableDoc.renderTopLevelFieldDetail(
113 |         mockContext,
114 |         fieldObject,
115 |         'components'
116 |       );
117 | 
118 |       expect(result).toHaveLength(1);
119 |       expect(result[0]).toEqual({
120 |         uriSuffix: 'components',
121 |         data: null,
122 |         isError: true,
123 |         errorText: `Error: Field "components" should be accessed via its list view (${mockContext.baseUri}components). Use the list view first.`,
124 |         renderAsList: true,
125 |       });
126 |     });
127 |   });
128 | 
129 |   // Test the interface methods (which currently return errors)
130 |   describe('Interface Methods', () => {
131 |     it('renderList should return error', () => {
132 |       const result = renderableDoc.renderList(mockContext);
133 |       expect(result).toHaveLength(1);
134 |       expect(result[0]).toMatchObject({
135 |         uriSuffix: 'error',
136 |         isError: true,
137 |         errorText: expect.stringContaining(
138 |           'List rendering is only supported for specific fields'
139 |         ) as string,
140 |         renderAsList: true,
141 |       });
142 |     });
143 | 
144 |     it('renderDetail should return error', () => {
145 |       const result = renderableDoc.renderDetail(mockContext);
146 |       expect(result).toHaveLength(1);
147 |       expect(result[0]).toMatchObject({
148 |         uriSuffix: 'error',
149 |         isError: true,
150 |         errorText: expect.stringContaining(
151 |           'Detail rendering requires specifying a top-level field'
152 |         ) as string,
153 |         renderAsList: true,
154 |       });
155 |     });
156 |   });
157 | 
158 |   // Test helper methods
159 |   describe('Helper Methods', () => {
160 |     it('getPathsObject should return paths', () => {
161 |       expect(renderableDoc.getPathsObject()).toBe(sampleDoc.paths);
162 |     });
163 |     it('getComponentsObject should return components', () => {
164 |       expect(renderableDoc.getComponentsObject()).toBe(sampleDoc.components);
165 |     });
166 |     it('getTopLevelField should return correct field', () => {
167 |       expect(renderableDoc.getTopLevelField('info')).toBe(sampleDoc.info);
168 |       expect(renderableDoc.getTopLevelField('servers')).toBe(sampleDoc.servers);
169 |       expect(renderableDoc.getTopLevelField('nonexistent')).toBeUndefined();
170 |     });
171 |   });
172 | });
173 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
  3 | // No longer need ResourceContents here
  4 | 
  5 | // Placeholder for other renderable objects we'll create
  6 | // import { RenderablePaths } from './paths.js'; // Add .js
  7 | // import { RenderableComponents } from './components.js'; // Add .js
  8 | 
  9 | /**
 10 |  * Wraps an OpenAPIV3.Document to make it renderable.
 11 |  * Handles rendering for top-level fields like 'info', 'servers', etc.
 12 |  * Delegates list rendering for 'paths' and 'components' to respective objects.
 13 |  */
 14 | export class RenderableDocument implements RenderableSpecObject {
 15 |   // TODO: Add RenderablePaths and RenderableComponents instances
 16 |   // private renderablePaths: RenderablePaths;
 17 |   // private renderableComponents: RenderableComponents;
 18 | 
 19 |   constructor(private document: OpenAPIV3.Document) {
 20 |     // Initialize renderable wrappers for paths and components here
 21 |     // this.renderablePaths = new RenderablePaths(document.paths);
 22 |     // this.renderableComponents = new RenderableComponents(document.components);
 23 |   }
 24 | 
 25 |   /**
 26 |    * Renders a list view. For the document level, this is intended
 27 |    * to be called only when the requested field is 'paths' or 'components'.
 28 |    * The actual routing/delegation will happen in the handler based on the field.
 29 |    */
 30 |   renderList(_context: RenderContext): RenderResultItem[] {
 31 |     // Prefix context with _
 32 |     // This method should ideally not be called directly on the document
 33 |     // without specifying 'paths' or 'components' as the field.
 34 |     // The handler for openapi://{field} will delegate to the appropriate
 35 |     // sub-object's renderList.
 36 |     // Returning an error result item.
 37 |     return [
 38 |       {
 39 |         uriSuffix: 'error',
 40 |         data: null, // No specific data for this error
 41 |         isError: true,
 42 |         errorText:
 43 |           'Error: List rendering is only supported for specific fields like "paths" or "components" at the top level.',
 44 |         renderAsList: true, // Errors often shown as plain text
 45 |       },
 46 |     ];
 47 |   }
 48 | 
 49 |   /**
 50 |    * Renders the detail view. For the document level, this should not be called
 51 |    * directly without specifying a field. The handler should call
 52 |    * `renderTopLevelFieldDetail` instead.
 53 |    */
 54 |   renderDetail(_context: RenderContext): RenderResultItem[] {
 55 |     // Prefix context with _
 56 |     // This method implementation fulfills the interface requirement,
 57 |     // but direct detail rendering of the whole document isn't meaningful here.
 58 |     return [
 59 |       {
 60 |         uriSuffix: 'error',
 61 |         data: null,
 62 |         isError: true,
 63 |         errorText:
 64 |           'Error: Detail rendering requires specifying a top-level field (e.g., "info", "servers").',
 65 |         renderAsList: true, // Errors often shown as plain text
 66 |       },
 67 |     ];
 68 |   }
 69 | 
 70 |   /**
 71 |    * Renders the detail view for a *specific* top-level field (e.g., 'info', 'servers').
 72 |    * This is called by the handler after identifying the field.
 73 |    *
 74 |    * @param context - The rendering context.
 75 |    * @param fieldObject - The actual top-level field object to render (e.g., document.info).
 76 |    * @param fieldName - The name of the field being rendered (e.g., 'info').
 77 |    * @returns An array of RenderResultItem representing the detail view.
 78 |    */
 79 |   renderTopLevelFieldDetail(
 80 |     context: RenderContext,
 81 |     fieldObject: unknown,
 82 |     fieldName: string
 83 |   ): RenderResultItem[] {
 84 |     // Ensure fieldObject is provided (handler should validate fieldName exists)
 85 |     if (fieldObject === undefined || fieldObject === null) {
 86 |       return [
 87 |         {
 88 |           uriSuffix: fieldName,
 89 |           data: null,
 90 |           isError: true,
 91 |           errorText: `Error: Field "${fieldName}" not found in the OpenAPI document.`,
 92 |           renderAsList: true,
 93 |         },
 94 |       ];
 95 |     }
 96 | 
 97 |     // Avoid rendering structural fields that have dedicated list views
 98 |     if (fieldName === 'paths' || fieldName === 'components') {
 99 |       return [
100 |         {
101 |           uriSuffix: fieldName,
102 |           data: null,
103 |           isError: true,
104 |           errorText: `Error: Field "${fieldName}" should be accessed via its list view (${context.baseUri}${fieldName}). Use the list view first.`,
105 |           renderAsList: true,
106 |         },
107 |       ];
108 |     }
109 | 
110 |     try {
111 |       // For successful detail rendering, return the data object itself.
112 |       // The handler will format it using the context.formatter.
113 |       return [
114 |         {
115 |           uriSuffix: fieldName,
116 |           data: fieldObject, // Pass the raw data
117 |           // isError defaults to false
118 |           // renderAsList defaults to false (meaning use detail formatter)
119 |         },
120 |       ];
121 |     } catch (error: unknown) {
122 |       // Handle potential errors during data access or initial checks
123 |       // Formatting errors will be caught by the handler later
124 |       return [
125 |         {
126 |           uriSuffix: fieldName,
127 |           data: null,
128 |           isError: true,
129 |           errorText: `Error preparing field "${fieldName}" for rendering: ${
130 |             error instanceof Error ? error.message : String(error)
131 |           }`,
132 |           renderAsList: true,
133 |         },
134 |       ];
135 |     }
136 |   } // End of renderTopLevelFieldDetail
137 | 
138 |   // --- Helper methods to access specific parts ---
139 | 
140 |   getPathsObject(): OpenAPIV3.PathsObject | undefined {
141 |     return this.document.paths;
142 |   }
143 | 
144 |   getComponentsObject(): OpenAPIV3.ComponentsObject | undefined {
145 |     return this.document.components;
146 |   }
147 | 
148 |   getTopLevelField(fieldName: string): unknown {
149 |     // Define allowed top-level OpenAPI document properties
150 |     const allowedFields: Array<keyof OpenAPIV3.Document> = [
151 |       'openapi',
152 |       'info',
153 |       'servers',
154 |       'paths',
155 |       'components',
156 |       'security',
157 |       'tags',
158 |       'externalDocs',
159 |     ];
160 | 
161 |     // Only allow access to documented OpenAPI properties
162 |     if (allowedFields.includes(fieldName as keyof OpenAPIV3.Document)) {
163 |       return this.document[fieldName as keyof OpenAPIV3.Document];
164 |     }
165 |     return undefined;
166 |   }
167 | } // End of RenderableDocument class
168 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RequestId } from '@modelcontextprotocol/sdk/types.js';
  3 | import { PathItemHandler } from '../../../../src/handlers/path-item-handler';
  4 | import { SpecLoaderService } from '../../../../src/types';
  5 | import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
  6 | import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  7 | import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
  8 | import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
  9 | 
 10 | // Mocks
 11 | const mockGetTransformedSpec = jest.fn();
 12 | const mockSpecLoader: SpecLoaderService = {
 13 |   getSpec: jest.fn(),
 14 |   getTransformedSpec: mockGetTransformedSpec,
 15 | };
 16 | 
 17 | const mockFormatter: IFormatter = new JsonFormatter(); // Needed for context
 18 | 
 19 | // Sample Data
 20 | const samplePathItem: OpenAPIV3.PathItemObject = {
 21 |   get: { summary: 'Get Item', responses: { '200': { description: 'OK' } } },
 22 |   post: { summary: 'Create Item', responses: { '201': { description: 'Created' } } },
 23 | };
 24 | const sampleSpec: OpenAPIV3.Document = {
 25 |   openapi: '3.0.3',
 26 |   info: { title: 'Test API', version: '1.0.0' },
 27 |   paths: {
 28 |     '/items': samplePathItem,
 29 |     '/empty': {}, // Path with no methods
 30 |   },
 31 |   components: {},
 32 | };
 33 | 
 34 | const encodedPathItems = encodeURIComponent('items');
 35 | const encodedPathEmpty = encodeURIComponent('empty');
 36 | const encodedPathNonExistent = encodeURIComponent('nonexistent');
 37 | 
 38 | describe('PathItemHandler', () => {
 39 |   let handler: PathItemHandler;
 40 | 
 41 |   beforeEach(() => {
 42 |     handler = new PathItemHandler(mockSpecLoader, mockFormatter);
 43 |     mockGetTransformedSpec.mockReset();
 44 |     mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
 45 |   });
 46 | 
 47 |   it('should return the correct template', () => {
 48 |     const template = handler.getTemplate();
 49 |     expect(template).toBeInstanceOf(ResourceTemplate);
 50 |     expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}');
 51 |   });
 52 | 
 53 |   describe('handleRequest (List Methods)', () => {
 54 |     const mockExtra = {
 55 |       signal: new AbortController().signal,
 56 |       sendNotification: jest.fn(),
 57 |       sendRequest: jest.fn(),
 58 |       requestId: 'test-request-id' as RequestId,
 59 |     };
 60 | 
 61 |     it('should list methods for a valid path', async () => {
 62 |       const variables: Variables = { path: encodedPathItems };
 63 |       const uri = new URL(`openapi://paths/${encodedPathItems}`);
 64 | 
 65 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 66 | 
 67 |       expect(mockGetTransformedSpec).toHaveBeenCalledWith({
 68 |         resourceType: 'schema',
 69 |         format: 'openapi',
 70 |       });
 71 |       expect(result.contents).toHaveLength(1);
 72 |       expect(result.contents[0]).toMatchObject({
 73 |         uri: `openapi://paths/${encodedPathItems}`,
 74 |         mimeType: 'text/plain',
 75 |         isError: false,
 76 |       });
 77 |       // Check for hint first, then methods
 78 |       expect(result.contents[0].text).toContain("Hint: Use 'openapi://paths/items/{method}'");
 79 |       expect(result.contents[0].text).toContain('GET: Get Item');
 80 |       expect(result.contents[0].text).toContain('POST: Create Item');
 81 |       // Ensure the old "Methods for..." header is not present if hint is first
 82 |       expect(result.contents[0].text).not.toContain('Methods for items:');
 83 |     });
 84 | 
 85 |     it('should handle path with no methods', async () => {
 86 |       const variables: Variables = { path: encodedPathEmpty };
 87 |       const uri = new URL(`openapi://paths/${encodedPathEmpty}`);
 88 | 
 89 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 90 | 
 91 |       expect(result.contents).toHaveLength(1);
 92 |       expect(result.contents[0]).toEqual({
 93 |         uri: `openapi://paths/${encodedPathEmpty}`,
 94 |         mimeType: 'text/plain',
 95 |         text: 'No standard HTTP methods found for path: empty',
 96 |         isError: false, // Not an error, just no methods
 97 |       });
 98 |     });
 99 | 
100 |     it('should return error for non-existent path', async () => {
101 |       const variables: Variables = { path: encodedPathNonExistent };
102 |       const uri = new URL(`openapi://paths/${encodedPathNonExistent}`);
103 |       const expectedLogMessage = /Path "\/nonexistent" not found/;
104 | 
105 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
106 |         handler.handleRequest(uri, variables, mockExtra)
107 |       );
108 | 
109 |       expect(result.contents).toHaveLength(1);
110 |       // Expect the specific error message from getValidatedPathItem
111 |       expect(result.contents[0]).toEqual({
112 |         uri: `openapi://paths/${encodedPathNonExistent}`,
113 |         mimeType: 'text/plain',
114 |         text: 'Path "/nonexistent" not found in the specification.',
115 |         isError: true,
116 |       });
117 |     });
118 | 
119 |     it('should handle spec loading errors', async () => {
120 |       const error = new Error('Spec load failed');
121 |       mockGetTransformedSpec.mockRejectedValue(error);
122 |       const variables: Variables = { path: encodedPathItems };
123 |       const uri = new URL(`openapi://paths/${encodedPathItems}`);
124 |       const expectedLogMessage = /Spec load failed/;
125 | 
126 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
127 |         handler.handleRequest(uri, variables, mockExtra)
128 |       );
129 | 
130 |       expect(result.contents).toHaveLength(1);
131 |       expect(result.contents[0]).toEqual({
132 |         uri: `openapi://paths/${encodedPathItems}`,
133 |         mimeType: 'text/plain',
134 |         text: 'Spec load failed',
135 |         isError: true,
136 |       });
137 |     });
138 | 
139 |     it('should handle non-OpenAPI v3 spec', async () => {
140 |       const invalidSpec = { swagger: '2.0', info: {} };
141 |       mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
142 |       const variables: Variables = { path: encodedPathItems };
143 |       const uri = new URL(`openapi://paths/${encodedPathItems}`);
144 |       const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
145 | 
146 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
147 |         handler.handleRequest(uri, variables, mockExtra)
148 |       );
149 | 
150 |       expect(result.contents).toHaveLength(1);
151 |       expect(result.contents[0]).toEqual({
152 |         uri: `openapi://paths/${encodedPathItems}`,
153 |         mimeType: 'text/plain',
154 |         text: 'Only OpenAPI v3 specifications are supported',
155 |         isError: true,
156 |       });
157 |     });
158 |   });
159 | });
160 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
  3 | import { getOperationSummary, createErrorResult, generateListHint } from './utils.js'; // Add .js
  4 | 
  5 | /**
  6 |  * Wraps an OpenAPIV3.PathItemObject to make it renderable.
  7 |  * Handles rendering the list of methods for a specific path and
  8 |  * the details of specific operations (methods).
  9 |  */
 10 | export class RenderablePathItem implements RenderableSpecObject {
 11 |   constructor(
 12 |     private pathItem: OpenAPIV3.PathItemObject | undefined,
 13 |     private path: string, // The raw, decoded path string e.g., "/users/{userId}"
 14 |     private pathUriSuffix: string // Built using buildPathItemUriSuffix(path) e.g., 'paths/users%7BuserId%7D'
 15 |   ) {}
 16 | 
 17 |   /**
 18 |    * Renders a token-efficient list of methods available for this path.
 19 |    * Corresponds to the `openapi://paths/{path}` URI.
 20 |    */
 21 |   renderList(context: RenderContext): RenderResultItem[] {
 22 |     if (!this.pathItem) {
 23 |       return createErrorResult(this.pathUriSuffix, 'Path item not found.');
 24 |     }
 25 | 
 26 |     // Correctly check if the lowercase key is one of the enum values
 27 |     const methods = Object.keys(this.pathItem).filter(key =>
 28 |       Object.values(OpenAPIV3.HttpMethods).includes(key.toLowerCase() as OpenAPIV3.HttpMethods)
 29 |     ) as OpenAPIV3.HttpMethods[];
 30 | 
 31 |     // Check if methods array is empty *after* filtering
 32 |     if (methods.length === 0) {
 33 |       // Return a specific non-error message indicating no methods were found
 34 |       return [
 35 |         {
 36 |           uriSuffix: this.pathUriSuffix,
 37 |           data: `No standard HTTP methods found for path: ${decodeURIComponent(
 38 |             this.pathUriSuffix.substring('paths/'.length) // Get original path for display
 39 |           )}`,
 40 |           renderAsList: true,
 41 |           // isError is implicitly false here
 42 |         },
 43 |       ];
 44 |     }
 45 | 
 46 |     // Sort methods first to get the correct example
 47 |     methods.sort();
 48 |     const firstMethodExample = methods.length > 0 ? methods[0] : undefined;
 49 | 
 50 |     // Generate hint using the new structure, providing the first *sorted* method as an example
 51 |     const hint = generateListHint(context, {
 52 |       itemType: 'pathMethod',
 53 |       parentPath: this.path, // Use the stored raw path
 54 |       firstItemExample: firstMethodExample,
 55 |     });
 56 |     // Hint includes leading newline, so start output with it directly
 57 |     let outputLines: string[] = [hint.trim(), '']; // Trim leading newline from hint for first line
 58 | 
 59 |     // Iterate over the already sorted methods
 60 |     methods.forEach(method => {
 61 |       const operation = this.getOperation(method);
 62 |       // Use summary or operationId (via getOperationSummary)
 63 |       const summaryText = getOperationSummary(operation);
 64 |       // Format as METHOD: Summary or just METHOD if no summary/opId
 65 |       outputLines.push(`${method.toUpperCase()}${summaryText ? `: ${summaryText}` : ''}`);
 66 |     });
 67 | 
 68 |     return [
 69 |       {
 70 |         uriSuffix: this.pathUriSuffix,
 71 |         data: outputLines.join('\n'), // Join lines into a single string
 72 |         renderAsList: true,
 73 |       },
 74 |     ];
 75 |   }
 76 | 
 77 |   /**
 78 |    * Renders the detail view for one or more specific operations (methods)
 79 |    * Renders the detail view. For a PathItem, this usually means listing
 80 |    * the methods, similar to renderList. The handler should call
 81 |    * `renderOperationDetail` for specific method details.
 82 |    */
 83 |   renderDetail(context: RenderContext): RenderResultItem[] {
 84 |     // Delegate to renderList as the primary view for a path item itself.
 85 |     return this.renderList(context);
 86 |   }
 87 | 
 88 |   /**
 89 |    * Renders the detail view for one or more specific operations (methods)
 90 |    * within this path item.
 91 |    * Corresponds to the `openapi://paths/{path}/{method*}` URI.
 92 |    * This is called by the handler after identifying the method(s).
 93 |    *
 94 |    * @param context - The rendering context.
 95 |    * @param methods - Array of method names (e.g., ['get', 'post']).
 96 |    * @returns An array of RenderResultItem representing the operation details.
 97 |    */
 98 |   renderOperationDetail(
 99 |     _context: RenderContext, // Context might be needed later
100 |     methods: string[]
101 |   ): RenderResultItem[] {
102 |     if (!this.pathItem) {
103 |       // Create error results for all requested methods if path item is missing
104 |       return methods.map(method => ({
105 |         uriSuffix: `${this.pathUriSuffix}/${method}`,
106 |         data: null,
107 |         isError: true,
108 |         errorText: 'Path item not found.',
109 |         renderAsList: true,
110 |       }));
111 |     }
112 | 
113 |     const results: RenderResultItem[] = [];
114 | 
115 |     for (const method of methods) {
116 |       const operation = this.getOperation(method);
117 |       const operationUriSuffix = `${this.pathUriSuffix}/${method}`;
118 | 
119 |       if (!operation) {
120 |         results.push({
121 |           uriSuffix: operationUriSuffix,
122 |           data: null,
123 |           isError: true,
124 |           errorText: `Method "${method.toUpperCase()}" not found for path.`,
125 |           renderAsList: true,
126 |         });
127 |       } else {
128 |         // Return the raw operation object; handler will format it
129 |         results.push({
130 |           uriSuffix: operationUriSuffix,
131 |           data: operation,
132 |           // isError: false (default)
133 |           // renderAsList: false (default)
134 |         });
135 |       }
136 |     }
137 |     return results;
138 |   }
139 | 
140 |   /**
141 |    * Gets the OperationObject for a specific HTTP method within this path item.
142 |    * Performs case-insensitive lookup.
143 |    * @param method - The HTTP method string (e.g., 'get', 'POST').
144 |    * @returns The OperationObject or undefined if not found.
145 |    */
146 |   getOperation(method: string): OpenAPIV3.OperationObject | undefined {
147 |     if (!this.pathItem) {
148 |       return undefined;
149 |     }
150 |     const lowerMethod = method.toLowerCase();
151 | 
152 |     // Check if the key is a standard HTTP method defined in the enum
153 |     if (Object.values(OpenAPIV3.HttpMethods).includes(lowerMethod as OpenAPIV3.HttpMethods)) {
154 |       const operation = this.pathItem[lowerMethod as keyof OpenAPIV3.PathItemObject];
155 |       // Basic check to ensure it looks like an operation object
156 |       if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
157 |         // The check above narrows the type sufficiently, assertion is redundant
158 |         return operation;
159 |       }
160 |     }
161 |     return undefined; // Not a valid method or not an operation object
162 |   }
163 | }
164 | 
```
Page 1/3FirstPrevNextLast