mcp-openapi-schema-explorer
Version:
MCP OpenAPI schema explorer
190 lines • 10.2 kB
JavaScript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; // Import ResourceTemplate
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { loadConfig } from './config.js';
// Import new handlers
import { TopLevelFieldHandler } from './handlers/top-level-field-handler.js';
import { PathItemHandler } from './handlers/path-item-handler.js';
import { OperationHandler } from './handlers/operation-handler.js';
import { ComponentMapHandler } from './handlers/component-map-handler.js';
import { ComponentDetailHandler } from './handlers/component-detail-handler.js';
import { OpenAPITransformer, ReferenceTransformService } from './services/reference-transform.js';
import { SpecLoaderService } from './services/spec-loader.js';
import { createFormatter } from './services/formatters.js';
import { encodeUriPathComponent } from './utils/uri-builder.js'; // Import specific function
import { isOpenAPIV3, getValidatedComponentMap } from './handlers/handler-utils.js'; // Import type guard and helper
import { VERSION } from './version.js'; // Import the generated version
async function main() {
try {
// Get spec path and options from command line arguments
const [, , specPath, ...args] = process.argv;
const options = {
outputFormat: args.includes('--output-format')
? args[args.indexOf('--output-format') + 1]
: undefined,
};
// Load configuration
const config = loadConfig(specPath, options);
// Initialize services
const referenceTransform = new ReferenceTransformService();
referenceTransform.registerTransformer('openapi', new OpenAPITransformer());
const specLoader = new SpecLoaderService(config.specPath, referenceTransform);
await specLoader.loadSpec();
// Get the loaded spec to extract the title
const spec = await specLoader.getSpec(); // Rename back to spec
// Get the transformed spec for use in completions
const transformedSpec = await specLoader.getTransformedSpec({
resourceType: 'schema', // Use a default context
format: 'openapi',
});
const defaultServerName = 'OpenAPI Schema Explorer';
// Use original spec for title
const serverName = spec.info?.title
? `Schema Explorer for ${spec.info.title}`
: defaultServerName;
// Brief help content for LLMs
const helpContent = `Use resorces/templates/list to get a list of available resources. Use openapi://paths to get a list of all endpoints.`;
// Create MCP server with dynamic name
const server = new McpServer({
name: serverName,
version: VERSION, // Use the imported version
}, {
instructions: helpContent,
});
// Set up formatter and new handlers
const formatter = createFormatter(config.outputFormat);
const topLevelFieldHandler = new TopLevelFieldHandler(specLoader, formatter);
const pathItemHandler = new PathItemHandler(specLoader, formatter);
const operationHandler = new OperationHandler(specLoader, formatter);
const componentMapHandler = new ComponentMapHandler(specLoader, formatter);
const componentDetailHandler = new ComponentDetailHandler(specLoader, formatter);
// --- Define Resource Templates and Register Handlers ---
// Helper to get dynamic field list for descriptions
const getFieldList = () => Object.keys(transformedSpec).join(', ');
// Helper to get dynamic component type list for descriptions
const getComponentTypeList = () => {
if (isOpenAPIV3(transformedSpec) && transformedSpec.components) {
return Object.keys(transformedSpec.components).join(', ');
}
return ''; // Return empty if no components or not V3
};
// 1. openapi://{field}
const fieldTemplate = new ResourceTemplate('openapi://{field}', {
list: undefined, // List is handled by the handler logic based on field value
complete: {
field: () => Object.keys(transformedSpec), // Use transformedSpec
},
});
server.resource('openapi-field', // Unique ID for the resource registration
fieldTemplate, {
// MimeType varies (text/plain for lists, JSON/YAML for details)
description: `Access top-level fields like ${getFieldList()}. (e.g., openapi://info)`,
title: 'OpenAPI Field/List', // Generic name
}, topLevelFieldHandler.handleRequest);
// 2. openapi://paths/{path}
const pathTemplate = new ResourceTemplate('openapi://paths/{path}', {
list: undefined, // List is handled by the handler
complete: {
path: () => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
},
});
server.resource('openapi-path-methods', pathTemplate, {
mimeType: 'text/plain', // This always returns a list
description: 'List methods for a specific path (URL encode paths with slashes). (e.g., openapi://paths/users%2F%7Bid%7D)',
title: 'Path Methods List',
}, pathItemHandler.handleRequest);
// 3. openapi://paths/{path}/{method*}
const operationTemplate = new ResourceTemplate('openapi://paths/{path}/{method*}', {
list: undefined, // Detail view handled by handler
complete: {
path: () => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
method: () => [
// Provide static list of common methods
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'OPTIONS',
'HEAD',
'TRACE',
],
},
});
server.resource('openapi-operation-detail', operationTemplate, {
mimeType: formatter.getMimeType(), // Detail view uses formatter
description: 'Get details for one or more operations (comma-separated). (e.g., openapi://paths/users%2F%7Bid%7D/get,post)',
title: 'Operation Detail',
}, operationHandler.handleRequest);
// 4. openapi://components/{type}
const componentMapTemplate = new ResourceTemplate('openapi://components/{type}', {
list: undefined, // List is handled by the handler
complete: {
type: () => {
// Use type guard to ensure spec is V3 before accessing components
if (isOpenAPIV3(transformedSpec)) {
return Object.keys(transformedSpec.components ?? {});
}
return []; // Return empty array if not V3 (shouldn't happen ideally)
},
},
});
server.resource('openapi-component-list', componentMapTemplate, {
mimeType: 'text/plain', // This always returns a list
description: `List components of a specific type like ${getComponentTypeList()}. (e.g., openapi://components/schemas)`,
title: 'Component List',
}, componentMapHandler.handleRequest);
// 5. openapi://components/{type}/{name*}
const componentDetailTemplate = new ResourceTemplate('openapi://components/{type}/{name*}', {
list: undefined, // Detail view handled by handler
complete: {
type: () => {
// Use type guard to ensure spec is V3 before accessing components
if (isOpenAPIV3(transformedSpec)) {
return Object.keys(transformedSpec.components ?? {});
}
return []; // Return empty array if not V3
},
name: () => {
// Provide names only if there's exactly one component type defined
if (isOpenAPIV3(transformedSpec) &&
transformedSpec.components &&
Object.keys(transformedSpec.components).length === 1) {
// Get the single component type key (e.g., 'schemas')
const componentTypeKey = Object.keys(transformedSpec.components)[0];
// Use the helper to safely get the map
try {
const componentTypeMap = getValidatedComponentMap(transformedSpec, componentTypeKey);
return Object.keys(componentTypeMap);
}
catch (error) {
// Should not happen if key came from Object.keys, but handle defensively
console.error(`Error getting component map for key ${componentTypeKey}:`, error);
return [];
}
}
// Otherwise, return no completions for name
return [];
},
},
});
server.resource('openapi-component-detail', componentDetailTemplate, {
mimeType: formatter.getMimeType(), // Detail view uses formatter
description: 'Get details for one or more components (comma-separated). (e.g., openapi://components/schemas/User,Task)',
title: 'Component Detail',
}, componentDetailHandler.handleRequest);
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
}
catch (error) {
console.error('Failed to start server:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Run the server
main().catch(error => {
console.error('Unhandled error:', error instanceof Error ? error.message : String(error));
process.exit(1);
});
//# sourceMappingURL=index.js.map