import type { NormalizedOperation } from '../types/index.js';
import type { JSONSchema, ProxyMetadata } from '../types/mcp-tool.js';
/**
* Generate JSON Schema for tool input from operation parameters and body
*/
export function generateInputSchema(operation: NormalizedOperation): JSONSchema {
const properties: Record<string, JSONSchema> = {};
const required: string[] = [];
// Add parameters (path, query, header)
for (const param of operation.parameters) {
properties[param.name] = {
...param.schema,
description: param.description ?? param.schema.description,
};
if (param.required) {
required.push(param.name);
}
}
// Add request body properties
if (operation.requestBody) {
const bodySchema = operation.requestBody.schema;
if (bodySchema.type === 'object' && bodySchema.properties) {
// Flatten object body properties into input schema
for (const [key, value] of Object.entries(bodySchema.properties)) {
// Avoid overwriting existing params
if (!(key in properties)) {
properties[key] = value;
}
}
// Add required body properties
if (bodySchema.required) {
for (const reqProp of bodySchema.required) {
if (!required.includes(reqProp)) {
required.push(reqProp);
}
}
}
} else {
// Non-object body: wrap in "body" property
properties['body'] = {
...bodySchema,
description: operation.requestBody.description ?? bodySchema.description ?? 'Request body',
};
if (operation.requestBody.required) {
required.push('body');
}
}
}
return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined,
};
}
/**
* Generate proxy metadata for executing the operation
*/
export function generateProxyMetadata(operation: NormalizedOperation): ProxyMetadata {
const pathParams: string[] = [];
const queryParams: string[] = [];
const headerParams: string[] = [];
const bodyParams: string[] = [];
// Categorize parameters by location
for (const param of operation.parameters) {
switch (param.in) {
case 'path':
pathParams.push(param.name);
break;
case 'query':
queryParams.push(param.name);
break;
case 'header':
headerParams.push(param.name);
break;
}
}
// Extract body params
if (operation.requestBody) {
const bodySchema = operation.requestBody.schema;
if (bodySchema.type === 'object' && bodySchema.properties) {
bodyParams.push(...Object.keys(bodySchema.properties));
} else {
// Non-object body wrapped in "body" property
bodyParams.push('body');
}
}
return {
method: operation.method,
path: operation.path,
pathParams,
queryParams,
headerParams,
contentType: operation.requestBody?.contentType,
bodySchema: operation.requestBody?.schema,
bodyParams,
tags: operation.tags,
};
}
/**
* Extract body params that should be excluded from path/query/header
*/
export function getBodyOnlyParams(proxy: ProxyMetadata): Set<string> {
const nonBodyParams = new Set([
...proxy.pathParams,
...proxy.queryParams,
...proxy.headerParams,
]);
return new Set(proxy.bodyParams.filter((p) => !nonBodyParams.has(p)));
}
/**
* Validate that input schema is valid JSON Schema
*/
export function validateInputSchema(schema: JSONSchema): string[] {
const errors: string[] = [];
if (schema.type !== 'object') {
errors.push('Input schema must be an object type');
}
if (schema.properties) {
for (const [name, prop] of Object.entries(schema.properties)) {
if (!prop.type && !prop.oneOf && !prop.anyOf && !prop.allOf) {
errors.push(`Property "${name}" is missing type definition`);
}
}
}
return errors;
}