#
tokens: 49077/50000 18/75 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

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

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  2 | import { ReadResourceResult, TextResourceContents } from '@modelcontextprotocol/sdk/types.js';
  3 | import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers';
  4 | import path from 'path';
  5 | 
  6 | // Helper function to parse JSON safely
  7 | function parseJsonSafely(text: string | undefined): unknown {
  8 |   if (text === undefined) {
  9 |     throw new Error('Received undefined text for JSON parsing');
 10 |   }
 11 |   try {
 12 |     return JSON.parse(text);
 13 |   } catch (e) {
 14 |     console.error('Failed to parse JSON:', text);
 15 |     throw new Error(`Invalid JSON received: ${e instanceof Error ? e.message : String(e)}`);
 16 |   }
 17 | }
 18 | 
 19 | // Type guard to check if content is TextResourceContents
 20 | function hasTextContent(
 21 |   content: ReadResourceResult['contents'][0]
 22 | ): content is TextResourceContents {
 23 |   return typeof (content as TextResourceContents).text === 'string';
 24 | }
 25 | 
 26 | describe('E2E Tests for Spec Loading Scenarios', () => {
 27 |   let testContext: McpTestContext | null = null; // Allow null for cleanup
 28 |   let client: Client | null = null; // Allow null
 29 | 
 30 |   // Helper to setup client for tests, allowing different spec paths
 31 |   async function setup(specPathOrUrl: string): Promise<void> {
 32 |     // Cleanup previous context if exists
 33 |     if (testContext) {
 34 |       await testContext.cleanup();
 35 |       testContext = null;
 36 |       client = null;
 37 |     }
 38 |     try {
 39 |       testContext = await startMcpServer(specPathOrUrl, { outputFormat: 'json' });
 40 |       client = testContext.client;
 41 |     } catch (error) {
 42 |       // Explicitly convert error to string for logging
 43 |       const errorMsg = error instanceof Error ? error.message : String(error);
 44 |       console.warn(`Skipping tests for ${specPathOrUrl} due to setup error: ${errorMsg}`);
 45 |       testContext = null; // Ensure cleanup doesn't run on failed setup
 46 |       client = null; // Ensure tests are skipped
 47 |     }
 48 |   }
 49 | 
 50 |   afterEach(async () => {
 51 |     await testContext?.cleanup();
 52 |     testContext = null;
 53 |     client = null;
 54 |   });
 55 | 
 56 |   // Helper to read resource and perform basic checks
 57 |   async function readResourceAndCheck(uri: string): Promise<ReadResourceResult['contents'][0]> {
 58 |     if (!client) throw new Error('Client not initialized, skipping test.');
 59 |     const result = await client.readResource({ uri });
 60 |     expect(result.contents).toHaveLength(1);
 61 |     const content = result.contents[0];
 62 |     expect(content.uri).toBe(uri);
 63 |     return content;
 64 |   }
 65 | 
 66 |   // Helper to read resource and check for text/plain list content
 67 |   async function checkTextListResponse(uri: string, expectedSubstrings: string[]): Promise<string> {
 68 |     const content = await readResourceAndCheck(uri);
 69 |     expect(content.mimeType).toBe('text/plain');
 70 |     expect(content.isError).toBeFalsy();
 71 |     if (!hasTextContent(content)) throw new Error('Expected text content');
 72 |     for (const sub of expectedSubstrings) {
 73 |       expect(content.text).toContain(sub);
 74 |     }
 75 |     return content.text;
 76 |   }
 77 | 
 78 |   // Helper to read resource and check for JSON detail content
 79 |   async function checkJsonDetailResponse(uri: string, expectedObject: object): Promise<unknown> {
 80 |     const content = await readResourceAndCheck(uri);
 81 |     expect(content.mimeType).toBe('application/json');
 82 |     expect(content.isError).toBeFalsy();
 83 |     if (!hasTextContent(content)) throw new Error('Expected text content');
 84 |     const data = parseJsonSafely(content.text);
 85 |     expect(data).toMatchObject(expectedObject);
 86 |     return data;
 87 |   }
 88 | 
 89 |   // --- Tests for Local Swagger v2.0 Spec ---
 90 |   describe('Local Swagger v2.0 Spec (sample-v2-api.json)', () => {
 91 |     const v2SpecPath = path.resolve(__dirname, '../../fixtures/sample-v2-api.json');
 92 | 
 93 |     beforeAll(async () => await setup(v2SpecPath)); // Use beforeAll for this block
 94 | 
 95 |     it('should retrieve the converted "info" field', async () => {
 96 |       if (!client) return; // Skip if setup failed
 97 |       await checkJsonDetailResponse('openapi://info', {
 98 |         title: 'Simple Swagger 2.0 API',
 99 |         version: '1.0.0',
100 |       });
101 |     });
102 | 
103 |     it('should retrieve the converted "paths" list', async () => {
104 |       if (!client) return; // Skip if setup failed
105 |       await checkTextListResponse('openapi://paths', [
106 |         'Hint:',
107 |         'GET /v2/ping', // Note the basePath is included
108 |       ]);
109 |     });
110 | 
111 |     it('should retrieve the converted "components" list', async () => {
112 |       if (!client) return; // Skip if setup failed
113 |       await checkTextListResponse('openapi://components', [
114 |         'Available Component Types:',
115 |         '- schemas',
116 |         "Hint: Use 'openapi://components/{type}'",
117 |       ]);
118 |     });
119 | 
120 |     it('should get details for converted schema Pong', async () => {
121 |       if (!client) return; // Skip if setup failed
122 |       await checkJsonDetailResponse('openapi://components/schemas/Pong', {
123 |         type: 'object',
124 |         properties: { message: { type: 'string', example: 'pong' } },
125 |       });
126 |     });
127 |   });
128 | 
129 |   // --- Tests for Remote OpenAPI v3.0 Spec (Petstore) ---
130 |   // Increase timeout for remote fetch
131 |   jest.setTimeout(20000); // 20 seconds
132 | 
133 |   describe('Remote OpenAPI v3.0 Spec (Petstore)', () => {
134 |     const petstoreUrl = 'https://petstore3.swagger.io/api/v3/openapi.json';
135 | 
136 |     beforeAll(async () => await setup(petstoreUrl)); // Use beforeAll for this block
137 | 
138 |     it('should retrieve the "info" field from Petstore', async () => {
139 |       if (!client) return; // Skip if setup failed
140 |       await checkJsonDetailResponse('openapi://info', {
141 |         title: 'Swagger Petstore - OpenAPI 3.0',
142 |         // version might change, so don't assert exact value
143 |       });
144 |     });
145 | 
146 |     it('should retrieve the "paths" list from Petstore', async () => {
147 |       if (!client) return; // Skip if setup failed
148 |       // Check for a known path
149 |       await checkTextListResponse('openapi://paths', ['/pet/{petId}']);
150 |     });
151 | 
152 |     it('should retrieve the "components" list from Petstore', async () => {
153 |       if (!client) return; // Skip if setup failed
154 |       // Check for known component types
155 |       await checkTextListResponse('openapi://components', [
156 |         '- schemas',
157 |         '- requestBodies',
158 |         '- securitySchemes',
159 |       ]);
160 |     });
161 | 
162 |     it('should get details for schema Pet from Petstore', async () => {
163 |       if (!client) return; // Skip if setup failed
164 |       await checkJsonDetailResponse('openapi://components/schemas/Pet', {
165 |         required: ['name', 'photoUrls'],
166 |         type: 'object',
167 |         // Check a known property
168 |         properties: { id: { type: 'integer', format: 'int64' } },
169 |       });
170 |     });
171 |   });
172 | });
173 | 
```

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

```markdown
 1 | # [1.3.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.2.1...v1.3.0) (2025-08-12)
 2 | 
 3 | ### Bug Fixes
 4 | 
 5 | - Use more generic instructions ([1372aae](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/1372aaea824f2b9eb5d4c3569acc4f38c82550fd))
 6 | 
 7 | ### Features
 8 | 
 9 | - add brief instructions so LLMs can better understand how to use the server ([c55c4ec](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/c55c4ec029a7603746bf506340d8e3ffd54a6532))
