Skip to main content
Glama
kadykov

OpenAPI Schema Explorer

resources.test.ts15.9 kB
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; // Import specific SDK types needed import { ReadResourceResult, TextResourceContents, // Removed unused CompleteRequest, CompleteResult } from '@modelcontextprotocol/sdk/types.js'; import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers'; import { FormattedResultItem } from '../../../src/handlers/handler-utils'; import path from 'path'; // Use the complex spec for E2E tests const complexSpecPath = path.resolve(__dirname, '../../fixtures/complex-endpoint.json'); // Helper function to parse JSON safely function parseJsonSafely(text: string | undefined): unknown { if (text === undefined) { throw new Error('Received undefined text for JSON parsing'); } try { return JSON.parse(text); } catch (e) { console.error('Failed to parse JSON:', text); throw new Error(`Invalid JSON received: ${e instanceof Error ? e.message : String(e)}`); } } // Type guard to check if content is TextResourceContents function hasTextContent( content: ReadResourceResult['contents'][0] ): content is TextResourceContents { // Check for the 'text' property specifically, differentiating from BlobResourceContents return typeof (content as TextResourceContents).text === 'string'; } describe('E2E Tests for Refactored Resources', () => { let testContext: McpTestContext; let client: Client; // Use the correct Client type // Helper to setup client for tests async function setup(specPath: string = complexSpecPath): Promise<void> { // Use complex spec by default testContext = await startMcpServer(specPath, { outputFormat: 'json' }); // Default to JSON client = testContext.client; // Get client from helper context // Initialization is handled by startMcpServer connecting the transport } afterEach(async () => { await testContext?.cleanup(); // Use cleanup function from helper }); // Helper to read resource and perform basic checks async function readResourceAndCheck(uri: string): Promise<ReadResourceResult['contents'][0]> { const result = await client.readResource({ uri }); expect(result.contents).toHaveLength(1); const content = result.contents[0]; expect(content.uri).toBe(uri); return content; } // Helper to read resource and check for text/plain list content async function checkTextListResponse(uri: string, expectedSubstrings: string[]): Promise<string> { const content = (await readResourceAndCheck(uri)) as FormattedResultItem; expect(content.mimeType).toBe('text/plain'); // expect(content.isError).toBeFalsy(); // Removed as SDK might strip this property if (!hasTextContent(content)) throw new Error('Expected text content'); for (const sub of expectedSubstrings) { expect(content.text).toContain(sub); } return content.text; } // Helper to read resource and check for JSON detail content async function checkJsonDetailResponse(uri: string, expectedObject: object): Promise<unknown> { const content = (await readResourceAndCheck(uri)) as FormattedResultItem; expect(content.mimeType).toBe('application/json'); // expect(content.isError).toBeFalsy(); // Removed as SDK might strip this property if (!hasTextContent(content)) throw new Error('Expected text content'); const data = parseJsonSafely(content.text); expect(data).toMatchObject(expectedObject); return data; } // Helper to read resource and check for error async function checkErrorResponse(uri: string, expectedErrorText: string): Promise<void> { const content = (await readResourceAndCheck(uri)) as FormattedResultItem; // expect(content.isError).toBe(true); // Removed as SDK might strip this property expect(content.mimeType).toBe('text/plain'); // Errors are plain text if (!hasTextContent(content)) throw new Error('Expected text content for error'); expect(content.text).toContain(expectedErrorText); } describe('openapi://{field}', () => { beforeEach(async () => await setup()); it('should retrieve the "info" field', async () => { // Matches complex-endpoint.json await checkJsonDetailResponse('openapi://info', { title: 'Complex Endpoint Test API', version: '1.0.0', }); }); it('should retrieve the "paths" list', async () => { // Matches complex-endpoint.json await checkTextListResponse('openapi://paths', [ 'Hint:', 'GET POST /api/v1/organizations/{orgId}/projects/{projectId}/tasks', ]); }); it('should retrieve the "components" list', async () => { // Matches complex-endpoint.json (only has schemas) await checkTextListResponse('openapi://components', [ 'Available Component Types:', '- schemas', "Hint: Use 'openapi://components/{type}'", ]); }); it('should return error for invalid field', async () => { const uri = 'openapi://invalidfield'; await checkErrorResponse(uri, 'Field "invalidfield" not found'); }); }); describe('openapi://paths/{path}', () => { beforeEach(async () => await setup()); it('should list methods for the complex task path', async () => { const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks'; const encodedPath = encodeURIComponent(complexPath); // Update expected format based on METHOD: Summary/OpId await checkTextListResponse(`openapi://paths/${encodedPath}`, [ "Hint: Use 'openapi://paths/api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks/{method}'", // Hint comes first now '', // Blank line after hint 'GET: Get Tasks', // METHOD: summary 'POST: Create Task', // METHOD: summary ]); }); it('should return error for non-existent path', async () => { const encodedPath = encodeURIComponent('nonexistent'); const uri = `openapi://paths/${encodedPath}`; // Updated error message from getValidatedPathItem await checkErrorResponse(uri, 'Path "/nonexistent" not found in the specification.'); }); }); describe('openapi://paths/{path}/{method*}', () => { beforeEach(async () => await setup()); it('should get details for GET on complex path', async () => { const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks'; const encodedPath = encodeURIComponent(complexPath); // Check operationId from complex-endpoint.json await checkJsonDetailResponse(`openapi://paths/${encodedPath}/get`, { operationId: 'getProjectTasks', }); }); it('should get details for multiple methods GET,POST on complex path', async () => { const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks'; const encodedPath = encodeURIComponent(complexPath); const result = await client.readResource({ uri: `openapi://paths/${encodedPath}/get,post` }); expect(result.contents).toHaveLength(2); const getContent = result.contents.find(c => c.uri.endsWith('/get')) as | FormattedResultItem | undefined; expect(getContent).toBeDefined(); // expect(getContent?.isError).toBeFalsy(); // Removed as SDK might strip this property if (!getContent || !hasTextContent(getContent)) throw new Error('Expected text content for GET'); const getData = parseJsonSafely(getContent.text); // Check operationId from complex-endpoint.json expect(getData).toMatchObject({ operationId: 'getProjectTasks' }); const postContent = result.contents.find(c => c.uri.endsWith('/post')) as | FormattedResultItem | undefined; expect(postContent).toBeDefined(); // expect(postContent?.isError).toBeFalsy(); // Removed as SDK might strip this property if (!postContent || !hasTextContent(postContent)) throw new Error('Expected text content for POST'); const postData = parseJsonSafely(postContent.text); // Check operationId from complex-endpoint.json expect(postData).toMatchObject({ operationId: 'createProjectTask' }); }); it('should return error for invalid method on complex path', async () => { const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks'; const encodedPath = encodeURIComponent(complexPath); const uri = `openapi://paths/${encodedPath}/put`; // Updated error message from getValidatedOperations await checkErrorResponse( uri, 'None of the requested methods (put) are valid for path "/api/v1/organizations/{orgId}/projects/{projectId}/tasks". Available methods: get, post' ); }); }); describe('openapi://components/{type}', () => { beforeEach(async () => await setup()); it('should list schemas', async () => { // Matches complex-endpoint.json await checkTextListResponse('openapi://components/schemas', [ 'Available schemas:', '- CreateTaskRequest', '- Task', '- TaskList', "Hint: Use 'openapi://components/schemas/{name}'", ]); }); it('should return error for invalid type', async () => { const uri = 'openapi://components/invalid'; await checkErrorResponse(uri, 'Invalid component type: invalid'); }); }); describe('openapi://components/{type}/{name*}', () => { beforeEach(async () => await setup()); it('should get details for schema Task', async () => { // Matches complex-endpoint.json await checkJsonDetailResponse('openapi://components/schemas/Task', { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' } }, }); }); it('should get details for multiple schemas Task,TaskList', async () => { // Matches complex-endpoint.json const result = await client.readResource({ uri: 'openapi://components/schemas/Task,TaskList', }); expect(result.contents).toHaveLength(2); const taskContent = result.contents.find(c => c.uri.endsWith('/Task')) as | FormattedResultItem | undefined; expect(taskContent).toBeDefined(); // expect(taskContent?.isError).toBeFalsy(); // Removed as SDK might strip this property if (!taskContent || !hasTextContent(taskContent)) throw new Error('Expected text content for Task'); const taskData = parseJsonSafely(taskContent.text); expect(taskData).toMatchObject({ properties: { id: { type: 'string' } } }); const taskListContent = result.contents.find(c => c.uri.endsWith('/TaskList')) as | FormattedResultItem | undefined; expect(taskListContent).toBeDefined(); // expect(taskListContent?.isError).toBeFalsy(); // Removed as SDK might strip this property if (!taskListContent || !hasTextContent(taskListContent)) throw new Error('Expected text content for TaskList'); const taskListData = parseJsonSafely(taskListContent.text); expect(taskListData).toMatchObject({ properties: { items: { type: 'array' } } }); }); it('should return error for invalid name', async () => { const uri = 'openapi://components/schemas/InvalidSchemaName'; // Updated error message from getValidatedComponentDetails with sorted names await checkErrorResponse( uri, 'None of the requested names (InvalidSchemaName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList' ); }); }); // Removed ListResourceTemplates test suite as the 'complete' property // is likely not part of the standard response payload. // We assume the templates are registered correctly in src/index.ts. describe('Completion Tests', () => { beforeEach(async () => await setup()); // Use the same setup it('should provide completions for {field}', async () => { const params = { argument: { name: 'field', value: '' }, // Empty value to get all ref: { type: 'ref/resource' as const, uri: 'openapi://{field}' }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); expect(result.completion.values).toEqual( expect.arrayContaining(['openapi', 'info', 'paths', 'components']) // Based on complex-endpoint.json ); expect(result.completion.values).toHaveLength(4); }); it('should provide completions for {path}', async () => { const params = { argument: { name: 'path', value: '' }, // Empty value to get all ref: { type: 'ref/resource' as const, uri: 'openapi://paths/{path}' }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); // Check for the encoded path from complex-endpoint.json expect(result.completion.values).toEqual([ 'api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks', ]); }); it('should provide completions for {method*}', async () => { const params = { argument: { name: 'method', value: '' }, // Empty value to get all ref: { type: 'ref/resource' as const, uri: 'openapi://paths/{path}/{method*}', // Use the exact template URI }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); // Check for the static list of methods defined in src/index.ts expect(result.completion.values).toEqual([ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', ]); }); it('should provide completions for {type}', async () => { const params = { argument: { name: 'type', value: '' }, // Empty value to get all ref: { type: 'ref/resource' as const, uri: 'openapi://components/{type}' }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); // Check for component types in complex-endpoint.json expect(result.completion.values).toEqual(['schemas']); }); // Updated test for conditional name completion it('should provide completions for {name*} when only one component type exists', async () => { // complex-endpoint.json only has 'schemas' const params = { argument: { name: 'name', value: '' }, ref: { type: 'ref/resource' as const, uri: 'openapi://components/{type}/{name*}', // Use the exact template URI }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); // Expect schema names from complex-endpoint.json expect(result.completion.values).toEqual( expect.arrayContaining(['CreateTaskRequest', 'Task', 'TaskList']) ); expect(result.completion.values).toHaveLength(3); }); // New test for multiple component types it('should NOT provide completions for {name*} when multiple component types exist', async () => { // Need to restart the server with the multi-component spec await testContext?.cleanup(); // Clean up previous server const multiSpecPath = path.resolve(__dirname, '../../fixtures/multi-component-types.json'); await setup(multiSpecPath); // Restart server with new spec const params = { argument: { name: 'name', value: '' }, ref: { type: 'ref/resource' as const, uri: 'openapi://components/{type}/{name*}', // Use the exact template URI }, }; const result = await client.complete(params); expect(result.completion).toBeDefined(); // Expect empty array because multiple types (schemas, parameters) exist expect(result.completion.values).toEqual([]); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kadykov/mcp-openapi-schema-explorer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server