Skip to content

Commit bb8c5c5

Browse files
authored
chore(release): 引入Deepwiki 渠道 (#24)
* feat: 增加对x6的适配 * feat: 增加DeepWiki渠道 * fix: 修复已知问题 * fix: 暂时忽略类型问题 * fix: 单测问题 * fix: x6提示词简化
1 parent 49daf56 commit bb8c5c5

6 files changed

Lines changed: 277 additions & 14 deletions

File tree

__tests__/tools/query_antv_document.json

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@
77
"properties": {
88
"library": {
99
"type": "string",
10-
"enum": ["g2", "g6", "l7", "x6", "f2", "s2", "g", "ava", "adc"],
10+
"enum": [
11+
"g2",
12+
"g6",
13+
"l7",
14+
"x6",
15+
"f2",
16+
"s2",
17+
"g",
18+
"ava",
19+
"adc"
20+
],
1121
"description": "Specified AntV library type, intelligently identified based on user query"
1222
},
1323
"query": {
@@ -48,11 +58,28 @@
4858
"description": "Subtask topic"
4959
}
5060
},
51-
"required": ["query", "topic"]
61+
"required": [
62+
"query",
63+
"topic"
64+
]
5265
},
5366
"description": "Decomposed subtask list for complex tasks, supports batch processing"
67+
},
68+
"channel": {
69+
"default": "Context7",
70+
"description": "Controls the trade-off between search speed and retrieval accuracy. Use \"Context7\" (Default) for quick, interactive responses (~2s latency). Use \"DeepWiki\" ONLY when the user explicitly requests \"deep research\", \"high accuracy\", \"comprehensive analysis\", or when the query is critical and requires verification, despite the slower speed (~20s latency).",
71+
"enum": [
72+
"Context7",
73+
"DeepWiki"
74+
],
75+
"type": "string"
5476
}
5577
},
56-
"required": ["library", "query", "topic", "intent"]
78+
"required": [
79+
"library",
80+
"query",
81+
"topic",
82+
"intent"
83+
]
5784
}
5885
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antv/mcp-server-antv",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "MCP Server for AntV visualization libraries development, which provides documentation context and examples for visualization developers.",
55
"main": "build/index.js",
66
"scripts": {
@@ -11,7 +11,6 @@
1111
"postbuild": "chmod +x build/index.js",
1212
"start": "npm run build && npx @modelcontextprotocol/inspector node build/index.js",
1313
"prepare": "npm run build",
14-
"prepublishOnly": "npm run build",
1514
"release": "npm publish",
1615
"test": "vitest"
1716
},
@@ -35,6 +34,7 @@
3534
],
3635
"dependencies": {
3736
"@modelcontextprotocol/sdk": "^1.12.0",
37+
"eventsource": "^4.1.0",
3838
"zod": "^3.25.75"
3939
},
4040
"devDependencies": {

src/constant.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export const ANTV_LIBRARY_META = {
5050
name: 'X6',
5151
description: 'Graph editing, flowcharts, diagram creation tools',
5252
keywords: '',
53-
codeStyle: '',
53+
codeStyle: `<convention>
54+
- By default, edges should connect to the node's border.
55+
</convention>
56+
`,
5457
},
5558
f2: {
5659
id: 'f2' as AntVLibrary,

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class AntVMCPServer {
1616
// Register tools with validation
1717
[ExtractAntVTopicTool, QueryAntVDocumentTool].forEach((tool) => {
1818
const { name, description, inputSchema, run } = tool;
19+
// @ts-ignore
1920
this.server.tool(name, description, inputSchema.shape, (async (
2021
args: any,
2122
) => {

src/tools/query_antv_document.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ANTV_LIBRARY_META,
1010
CONTEXT7_TOKENS,
1111
} from '../constant';
12+
import { adaptedQueryDeepWiki } from '../utils/deepwiki';
1213

1314
const QueryAntVDocumentInputSchema = z.object({
1415
library: z
@@ -50,10 +51,41 @@ const QueryAntVDocumentInputSchema = z.object({
5051
.describe(
5152
'Decomposed subtask list for complex tasks, supports batch processing',
5253
),
54+
channel: z
55+
.enum(['Context7', 'DeepWiki'])
56+
.optional()
57+
.default('Context7')
58+
.describe(
59+
'Controls the trade-off between search speed and retrieval accuracy. ' +
60+
'Use "Context7" (Default) for quick, interactive responses (~2s latency). ' +
61+
'Use "DeepWiki" ONLY when the user explicitly requests "deep research", "high accuracy", "comprehensive analysis", or when the query is critical and requires verification, despite the slower speed (~20s latency).'
62+
),
5363
});
5464

5565
type QueryAntVDocumentArgs = z.infer<typeof QueryAntVDocumentInputSchema>;
5666

67+
export async function queryDocRouter(params: {
68+
args: QueryAntVDocumentArgs;
69+
libraryId: string,
70+
topic: string,
71+
tokens?: number
72+
}) {
73+
const { args, libraryId, topic, tokens } = params;
74+
if (args.channel === 'DeepWiki') {
75+
return await adaptedQueryDeepWiki({
76+
repoName: args.library,
77+
question: topic,
78+
});
79+
80+
} else {
81+
return await fetchLibraryDocumentation(
82+
libraryId,
83+
topic,
84+
tokens,
85+
);
86+
}
87+
}
88+
5789
async function handleComplexTask(
5890
args: QueryAntVDocumentArgs,
5991
libraryId: string,
@@ -74,11 +106,12 @@ async function handleComplexTask(
74106
logger.info(
75107
`Processing subtask ${index + 1}/${subTasks.length}: ${subTask.topic}`,
76108
);
77-
const { documentation, error } = await fetchLibraryDocumentation(
109+
const { documentation, error } = await queryDocRouter({
110+
args,
78111
libraryId,
79-
subTask.topic,
80-
tokenPerSubTask,
81-
);
112+
topic: subTask.topic,
113+
tokens: tokenPerSubTask,
114+
});
82115
return { task: subTask, documentation, error };
83116
} catch (error) {
84117
logger.error(`Failed to process subtask ${index + 1}:`, error);
@@ -241,11 +274,12 @@ When to use this tool:
241274
hasDocumentation = result.hasDocumentation;
242275
} else {
243276
// Handle simple query
244-
const { documentation, error } = await fetchLibraryDocumentation(
277+
const { documentation, error } = await queryDocRouter({
278+
args,
245279
libraryId,
246-
args.topic,
247-
args.tokens,
248-
);
280+
topic: args.topic,
281+
tokens: args.tokens,
282+
});
249283
hasDocumentation =
250284
documentation !== null && documentation.trim() !== '';
251285
response = generateSimpleResponse(args, documentation, error);

src/utils/deepwiki.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3+
import EventSource from "eventsource";
4+
import { logger } from './logger';
5+
import { AntVLibrary } from '../types';
6+
7+
// ---------------------------------------------------------
8+
// 1. 全局环境配置
9+
// ---------------------------------------------------------
10+
// Node.js 环境下 MCP SDK 依赖全局 EventSource
11+
// 作为底层包,为了保证运行时安全,仅在未定义时赋值
12+
if (!global.EventSource) {
13+
// @ts-ignore
14+
global.EventSource = EventSource;
15+
}
16+
17+
// ---------------------------------------------------------
18+
// 2. 单例状态管理
19+
// ---------------------------------------------------------
20+
let _client: Client | null = null;
21+
let _transport: SSEClientTransport | null = null;
22+
let _connectingPromise: Promise<Client> | null = null;
23+
24+
/**
25+
* 获取或初始化 MCP 客户端 (单例模式)
26+
* 包含防止并发重复连接的锁机制
27+
*/
28+
async function getMcpClient(): Promise<Client> {
29+
// 如果客户端已存在且传输层看似正常,直接返回
30+
// 注意:SSEClientTransport 目前没有直观的 isConnected 属性,
31+
// 这里的检查主要是防止对象为空。更严谨的做法是监听 transport 的 close 事件来重置 _client。
32+
if (_client && _transport) {
33+
return _client;
34+
}
35+
36+
// 如果正在连接中,等待同一个 Promise 结果,防止并发请求导致多次 new Client
37+
if (_connectingPromise) {
38+
return _connectingPromise;
39+
}
40+
41+
// 开始初始化连接
42+
_connectingPromise = (async () => {
43+
try {
44+
logger.info("DeepWiki MCP: 初始化连接...");
45+
46+
const transport = new SSEClientTransport(
47+
new URL("https://mcp.deepwiki.com/sse")
48+
);
49+
50+
const client = new Client(
51+
{
52+
name: "deepwiki-node-client",
53+
version: "1.0.0",
54+
},
55+
{ capabilities: {} }
56+
);
57+
58+
// 错误处理:监听传输层关闭或错误,以便下次重连
59+
transport.onerror = (err) => {
60+
logger.error("DeepWiki MCP Transport Error:", err);
61+
resetClient(); // 出错时重置,下次调用会触发重连
62+
};
63+
64+
transport.onclose = () => {
65+
logger.info("DeepWiki MCP Connection Closed");
66+
resetClient();
67+
};
68+
69+
await client.connect(transport);
70+
71+
// 连接成功,赋值给模块级变量
72+
_client = client;
73+
_transport = transport;
74+
logger.info("DeepWiki MCP: 连接成功");
75+
76+
return client;
77+
} catch (error) {
78+
logger.error("DeepWiki MCP: 连接失败", error);
79+
resetClient();
80+
throw error;
81+
} finally {
82+
_connectingPromise = null; // 释放锁
83+
}
84+
})();
85+
86+
return _connectingPromise;
87+
}
88+
89+
/**
90+
* 重置客户端状态,强制下一次请求重新连接
91+
*/
92+
function resetClient() {
93+
_client = null;
94+
_transport = null;
95+
// 注意:不需要手动 close transport,因为通常是在出错或断开时调用此方法
96+
}
97+
98+
// ---------------------------------------------------------
99+
// 3. 业务导出函数
100+
// ---------------------------------------------------------
101+
102+
/**
103+
* 查询 DeepWiki
104+
* 外部调用就像调用普通函数一样,无需关心连接管理
105+
*/
106+
export async function queryDeepWiki(_params: {repoName: string, question: string}): Promise<string> {
107+
try {
108+
const params = {
109+
..._params,
110+
};
111+
if (!params.repoName.includes('/')) {
112+
params.repoName = getRepoName(params.repoName as AntVLibrary);
113+
}
114+
115+
// 获取单例客户端 (自动处理连接)
116+
const client = await getMcpClient();
117+
118+
// 调用工具
119+
const result: any = await client.callTool({
120+
name: "ask_question",
121+
arguments: params,
122+
});
123+
124+
// ---------------------------------------------------------
125+
// 4. 结果处理逻辑
126+
// ---------------------------------------------------------
127+
// result.content 是一个数组,可能包含 text, image 等
128+
// 这里我们使用 reduce 提取所有 text 类型的内容并拼接
129+
let answer = result.content
130+
.filter((item: any) => item.type === "text")
131+
.map((item: any) => item.text)
132+
.join("\n"); // 如果有多段文本,用换行符拼接
133+
134+
const regex = /Wiki pages you might want to explore:|View this search on DeepWiki:/i;
135+
const splitIndex = answer.search(regex);
136+
// 如果找到了标记 (splitIndex !== -1)
137+
if (splitIndex !== -1) {
138+
// 截取从开头到标记之前的部分,并清理末尾的空白
139+
answer = answer.slice(0, splitIndex).trimEnd();
140+
}
141+
142+
if (!answer || answer.startsWith('Error')) {
143+
// 防御性编程:如果返回内容为空
144+
throw new Error("DeepWiki return Empty/Error Answer, Answer = " + answer);
145+
}
146+
147+
return answer;
148+
149+
} catch (error) {
150+
logger.error("DeepWiki Query Error:", error);
151+
// 根据业务需求,这里可以选择抛出错误,或者返回空字符串/错误提示字符串
152+
// 如果是网络断开等错误,resetClient 会在下一次调用时触发重连
153+
if (_client) {
154+
// 如果调用过程中报错,为了保险起见,可以考虑重置连接状态
155+
// 视具体错误类型而定,这里简单处理:
156+
// resetClient();
157+
}
158+
throw error;
159+
}
160+
}
161+
162+
export async function adaptedQueryDeepWiki(_params: { repoName: string, question: string }) {
163+
try {
164+
const answer = await queryDeepWiki(_params);
165+
return { documentation: answer }
166+
} catch (error) {
167+
return {
168+
documentation: null,
169+
error: error instanceof Error ? error.message : String(error),
170+
}
171+
}
172+
}
173+
174+
/**
175+
* (可选) 显式关闭连接
176+
* 供 EggJS 应用在 app.beforeClose 时调用
177+
*/
178+
export async function closeDeepWikiConnection() {
179+
if (_transport) {
180+
logger.info("DeepWiki MCP: 关闭连接...");
181+
// SDK 目前可能没有直接暴露 close 方法,通常关闭 transport 即可
182+
// SSEClientTransport 内部并没有显式的 close 方法暴露出来,
183+
// 但我们可以将引用置空,让 GC 回收,或者依赖进程退出
184+
// 如果 SSEClientTransport 实现了 close,则调用:
185+
// await _transport.close();
186+
187+
// 目前 SDK 版本可以直接置空
188+
resetClient();
189+
}
190+
}
191+
192+
/**
193+
* Get the DeepWiki repoName corresponding to the AntV organization.
194+
*/
195+
export function getRepoName(library: AntVLibrary): string {
196+
if (library === 'adc') return 'ant-design/ant-design-charts';
197+
return `antvis/${library.toUpperCase()}`;
198+
}

0 commit comments

Comments
 (0)