10 | 
11 | ## [1.2.1](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.2.0...v1.2.1) (2025-04-13)
12 | 
13 | ### Bug Fixes
14 | 
15 | - update Node.js setup to match Dockerfile version and include dev dependencies ([8658705](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/86587059268ad4c18d219729b39e4e4f990e05e9))
16 | 
17 | # [1.2.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.1.0...v1.2.0) (2025-04-13)
18 | 
19 | ### Bug Fixes
20 | 
21 | - remove husky.sh sourcing from pre-commit hook ([2cf9455](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2cf9455f1432cb0c6cbda71d61cad9f2f87031ab))
22 | - update Docker Hub login to use secrets for credentials ([ab2136b](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ab2136bd8c052d7287ef1fd6d2768a9fd93148c8))
23 | 
24 | ### Features
25 | 
26 | - implement Docker support with multi-stage builds and CI integration ([910dc02](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/910dc021b3e203574dee93198ce5896a9e8aa16d))
27 | 
28 | # [1.1.0](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.2...v1.1.0) (2025-04-13)
29 | 
30 | ### Features
31 | 
32 | - enhance component and path item rendering with descriptions and examples in hints ([6989159](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/698915972338b4a16419c9cea3e2377b7701f50b))
33 | 
34 | ## [1.0.2](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.1...v1.0.2) (2025-04-13)
35 | 
36 | ### Bug Fixes
37 | 
38 | - update CI workflow to use RELEASE_TOKEN and disable credential persistence ([e7b18f9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e7b18f9055b95f0e2c6e2a356cb87482db6205da))
39 | 
40 | ## [1.0.1](https://github.com/kadykov/mcp-openapi-schema-explorer/compare/v1.0.0...v1.0.1) (2025-04-12)
41 | 
42 | ### Bug Fixes
43 | 
44 | - add openapi-types dependency to package.json and package-lock.json ([d348fb9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/d348fb92a30cdb9d213ee92f1779258f43bbbcd9))
45 | 
46 | # 1.0.0 (2025-04-12)
47 | 
48 | ### Bug Fixes
49 | 
50 | - add codecov badge to README for improved visibility of test coverage ([ed7bf93](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ed7bf93de6c6efbf3a890551b67321b0d003c3cf))
51 | 
52 | ### Features
53 | 
54 | - add CI workflow and dependabot configuration for automated updates ([2d0b22e](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2d0b22ea20afd58297b2169d3761db32b4c92606))
55 | - Add configuration management for OpenAPI Explorer ([b9f4771](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b9f47712e754983d292bd6d53c82fa7e344b45a6))
56 | - add CONTRIBUTING.md and enhance README with detailed project information ([1f4b2d5](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/1f4b2d59d7a19e54556cf8933fc4e4952d8f438c))
57 | - Add end-to-end tests for OpenAPI resource handling ([d1ba7ab](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/d1ba7ab5db84717ed6c326d0c7d625906572be2c))
58 | - Add pre-commit hook to format staged files with Prettier ([af58250](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/af582509fadbffd52afcd36d6113a1965a2bfcef))
59 | - Add SchemaListHandler and implement schema listing resource with error handling ([873bbee](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/873bbee9cee5233e97202458a6b261e6ac58b651))
60 | - Add support for minified JSON output format and related enhancements ([f0cb5b8](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f0cb5b80eeb73d2656b1d8fb37ab8fe21dacf12a))
61 | - Enhance endpoint features and add endpoint list handler with improved error handling ([32082ac](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/32082acd3f187bb0611a2adbbfb107f0c153aae2))
62 | - Enhance OpenAPI resource handling with new templates and completion tests ([45e4938](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/45e4938b226dc6e1baeb506b8c23c615fef78065))
63 | - Enhance output formatting with JSON and YAML support, including formatter implementations and configuration updates ([e63fafe](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e63fafe82abb36a56bbb976ff3098f2d4d6a7d6c))
64 | - Implement dynamic server name based on OpenAPI spec title ([aaa691f](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/aaa691fa2c545a433e09fb3f1faa0d31d4e8624d))
65 | - Implement EndpointListHandler and add endpoint list resource to server ([b81a606](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b81a60645eeec9b2e9bd7eb46914cdf3178f9457))
66 | - Implement Map-based validation helpers to enhance security and error handling ([a4394c9](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/a4394c9846482d53436019a0498ca5d91fddefdf))
67 | - Implement resource completion logic and add related tests ([de8f297](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/de8f29785882a6bd68d4fcaf38de971de4bad222))
68 | - Implement SchemaHandler and add schema resource support with error handling ([2fae461](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/2fae461e5de51b7610135922b4a4c9a55cd5b126))
69 | - initialize MCP OpenAPI schema explorer project ([fd64242](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/fd642421274172e5ca330c9b85015f597f4a96c1))
70 | - Introduce suppressExpectedConsoleError utility to manage console.error during tests ([ef088c2](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ef088c2f98bacd0dd7ae3f4aa75e44ba52a41712))
71 | - Update dependencies to include swagger2openapi and @types/js-yaml ([8acb951](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/8acb951eb88843c72f8eb7d6d7feff681b56ff84))
72 | - update descriptions in API methods to include URL-encoding notes ([b71dbdf](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/b71dbdfd8c5f0c02d9a47f99143416787f76bf50))
73 | - Update endpoint URI template to support wildcard parameters ([ce1281f](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/ce1281f16f81a0fd7a74b20fe6bb92e7ed19e158))
74 | - Update EndpointHandler to return detailed operation responses for GET and POST methods ([af55400](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/af554008c35c9be5bdbf53e51b791e90d135e283))
75 | - Update license compliance check to include Python-2.0 ([e00c5e2](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/e00c5e23cca6070d6833017b567d7c5402276f45))
76 | - Update MCP inspector command to support YAML output format ([f7fb551](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f7fb551cc3a9d7e84fb47100cf8e0430c2634070))
77 | - update release job to match Node.js version and include dev dependencies ([f3aeb87](https://github.com/kadykov/mcp-openapi-schema-explorer/commit/f3aeb87dcd8bed9920fe2eccdcd8f253b310f761))
78 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RequestId } from '@modelcontextprotocol/sdk/types.js';
  3 | import { TopLevelFieldHandler } from '../../../../src/handlers/top-level-field-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(), // Not used by this handler directly
 14 |   getTransformedSpec: mockGetTransformedSpec,
 15 | };
 16 | 
 17 | const mockFormatter: IFormatter = new JsonFormatter(); // Use real formatter for structure check
 18 | 
 19 | // Sample Data
 20 | const sampleSpec: OpenAPIV3.Document = {
 21 |   openapi: '3.0.3',
 22 |   info: { title: 'Test API', version: '1.1.0' },
 23 |   paths: { '/test': { get: { responses: { '200': { description: 'OK' } } } } },
 24 |   components: { schemas: { Test: { type: 'string' } } },
 25 |   servers: [{ url: 'http://example.com' }],
 26 | };
 27 | 
 28 | describe('TopLevelFieldHandler', () => {
 29 |   let handler: TopLevelFieldHandler;
 30 | 
 31 |   beforeEach(() => {
 32 |     handler = new TopLevelFieldHandler(mockSpecLoader, mockFormatter);
 33 |     mockGetTransformedSpec.mockReset(); // Reset mock before each test
 34 |   });
 35 | 
 36 |   it('should return the correct template', () => {
 37 |     const template = handler.getTemplate();
 38 |     expect(template).toBeInstanceOf(ResourceTemplate);
 39 |     // Compare against the string representation of the UriTemplate object
 40 |     expect(template.uriTemplate.toString()).toBe('openapi://{field}');
 41 |   });
 42 | 
 43 |   describe('handleRequest', () => {
 44 |     const mockExtra = {
 45 |       signal: new AbortController().signal,
 46 |       sendNotification: jest.fn(),
 47 |       sendRequest: jest.fn(),
 48 |       requestId: 'test-request-id' as RequestId,
 49 |     };
 50 | 
 51 |     it('should handle request for "info" field', async () => {
 52 |       mockGetTransformedSpec.mockResolvedValue(sampleSpec);
 53 |       const variables: Variables = { field: 'info' };
 54 |       const uri = new URL('openapi://info');
 55 | 
 56 |       // Pass the mock extra object as the third argument
 57 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 58 | 
 59 |       expect(mockGetTransformedSpec).toHaveBeenCalledWith({
 60 |         resourceType: 'schema',
 61 |         format: 'openapi',
 62 |       });
 63 |       expect(result.contents).toHaveLength(1);
 64 |       expect(result.contents[0]).toEqual({
 65 |         uri: 'openapi://info',
 66 |         mimeType: 'application/json',
 67 |         text: JSON.stringify(sampleSpec.info, null, 2),
 68 |         isError: false,
 69 |       });
 70 |     });
 71 | 
 72 |     it('should handle request for "servers" field', async () => {
 73 |       mockGetTransformedSpec.mockResolvedValue(sampleSpec);
 74 |       const variables: Variables = { field: 'servers' };
 75 |       const uri = new URL('openapi://servers');
 76 | 
 77 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 78 | 
 79 |       expect(result.contents).toHaveLength(1);
 80 |       expect(result.contents[0]).toEqual({
 81 |         uri: 'openapi://servers',
 82 |         mimeType: 'application/json',
 83 |         text: JSON.stringify(sampleSpec.servers, null, 2),
 84 |         isError: false,
 85 |       });
 86 |     });
 87 | 
 88 |     it('should handle request for "paths" field (list view)', async () => {
 89 |       mockGetTransformedSpec.mockResolvedValue(sampleSpec);
 90 |       const variables: Variables = { field: 'paths' };
 91 |       const uri = new URL('openapi://paths');
 92 | 
 93 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 94 | 
 95 |       expect(result.contents).toHaveLength(1);
 96 |       expect(result.contents[0].uri).toBe('openapi://paths');
 97 |       expect(result.contents[0].mimeType).toBe('text/plain');
 98 |       expect(result.contents[0].isError).toBe(false);
 99 |       expect(result.contents[0].text).toContain('GET /test'); // Check content format
100 |       // Check that the hint contains the essential URI patterns
101 |       expect(result.contents[0].text).toContain('Hint:');
102 |       expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}');
103 |       expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}/{method}');
104 |     });
105 | 
106 |     it('should handle request for "components" field (list view)', async () => {
107 |       mockGetTransformedSpec.mockResolvedValue(sampleSpec);
108 |       const variables: Variables = { field: 'components' };
109 |       const uri = new URL('openapi://components');
110 | 
111 |       const result = await handler.handleRequest(uri, variables, mockExtra);
112 | 
113 |       expect(result.contents).toHaveLength(1);
114 |       expect(result.contents[0].uri).toBe('openapi://components');
115 |       expect(result.contents[0].mimeType).toBe('text/plain');
116 |       expect(result.contents[0].isError).toBe(false);
117 |       expect(result.contents[0].text).toContain('- schemas'); // Check content format
118 |       expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/{type}'");
119 |     });
120 | 
121 |     it('should return error for non-existent field', async () => {
122 |       mockGetTransformedSpec.mockResolvedValue(sampleSpec);
123 |       const variables: Variables = { field: 'nonexistent' };
124 |       const uri = new URL('openapi://nonexistent');
125 | 
126 |       const result = await handler.handleRequest(uri, variables, mockExtra);
127 | 
128 |       expect(result.contents).toHaveLength(1);
129 |       expect(result.contents[0]).toEqual({
130 |         uri: 'openapi://nonexistent',
131 |         mimeType: 'text/plain',
132 |         text: 'Error: Field "nonexistent" not found in the OpenAPI document.',
133 |         isError: true,
134 |       });
135 |     });
136 | 
137 |     it('should handle spec loading errors', async () => {
138 |       const error = new Error('Failed to load spec');
139 |       mockGetTransformedSpec.mockRejectedValue(error);
140 |       const variables: Variables = { field: 'info' };
141 |       const uri = new URL('openapi://info');
142 |       // Match the core error message using RegExp
143 |       const expectedLogMessage = /Failed to load spec/;
144 | 
145 |       // Use the helper, letting TypeScript infer the return type
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://info',
153 |         mimeType: 'text/plain',
154 |         text: 'Failed to load spec',
155 |         isError: true,
156 |       });
157 |     });
158 | 
159 |     it('should handle non-OpenAPI v3 spec', async () => {
160 |       const invalidSpec = { swagger: '2.0', info: {} }; // Not OpenAPI v3
161 |       mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
162 |       const variables: Variables = { field: 'info' };
163 |       const uri = new URL('openapi://info');
164 |       // Match the core error message using RegExp
165 |       const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
166 | 
167 |       // Use the helper, letting TypeScript infer the return type
168 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
169 |         handler.handleRequest(uri, variables, mockExtra)
170 |       );
171 | 
172 |       expect(result.contents).toHaveLength(1);
173 |       expect(result.contents[0]).toEqual({
174 |         uri: 'openapi://info',
175 |         mimeType: 'text/plain',
176 |         text: 'Only OpenAPI v3 specifications are supported',
177 |         isError: true,
178 |       });
179 |     });
180 |   });
181 | });
182 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RequestId } from '@modelcontextprotocol/sdk/types.js';
  3 | import { ComponentMapHandler } from '../../../../src/handlers/component-map-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 sampleSpec: OpenAPIV3.Document = {
 21 |   openapi: '3.0.3',
 22 |   info: { title: 'Test API', version: '1.0.0' },
 23 |   paths: {},
 24 |   components: {
 25 |     schemas: {
 26 |       User: { type: 'object', properties: { name: { type: 'string' } } },
 27 |       Error: { type: 'object', properties: { message: { type: 'string' } } },
 28 |     },
 29 |     parameters: {
 30 |       limitParam: { name: 'limit', in: 'query', schema: { type: 'integer' } },
 31 |     },
 32 |     examples: {}, // Empty type
 33 |   },
 34 | };
 35 | 
 36 | describe('ComponentMapHandler', () => {
 37 |   let handler: ComponentMapHandler;
 38 | 
 39 |   beforeEach(() => {
 40 |     handler = new ComponentMapHandler(mockSpecLoader, mockFormatter);
 41 |     mockGetTransformedSpec.mockReset();
 42 |     mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
 43 |   });
 44 | 
 45 |   it('should return the correct template', () => {
 46 |     const template = handler.getTemplate();
 47 |     expect(template).toBeInstanceOf(ResourceTemplate);
 48 |     expect(template.uriTemplate.toString()).toBe('openapi://components/{type}');
 49 |   });
 50 | 
 51 |   describe('handleRequest (List Component Names)', () => {
 52 |     const mockExtra = {
 53 |       signal: new AbortController().signal,
 54 |       sendNotification: jest.fn(),
 55 |       sendRequest: jest.fn(),
 56 |       requestId: 'test-request-id' as RequestId,
 57 |     };
 58 | 
 59 |     it('should list names for a valid component type (schemas)', async () => {
 60 |       const variables: Variables = { type: 'schemas' };
 61 |       const uri = new URL('openapi://components/schemas');
 62 | 
 63 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 64 | 
 65 |       expect(mockGetTransformedSpec).toHaveBeenCalledWith({
 66 |         resourceType: 'schema',
 67 |         format: 'openapi',
 68 |       });
 69 |       expect(result.contents).toHaveLength(1);
 70 |       expect(result.contents[0]).toMatchObject({
 71 |         uri: 'openapi://components/schemas',
 72 |         mimeType: 'text/plain',
 73 |         isError: false,
 74 |       });
 75 |       expect(result.contents[0].text).toContain('Available schemas:');
 76 |       expect(result.contents[0].text).toMatch(/-\sError\n/); // Sorted
 77 |       expect(result.contents[0].text).toMatch(/-\sUser\n/);
 78 |       expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/schemas/{name}'");
 79 |     });
 80 | 
 81 |     it('should list names for another valid type (parameters)', async () => {
 82 |       const variables: Variables = { type: 'parameters' };
 83 |       const uri = new URL('openapi://components/parameters');
 84 | 
 85 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 86 | 
 87 |       expect(result.contents).toHaveLength(1);
 88 |       expect(result.contents[0]).toMatchObject({
 89 |         uri: 'openapi://components/parameters',
 90 |         mimeType: 'text/plain',
 91 |         isError: false,
 92 |       });
 93 |       expect(result.contents[0].text).toContain('Available parameters:');
 94 |       expect(result.contents[0].text).toMatch(/-\slimitParam\n/);
 95 |       expect(result.contents[0].text).toContain(
 96 |         "Hint: Use 'openapi://components/parameters/{name}'"
 97 |       );
 98 |     });
 99 | 
100 |     it('should handle component type with no components defined (examples)', async () => {
101 |       const variables: Variables = { type: 'examples' };
102 |       const uri = new URL('openapi://components/examples');
103 | 
104 |       const result = await handler.handleRequest(uri, variables, mockExtra);
105 | 
106 |       expect(result.contents).toHaveLength(1);
107 |       expect(result.contents[0]).toEqual({
108 |         uri: 'openapi://components/examples',
109 |         mimeType: 'text/plain',
110 |         text: 'No components of type "examples" found.',
111 |         isError: true, // Treat as error because map exists but is empty
112 |       });
113 |     });
114 | 
115 |     it('should handle component type not present in spec (securitySchemes)', async () => {
116 |       const variables: Variables = { type: 'securitySchemes' };
117 |       const uri = new URL('openapi://components/securitySchemes');
118 |       const expectedLogMessage = /Component type "securitySchemes" not found/;
119 | 
120 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
121 |         handler.handleRequest(uri, variables, mockExtra)
122 |       );
123 | 
124 |       expect(result.contents).toHaveLength(1);
125 |       // Expect the specific error message from getValidatedComponentMap
126 |       expect(result.contents[0]).toEqual({
127 |         uri: 'openapi://components/securitySchemes',
128 |         mimeType: 'text/plain',
129 |         text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters, examples',
130 |         isError: true,
131 |       });
132 |     });
133 | 
134 |     it('should return error for invalid component type', async () => {
135 |       const variables: Variables = { type: 'invalidType' };
136 |       const uri = new URL('openapi://components/invalidType');
137 |       const expectedLogMessage = /Invalid component type: invalidType/;
138 | 
139 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
140 |         handler.handleRequest(uri, variables, mockExtra)
141 |       );
142 | 
143 |       expect(result.contents).toHaveLength(1);
144 |       expect(result.contents[0]).toEqual({
145 |         uri: 'openapi://components/invalidType',
146 |         mimeType: 'text/plain',
147 |         text: 'Invalid component type: invalidType',
148 |         isError: true,
149 |       });
150 |       expect(mockGetTransformedSpec).not.toHaveBeenCalled(); // Should fail before loading spec
151 |     });
152 | 
153 |     it('should handle spec loading errors', async () => {
154 |       const error = new Error('Spec load failed');
155 |       mockGetTransformedSpec.mockRejectedValue(error);
156 |       const variables: Variables = { type: 'schemas' };
157 |       const uri = new URL('openapi://components/schemas');
158 |       const expectedLogMessage = /Spec load failed/;
159 | 
160 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
161 |         handler.handleRequest(uri, variables, mockExtra)
162 |       );
163 | 
164 |       expect(result.contents).toHaveLength(1);
165 |       expect(result.contents[0]).toEqual({
166 |         uri: 'openapi://components/schemas',
167 |         mimeType: 'text/plain',
168 |         text: 'Spec load failed',
169 |         isError: true,
170 |       });
171 |     });
172 | 
173 |     it('should handle non-OpenAPI v3 spec', async () => {
174 |       const invalidSpec = { swagger: '2.0', info: {} };
175 |       mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
176 |       const variables: Variables = { type: 'schemas' };
177 |       const uri = new URL('openapi://components/schemas');
178 |       const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
179 | 
180 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
181 |         handler.handleRequest(uri, variables, mockExtra)
182 |       );
183 | 
184 |       expect(result.contents).toHaveLength(1);
185 |       expect(result.contents[0]).toEqual({
186 |         uri: 'openapi://components/schemas',
187 |         mimeType: 'text/plain',
188 |         text: 'Only OpenAPI v3 specifications are supported',
189 |         isError: true,
190 |       });
191 |     });
192 |   });
193 | });
194 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderablePathItem } from '../../../../src/rendering/path-item';
  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 PathItem Object Fixture
 14 | const samplePathItem: OpenAPIV3.PathItemObject = {
 15 |   get: {
 16 |     summary: 'Get Item',
 17 |     responses: { '200': { description: 'OK' } },
 18 |   },
 19 |   post: {
 20 |     summary: 'Create Item',
 21 |     responses: { '201': { description: 'Created' } },
 22 |   },
 23 |   delete: {
 24 |     // No summary
 25 |     responses: { '204': { description: 'No Content' } },
 26 |   },
 27 |   parameters: [
 28 |     // Example path-level parameter
 29 |     { name: 'commonParam', in: 'query', schema: { type: 'string' } },
 30 |   ],
 31 | };
 32 | 
 33 | // Define both the raw path and the expected suffix (built using the builder logic)
 34 | const rawPath = '/items';
 35 | const pathUriSuffix = 'paths/items'; // Builder removes leading '/' and encodes, but '/items' has no special chars
 36 | 
 37 | describe('RenderablePathItem', () => {
 38 |   describe('renderList (List Methods)', () => {
 39 |     it('should render a list of methods correctly', () => {
 40 |       // Provide all 3 arguments to constructor
 41 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
 42 |       const result = renderable.renderList(mockContext);
 43 | 
 44 |       expect(result).toHaveLength(1);
 45 |       expect(result[0].uriSuffix).toBe(pathUriSuffix);
 46 |       expect(result[0].renderAsList).toBe(true);
 47 |       expect(result[0].isError).toBeUndefined();
 48 | 
 49 |       // Define expected output lines based on the new format and builder logic
 50 |       // generateListHint uses buildOperationUriSuffix which encodes the path
 51 |       // Since rawPath is '/items', encoded is 'items'.
 52 |       // The first sorted method is 'delete'.
 53 |       const expectedHint =
 54 |         "Hint: Use 'openapi://paths/items/{method}' to view details for a specific operation. (e.g., openapi://paths/items/delete)";
 55 |       const expectedLineDelete = 'DELETE'; // No summary/opId
 56 |       const expectedLineGet = 'GET: Get Item'; // Summary exists
 57 |       const expectedLinePost = 'POST: Create Item'; // Summary exists
 58 |       const expectedOutput = `${expectedHint}\n\n${expectedLineDelete}\n${expectedLineGet}\n${expectedLinePost}`;
 59 | 
 60 |       // Check the full output string
 61 |       expect(result[0].data).toBe(expectedOutput);
 62 |     });
 63 | 
 64 |     it('should handle path item with no standard methods', () => {
 65 |       const noMethodsPathItem: OpenAPIV3.PathItemObject = {
 66 |         parameters: samplePathItem.parameters,
 67 |       };
 68 |       // Provide all 3 arguments to constructor
 69 |       const renderable = new RenderablePathItem(noMethodsPathItem, rawPath, pathUriSuffix);
 70 |       const result = renderable.renderList(mockContext);
 71 |       expect(result).toHaveLength(1);
 72 |       expect(result[0]).toEqual({
 73 |         uriSuffix: pathUriSuffix,
 74 |         data: 'No standard HTTP methods found for path: items',
 75 |         renderAsList: true,
 76 |       });
 77 |     });
 78 | 
 79 |     it('should return error if path item is undefined', () => {
 80 |       // Provide all 3 arguments to constructor
 81 |       const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
 82 |       const result = renderable.renderList(mockContext);
 83 |       expect(result).toHaveLength(1);
 84 |       expect(result[0]).toMatchObject({
 85 |         uriSuffix: pathUriSuffix,
 86 |         isError: true,
 87 |         errorText: 'Path item not found.',
 88 |         renderAsList: true,
 89 |       });
 90 |     });
 91 |   });
 92 | 
 93 |   describe('renderOperationDetail (Get Operation Detail)', () => {
 94 |     it('should return detail for a single valid method', () => {
 95 |       // Provide all 3 arguments to constructor
 96 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
 97 |       const result = renderable.renderOperationDetail(mockContext, ['get']);
 98 |       expect(result).toHaveLength(1);
 99 |       expect(result[0]).toEqual({
100 |         uriSuffix: `${pathUriSuffix}/get`,
101 |         data: samplePathItem.get, // Expect raw operation object
102 |       });
103 |     });
104 | 
105 |     it('should return details for multiple valid methods', () => {
106 |       // Provide all 3 arguments to constructor
107 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
108 |       const result = renderable.renderOperationDetail(mockContext, ['post', 'delete']);
109 |       expect(result).toHaveLength(2);
110 |       expect(result).toContainEqual({
111 |         uriSuffix: `${pathUriSuffix}/post`,
112 |         data: samplePathItem.post,
113 |       });
114 |       expect(result).toContainEqual({
115 |         uriSuffix: `${pathUriSuffix}/delete`,
116 |         data: samplePathItem.delete,
117 |       });
118 |     });
119 | 
120 |     it('should return error for non-existent method', () => {
121 |       // Provide all 3 arguments to constructor
122 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
123 |       const result = renderable.renderOperationDetail(mockContext, ['put']);
124 |       expect(result).toHaveLength(1);
125 |       expect(result[0]).toEqual({
126 |         uriSuffix: `${pathUriSuffix}/put`,
127 |         data: null,
128 |         isError: true,
129 |         errorText: 'Method "PUT" not found for path.',
130 |         renderAsList: true,
131 |       });
132 |     });
133 | 
134 |     it('should handle mix of valid and invalid methods', () => {
135 |       // Provide all 3 arguments to constructor
136 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
137 |       const result = renderable.renderOperationDetail(mockContext, ['get', 'patch']);
138 |       expect(result).toHaveLength(2);
139 |       expect(result).toContainEqual({
140 |         uriSuffix: `${pathUriSuffix}/get`,
141 |         data: samplePathItem.get,
142 |       });
143 |       expect(result).toContainEqual({
144 |         uriSuffix: `${pathUriSuffix}/patch`,
145 |         data: null,
146 |         isError: true,
147 |         errorText: 'Method "PATCH" not found for path.',
148 |         renderAsList: true,
149 |       });
150 |     });
151 | 
152 |     it('should return error if path item is undefined', () => {
153 |       // Provide all 3 arguments to constructor
154 |       const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
155 |       const result = renderable.renderOperationDetail(mockContext, ['get']);
156 |       expect(result).toHaveLength(1);
157 |       expect(result[0]).toEqual({
158 |         uriSuffix: `${pathUriSuffix}/get`,
159 |         data: null,
160 |         isError: true,
161 |         errorText: 'Path item not found.',
162 |         renderAsList: true,
163 |       });
164 |     });
165 |   });
166 | 
167 |   describe('renderDetail (Interface Method)', () => {
168 |     it('should delegate to renderList', () => {
169 |       // Provide all 3 arguments to constructor
170 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
171 |       const listResult = renderable.renderList(mockContext);
172 |       const detailResult = renderable.renderDetail(mockContext);
173 |       expect(detailResult).toEqual(listResult);
174 |     });
175 |   });
176 | 
177 |   describe('getOperation', () => {
178 |     it('should return correct operation object (case-insensitive)', () => {
179 |       // Provide all 3 arguments to constructor
180 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
181 |       expect(renderable.getOperation('get')).toBe(samplePathItem.get);
182 |       expect(renderable.getOperation('POST')).toBe(samplePathItem.post);
183 |       expect(renderable.getOperation('Delete')).toBe(samplePathItem.delete);
184 |     });
185 | 
186 |     it('should return undefined for non-existent method', () => {
187 |       // Provide all 3 arguments to constructor
188 |       const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
189 |       expect(renderable.getOperation('put')).toBeUndefined();
190 |     });
191 | 
192 |     it('should return undefined if path item is undefined', () => {
193 |       // Provide all 3 arguments to constructor
194 |       const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
195 |       expect(renderable.getOperation('get')).toBeUndefined();
196 |     });
197 |   });
198 | });
199 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RequestId } from '@modelcontextprotocol/sdk/types.js';
  3 | import { OperationHandler } from '../../../../src/handlers/operation-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();
 18 | 
 19 | // Sample Data
 20 | const getOperation: OpenAPIV3.OperationObject = {
 21 |   summary: 'Get Item',
 22 |   responses: { '200': { description: 'OK' } },
 23 | };
 24 | const postOperation: OpenAPIV3.OperationObject = {
 25 |   summary: 'Create Item',
 26 |   responses: { '201': { description: 'Created' } },
 27 | };
 28 | const sampleSpec: OpenAPIV3.Document = {
 29 |   openapi: '3.0.3',
 30 |   info: { title: 'Test API', version: '1.0.0' },
 31 |   paths: {
 32 |     '/items': {
 33 |       get: getOperation,
 34 |       post: postOperation,
 35 |     },
 36 |     '/items/{id}': {
 37 |       get: { summary: 'Get Single Item', responses: { '200': { description: 'OK' } } },
 38 |     },
 39 |   },
 40 |   components: {},
 41 | };
 42 | 
 43 | const encodedPathItems = encodeURIComponent('items');
 44 | const encodedPathNonExistent = encodeURIComponent('nonexistent');
 45 | 
 46 | describe('OperationHandler', () => {
 47 |   let handler: OperationHandler;
 48 | 
 49 |   beforeEach(() => {
 50 |     handler = new OperationHandler(mockSpecLoader, mockFormatter);
 51 |     mockGetTransformedSpec.mockReset();
 52 |     mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
 53 |   });
 54 | 
 55 |   it('should return the correct template', () => {
 56 |     const template = handler.getTemplate();
 57 |     expect(template).toBeInstanceOf(ResourceTemplate);
 58 |     expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}/{method*}');
 59 |   });
 60 | 
 61 |   describe('handleRequest', () => {
 62 |     const mockExtra = {
 63 |       signal: new AbortController().signal,
 64 |       sendNotification: jest.fn(),
 65 |       sendRequest: jest.fn(),
 66 |       requestId: 'test-request-id' as RequestId,
 67 |     };
 68 | 
 69 |     it('should return detail for a single valid method', async () => {
 70 |       const variables: Variables = { path: encodedPathItems, method: 'get' }; // Use 'method' key
 71 |       const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
 72 | 
 73 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 74 | 
 75 |       expect(mockGetTransformedSpec).toHaveBeenCalledWith({
 76 |         resourceType: 'schema',
 77 |         format: 'openapi',
 78 |       });
 79 |       expect(result.contents).toHaveLength(1);
 80 |       expect(result.contents[0]).toEqual({
 81 |         uri: `openapi://paths/${encodedPathItems}/get`,
 82 |         mimeType: 'application/json',
 83 |         text: JSON.stringify(getOperation, null, 2),
 84 |         isError: false,
 85 |       });
 86 |     });
 87 | 
 88 |     it('should return details for multiple valid methods (array input)', async () => {
 89 |       const variables: Variables = { path: encodedPathItems, method: ['get', 'post'] }; // Use 'method' key with array
 90 |       const uri = new URL(`openapi://paths/${encodedPathItems}/get,post`); // URI might not reflect array input
 91 | 
 92 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 93 | 
 94 |       expect(result.contents).toHaveLength(2);
 95 |       expect(result.contents).toContainEqual({
 96 |         uri: `openapi://paths/${encodedPathItems}/get`,
 97 |         mimeType: 'application/json',
 98 |         text: JSON.stringify(getOperation, null, 2),
 99 |         isError: false,
100 |       });
101 |       expect(result.contents).toContainEqual({
102 |         uri: `openapi://paths/${encodedPathItems}/post`,
103 |         mimeType: 'application/json',
104 |         text: JSON.stringify(postOperation, null, 2),
105 |         isError: false,
106 |       });
107 |     });
108 | 
109 |     it('should return error for non-existent path', async () => {
110 |       const variables: Variables = { path: encodedPathNonExistent, method: 'get' };
111 |       const uri = new URL(`openapi://paths/${encodedPathNonExistent}/get`);
112 |       const expectedLogMessage = /Path "\/nonexistent" not found/;
113 | 
114 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
115 |         handler.handleRequest(uri, variables, mockExtra)
116 |       );
117 | 
118 |       expect(result.contents).toHaveLength(1);
119 |       // Expect the specific error message from getValidatedPathItem
120 |       expect(result.contents[0]).toEqual({
121 |         uri: `openapi://paths/${encodedPathNonExistent}/get`,
122 |         mimeType: 'text/plain',
123 |         text: 'Path "/nonexistent" not found in the specification.',
124 |         isError: true,
125 |       });
126 |     });
127 | 
128 |     it('should return error for non-existent method', async () => {
129 |       const variables: Variables = { path: encodedPathItems, method: 'put' };
130 |       const uri = new URL(`openapi://paths/${encodedPathItems}/put`);
131 |       const expectedLogMessage = /None of the requested methods \(put\) are valid/;
132 | 
133 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
134 |         handler.handleRequest(uri, variables, mockExtra)
135 |       );
136 | 
137 |       expect(result.contents).toHaveLength(1);
138 |       // Expect the specific error message from getValidatedOperations
139 |       expect(result.contents[0]).toEqual({
140 |         uri: `openapi://paths/${encodedPathItems}/put`,
141 |         mimeType: 'text/plain',
142 |         text: 'None of the requested methods (put) are valid for path "/items". Available methods: get, post',
143 |         isError: true,
144 |       });
145 |     });
146 | 
147 |     // Remove test for mix of valid/invalid methods, as getValidatedOperations throws now
148 |     // it('should handle mix of valid and invalid methods', async () => { ... });
149 | 
150 |     it('should handle empty method array', async () => {
151 |       const variables: Variables = { path: encodedPathItems, method: [] };
152 |       const uri = new URL(`openapi://paths/${encodedPathItems}/`);
153 |       const expectedLogMessage = /No valid HTTP method specified/;
154 | 
155 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
156 |         handler.handleRequest(uri, variables, mockExtra)
157 |       );
158 | 
159 |       expect(result.contents).toHaveLength(1);
160 |       expect(result.contents[0]).toEqual({
161 |         uri: `openapi://paths/${encodedPathItems}/`,
162 |         mimeType: 'text/plain',
163 |         text: 'No valid HTTP method specified.',
164 |         isError: true,
165 |       });
166 |     });
167 | 
168 |     it('should handle spec loading errors', async () => {
169 |       const error = new Error('Spec load failed');
170 |       mockGetTransformedSpec.mockRejectedValue(error);
171 |       const variables: Variables = { path: encodedPathItems, method: 'get' };
172 |       const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
173 |       const expectedLogMessage = /Spec load failed/;
174 | 
175 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
176 |         handler.handleRequest(uri, variables, mockExtra)
177 |       );
178 | 
179 |       expect(result.contents).toHaveLength(1);
180 |       expect(result.contents[0]).toEqual({
181 |         uri: `openapi://paths/${encodedPathItems}/get`,
182 |         mimeType: 'text/plain',
183 |         text: 'Spec load failed',
184 |         isError: true,
185 |       });
186 |     });
187 | 
188 |     it('should handle non-OpenAPI v3 spec', async () => {
189 |       const invalidSpec = { swagger: '2.0', info: {} };
190 |       mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
191 |       const variables: Variables = { path: encodedPathItems, method: 'get' };
192 |       const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
193 |       const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
194 | 
195 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
196 |         handler.handleRequest(uri, variables, mockExtra)
197 |       );
198 | 
199 |       expect(result.contents).toHaveLength(1);
200 |       expect(result.contents[0]).toEqual({
201 |         uri: `openapi://paths/${encodedPathItems}/get`,
202 |         mimeType: 'text/plain',
203 |         text: 'Only OpenAPI v3 specifications are supported',
204 |         isError: true,
205 |       });
206 |     });
207 |   });
208 | });
209 | 
```

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

```markdown
  1 | # MCP OpenAPI Schema Explorer Usage Guide
  2 | 
  3 | This guide explains how to add the MCP OpenAPI Schema Explorer server to your MCP client (e.g., Claude Desktop, Windsurf, Cline). This involves adding a configuration entry to your client's settings file that tells the client how to run the server process. The server itself doesn't require separate configuration beyond the command-line arguments specified in the client settings.
  4 | 
  5 | ## Prerequisites
  6 | 
  7 | 1.  Node.js (Latest LTS version recommended) OR Docker installed.
  8 | 2.  Access to an OpenAPI v3.0 or Swagger v2.0 specification file, either via a local file path or a remote HTTP/HTTPS URL.
  9 | 3.  An MCP client application (e.g., Claude Desktop, Windsurf, Cline, etc.).
 10 | 
 11 | ## Installation
 12 | 
 13 | For the recommended usage methods (`npx` and Docker, described below), **no separate installation step is required**. Your MCP client will download the package or pull the Docker image automatically based on the configuration you provide in its settings.
 14 | 
 15 | If you prefer to install the server explicitly:
 16 | 
 17 | - **Global Install:** Run `npm install -g mcp-openapi-schema-explorer`. See **Usage Method 3** for how to configure your client to use this.
 18 | - **Local Install (for Development):** Clone the repository (`git clone ...`), install dependencies (`npm install`), and build (`npm run build`). See **Usage Method 4** for how to configure your client to use this.
 19 | 
 20 | ## Usage Method 1: npx (Recommended)
 21 | 
 22 | This is the recommended method as it avoids global/local installation and ensures you use the latest published version.
 23 | 
 24 | ### Client Configuration Entry (npx Method)
 25 | 
 26 | Add the following JSON object to the `mcpServers` section of your MCP client's configuration file (e.g., `claude_desktop_config.json`). This entry instructs the client on how to run the server using `npx`:
 27 | 
 28 | ```json
 29 | {
 30 |   "mcpServers": {
 31 |     "My API Spec (npx)": {
 32 |       "command": "npx",
 33 |       "args": [
 34 |         "-y",
 35 |         "mcp-openapi-schema-explorer@latest",
 36 |         "<path-or-url-to-spec>",
 37 |         "--output-format",
 38 |         "yaml"
 39 |       ],
 40 |       "env": {}
 41 |     }
 42 |   }
 43 | }
 44 | ```
 45 | 
 46 | **Configuration Details:**
 47 | 
 48 | 1.  **Replace `"My API Spec (npx)"`:** Choose a descriptive name for this server instance.
 49 | 2.  **Replace `<path-or-url-to-spec>`:** Provide the **required** absolute local file path (e.g., `/path/to/your/api.yaml`) or the full remote URL (e.g., `https://petstore3.swagger.io/api/v3/openapi.json`).
 50 | 3.  **(Optional)** Adjust the `--output-format` value (`yaml`, `json`, `json-minified`). Defaults to `json`.
 51 | 
 52 | ## Usage Method 2: Docker
 53 | 
 54 | You can instruct your MCP client to run the server using the official Docker image: `kadykov/mcp-openapi-schema-explorer`.
 55 | 
 56 | ### Client Configuration Entry (Docker Method)
 57 | 
 58 | - **Using a Remote URL:**
 59 | 
 60 |   ```json
 61 |   {
 62 |     "mcpServers": {
 63 |       "My API Spec (Docker Remote)": {
 64 |         "command": "docker",
 65 |         "args": [
 66 |           "run",
 67 |           "--rm",
 68 |           "-i",
 69 |           "kadykov/mcp-openapi-schema-explorer:latest",
 70 |           "<remote-url-to-spec>"
 71 |         ],
 72 |         "env": {}
 73 |       }
 74 |     }
 75 |   }
 76 |   ```
 77 | 
 78 | - **Using a Local File:** (Requires mounting the file into the container)
 79 |   ```json
 80 |   {
 81 |     "mcpServers": {
 82 |       "My API Spec (Docker Local)": {
 83 |         "command": "docker",
 84 |         "args": [
 85 |           "run",
 86 |           "--rm",
 87 |           "-i",
 88 |           "-v",
 89 |           "/full/host/path/to/spec.yaml:/spec/api.yaml",
 90 |           "kadykov/mcp-openapi-schema-explorer:latest",
 91 |           "/spec/api.yaml",
 92 |           "--output-format",
 93 |           "yaml"
 94 |         ],
 95 |         "env": {}
 96 |       }
 97 |     }
 98 |   }
 99 |   ```
