mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 08:59:02 +08:00
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:
parent
9e7eee826d
commit
53528770f8
@ -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}`)
|
||||
}
|
||||
|
||||
163
src/main/mcpServers/hub/index.ts
Normal file
163
src/main/mcpServers/hub/index.ts
Normal 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
|
||||
95
src/main/mcpServers/hub/mcp-bridge.ts
Normal file
95
src/main/mcpServers/hub/mcp-bridge.ts
Normal 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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user