#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import dotenv from "dotenv";
// 載入環境變數
dotenv.config();
// 解析命令列參數
function parseArgs(): {
endpoint?: string;
apiKey?: string;
knowledgeId?: string;
} {
const args = process.argv.slice(2);
const result: { endpoint?: string; apiKey?: string; knowledgeId?: string } = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
if (arg === "--endpoint" && nextArg) {
result.endpoint = nextArg;
i++;
} else if (arg === "--api-key" && nextArg) {
result.apiKey = nextArg;
i++;
} else if (arg === "--knowledge-id" && nextArg) {
result.knowledgeId = nextArg;
i++;
}
}
return result;
}
// 取得配置(優先使用命令列參數,其次使用環境變數)
const cliArgs = parseArgs();
const CONFIG = {
endpoint: cliArgs.endpoint || process.env.DIFY_KNOWLEDGE_ENDPOINT,
apiKey: cliArgs.apiKey || process.env.DIFY_API_KEY,
knowledgeId: cliArgs.knowledgeId || process.env.DIFY_KNOWLEDGE_ID,
};
// 定義 API 類型
interface RetrievalRequest {
knowledge_id: string;
query: string;
retrieval_setting: {
top_k: number;
score_threshold: number;
};
metadata_condition?: {
logical_operator?: "and" | "or";
conditions: Array<{
name: string[];
comparison_operator: string;
value?: string;
}>;
};
}
interface RetrievalRecord {
content: string;
score: number;
title: string;
metadata?: Record<string, any>;
}
interface RetrievalResponse {
records: RetrievalRecord[];
}
// Dify Knowledge API 客戶端
class DifyKnowledgeClient {
private endpoint: string;
private apiKey: string;
constructor(endpoint: string, apiKey: string) {
this.endpoint = endpoint.replace(/\/$/, ""); // 移除尾部斜線
this.apiKey = apiKey;
}
async retrieval(request: RetrievalRequest): Promise<RetrievalResponse> {
const url = `${this.endpoint}/retrieval`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`,
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `API request failed with status ${response.status}`;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error_code && errorJson.error_msg) {
errorMessage = `Error ${errorJson.error_code}: ${errorJson.error_msg}`;
}
} catch {
errorMessage += `: ${errorText}`;
}
throw new Error(errorMessage);
}
return await response.json();
}
}
// 創建 MCP Server
const server = new McpServer(
{
name: "dify-external-knowledge-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// 註冊 query_dify_knowledge 工具
server.registerTool(
"query_dify_knowledge",
{
description: "Query Dify external knowledge base to retrieve relevant information based on a search query. Returns ranked documents with content, scores, titles, and metadata.",
inputSchema: z.object({
query: z.string().describe("User's search query or question"),
top_k: z.number().optional().default(5).describe("Maximum number of results to return (default: 5)"),
score_threshold: z.number().min(0).max(1).optional().default(0.5).describe("Minimum relevance score threshold (0-1, default: 0.5)"),
metadata_condition: z.object({
logical_operator: z.enum(["and", "or"]).optional().describe("Logical operator for combining conditions (default: and)"),
conditions: z.array(
z.object({
name: z.array(z.string()).describe("Metadata field names to filter on"),
comparison_operator: z.string().describe("Comparison operator (contains, not contains, is, is not, etc.)"),
value: z.string().optional().describe("Value to compare against (optional for empty/null operators)"),
})
).describe("Conditions list"),
}).optional().describe("Optional metadata filtering conditions"),
}),
},
async (args) => {
// 驗證配置
const endpoint = CONFIG.endpoint;
const apiKey = CONFIG.apiKey;
const knowledgeId = CONFIG.knowledgeId;
if (!endpoint || !apiKey || !knowledgeId) {
return {
content: [
{
type: "text",
text: "Error: DIFY_KNOWLEDGE_ENDPOINT, DIFY_API_KEY, and DIFY_KNOWLEDGE_ID must be set via environment variables or command line arguments (--endpoint, --api-key, --knowledge-id)",
},
],
isError: true,
};
}
try {
// 構建 API 請求
const retrievalRequest: RetrievalRequest = {
knowledge_id: knowledgeId,
query: args.query,
retrieval_setting: {
top_k: args.top_k ?? 5,
score_threshold: args.score_threshold ?? 0.5,
},
};
if (args.metadata_condition) {
retrievalRequest.metadata_condition = args.metadata_condition;
}
// 調用 Dify API
const client = new DifyKnowledgeClient(endpoint, apiKey);
const response = await client.retrieval(retrievalRequest);
// 格式化回應
const formattedResults = response.records.map((record, index) => {
return `
### Result ${index + 1} (Score: ${record.score.toFixed(3)})
**Title**: ${record.title}
**Content**:
${record.content}
${record.metadata ? `\n**Metadata**: ${JSON.stringify(record.metadata, null, 2)}` : ""}
---
`.trim();
}).join("\n\n");
return {
content: [
{
type: "text",
text: `Found ${response.records.length} result(s) for query: "${args.query}"\n\n${formattedResults}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
// 啟動 Server
async function main() {
// 顯示配置來源(僅在有配置時)
if (CONFIG.endpoint || CONFIG.apiKey || CONFIG.knowledgeId) {
const configSource = cliArgs.endpoint ? "command line arguments" : "environment variables";
console.error(`Dify External Knowledge MCP Server starting with config from ${configSource}`);
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Dify External Knowledge MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});