100 |   **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.
101 | 
102 | ## Usage Method 3: Global Installation (Less Common)
103 | 
104 | You can install the package globally, although `npx` is generally preferred.
105 | 
106 | ```bash
107 | # Run this command once in your terminal
108 | npm install -g mcp-openapi-schema-explorer
109 | ```
110 | 
111 | ### Client Configuration Entry (Global Install Method)
112 | 
113 | 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.
114 | 
115 | ```json
116 | {
117 |   "mcpServers": {
118 |     "My API Spec (Global)": {
119 |       "command": "mcp-openapi-schema-explorer",
120 |       "args": ["<path-or-url-to-spec>", "--output-format", "yaml"],
121 |       "env": {}
122 |     }
123 |   }
124 | }
125 | ```
126 | 
127 | - **`command`:** Use the globally installed command name. You might need the full path if it's not in your system's PATH environment variable accessible by the MCP client.
128 | 
129 | ## Usage Method 4: Local Development/Installation
130 | 
131 | This method is useful for development or running a locally modified version of the server.
132 | 
133 | ### Setup Steps (Run once in your terminal)
134 | 
135 | 1.  Clone the repository: `git clone https://github.com/kadykov/mcp-openapi-schema-explorer.git`
136 | 2.  Navigate into the directory: `cd mcp-openapi-schema-explorer`
137 | 3.  Install dependencies: `npm install`
138 | 4.  Build the project: `npm run build` (or `just build`)
139 | 
140 | ### Client Configuration Entry (Local Development Method)
141 | 
142 | Add the following entry to your MCP client's configuration file. This instructs the client to run the locally built server using `node`.
143 | 
144 | ```json
145 | {
146 |   "mcpServers": {
147 |     "My API Spec (Local Dev)": {
148 |       "command": "node",
149 |       "args": [
150 |         "/full/path/to/cloned/mcp-openapi-schema-explorer/dist/src/index.js",
151 |         "<path-or-url-to-spec>",
152 |         "--output-format",
153 |         "yaml"
154 |       ],
155 | 
156 |       "env": {}
157 |     }
158 |   }
159 | }
160 | ```
161 | 
162 | **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.
163 | 
164 | ## Verification
165 | 
166 | After adding the server entry to your MCP client's configuration:
167 | 
168 | 1.  The server should appear in the list of available MCP servers within your client (e.g., named "My API Spec (npx)" or whatever key you used). The server name might dynamically update based on the spec's `info.title` (e.g., "Schema Explorer for Petstore API").
169 | 2.  Test the connection by accessing a basic resource, for example (using your chosen server name):
170 |     ```
171 |     /mcp "My API Spec (npx)" access openapi://info
172 |     ```
173 | 
174 | ## Troubleshooting
175 | 
176 | Common issues and solutions:
177 | 
178 | 1.  **Server Fails to Start:**
179 |     - Verify the `<path-or-url-to-spec>` is correct, accessible, and properly quoted in the JSON configuration.
180 |     - Ensure the specification file is a valid OpenAPI v3.0 or Swagger v2.0 document (JSON or YAML).
181 |     - Check Node.js version (LTS recommended) if using `npx`, global, or local install.
182 |     - Check Docker installation and permissions if using Docker.
183 |     - For remote URLs, check network connectivity.
184 |     - For Docker with local files, ensure the volume mount path (`-v` flag) is correct and the host path exists.
185 |     - For Local Development, ensure the path to `dist/src/index.js` is correct and the project has been built (`npm run build`).
186 | 2.  **Resources Not Loading or Errors:**
187 |     - Double-check the resource URI syntax (e.g., `openapi://paths`, `openapi://components/schemas/MySchema`). Remember that path segments in URIs need URL encoding (e.g., `/users/{id}` becomes `users%2F%7Bid%7D`).
188 |     - Ensure the requested path, method, or component exists in the specification.
189 | 
190 | ## Environment Variables
191 | 
192 | No environment variables are required for the server to operate.
193 | 
194 | ## Additional Notes
195 | 
196 | - The server automatically handles loading specs from local files or remote URLs.
197 | - Swagger v2.0 specifications are automatically converted to OpenAPI v3.0 internally.
198 | - Internal references (`#/components/...`) are transformed into clickable MCP URIs (`openapi://components/...`).
199 | - The server name displayed in the client might be dynamically generated from the specification's title.
200 | 
201 | ## Support
202 | 
203 | If you encounter any issues:
204 | 
205 | 1.  Check the project's main README for more details: [https://github.com/kadykov/mcp-openapi-schema-explorer#readme](https://github.com/kadykov/mcp-openapi-schema-explorer#readme)
206 | 2.  Submit an issue on GitHub: [https://github.com/kadykov/mcp-openapi-schema-explorer/issues](https://github.com/kadykov/mcp-openapi-schema-explorer/issues)
207 | 
208 | ---
209 | 
210 | This guide provides instructions for adding the server to your MCP client using various execution methods. Refer to the main project README for comprehensive documentation on features and resource usage.
211 | 
```

--------------------------------------------------------------------------------
/src/rendering/components.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
  3 | import { createErrorResult, generateListHint } from './utils.js'; // Add .js
  4 | 
  5 | // Define valid component types based on OpenAPIV3.ComponentsObject keys
  6 | export type ComponentType = keyof OpenAPIV3.ComponentsObject;
  7 | export const VALID_COMPONENT_TYPES: ComponentType[] = [
  8 |   'schemas',
  9 |   'responses',
 10 |   'parameters',
 11 |   'examples',
 12 |   'requestBodies',
 13 |   'headers',
 14 |   'securitySchemes',
 15 |   'links',
 16 |   'callbacks',
 17 |   // 'pathItems' is technically allowed but we handle paths separately
 18 | ];
 19 | 
 20 | // Simple descriptions for component types
 21 | const componentTypeDescriptions: Record<ComponentType, string> = {
 22 |   schemas: 'Reusable data structures (models)',
 23 |   responses: 'Reusable API responses',
 24 |   parameters: 'Reusable request parameters (query, path, header, cookie)',
 25 |   examples: 'Reusable examples of media type payloads',
 26 |   requestBodies: 'Reusable request body definitions',
 27 |   headers: 'Reusable header definitions for responses',
 28 |   securitySchemes: 'Reusable security scheme definitions (e.g., API keys, OAuth2)',
 29 |   links: 'Reusable descriptions of links between responses and operations',
 30 |   callbacks: 'Reusable descriptions of callback operations',
 31 |   // pathItems: 'Reusable path item definitions (rarely used directly here)' // Excluded as per comment above
 32 | };
 33 | // Use a Map for safer lookups against prototype pollution
 34 | const componentDescriptionsMap = new Map(Object.entries(componentTypeDescriptions));
 35 | 
 36 | /**
 37 |  * Wraps an OpenAPIV3.ComponentsObject to make it renderable.
 38 |  * Handles listing the available component types.
 39 |  */
 40 | export class RenderableComponents implements RenderableSpecObject {
 41 |   constructor(private components: OpenAPIV3.ComponentsObject | undefined) {}
 42 | 
 43 |   /**
 44 |    * Renders a list of available component types found in the spec.
 45 |    * Corresponds to the `openapi://components` URI.
 46 |    */
 47 |   renderList(context: RenderContext): RenderResultItem[] {
 48 |     if (!this.components || Object.keys(this.components).length === 0) {
 49 |       return createErrorResult('components', 'No components found in the specification.');
 50 |     }
 51 | 
 52 |     const availableTypes = Object.keys(this.components).filter((key): key is ComponentType =>
 53 |       VALID_COMPONENT_TYPES.includes(key as ComponentType)
 54 |     );
 55 | 
 56 |     if (availableTypes.length === 0) {
 57 |       return createErrorResult('components', 'No valid component types found.');
 58 |     }
 59 | 
 60 |     let listText = 'Available Component Types:\n\n';
 61 |     availableTypes.sort().forEach(type => {
 62 |       const description = componentDescriptionsMap.get(type) ?? 'Unknown component type'; // Removed unnecessary 'as ComponentType'
 63 |       listText += `- ${type}: ${description}\n`;
 64 |     });
 65 | 
 66 |     // Use the new hint generator structure, providing the first type as an example
 67 |     const firstTypeExample = availableTypes.length > 0 ? availableTypes[0] : undefined;
 68 |     listText += generateListHint(context, {
 69 |       itemType: 'componentType',
 70 |       firstItemExample: firstTypeExample,
 71 |     });
 72 | 
 73 |     return [
 74 |       {
 75 |         uriSuffix: 'components',
 76 |         data: listText,
 77 |         renderAsList: true,
 78 |       },
 79 |     ];
 80 |   }
 81 | 
 82 |   /**
 83 |    * Detail view for the main 'components' object isn't meaningful.
 84 |    */
 85 |   renderDetail(context: RenderContext): RenderResultItem[] {
 86 |     return this.renderList(context);
 87 |   }
 88 | 
 89 |   /**
 90 |    * Gets the map object for a specific component type.
 91 |    * @param type - The component type (e.g., 'schemas').
 92 |    * @returns The map (e.g., ComponentsObject['schemas']) or undefined.
 93 |    */
 94 |   getComponentMap(type: ComponentType):
 95 |     | Record<
 96 |         string,
 97 |         | OpenAPIV3.SchemaObject
 98 |         | OpenAPIV3.ResponseObject
 99 |         | OpenAPIV3.ParameterObject
100 |         | OpenAPIV3.ExampleObject
101 |         | OpenAPIV3.RequestBodyObject
102 |         | OpenAPIV3.HeaderObject
103 |         | OpenAPIV3.SecuritySchemeObject
104 |         | OpenAPIV3.LinkObject
105 |         | OpenAPIV3.CallbackObject
106 |         | OpenAPIV3.ReferenceObject // Include ReferenceObject
107 |       >
108 |     | undefined {
109 |     // Use Map for safe access
110 |     if (!this.components) {
111 |       return undefined;
112 |     }
113 |     const componentsMap = new Map(Object.entries(this.components));
114 |     // Cast needed as Map.get returns the value type or undefined
115 |     return componentsMap.get(type) as ReturnType<RenderableComponents['getComponentMap']>;
116 |   }
117 | }
118 | 
119 | // =====================================================================
120 | 
121 | /**
122 |  * Wraps a map of components of a specific type (e.g., all schemas).
123 |  * Handles listing component names and rendering component details.
124 |  */
125 | export class RenderableComponentMap implements RenderableSpecObject {
126 |   constructor(
127 |     private componentMap: ReturnType<RenderableComponents['getComponentMap']>,
128 |     private componentType: ComponentType, // e.g., 'schemas'
129 |     private mapUriSuffix: string // e.g., 'components/schemas'
130 |   ) {}
131 | 
132 |   /**
133 |    * Renders a list of component names for the specific type.
134 |    * Corresponds to the `openapi://components/{type}` URI.
135 |    */
136 |   renderList(context: RenderContext): RenderResultItem[] {
137 |     if (!this.componentMap || Object.keys(this.componentMap).length === 0) {
138 |       return createErrorResult(
139 |         this.mapUriSuffix,
140 |         `No components of type "${this.componentType}" found.`
141 |       );
142 |     }
143 | 
144 |     const names = Object.keys(this.componentMap).sort();
145 |     let listText = `Available ${this.componentType}:\n\n`;
146 |     names.forEach(name => {
147 |       listText += `- ${name}\n`;
148 |     });
149 | 
150 |     // Use the new hint generator structure, providing parent type and first name as example
151 |     const firstNameExample = names.length > 0 ? names[0] : undefined;
152 |     listText += generateListHint(context, {
153 |       itemType: 'componentName',
154 |       parentComponentType: this.componentType,
155 |       firstItemExample: firstNameExample,
156 |     });
157 | 
158 |     return [
159 |       {
160 |         uriSuffix: this.mapUriSuffix,
161 |         data: listText,
162 |         renderAsList: true,
163 |       },
164 |     ];
165 |   }
166 | 
167 |   /**
168 |    * Renders the detail view for one or more specific named components
169 |    * Renders the detail view. For a component map, this usually means listing
170 |    * the component names, similar to renderList. The handler should call
171 |    * `renderComponentDetail` for specific component details.
172 |    */
173 |   renderDetail(context: RenderContext): RenderResultItem[] {
174 |     // Delegate to renderList as the primary view for a component map itself.
175 |     return this.renderList(context);
176 |   }
177 | 
178 |   /**
179 |    * Renders the detail view for one or more specific named components
180 |    * within this map.
181 |    * Corresponds to the `openapi://components/{type}/{name*}` URI.
182 |    * This is called by the handler after identifying the name(s).
183 |    *
184 |    * @param _context - The rendering context (might be needed later).
185 |    * @param names - Array of component names.
186 |    * @returns An array of RenderResultItem representing the component details.
187 |    */
188 |   renderComponentDetail(_context: RenderContext, names: string[]): RenderResultItem[] {
189 |     if (!this.componentMap) {
190 |       // Create error results for all requested names if map is missing
191 |       return names.map(name => ({
192 |         uriSuffix: `${this.mapUriSuffix}/${name}`,
193 |         data: null,
194 |         isError: true,
195 |         errorText: `Component map for type "${this.componentType}" not found.`,
196 |         renderAsList: true,
197 |       }));
198 |     }
199 | 
200 |     const results: RenderResultItem[] = [];
201 |     for (const name of names) {
202 |       const component = this.getComponent(name);
203 |       const componentUriSuffix = `${this.mapUriSuffix}/${name}`;
204 | 
205 |       if (!component) {
206 |         results.push({
207 |           uriSuffix: componentUriSuffix,
208 |           data: null,
209 |           isError: true,
210 |           errorText: `Component "${name}" of type "${this.componentType}" not found.`,
211 |           renderAsList: true,
212 |         });
213 |       } else {
214 |         // Return the raw component object; handler will format it
215 |         results.push({
216 |           uriSuffix: componentUriSuffix,
217 |           data: component,
218 |         });
219 |       }
220 |     }
221 |     return results;
222 |   }
223 | 
224 |   /**
225 |    * Gets a specific component object by name.
226 |    * @param name - The name of the component.
227 |    * @returns The component object (or ReferenceObject) or undefined.
228 |    */
229 |   getComponent(
230 |     name: string
231 |   ):
232 |     | OpenAPIV3.SchemaObject
233 |     | OpenAPIV3.ResponseObject
234 |     | OpenAPIV3.ParameterObject
235 |     | OpenAPIV3.ExampleObject
236 |     | OpenAPIV3.RequestBodyObject
237 |     | OpenAPIV3.HeaderObject
238 |     | OpenAPIV3.SecuritySchemeObject
239 |     | OpenAPIV3.LinkObject
240 |     | OpenAPIV3.CallbackObject
241 |     | OpenAPIV3.ReferenceObject
242 |     | undefined {
243 |     // Use Map for safe access
244 |     if (!this.componentMap) {
245 |       return undefined;
246 |     }
247 |     const detailsMap = new Map(Object.entries(this.componentMap));
248 |     // No cast needed, Map.get returns the correct type (ValueType | undefined)
249 |     return detailsMap.get(name);
250 |   }
251 | }
252 | 
```

--------------------------------------------------------------------------------
/test/__tests__/unit/services/spec-loader.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { SpecLoaderService } from '../../../../src/services/spec-loader.js';
  2 | import { ReferenceTransformService } from '../../../../src/services/reference-transform.js';
  3 | import { OpenAPIV3 } from 'openapi-types';
  4 | 
  5 | // Define mock implementations first
  6 | const mockConvertUrlImplementation = jest.fn();
  7 | const mockConvertFileImplementation = jest.fn();
  8 | 
  9 | // Mock the module, referencing the defined implementations
 10 | // IMPORTANT: The factory function for jest.mock runs BEFORE top-level variable assignments in the module scope.
 11 | // We need to access the mocks indirectly.
 12 | interface Swagger2OpenapiResult {
 13 |   openapi: OpenAPIV3.Document;
 14 |   options: unknown; // Use unknown for options as we don't have precise types here
 15 | }
 16 | 
 17 | jest.mock('swagger2openapi', () => {
 18 |   // Return an object where the properties are functions that call our mocks
 19 |   return {
 20 |     convertUrl: (url: string, options: unknown): Promise<Swagger2OpenapiResult> =>
 21 |       mockConvertUrlImplementation(url, options) as Promise<Swagger2OpenapiResult>, // Cast return type
 22 |     convertFile: (filename: string, options: unknown): Promise<Swagger2OpenapiResult> =>
 23 |       mockConvertFileImplementation(filename, options) as Promise<Swagger2OpenapiResult>, // Cast return type
 24 |   };
 25 | });
 26 | 
 27 | describe('SpecLoaderService', () => {
 28 |   const mockV3Spec: OpenAPIV3.Document = {
 29 |     openapi: '3.0.0',
 30 |     info: {
 31 |       title: 'Test V3 API',
 32 |       version: '1.0.0',
 33 |     },
 34 |     paths: {},
 35 |   };
 36 | 
 37 |   // Simulate the structure returned by swagger2openapi
 38 |   const mockS2OResult = {
 39 |     openapi: mockV3Spec,
 40 |     options: {}, // Add other properties if needed by tests
 41 |   };
 42 | 
 43 |   let referenceTransform: ReferenceTransformService;
 44 | 
 45 |   beforeEach(() => {
 46 |     // Reset the mock implementations
 47 |     mockConvertUrlImplementation.mockReset();
 48 |     mockConvertFileImplementation.mockReset();
 49 |     referenceTransform = new ReferenceTransformService();
 50 |     // Mock the transformDocument method for simplicity in these tests
 51 |     jest.spyOn(referenceTransform, 'transformDocument').mockImplementation(spec => spec);
 52 |   });
 53 | 
 54 |   describe('loadSpec', () => {
 55 |     it('loads local v3 spec using convertFile', async () => {
 56 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
 57 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
 58 |       const spec = await loader.loadSpec();
 59 | 
 60 |       expect(mockConvertFileImplementation).toHaveBeenCalledWith(
 61 |         '/path/to/spec.json',
 62 |         expect.any(Object)
 63 |       );
 64 |       expect(mockConvertUrlImplementation).not.toHaveBeenCalled();
 65 |       expect(spec).toEqual(mockV3Spec);
 66 |     });
 67 | 
 68 |     it('loads remote v3 spec using convertUrl', async () => {
 69 |       mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
 70 |       const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);
 71 |       const spec = await loader.loadSpec();
 72 | 
 73 |       expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
 74 |         'http://example.com/spec.json',
 75 |         expect.any(Object)
 76 |       );
 77 |       expect(mockConvertFileImplementation).not.toHaveBeenCalled();
 78 |       expect(spec).toEqual(mockV3Spec);
 79 |     });
 80 | 
 81 |     it('loads and converts local v2 spec using convertFile', async () => {
 82 |       // Assume convertFile handles v2 internally and returns v3
 83 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
 84 |       const loader = new SpecLoaderService('/path/to/v2spec.json', referenceTransform);
 85 |       const spec = await loader.loadSpec();
 86 | 
 87 |       expect(mockConvertFileImplementation).toHaveBeenCalledWith(
 88 |         '/path/to/v2spec.json',
 89 |         expect.any(Object)
 90 |       );
 91 |       expect(mockConvertUrlImplementation).not.toHaveBeenCalled();
 92 |       expect(spec).toEqual(mockV3Spec); // Should be the converted v3 spec
 93 |     });
 94 | 
 95 |     it('loads and converts remote v2 spec using convertUrl', async () => {
 96 |       // Assume convertUrl handles v2 internally and returns v3
 97 |       mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
 98 |       const loader = new SpecLoaderService('https://example.com/v2spec.yaml', referenceTransform);
 99 |       const spec = await loader.loadSpec();
100 | 
101 |       expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
102 |         'https://example.com/v2spec.yaml',
103 |         expect.any(Object)
104 |       );
105 |       expect(mockConvertFileImplementation).not.toHaveBeenCalled();
106 |       expect(spec).toEqual(mockV3Spec); // Should be the converted v3 spec
107 |     });
108 | 
109 |     it('throws error if convertFile fails', async () => {
110 |       const loadError = new Error('File not found');
111 |       mockConvertFileImplementation.mockRejectedValue(loadError);
112 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
113 | 
114 |       await expect(loader.loadSpec()).rejects.toThrow(
115 |         'Failed to load/convert OpenAPI spec from /path/to/spec.json: File not found'
116 |       );
117 |     });
118 | 
119 |     it('throws error if convertUrl fails', async () => {
120 |       const loadError = new Error('Network error');
121 |       mockConvertUrlImplementation.mockRejectedValue(loadError);
122 |       const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);
123 | 
124 |       await expect(loader.loadSpec()).rejects.toThrow(
125 |         'Failed to load/convert OpenAPI spec from http://example.com/spec.json: Network error'
126 |       );
127 |     });
128 | 
129 |     it('throws error if result object is invalid', async () => {
130 |       mockConvertFileImplementation.mockResolvedValue({ options: {} }); // Missing openapi property
131 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
132 | 
133 |       await expect(loader.loadSpec()).rejects.toThrow(
134 |         'Failed to load/convert OpenAPI spec from /path/to/spec.json: Conversion or parsing failed to produce an OpenAPI document.'
135 |       );
136 |     });
137 |   });
138 | 
139 |   describe('getSpec', () => {
140 |     it('returns loaded spec after loadSpec called', async () => {
141 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
142 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
143 |       await loader.loadSpec(); // Load first
144 |       const spec = await loader.getSpec();
145 | 
146 |       expect(spec).toEqual(mockV3Spec);
147 |       // Ensure loadSpec was only called once implicitly by the first await
148 |       expect(mockConvertFileImplementation).toHaveBeenCalledTimes(1);
149 |     });
150 | 
151 |     it('loads spec via convertFile if not already loaded', async () => {
152 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
153 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
154 |       const spec = await loader.getSpec(); // Should trigger loadSpec
155 | 
156 |       expect(mockConvertFileImplementation).toHaveBeenCalledWith(
157 |         '/path/to/spec.json',
158 |         expect.any(Object)
159 |       );
160 |       expect(spec).toEqual(mockV3Spec);
161 |     });
162 | 
163 |     it('loads spec via convertUrl if not already loaded', async () => {
164 |       mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
165 |       const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);
166 |       const spec = await loader.getSpec(); // Should trigger loadSpec
167 | 
168 |       expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
169 |         'http://example.com/spec.json',
170 |         expect.any(Object)
171 |       );
172 |       expect(spec).toEqual(mockV3Spec);
173 |     });
174 |   });
175 | 
176 |   describe('getTransformedSpec', () => {
177 |     // Mock the transformer to return a distinctly modified object
178 |     const mockTransformedSpec = {
179 |       ...mockV3Spec,
180 |       info: { ...mockV3Spec.info, title: 'Transformed API' },
181 |     };
182 | 
183 |     beforeEach(() => {
184 |       jest
185 |         .spyOn(referenceTransform, 'transformDocument')
186 |         .mockImplementation(() => mockTransformedSpec);
187 |     });
188 | 
189 |     it('returns transformed spec after loading', async () => {
190 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
191 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
192 |       const spec = await loader.getTransformedSpec({ resourceType: 'endpoint', format: 'openapi' }); // Should load then transform
193 | 
194 |       expect(mockConvertFileImplementation).toHaveBeenCalledTimes(1); // Ensure loading happened
195 |       const transformSpy = jest.spyOn(referenceTransform, 'transformDocument');
196 |       expect(transformSpy).toHaveBeenCalledWith(
197 |         mockV3Spec,
198 |         expect.objectContaining({ resourceType: 'endpoint', format: 'openapi' })
199 |       );
200 |       expect(spec).toEqual(mockTransformedSpec);
201 |     });
202 | 
203 |     it('loads spec if not loaded before transforming', async () => {
204 |       mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
205 |       const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
206 |       await loader.getTransformedSpec({ resourceType: 'endpoint', format: 'openapi' }); // Trigger load
207 | 
208 |       expect(mockConvertFileImplementation).toHaveBeenCalledWith(
209 |         '/path/to/spec.json',
210 |         expect.any(Object)
211 |       );
212 |     });
213 |   });
214 | });
215 | 
```

