Skip to content

Commit 18b063c

Browse files
committed
make mcp search as native search
1 parent c477f66 commit 18b063c

File tree

11 files changed

+313
-13
lines changed

11 files changed

+313
-13
lines changed

src/main/presenter/configPresenter/mcpConfHelper.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ const DEFAULT_INMEMORY_SERVERS: Record<string, MCPServerConfig> = {
3838
command: 'artifacts',
3939
env: {},
4040
disable: false
41+
},
42+
bochaSearch: {
43+
args: [],
44+
descriptions: 'DeepChat内置网络搜索服务',
45+
icons: '🔍',
46+
autoApprove: ['all'],
47+
type: 'inmemory' as MCPServerType,
48+
command: 'bochaSearch',
49+
env: {
50+
apiKey: 'YOUR_BOCHA_API_KEY' // 需要用户提供实际的API Key
51+
},
52+
disable: false
4153
}
4254
}
4355

src/main/presenter/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ export class Presenter implements IPresenter {
8282

8383
// 流式响应事件
8484
eventBus.on(STREAM_EVENTS.RESPONSE, (msg) => {
85-
this.windowPresenter.mainWindow?.webContents.send(STREAM_EVENTS.RESPONSE, msg)
85+
const dataToRender = { ...msg }
86+
delete dataToRender.tool_call_response_raw // 删除 rawData 字段,此处不需要上发给 renderer,节约性能
87+
this.windowPresenter.mainWindow?.webContents.send(STREAM_EVENTS.RESPONSE, dataToRender)
8688
})
8789

8890
eventBus.on(STREAM_EVENTS.END, (msg) => {

src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,6 @@ ${context}
819819
role: 'user',
820820
content: contentBlocks
821821
})
822-
823822
yield {
824823
content: '',
825824
tool_call: 'end',
@@ -829,7 +828,8 @@ ${context}
829828
tool_call_id: `anthropic-${toolCall.id}`,
830829
tool_call_server_name: mcpToolCall.server.name,
831830
tool_call_server_icons: mcpToolCall.server.icons,
832-
tool_call_server_description: mcpToolCall.server.description
831+
tool_call_server_description: mcpToolCall.server.description,
832+
tool_call_response_raw: toolResponse.rawData
833833
}
834834
} catch (error) {
835835
console.error('工具调用失败:', error)

src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,8 @@ export class GeminiProvider extends BaseLLMProvider {
928928
tool_call_id: toolCallId,
929929
tool_call_server_name: mcpToolCall.server.name,
930930
tool_call_server_icons: mcpToolCall.server.icons,
931-
tool_call_server_description: mcpToolCall.server.description
931+
tool_call_server_description: mcpToolCall.server.description,
932+
tool_call_response_raw: toolResponse.rawData
932933
}
933934

934935
// 设置需要继续对话的标志

src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,8 @@ export class OllamaProvider extends BaseLLMProvider {
495495
tool_call_id: `ollama-${toolCall.id}`,
496496
tool_call_server_name: mcpTool.server.name,
497497
tool_call_server_icons: mcpTool.server.icons,
498-
tool_call_server_description: mcpTool.server.description
498+
tool_call_server_description: mcpTool.server.description,
499+
tool_call_response_raw: toolCallResponse.rawData
499500
}
500501
// 将工具响应添加到消息中
501502
conversationMessages.push({

src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,8 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
557557
tool_call_params: toolCall.function.arguments,
558558
tool_call_server_name: mcpTool.server.name,
559559
tool_call_server_icons: mcpTool.server.icons,
560-
tool_call_server_description: mcpTool.server.description
560+
tool_call_server_description: mcpTool.server.description,
561+
tool_call_response_raw: toolCallResponse.rawData
561562
}
562563
// 将工具响应添加到消息中
563564
if (supportsFunctionCall) {
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2+
import {
3+
CallToolRequestSchema,
4+
ListToolsRequestSchema,
5+
ToolSchema
6+
} from '@modelcontextprotocol/sdk/types.js'
7+
import { z } from 'zod'
8+
import { zodToJsonSchema } from 'zod-to-json-schema'
9+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport'
10+
import axios from 'axios'
11+
12+
// Schema definitions
13+
const BochaWebSearchArgsSchema = z.object({
14+
query: z.string().describe('Search query (max 400 chars, 50 words)'),
15+
count: z.number().optional().default(10).describe('Number of results (1-50, default 10)'),
16+
page: z.number().optional().default(1).describe('Page number (default 1)')
17+
})
18+
19+
const ToolInputSchema = ToolSchema.shape.inputSchema
20+
type ToolInput = z.infer<typeof ToolInputSchema>
21+
22+
// 定义Bocha API返回的数据结构
23+
interface BochaSearchResponse {
24+
msg: string | null
25+
data: {
26+
_type: string
27+
queryContext: {
28+
originalQuery: string
29+
}
30+
webPages: {
31+
webSearchUrl: string
32+
totalEstimatedMatches: number
33+
value: Array<{
34+
id: string | null
35+
name: string
36+
url: string
37+
displayUrl: string
38+
snippet: string
39+
siteName: string
40+
siteIcon: string
41+
dateLastCrawled: string
42+
cachedPageUrl: string | null
43+
language: string | null
44+
isFamilyFriendly: boolean | null
45+
isNavigational: boolean | null
46+
}>
47+
isFamilyFriendly: boolean | null
48+
}
49+
videos: unknown | null
50+
}
51+
}
52+
53+
export class BochaSearchServer {
54+
private server: Server
55+
private apiKey: string
56+
57+
constructor(env?: Record<string, string>) {
58+
if (!env?.apiKey) {
59+
throw new Error('需要提供Bocha API Key')
60+
}
61+
this.apiKey = env.apiKey
62+
63+
// 创建服务器实例
64+
this.server = new Server(
65+
{
66+
name: 'deepchat-inmemory/bocha-search-server',
67+
version: '0.1.0'
68+
},
69+
{
70+
capabilities: {
71+
tools: {}
72+
}
73+
}
74+
)
75+
76+
// 设置请求处理器
77+
this.setupRequestHandlers()
78+
}
79+
80+
// 启动服务器
81+
public startServer(transport: Transport): void {
82+
this.server.connect(transport)
83+
}
84+
85+
// 设置请求处理器
86+
private setupRequestHandlers(): void {
87+
// 设置工具列表处理器
88+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
89+
return {
90+
tools: [
91+
{
92+
name: 'bocha_web_search',
93+
description:
94+
'Performs a web search using the Bocha AI Search API, ideal for general queries, news, articles, and online content.。' +
95+
'Use this for broad information gathering, recent events, or when you need diverse web sources.' +
96+
'Supports pagination, content filtering, and freshness controls. ' +
97+
'Maximum 50 results per request, with page for pagination.',
98+
inputSchema: zodToJsonSchema(BochaWebSearchArgsSchema) as ToolInput
99+
}
100+
]
101+
}
102+
})
103+
104+
// 设置工具调用处理器
105+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
106+
try {
107+
const { name, arguments: args } = request.params
108+
109+
switch (name) {
110+
case 'bocha_web_search': {
111+
const parsed = BochaWebSearchArgsSchema.safeParse(args)
112+
if (!parsed.success) {
113+
throw new Error(`无效的搜索参数: ${parsed.error}`)
114+
}
115+
116+
const { query, count } = parsed.data
117+
118+
// 调用Bocha API
119+
const response = await axios.post(
120+
'https://api.bochaai.com/v1/web-search',
121+
{
122+
query,
123+
summary: true,
124+
freshness: 'noLimit',
125+
count
126+
},
127+
{
128+
headers: {
129+
Authorization: `Bearer ${this.apiKey}`,
130+
'Content-Type': 'application/json'
131+
}
132+
}
133+
)
134+
135+
// 处理响应数据
136+
const searchResponse = response.data as BochaSearchResponse
137+
138+
if (!searchResponse.data?.webPages?.value) {
139+
return {
140+
content: [
141+
{
142+
type: 'text',
143+
text: '搜索未返回任何结果。'
144+
}
145+
]
146+
}
147+
}
148+
149+
// 将结果转换为MCP资源格式
150+
const results = searchResponse.data.webPages.value.map((item, index) => {
151+
// 构建blob内容
152+
const blobContent = {
153+
title: item.name,
154+
url: item.url,
155+
rank: index + 1,
156+
content: item.snippet,
157+
icon: item.siteIcon
158+
}
159+
160+
return {
161+
type: 'resource',
162+
resource: {
163+
uri: item.url,
164+
mimeType: 'application/deepchat-webpage',
165+
text: JSON.stringify(blobContent)
166+
}
167+
}
168+
})
169+
170+
// 添加搜索摘要
171+
const summary = {
172+
type: 'text',
173+
text: `为您找到关于"${query}"的${results.length}个结果`
174+
}
175+
176+
return {
177+
content: [summary, ...results]
178+
}
179+
}
180+
181+
default:
182+
throw new Error(`未知工具: ${name}`)
183+
}
184+
} catch (error) {
185+
const errorMessage = error instanceof Error ? error.message : String(error)
186+
return {
187+
content: [{ type: 'text', text: `错误: ${errorMessage}` }],
188+
isError: true
189+
}
190+
}
191+
})
192+
}
193+
}

