cherry-studio/src/main/mcpServers/dify-knowledge.ts
Phantom 0187f1780e
Refactor/migrate zod v4 (#10002)
* build: 更新 zod 依赖至 v4.1.5

* refactor: 将zod导入从'zod/v4'更新为'zod'

统一使用zod主包而非v4子路径,简化依赖管理

* refactor: 统一使用命名导入方式导入zod库

* refactor(mcpServers): 更新RequestPayloadSchema的url和headers类型定义

* refactor(mcp): 将默认值从pipe移动到string以简化schema

默认值'stdio'从pipe操作中移动到string操作,使schema定义更清晰

* refactor(Markdown): 简化 CitationSchema 中 url 的验证方式

* refactor: 将类型断言改为使用 satisfies 操作符

* build: 更新 aiCore 的 zod 依赖至 v4.1.5

* refactor: 更新zod导入方式并简化基础provider类型定义

将zod从'zod/v4'更新为'zod'导入
将baseProviderSchema替换为更简洁的接口类型定义

* fix(providers): 优化provider配置的schema定义并添加注释

调整creator函数的类型定义,增加输入输出类型约束
2025-09-07 16:39:22 +08:00

260 lines
7.4 KiB
TypeScript

// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { net } from 'electron'
import { z } from 'zod'
const logger = loggerService.withContext('DifyKnowledgeServer')
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
}>
}
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[]) {
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: z.toJSONSchema(SearchKnowledgeArgsSchema)
}
]
}
})
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}`)
}
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<McpResponse> {
try {
const url = `${apiHost.replace(/\/$/, '')}/datasets`
const response = await net.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) {
logger.error('Error fetching knowledge list:', error as 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<McpResponse> {
try {
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
const response = await net.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) {
logger.error('Error searching knowledge:', error as Error)
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
isError: true
}
}
}
}
export default DifyKnowledgeServer