--------------------------------------------------------------------------------
/test/__tests__/unit/services/reference-transform.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import {
  3 |   OpenAPITransformer,
  4 |   ReferenceTransformService,
  5 |   TransformContext,
  6 | } from '../../../../src/services/reference-transform';
  7 | 
  8 | describe('ReferenceTransformService', () => {
  9 |   let service: ReferenceTransformService;
 10 |   let transformer: OpenAPITransformer;
 11 | 
 12 |   beforeEach(() => {
 13 |     service = new ReferenceTransformService();
 14 |     transformer = new OpenAPITransformer();
 15 |     service.registerTransformer('openapi', transformer);
 16 |   });
 17 | 
 18 |   it('throws error for unknown format', () => {
 19 |     const context: TransformContext = {
 20 |       resourceType: 'endpoint',
 21 |       format: 'unknown' as 'openapi' | 'asyncapi' | 'graphql',
 22 |     };
 23 | 
 24 |     expect(() => service.transformDocument({}, context)).toThrow(
 25 |       'No transformer registered for format: unknown'
 26 |     );
 27 |   });
 28 | 
 29 |   it('transforms document using registered transformer', () => {
 30 |     const context: TransformContext = {
 31 |       resourceType: 'endpoint',
 32 |       format: 'openapi',
 33 |       path: '/tasks',
 34 |       method: 'get',
 35 |     };
 36 | 
 37 |     const doc: OpenAPIV3.Document = {
 38 |       openapi: '3.0.0',
 39 |       info: {
 40 |         title: 'Test API',
 41 |         version: '1.0.0',
 42 |       },
 43 |       paths: {
 44 |         '/tasks': {
 45 |           get: {
 46 |             responses: {
 47 |               '200': {
 48 |                 description: 'Success',
 49 |                 content: {
 50 |                   'application/json': {
 51 |                     schema: {
 52 |                       $ref: '#/components/schemas/Task',
 53 |                     },
 54 |                   },
 55 |                 },
 56 |               },
 57 |             },
 58 |           },
 59 |         },
 60 |       },
 61 |     };
 62 | 
 63 |     const result = service.transformDocument(doc, context);
 64 |     const operation = result.paths?.['/tasks']?.get;
 65 |     const response = operation?.responses?.['200'];
 66 |     expect(response).toBeDefined();
 67 |     expect('content' in response!).toBeTruthy();
 68 |     const responseObj = response! as OpenAPIV3.ResponseObject;
 69 |     expect(responseObj.content?.['application/json']?.schema).toBeDefined();
 70 |     // Expect the new format
 71 |     expect(responseObj.content!['application/json'].schema).toEqual({
 72 |       $ref: 'openapi://components/schemas/Task',
 73 |     });
 74 |   });
 75 | });
 76 | 
 77 | describe('OpenAPITransformer', () => {
 78 |   let transformer: OpenAPITransformer;
 79 | 
 80 |   beforeEach(() => {
 81 |     transformer = new OpenAPITransformer();
 82 |   });
 83 | 
 84 |   it('transforms schema references', () => {
 85 |     const context: TransformContext = {
 86 |       resourceType: 'endpoint',
 87 |       format: 'openapi',
 88 |     };
 89 | 
 90 |     const doc: OpenAPIV3.Document = {
 91 |       openapi: '3.0.0',
 92 |       info: { title: 'Test API', version: '1.0.0' },
 93 |       paths: {},
 94 |       components: {
 95 |         schemas: {
 96 |           Task: {
 97 |             $ref: '#/components/schemas/TaskId',
 98 |           },
 99 |         },
100 |       },
101 |     };
102 | 
103 |     const result = transformer.transformRefs(doc, context);
104 |     // Expect the new format
105 |     expect(result.components?.schemas?.Task).toEqual({
106 |       $ref: 'openapi://components/schemas/TaskId',
107 |     });
108 |   });
109 | 
110 |   it('handles nested references', () => {
111 |     const context: TransformContext = {
112 |       resourceType: 'endpoint',
113 |       format: 'openapi',
114 |     };
115 | 
116 |     const doc: OpenAPIV3.Document = {
117 |       openapi: '3.0.0',
118 |       info: { title: 'Test API', version: '1.0.0' },
119 |       paths: {
120 |         '/tasks': {
121 |           post: {
122 |             requestBody: {
123 |               required: true,
124 |               description: 'Task creation',
125 |               content: {
126 |                 'application/json': {
127 |                   schema: {
128 |                     $ref: '#/components/schemas/Task',
129 |                   },
130 |                 },
131 |               },
132 |             },
133 |             responses: {
134 |               '201': {
135 |                 description: 'Created',
136 |                 content: {
137 |                   'application/json': {
138 |                     schema: {
139 |                       $ref: '#/components/schemas/Task',
140 |                     },
141 |                   },
142 |                 },
143 |               },
144 |             },
145 |           },
146 |         },
147 |       },
148 |     };
149 | 
150 |     const result = transformer.transformRefs(doc, context);
151 |     const taskPath = result.paths?.['/tasks'];
152 |     expect(taskPath?.post).toBeDefined();
153 |     const operation = taskPath!.post!;
154 |     expect(operation.requestBody).toBeDefined();
155 |     expect('content' in operation.requestBody!).toBeTruthy();
156 |     const requestBody = operation.requestBody! as OpenAPIV3.RequestBodyObject;
157 |     expect(requestBody.content?.['application/json']?.schema).toBeDefined();
158 |     // Expect the new format
159 |     expect(requestBody.content['application/json'].schema).toEqual({
160 |       $ref: 'openapi://components/schemas/Task',
161 |     });
162 | 
163 |     // Also check the response reference in the same test
164 |     const response = operation.responses?.['201'];
165 |     expect(response).toBeDefined();
166 |     expect('content' in response).toBeTruthy();
167 |     const responseObj = response as OpenAPIV3.ResponseObject;
168 |     expect(responseObj.content?.['application/json']?.schema).toBeDefined();
169 |     // Expect the new format
170 |     expect(responseObj.content!['application/json'].schema).toEqual({
171 |       $ref: 'openapi://components/schemas/Task',
172 |     });
173 |   });
174 | 
175 |   it('keeps external references unchanged', () => {
176 |     const context: TransformContext = {
177 |       resourceType: 'endpoint',
178 |       format: 'openapi',
179 |     };
180 | 
181 |     const doc: OpenAPIV3.Document = {
182 |       openapi: '3.0.0',
183 |       info: { title: 'Test API', version: '1.0.0' },
184 |       paths: {},
185 |       components: {
186 |         schemas: {
187 |           Task: {
188 |             $ref: 'https://example.com/schemas/Task',
189 |           },
190 |         },
191 |       },
192 |     };
193 | 
194 |     const result = transformer.transformRefs(doc, context);
195 |     const task = result.components?.schemas?.Task as OpenAPIV3.ReferenceObject;
196 |     expect(task).toEqual({
197 |       $ref: 'https://example.com/schemas/Task',
198 |     });
199 |   });
200 | 
201 |   // This test is now invalid as non-schema internal refs *should* be transformed
202 |   // it('keeps non-schema internal references unchanged', () => { ... });
203 | 
204 |   it('transforms parameter references', () => {
205 |     const context: TransformContext = {
206 |       resourceType: 'endpoint',
207 |       format: 'openapi',
208 |     };
209 |     const doc: OpenAPIV3.Document = {
210 |       openapi: '3.0.0',
211 |       info: { title: 'Test API', version: '1.0.0' },
212 |       paths: {},
213 |       components: {
214 |         parameters: {
215 |           UserIdParam: {
216 |             $ref: '#/components/parameters/UserId', // Reference another parameter
217 |           },
218 |         },
219 |       },
220 |     };
221 |     const result = transformer.transformRefs(doc, context);
222 |     expect(result.components?.parameters?.UserIdParam).toEqual({
223 |       $ref: 'openapi://components/parameters/UserId', // Expect transformation
224 |     });
225 |   });
226 | 
227 |   it('transforms response references', () => {
228 |     const context: TransformContext = {
229 |       resourceType: 'endpoint',
230 |       format: 'openapi',
231 |     };
232 |     const doc: OpenAPIV3.Document = {
233 |       openapi: '3.0.0',
234 |       info: { title: 'Test API', version: '1.0.0' },
235 |       paths: {},
236 |       components: {
237 |         responses: {
238 |           GenericError: {
239 |             $ref: '#/components/responses/ErrorModel', // Reference another response
240 |           },
241 |         },
242 |       },
243 |     };
244 |     const result = transformer.transformRefs(doc, context);
245 |     expect(result.components?.responses?.GenericError).toEqual({
246 |       $ref: 'openapi://components/responses/ErrorModel', // Expect transformation
247 |     });
248 |   });
249 | 
250 |   // Add tests for other component types if needed (examples, requestBodies, etc.)
251 | 
252 |   it('handles arrays properly', () => {
253 |     const context: TransformContext = {
254 |       resourceType: 'endpoint',
255 |       format: 'openapi',
256 |     };
257 | 
258 |     const doc: OpenAPIV3.Document = {
259 |       openapi: '3.0.0',
260 |       info: { title: 'Test API', version: '1.0.0' },
261 |       paths: {},
262 |       components: {
263 |         schemas: {
264 |           TaskList: {
265 |             type: 'object',
266 |             properties: {
267 |               items: {
268 |                 type: 'array',
269 |                 items: {
270 |                   $ref: '#/components/schemas/Task',
271 |                 },
272 |               },
273 |             },
274 |           },
275 |         },
276 |       },
277 |     };
278 | 
279 |     const result = transformer.transformRefs(doc, context);
280 |     const schema = result.components?.schemas?.TaskList;
281 |     expect(schema).toBeDefined();
282 |     expect('properties' in schema!).toBeTruthy();
283 |     const schemaObject = schema! as OpenAPIV3.SchemaObject;
284 |     expect(schemaObject.properties?.items).toBeDefined();
285 |     const arraySchema = schemaObject.properties!.items as OpenAPIV3.ArraySchemaObject;
286 |     // Expect the new format
287 |     expect(arraySchema.items).toEqual({
288 |       $ref: 'openapi://components/schemas/Task',
289 |     });
290 |   });
291 | 
292 |   it('preserves non-reference values', () => {
293 |     const context: TransformContext = {
294 |       resourceType: 'endpoint',
295 |       format: 'openapi',
296 |     };
297 | 
298 |     const doc: OpenAPIV3.Document = {
299 |       openapi: '3.0.0',
300 |       info: {
301 |         title: 'Test API',
302 |         version: '1.0.0',
303 |       },
304 |       paths: {},
305 |       components: {
306 |         schemas: {
307 |           Test: {
308 |             type: 'object',
309 |             properties: {
310 |               name: { type: 'string' },
311 |             },
312 |           },
313 |         },
314 |       },
315 |     };
316 | 
317 |     const result = transformer.transformRefs(doc, context);
318 |     expect(result).toEqual(doc);
319 |   });
320 | });
321 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderContext, RenderResultItem } from '../rendering/types.js'; // Already has .js
  3 | // Remove McpError/ErrorCode import - use standard Error
  4 | 
  5 | // Define the structure expected for each item in the contents array
  6 | export type FormattedResultItem = {
  7 |   uri: string;
  8 |   mimeType?: string;
  9 |   text: string;
 10 |   isError?: boolean;
 11 | };
 12 | 
 13 | /**
 14 |  * Formats RenderResultItem array into an array compatible with the 'contents'
 15 |  * property of ReadResourceResultSchema (specifically TextResourceContents).
 16 |  */
 17 | export function formatResults(
 18 |   context: RenderContext,
 19 |   items: RenderResultItem[]
 20 | ): FormattedResultItem[] {
 21 |   // Add type check for formatter existence in context
 22 |   if (!context.formatter) {
 23 |     throw new Error('Formatter is missing in RenderContext for formatResults');
 24 |   }
 25 |   return items.map(item => {
 26 |     const uri = `${context.baseUri}${item.uriSuffix}`;
 27 |     let text: string;
 28 |     let mimeType: string;
 29 | 
 30 |     if (item.isError) {
 31 |       text = item.errorText || 'An unknown error occurred.';
 32 |       mimeType = 'text/plain';
 33 |     } else if (item.renderAsList) {
 34 |       text = typeof item.data === 'string' ? item.data : 'Invalid list data';
 35 |       mimeType = 'text/plain';
 36 |     } else {
 37 |       // Detail view: format using the provided formatter
 38 |       try {
 39 |         text = context.formatter.format(item.data);
 40 |         mimeType = context.formatter.getMimeType();
 41 |       } catch (formatError: unknown) {
 42 |         text = `Error formatting data for ${uri}: ${
 43 |           formatError instanceof Error ? formatError.message : String(formatError)
 44 |         }`;
 45 |         mimeType = 'text/plain';
 46 |         // Ensure isError is true if formatting fails
 47 |         item.isError = true;
 48 |         item.errorText = text; // Store the formatting error message
 49 |       }
 50 |     }
 51 | 
 52 |     // Construct the final object, prioritizing item.isError
 53 |     const finalItem: FormattedResultItem = {
 54 |       uri: uri,
 55 |       mimeType: mimeType,
 56 |       text: item.isError ? item.errorText || 'An unknown error occurred.' : text,
 57 |       isError: item.isError ?? false, // Default to false if not explicitly set
 58 |     };
 59 |     // Ensure mimeType is text/plain for errors
 60 |     if (finalItem.isError) {
 61 |       finalItem.mimeType = 'text/plain';
 62 |     }
 63 | 
 64 |     return finalItem;
 65 |   });
 66 | }
 67 | 
 68 | /**
 69 |  * Type guard to check if an object is an OpenAPIV3.Document.
 70 |  */
 71 | export function isOpenAPIV3(spec: unknown): spec is OpenAPIV3.Document {
 72 |   return (
 73 |     typeof spec === 'object' &&
 74 |     spec !== null &&
 75 |     'openapi' in spec &&
 76 |     typeof (spec as { openapi: unknown }).openapi === 'string' &&
 77 |     (spec as { openapi: string }).openapi.startsWith('3.')
 78 |   );
 79 | }
 80 | 
 81 | /**
 82 |  * Safely retrieves a PathItemObject from the specification using a Map.
 83 |  * Throws an McpError if the path is not found.
 84 |  *
 85 |  * @param spec The OpenAPIV3 Document.
 86 |  * @param path The decoded path string (e.g., '/users/{id}').
 87 |  * @returns The validated PathItemObject.
 88 |  * @throws {McpError} If the path is not found in spec.paths.
 89 |  */
 90 | export function getValidatedPathItem(
 91 |   spec: OpenAPIV3.Document,
 92 |   path: string
 93 | ): OpenAPIV3.PathItemObject {
 94 |   if (!spec.paths) {
 95 |     // Use standard Error
 96 |     throw new Error('Specification does not contain any paths.');
 97 |   }
 98 |   const pathsMap = new Map(Object.entries(spec.paths));
 99 |   const pathItem = pathsMap.get(path);
100 | 
101 |   if (!pathItem) {
102 |     const errorMessage = `Path "${path}" not found in the specification.`;
103 |     // Use standard Error
104 |     throw new Error(errorMessage);
105 |   }
106 |   // We assume the spec structure is valid if the key exists
107 |   return pathItem as OpenAPIV3.PathItemObject;
108 | }
109 | 
110 | /**
111 |  * Validates requested HTTP methods against a PathItemObject using a Map.
112 |  * Returns the list of valid requested methods.
113 |  * Throws an McpError if none of the requested methods are valid for the path item.
114 |  *
115 |  * @param pathItem The PathItemObject to check against.
116 |  * @param requestedMethods An array of lowercase HTTP methods requested by the user.
117 |  * @param pathForError The path string, used for creating informative error messages.
118 |  * @returns An array of the requested methods that are valid for this path item.
119 |  * @throws {McpError} If none of the requested methods are valid.
120 |  */
121 | export function getValidatedOperations(
122 |   pathItem: OpenAPIV3.PathItemObject,
123 |   requestedMethods: string[],
124 |   pathForError: string
125 | ): string[] {
126 |   const operationsMap = new Map<string, OpenAPIV3.OperationObject>();
127 |   Object.entries(pathItem).forEach(([method, operation]) => {
128 |     // Check if the key is a standard HTTP method before adding
129 |     if (
130 |       ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
131 |         method.toLowerCase()
132 |       )
133 |     ) {
134 |       operationsMap.set(method.toLowerCase(), operation as OpenAPIV3.OperationObject);
135 |     }
136 |   });
137 | 
138 |   // Validate using lowercase versions, but preserve original case for return
139 |   const requestedMethodsLower = requestedMethods.map(m => m.toLowerCase());
140 |   const validLowerMethods = requestedMethodsLower.filter(m => operationsMap.has(m));
141 | 
142 |   if (validLowerMethods.length === 0) {
143 |     const availableMethods = Array.from(operationsMap.keys()).join(', ');
144 |     // Show original case in error message for clarity
145 |     const errorMessage = `None of the requested methods (${requestedMethods.join(', ')}) are valid for path "${pathForError}". Available methods: ${availableMethods}`;
146 |     // Use standard Error
147 |     throw new Error(errorMessage);
148 |   }
149 | 
150 |   // Return the methods from the *original* requestedMethods array
151 |   // that correspond to the valid lowercase methods found.
152 |   return requestedMethods.filter(m => validLowerMethods.includes(m.toLowerCase()));
153 | }
154 | 
155 | /**
156 |  * Safely retrieves the component map for a specific type (e.g., schemas, responses)
157 |  * from the specification using a Map.
158 |  * Throws an McpError if spec.components or the specific type map is not found.
159 |  *
160 |  * @param spec The OpenAPIV3 Document.
161 |  * @param type The ComponentType string (e.g., 'schemas', 'responses').
162 |  * @returns The validated component map object (e.g., spec.components.schemas).
163 |  * @throws {McpError} If spec.components or the type map is not found.
164 |  */
165 | export function getValidatedComponentMap(
166 |   spec: OpenAPIV3.Document,
167 |   type: string // Keep as string for validation flexibility
168 | ): NonNullable<OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]> {
169 |   if (!spec.components) {
170 |     // Use standard Error
171 |     throw new Error('Specification does not contain a components section.');
172 |   }
173 |   // Validate the requested type against the actual keys in spec.components
174 |   const componentsMap = new Map(Object.entries(spec.components));
175 |   // Add type assertion for clarity, although the check below handles undefined
176 |   const componentMapObj = componentsMap.get(type) as
177 |     | OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
178 |     | undefined;
179 | 
180 |   if (!componentMapObj) {
181 |     const availableTypes = Array.from(componentsMap.keys()).join(', ');
182 |     const errorMessage = `Component type "${type}" not found in the specification. Available types: ${availableTypes}`;
183 |     // Use standard Error
184 |     throw new Error(errorMessage);
185 |   }
186 |   // We assume the spec structure is valid if the key exists
187 |   return componentMapObj as NonNullable<
188 |     OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
189 |   >;
190 | }
191 | 
192 | /**
193 |  * Validates requested component names against a specific component map (e.g., schemas).
194 |  * Returns an array of objects containing the valid name and its corresponding detail object.
195 |  * Throws an McpError if none of the requested names are valid for the component map.
196 |  *
197 |  * @param componentMap The specific component map object (e.g., spec.components.schemas).
198 |  * @param requestedNames An array of component names requested by the user.
199 |  * @param componentTypeForError The component type string, used for creating informative error messages.
200 |  * @param detailsMap A Map created from the specific component map object (e.g., new Map(Object.entries(spec.components.schemas))).
201 |  * @param requestedNames An array of component names requested by the user.
202 |  * @param componentTypeForError The component type string, used for creating informative error messages.
203 |  * @returns An array of { name: string, detail: V } for valid requested names, where V is the value type of the Map.
204 |  * @throws {McpError} If none of the requested names are valid.
205 |  */
206 | // Modify to accept a Map directly
207 | export function getValidatedComponentDetails<V extends OpenAPIV3.ReferenceObject | object>(
208 |   detailsMap: Map<string, V>, // Accept Map<string, V>
209 |   requestedNames: string[],
210 |   componentTypeForError: string
211 | ): { name: string; detail: V }[] {
212 |   // No longer need to create the map inside the function
213 |   const validDetails = requestedNames
214 |     .map(name => {
215 |       const detail = detailsMap.get(name); // detail will be V | undefined
216 |       return detail ? { name, detail } : null;
217 |     })
218 |     // Type predicate ensures we filter out nulls and have the correct type
219 |     .filter((item): item is { name: string; detail: V } => item !== null);
220 | 
221 |   if (validDetails.length === 0) {
222 |     // Sort available names for deterministic error messages
223 |     const availableNames = Array.from(detailsMap.keys()).sort().join(', ');
224 |     const errorMessage = `None of the requested names (${requestedNames.join(', ')}) are valid for component type "${componentTypeForError}". Available names: ${availableNames}`;
225 |     // Use standard Error
226 |     throw new Error(errorMessage);
227 |   }
228 | 
229 |   return validDetails;
230 | }
231 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RequestId } from '@modelcontextprotocol/sdk/types.js';
  3 | import { ComponentDetailHandler } from '../../../../src/handlers/component-detail-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();
 18 | 
 19 | // Sample Data
 20 | const userSchema: OpenAPIV3.SchemaObject = {
 21 |   type: 'object',
 22 |   properties: { name: { type: 'string' } },
 23 | };
 24 | const errorSchema: OpenAPIV3.SchemaObject = {
 25 |   type: 'object',
 26 |   properties: { message: { type: 'string' } },
 27 | };
 28 | const limitParam: OpenAPIV3.ParameterObject = {
 29 |   name: 'limit',
 30 |   in: 'query',
 31 |   schema: { type: 'integer' },
 32 | };
 33 | 
 34 | const sampleSpec: OpenAPIV3.Document = {
 35 |   openapi: '3.0.3',
 36 |   info: { title: 'Test API', version: '1.0.0' },
 37 |   paths: {},
 38 |   components: {
 39 |     schemas: {
 40 |       User: userSchema,
 41 |       Error: errorSchema,
 42 |     },
 43 |     parameters: {
 44 |       limitParam: limitParam,
 45 |     },
 46 |     // No securitySchemes defined
 47 |   },
 48 | };
 49 | 
 50 | describe('ComponentDetailHandler', () => {
 51 |   let handler: ComponentDetailHandler;
 52 | 
 53 |   beforeEach(() => {
 54 |     handler = new ComponentDetailHandler(mockSpecLoader, mockFormatter);
 55 |     mockGetTransformedSpec.mockReset();
 56 |     mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
 57 |   });
 58 | 
 59 |   it('should return the correct template', () => {
 60 |     const template = handler.getTemplate();
 61 |     expect(template).toBeInstanceOf(ResourceTemplate);
 62 |     expect(template.uriTemplate.toString()).toBe('openapi://components/{type}/{name*}');
 63 |   });
 64 | 
 65 |   describe('handleRequest', () => {
 66 |     const mockExtra = {
 67 |       signal: new AbortController().signal,
 68 |       sendNotification: jest.fn(),
 69 |       sendRequest: jest.fn(),
 70 |       requestId: 'test-request-id' as RequestId,
 71 |     };
 72 | 
 73 |     it('should return detail for a single valid component (schema)', async () => {
 74 |       const variables: Variables = { type: 'schemas', name: 'User' }; // Use 'name' key
 75 |       const uri = new URL('openapi://components/schemas/User');
 76 | 
 77 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 78 | 
 79 |       expect(mockGetTransformedSpec).toHaveBeenCalledWith({
 80 |         resourceType: 'schema',
 81 |         format: 'openapi',
 82 |       });
 83 |       expect(result.contents).toHaveLength(1);
 84 |       expect(result.contents[0]).toEqual({
 85 |         uri: 'openapi://components/schemas/User',
 86 |         mimeType: 'application/json',
 87 |         text: JSON.stringify(userSchema, null, 2),
 88 |         isError: false,
 89 |       });
 90 |     });
 91 | 
 92 |     it('should return detail for a single valid component (parameter)', async () => {
 93 |       const variables: Variables = { type: 'parameters', name: 'limitParam' };
 94 |       const uri = new URL('openapi://components/parameters/limitParam');
 95 | 
 96 |       const result = await handler.handleRequest(uri, variables, mockExtra);
 97 | 
 98 |       expect(result.contents).toHaveLength(1);
 99 |       expect(result.contents[0]).toEqual({
100 |         uri: 'openapi://components/parameters/limitParam',
101 |         mimeType: 'application/json',
102 |         text: JSON.stringify(limitParam, null, 2),
103 |         isError: false,
104 |       });
105 |     });
106 | 
107 |     it('should return details for multiple valid components (array input)', async () => {
108 |       const variables: Variables = { type: 'schemas', name: ['User', 'Error'] }; // Use 'name' key with array
109 |       const uri = new URL('openapi://components/schemas/User,Error'); // URI might not reflect array input
110 | 
111 |       const result = await handler.handleRequest(uri, variables, mockExtra);
112 | 
113 |       expect(result.contents).toHaveLength(2);
114 |       expect(result.contents).toContainEqual({
115 |         uri: 'openapi://components/schemas/User',
116 |         mimeType: 'application/json',
117 |         text: JSON.stringify(userSchema, null, 2),
118 |         isError: false,
119 |       });
120 |       expect(result.contents).toContainEqual({
121 |         uri: 'openapi://components/schemas/Error',
122 |         mimeType: 'application/json',
123 |         text: JSON.stringify(errorSchema, null, 2),
124 |         isError: false,
125 |       });
126 |     });
127 | 
128 |     it('should return error for invalid component type', async () => {
129 |       const variables: Variables = { type: 'invalidType', name: 'SomeName' };
130 |       const uri = new URL('openapi://components/invalidType/SomeName');
131 |       const expectedLogMessage = /Invalid component type: invalidType/;
132 | 
133 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
134 |         handler.handleRequest(uri, variables, mockExtra)
135 |       );
136 | 
137 |       expect(result.contents).toHaveLength(1);
138 |       expect(result.contents[0]).toEqual({
139 |         uri: 'openapi://components/invalidType/SomeName',
140 |         mimeType: 'text/plain',
141 |         text: 'Invalid component type: invalidType',
142 |         isError: true,
143 |       });
144 |       expect(mockGetTransformedSpec).not.toHaveBeenCalled();
145 |     });
146 | 
147 |     it('should return error for non-existent component type in spec', async () => {
148 |       const variables: Variables = { type: 'securitySchemes', name: 'apiKey' };
149 |       const uri = new URL('openapi://components/securitySchemes/apiKey');
150 |       const expectedLogMessage = /Component type "securitySchemes" not found/;
151 | 
152 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
153 |         handler.handleRequest(uri, variables, mockExtra)
154 |       );
155 | 
156 |       expect(result.contents).toHaveLength(1);
157 |       // Expect the specific error message from getValidatedComponentMap
158 |       expect(result.contents[0]).toEqual({
159 |         uri: 'openapi://components/securitySchemes/apiKey',
160 |         mimeType: 'text/plain',
161 |         text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters',
162 |         isError: true,
163 |       });
164 |     });
165 | 
166 |     it('should return error for non-existent component name', async () => {
167 |       const variables: Variables = { type: 'schemas', name: 'NonExistent' };
168 |       const uri = new URL('openapi://components/schemas/NonExistent');
169 |       const expectedLogMessage = /None of the requested names \(NonExistent\) are valid/;
170 | 
171 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
172 |         handler.handleRequest(uri, variables, mockExtra)
173 |       );
174 | 
175 |       expect(result.contents).toHaveLength(1);
176 |       // Expect the specific error message from getValidatedComponentDetails
177 |       expect(result.contents[0]).toEqual({
178 |         uri: 'openapi://components/schemas/NonExistent',
179 |         mimeType: 'text/plain',
180 |         // Expect sorted names: Error, User
181 |         text: 'None of the requested names (NonExistent) are valid for component type "schemas". Available names: Error, User',
182 |         isError: true,
183 |       });
184 |     });
185 | 
186 |     // Remove test for mix of valid/invalid names, as getValidatedComponentDetails throws now
187 |     // it('should handle mix of valid and invalid component names', async () => { ... });
188 | 
189 |     it('should handle empty name array', async () => {
190 |       const variables: Variables = { type: 'schemas', name: [] };
191 |       const uri = new URL('openapi://components/schemas/');
192 |       const expectedLogMessage = /No valid component name specified/;
193 | 
194 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
195 |         handler.handleRequest(uri, variables, mockExtra)
196 |       );
197 | 
198 |       expect(result.contents).toHaveLength(1);
199 |       expect(result.contents[0]).toEqual({
200 |         uri: 'openapi://components/schemas/',
201 |         mimeType: 'text/plain',
202 |         text: 'No valid component name specified.',
203 |         isError: true,
204 |       });
205 |     });
206 | 
207 |     it('should handle spec loading errors', async () => {
208 |       const error = new Error('Spec load failed');
209 |       mockGetTransformedSpec.mockRejectedValue(error);
210 |       const variables: Variables = { type: 'schemas', name: 'User' };
211 |       const uri = new URL('openapi://components/schemas/User');
212 |       const expectedLogMessage = /Spec load failed/;
213 | 
214 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
215 |         handler.handleRequest(uri, variables, mockExtra)
216 |       );
217 | 
218 |       expect(result.contents).toHaveLength(1);
219 |       expect(result.contents[0]).toEqual({
220 |         uri: 'openapi://components/schemas/User',
221 |         mimeType: 'text/plain',
222 |         text: 'Spec load failed',
223 |         isError: true,
224 |       });
225 |     });
226 | 
227 |     it('should handle non-OpenAPI v3 spec', async () => {
228 |       const invalidSpec = { swagger: '2.0', info: {} };
229 |       mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
230 |       const variables: Variables = { type: 'schemas', name: 'User' };
231 |       const uri = new URL('openapi://components/schemas/User');
232 |       const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
233 | 
234 |       const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
235 |         handler.handleRequest(uri, variables, mockExtra)
236 |       );
237 | 
238 |       expect(result.contents).toHaveLength(1);
239 |       expect(result.contents[0]).toEqual({
240 |         uri: 'openapi://components/schemas/User',
241 |         mimeType: 'text/plain',
242 |         text: 'Only OpenAPI v3 specifications are supported',
243 |         isError: true,
244 |       });
245 |     });
246 |   });
247 | });
248 | 
```

--------------------------------------------------------------------------------
/memory-bank/progress.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Project Progress
  2 | 
  3 | ## Completed Features
  4 | 
  5 | ### Core Refactoring & New Resource Structure (✓)
  6 | 
  7 | 1.  **Unified URI Structure:** Implemented a consistent URI structure based on OpenAPI spec hierarchy:
  8 |     - `openapi://{field}`: Access top-level fields (info, servers, tags) or list paths/component types.
  9 |     - `openapi://paths/{path}`: List methods for a specific path.
 10 |     - `openapi://paths/{path}/{method*}`: Get details for one or more operations.
 11 |     - `openapi://components/{type}`: List names for a specific component type.
 12 |     - `openapi://components/{type}/{name*}`: Get details for one or more components.
 13 | 2.  **OOP Rendering Layer:** Introduced `Renderable*` classes (`RenderableDocument`, `RenderablePaths`, `RenderablePathItem`, `RenderableComponents`, `RenderableComponentMap`) to encapsulate rendering logic.
 14 |     - Uses `RenderContext` and intermediate `RenderResultItem` structure.
 15 |     - Supports token-efficient text lists and formatted detail views (JSON/YAML).
 16 | 3.  **Refactored Handlers:** Created new, focused handlers for each URI pattern:
 17 |     - `TopLevelFieldHandler`
 18 |     - `PathItemHandler`
 19 |     - `OperationHandler`
 20 |     - `ComponentMapHandler`
 21 |     - `ComponentDetailHandler`
 22 |     - Uses shared utilities (`handler-utils.ts`).
 23 | 4.  **Multi-Value Support:** Correctly handles `*` variables (`method*`, `name*`) passed as arrays by the SDK.
 24 | 5.  **Testing:**
 25 |     - Added unit tests for all new `Renderable*` classes.
 26 |     - Added unit tests for all new handler classes.
 27 |     - Added E2E tests covering the new URI structure and functionality using `complex-endpoint.json`.
 28 | 6.  **Archived Old Code:** Moved previous handler/test implementations to `local-docs/old-implementation/`.
 29 | 
 30 | ### Previous Features (Now Integrated/Superseded)
 31 | 
 32 | - Schema Listing (Superseded by `openapi://components/schemas`)
 33 | - Schema Details (Superseded by `openapi://components/schemas/{name*}`)
 34 | - Endpoint Details (Superseded by `openapi://paths/{path}/{method*}`)
 35 | - Endpoint List (Superseded by `openapi://paths`)
 36 | 
 37 | ### Remote Spec & Swagger v2.0 Support (✓)
 38 | 
 39 | 1.  **Remote Loading:** Added support for loading specifications via HTTP/HTTPS URLs using `swagger2openapi`.
 40 | 2.  **Swagger v2.0 Conversion:** Added support for automatically converting Swagger v2.0 specifications to OpenAPI v3.0 using `swagger2openapi`.
 41 | 3.  **Dependency Change:** Replaced `@apidevtools/swagger-parser` with `swagger2openapi` for loading and conversion.
 42 | 4.  **Configuration Update:** Confirmed and documented that configuration uses CLI arguments (`<path-or-url-to-spec>`) instead of environment variables.
 43 | 5.  **Testing:**
 44 |     - Updated `SpecLoaderService` unit tests to mock `swagger2openapi` and cover new scenarios (local/remote, v2/v3).
 45 |     - Created new E2E test file (`spec-loading.test.ts`) to verify loading from local v2 and remote v3 sources.
 46 |     - Added v2.0 test fixture (`sample-v2-api.json`).
 47 | 
 48 | ### Docker Support (✓)
 49 | 
 50 | 1.  **Dockerfile:** Added a multi-stage production `Dockerfile` at the root. Moved the devcontainer Dockerfile to `.devcontainer/`.
 51 | 2.  **Release Integration:** Added `@codedependant/semantic-release-docker` plugin to `.releaserc.json` to automate Docker image building and publishing to Docker Hub (`kadykov/mcp-openapi-schema-explorer`).
 52 | 3.  **CI Workflow:** Updated the `release` job in `.github/workflows/ci.yml` to set up Docker environment (QEMU, Buildx, Login) and use `cycjimmy/semantic-release-action@v4` with the Docker plugin included in `extra_plugins`. Removed redundant Node setup/install steps from the release job.
 53 | 4.  **Documentation:** Updated `README.md` with instructions and examples for running the server via Docker.
 54 | 
 55 | ## Technical Features (✓)
 56 | 
 57 | ### Codebase Organization (Updated)
 58 | 
 59 | 1. File Structure
 60 | 
 61 |    - `src/handlers/`: Contains individual handlers and `handler-utils.ts`.
 62 |    - `src/rendering/`: Contains `Renderable*` classes, `types.ts`, `utils.ts`.
 63 |    - `src/services/`: Updated `spec-loader.ts` to use `swagger2openapi`. Added `formatters.ts`.
 64 |    - `src/`: `index.ts`, `config.ts`, `types.ts`.
 65 |    - `test/`: Updated unit tests (`spec-loader.test.ts`, `formatters.test.ts`), added new E2E test (`spec-loading.test.ts`), added v2 fixture. Updated `format.test.ts`. Updated `mcp-test-helpers.ts`.
 66 |    - `local-docs/old-implementation/`: Archived previous code.
 67 | 
 68 | 2. Testing Structure
 69 | 
 70 |    - Unit tests for rendering classes (`test/__tests__/unit/rendering/`).
 71 |    - Unit tests for handlers (`test/__tests__/unit/handlers/`).
 72 |    - Unit tests for services (`spec-loader.test.ts`, `reference-transform.test.ts`, `formatters.test.ts`).
 73 |    - E2E tests (`refactored-resources.test.ts`, `spec-loading.test.ts`, `format.test.ts`). Added tests for `json-minified`.
 74 |    - Fixtures (`test/fixtures/`, including v2 and v3).
 75 |    - Test utils (`test/utils/`). Updated `StartServerOptions` type.
 76 | 
 77 | 3. Type System
 78 | 
 79 |    - OpenAPI v3 types.
 80 |    - `RenderableSpecObject`, `RenderContext`, `RenderResultItem` interfaces.
 81 |    - `FormattedResultItem` type for handler results.
 82 |    - `OutputFormat` type updated.
 83 |    - `IFormatter` interface.
 84 | 
 85 | 4. Error Handling
 86 |    - Consistent error handling via `createErrorResult` and `formatResults`.
 87 |    - Errors formatted as `text/plain`.
 88 | 
 89 | ### Reference Transformation (✓ - Updated)
 90 | 
 91 | - Centralized URI generation logic in `src/utils/uri-builder.ts`.
 92 | - `ReferenceTransformService` now correctly transforms all `#/components/...` refs to `openapi://components/{type}/{name}` using the URI builder.
 93 | - Path encoding corrected to remove leading slashes before encoding.
 94 | - Unit tests updated and passing for URI builder and transformer.
 95 | 
 96 | ### Output Format Enhancement (✓)
 97 | 
 98 | - Added `json-minified` output format option (`--output-format json-minified`).
 99 | - Implemented `MinifiedJsonFormatter` in `src/services/formatters.ts`.
