mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: Add OpenAPI support with comprehensive configuration options and client integration (#184)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
343
src/clients/openapi.ts
Normal file
343
src/clients/openapi.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { ServerConfig, OpenAPISecurityConfig } from '../types/index.js';
|
||||
|
||||
export interface OpenAPIToolInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: Record<string, unknown>;
|
||||
operationId: string;
|
||||
method: string;
|
||||
path: string;
|
||||
parameters?: OpenAPIV3.ParameterObject[];
|
||||
requestBody?: OpenAPIV3.RequestBodyObject;
|
||||
responses?: OpenAPIV3.ResponsesObject;
|
||||
}
|
||||
|
||||
export class OpenAPIClient {
|
||||
private httpClient: AxiosInstance;
|
||||
private spec: OpenAPIV3.Document | null = null;
|
||||
private tools: OpenAPIToolInfo[] = [];
|
||||
private baseUrl: string;
|
||||
private securityConfig?: OpenAPISecurityConfig;
|
||||
|
||||
constructor(private config: ServerConfig) {
|
||||
if (!config.openapi?.url && !config.openapi?.schema) {
|
||||
throw new Error('OpenAPI URL or schema is required');
|
||||
}
|
||||
|
||||
// 初始 baseUrl,将在 initialize() 中从 OpenAPI servers 字段更新
|
||||
this.baseUrl = config.openapi?.url ? this.extractBaseUrl(config.openapi.url) : '';
|
||||
this.securityConfig = config.openapi.security;
|
||||
|
||||
this.httpClient = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
timeout: config.options?.timeout || 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config.headers,
|
||||
},
|
||||
});
|
||||
|
||||
this.setupSecurity();
|
||||
}
|
||||
|
||||
private extractBaseUrl(specUrl: string): string {
|
||||
try {
|
||||
const url = new URL(specUrl);
|
||||
return `${url.protocol}//${url.host}`;
|
||||
} catch {
|
||||
// If specUrl is a relative path, assume current host
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private setupSecurity(): void {
|
||||
if (!this.securityConfig || this.securityConfig.type === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.securityConfig.type) {
|
||||
case 'apiKey':
|
||||
if (this.securityConfig.apiKey) {
|
||||
const { name, in: location, value } = this.securityConfig.apiKey;
|
||||
if (location === 'header') {
|
||||
this.httpClient.defaults.headers.common[name] = value;
|
||||
} else if (location === 'query') {
|
||||
this.httpClient.interceptors.request.use((config: any) => {
|
||||
config.params = { ...config.params, [name]: value };
|
||||
return config;
|
||||
});
|
||||
}
|
||||
// Note: Cookie authentication would need additional setup
|
||||
}
|
||||
break;
|
||||
|
||||
case 'http':
|
||||
if (this.securityConfig.http) {
|
||||
const { scheme, credentials } = this.securityConfig.http;
|
||||
if (scheme === 'bearer' && credentials) {
|
||||
this.httpClient.defaults.headers.common['Authorization'] = `Bearer ${credentials}`;
|
||||
} else if (scheme === 'basic' && credentials) {
|
||||
this.httpClient.defaults.headers.common['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'oauth2':
|
||||
if (this.securityConfig.oauth2?.token) {
|
||||
this.httpClient.defaults.headers.common['Authorization'] =
|
||||
`Bearer ${this.securityConfig.oauth2.token}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'openIdConnect':
|
||||
if (this.securityConfig.openIdConnect?.token) {
|
||||
this.httpClient.defaults.headers.common['Authorization'] =
|
||||
`Bearer ${this.securityConfig.openIdConnect.token}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Parse and dereference the OpenAPI specification
|
||||
if (this.config.openapi?.url) {
|
||||
this.spec = (await SwaggerParser.dereference(
|
||||
this.config.openapi.url,
|
||||
)) as OpenAPIV3.Document;
|
||||
} else if (this.config.openapi?.schema) {
|
||||
// For schema object, we need to pass it as a cloned object
|
||||
this.spec = (await SwaggerParser.dereference(
|
||||
JSON.parse(JSON.stringify(this.config.openapi.schema)),
|
||||
)) as OpenAPIV3.Document;
|
||||
} else {
|
||||
throw new Error('Either OpenAPI URL or schema must be provided');
|
||||
}
|
||||
|
||||
// 从 OpenAPI servers 字段更新 baseUrl
|
||||
this.updateBaseUrlFromServers();
|
||||
|
||||
this.extractTools();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to load OpenAPI specification: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
private updateBaseUrlFromServers(): void {
|
||||
if (!this.spec?.servers || this.spec.servers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取第一个 server 的 URL
|
||||
const serverUrl = this.spec.servers[0].url;
|
||||
|
||||
// 如果是相对路径,需要与原始 spec URL 结合
|
||||
if (serverUrl.startsWith('/')) {
|
||||
// 相对路径,使用原始 spec URL 的协议和主机
|
||||
if (this.config.openapi?.url) {
|
||||
const originalUrl = new URL(this.config.openapi.url);
|
||||
this.baseUrl = `${originalUrl.protocol}//${originalUrl.host}${serverUrl}`;
|
||||
}
|
||||
} else if (serverUrl.startsWith('http://') || serverUrl.startsWith('https://')) {
|
||||
// 绝对路径
|
||||
this.baseUrl = serverUrl;
|
||||
} else {
|
||||
// 相对路径但不以 / 开头,可能是相对于当前路径
|
||||
if (this.config.openapi?.url) {
|
||||
const originalUrl = new URL(this.config.openapi.url);
|
||||
this.baseUrl = `${originalUrl.protocol}//${originalUrl.host}/${serverUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 HTTP 客户端的 baseURL
|
||||
this.httpClient.defaults.baseURL = this.baseUrl;
|
||||
}
|
||||
|
||||
private extractTools(): void {
|
||||
if (!this.spec?.paths) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tools = [];
|
||||
|
||||
for (const [path, pathItem] of Object.entries(this.spec.paths)) {
|
||||
if (!pathItem) continue;
|
||||
|
||||
const methods = [
|
||||
'get',
|
||||
'post',
|
||||
'put',
|
||||
'delete',
|
||||
'patch',
|
||||
'head',
|
||||
'options',
|
||||
'trace',
|
||||
] as const;
|
||||
|
||||
for (const method of methods) {
|
||||
const operation = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
||||
if (!operation || !operation.operationId) continue;
|
||||
|
||||
const tool: OpenAPIToolInfo = {
|
||||
name: operation.operationId,
|
||||
description:
|
||||
operation.summary || operation.description || `${method.toUpperCase()} ${path}`,
|
||||
inputSchema: this.generateInputSchema(operation, path, method as string),
|
||||
operationId: operation.operationId,
|
||||
method: method as string,
|
||||
path,
|
||||
parameters: operation.parameters as OpenAPIV3.ParameterObject[],
|
||||
requestBody: operation.requestBody as OpenAPIV3.RequestBodyObject,
|
||||
responses: operation.responses,
|
||||
};
|
||||
|
||||
this.tools.push(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generateInputSchema(
|
||||
operation: OpenAPIV3.OperationObject,
|
||||
_path: string,
|
||||
_method: string,
|
||||
): Record<string, unknown> {
|
||||
const schema: Record<string, unknown> = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
};
|
||||
|
||||
const properties = schema.properties as Record<string, unknown>;
|
||||
const required = schema.required as string[];
|
||||
|
||||
// Handle path parameters
|
||||
const pathParams = operation.parameters?.filter(
|
||||
(p: any) => 'in' in p && p.in === 'path',
|
||||
) as OpenAPIV3.ParameterObject[];
|
||||
|
||||
if (pathParams?.length) {
|
||||
for (const param of pathParams) {
|
||||
properties[param.name] = {
|
||||
type: 'string',
|
||||
description: param.description || `Path parameter: ${param.name}`,
|
||||
};
|
||||
if (param.required) {
|
||||
required.push(param.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle query parameters
|
||||
const queryParams = operation.parameters?.filter(
|
||||
(p: any) => 'in' in p && p.in === 'query',
|
||||
) as OpenAPIV3.ParameterObject[];
|
||||
|
||||
if (queryParams?.length) {
|
||||
for (const param of queryParams) {
|
||||
properties[param.name] = param.schema || {
|
||||
type: 'string',
|
||||
description: param.description || `Query parameter: ${param.name}`,
|
||||
};
|
||||
if (param.required) {
|
||||
required.push(param.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle request body
|
||||
if (operation.requestBody && 'content' in operation.requestBody) {
|
||||
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
|
||||
const jsonContent = requestBody.content?.['application/json'];
|
||||
|
||||
if (jsonContent?.schema) {
|
||||
properties['body'] = jsonContent.schema;
|
||||
if (requestBody.required) {
|
||||
required.push('body');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
async callTool(toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
const tool = this.tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool '${toolName}' not found`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build the request URL with path parameters
|
||||
let url = tool.path;
|
||||
const pathParams = tool.parameters?.filter((p) => p.in === 'path') || [];
|
||||
|
||||
for (const param of pathParams) {
|
||||
const value = args[param.name];
|
||||
if (value !== undefined) {
|
||||
url = url.replace(`{${param.name}}`, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
const queryParams: Record<string, unknown> = {};
|
||||
const queryParamDefs = tool.parameters?.filter((p) => p.in === 'query') || [];
|
||||
|
||||
for (const param of queryParamDefs) {
|
||||
const value = args[param.name];
|
||||
if (value !== undefined) {
|
||||
queryParams[param.name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare request configuration
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
method: tool.method as any,
|
||||
url,
|
||||
params: queryParams,
|
||||
};
|
||||
|
||||
// Add request body if applicable
|
||||
if (args.body && ['post', 'put', 'patch'].includes(tool.method)) {
|
||||
requestConfig.data = args.body;
|
||||
}
|
||||
|
||||
// Add headers if any header parameters are defined
|
||||
const headerParams = tool.parameters?.filter((p) => p.in === 'header') || [];
|
||||
if (headerParams.length > 0) {
|
||||
requestConfig.headers = {};
|
||||
for (const param of headerParams) {
|
||||
const value = args[param.name];
|
||||
if (value !== undefined) {
|
||||
requestConfig.headers[param.name] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.httpClient.request(requestConfig);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(
|
||||
`API call failed: ${error.response?.status} ${error.response?.statusText} - ${JSON.stringify(error.response?.data)}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getTools(): OpenAPIToolInfo[] {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
getSpec(): OpenAPIV3.Document | null {
|
||||
return this.spec;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
// No persistent connection to close for OpenAPI
|
||||
}
|
||||
}
|
||||
@@ -63,19 +63,25 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.url && (!config.command || !config.args)) {
|
||||
if (
|
||||
!config.url &&
|
||||
!config.openapi?.url &&
|
||||
!config.openapi?.schema &&
|
||||
(!config.command || !config.args)
|
||||
) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server configuration must include either a URL or command with arguments',
|
||||
message:
|
||||
'Server configuration must include either a URL, OpenAPI specification URL or schema, or command with arguments',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the server type if specified
|
||||
if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) {
|
||||
if (config.type && !['stdio', 'sse', 'streamable-http', 'openapi'].includes(config.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server type must be one of: stdio, sse, streamable-http',
|
||||
message: 'Server type must be one of: stdio, sse, streamable-http, openapi',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -89,6 +95,15 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that OpenAPI specification URL or schema is provided for openapi type
|
||||
if (config.type === 'openapi' && !config.openapi?.url && !config.openapi?.schema) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'OpenAPI specification URL or schema is required for openapi server type',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate headers if provided
|
||||
if (config.headers && typeof config.headers !== 'object') {
|
||||
res.status(400).json({
|
||||
@@ -98,7 +113,7 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that headers are only used with sse and streamable-http types
|
||||
// Validate that headers are only used with sse, streamable-http, and openapi types
|
||||
if (config.headers && config.type === 'stdio') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -185,19 +200,25 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.url && (!config.command || !config.args)) {
|
||||
if (
|
||||
!config.url &&
|
||||
!config.openapi?.url &&
|
||||
!config.openapi?.schema &&
|
||||
(!config.command || !config.args)
|
||||
) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server configuration must include either a URL or command with arguments',
|
||||
message:
|
||||
'Server configuration must include either a URL, OpenAPI specification URL or schema, or command with arguments',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the server type if specified
|
||||
if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) {
|
||||
if (config.type && !['stdio', 'sse', 'streamable-http', 'openapi'].includes(config.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server type must be one of: stdio, sse, streamable-http',
|
||||
message: 'Server type must be one of: stdio, sse, streamable-http, openapi',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -211,6 +232,15 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that OpenAPI specification URL or schema is provided for openapi type
|
||||
if (config.type === 'openapi' && !config.openapi?.url && !config.openapi?.schema) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'OpenAPI specification URL or schema is required for openapi server type',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate headers if provided
|
||||
if (config.headers && typeof config.headers !== 'object') {
|
||||
res.status(400).json({
|
||||
@@ -220,7 +250,7 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that headers are only used with sse and streamable-http types
|
||||
// Validate that headers are only used with sse, streamable-http, and openapi types
|
||||
if (config.headers && config.type === 'stdio') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
|
||||
@@ -10,6 +10,7 @@ import config from '../config/index.js';
|
||||
import { getGroup } from './sseService.js';
|
||||
import { getServersInGroup } from './groupService.js';
|
||||
import { saveToolsAsVectorEmbeddings, searchToolsByVector } from './vectorSearchService.js';
|
||||
import { OpenAPIClient } from '../clients/openapi.js';
|
||||
|
||||
const servers: { [sessionId: string]: Server } = {};
|
||||
|
||||
@@ -101,7 +102,7 @@ export const syncToolEmbedding = async (serverName: string, toolName: string) =>
|
||||
let serverInfos: ServerInfo[] = [];
|
||||
|
||||
// Initialize MCP server clients
|
||||
export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => {
|
||||
export const initializeClientsFromSettings = async (isInit: boolean): Promise<ServerInfo[]> => {
|
||||
const settings = loadSettings();
|
||||
const existingServerInfos = serverInfos;
|
||||
serverInfos = [];
|
||||
@@ -135,7 +136,85 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
}
|
||||
|
||||
let transport;
|
||||
if (conf.type === 'streamable-http') {
|
||||
let openApiClient;
|
||||
|
||||
if (conf.type === 'openapi') {
|
||||
// Handle OpenAPI type servers
|
||||
if (!conf.openapi?.url && !conf.openapi?.schema) {
|
||||
console.warn(
|
||||
`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`,
|
||||
);
|
||||
serverInfos.push({
|
||||
name,
|
||||
status: 'disconnected',
|
||||
error: 'Missing OpenAPI specification URL or schema',
|
||||
tools: [],
|
||||
createTime: Date.now(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create OpenAPI client instance
|
||||
openApiClient = new OpenAPIClient(conf);
|
||||
|
||||
// Add server with connecting status first
|
||||
const serverInfo: ServerInfo = {
|
||||
name,
|
||||
status: 'connecting',
|
||||
error: null,
|
||||
tools: [],
|
||||
createTime: Date.now(),
|
||||
enabled: conf.enabled === undefined ? true : conf.enabled,
|
||||
};
|
||||
serverInfos.push(serverInfo);
|
||||
|
||||
console.log(`Initializing OpenAPI server: ${name}...`);
|
||||
|
||||
// Perform async initialization
|
||||
await openApiClient.initialize();
|
||||
|
||||
// Convert OpenAPI tools to MCP tool format
|
||||
const openApiTools = openApiClient.getTools();
|
||||
const mcpTools: ToolInfo[] = openApiTools.map((tool) => ({
|
||||
name: `${name}-${tool.name}`,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
|
||||
// Update server info with successful initialization
|
||||
serverInfo.status = 'connected';
|
||||
serverInfo.tools = mcpTools;
|
||||
serverInfo.openApiClient = openApiClient;
|
||||
|
||||
console.log(
|
||||
`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`,
|
||||
);
|
||||
|
||||
// Save tools as vector embeddings for search
|
||||
saveToolsAsVectorEmbeddings(name, mcpTools);
|
||||
continue;
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize OpenAPI server ${name}:`, error);
|
||||
|
||||
// Find and update the server info if it was already added
|
||||
const existingServerIndex = serverInfos.findIndex((s) => s.name === name);
|
||||
if (existingServerIndex !== -1) {
|
||||
serverInfos[existingServerIndex].status = 'disconnected';
|
||||
serverInfos[existingServerIndex].error = `Failed to initialize OpenAPI server: ${error}`;
|
||||
} else {
|
||||
// Add new server info with error status
|
||||
serverInfos.push({
|
||||
name,
|
||||
status: 'disconnected',
|
||||
error: `Failed to initialize OpenAPI server: ${error}`,
|
||||
tools: [],
|
||||
createTime: Date.now(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else if (conf.type === 'streamable-http') {
|
||||
const options: any = {};
|
||||
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
||||
options.requestInit = {
|
||||
@@ -300,7 +379,7 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
|
||||
// Register all MCP tools
|
||||
export const registerAllTools = async (isInit: boolean): Promise<void> => {
|
||||
initializeClientsFromSettings(isInit);
|
||||
await initializeClientsFromSettings(isInit);
|
||||
};
|
||||
|
||||
// Get all server information
|
||||
@@ -739,7 +818,38 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
throw new Error(`Tool '${toolName}' not found on server '${targetServerInfo.name}'`);
|
||||
}
|
||||
|
||||
// Call the tool on the target server
|
||||
// Handle OpenAPI servers differently
|
||||
if (targetServerInfo.openApiClient) {
|
||||
// For OpenAPI servers, use the OpenAPI client
|
||||
const openApiClient = targetServerInfo.openApiClient;
|
||||
|
||||
// Use toolArgs if it has properties, otherwise fallback to request.params.arguments
|
||||
const finalArgs =
|
||||
toolArgs && Object.keys(toolArgs).length > 0 ? toolArgs : request.params.arguments || {};
|
||||
|
||||
console.log(
|
||||
`Invoking OpenAPI tool '${toolName}' on server '${targetServerInfo.name}' with arguments: ${JSON.stringify(finalArgs)}`,
|
||||
);
|
||||
|
||||
// Remove server prefix from tool name if present
|
||||
const cleanToolName = toolName.startsWith(`${targetServerInfo.name}-`)
|
||||
? toolName.replace(`${targetServerInfo.name}-`, '')
|
||||
: toolName;
|
||||
|
||||
const result = await openApiClient.callTool(cleanToolName, finalArgs);
|
||||
|
||||
console.log(`OpenAPI tool invocation result: ${JSON.stringify(result)}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Call the tool on the target server (MCP servers)
|
||||
const client = targetServerInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${targetServerInfo.name}`);
|
||||
@@ -774,9 +884,38 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
if (!serverInfo) {
|
||||
throw new Error(`Server not found: ${request.params.name}`);
|
||||
}
|
||||
|
||||
// Handle OpenAPI servers differently
|
||||
if (serverInfo.openApiClient) {
|
||||
// For OpenAPI servers, use the OpenAPI client
|
||||
const openApiClient = serverInfo.openApiClient;
|
||||
|
||||
// Remove server prefix from tool name if present
|
||||
const cleanToolName = request.params.name.startsWith(`${serverInfo.name}-`)
|
||||
? request.params.name.replace(`${serverInfo.name}-`, '')
|
||||
: request.params.name;
|
||||
|
||||
console.log(
|
||||
`Invoking OpenAPI tool '${cleanToolName}' on server '${serverInfo.name}' with arguments: ${JSON.stringify(request.params.arguments)}`,
|
||||
);
|
||||
|
||||
const result = await openApiClient.callTool(cleanToolName, request.params.arguments || {});
|
||||
|
||||
console.log(`OpenAPI tool invocation result: ${JSON.stringify(result)}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Handle MCP servers
|
||||
const client = serverInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${request.params.name}`);
|
||||
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
||||
}
|
||||
|
||||
request.params.name = request.params.name.startsWith(`${serverInfo.name}-`)
|
||||
|
||||
@@ -99,16 +99,55 @@ export interface McpSettings {
|
||||
|
||||
// Configuration details for an individual server
|
||||
export interface ServerConfig {
|
||||
type?: 'stdio' | 'sse' | 'streamable-http'; // Type of server
|
||||
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi'; // Type of server
|
||||
url?: string; // URL for SSE or streamable HTTP servers
|
||||
command?: string; // Command to execute for stdio-based servers
|
||||
args?: string[]; // Arguments for the command
|
||||
env?: Record<string, string>; // Environment variables
|
||||
headers?: Record<string, string>; // HTTP headers for SSE/streamable-http servers
|
||||
headers?: Record<string, string>; // HTTP headers for SSE/streamable-http/openapi servers
|
||||
enabled?: boolean; // Flag to enable/disable the server
|
||||
keepAliveInterval?: number; // Keep-alive ping interval in milliseconds (default: 60000ms for SSE servers)
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
options?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
|
||||
// OpenAPI specific configuration
|
||||
openapi?: {
|
||||
url?: string; // OpenAPI specification URL
|
||||
schema?: Record<string, any>; // Complete OpenAPI JSON schema
|
||||
version?: string; // OpenAPI version (default: '3.1.0')
|
||||
security?: OpenAPISecurityConfig; // Security configuration for API calls
|
||||
};
|
||||
}
|
||||
|
||||
// OpenAPI Security Configuration
|
||||
export interface OpenAPISecurityConfig {
|
||||
type: 'none' | 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';
|
||||
// API Key authentication
|
||||
apiKey?: {
|
||||
name: string; // Header/query/cookie name
|
||||
in: 'header' | 'query' | 'cookie';
|
||||
value: string; // The API key value
|
||||
};
|
||||
// HTTP authentication (Basic, Bearer, etc.)
|
||||
http?: {
|
||||
scheme: 'basic' | 'bearer' | 'digest'; // HTTP auth scheme
|
||||
bearerFormat?: string; // Bearer token format (e.g., JWT)
|
||||
credentials?: string; // Base64 encoded credentials for basic auth or bearer token
|
||||
};
|
||||
// OAuth2 (simplified - mainly for bearer tokens)
|
||||
oauth2?: {
|
||||
tokenUrl?: string; // Token endpoint for client credentials flow
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
scopes?: string[]; // Required scopes
|
||||
token?: string; // Pre-obtained access token
|
||||
};
|
||||
// OpenID Connect
|
||||
openIdConnect?: {
|
||||
url: string; // OpenID Connect discovery URL
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
token?: string; // Pre-obtained ID token
|
||||
};
|
||||
}
|
||||
|
||||
// Information about a server's status and tools
|
||||
@@ -117,8 +156,9 @@ export interface ServerInfo {
|
||||
status: 'connected' | 'connecting' | 'disconnected'; // Current connection status
|
||||
error: string | null; // Error message if any
|
||||
tools: ToolInfo[]; // List of tools available on the server
|
||||
client?: Client; // Client instance for communication
|
||||
client?: Client; // Client instance for communication (MCP clients)
|
||||
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
|
||||
openApiClient?: any; // OpenAPI client instance for openapi type servers
|
||||
options?: RequestOptions; // Options for requests
|
||||
createTime: number; // Timestamp of when the server was created
|
||||
enabled?: boolean; // Flag to indicate if the server is enabled
|
||||
|
||||
Reference in New Issue
Block a user