From 53528770f8ce09716f8e49b389e21477b89bec35 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 24 Dec 2025 12:21:21 +0800 Subject: [PATCH] feat(mcp): integrate hub server with MCP infrastructure - Create HubServer class with search/exec tools - Implement mcp-bridge for calling tools via MCPService - Register hub server in factory with dependency injection - Initialize hub dependencies in MCPService constructor - Add hub server description label for i18n Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e Co-authored-by: Amp --- src/main/mcpServers/factory.ts | 26 +++- src/main/mcpServers/hub/index.ts | 163 ++++++++++++++++++++++++++ src/main/mcpServers/hub/mcp-bridge.ts | 95 +++++++++++++++ src/main/services/MCPService.ts | 24 +++- src/renderer/src/i18n/label.ts | 3 +- 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 src/main/mcpServers/hub/index.ts create mode 100644 src/main/mcpServers/hub/mcp-bridge.ts diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 909901c1c8..fd8a4ff020 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import type { Server } from '@modelcontextprotocol/sdk/server/index.js' -import type { BuiltinMCPServerName } from '@types' +import type { BuiltinMCPServerName, MCPServer } from '@types' import { BuiltinMCPServerNames } from '@types' import BraveSearchServer from './brave-search' @@ -9,12 +9,30 @@ import DiDiMcpServer from './didi-mcp' import DifyKnowledgeServer from './dify-knowledge' import FetchServer from './fetch' import FileSystemServer from './filesystem' +import HubServer from './hub' import MemoryServer from './memory' import PythonServer from './python' import ThinkingServer from './sequentialthinking' const logger = loggerService.withContext('MCPFactory') +interface HubServerDependencies { + mcpService: { + listTools(_: null, server: MCPServer): Promise + callTool( + _: null, + args: { server: MCPServer; name: string; args: unknown; callId?: string } + ): Promise<{ content: Array<{ type: string; text?: string }> }> + } + mcpServersGetter: () => MCPServer[] +} + +let hubServerDependencies: HubServerDependencies | null = null + +export function setHubServerDependencies(deps: HubServerDependencies): void { + hubServerDependencies = deps +} + export function createInMemoryMCPServer( name: BuiltinMCPServerName, args: string[] = [], @@ -52,6 +70,12 @@ export function createInMemoryMCPServer( case BuiltinMCPServerNames.browser: { return new BrowserServer().server } + case BuiltinMCPServerNames.hub: { + if (!hubServerDependencies) { + throw new Error('Hub server dependencies not set. Call setHubServerDependencies first.') + } + return new HubServer(hubServerDependencies.mcpService, hubServerDependencies.mcpServersGetter).server + } default: throw new Error(`Unknown in-memory MCP server: ${name}`) } diff --git a/src/main/mcpServers/hub/index.ts b/src/main/mcpServers/hub/index.ts new file mode 100644 index 0000000000..1ec294692c --- /dev/null +++ b/src/main/mcpServers/hub/index.ts @@ -0,0 +1,163 @@ +import { loggerService } from '@logger' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js' +import type { MCPServer } from '@types' + +import { setMCPServersGetter, setMCPService } from './mcp-bridge' +import { Runtime } from './runtime' +import { searchTools } from './search' +import { ToolRegistry } from './tool-registry' +import type { ExecInput, SearchQuery } from './types' + +const logger = loggerService.withContext('MCPServer:Hub') + +interface MCPServiceInterface { + listTools(_: null, server: MCPServer): Promise + callTool( + _: null, + args: { server: MCPServer; name: string; args: unknown; callId?: string } + ): Promise<{ content: Array<{ type: string; text?: string }> }> +} + +export class HubServer { + public server: Server + private toolRegistry: ToolRegistry + private runtime: Runtime + + constructor(mcpService: MCPServiceInterface, mcpServersGetter: () => MCPServer[]) { + setMCPService(mcpService as any) + setMCPServersGetter(mcpServersGetter) + + this.toolRegistry = new ToolRegistry() + this.runtime = new Runtime() + + this.server = new Server( + { + name: 'hub-server', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ) + + this.setupRequestHandlers() + } + + private setupRequestHandlers(): void { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'search', + description: + 'Search for available MCP tools by keywords. Returns JavaScript function declarations with JSDoc that can be used in the exec tool.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: + 'Search keywords, comma-separated for OR matching. Example: "chrome,browser" matches tools with "chrome" OR "browser"' + }, + limit: { + type: 'number', + description: 'Maximum number of tools to return (default: 10, max: 50)' + } + }, + required: ['query'] + } + }, + { + name: 'exec', + description: + 'Execute JavaScript code that calls MCP tools. Use the search tool first to discover available tools and their signatures.', + inputSchema: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'JavaScript code to execute. Can use async/await. Available helpers: parallel(...promises), settle(...promises). The last expression is returned.' + } + }, + required: ['code'] + } + } + ] + } + }) + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + if (!args) { + throw new McpError(ErrorCode.InvalidParams, 'No arguments provided') + } + + try { + switch (name) { + case 'search': + return await this.handleSearch(args as unknown as SearchQuery) + case 'exec': + return await this.handleExec(args as unknown as ExecInput) + default: + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`) + } + } catch (error) { + if (error instanceof McpError) { + throw error + } + logger.error(`Error executing tool ${name}:`, error as Error) + throw new McpError( + ErrorCode.InternalError, + `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` + ) + } + }) + } + + private async handleSearch(query: SearchQuery) { + if (!query.query || typeof query.query !== 'string') { + throw new McpError(ErrorCode.InvalidParams, 'query parameter is required and must be a string') + } + + const tools = await this.toolRegistry.getTools() + const result = searchTools(tools, query) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + } + + private async handleExec(input: ExecInput) { + if (!input.code || typeof input.code !== 'string') { + throw new McpError(ErrorCode.InvalidParams, 'code parameter is required and must be a string') + } + + const tools = await this.toolRegistry.getTools() + const result = await this.runtime.execute(input.code, tools) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + } + + invalidateCache(): void { + this.toolRegistry.invalidate() + } +} + +export default HubServer diff --git a/src/main/mcpServers/hub/mcp-bridge.ts b/src/main/mcpServers/hub/mcp-bridge.ts new file mode 100644 index 0000000000..22c0c41c70 --- /dev/null +++ b/src/main/mcpServers/hub/mcp-bridge.ts @@ -0,0 +1,95 @@ +import { loggerService } from '@logger' +import { BuiltinMCPServerNames, type MCPServer, type MCPTool } from '@types' + +const logger = loggerService.withContext('MCPServer:Hub:Bridge') + +let mcpServiceInstance: MCPServiceInterface | null = null +let mcpServersGetter: (() => MCPServer[]) | null = null + +interface MCPServiceInterface { + listTools(_: null, server: MCPServer): Promise + callTool( + _: null, + args: { server: MCPServer; name: string; args: unknown; callId?: string } + ): Promise<{ content: Array<{ type: string; text?: string }> }> +} + +export function setMCPService(service: MCPServiceInterface): void { + mcpServiceInstance = service +} + +export function setMCPServersGetter(getter: () => MCPServer[]): void { + mcpServersGetter = getter +} + +export function getActiveServers(): MCPServer[] { + if (!mcpServersGetter) { + logger.warn('MCP servers getter not set') + return [] + } + + const servers = mcpServersGetter() + return servers.filter((s) => s.isActive && s.name !== BuiltinMCPServerNames.hub) +} + +export async function listToolsFromServer(server: MCPServer): Promise { + if (!mcpServiceInstance) { + logger.error('MCP service not initialized') + return [] + } + + try { + return await mcpServiceInstance.listTools(null, server) + } catch (error) { + logger.error(`Failed to list tools from server ${server.name}:`, error as Error) + return [] + } +} + +export async function callMcpTool(toolId: string, params: unknown): Promise { + if (!mcpServiceInstance) { + throw new Error('MCP service not initialized') + } + + const parts = toolId.split('__') + if (parts.length < 2) { + throw new Error(`Invalid tool ID format: ${toolId}`) + } + + const serverId = parts[0] + const toolName = parts.slice(1).join('__') + + const servers = getActiveServers() + const server = servers.find((s) => s.id === serverId) + + if (!server) { + throw new Error(`Server not found: ${serverId}`) + } + + logger.debug(`Calling tool ${toolName} on server ${server.name}`) + + const result = await mcpServiceInstance.callTool(null, { + server, + name: toolName, + args: params + }) + + return extractToolResult(result) +} + +function extractToolResult(result: { content: Array<{ type: string; text?: string }> }): unknown { + if (!result.content || result.content.length === 0) { + return null + } + + const textContent = result.content.find((c) => c.type === 'text') + if (textContent?.text) { + try { + return JSON.parse(textContent.text) + } catch { + return textContent.text + } + } + + return result.content +} diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 5fc5cf8682..2cbc1e8155 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -3,7 +3,7 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' -import { createInMemoryMCPServer } from '@main/mcpServers/factory' +import { createInMemoryMCPServer, setHubServerDependencies } from '@main/mcpServers/factory' import { makeSureDirExists, removeEnvProxy } from '@main/utils' import { buildFunctionCallToolName } from '@main/utils/mcp' import { findCommandInShellEnv, getBinaryName, getBinaryPath, isBinaryExists } from '@main/utils/process' @@ -58,6 +58,7 @@ import DxtService from './DxtService' import { CallBackServer } from './mcp/oauth/callback' import { McpOAuthClientProvider } from './mcp/oauth/provider' import { ServerLogBuffer } from './mcp/ServerLogBuffer' +import { reduxService } from './ReduxService' import { windowService } from './WindowService' // Generic type for caching wrapped functions @@ -163,6 +164,27 @@ class McpService { this.checkMcpConnectivity = this.checkMcpConnectivity.bind(this) this.getServerVersion = this.getServerVersion.bind(this) this.getServerLogs = this.getServerLogs.bind(this) + + this.initializeHubDependencies() + } + + private initializeHubDependencies(): void { + setHubServerDependencies({ + mcpService: { + listTools: (_: null, server: MCPServer) => this.listToolsImpl(server), + callTool: async (_: null, args: { server: MCPServer; name: string; args: unknown; callId?: string }) => { + return this.callTool(null as unknown as Electron.IpcMainInvokeEvent, args) + } + }, + mcpServersGetter: () => { + try { + const servers = reduxService.selectSync('state.mcp.servers') + return servers || [] + } catch { + return [] + } + } + }) } private getServerKey(server: MCPServer): string { diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index 2e6f84026e..ebe243f6d2 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -332,7 +332,8 @@ const builtInMcpDescriptionKeyMap: Record = { [BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python', [BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp', [BuiltinMCPServerNames.browser]: 'settings.mcp.builtinServersDescriptions.browser', - [BuiltinMCPServerNames.nowledgeMem]: 'settings.mcp.builtinServersDescriptions.nowledge_mem' + [BuiltinMCPServerNames.nowledgeMem]: 'settings.mcp.builtinServersDescriptions.nowledge_mem', + [BuiltinMCPServerNames.hub]: 'settings.mcp.builtinServersDescriptions.hub' } as const export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {