Skip to main content
Glama
openapi-validator.js27.9 kB
/** * OpenAPI/REST API Validator * Validates OpenAPI specifications and REST API designs for best practices and compliance */ import { promisify } from 'util'; import { exec } from 'child_process'; import { existsSync, readFileSync } from 'fs'; import { join, extname } from 'path'; const execAsync = promisify(exec); /** * OpenAPI/REST API Validator Tool */ export const openApiValidatorTool = { name: 'openapi_rest_validator', description: 'Validates OpenAPI specifications and REST API designs for compliance, security, and best practices.', inputSchema: { type: 'object', properties: { spec: { type: 'string', description: 'OpenAPI specification content (JSON or YAML)', }, specPath: { type: 'string', description: 'Path to OpenAPI specification file', }, version: { type: 'string', enum: ['2.0', '3.0', '3.1', 'auto'], default: 'auto', description: 'OpenAPI specification version', }, validationType: { type: 'string', enum: ['syntax', 'semantic', 'security', 'best-practices', 'all'], default: 'all', description: 'Type of validation to perform', }, strictness: { type: 'string', enum: ['minimal', 'standard', 'strict'], default: 'standard', description: 'Validation strictness level', }, includeExamples: { type: 'boolean', default: true, description: 'Validate examples in the specification', }, checkSecurity: { type: 'boolean', default: true, description: 'Perform security-focused validation', }, restCompliance: { type: 'boolean', default: true, description: 'Check REST API design compliance', }, limit: { type: 'number', description: 'Maximum number of issues to return (default: 50)', default: 50, minimum: 1, maximum: 500, }, offset: { type: 'number', description: 'Starting index for pagination (default: 0)', default: 0, minimum: 0, }, }, required: [], }, }; /** * Handle OpenAPI/REST API validation */ export async function handleOpenApiValidator(request) { try { const { spec, specPath, version = 'auto', validationType = 'all', strictness = 'standard', includeExamples = true, checkSecurity = true, restCompliance = true, limit = 50, offset = 0, } = request.params; let specContent = spec; let filename = 'openapi.yaml'; // Load spec from file if path provided if (specPath) { if (!existsSync(specPath)) { return { content: [{ type: 'text', text: `❌ OpenAPI specification file not found: ${specPath}`, }], }; } specContent = readFileSync(specPath, 'utf8'); filename = specPath; } if (!specContent) { return { content: [{ type: 'text', text: '❌ No OpenAPI specification provided', }], }; } // Parse the specification let parsedSpec; try { parsedSpec = parseOpenApiSpec(specContent, filename); } catch (parseError) { return { content: [{ type: 'text', text: `❌ Failed to parse OpenAPI specification: ${parseError.message}`, }], }; } // Detect version if auto const detectedVersion = version === 'auto' ? detectOpenApiVersion(parsedSpec) : version; const analysis = { version: detectedVersion, filename, issues: [], metrics: { totalPaths: 0, totalOperations: 0, totalSchemas: 0, securitySchemes: 0, documentedOperations: 0, validExamples: 0, totalExamples: 0, }, security: { hasAuth: false, vulnerabilities: [], recommendations: [], }, restCompliance: { score: 100, violations: [], }, }; // Perform different types of validation if (validationType === 'syntax' || validationType === 'all') { await validateSyntax(parsedSpec, analysis, { strictness }); } if (validationType === 'semantic' || validationType === 'all') { await validateSemantics(parsedSpec, analysis, { strictness, includeExamples }); } if ((validationType === 'security' || validationType === 'all') && checkSecurity) { await validateSecurity(parsedSpec, analysis, { strictness }); } if ((validationType === 'best-practices' || validationType === 'all') && restCompliance) { await validateRestCompliance(parsedSpec, analysis, { strictness }); } // Filter and paginate issues const filteredIssues = analysis.issues.slice(offset, offset + limit); // Calculate overall score const overallScore = calculateApiScore(analysis); // Build response const response = { validation: { version: detectedVersion, type: validationType, strictness, filename, score: overallScore, }, metrics: analysis.metrics, issues: filteredIssues, security: checkSecurity ? analysis.security : undefined, restCompliance: restCompliance ? analysis.restCompliance : undefined, summary: { totalIssues: analysis.issues.length, critical: analysis.issues.filter(i => i.severity === 'critical').length, high: analysis.issues.filter(i => i.severity === 'high').length, medium: analysis.issues.filter(i => i.severity === 'medium').length, low: analysis.issues.filter(i => i.severity === 'low').length, }, pagination: { offset, limit, total: analysis.issues.length, hasMore: offset + limit < analysis.issues.length, }, }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2), }], }; } catch (error) { console.error('[OpenApiValidator] Error:', error); return { content: [{ type: 'text', text: `❌ OpenAPI validation failed: ${error.message}`, }], }; } } /** * Parse OpenAPI specification (JSON or YAML) */ function parseOpenApiSpec(content, filename) { const isYaml = filename.endsWith('.yaml') || filename.endsWith('.yml') || content.trim().startsWith('openapi:') || content.trim().startsWith('swagger:'); if (isYaml) { // Simple YAML parsing for OpenAPI (in production, use a proper YAML parser) return parseSimpleYaml(content); } else { return JSON.parse(content); } } /** * Simple YAML parser for OpenAPI specs */ function parseSimpleYaml(yamlContent) { // This is a simplified YAML parser for demonstration // In production, use a proper YAML library like 'js-yaml' const lines = yamlContent.split('\n'); const result = {}; const stack = [result]; let currentIndent = 0; for (const line of lines) { if (!line.trim() || line.trim().startsWith('#')) continue; const indent = line.match(/^\s*/)[0].length; const content = line.trim(); if (content.includes(':')) { const [key, value] = content.split(':', 2); const cleanKey = key.trim(); const cleanValue = value?.trim() || ''; // Adjust stack based on indentation while (stack.length > 1 && indent <= currentIndent) { stack.pop(); currentIndent -= 2; } const current = stack[stack.length - 1]; if (cleanValue) { // Try to parse as number or boolean if (cleanValue === 'true') current[cleanKey] = true; else if (cleanValue === 'false') current[cleanKey] = false; else if (!isNaN(cleanValue)) current[cleanKey] = Number(cleanValue); else current[cleanKey] = cleanValue.replace(/^['"]|['"]$/g, ''); } else { // Object or array current[cleanKey] = {}; stack.push(current[cleanKey]); currentIndent = indent; } } } return result; } /** * Detect OpenAPI version from spec */ function detectOpenApiVersion(spec) { if (spec.openapi) { return spec.openapi.startsWith('3.1') ? '3.1' : '3.0'; } else if (spec.swagger) { return '2.0'; } return '3.0'; // default assumption } /** * Validate OpenAPI syntax */ async function validateSyntax(spec, analysis, options) { // Check required root properties if (!spec.openapi && !spec.swagger) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: 'Missing required "openapi" or "swagger" version field', path: '/', recommendation: 'Add version field (e.g., "openapi": "3.0.0")', }); } if (!spec.info) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: 'Missing required "info" object', path: '/', recommendation: 'Add info object with title and version', }); } else { if (!spec.info.title) { analysis.issues.push({ type: 'syntax', severity: 'high', message: 'Missing required "title" in info object', path: '/info', recommendation: 'Add descriptive title for the API', }); } if (!spec.info.version) { analysis.issues.push({ type: 'syntax', severity: 'high', message: 'Missing required "version" in info object', path: '/info', recommendation: 'Add version field (e.g., "1.0.0")', }); } } if (!spec.paths) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: 'Missing required "paths" object', path: '/', recommendation: 'Add paths object with API endpoints', }); return; } // Validate paths structure validatePathsStructure(spec.paths, analysis, options); // Update metrics analysis.metrics.totalPaths = Object.keys(spec.paths || {}).length; analysis.metrics.totalSchemas = Object.keys(spec.components?.schemas || spec.definitions || {}).length; } /** * Validate paths structure */ function validatePathsStructure(paths, analysis, options) { let totalOperations = 0; let documentedOperations = 0; for (const [path, pathItem] of Object.entries(paths)) { // Validate path format if (!path.startsWith('/')) { analysis.issues.push({ type: 'syntax', severity: 'high', message: `Path "${path}" must start with "/"`, path: `/paths/${path}`, recommendation: 'Ensure all paths start with forward slash', }); } // Check for path parameters format const pathParams = path.match(/{[^}]+}/g) || []; pathParams.forEach(param => { if (!param.match(/^{[a-zA-Z_][a-zA-Z0-9_]*}$/)) { analysis.issues.push({ type: 'syntax', severity: 'medium', message: `Invalid path parameter format: ${param}`, path: `/paths/${path}`, recommendation: 'Use valid parameter names (alphanumeric and underscore only)', }); } }); // Validate operations const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace']; for (const [method, operation] of Object.entries(pathItem)) { if (httpMethods.includes(method.toLowerCase())) { totalOperations++; if (operation.summary || operation.description) { documentedOperations++; } else if (options.strictness !== 'minimal') { analysis.issues.push({ type: 'documentation', severity: 'medium', message: `Operation ${method.toUpperCase()} ${path} missing summary/description`, path: `/paths/${path}/${method}`, recommendation: 'Add summary or description to document the operation', }); } // Validate operation structure validateOperation(path, method, operation, analysis, options); } } } analysis.metrics.totalOperations = totalOperations; analysis.metrics.documentedOperations = documentedOperations; } /** * Validate individual operation */ function validateOperation(path, method, operation, analysis, options) { // Check for operationId uniqueness (would need to track globally) if (!operation.operationId && options.strictness === 'strict') { analysis.issues.push({ type: 'best-practices', severity: 'low', message: `Operation ${method.toUpperCase()} ${path} missing operationId`, path: `/paths/${path}/${method}`, recommendation: 'Add unique operationId for better code generation', }); } // Validate responses if (!operation.responses) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: `Operation ${method.toUpperCase()} ${path} missing responses`, path: `/paths/${path}/${method}`, recommendation: 'Add responses object with at least success response', }); } else { validateResponses(path, method, operation.responses, analysis, options); } // Validate parameters if (operation.parameters) { validateParameters(path, method, operation.parameters, analysis, options); } // Check for request body in appropriate methods if (['post', 'put', 'patch'].includes(method.toLowerCase()) && !operation.requestBody && !operation.parameters?.some(p => p.in === 'body')) { if (options.strictness !== 'minimal') { analysis.issues.push({ type: 'best-practices', severity: 'medium', message: `${method.toUpperCase()} operation typically requires request body`, path: `/paths/${path}/${method}`, recommendation: 'Consider adding requestBody for data modification operations', }); } } } /** * Validate responses */ function validateResponses(path, method, responses, analysis, options) { const statusCodes = Object.keys(responses); // Check for success response const hasSuccess = statusCodes.some(code => code.startsWith('2') || code === 'default'); if (!hasSuccess) { analysis.issues.push({ type: 'best-practices', severity: 'high', message: `Operation ${method.toUpperCase()} ${path} missing success response`, path: `/paths/${path}/${method}/responses`, recommendation: 'Add at least one 2xx success response', }); } // Check for error responses const hasErrorResponse = statusCodes.some(code => code.startsWith('4') || code.startsWith('5')); if (!hasErrorResponse && options.strictness !== 'minimal') { analysis.issues.push({ type: 'best-practices', severity: 'low', message: `Operation ${method.toUpperCase()} ${path} missing error responses`, path: `/paths/${path}/${method}/responses`, recommendation: 'Add common error responses (400, 401, 404, 500)', }); } // Validate individual responses for (const [statusCode, response] of Object.entries(responses)) { if (!response.description && statusCode !== 'default') { analysis.issues.push({ type: 'syntax', severity: 'medium', message: `Response ${statusCode} missing required description`, path: `/paths/${path}/${method}/responses/${statusCode}`, recommendation: 'Add description explaining when this response occurs', }); } } } /** * Validate parameters */ function validateParameters(path, method, parameters, analysis, options) { const paramNames = new Set(); parameters.forEach((param, index) => { // Check for duplicate parameter names const paramKey = `${param.name}-${param.in}`; if (paramNames.has(paramKey)) { analysis.issues.push({ type: 'syntax', severity: 'high', message: `Duplicate parameter "${param.name}" in ${param.in}`, path: `/paths/${path}/${method}/parameters/${index}`, recommendation: 'Ensure parameter names are unique within their location', }); } paramNames.add(paramKey); // Validate required fields if (!param.name) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: 'Parameter missing required "name" field', path: `/paths/${path}/${method}/parameters/${index}`, recommendation: 'Add name field to parameter', }); } if (!param.in) { analysis.issues.push({ type: 'syntax', severity: 'critical', message: 'Parameter missing required "in" field', path: `/paths/${path}/${method}/parameters/${index}`, recommendation: 'Add "in" field (query, header, path, cookie)', }); } if (param.in === 'path' && !param.required) { analysis.issues.push({ type: 'syntax', severity: 'high', message: `Path parameter "${param.name}" must be required`, path: `/paths/${path}/${method}/parameters/${index}`, recommendation: 'Set required: true for path parameters', }); } }); } /** * Validate semantic aspects */ async function validateSemantics(spec, analysis, options) { // Check for consistent naming conventions validateNamingConventions(spec, analysis); // Validate examples if present if (options.includeExamples) { validateExamples(spec, analysis); } // Check for proper HTTP status code usage validateHttpStatusCodes(spec, analysis); // Validate schema references validateSchemaReferences(spec, analysis); } /** * Validate naming conventions */ function validateNamingConventions(spec, analysis) { // Check path naming (kebab-case is common) for (const path of Object.keys(spec.paths || {})) { const segments = path.split('/').filter(s => s && !s.startsWith('{')); segments.forEach(segment => { if (segment.includes('_') || /[A-Z]/.test(segment)) { analysis.issues.push({ type: 'best-practices', severity: 'low', message: `Path segment "${segment}" should use kebab-case`, path: `/paths/${path}`, recommendation: 'Use kebab-case for path segments (e.g., /user-profiles)', }); } }); } // Check schema property naming (camelCase is common) const schemas = spec.components?.schemas || spec.definitions || {}; for (const [schemaName, schema] of Object.entries(schemas)) { if (schema.properties) { for (const propName of Object.keys(schema.properties)) { if (propName.includes('-') || propName.includes('_')) { analysis.issues.push({ type: 'best-practices', severity: 'low', message: `Property "${propName}" should use camelCase`, path: `/components/schemas/${schemaName}/properties/${propName}`, recommendation: 'Use camelCase for property names (e.g., firstName)', }); } } } } } /** * Validate examples */ function validateExamples(spec, analysis) { // This would validate examples against schemas // For now, just count examples let totalExamples = 0; let validExamples = 0; // Count examples in schemas const schemas = spec.components?.schemas || spec.definitions || {}; for (const schema of Object.values(schemas)) { if (schema.example) { totalExamples++; validExamples++; // Assume valid for now } } analysis.metrics.totalExamples = totalExamples; analysis.metrics.validExamples = validExamples; } /** * Validate HTTP status codes */ function validateHttpStatusCodes(spec, analysis) { for (const [path, pathItem] of Object.entries(spec.paths || {})) { for (const [method, operation] of Object.entries(pathItem)) { if (!operation.responses) continue; for (const statusCode of Object.keys(operation.responses)) { if (statusCode === 'default') continue; // Check for invalid status codes if (!/^[1-5]\d{2}$/.test(statusCode) && statusCode !== 'default') { analysis.issues.push({ type: 'semantic', severity: 'medium', message: `Invalid HTTP status code: ${statusCode}`, path: `/paths/${path}/${method}/responses/${statusCode}`, recommendation: 'Use valid HTTP status codes (100-599)', }); } // Check for inappropriate status codes for methods const methodUpper = method.toUpperCase(); if (methodUpper === 'POST' && statusCode === '200') { analysis.issues.push({ type: 'best-practices', severity: 'low', message: 'POST operations typically return 201 for resource creation', path: `/paths/${path}/${method}/responses/${statusCode}`, recommendation: 'Consider using 201 Created for successful POST operations', }); } } } } } /** * Validate schema references */ function validateSchemaReferences(spec, analysis) { const schemas = new Set(Object.keys(spec.components?.schemas || spec.definitions || {})); // Simple reference validation (would need recursive traversal in production) const specString = JSON.stringify(spec); const references = specString.match(/"\$ref":\s*"[^"]+"/g) || []; references.forEach(ref => { const refPath = ref.match(/"([^"]+)"/)[1]; if (refPath.includes('#/components/schemas/') || refPath.includes('#/definitions/')) { const schemaName = refPath.split('/').pop(); if (!schemas.has(schemaName)) { analysis.issues.push({ type: 'semantic', severity: 'high', message: `Reference to undefined schema: ${schemaName}`, path: 'various', recommendation: `Define schema "${schemaName}" or fix reference`, }); } } }); } /** * Validate security aspects */ async function validateSecurity(spec, analysis, options) { analysis.security.hasAuth = !!(spec.components?.securitySchemes || spec.securityDefinitions); analysis.metrics.securitySchemes = Object.keys(spec.components?.securitySchemes || spec.securityDefinitions || {}).length; // Check for global security if (!spec.security && !analysis.security.hasAuth) { analysis.security.vulnerabilities.push({ type: 'missing_security', severity: 'critical', message: 'API has no security schemes defined', recommendation: 'Add authentication/authorization mechanisms', }); } // Check for HTTPS requirement if (spec.schemes && spec.schemes.includes('http') && !spec.schemes.includes('https')) { analysis.security.vulnerabilities.push({ type: 'insecure_transport', severity: 'high', message: 'API allows HTTP connections', recommendation: 'Require HTTPS for all connections', }); } // Check for sensitive data in examples const specString = JSON.stringify(spec).toLowerCase(); const sensitivePatterns = ['password', 'token', 'key', 'secret']; sensitivePatterns.forEach(pattern => { if (specString.includes(`"${pattern}":`)) { analysis.security.vulnerabilities.push({ type: 'sensitive_data_exposure', severity: 'medium', message: `Possible sensitive data in specification: ${pattern}`, recommendation: 'Avoid including sensitive data in API specifications', }); } }); // Security recommendations if (analysis.metrics.securitySchemes === 0) { analysis.security.recommendations.push('Implement authentication (OAuth 2.0, API keys, etc.)'); } analysis.security.recommendations.push('Use HTTPS for all API communications'); analysis.security.recommendations.push('Implement proper authorization checks'); analysis.security.recommendations.push('Validate all input parameters'); } /** * Validate REST compliance */ async function validateRestCompliance(spec, analysis, options) { let score = 100; const violations = []; for (const [path, pathItem] of Object.entries(spec.paths || {})) { for (const [method, operation] of Object.entries(pathItem)) { const methodUpper = method.toUpperCase(); // Check for proper HTTP method usage if (methodUpper === 'GET' && operation.requestBody) { violations.push({ type: 'http_method_misuse', severity: 'medium', message: `GET ${path} should not have request body`, path: `/paths/${path}/${method}`, recommendation: 'Use query parameters for GET requests', }); score -= 5; } if (['DELETE', 'GET', 'HEAD'].includes(methodUpper) && operation.requestBody) { violations.push({ type: 'http_method_misuse', severity: 'medium', message: `${methodUpper} ${path} should not have request body`, path: `/paths/${path}/${method}`, recommendation: `${methodUpper} operations should not include request bodies`, }); score -= 5; } // Check for proper resource naming if (path.includes('/get') || path.includes('/post') || path.includes('/delete')) { violations.push({ type: 'resource_naming', severity: 'low', message: `Path "${path}" includes HTTP method in URL`, path: `/paths/${path}`, recommendation: 'Use HTTP methods instead of including them in the path', }); score -= 2; } // Check for proper status codes if (operation.responses) { const statusCodes = Object.keys(operation.responses); if (methodUpper === 'POST' && !statusCodes.includes('201') && statusCodes.includes('200')) { violations.push({ type: 'status_code_usage', severity: 'low', message: `POST ${path} returns 200 instead of 201`, path: `/paths/${path}/${method}/responses`, recommendation: 'Use 201 Created for successful resource creation', }); score -= 2; } if (methodUpper === 'DELETE' && statusCodes.includes('200') && !statusCodes.includes('204')) { violations.push({ type: 'status_code_usage', severity: 'low', message: `DELETE ${path} could return 204 No Content`, path: `/paths/${path}/${method}/responses`, recommendation: 'Consider 204 No Content for successful DELETE operations', }); score -= 1; } } } // Check for proper resource hierarchy const pathSegments = path.split('/').filter(s => s); if (pathSegments.length > 6) { violations.push({ type: 'resource_hierarchy', severity: 'low', message: `Path "${path}" is deeply nested`, path: `/paths/${path}`, recommendation: 'Consider flattening resource hierarchy', }); score -= 1; } } analysis.restCompliance.score = Math.max(0, score); analysis.restCompliance.violations = violations; analysis.issues.push(...violations); } /** * Calculate overall API score */ function calculateApiScore(analysis) { let score = 100; // Deduct points for issues analysis.issues.forEach(issue => { switch (issue.severity) { case 'critical': score -= 15; break; case 'high': score -= 10; break; case 'medium': score -= 5; break; case 'low': score -= 2; break; } }); // Factor in documentation coverage if (analysis.metrics.totalOperations > 0) { const docCoverage = analysis.metrics.documentedOperations / analysis.metrics.totalOperations; if (docCoverage < 0.8) { score *= (0.5 + docCoverage * 0.5); // Penalize poor documentation } } // Factor in security if (analysis.security.vulnerabilities.length > 0) { score *= 0.8; // Security issues significantly impact score } return Math.max(0, Math.min(100, Math.round(score))); }

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/moikas-code/moidvk'

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