#
tokens: 5248/50000 1/75 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 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/resources.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  2 | // Import specific SDK types needed
  3 | import {
  4 |   ReadResourceResult,
  5 |   TextResourceContents,
  6 |   // Removed unused CompleteRequest, CompleteResult
  7 | } from '@modelcontextprotocol/sdk/types.js';
  8 | import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers';
  9 | import path from 'path';
 10 | 
 11 | // Use the complex spec for E2E tests
 12 | const complexSpecPath = path.resolve(__dirname, '../../fixtures/complex-endpoint.json');
 13 | 
 14 | // Helper function to parse JSON safely
 15 | function parseJsonSafely(text: string | undefined): unknown {
 16 |   if (text === undefined) {
 17 |     throw new Error('Received undefined text for JSON parsing');
 18 |   }
 19 |   try {
 20 |     return JSON.parse(text);
 21 |   } catch (e) {
 22 |     console.error('Failed to parse JSON:', text);
 23 |     throw new Error(`Invalid JSON received: ${e instanceof Error ? e.message : String(e)}`);
 24 |   }
 25 | }
 26 | 
 27 | // Type guard to check if content is TextResourceContents
 28 | function hasTextContent(
 29 |   content: ReadResourceResult['contents'][0]
 30 | ): content is TextResourceContents {
 31 |   // Check for the 'text' property specifically, differentiating from BlobResourceContents
 32 |   return typeof (content as TextResourceContents).text === 'string';
 33 | }
 34 | 
 35 | describe('E2E Tests for Refactored Resources', () => {
 36 |   let testContext: McpTestContext;
 37 |   let client: Client; // Use the correct Client type
 38 | 
 39 |   // Helper to setup client for tests
 40 |   async function setup(specPath: string = complexSpecPath): Promise<void> {
 41 |     // Use complex spec by default
 42 |     testContext = await startMcpServer(specPath, { outputFormat: 'json' }); // Default to JSON
 43 |     client = testContext.client; // Get client from helper context
 44 |     // Initialization is handled by startMcpServer connecting the transport
 45 |   }
 46 | 
 47 |   afterEach(async () => {
 48 |     await testContext?.cleanup(); // Use cleanup function from helper
 49 |   });
 50 | 
 51 |   // Helper to read resource and perform basic checks
 52 |   async function readResourceAndCheck(uri: string): Promise<ReadResourceResult['contents'][0]> {
 53 |     const result = await client.readResource({ uri });
 54 |     expect(result.contents).toHaveLength(1);
 55 |     const content = result.contents[0];
 56 |     expect(content.uri).toBe(uri);
 57 |     return content;
 58 |   }
 59 | 
 60 |   // Helper to read resource and check for text/plain list content
 61 |   async function checkTextListResponse(uri: string, expectedSubstrings: string[]): Promise<string> {
 62 |     const content = await readResourceAndCheck(uri);
 63 |     expect(content.mimeType).toBe('text/plain');
 64 |     expect(content.isError).toBeFalsy();
 65 |     if (!hasTextContent(content)) throw new Error('Expected text content');
 66 |     for (const sub of expectedSubstrings) {
 67 |       expect(content.text).toContain(sub);
 68 |     }
 69 |     return content.text;
 70 |   }
 71 | 
 72 |   // Helper to read resource and check for JSON detail content
 73 |   async function checkJsonDetailResponse(uri: string, expectedObject: object): Promise<unknown> {
 74 |     const content = await readResourceAndCheck(uri);
 75 |     expect(content.mimeType).toBe('application/json');
 76 |     expect(content.isError).toBeFalsy();
 77 |     if (!hasTextContent(content)) throw new Error('Expected text content');
 78 |     const data = parseJsonSafely(content.text);
 79 |     expect(data).toMatchObject(expectedObject);
 80 |     return data;
 81 |   }
 82 | 
 83 |   // Helper to read resource and check for error
 84 |   async function checkErrorResponse(uri: string, expectedErrorText: string): Promise<void> {
 85 |     const content = await readResourceAndCheck(uri);
 86 |     expect(content.isError).toBe(true);
 87 |     expect(content.mimeType).toBe('text/plain'); // Errors are plain text
 88 |     if (!hasTextContent(content)) throw new Error('Expected text content for error');
 89 |     expect(content.text).toContain(expectedErrorText);
 90 |   }
 91 | 
 92 |   describe('openapi://{field}', () => {
 93 |     beforeEach(async () => await setup());
 94 | 
 95 |     it('should retrieve the "info" field', async () => {
 96 |       // Matches complex-endpoint.json
 97 |       await checkJsonDetailResponse('openapi://info', {
 98 |         title: 'Complex Endpoint Test API',
 99 |         version: '1.0.0',
100 |       });
101 |     });
102 | 
103 |     it('should retrieve the "paths" list', async () => {
104 |       // Matches complex-endpoint.json
105 |       await checkTextListResponse('openapi://paths', [
106 |         'Hint:',
107 |         'GET POST /api/v1/organizations/{orgId}/projects/{projectId}/tasks',
108 |       ]);
109 |     });
110 | 
111 |     it('should retrieve the "components" list', async () => {
112 |       // Matches complex-endpoint.json (only has schemas)
113 |       await checkTextListResponse('openapi://components', [
114 |         'Available Component Types:',
115 |         '- schemas',
116 |         "Hint: Use 'openapi://components/{type}'",
117 |       ]);
118 |     });
119 | 
120 |     it('should return error for invalid field', async () => {
121 |       const uri = 'openapi://invalidfield';
122 |       await checkErrorResponse(uri, 'Field "invalidfield" not found');
123 |     });
124 |   });
125 | 
126 |   describe('openapi://paths/{path}', () => {
127 |     beforeEach(async () => await setup());
128 | 
129 |     it('should list methods for the complex task path', async () => {
130 |       const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
131 |       const encodedPath = encodeURIComponent(complexPath);
132 |       // Update expected format based on METHOD: Summary/OpId
133 |       await checkTextListResponse(`openapi://paths/${encodedPath}`, [
134 |         "Hint: Use 'openapi://paths/api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks/{method}'", // Hint comes first now
135 |         '', // Blank line after hint
136 |         'GET: Get Tasks', // METHOD: summary
137 |         'POST: Create Task', // METHOD: summary
138 |       ]);
139 |     });
140 | 
141 |     it('should return error for non-existent path', async () => {
142 |       const encodedPath = encodeURIComponent('nonexistent');
143 |       const uri = `openapi://paths/${encodedPath}`;
144 |       // Updated error message from getValidatedPathItem
145 |       await checkErrorResponse(uri, 'Path "/nonexistent" not found in the specification.');
146 |     });
147 |   });
148 | 
149 |   describe('openapi://paths/{path}/{method*}', () => {
150 |     beforeEach(async () => await setup());
151 | 
152 |     it('should get details for GET on complex path', async () => {
153 |       const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
154 |       const encodedPath = encodeURIComponent(complexPath);
155 |       // Check operationId from complex-endpoint.json
156 |       await checkJsonDetailResponse(`openapi://paths/${encodedPath}/get`, {
157 |         operationId: 'getProjectTasks',
158 |       });
159 |     });
160 | 
161 |     it('should get details for multiple methods GET,POST on complex path', async () => {
162 |       const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
163 |       const encodedPath = encodeURIComponent(complexPath);
164 |       const result = await client.readResource({ uri: `openapi://paths/${encodedPath}/get,post` });
165 |       expect(result.contents).toHaveLength(2);
166 | 
167 |       const getContent = result.contents.find(c => c.uri.endsWith('/get'));
168 |       expect(getContent).toBeDefined();
169 |       expect(getContent?.isError).toBeFalsy();
170 |       if (!getContent || !hasTextContent(getContent))
171 |         throw new Error('Expected text content for GET');
172 |       const getData = parseJsonSafely(getContent.text);
173 |       // Check operationId from complex-endpoint.json
174 |       expect(getData).toMatchObject({ operationId: 'getProjectTasks' });
175 | 
176 |       const postContent = result.contents.find(c => c.uri.endsWith('/post'));
177 |       expect(postContent).toBeDefined();
178 |       expect(postContent?.isError).toBeFalsy();
179 |       if (!postContent || !hasTextContent(postContent))
180 |         throw new Error('Expected text content for POST');
181 |       const postData = parseJsonSafely(postContent.text);
182 |       // Check operationId from complex-endpoint.json
183 |       expect(postData).toMatchObject({ operationId: 'createProjectTask' });
184 |     });
185 | 
186 |     it('should return error for invalid method on complex path', async () => {
187 |       const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
188 |       const encodedPath = encodeURIComponent(complexPath);
189 |       const uri = `openapi://paths/${encodedPath}/put`;
190 |       // Updated error message from getValidatedOperations
191 |       await checkErrorResponse(
192 |         uri,
193 |         'None of the requested methods (put) are valid for path "/api/v1/organizations/{orgId}/projects/{projectId}/tasks". Available methods: get, post'
194 |       );
195 |     });
196 |   });
197 | 
198 |   describe('openapi://components/{type}', () => {
199 |     beforeEach(async () => await setup());
200 | 
201 |     it('should list schemas', async () => {
202 |       // Matches complex-endpoint.json
203 |       await checkTextListResponse('openapi://components/schemas', [
204 |         'Available schemas:',
205 |         '- CreateTaskRequest',
206 |         '- Task',
207 |         '- TaskList',
208 |         "Hint: Use 'openapi://components/schemas/{name}'",
209 |       ]);
210 |     });
211 | 
212 |     it('should return error for invalid type', async () => {
213 |       const uri = 'openapi://components/invalid';
214 |       await checkErrorResponse(uri, 'Invalid component type: invalid');
215 |     });
216 |   });
217 | 
218 |   describe('openapi://components/{type}/{name*}', () => {
219 |     beforeEach(async () => await setup());
220 | 
221 |     it('should get details for schema Task', async () => {
222 |       // Matches complex-endpoint.json
223 |       await checkJsonDetailResponse('openapi://components/schemas/Task', {
224 |         type: 'object',
225 |         properties: { id: { type: 'string' }, title: { type: 'string' } },
226 |       });
227 |     });
228 | 
229 |     it('should get details for multiple schemas Task,TaskList', async () => {
230 |       // Matches complex-endpoint.json
231 |       const result = await client.readResource({
232 |         uri: 'openapi://components/schemas/Task,TaskList',
233 |       });
234 |       expect(result.contents).toHaveLength(2);
235 | 
236 |       const taskContent = result.contents.find(c => c.uri.endsWith('/Task'));
237 |       expect(taskContent).toBeDefined();
238 |       expect(taskContent?.isError).toBeFalsy();
239 |       if (!taskContent || !hasTextContent(taskContent))
240 |         throw new Error('Expected text content for Task');
241 |       const taskData = parseJsonSafely(taskContent.text);
242 |       expect(taskData).toMatchObject({ properties: { id: { type: 'string' } } });
243 | 
244 |       const taskListContent = result.contents.find(c => c.uri.endsWith('/TaskList'));
245 |       expect(taskListContent).toBeDefined();
246 |       expect(taskListContent?.isError).toBeFalsy();
247 |       if (!taskListContent || !hasTextContent(taskListContent))
248 |         throw new Error('Expected text content for TaskList');
249 |       const taskListData = parseJsonSafely(taskListContent.text);
250 |       expect(taskListData).toMatchObject({ properties: { items: { type: 'array' } } });
251 |     });
252 | 
253 |     it('should return error for invalid name', async () => {
254 |       const uri = 'openapi://components/schemas/InvalidSchemaName';
255 |       // Updated error message from getValidatedComponentDetails with sorted names
256 |       await checkErrorResponse(
257 |         uri,
258 |         'None of the requested names (InvalidSchemaName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList'
259 |       );
260 |     });
261 |   });
262 | 
263 |   // Removed ListResourceTemplates test suite as the 'complete' property
264 |   // is likely not part of the standard response payload.
265 |   // We assume the templates are registered correctly in src/index.ts.
266 | 
267 |   describe('Completion Tests', () => {
268 |     beforeEach(async () => await setup()); // Use the same setup
269 | 
270 |     it('should provide completions for {field}', async () => {
271 |       const params = {
272 |         argument: { name: 'field', value: '' }, // Empty value to get all
273 |         ref: { type: 'ref/resource' as const, uri: 'openapi://{field}' },
274 |       };
275 |       const result = await client.complete(params);
276 |       expect(result.completion).toBeDefined();
277 |       expect(result.completion.values).toEqual(
278 |         expect.arrayContaining(['openapi', 'info', 'paths', 'components']) // Based on complex-endpoint.json
279 |       );
280 |       expect(result.completion.values).toHaveLength(4);
281 |     });
282 | 
283 |     it('should provide completions for {path}', async () => {
284 |       const params = {
285 |         argument: { name: 'path', value: '' }, // Empty value to get all
286 |         ref: { type: 'ref/resource' as const, uri: 'openapi://paths/{path}' },
287 |       };
288 |       const result = await client.complete(params);
289 |       expect(result.completion).toBeDefined();
290 |       // Check for the encoded path from complex-endpoint.json
291 |       expect(result.completion.values).toEqual([
292 |         'api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks',
293 |       ]);
294 |     });
295 | 
296 |     it('should provide completions for {method*}', async () => {
297 |       const params = {
298 |         argument: { name: 'method', value: '' }, // Empty value to get all
299 |         ref: {
300 |           type: 'ref/resource' as const,
301 |           uri: 'openapi://paths/{path}/{method*}', // Use the exact template URI
302 |         },
303 |       };
304 |       const result = await client.complete(params);
305 |       expect(result.completion).toBeDefined();
306 |       // Check for the static list of methods defined in src/index.ts
307 |       expect(result.completion.values).toEqual([
308 |         'GET',
309 |         'POST',
310 |         'PUT',
311 |         'DELETE',
312 |         'PATCH',
313 |         'OPTIONS',
314 |         'HEAD',
315 |         'TRACE',
316 |       ]);
317 |     });
318 | 
319 |     it('should provide completions for {type}', async () => {
320 |       const params = {
321 |         argument: { name: 'type', value: '' }, // Empty value to get all
322 |         ref: { type: 'ref/resource' as const, uri: 'openapi://components/{type}' },
323 |       };
324 |       const result = await client.complete(params);
325 |       expect(result.completion).toBeDefined();
326 |       // Check for component types in complex-endpoint.json
327 |       expect(result.completion.values).toEqual(['schemas']);
328 |     });
329 | 
330 |     // Updated test for conditional name completion
331 |     it('should provide completions for {name*} when only one component type exists', async () => {
332 |       // complex-endpoint.json only has 'schemas'
333 |       const params = {
334 |         argument: { name: 'name', value: '' },
335 |         ref: {
336 |           type: 'ref/resource' as const,
337 |           uri: 'openapi://components/{type}/{name*}', // Use the exact template URI
338 |         },
339 |       };
340 |       const result = await client.complete(params);
341 |       expect(result.completion).toBeDefined();
342 |       // Expect schema names from complex-endpoint.json
343 |       expect(result.completion.values).toEqual(
344 |         expect.arrayContaining(['CreateTaskRequest', 'Task', 'TaskList'])
345 |       );
346 |       expect(result.completion.values).toHaveLength(3);
347 |     });
348 | 
349 |     // New test for multiple component types
350 |     it('should NOT provide completions for {name*} when multiple component types exist', async () => {
351 |       // Need to restart the server with the multi-component spec
352 |       await testContext?.cleanup(); // Clean up previous server
353 |       const multiSpecPath = path.resolve(__dirname, '../../fixtures/multi-component-types.json');
354 |       await setup(multiSpecPath); // Restart server with new spec
355 | 
356 |       const params = {
357 |         argument: { name: 'name', value: '' },
358 |         ref: {
359 |           type: 'ref/resource' as const,
360 |           uri: 'openapi://components/{type}/{name*}', // Use the exact template URI
361 |         },
362 |       };
363 |       const result = await client.complete(params);
364 |       expect(result.completion).toBeDefined();
365 |       // Expect empty array because multiple types (schemas, parameters) exist
366 |       expect(result.completion.values).toEqual([]);
367 |     });
368 |   });
369 | });
370 | 
```
Page 3/3FirstPrevNextLast