100 | - Updated configuration (`src/config.ts`) to accept the new format.
101 | - Added unit tests for the new formatter (`test/__tests__/unit/services/formatters.test.ts`).
102 | - Added E2E tests (`test/__tests__/e2e/format.test.ts`) to verify the new format.
103 | - Updated test helper types (`test/utils/mcp-test-helpers.ts`).
104 | 
105 | ### Dynamic Server Name (✓)
106 | 
107 | - Modified `src/index.ts` to load the OpenAPI spec before server initialization.
108 | - Extracted `info.title` from the loaded spec.
109 | - Set the `McpServer` name dynamically using the format `Schema Explorer for {title}` with a fallback.
110 | 
111 | ### Dependency Cleanup & Release Automation (✓)
112 | 
113 | 1.  **Dependency Correction:** Correctly categorized runtime (`swagger2openapi`) vs. development (`@types/*`) dependencies in `package.json`. Removed unused types.
114 | 2.  **Automated Releases:** Implemented `semantic-release` with conventional commit analysis, changelog generation, npm publishing, and GitHub releases.
115 | 3.  **Dynamic Versioning:** Server version is now dynamically injected via `src/version.ts`, which is generated during the release process by `semantic-release` using `scripts/generate-version.js`. A default version file is tracked in Git for local builds.
116 | 4.  **CI Workflow:** Updated `.github/workflows/ci.yml` to use Node 22, use `just` for running checks (`just all`, `just security`), and includes a `release` job using `cycjimmy/semantic-release-action@v4` to automate npm and Docker Hub publishing on pushes to `main`.
117 | 
118 | ## Planned Features (⏳)
119 | 
120 | - **Handler Unit Tests:** Complete unit tests for all new handlers (mocking services).
121 | - **Refactor Helpers:** Move duplicated helpers (`formatResults`, `isOpenAPIV3`) from handlers to `handler-utils.ts`. (Deferred during refactor).
122 | - **Security Validation (✓):** Implemented Map-based validation helpers in `handler-utils.ts` and refactored handlers/rendering classes to resolve object injection warnings.
123 | - **Completion Logic (✓):** Implemented `complete` callbacks in `ResourceTemplate` definitions within `src/index.ts` for `{field}`, `{path}`, `{method*}`, `{type}`, and conditionally for `{name*}`. Added E2E tests using `client.complete()`.
124 | - **Reference Traversal:** Service to resolve `$ref` URIs (e.g., follow `openapi://components/schemas/Task` from an endpoint detail).
125 | - **Enhanced Component Support:** Ensure all component types listed in `VALID_COMPONENT_TYPES` are fully handled if present in spec. (Reference transformation now supports all types).
126 | - **Parameter Validation:** Add validation logic if needed. (Current Map-based approach handles key validation).
127 | - **Further Token Optimizations:** Explore more ways to reduce token usage in list/detail views.
128 | - **README Enhancements:** Add details on release process, secrets/vars setup. (Partially done).
129 | 
130 | ## Technical Improvements (Ongoing)
131 | 
132 | 1. Code Quality
133 | 
134 |    - OOP design for rendering.
135 |    - Clear separation of concerns (Rendering vs. Handling vs. Services).
136 |    - Improved type safety in rendering/handling logic.
137 | 
138 | 2. Testing
139 | 
140 |    - Unit tests added for rendering logic.
141 |    - Unit tests updated for URI builder, reference transformer, and path item rendering.
142 |    - E2E tests updated for new structure and complex fixture. Added tests for resource completion.
143 |    - Unit tests for `SpecLoaderService` updated for `swagger2openapi`.
144 |    - CI workflow updated to use `just` and includes automated release job for npm and Docker.
145 |    - Handler unit tests updated to accommodate `@modelcontextprotocol/sdk` v1.11.0 changes (`RequestHandlerExtra` now requires `requestId`, and `RequestId` import path corrected to `@modelcontextprotocol/sdk/types.js`).
146 | 
147 | 3. API Design
148 |    - New URI structure implemented, aligned with OpenAPI spec.
149 |    - Consistent list/detail pattern via rendering layer.
150 |    - Server now accepts remote URLs and Swagger v2.0 specs via CLI argument.
151 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; // Import ResourceTemplate
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import { OpenAPI } from 'openapi-types'; // Import OpenAPIV3 as well
  5 | import { loadConfig } from './config.js';
  6 | 
  7 | // Import new handlers
  8 | import { TopLevelFieldHandler } from './handlers/top-level-field-handler.js';
  9 | import { PathItemHandler } from './handlers/path-item-handler.js';
 10 | import { OperationHandler } from './handlers/operation-handler.js';
 11 | import { ComponentMapHandler } from './handlers/component-map-handler.js';
 12 | import { ComponentDetailHandler } from './handlers/component-detail-handler.js';
 13 | import { OpenAPITransformer, ReferenceTransformService } from './services/reference-transform.js';
 14 | import { SpecLoaderService } from './services/spec-loader.js';
 15 | import { createFormatter } from './services/formatters.js';
 16 | import { encodeUriPathComponent } from './utils/uri-builder.js'; // Import specific function
 17 | import { isOpenAPIV3, getValidatedComponentMap } from './handlers/handler-utils.js'; // Import type guard and helper
 18 | import { VERSION } from './version.js'; // Import the generated version
 19 | 
 20 | async function main(): Promise<void> {
 21 |   try {
 22 |     // Get spec path and options from command line arguments
 23 |     const [, , specPath, ...args] = process.argv;
 24 |     const options = {
 25 |       outputFormat: args.includes('--output-format')
 26 |         ? args[args.indexOf('--output-format') + 1]
 27 |         : undefined,
 28 |     };
 29 | 
 30 |     // Load configuration
 31 |     const config = loadConfig(specPath, options);
 32 | 
 33 |     // Initialize services
 34 |     const referenceTransform = new ReferenceTransformService();
 35 |     referenceTransform.registerTransformer('openapi', new OpenAPITransformer());
 36 | 
 37 |     const specLoader = new SpecLoaderService(config.specPath, referenceTransform);
 38 |     await specLoader.loadSpec();
 39 | 
 40 |     // Get the loaded spec to extract the title
 41 |     const spec: OpenAPI.Document = await specLoader.getSpec(); // Rename back to spec
 42 |     // Get the transformed spec for use in completions
 43 |     const transformedSpec: OpenAPI.Document = await specLoader.getTransformedSpec({
 44 |       resourceType: 'schema', // Use a default context
 45 |       format: 'openapi',
 46 |     });
 47 |     const defaultServerName = 'OpenAPI Schema Explorer';
 48 |     // Use original spec for title
 49 |     const serverName = spec.info?.title
 50 |       ? `Schema Explorer for ${spec.info.title}`
 51 |       : defaultServerName;
 52 | 
 53 |     // Brief help content for LLMs
 54 |     const helpContent = `Use resorces/templates/list to get a list of available resources. Use openapi://paths to get a list of all endpoints.`;
 55 | 
 56 |     // Create MCP server with dynamic name
 57 |     const server = new McpServer(
 58 |       {
 59 |         name: serverName,
 60 |         version: VERSION, // Use the imported version
 61 |       },
 62 |       {
 63 |         instructions: helpContent,
 64 |       }
 65 |     );
 66 | 
 67 |     // Set up formatter and new handlers
 68 |     const formatter = createFormatter(config.outputFormat);
 69 |     const topLevelFieldHandler = new TopLevelFieldHandler(specLoader, formatter);
 70 |     const pathItemHandler = new PathItemHandler(specLoader, formatter);
 71 |     const operationHandler = new OperationHandler(specLoader, formatter);
 72 |     const componentMapHandler = new ComponentMapHandler(specLoader, formatter);
 73 |     const componentDetailHandler = new ComponentDetailHandler(specLoader, formatter);
 74 | 
 75 |     // --- Define Resource Templates and Register Handlers ---
 76 | 
 77 |     // Helper to get dynamic field list for descriptions
 78 |     const getFieldList = (): string => Object.keys(transformedSpec).join(', ');
 79 |     // Helper to get dynamic component type list for descriptions
 80 |     const getComponentTypeList = (): string => {
 81 |       if (isOpenAPIV3(transformedSpec) && transformedSpec.components) {
 82 |         return Object.keys(transformedSpec.components).join(', ');
 83 |       }
 84 |       return ''; // Return empty if no components or not V3
 85 |     };
 86 | 
 87 |     // 1. openapi://{field}
 88 |     const fieldTemplate = new ResourceTemplate('openapi://{field}', {
 89 |       list: undefined, // List is handled by the handler logic based on field value
 90 |       complete: {
 91 |         field: (): string[] => Object.keys(transformedSpec), // Use transformedSpec
 92 |       },
 93 |     });
 94 |     server.resource(
 95 |       'openapi-field', // Unique ID for the resource registration
 96 |       fieldTemplate,
 97 |       {
 98 |         // MimeType varies (text/plain for lists, JSON/YAML for details)
 99 |         description: `Access top-level fields like ${getFieldList()}. (e.g., openapi://info)`,
100 |         name: 'OpenAPI Field/List', // Generic name
101 |       },
102 |       topLevelFieldHandler.handleRequest
103 |     );
104 | 
105 |     // 2. openapi://paths/{path}
106 |     const pathTemplate = new ResourceTemplate('openapi://paths/{path}', {
107 |       list: undefined, // List is handled by the handler
108 |       complete: {
109 |         path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
110 |       },
111 |     });
112 |     server.resource(
113 |       'openapi-path-methods',
114 |       pathTemplate,
115 |       {
116 |         mimeType: 'text/plain', // This always returns a list
117 |         description:
118 |           'List methods for a specific path (URL encode paths with slashes). (e.g., openapi://paths/users%2F%7Bid%7D)',
119 |         name: 'Path Methods List',
120 |       },
121 |       pathItemHandler.handleRequest
122 |     );
123 | 
124 |     // 3. openapi://paths/{path}/{method*}
125 |     const operationTemplate = new ResourceTemplate('openapi://paths/{path}/{method*}', {
126 |       list: undefined, // Detail view handled by handler
127 |       complete: {
128 |         path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
129 |         method: (): string[] => [
130 |           // Provide static list of common methods
131 |           'GET',
132 |           'POST',
133 |           'PUT',
134 |           'DELETE',
135 |           'PATCH',
136 |           'OPTIONS',
137 |           'HEAD',
138 |           'TRACE',
139 |         ],
140 |       },
141 |     });
142 |     server.resource(
143 |       'openapi-operation-detail',
144 |       operationTemplate,
145 |       {
146 |         mimeType: formatter.getMimeType(), // Detail view uses formatter
147 |         description:
148 |           'Get details for one or more operations (comma-separated). (e.g., openapi://paths/users%2F%7Bid%7D/get,post)',
149 |         name: 'Operation Detail',
150 |       },
151 |       operationHandler.handleRequest
152 |     );
153 | 
154 |     // 4. openapi://components/{type}
155 |     const componentMapTemplate = new ResourceTemplate('openapi://components/{type}', {
156 |       list: undefined, // List is handled by the handler
157 |       complete: {
158 |         type: (): string[] => {
159 |           // Use type guard to ensure spec is V3 before accessing components
160 |           if (isOpenAPIV3(transformedSpec)) {
161 |             return Object.keys(transformedSpec.components ?? {});
162 |           }
163 |           return []; // Return empty array if not V3 (shouldn't happen ideally)
164 |         },
165 |       },
166 |     });
167 |     server.resource(
168 |       'openapi-component-list',
169 |       componentMapTemplate,
170 |       {
171 |         mimeType: 'text/plain', // This always returns a list
172 |         description: `List components of a specific type like ${getComponentTypeList()}. (e.g., openapi://components/schemas)`,
173 |         name: 'Component List',
174 |       },
175 |       componentMapHandler.handleRequest
176 |     );
177 | 
178 |     // 5. openapi://components/{type}/{name*}
179 |     const componentDetailTemplate = new ResourceTemplate('openapi://components/{type}/{name*}', {
180 |       list: undefined, // Detail view handled by handler
181 |       complete: {
182 |         type: (): string[] => {
183 |           // Use type guard to ensure spec is V3 before accessing components
184 |           if (isOpenAPIV3(transformedSpec)) {
185 |             return Object.keys(transformedSpec.components ?? {});
186 |           }
187 |           return []; // Return empty array if not V3
188 |         },
189 |         name: (): string[] => {
190 |           // Provide names only if there's exactly one component type defined
191 |           if (
192 |             isOpenAPIV3(transformedSpec) &&
193 |             transformedSpec.components &&
194 |             Object.keys(transformedSpec.components).length === 1
195 |           ) {
196 |             // Get the single component type key (e.g., 'schemas')
197 |             const componentTypeKey = Object.keys(transformedSpec.components)[0];
198 |             // Use the helper to safely get the map
199 |             try {
200 |               const componentTypeMap = getValidatedComponentMap(transformedSpec, componentTypeKey);
201 |               return Object.keys(componentTypeMap);
202 |             } catch (error) {
203 |               // Should not happen if key came from Object.keys, but handle defensively
204 |               console.error(`Error getting component map for key ${componentTypeKey}:`, error);
205 |               return [];
206 |             }
207 |           }
208 |           // Otherwise, return no completions for name
209 |           return [];
210 |         },
211 |       },
212 |     });
213 |     server.resource(
214 |       'openapi-component-detail',
215 |       componentDetailTemplate,
216 |       {
217 |         mimeType: formatter.getMimeType(), // Detail view uses formatter
218 |         description:
219 |           'Get details for one or more components (comma-separated). (e.g., openapi://components/schemas/User,Task)',
220 |         name: 'Component Detail',
221 |       },
222 |       componentDetailHandler.handleRequest
223 |     );
224 | 
225 |     // Start server
226 |     const transport = new StdioServerTransport();
227 |     await server.connect(transport);
228 |   } catch (error) {
229 |     console.error(
230 |       'Failed to start server:',
231 |       error instanceof Error ? error.message : String(error)
232 |     );
233 |     process.exit(1);
234 |   }
235 | }
236 | 
237 | // Run the server
238 | main().catch(error => {
239 |   console.error('Unhandled error:', error instanceof Error ? error.message : String(error));
240 |   process.exit(1);
241 | });
242 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import {
  3 |   getValidatedPathItem,
  4 |   getValidatedOperations,
  5 |   getValidatedComponentMap,
  6 |   getValidatedComponentDetails,
  7 |   // We might also test formatResults and isOpenAPIV3 if needed, but focus on new helpers first
  8 | } from '../../../../src/handlers/handler-utils.js'; // Adjust path as needed
  9 | 
 10 | // --- Mocks and Fixtures ---
 11 | 
 12 | const mockSpec: OpenAPIV3.Document = {
 13 |   openapi: '3.0.0',
 14 |   info: { title: 'Test API', version: '1.0.0' },
 15 |   paths: {
 16 |     '/users': {
 17 |       get: { responses: { '200': { description: 'OK' } } },
 18 |       post: { responses: { '201': { description: 'Created' } } },
 19 |     },
 20 |     '/users/{id}': {
 21 |       get: { responses: { '200': { description: 'OK' } } },
 22 |       delete: { responses: { '204': { description: 'No Content' } } },
 23 |       parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
 24 |     },
 25 |     '/items': {
 26 |       // Path item with no standard methods
 27 |       description: 'Collection of items',
 28 |       parameters: [],
 29 |     },
 30 |   },
 31 |   components: {
 32 |     schemas: {
 33 |       User: { type: 'object', properties: { id: { type: 'string' } } },
 34 |       Error: { type: 'object', properties: { message: { type: 'string' } } },
 35 |     },
 36 |     responses: {
 37 |       NotFound: { description: 'Resource not found' },
 38 |     },
 39 |     // Intentionally missing 'parameters' section for testing
 40 |   },
 41 | };
 42 | 
 43 | const mockSpecNoPaths: OpenAPIV3.Document = {
 44 |   openapi: '3.0.0',
 45 |   info: { title: 'Test API', version: '1.0.0' },
 46 |   paths: {}, // Empty paths
 47 |   components: {},
 48 | };
 49 | 
 50 | const mockSpecNoComponents: OpenAPIV3.Document = {
 51 |   openapi: '3.0.0',
 52 |   info: { title: 'Test API', version: '1.0.0' },
 53 |   paths: { '/ping': { get: { responses: { '200': { description: 'OK' } } } } },
 54 |   // No components section
 55 | };
 56 | 
 57 | // --- Tests ---
 58 | 
 59 | describe('Handler Utils', () => {
 60 |   // --- getValidatedPathItem ---
 61 |   describe('getValidatedPathItem', () => {
 62 |     it('should return the path item object for a valid path', () => {
 63 |       const pathItem = getValidatedPathItem(mockSpec, '/users');
 64 |       expect(pathItem).toBeDefined();
 65 |       expect(pathItem).toHaveProperty('get');
 66 |       expect(pathItem).toHaveProperty('post');
 67 |     });
 68 | 
 69 |     it('should return the path item object for a path with parameters', () => {
 70 |       const pathItem = getValidatedPathItem(mockSpec, '/users/{id}');
 71 |       expect(pathItem).toBeDefined();
 72 |       expect(pathItem).toHaveProperty('get');
 73 |       expect(pathItem).toHaveProperty('delete');
 74 |       expect(pathItem).toHaveProperty('parameters');
 75 |     });
 76 | 
 77 |     it('should throw Error if path is not found', () => {
 78 |       expect(() => getValidatedPathItem(mockSpec, '/nonexistent')).toThrow(
 79 |         new Error('Path "/nonexistent" not found in the specification.')
 80 |       );
 81 |     });
 82 | 
 83 |     it('should throw Error if spec has no paths object', () => {
 84 |       const specWithoutPaths = { ...mockSpec, paths: undefined };
 85 |       // @ts-expect-error - Intentionally passing spec with undefined paths to test error handling
 86 |       expect(() => getValidatedPathItem(specWithoutPaths, '/users')).toThrow(
 87 |         new Error('Specification does not contain any paths.')
 88 |       );
 89 |     });
 90 | 
 91 |     it('should throw Error if spec has empty paths object', () => {
 92 |       expect(() => getValidatedPathItem(mockSpecNoPaths, '/users')).toThrow(
 93 |         new Error('Path "/users" not found in the specification.')
 94 |       );
 95 |     });
 96 |   });
 97 | 
 98 |   // --- getValidatedOperations ---
 99 |   describe('getValidatedOperations', () => {
100 |     const usersPathItem = mockSpec.paths['/users'] as OpenAPIV3.PathItemObject;
101 |     const userIdPathItem = mockSpec.paths['/users/{id}'] as OpenAPIV3.PathItemObject;
102 |     const itemsPathItem = mockSpec.paths['/items'] as OpenAPIV3.PathItemObject;
103 | 
104 |     it('should return valid requested methods when all exist', () => {
105 |       const validMethods = getValidatedOperations(usersPathItem, ['get', 'post'], '/users');
106 |       expect(validMethods).toEqual(['get', 'post']);
107 |     });
108 | 
109 |     it('should return valid requested methods when some exist', () => {
110 |       const validMethods = getValidatedOperations(usersPathItem, ['get', 'put', 'post'], '/users');
111 |       expect(validMethods).toEqual(['get', 'post']);
112 |     });
113 | 
114 |     it('should return valid requested methods ignoring case', () => {
115 |       const validMethods = getValidatedOperations(usersPathItem, ['GET', 'POST'], '/users');
116 |       // Note: the helper expects lowercase input, but the internal map uses lowercase keys
117 |       expect(validMethods).toEqual(['GET', 'POST']); // It returns the original case of valid inputs
118 |     });
119 | 
120 |     it('should return only the valid method when one exists', () => {
121 |       const validMethods = getValidatedOperations(
122 |         userIdPathItem,
123 |         ['delete', 'patch'],
124 |         '/users/{id}'
125 |       );
126 |       expect(validMethods).toEqual(['delete']);
127 |     });
128 | 
129 |     it('should throw Error if no requested methods are valid', () => {
130 |       expect(() => getValidatedOperations(usersPathItem, ['put', 'delete'], '/users')).toThrow(
131 |         new Error(
132 |           'None of the requested methods (put, delete) are valid for path "/users". Available methods: get, post'
133 |         )
134 |       );
135 |     });
136 | 
137 |     it('should throw Error if requested methods array is empty', () => {
138 |       // The calling handler should prevent this, but test the helper
139 |       expect(() => getValidatedOperations(usersPathItem, [], '/users')).toThrow(
140 |         new Error(
141 |           'None of the requested methods () are valid for path "/users". Available methods: get, post'
142 |         )
143 |       );
144 |     });
145 | 
146 |     it('should throw Error if path item has no valid methods', () => {
147 |       expect(() => getValidatedOperations(itemsPathItem, ['get'], '/items')).toThrow(
148 |         new Error(
149 |           'None of the requested methods (get) are valid for path "/items". Available methods: '
150 |         )
151 |       );
152 |     });
153 |   });
154 | 
155 |   // --- getValidatedComponentMap ---
156 |   describe('getValidatedComponentMap', () => {
157 |     it('should return the component map for a valid type', () => {
158 |       const schemasMap = getValidatedComponentMap(mockSpec, 'schemas');
159 |       expect(schemasMap).toBeDefined();
160 |       expect(schemasMap).toHaveProperty('User');
161 |       expect(schemasMap).toHaveProperty('Error');
162 |     });
163 | 
164 |     it('should return the component map for another valid type', () => {
165 |       const responsesMap = getValidatedComponentMap(mockSpec, 'responses');
166 |       expect(responsesMap).toBeDefined();
167 |       expect(responsesMap).toHaveProperty('NotFound');
168 |     });
169 | 
170 |     it('should throw Error if component type is not found', () => {
171 |       expect(() => getValidatedComponentMap(mockSpec, 'parameters')).toThrow(
172 |         new Error(
173 |           'Component type "parameters" not found in the specification. Available types: schemas, responses'
174 |         )
175 |       );
176 |     });
177 | 
178 |     it('should throw Error if spec has no components section', () => {
179 |       expect(() => getValidatedComponentMap(mockSpecNoComponents, 'schemas')).toThrow(
180 |         new Error('Specification does not contain a components section.')
181 |       );
182 |     });
183 |   });
184 | 
185 |   // --- getValidatedComponentDetails ---
186 |   describe('getValidatedComponentDetails', () => {
187 |     const schemasMap = mockSpec.components?.schemas as Record<string, OpenAPIV3.SchemaObject>;
188 |     const responsesMap = mockSpec.components?.responses as Record<string, OpenAPIV3.ResponseObject>;
189 |     const detailsMapSchemas = new Map(Object.entries(schemasMap));
190 |     const detailsMapResponses = new Map(Object.entries(responsesMap));
191 | 
192 |     it('should return details for valid requested names', () => {
193 |       const validDetails = getValidatedComponentDetails(
194 |         detailsMapSchemas,
195 |         ['User', 'Error'],
196 |         'schemas'
197 |       );
198 |       expect(validDetails).toHaveLength(2);
199 |       expect(validDetails[0].name).toBe('User');
200 |       expect(validDetails[0].detail).toEqual(schemasMap['User']);
201 |       expect(validDetails[1].name).toBe('Error');
202 |       expect(validDetails[1].detail).toEqual(schemasMap['Error']);
203 |     });
204 | 
205 |     it('should return details for a single valid requested name', () => {
206 |       const validDetails = getValidatedComponentDetails(detailsMapSchemas, ['User'], 'schemas');
207 |       expect(validDetails).toHaveLength(1);
208 |       expect(validDetails[0].name).toBe('User');
209 |       expect(validDetails[0].detail).toEqual(schemasMap['User']);
210 |     });
211 | 
212 |     it('should return only details for valid names when some are invalid', () => {
213 |       const validDetails = getValidatedComponentDetails(
214 |         detailsMapSchemas,
215 |         ['User', 'NonExistent', 'Error'],
216 |         'schemas'
217 |       );
218 |       expect(validDetails).toHaveLength(2);
219 |       expect(validDetails[0].name).toBe('User');
220 |       expect(validDetails[1].name).toBe('Error');
221 |     });
222 | 
223 |     it('should throw Error if no requested names are valid', () => {
224 |       expect(() =>
225 |         getValidatedComponentDetails(detailsMapSchemas, ['NonExistent1', 'NonExistent2'], 'schemas')
226 |       ).toThrow(
227 |         new Error(
228 |           // Expect sorted names: Error, User
229 |           'None of the requested names (NonExistent1, NonExistent2) are valid for component type "schemas". Available names: Error, User'
230 |         )
231 |       );
232 |     });
233 | 
234 |     it('should throw Error if requested names array is empty', () => {
235 |       // The calling handler should prevent this, but test the helper
236 |       expect(() => getValidatedComponentDetails(detailsMapSchemas, [], 'schemas')).toThrow(
237 |         new Error(
238 |           // Expect sorted names: Error, User
239 |           'None of the requested names () are valid for component type "schemas". Available names: Error, User'
240 |         )
241 |       );
242 |     });
243 | 
244 |     it('should work for other component types (responses)', () => {
245 |       const validDetails = getValidatedComponentDetails(
246 |         detailsMapResponses,
247 |         ['NotFound'],
248 |         'responses'
249 |       );
250 |       expect(validDetails).toHaveLength(1);
251 |       expect(validDetails[0].name).toBe('NotFound');
252 |       expect(validDetails[0].detail).toEqual(responsesMap['NotFound']);
253 |     });
254 |   });
255 | });
256 | 
```

--------------------------------------------------------------------------------
/test/__tests__/e2e/format.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  2 | import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers.js'; // Import McpTestContext
  3 | import { load as yamlLoad } from 'js-yaml';
  4 | // Remove old test types/guards if not needed, or adapt them
  5 | // import { isEndpointErrorResponse } from '../../utils/test-types.js';
  6 | // import type { EndpointResponse, ResourceResponse } from '../../utils/test-types.js';
  7 | // Import specific SDK types needed
  8 | import { ReadResourceResult, TextResourceContents } from '@modelcontextprotocol/sdk/types.js';
  9 | // Generic type guard for simple object check
 10 | function isObject(obj: unknown): obj is Record<string, unknown> {
 11 |   return typeof obj === 'object' && obj !== null;
 12 | }
 13 | 
 14 | // Type guard to check if content is TextResourceContents
 15 | function hasTextContent(
 16 |   content: ReadResourceResult['contents'][0]
 17 | ): content is TextResourceContents {
 18 |   // Check for the 'text' property specifically and ensure it's not undefined
 19 |   return content && typeof (content as TextResourceContents).text === 'string';
 20 | }
 21 | 
 22 | function parseJson(text: string | undefined): unknown {
 23 |   if (text === undefined) throw new Error('Cannot parse undefined text');
 24 |   return JSON.parse(text);
 25 | }
 26 | 
 27 | function parseYaml(text: string | undefined): unknown {
 28 |   if (text === undefined) throw new Error('Cannot parse undefined text');
 29 |   const result = yamlLoad(text);
 30 |   if (result === undefined) {
 31 |     throw new Error('Invalid YAML: parsing resulted in undefined');
 32 |   }
 33 |   return result;
 34 | }
 35 | 
 36 | function safeParse(text: string | undefined, format: 'json' | 'yaml'): unknown {
 37 |   try {
 38 |     return format === 'json' ? parseJson(text) : parseYaml(text);
 39 |   } catch (error) {
 40 |     throw new Error(
 41 |       `Failed to parse ${format} content: ${error instanceof Error ? error.message : String(error)}`
 42 |     );
 43 |   }
 44 | }
 45 | 
 46 | // Removed old parseEndpointResponse
 47 | 
 48 | describe('Output Format E2E', () => {
 49 |   let testContext: McpTestContext;
 50 |   let client: Client;
 51 | 
 52 |   afterEach(async () => {
 53 |     await testContext?.cleanup();
 54 |   });
 55 | 
 56 |   describe('JSON format (default)', () => {
 57 |     beforeEach(async () => {
 58 |       testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
 59 |         outputFormat: 'json',
 60 |       });
 61 |       client = testContext.client;
 62 |     });
 63 | 
 64 |     it('should return JSON for openapi://info', async () => {
 65 |       const result = await client.readResource({ uri: 'openapi://info' });
 66 |       expect(result.contents).toHaveLength(1);
 67 |       const content = result.contents[0];
 68 |       expect(content.mimeType).toBe('application/json');
 69 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
 70 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
 71 |       const data = safeParse(content.text, 'json');
 72 |       expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API'); // Use bracket notation after guard
 73 |     });
 74 | 
 75 |     it('should return JSON for operation detail', async () => {
 76 |       const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
 77 |       const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
 78 |       expect(result.contents).toHaveLength(1);
 79 |       const content = result.contents[0];
 80 |       expect(content.mimeType).toBe('application/json');
 81 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
 82 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
 83 |       const data = safeParse(content.text, 'json');
 84 |       expect(isObject(data) && data['operationId']).toBe('getProjectTasks'); // Use bracket notation after guard
 85 |     });
 86 | 
 87 |     it('should return JSON for component detail', async () => {
 88 |       const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
 89 |       expect(result.contents).toHaveLength(1);
 90 |       const content = result.contents[0];
 91 |       expect(content.mimeType).toBe('application/json');
 92 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
 93 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
 94 |       const data = safeParse(content.text, 'json');
 95 |       expect(isObject(data) && data['type']).toBe('object'); // Use bracket notation after guard
 96 |       expect(
 97 |         isObject(data) &&
 98 |           isObject(data['properties']) &&
 99 |           isObject(data['properties']['id']) &&
100 |           data['properties']['id']['type']
101 |       ).toBe('string'); // Use bracket notation with type checking
102 |     });
103 |   });
104 | 
105 |   describe('YAML format', () => {
106 |     beforeEach(async () => {
107 |       testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
108 |         outputFormat: 'yaml',
109 |       });
110 |       client = testContext.client;
111 |     });
112 | 
113 |     it('should return YAML for openapi://info', async () => {
114 |       const result = await client.readResource({ uri: 'openapi://info' });
115 |       expect(result.contents).toHaveLength(1);
116 |       const content = result.contents[0];
117 |       expect(content.mimeType).toBe('text/yaml');
118 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
119 |       expect(() => safeParse(content.text, 'yaml')).not.toThrow();
120 |       expect(content.text).toContain('title: Complex Endpoint Test API');
121 |       expect(content.text).toMatch(/\n$/);
122 |     });
123 | 
124 |     it('should return YAML for operation detail', async () => {
125 |       const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
126 |       const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
127 |       expect(result.contents).toHaveLength(1);
128 |       const content = result.contents[0];
129 |       expect(content.mimeType).toBe('text/yaml');
130 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
131 |       expect(() => safeParse(content.text, 'yaml')).not.toThrow();
132 |       expect(content.text).toContain('operationId: getProjectTasks');
133 |       expect(content.text).toMatch(/\n$/);
134 |     });
135 | 
136 |     it('should return YAML for component detail', async () => {
137 |       const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
138 |       expect(result.contents).toHaveLength(1);
139 |       const content = result.contents[0];
140 |       expect(content.mimeType).toBe('text/yaml');
141 |       if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
142 |       expect(() => safeParse(content.text, 'yaml')).not.toThrow();
143 |       expect(content.text).toContain('type: object');
144 |       expect(content.text).toContain('properties:');
145 |       expect(content.text).toContain('id:');
146 |       expect(content.text).toMatch(/\n$/);
147 |     });
148 | 
149 |     // Note: The test for listResourceTemplates is removed as it tested old template structure.
150 |     // We could add a new test here if needed, but the mimeType for templates isn't explicitly set anymore.
151 | 
152 |     it('should handle errors in YAML format (e.g., invalid component name)', async () => {
153 |       const result = await client.readResource({ uri: 'openapi://components/schemas/InvalidName' });
154 |       expect(result.contents).toHaveLength(1);
155 |       const content = result.contents[0];
156 |       // Errors are always text/plain, regardless of configured output format
157 |       expect(content.mimeType).toBe('text/plain');
158 |       expect(content.isError).toBe(true);
159 |       if (!hasTextContent(content)) throw new Error('Expected text');
160 |       // Updated error message from getValidatedComponentDetails with sorted names
161 |       expect(content.text).toContain(
162 |         'None of the requested names (InvalidName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList'
163 |       );
164 |     });
165 |   });
166 | 
167 |   describe('Minified JSON format', () => {
168 |     beforeEach(async () => {
169 |       testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
170 |         outputFormat: 'json-minified',
171 |       });
172 |       client = testContext.client;
173 |     });
174 | 
175 |     it('should return minified JSON for openapi://info', async () => {
176 |       const result = await client.readResource({ uri: 'openapi://info' });
177 |       expect(result.contents).toHaveLength(1);
178 |       const content = result.contents[0];
179 |       expect(content.mimeType).toBe('application/json');
180 |       if (!hasTextContent(content)) throw new Error('Expected text content');
181 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
182 |       const data = safeParse(content.text, 'json');
183 |       expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API');
184 |       // Check for lack of pretty-printing whitespace
185 |       expect(content.text).not.toContain('\n ');
186 |       expect(content.text).not.toContain('  '); // Double check no indentation
187 |     });
188 | 
189 |     it('should return minified JSON for operation detail', async () => {
190 |       const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
191 |       const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
192 |       expect(result.contents).toHaveLength(1);
193 |       const content = result.contents[0];
194 |       expect(content.mimeType).toBe('application/json');
195 |       if (!hasTextContent(content)) throw new Error('Expected text content');
196 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
197 |       const data = safeParse(content.text, 'json');
198 |       expect(isObject(data) && data['operationId']).toBe('getProjectTasks');
199 |       // Check for lack of pretty-printing whitespace
200 |       expect(content.text).not.toContain('\n ');
201 |       expect(content.text).not.toContain('  ');
202 |     });
203 | 
204 |     it('should return minified JSON for component detail', async () => {
205 |       const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
206 |       expect(result.contents).toHaveLength(1);
207 |       const content = result.contents[0];
208 |       expect(content.mimeType).toBe('application/json');
209 |       if (!hasTextContent(content)) throw new Error('Expected text content');
210 |       expect(() => safeParse(content.text, 'json')).not.toThrow();
211 |       const data = safeParse(content.text, 'json');
212 |       expect(isObject(data) && data['type']).toBe('object');
213 |       expect(
214 |         isObject(data) &&
215 |           isObject(data['properties']) &&
216 |           isObject(data['properties']['id']) &&
217 |           data['properties']['id']['type']
218 |       ).toBe('string');
219 |       // Check for lack of pretty-printing whitespace
220 |       expect(content.text).not.toContain('\n ');
221 |       expect(content.text).not.toContain('  ');
222 |     });
223 |   });
224 | });
225 | 
```

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

