utils.js•7.23 kB
/**
* Utility functions for OpenAPI MCP Server
*/
/**
* Deep clone an object
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
/**
* Convert OpenAPI parameter to JSON Schema property
*/
export function parameterToJsonSchema(parameter) {
const schema = {
type: parameter.schema?.type || 'string',
description: parameter.description || `${parameter.name} parameter`
};
// Copy schema properties
if (parameter.schema) {
const { type, format, enum: enumValues, minimum, maximum, pattern, items, properties } = parameter.schema;
if (format) schema.format = format;
if (enumValues) schema.enum = enumValues;
if (minimum !== undefined) schema.minimum = minimum;
if (maximum !== undefined) schema.maximum = maximum;
if (pattern) schema.pattern = pattern;
if (items) schema.items = items;
if (properties) schema.properties = properties;
}
// Add example if available
if (parameter.example !== undefined) {
schema.example = parameter.example;
}
return schema;
}
/**
* Generate a tool name from operation details
*/
export function generateToolName(operation, method, path) {
// First priority: operationId
if (operation.operationId) {
return sanitizeToolName(operation.operationId);
}
// Second priority: summary
if (operation.summary) {
const summary = operation.summary
.toLowerCase()
.replace(/[^a-zA-Z0-9\s]/g, '')
.replace(/\s+/g, '_');
return sanitizeToolName(summary);
}
// Last resort: method + path
const cleanPath = path
.replace(/^\//, '')
.replace(/[{}]/g, '')
.replace(/[^a-zA-Z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/_$/, '');
return sanitizeToolName(`${method.toLowerCase()}_${cleanPath}`);
}
/**
* Sanitize a tool name to ensure it's valid
*/
export function sanitizeToolName(name) {
return name
.replace(/[^a-zA-Z0-9_]/g, '_')
.replace(/^[^a-zA-Z]/, 'tool_$&')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.toLowerCase();
}
/**
* Extract path parameters from a path string
*/
export function extractPathParameters(path) {
const matches = path.match(/{([^}]+)}/g) || [];
return matches.map(match => match.slice(1, -1));
}
/**
* Merge multiple JSON schemas
*/
export function mergeSchemas(...schemas) {
const merged = {
type: 'object',
properties: {},
required: []
};
schemas.forEach(schema => {
if (!schema || schema.type !== 'object') return;
// Merge properties
if (schema.properties) {
Object.assign(merged.properties, schema.properties);
}
// Merge required fields
if (schema.required && Array.isArray(schema.required)) {
merged.required.push(...schema.required);
}
});
// Remove duplicate required fields
merged.required = [...new Set(merged.required)];
return merged;
}
/**
* Validate that a value matches a JSON schema type
*/
export function validateType(value, schemaType) {
switch (schemaType) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'integer':
return Number.isInteger(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return value !== null && typeof value === 'object' && !Array.isArray(value);
case 'null':
return value === null;
default:
return true; // Allow unknown types
}
}
/**
* Convert HTTP status code to category
*/
export function getStatusCategory(statusCode) {
const code = parseInt(statusCode);
if (code >= 200 && code < 300) return 'success';
if (code >= 300 && code < 400) return 'redirect';
if (code >= 400 && code < 500) return 'client_error';
if (code >= 500) return 'server_error';
return 'unknown';
}
/**
* Format error message for MCP response
*/
export function formatErrorResponse(error, statusCode = null) {
const response = {
error: true,
message: error.message || 'An unknown error occurred'
};
if (statusCode) {
response.status = statusCode;
response.category = getStatusCategory(statusCode);
}
if (error.status) {
response.status = error.status;
response.category = getStatusCategory(error.status);
}
if (error.data) {
response.data = error.data;
}
return response;
}
/**
* Create a success response for MCP
*/
export function formatSuccessResponse(data, statusCode = 200, headers = {}) {
return {
success: true,
status: statusCode,
category: getStatusCategory(statusCode),
headers: headers,
data: data
};
}
/**
* Check if an HTTP method typically has a request body
*/
export function methodHasBody(method) {
const methodsWithBody = ['post', 'put', 'patch'];
return methodsWithBody.includes(method.toLowerCase());
}
/**
* Check if a parameter is a path parameter
*/
export function isPathParameter(parameter) {
return parameter.in === 'path';
}
/**
* Check if a parameter is a query parameter
*/
export function isQueryParameter(parameter) {
return parameter.in === 'query';
}
/**
* Check if a parameter is a header parameter
*/
export function isHeaderParameter(parameter) {
return parameter.in === 'header';
}
/**
* Parse content type from media type string
*/
export function parseContentType(mediaType) {
const [type, ...params] = mediaType.split(';');
const parsed = { type: type.trim() };
params.forEach(param => {
const [key, value] = param.split('=');
if (key && value) {
parsed[key.trim()] = value.trim().replace(/['"]/g, '');
}
});
return parsed;
}
/**
* Get the primary content type from an OpenAPI content object
*/
export function getPrimaryContentType(content) {
if (!content) return null;
// Prefer JSON
if (content['application/json']) return 'application/json';
// Then any JSON variant
const jsonType = Object.keys(content).find(type => type.includes('json'));
if (jsonType) return jsonType;
// Then any text type
const textType = Object.keys(content).find(type => type.startsWith('text/'));
if (textType) return textType;
// Return first available
return Object.keys(content)[0];
}
/**
* Safely get a nested property from an object
*/
export function getNestedProperty(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
/**
* Set a nested property in an object
*/
export function setNestedProperty(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const target = keys.reduce((current, key) => {
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
return current[key];
}, obj);
target[lastKey] = value;
}