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 <amp@ampcode.com>
This commit is contained in:
Vaayne 2025-12-24 12:21:21 +08:00
parent 9e7eee826d
commit 53528770f8
5 changed files with 308 additions and 3 deletions

View File

@ -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<unknown[]>
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}`)
}

View File

@ -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<unknown[]>
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

View File

@ -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<MCPTool[]>
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<MCPTool[]> {
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<unknown> {
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
}

View File

@ -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<MCPServer[]>('state.mcp.servers')
return servers || []
} catch {
return []
}
}
})
}
private getServerKey(server: MCPServer): string {

View File

@ -332,7 +332,8 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
[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 => {