```typescript
  1 | import { OpenAPIV3 } from 'openapi-types';
  2 | import { RenderableComponents, RenderableComponentMap } from '../../../../src/rendering/components';
  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 Components Object Fixture
 14 | const sampleComponents: OpenAPIV3.ComponentsObject = {
 15 |   schemas: {
 16 |     User: { type: 'object', properties: { name: { type: 'string' } } },
 17 |     Error: { type: 'object', properties: { message: { type: 'string' } } },
 18 |   },
 19 |   parameters: {
 20 |     userIdParam: { name: 'userId', in: 'path', required: true, schema: { type: 'integer' } },
 21 |   },
 22 |   responses: {
 23 |     NotFound: { description: 'Resource not found' },
 24 |   },
 25 |   // Intentionally empty type
 26 |   examples: {},
 27 |   // Intentionally missing type
 28 |   // securitySchemes: {}
 29 | };
 30 | 
 31 | const emptyComponents: OpenAPIV3.ComponentsObject = {};
 32 | 
 33 | describe('RenderableComponents (List Types)', () => {
 34 |   it('should list available component types correctly', () => {
 35 |     const renderable = new RenderableComponents(sampleComponents);
 36 |     const result = renderable.renderList(mockContext);
 37 | 
 38 |     expect(result).toHaveLength(1);
 39 |     expect(result[0].uriSuffix).toBe('components');
 40 |     expect(result[0].renderAsList).toBe(true);
 41 |     expect(result[0].isError).toBeUndefined();
 42 |     expect(result[0].data).toContain('Available Component Types:');
 43 |     // Check sorted types with descriptions
 44 |     expect(result[0].data).toMatch(/-\s+examples: Reusable examples of media type payloads\n/);
 45 |     expect(result[0].data).toMatch(
 46 |       /-\s+parameters: Reusable request parameters \(query, path, header, cookie\)\n/
 47 |     );
 48 |     expect(result[0].data).toMatch(/-\s+responses: Reusable API responses\n/);
 49 |     expect(result[0].data).toMatch(/-\s+schemas: Reusable data structures \(models\)\n/);
 50 |     expect(result[0].data).not.toContain('- securitySchemes'); // Missing type
 51 |     // Check hint with example
 52 |     expect(result[0].data).toContain(
 53 |       "Hint: Use 'openapi://components/{type}' to view details for a specific component type. (e.g., openapi://components/examples)"
 54 |     );
 55 |   });
 56 | 
 57 |   it('should handle empty components object', () => {
 58 |     const renderable = new RenderableComponents(emptyComponents);
 59 |     const result = renderable.renderList(mockContext);
 60 |     expect(result).toHaveLength(1);
 61 |     expect(result[0]).toMatchObject({
 62 |       uriSuffix: 'components',
 63 |       isError: true, // Changed expectation: should be an error if no components
 64 |       errorText: 'No components found in the specification.',
 65 |       renderAsList: true,
 66 |     });
 67 |   });
 68 | 
 69 |   it('should handle components object with no valid types', () => {
 70 |     // Create object with only an extension property but no valid component types
 71 |     const invalidComponents = { 'x-custom': {} } as OpenAPIV3.ComponentsObject;
 72 |     const renderable = new RenderableComponents(invalidComponents);
 73 |     const result = renderable.renderList(mockContext);
 74 |     expect(result).toHaveLength(1);
 75 |     expect(result[0]).toMatchObject({
 76 |       uriSuffix: 'components',
 77 |       isError: true,
 78 |       errorText: 'No valid component types found.',
 79 |       renderAsList: true,
 80 |     });
 81 |   });
 82 | 
 83 |   it('should handle undefined components object', () => {
 84 |     const renderable = new RenderableComponents(undefined);
 85 |     const result = renderable.renderList(mockContext);
 86 |     expect(result).toHaveLength(1);
 87 |     expect(result[0]).toMatchObject({
 88 |       uriSuffix: 'components',
 89 |       isError: true,
 90 |       errorText: 'No components found in the specification.',
 91 |       renderAsList: true,
 92 |     });
 93 |   });
 94 | 
 95 |   it('renderDetail should delegate to renderList', () => {
 96 |     const renderable = new RenderableComponents(sampleComponents);
 97 |     const listResult = renderable.renderList(mockContext);
 98 |     const detailResult = renderable.renderDetail(mockContext);
 99 |     expect(detailResult).toEqual(listResult);
100 |   });
101 | 
102 |   it('getComponentMap should return correct map', () => {
103 |     const renderable = new RenderableComponents(sampleComponents);
104 |     expect(renderable.getComponentMap('schemas')).toBe(sampleComponents.schemas);
105 |     expect(renderable.getComponentMap('parameters')).toBe(sampleComponents.parameters);
106 |     expect(renderable.getComponentMap('examples')).toBe(sampleComponents.examples);
107 |     expect(renderable.getComponentMap('securitySchemes')).toBeUndefined();
108 |   });
109 | });
110 | 
111 | describe('RenderableComponentMap (List/Detail Names)', () => {
112 |   const schemasMap = sampleComponents.schemas;
113 |   const parametersMap = sampleComponents.parameters;
114 |   const emptyMap = sampleComponents.examples;
115 |   const schemasUriSuffix = 'components/schemas';
116 |   const paramsUriSuffix = 'components/parameters';
117 | 
118 |   describe('renderList (List Names)', () => {
119 |     it('should list component names correctly (schemas)', () => {
120 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
121 |       const result = renderable.renderList(mockContext);
122 |       expect(result).toHaveLength(1);
123 |       expect(result[0].uriSuffix).toBe(schemasUriSuffix);
124 |       expect(result[0].renderAsList).toBe(true);
125 |       expect(result[0].isError).toBeUndefined();
126 |       expect(result[0].data).toContain('Available schemas:');
127 |       expect(result[0].data).toMatch(/-\s+Error\n/); // Sorted
128 |       expect(result[0].data).toMatch(/-\s+User\n/);
129 |       // Check hint with example
130 |       expect(result[0].data).toContain(
131 |         "Hint: Use 'openapi://components/schemas/{name}' to view details for a specific schema. (e.g., openapi://components/schemas/Error)"
132 |       );
133 |     });
134 | 
135 |     it('should list component names correctly (parameters)', () => {
136 |       const renderable = new RenderableComponentMap(parametersMap, 'parameters', paramsUriSuffix);
137 |       const result = renderable.renderList(mockContext);
138 |       expect(result).toHaveLength(1);
139 |       expect(result[0].uriSuffix).toBe(paramsUriSuffix);
140 |       expect(result[0].data).toContain('Available parameters:');
141 |       expect(result[0].data).toMatch(/-\s+userIdParam\n/);
142 |       // Check hint with example
143 |       expect(result[0].data).toContain(
144 |         "Hint: Use 'openapi://components/parameters/{name}' to view details for a specific parameter. (e.g., openapi://components/parameters/userIdParam)"
145 |       );
146 |     });
147 | 
148 |     it('should handle empty component map', () => {
149 |       const renderable = new RenderableComponentMap(emptyMap, 'examples', 'components/examples');
150 |       const result = renderable.renderList(mockContext);
151 |       expect(result).toHaveLength(1);
152 |       expect(result[0]).toMatchObject({
153 |         uriSuffix: 'components/examples',
154 |         isError: true,
155 |         errorText: 'No components of type "examples" found.',
156 |         renderAsList: true,
157 |       });
158 |     });
159 | 
160 |     it('should handle undefined component map', () => {
161 |       const renderable = new RenderableComponentMap(
162 |         undefined,
163 |         'securitySchemes',
164 |         'components/securitySchemes'
165 |       );
166 |       const result = renderable.renderList(mockContext);
167 |       expect(result).toHaveLength(1);
168 |       expect(result[0]).toMatchObject({
169 |         uriSuffix: 'components/securitySchemes',
170 |         isError: true,
171 |         errorText: 'No components of type "securitySchemes" found.',
172 |         renderAsList: true,
173 |       });
174 |     });
175 |   });
176 | 
177 |   describe('renderComponentDetail (Get Component Detail)', () => {
178 |     it('should return detail for a single valid component', () => {
179 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
180 |       const result = renderable.renderComponentDetail(mockContext, ['User']);
181 |       expect(result).toHaveLength(1);
182 |       expect(result[0]).toEqual({
183 |         uriSuffix: `${schemasUriSuffix}/User`,
184 |         data: schemasMap?.User, // Expect raw component object
185 |       });
186 |     });
187 | 
188 |     it('should return details for multiple valid components', () => {
189 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
190 |       const result = renderable.renderComponentDetail(mockContext, ['Error', 'User']);
191 |       expect(result).toHaveLength(2);
192 |       expect(result).toContainEqual({
193 |         uriSuffix: `${schemasUriSuffix}/Error`,
194 |         data: schemasMap?.Error,
195 |       });
196 |       expect(result).toContainEqual({
197 |         uriSuffix: `${schemasUriSuffix}/User`,
198 |         data: schemasMap?.User,
199 |       });
200 |     });
201 | 
202 |     it('should return error for non-existent component', () => {
203 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
204 |       const result = renderable.renderComponentDetail(mockContext, ['NonExistent']);
205 |       expect(result).toHaveLength(1);
206 |       expect(result[0]).toEqual({
207 |         uriSuffix: `${schemasUriSuffix}/NonExistent`,
208 |         data: null,
209 |         isError: true,
210 |         errorText: 'Component "NonExistent" of type "schemas" not found.',
211 |         renderAsList: true,
212 |       });
213 |     });
214 | 
215 |     it('should handle mix of valid and invalid components', () => {
216 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
217 |       const result = renderable.renderComponentDetail(mockContext, ['User', 'Invalid']);
218 |       expect(result).toHaveLength(2);
219 |       expect(result).toContainEqual({
220 |         uriSuffix: `${schemasUriSuffix}/User`,
221 |         data: schemasMap?.User,
222 |       });
223 |       expect(result).toContainEqual({
224 |         uriSuffix: `${schemasUriSuffix}/Invalid`,
225 |         data: null,
226 |         isError: true,
227 |         errorText: 'Component "Invalid" of type "schemas" not found.',
228 |         renderAsList: true,
229 |       });
230 |     });
231 | 
232 |     it('should return error if component map is undefined', () => {
233 |       const renderable = new RenderableComponentMap(
234 |         undefined,
235 |         'securitySchemes',
236 |         'components/securitySchemes'
237 |       );
238 |       const result = renderable.renderComponentDetail(mockContext, ['apiKey']);
239 |       expect(result).toHaveLength(1);
240 |       expect(result[0]).toEqual({
241 |         uriSuffix: 'components/securitySchemes/apiKey',
242 |         data: null,
243 |         isError: true,
244 |         errorText: 'Component map for type "securitySchemes" not found.',
245 |         renderAsList: true,
246 |       });
247 |     });
248 |   });
249 | 
250 |   describe('renderDetail (Interface Method)', () => {
251 |     it('should delegate to renderList', () => {
252 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
253 |       const listResult = renderable.renderList(mockContext);
254 |       const detailResult = renderable.renderDetail(mockContext);
255 |       expect(detailResult).toEqual(listResult);
256 |     });
257 |   });
258 | 
259 |   describe('getComponent', () => {
260 |     it('should return correct component object', () => {
261 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
262 |       expect(renderable.getComponent('User')).toBe(schemasMap?.User);
263 |       expect(renderable.getComponent('Error')).toBe(schemasMap?.Error);
264 |     });
265 | 
266 |     it('should return undefined for non-existent component', () => {
267 |       const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
268 |       expect(renderable.getComponent('NonExistent')).toBeUndefined();
269 |     });
270 | 
271 |     it('should return undefined if component map is undefined', () => {
272 |       const renderable = new RenderableComponentMap(
273 |         undefined,
274 |         'securitySchemes',
275 |         'components/securitySchemes'
276 |       );
277 |       expect(renderable.getComponent('apiKey')).toBeUndefined();
278 |     });
279 |   });
280 | });
281 | 
```

