diff --git a/src/main/mcpServers/dify-knowledge.ts b/src/main/mcpServers/dify-knowledge.ts new file mode 100644 index 0000000000..c84d4fe3c8 --- /dev/null +++ b/src/main/mcpServers/dify-knowledge.ts @@ -0,0 +1,263 @@ +// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' + +interface DifyKnowledgeServerConfig { + difyKey: string + apiHost: string +} + +interface DifyListKnowledgeResponse { + id: string + name: string + description: string +} + +interface DifySearchKnowledgeResponse { + query: { + content: string + } + records: Array<{ + segment: { + id: string + position: number + document_id: string + content: string + keywords: string[] + document?: { + id: string + data_source_type: string + name: string + } + } + score: number + }> +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ToolInputSchema = ToolSchema.shape.inputSchema +type ToolInput = z.infer + +const SearchKnowledgeArgsSchema = z.object({ + id: z.string().describe('Knowledge ID'), + query: z.string().describe('Query string'), + topK: z.number().optional().describe('Number of top results to return') +}) + +type McpResponse = { + content: Array<{ type: 'text'; text: string }> + isError?: boolean +} + +class DifyKnowledgeServer { + public server: Server + private config: DifyKnowledgeServerConfig + + constructor(difyKey: string, args: string[]) { + console.log('DifyKnowledgeServer args', args) + if (args.length === 0) { + throw new Error('DifyKnowledgeServer requires at least one argument') + } + this.config = { + difyKey: difyKey, + apiHost: args[0] + } + this.server = new Server( + { + name: '@cherry/dify-knowledge-server', + version: '0.1.0' + }, + { + capabilities: { + tools: {} + } + } + ) + this.initialize() + } + + initialize() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'list_knowledges', + description: 'List all knowledges', + inputSchema: { + type: 'object', + properties: {}, + required: [] + } + }, + { + name: 'search_knowledge', + description: 'Search knowledge by id and query', + inputSchema: zodToJsonSchema(SearchKnowledgeArgsSchema) as ToolInput + } + ] + } + }) + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + const { name, arguments: args } = request.params + switch (name) { + case 'list_knowledges': { + return await this.performListKnowledges(this.config.difyKey, this.config.apiHost) + } + case 'search_knowledge': { + const parsed = SearchKnowledgeArgsSchema.safeParse(args) + if (!parsed.success) { + const errorDetails = JSON.stringify(parsed.error.format(), null, 2) + throw new Error(`无效的参数:\n${errorDetails}`) + } + + console.log('DifyKnowledgeServer search_knowledge parsed', parsed.data) + return await this.performSearchKnowledge( + parsed.data.id, + parsed.data.query, + parsed.data.topK || 6, + this.config.difyKey, + this.config.apiHost + ) + } + default: + throw new Error(`Unknown tool: ${name}`) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + content: [{ type: 'text', text: `Error: ${errorMessage}` }], + isError: true + } + } + }) + } + + private async performListKnowledges(difyKey: string, apiHost: string): Promise { + try { + const url = `${apiHost.replace(/\/$/, '')}/datasets` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${difyKey}` + } + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`) + } + + const apiResponse = await response.json() + + const knowledges: DifyListKnowledgeResponse[] = + apiResponse?.data?.map((item: any) => ({ + id: item.id, + name: item.name, + description: item.description || '' + })) || [] + + const listText = + knowledges.length > 0 + ? knowledges.map((k) => `- **${k.name}** (ID: ${k.id})\n ${k.description || 'No Description'}`).join('\n') + : '- No knowledges found.' + + const formattedText = `### 可用知识库:\n\n${listText}` + + return { + content: [{ type: 'text', text: formattedText }] + } + } catch (error) { + console.error('获取知识库列表时出错:', error) + const errorMessage = error instanceof Error ? error.message : String(error) + // 返回包含错误信息的 MCP 响应 + return { + content: [{ type: 'text', text: `Accessing Knowledge Error: ${errorMessage}` }], + isError: true + } + } + } + + private async performSearchKnowledge( + id: string, + query: string, + topK: number, + difyKey: string, + apiHost: string + ): Promise { + try { + const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve` + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${difyKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: query, + retrieval_model: { + top_k: topK, + // will be error if not set + reranking_enable: null, + score_threshold_enabled: null + } + }) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`) + } + + const searchResponse: DifySearchKnowledgeResponse = await response.json() + + if (!searchResponse || !Array.isArray(searchResponse.records)) { + throw new Error(`从 Dify API 收到的响应格式无效: ${JSON.stringify(searchResponse)}`) + } + + const header = `### Query: ${query}\n\n` + let body: string + + if (searchResponse.records.length === 0) { + body = 'No results found.' + } else { + const resultsText = searchResponse.records + .map((record, index) => { + const docName = record.segment.document?.name || 'Unknown Document' + const content = record.segment.content.trim() + const score = record.score + const keywords = record.segment.keywords || [] + + let resultEntry = `#### ${index + 1}. ${docName} (Relevant Score: ${(score * 100).toFixed(1)}%)` + resultEntry += `\n${content}` + if (keywords.length > 0) { + resultEntry += `\n*Keywords: ${keywords.join(', ')}*` + } + return resultEntry + }) + .join('\n\n') + + body = `Found ${searchResponse.records.length} results:\n\n${resultsText}` + } + + const formattedText = header + body + + return { + content: [{ type: 'text', text: formattedText }] + } + } catch (error) { + console.error('搜索知识库时出错:', error) + const errorMessage = error instanceof Error ? error.message : String(error) + return { + content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }], + isError: true + } + } + } +} + +export default DifyKnowledgeServer diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 479ec23c1f..1c508f8844 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -2,6 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js' import Logger from 'electron-log' import BraveSearchServer from './brave-search' +import DifyKnowledgeServer from './dify-knowledge' import FetchServer from './fetch' import FileSystemServer from './filesystem' import MemoryServer from './memory' @@ -26,6 +27,10 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs: case '@cherry/filesystem': { return new FileSystemServer(args).server } + case '@cherry/dify-knowledge': { + const difyKey = envs.DIFY_KEY + return new DifyKnowledgeServer(difyKey, args).server + } default: throw new Error(`Unknown in-memory MCP server: ${name}`) } diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 1d075e70e2..03d6caaac4 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -97,6 +97,13 @@ export const builtinMCPServers: MCPServer[] = [ type: 'inMemory', description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器', isActive: false + }, + { + id: nanoid(), + name: '@cherry/dify-knowledge', + type: 'inMemory', + description: 'Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互', + isActive: false } ]