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