src/main/presenter/mcpPresenter/inMemoryServers/builder.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { ArtifactsServer } from './artifactsServer'
22
import { FileSystemServer } from './filesystem'
3+
import { BochaSearchServer } from './bochaSearchServer'
34

4-
export function getInMemoryServer(serverName: string, args: string[]) {
5+
export function getInMemoryServer(
6+
serverName: string,
7+
args: string[],
8+
env?: Record<string, string>
9+
) {
510
switch (serverName) {
611
case 'buildInFileSystem':
712
return new FileSystemServer(args)
813
case 'Artifacts':
914
return new ArtifactsServer()
15+
case 'bochaSearch':
16+
return new BochaSearchServer(env)
1017
default:
1118
throw new Error(`Unknown in-memory server: ${serverName}`)
1219
}

src/main/presenter/mcpPresenter/mcpClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export class McpClient {
9797
const runtimePath = path
9898
.join(app.getAppPath(), 'runtime', 'node')
9999
.replace('app.asar', 'app.asar.unpacked')
100-
console.log('runtimePath', runtimePath)
100+
console.info('runtimePath', runtimePath)
101101
// 检查运行时文件是否存在
102102
if (process.platform === 'win32') {
103103
const nodeExe = path.join(runtimePath, 'node.exe')
@@ -130,13 +130,13 @@ export class McpClient {
130130
if (this.serverConfig.type === 'inmemory') {
131131
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
132132
const _args = Array.isArray(this.serverConfig.args) ? this.serverConfig.args : []
133-
const _server = getInMemoryServer(this.serverName, _args)
133+
const _env = this.serverConfig.env ? (this.serverConfig.env as Record<string, string>) : {}
134+
const _server = getInMemoryServer(this.serverName, _args, _env)
134135
_server.startServer(serverTransport)
135136
this.transport = clientTransport
136137
} else if (this.serverConfig.type === 'stdio') {
137138
// 创建合适的transport
138139
const command = this.serverConfig.command as string
139-
console.log('final command', command)
140140
const HOME_DIR = app.getPath('home')
141141

142142
// 定义允许的环境变量白名单

0 commit comments

Comments
 (0)