import type { NormalizedOperation, HttpMethod } from '../types/index.js';
import type { JSONSchema } from '../types/mcp-tool.js';
/**
* Ensure operation ID is unique by appending a counter if needed
*/
export function ensureUniqueOperationIds(operations: NormalizedOperation[]): NormalizedOperation[] {
const idCount = new Map<string, number>();
return operations.map((op) => {
const count = idCount.get(op.operationId) ?? 0;
idCount.set(op.operationId, count + 1);
if (count === 0) {
return op;
}
return {
...op,
operationId: `${op.operationId}_${count}`,
};
});
}
/**
* Sort operations by path and method for consistent ordering
*/
export function sortOperations(operations: NormalizedOperation[]): NormalizedOperation[] {
const methodOrder: Record<HttpMethod, number> = {
GET: 1,
POST: 2,
PUT: 3,
PATCH: 4,
DELETE: 5,
HEAD: 6,
OPTIONS: 7,
};
return [...operations].sort((a, b) => {
const pathCompare = a.path.localeCompare(b.path);
if (pathCompare !== 0) return pathCompare;
return (methodOrder[a.method] ?? 99) - (methodOrder[b.method] ?? 99);
});
}
/**
* Flatten nested object schemas to a simpler structure
* This helps with generating cleaner tool input schemas
*/
export function simplifySchema(schema: JSONSchema, maxDepth = 3, currentDepth = 0): JSONSchema {
if (currentDepth >= maxDepth) {
// At max depth, convert complex types to string
return { type: 'string', description: schema.description };
}
if (schema.type === 'object' && schema.properties) {
const simplified: JSONSchema = {
type: 'object',
properties: {},
description: schema.description,
};
for (const [key, value] of Object.entries(schema.properties)) {
simplified.properties![key] = simplifySchema(value, maxDepth, currentDepth + 1);
}
if (schema.required) {
simplified.required = schema.required;
}
return simplified;
}
if (schema.type === 'array' && schema.items) {
return {
type: 'array',
items: simplifySchema(schema.items, maxDepth, currentDepth + 1),
description: schema.description,
};
}
// For allOf/anyOf/oneOf, take the first option
if (schema.allOf?.[0]) {
return simplifySchema(schema.allOf[0], maxDepth, currentDepth);
}
if (schema.anyOf?.[0]) {
return simplifySchema(schema.anyOf[0], maxDepth, currentDepth);
}
if (schema.oneOf?.[0]) {
return simplifySchema(schema.oneOf[0], maxDepth, currentDepth);
}
return schema;
}
/**
* Check if an operation is considered "dangerous"
* (modifies data or has side effects)
*/
export function isDangerousOperation(operation: NormalizedOperation): boolean {
// DELETE is always dangerous
if (operation.method === 'DELETE') {
return true;
}
// Check for dangerous keywords in operation ID or description
const dangerousKeywords = [
'delete',
'remove',
'destroy',
'purge',
'reset',
'revoke',
'cancel',
'terminate',
'wipe',
'clear',
];
const text = `${operation.operationId} ${operation.summary ?? ''} ${operation.description ?? ''}`.toLowerCase();
return dangerousKeywords.some((keyword) => text.includes(keyword));
}
/**
* Filter operations by method
*/
export function filterByMethod(
operations: NormalizedOperation[],
methods: HttpMethod[]
): NormalizedOperation[] {
const methodSet = new Set(methods);
return operations.filter((op) => !methodSet.has(op.method));
}
/**
* Filter out deprecated operations
*/
export function filterDeprecated(
operations: NormalizedOperation[],
excludeDeprecated = true
): NormalizedOperation[] {
if (!excludeDeprecated) return operations;
return operations.filter((op) => !op.deprecated);
}
/**
* Get statistics about operations
*/
export function getOperationStats(operations: NormalizedOperation[]) {
const stats = {
total: operations.length,
byMethod: {} as Record<string, number>,
deprecated: 0,
withRequestBody: 0,
withAuth: 0,
tags: new Set<string>(),
};
for (const op of operations) {
// Count by method
stats.byMethod[op.method] = (stats.byMethod[op.method] ?? 0) + 1;
// Count deprecated
if (op.deprecated) {
stats.deprecated++;
}
// Count with request body
if (op.requestBody) {
stats.withRequestBody++;
}
// Count with security
if (op.security.length > 0) {
stats.withAuth++;
}
// Collect tags
for (const tag of op.tags) {
stats.tags.add(tag);
}
}
return {
...stats,
tags: Array.from(stats.tags),
};
}