--------------------------------------------------------------------------------
/memory-bank/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
  1 | # System Patterns
  2 | 
  3 | ## Architecture Overview
  4 | 
  5 | ```mermaid
  6 | graph TD
  7 |     CliArg[CLI Argument (Path/URL)] --> Config[src/config.ts]
  8 |     Config --> Server[MCP Server]
  9 | 
 10 |     CliArg --> SpecLoader[Spec Loader Service]
 11 |     SpecLoader -- Uses --> S2OLib[swagger2openapi Lib]
 12 |     SpecLoader --> Transform[Reference Transform Service]
 13 |     Transform --> Handlers[Resource Handlers]
 14 | 
 15 |     Server --> Handlers
 16 | 
 17 |     subgraph Services
 18 |         SpecLoader
 19 |         Transform
 20 |         S2OLib
 21 |     end
 22 | 
 23 |     subgraph Handlers
 24 |         TopLevelFieldHandler[TopLevelField Handler (openapi://{field})]
 25 |         PathItemHandler[PathItem Handler (openapi://paths/{path})]
 26 |         OperationHandler[Operation Handler (openapi://paths/{path}/{method*})]
 27 |         ComponentMapHandler[ComponentMap Handler (openapi://components/{type})]
 28 |         ComponentDetailHandler[ComponentDetail Handler (openapi://components/{type}/{name*})]
 29 |     end
 30 | 
 31 |     subgraph Formatters
 32 |         JsonFormatter[Json Formatter]
 33 |         YamlFormatter[Yaml Formatter]
 34 |         MinifiedJsonFormatter[Minified Json Formatter]
 35 |     end
 36 | 
 37 |     subgraph Rendering (OOP)
 38 |         RenderableDocument[RenderableDocument]
 39 |         RenderablePaths[RenderablePaths]
 40 |         RenderablePathItem[RenderablePathItem]
 41 |         RenderableComponents[RenderableComponents]
 42 |         RenderableComponentMap[RenderableComponentMap]
 43 |         RenderUtils[Rendering Utils]
 44 |     end
 45 | 
 46 |     Handlers --> Rendering
 47 |     SpecLoader --> Rendering
 48 | 
 49 |     subgraph Utils
 50 |         UriBuilder[URI Builder (src/utils)]
 51 |     end
 52 | 
 53 |     UriBuilder --> Transform
 54 |     UriBuilder --> RenderUtils
 55 | ```
 56 | 
 57 | ## Component Structure
 58 | 
 59 | ### Services Layer
 60 | 
 61 | - **SpecLoaderService (`src/services/spec-loader.ts`):**
 62 |   - Uses `swagger2openapi` library.
 63 |   - Loads specification from local file path or remote URL provided via CLI argument.
 64 |   - Handles parsing of JSON/YAML.
 65 |   - Automatically converts Swagger v2.0 specs to OpenAPI v3.0 objects.
 66 |   - Provides the resulting OpenAPI v3.0 document object.
 67 |   - Handles errors during loading/conversion.
 68 | - **ReferenceTransformService (`src/services/reference-transform.ts`):**
 69 |   - Takes the OpenAPI v3.0 document from `SpecLoaderService`.
 70 |   - Traverses the document and transforms internal references (e.g., `#/components/schemas/MySchema`) into MCP URIs (e.g., `openapi://components/schemas/MySchema`).
 71 |   - Uses `UriBuilder` utility for consistent URI generation.
 72 |   - Returns the transformed OpenAPI v3.0 document.
 73 | - **Formatters (`src/services/formatters.ts`):**
 74 |   - Provide implementations for different output formats (JSON, YAML, Minified JSON).
 75 |   - Used by handlers to serialize detail view responses.
 76 |   - `IFormatter` interface defines `format()` and `getMimeType()`.
 77 |   - `createFormatter` function instantiates the correct formatter based on `OutputFormat` type (`json`, `yaml`, `json-minified`).
 78 | 
 79 | ### Rendering Layer (OOP)
 80 | 
 81 | - **Renderable Classes:** Wrapper classes (`RenderableDocument`, `RenderablePaths`, `RenderablePathItem`, `RenderableComponents`, `RenderableComponentMap`) implement `RenderableSpecObject` interface.
 82 | - **Interface:** `RenderableSpecObject` defines `renderList()` and `renderDetail()` methods returning `RenderResultItem[]`.
 83 | - **RenderResultItem:** Intermediate structure holding data (`unknown`), `uriSuffix`, `isError?`, `errorText?`, `renderAsList?`.
 84 | - **RenderContext:** Passed to render methods, contains `formatter` and `baseUri`.
 85 | - **Utils:** Helper functions (`getOperationSummary`, `generateListHint`, `createErrorResult`) in `src/rendering/utils.ts`. `generateListHint` now uses centralized URI builder logic.
 86 | 
 87 | ### Handler Layer
 88 | 
 89 | - **Structure:** Separate handlers for each distinct URI pattern/resource type.
 90 | - **Responsibilities:**
 91 |   - Parse URI variables provided by SDK.
 92 |   - Load/retrieve the transformed spec via `SpecLoaderService`.
 93 |   - Instantiate appropriate `Renderable*` classes.
 94 |   - Invoke the correct rendering method (`renderList` or a specific detail method like `renderTopLevelFieldDetail`, `renderOperationDetail`, `renderComponentDetail`).
 95 |   - Format the `RenderResultItem[]` using `formatResults` from `src/handlers/handler-utils.ts`.
 96 |   - Construct the final `{ contents: ... }` response object.
 97 |   - Instantiate `RenderablePathItem` correctly with raw path and built suffix.
 98 | - **Handlers:**
 99 |   - `TopLevelFieldHandler`: Handles `openapi://{field}`. Delegates list rendering for `paths`/`components` to `RenderablePaths`/`RenderableComponents`. Renders details for other fields (`info`, `servers`, etc.) via `RenderableDocument.renderTopLevelFieldDetail`.
