import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { TopLevelFieldHandler } from '../../../../src/handlers/top-level-field-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
import { FormattedResultItem } from '../../../../src/handlers/handler-utils';
// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
getSpec: jest.fn(), // Not used by this handler directly
getTransformedSpec: mockGetTransformedSpec,
};
const mockFormatter: IFormatter = new JsonFormatter(); // Use real formatter for structure check
// Sample Data
const sampleSpec: OpenAPIV3.Document = {
openapi: '3.0.3',
info: { title: 'Test API', version: '1.1.0' },
paths: { '/test': { get: { responses: { '200': { description: 'OK' } } } } },
components: { schemas: { Test: { type: 'string' } } },
servers: [{ url: 'http://example.com' }],
};
describe('TopLevelFieldHandler', () => {
let handler: TopLevelFieldHandler;
beforeEach(() => {
handler = new TopLevelFieldHandler(mockSpecLoader, mockFormatter);
mockGetTransformedSpec.mockReset(); // Reset mock before each test
});
it('should return the correct template', () => {
const template = handler.getTemplate();
expect(template).toBeInstanceOf(ResourceTemplate);
// Compare against the string representation of the UriTemplate object
expect(template.uriTemplate.toString()).toBe('openapi://{field}');
});
describe('handleRequest', () => {
const mockExtra = {
signal: new AbortController().signal,
sendNotification: jest.fn(),
sendRequest: jest.fn(),
requestId: 'test-request-id' as RequestId,
};
it('should handle request for "info" field', async () => {
mockGetTransformedSpec.mockResolvedValue(sampleSpec);
const variables: Variables = { field: 'info' };
const uri = new URL('openapi://info');
// Pass the mock extra object as the third argument
const result = await handler.handleRequest(uri, variables, mockExtra);
expect(mockGetTransformedSpec).toHaveBeenCalledWith({
resourceType: 'schema',
format: 'openapi',
});
expect(result.contents).toHaveLength(1);
expect(result.contents[0]).toEqual({
uri: 'openapi://info',
mimeType: 'application/json',
text: JSON.stringify(sampleSpec.info, null, 2),
isError: false,
});
});
it('should handle request for "servers" field', async () => {
mockGetTransformedSpec.mockResolvedValue(sampleSpec);
const variables: Variables = { field: 'servers' };
const uri = new URL('openapi://servers');
const result = await handler.handleRequest(uri, variables, mockExtra);
expect(result.contents).toHaveLength(1);
expect(result.contents[0]).toEqual({
uri: 'openapi://servers',
mimeType: 'application/json',
text: JSON.stringify(sampleSpec.servers, null, 2),
isError: false,
});
});
it('should handle request for "paths" field (list view)', async () => {
mockGetTransformedSpec.mockResolvedValue(sampleSpec);
const variables: Variables = { field: 'paths' };
const uri = new URL('openapi://paths');
const result = await handler.handleRequest(uri, variables, mockExtra);
expect(result.contents).toHaveLength(1);
const content = result.contents[0] as FormattedResultItem;
expect(content.uri).toBe('openapi://paths');
expect(content.mimeType).toBe('text/plain');
expect(content.isError).toBe(false);
expect(content.text).toContain('GET /test'); // Check content format
// Check that the hint contains the essential URI patterns
expect(content.text).toContain('Hint:');
expect(content.text).toContain('openapi://paths/{encoded_path}');
expect(content.text).toContain('openapi://paths/{encoded_path}/{method}');
});
it('should handle request for "components" field (list view)', async () => {
mockGetTransformedSpec.mockResolvedValue(sampleSpec);
const variables: Variables = { field: 'components' };
const uri = new URL('openapi://components');
const result = await handler.handleRequest(uri, variables, mockExtra);
expect(result.contents).toHaveLength(1);
const content = result.contents[0] as FormattedResultItem;
expect(content.uri).toBe('openapi://components');
expect(content.mimeType).toBe('text/plain');
expect(content.isError).toBe(false);
expect(content.text).toContain('- schemas'); // Check content format
expect(content.text).toContain("Hint: Use 'openapi://components/{type}'");
});
it('should return error for non-existent field', async () => {
mockGetTransformedSpec.mockResolvedValue(sampleSpec);
const variables: Variables = { field: 'nonexistent' };
const uri = new URL('openapi://nonexistent');
const result = await handler.handleRequest(uri, variables, mockExtra);
expect(result.contents).toHaveLength(1);
expect(result.contents[0]).toEqual({
uri: 'openapi://nonexistent',
mimeType: 'text/plain',
text: 'Error: Field "nonexistent" not found in the OpenAPI document.',
isError: true,
});
});
it('should handle spec loading errors', async () => {
const error = new Error('Failed to load spec');
mockGetTransformedSpec.mockRejectedValue(error);
const variables: Variables = { field: 'info' };
const uri = new URL('openapi://info');
// Match the core error message using RegExp
const expectedLogMessage = /Failed to load spec/;
// Use the helper, letting TypeScript infer the return type
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
handler.handleRequest(uri, variables, mockExtra)
);
expect(result.contents).toHaveLength(1);
expect(result.contents[0]).toEqual({
uri: 'openapi://info',
mimeType: 'text/plain',
text: 'Failed to load spec',
isError: true,
});
});
it('should handle non-OpenAPI v3 spec', async () => {
const invalidSpec = { swagger: '2.0', info: {} }; // Not OpenAPI v3
mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
const variables: Variables = { field: 'info' };
const uri = new URL('openapi://info');
// Match the core error message using RegExp
const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
// Use the helper, letting TypeScript infer the return type
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
handler.handleRequest(uri, variables, mockExtra)
);
expect(result.contents).toHaveLength(1);
expect(result.contents[0]).toEqual({
uri: 'openapi://info',
mimeType: 'text/plain',
text: 'Only OpenAPI v3 specifications are supported',
isError: true,
});
});
});
});