mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 21:49:13 -05:00
367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
import { OpenAPIV3 } from 'openapi-types';
|
|
import { Tool } from '../types/index.js';
|
|
import { getServersInfo } from './mcpService.js';
|
|
import config, { getNameSeparator } from '../config/index.js';
|
|
import { getSystemConfigDao } from '../dao/index.js';
|
|
|
|
/**
|
|
* Service for generating OpenAPI 3.x specifications from MCP tools
|
|
* This enables integration with OpenWebUI and other OpenAPI-compatible systems
|
|
*/
|
|
|
|
export interface OpenAPIGenerationOptions {
|
|
title?: string;
|
|
description?: string;
|
|
version?: string;
|
|
serverUrl?: string;
|
|
includeDisabledTools?: boolean;
|
|
groupFilter?: string;
|
|
serverFilter?: string[];
|
|
}
|
|
|
|
/**
|
|
* Convert MCP tool input schema to OpenAPI parameter or request body schema
|
|
*/
|
|
function convertToolSchemaToOpenAPI(tool: Tool): {
|
|
parameters?: OpenAPIV3.ParameterObject[];
|
|
requestBody?: OpenAPIV3.RequestBodyObject;
|
|
} {
|
|
const schema = tool.inputSchema as any;
|
|
|
|
if (!schema || typeof schema !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
// If schema has properties, convert them to parameters or request body
|
|
if (schema.properties && typeof schema.properties === 'object') {
|
|
const properties = schema.properties;
|
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
|
|
// For simple tools with only primitive parameters, use query parameters
|
|
const hasComplexTypes = Object.values(properties).some(
|
|
(prop: any) =>
|
|
prop.type === 'object' ||
|
|
prop.type === 'array' ||
|
|
prop.type === 'string',
|
|
);
|
|
|
|
if (!hasComplexTypes && Object.keys(properties).length <= 10) {
|
|
// Use query parameters for simple tools
|
|
const parameters: OpenAPIV3.ParameterObject[] = Object.entries(properties).map(
|
|
([name, prop]: [string, any]) => ({
|
|
name,
|
|
in: 'query',
|
|
required: required.includes(name),
|
|
description: prop.description || `Parameter ${name}`,
|
|
schema: {
|
|
type: prop.type || 'string',
|
|
...(prop.enum && { enum: prop.enum }),
|
|
...(prop.default !== undefined && { default: prop.default }),
|
|
...(prop.format && { format: prop.format }),
|
|
},
|
|
}),
|
|
);
|
|
|
|
return { parameters };
|
|
} else {
|
|
// Use request body for complex tools
|
|
const requestBody: OpenAPIV3.RequestBodyObject = {
|
|
required: required.length > 0,
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties,
|
|
...(required.length > 0 && { required }),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return { requestBody };
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Generate OpenAPI operation from MCP tool
|
|
*/
|
|
function generateOperationFromTool(tool: Tool, serverName: string): OpenAPIV3.OperationObject {
|
|
const { parameters, requestBody } = convertToolSchemaToOpenAPI(tool);
|
|
const operation: OpenAPIV3.OperationObject = {
|
|
summary: tool.description || `Execute ${tool.name} tool`,
|
|
description: tool.description || `Execute the ${tool.name} tool from ${serverName} server`,
|
|
operationId: `${tool.name}`,
|
|
tags: [serverName],
|
|
...(parameters && parameters.length > 0 && { parameters }),
|
|
...(requestBody && { requestBody }),
|
|
responses: {
|
|
'200': {
|
|
description: 'Successful tool execution',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
content: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
type: { type: 'string' },
|
|
text: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
isError: { type: 'boolean' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'400': {
|
|
description: 'Bad request - invalid parameters',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
error: { type: 'string' },
|
|
message: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'500': {
|
|
description: 'Internal server error',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
error: { type: 'string' },
|
|
message: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return operation;
|
|
}
|
|
|
|
/**
|
|
* Generate OpenAPI specification from MCP tools
|
|
*/
|
|
export async function generateOpenAPISpec(
|
|
options: OpenAPIGenerationOptions = {},
|
|
): Promise<OpenAPIV3.Document> {
|
|
const serverInfos = await getServersInfo();
|
|
|
|
// Filter servers based on options
|
|
let filteredServers = serverInfos.filter(
|
|
(server) =>
|
|
server.status === 'connected' &&
|
|
(!options.serverFilter || options.serverFilter.includes(server.name)),
|
|
);
|
|
|
|
// Apply group filter if specified
|
|
const groupConfig: Map<string, string[] | 'all'> = new Map();
|
|
if (options.groupFilter) {
|
|
const { getGroupByIdOrName } = await import('./groupService.js');
|
|
const group = await getGroupByIdOrName(options.groupFilter);
|
|
if (group) {
|
|
// Extract server names and their tool configurations from group
|
|
const groupServerNames: string[] = [];
|
|
for (const server of group.servers) {
|
|
if (typeof server === 'string') {
|
|
groupServerNames.push(server);
|
|
groupConfig.set(server, 'all');
|
|
} else {
|
|
groupServerNames.push(server.name);
|
|
groupConfig.set(server.name, server.tools || 'all');
|
|
}
|
|
}
|
|
// Filter to only servers in the group
|
|
filteredServers = filteredServers.filter((server) => groupServerNames.includes(server.name));
|
|
} else {
|
|
// Group not found, return empty specification
|
|
filteredServers = [];
|
|
}
|
|
}
|
|
|
|
// Collect all tools from filtered servers
|
|
const allTools: Array<{ tool: Tool; serverName: string }> = [];
|
|
|
|
for (const serverInfo of filteredServers) {
|
|
const tools = options.includeDisabledTools
|
|
? serverInfo.tools
|
|
: serverInfo.tools.filter((tool) => tool.enabled !== false);
|
|
|
|
// Apply group-specific tool filtering if group filter is specified
|
|
let filteredTools = tools;
|
|
if (options.groupFilter && groupConfig.has(serverInfo.name)) {
|
|
const allowedTools = groupConfig.get(serverInfo.name);
|
|
if (allowedTools !== 'all') {
|
|
// Filter tools to only include those specified in the group configuration
|
|
const separator = getNameSeparator();
|
|
filteredTools = tools.filter(
|
|
(tool) =>
|
|
Array.isArray(allowedTools) &&
|
|
allowedTools.includes(tool.name.replace(serverInfo.name + separator, '')),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const tool of filteredTools) {
|
|
allTools.push({ tool, serverName: serverInfo.name });
|
|
}
|
|
}
|
|
|
|
// Generate paths from tools
|
|
const paths: OpenAPIV3.PathsObject = {};
|
|
const separator = getNameSeparator();
|
|
|
|
for (const { tool, serverName } of allTools) {
|
|
const operation = generateOperationFromTool(tool, serverName);
|
|
const { requestBody } = convertToolSchemaToOpenAPI(tool);
|
|
|
|
// Extract the tool name without server prefix
|
|
// Tool names are in format: serverName + separator + toolName
|
|
const prefix = `${serverName}${separator}`;
|
|
const toolNameOnly = tool.name.startsWith(prefix)
|
|
? tool.name.substring(prefix.length)
|
|
: tool.name;
|
|
|
|
// Create path for the tool with URL-encoded server and tool names
|
|
// This handles cases where names contain slashes (e.g., "com.atlassian/atlassian-mcp-server")
|
|
const pathName = `/tools/${encodeURIComponent(serverName)}/${encodeURIComponent(toolNameOnly)}`;
|
|
const method = requestBody ? 'post' : 'get';
|
|
|
|
if (!paths[pathName]) {
|
|
paths[pathName] = {};
|
|
}
|
|
|
|
paths[pathName][method] = operation;
|
|
}
|
|
|
|
const systemConfigDao = getSystemConfigDao();
|
|
const systemConfig = await systemConfigDao.get();
|
|
// Get server URL
|
|
const baseUrl =
|
|
options.serverUrl || systemConfig?.install?.baseUrl || `http://localhost:${config.port}`;
|
|
const serverUrl = `${baseUrl}${config.basePath}/api`;
|
|
|
|
// Generate OpenAPI document
|
|
const openApiDoc: OpenAPIV3.Document = {
|
|
openapi: '3.0.3',
|
|
info: {
|
|
title: options.title || 'MCPHub API',
|
|
description:
|
|
options.description ||
|
|
'OpenAPI specification for MCP tools managed by MCPHub. This enables integration with OpenWebUI and other OpenAPI-compatible systems.',
|
|
version: options.version || '1.0.0',
|
|
contact: {
|
|
name: 'MCPHub',
|
|
url: 'https://github.com/samanhappy/mcphub',
|
|
},
|
|
license: {
|
|
name: 'ISC',
|
|
url: 'https://github.com/samanhappy/mcphub/blob/main/LICENSE',
|
|
},
|
|
},
|
|
servers: [
|
|
{
|
|
url: serverUrl,
|
|
description: 'MCPHub API Server',
|
|
},
|
|
],
|
|
paths,
|
|
components: {
|
|
schemas: {
|
|
ToolResponse: {
|
|
type: 'object',
|
|
properties: {
|
|
content: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
type: { type: 'string' },
|
|
text: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
isError: { type: 'boolean' },
|
|
},
|
|
},
|
|
ErrorResponse: {
|
|
type: 'object',
|
|
properties: {
|
|
error: { type: 'string' },
|
|
message: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
securitySchemes: {
|
|
bearerAuth: {
|
|
type: 'http',
|
|
scheme: 'bearer',
|
|
bearerFormat: 'JWT',
|
|
},
|
|
},
|
|
},
|
|
security: [
|
|
{
|
|
bearerAuth: [],
|
|
},
|
|
],
|
|
tags: filteredServers.map((server) => ({
|
|
name: server.name,
|
|
description: `Tools from ${server.name} server`,
|
|
})),
|
|
};
|
|
|
|
return openApiDoc;
|
|
}
|
|
|
|
/**
|
|
* Get available server names for filtering
|
|
*/
|
|
export async function getAvailableServers(): Promise<string[]> {
|
|
const serverInfos = await getServersInfo();
|
|
return serverInfos.filter((server) => server.status === 'connected').map((server) => server.name);
|
|
}
|
|
|
|
/**
|
|
* Get statistics about available tools
|
|
*/
|
|
export async function getToolStats(): Promise<{
|
|
totalServers: number;
|
|
totalTools: number;
|
|
serverBreakdown: Array<{ name: string; toolCount: number; status: string }>;
|
|
}> {
|
|
const serverInfos = await getServersInfo();
|
|
|
|
const serverBreakdown = serverInfos.map((server) => ({
|
|
name: server.name,
|
|
toolCount: server.tools.length,
|
|
status: server.status,
|
|
}));
|
|
|
|
const totalTools = serverInfos
|
|
.filter((server) => server.status === 'connected')
|
|
.reduce((sum, server) => sum + server.tools.length, 0);
|
|
|
|
return {
|
|
totalServers: serverInfos.filter((server) => server.status === 'connected').length,
|
|
totalTools,
|
|
serverBreakdown,
|
|
};
|
|
}
|