100 |   - `PathItemHandler`: Handles `openapi://paths/{path}`. Uses `RenderablePathItem.renderList` to list methods. Instantiates `RenderablePathItem` with raw path and built suffix.
101 |   - `OperationHandler`: Handles `openapi://paths/{path}/{method*}`. Uses `RenderablePathItem.renderOperationDetail` for operation details. Handles multi-value `method` variable. Instantiates `RenderablePathItem` with raw path and built suffix.
102 |   - `ComponentMapHandler`: Handles `openapi://components/{type}`. Uses `RenderableComponentMap.renderList` to list component names.
103 |   - `ComponentDetailHandler`: Handles `openapi://components/{type}/{name*}`. Uses `RenderableComponentMap.renderComponentDetail` for component details. Handles multi-value `name` variable.
104 | - **Utils:** Shared functions (`formatResults`, `isOpenAPIV3`, `FormattedResultItem` type, validation helpers) in `src/handlers/handler-utils.ts`.
105 | 
106 | ### Utilities Layer
107 | 
108 | - **URI Builder (`src/utils/uri-builder.ts`):**
109 |   - Centralized functions for building full URIs (`openapi://...`) and URI suffixes.
110 |   - Handles encoding of path components (removing leading slash first).
111 |   - Used by `ReferenceTransformService` and the rendering layer (`generateListHint`, `Renderable*` classes) to ensure consistency.
112 | 
113 | ### Configuration Layer (`src/config.ts`)
114 | 
115 | - Parses command-line arguments.
116 | - Expects a single required argument: the path or URL to the specification file.
117 | - Supports an optional `--output-format` argument (`json`, `yaml`, `json-minified`).
118 | - Validates arguments and provides usage instructions on error.
119 |   - Creates the `ServerConfig` object used by the server.
120 | 
121 | ## Release Automation (`semantic-release`)
122 | 
123 | - **Configuration:** Defined in `.releaserc.json`.
124 | - **Workflow:**
125 |   1.  `@semantic-release/commit-analyzer`: Determines release type from conventional commits.
126 |   2.  `@semantic-release/release-notes-generator`: Generates release notes.
127 |   3.  `@semantic-release/changelog`: Updates `CHANGELOG.md`.
128 |   4.  `@semantic-release/npm`: Updates `version` in `package.json`.
129 |   5.  `@semantic-release/exec`: Runs `scripts/generate-version.js` to create/update `src/version.ts` with the new version.
130 |   6.  `@semantic-release/git`: Commits `package.json`, `package-lock.json`, `CHANGELOG.md`, and `src/version.ts`. Creates Git tag.
131 |   7.  `@codedependant/semantic-release-docker`: Builds the Docker image using `./Dockerfile` and pushes it to Docker Hub (`kadykov/mcp-openapi-schema-explorer`) with `latest` and version tags.
132 |   8.  `@semantic-release/github`: Creates GitHub Release.
133 | - **Trigger:** Executed by the `release` job in the GitHub Actions workflow (`.github/workflows/ci.yml`) on pushes to the `main` branch, using `cycjimmy/semantic-release-action@v4`.
134 | - **CI Action:** The `cycjimmy/semantic-release-action` handles installing `semantic-release` and the plugins listed in its `extra_plugins` input (`@semantic-release/changelog`, `@semantic-release/exec`, `@semantic-release/git`, `@codedependant/semantic-release-docker`).
135 | - **Docker Environment:** The CI job sets up Docker QEMU, Buildx, and logs into Docker Hub before running the semantic-release action.
136 | - **Versioning:** The server version in `src/index.ts` is dynamically imported from the generated `src/version.ts`. A default `src/version.ts` (with `0.0.0-dev`) is kept in the repository for local builds.
137 | 
138 | ## Resource Design Patterns
139 | 
140 | ### URI Structure (Revised)
141 | 
142 | - Implicit List/Detail based on path depth.
143 | - Aligned with OpenAPI specification structure.
144 | - **Templates:**
145 |   - `openapi://{field}`: Top-level field details (info, servers) or list trigger (paths, components).
146 |   - `openapi://paths/{path}`: List methods for a specific path.
147 |   - `openapi://paths/{path}/{method*}`: Operation details (supports multiple methods).
148 |   - `openapi://components/{type}`: List names for a specific component type.
149 |   - `openapi://components/{type}/{name*}`: Component details (supports multiple names).
150 | - **Completions:**
151 |   - Defined directly in `src/index.ts` within `ResourceTemplate` definitions passed to `server.resource()`.
152 |   - Uses the `transformedSpec` object loaded before server initialization.
153 |   - Provides suggestions for `{field}`, `{path}`, `{method*}`, `{type}`.
154 |   - Provides suggestions for `{name*}` _only_ if the spec contains exactly one component type.
155 | - **Reference URIs (Corrected):**
156 |   - Internal `$ref`s like `#/components/schemas/MySchema` are transformed by `ReferenceTransformService` into resolvable MCP URIs: `openapi://components/schemas/MySchema`.
157 |   - This applies to all component types under `#/components/`.
158 |   - External references remain unchanged.
159 | 
160 | ### Response Format Patterns
161 | 
162 | 1. **Token-Efficient Lists:**
163 |    - `text/plain` format used for all list views (`openapi://paths`, `openapi://components`, `openapi://paths/{path}`, `openapi://components/{type}`).
164 |    - Include hints for navigating to detail views, generated via `generateListHint` using the centralized URI builder.
165 |    - `openapi://paths` format: `METHOD1 METHOD2 /path`
166 |    - `openapi://paths/{path}` format: `METHOD: Summary/OpId`
167 |    - `openapi://components` format: `- type`
168 |    - `openapi://components/{type}` format: `- name`
169 | 2. **Detail Views:**
170 |    - Use configured formatter (JSON/YAML/Minified JSON via `IFormatter`).
171 |    - Handled by `openapi://{field}` (for non-structural fields), `openapi://paths/{path}/{method*}`, `openapi://components/{type}/{name*}`.
172 | 3. **Error Handling:**
173 |    - Handlers catch errors and use `createErrorResult` utility.
174 |    - `formatResults` utility formats errors into `FormattedResultItem` with `isError: true`, `mimeType: 'text/plain'`, and error message in `text`.
175 | 4. **Type Safety:**
176 |    - Strong typing with OpenAPI v3 types.
177 |    - `Renderable*` classes encapsulate type-specific logic.
178 |    - `isOpenAPIV3` type guard used in handlers.
179 | 
180 | ## Extension Points
181 | 
182 | 1. Reference Transformers:
183 | 
184 |    - AsyncAPI transformer
185 |    - GraphQL transformer
186 |    - Custom format transformers
187 | 
188 | 2. Resource Handlers:
189 | 
190 |    - Schema resource handler
191 |    - Additional reference handlers
192 |    - Custom format handlers (via `IFormatter` interface)
193 | 
194 | 3. URI Resolution:
195 | 
196 |    - Reference transformation service (`ReferenceTransformService`) handles converting `#/components/{type}/{name}` to `openapi://components/{type}/{name}` URIs during spec loading.
197 |    - Cross-resource linking is implicit via generated URIs in hints and transformed refs.
198 |    - External references are currently kept as-is.
199 | 
200 | 4. Validation:
201 |    - Parameter validation
202 |    - Reference validation
203 |    - Format-specific validation
204 | 
205 | ## Testing Strategy
206 | 
207 | 1. **Unit Tests:**
208 |    - `SpecLoaderService`: Mock `swagger2openapi` to test local/remote and v2/v3 loading logic, including error handling.
209 |    - `ReferenceTransformService`: Verify correct transformation of `#/components/...` refs to MCP URIs.
210 |    - Rendering Classes (`Renderable*`): Test list and detail rendering logic.
211 |    - Handlers: Mock services (`SpecLoaderService`, `IFormatter`) to test URI parsing and delegation to rendering classes.
212 |    - `UriBuilder`: Test URI encoding and generation.
213 | 2. **E2E Tests:**
214 |    - Use `mcp-test-helpers` to start the server with different spec inputs.
215 |    - **`spec-loading.test.ts`:** Verify basic resource access (`info`, `paths`, `components`, specific component detail) works correctly when loading:
216 |      - Local Swagger v2.0 spec (`test/fixtures/sample-v2-api.json`).
217 |      - Remote OpenAPI v3.0 spec (e.g., Petstore URL).
218 |    - **`refactored-resources.test.ts`:** Continue to test detailed resource interactions (multi-value params, specific path/method/component combinations, errors) using the primary complex local v3 fixture (`complex-endpoint.json`).
219 |    - **`format.test.ts`:** Verify different output formats (JSON/YAML/Minified JSON) work as expected.
220 |    - **Completion Tests:** Added to `refactored-resources.test.ts` using `client.complete()` to verify completion logic.
221 | 3. **Test Support:**
222 |    - Type-safe test utilities (`mcp-test-helpers`). Updated `StartServerOptions` to include `json-minified`.
223 |    - Test fixtures for v2.0 and v3.0 specs.
224 | 4. **CI Integration (`.github/workflows/ci.yml`):**
225 |    - **`test` Job:** Runs on push/PR to `main`. Uses Node 22, installs `just`, runs `npm ci`, then `just all` (format, lint, build, test). Uploads coverage.
226 |    - **`security` Job:** Runs on push/PR to `main`. Uses Node 22, installs `just`, runs `npm ci`, then `just security` (audit, licenses). Runs CodeQL analysis separately.
227 |    - **`release` Job:** Runs _only_ on push to `main` after `test` and `security` pass. Checks out full history, sets up Docker (QEMU, Buildx, Login), then runs `cycjimmy/semantic-release-action@v4` with necessary `extra_plugins` and environment variables (`GITHUB_TOKEN`, `NPM_TOKEN`, Docker Hub credentials handled by login action).
228 | 
```
Page 2/3FirstPrevNextLast