@douyinfe/semi-mcp 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { tmpdir } from "os";
|
|
5
8
|
const UNPKG_BASE_URL = 'https://unpkg.com';
|
|
6
9
|
const NPMMIRROR_BASE_URL = 'https://registry.npmmirror.com';
|
|
7
|
-
|
|
10
|
+
function flattenDirectoryStructure(item, result = []) {
|
|
11
|
+
result.push({
|
|
12
|
+
path: item.path,
|
|
13
|
+
type: item.type,
|
|
14
|
+
size: item.size
|
|
15
|
+
});
|
|
16
|
+
if (item.files && Array.isArray(item.files)) for (const file of item.files)flattenDirectoryStructure(file, result);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
async function fetchDirectoryListFromSource(baseUrl, packageName, version, path, isNpmMirror = false) {
|
|
8
20
|
const url = isNpmMirror ? `${baseUrl}/${packageName}/${version}/files/${path}/?meta` : `${baseUrl}/${packageName}@${version}/${path}/?meta`;
|
|
9
21
|
const response = await fetch(url, {
|
|
10
22
|
headers: {
|
|
@@ -35,15 +47,62 @@ async function fetchFromSource(baseUrl, packageName, version, path, isNpmMirror
|
|
|
35
47
|
};
|
|
36
48
|
};
|
|
37
49
|
if (Array.isArray(data)) return data.map(normalizeType);
|
|
38
|
-
if (data && 'object' == typeof data && 'files' in data
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
if (data && 'object' == typeof data && 'files' in data) {
|
|
51
|
+
if (Array.isArray(data.files)) {
|
|
52
|
+
const flattened = [];
|
|
53
|
+
for (const item of data.files)flattenDirectoryStructure(item, flattened);
|
|
54
|
+
return flattened.map(normalizeType);
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
if (data && 'object' == typeof data && 'path' in data) {
|
|
59
|
+
const singleItem = data;
|
|
60
|
+
if (singleItem.files && Array.isArray(singleItem.files)) {
|
|
61
|
+
const flattened = [];
|
|
62
|
+
flattenDirectoryStructure(singleItem, flattened);
|
|
63
|
+
return flattened.map(normalizeType);
|
|
64
|
+
}
|
|
65
|
+
return [
|
|
66
|
+
normalizeType(singleItem)
|
|
67
|
+
];
|
|
68
|
+
}
|
|
42
69
|
throw new Error('无法解析目录列表数据格式');
|
|
43
70
|
}
|
|
44
71
|
async function fetchDirectoryList(packageName, version, path) {
|
|
45
|
-
const unpkgPromise =
|
|
46
|
-
const npmmirrorPromise =
|
|
72
|
+
const unpkgPromise = fetchDirectoryListFromSource(UNPKG_BASE_URL, packageName, version, path, false);
|
|
73
|
+
const npmmirrorPromise = fetchDirectoryListFromSource(NPMMIRROR_BASE_URL, packageName, version, path, true);
|
|
74
|
+
const unpkgWithFallback = unpkgPromise.catch(()=>new Promise(()=>{}));
|
|
75
|
+
const npmmirrorWithFallback = npmmirrorPromise.catch(()=>new Promise(()=>{}));
|
|
76
|
+
const raceResult = await Promise.race([
|
|
77
|
+
unpkgWithFallback,
|
|
78
|
+
npmmirrorWithFallback
|
|
79
|
+
]).catch(()=>null);
|
|
80
|
+
if (raceResult) return raceResult;
|
|
81
|
+
const results = await Promise.allSettled([
|
|
82
|
+
unpkgPromise,
|
|
83
|
+
npmmirrorPromise
|
|
84
|
+
]);
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const result of results)if ('rejected' === result.status) errors.push(result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
|
|
87
|
+
throw new Error(`所有数据源都失败了: ${errors.map((e)=>e.message).join('; ')}`);
|
|
88
|
+
}
|
|
89
|
+
const fetch_file_content_UNPKG_BASE_URL = 'https://unpkg.com';
|
|
90
|
+
const fetch_file_content_NPMMIRROR_BASE_URL = 'https://registry.npmmirror.com';
|
|
91
|
+
async function fetchFileContentFromSource(baseUrl, packageName, version, filePath, isNpmMirror = false) {
|
|
92
|
+
const url = isNpmMirror ? `${baseUrl}/${packageName}/${version}/files/${filePath}` : `${baseUrl}/${packageName}@${version}/${filePath}`;
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
headers: {
|
|
95
|
+
Accept: 'text/plain, application/json, */*'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) throw new Error(`获取文件失败: ${response.status} ${response.statusText}`);
|
|
99
|
+
const content = await response.text();
|
|
100
|
+
if (content.trim().startsWith('<!DOCTYPE html>') || content.includes('npmmirror 镜像站')) throw new Error('返回了 HTML 错误页面');
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
103
|
+
async function fetchFileContent(packageName, version, filePath) {
|
|
104
|
+
const unpkgPromise = fetchFileContentFromSource(fetch_file_content_UNPKG_BASE_URL, packageName, version, filePath, false);
|
|
105
|
+
const npmmirrorPromise = fetchFileContentFromSource(fetch_file_content_NPMMIRROR_BASE_URL, packageName, version, filePath, true);
|
|
47
106
|
const unpkgWithFallback = unpkgPromise.catch(()=>new Promise(()=>{}));
|
|
48
107
|
const npmmirrorWithFallback = npmmirrorPromise.catch(()=>new Promise(()=>{}));
|
|
49
108
|
const raceResult = await Promise.race([
|
|
@@ -91,6 +150,11 @@ const getSemiDocumentTool = {
|
|
|
91
150
|
version: {
|
|
92
151
|
type: 'string',
|
|
93
152
|
description: '版本号,例如 2.89.1。如果不提供,默认使用 latest'
|
|
153
|
+
},
|
|
154
|
+
get_path: {
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
description: '如果为 true,将文档写入操作系统临时目录并返回路径,而不是在响应中返回文档内容。默认为 false',
|
|
157
|
+
default: false
|
|
94
158
|
}
|
|
95
159
|
},
|
|
96
160
|
required: []
|
|
@@ -117,18 +181,36 @@ async function getComponentDocuments(componentName, version) {
|
|
|
117
181
|
}
|
|
118
182
|
if (-1 === categoryIndex || categoryIndex >= pathParts.length) return null;
|
|
119
183
|
const category = pathParts[categoryIndex];
|
|
120
|
-
const
|
|
184
|
+
const documentPromises = componentFiles.map(async (file)=>{
|
|
185
|
+
const filePath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
|
|
121
186
|
const parts = file.path.split('/');
|
|
122
|
-
|
|
123
|
-
|
|
187
|
+
const fileName = parts[parts.length - 1];
|
|
188
|
+
try {
|
|
189
|
+
const content = await fetchFileContent(packageName, version, filePath);
|
|
190
|
+
return {
|
|
191
|
+
name: fileName,
|
|
192
|
+
path: file.path,
|
|
193
|
+
content: content
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return {
|
|
198
|
+
name: fileName,
|
|
199
|
+
path: file.path,
|
|
200
|
+
content: `获取文档内容失败: ${errorMessage}`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const documents = await Promise.all(documentPromises);
|
|
124
205
|
return {
|
|
125
206
|
category,
|
|
126
|
-
documents:
|
|
207
|
+
documents: documents.sort((a, b)=>a.name.localeCompare(b.name))
|
|
127
208
|
};
|
|
128
209
|
}
|
|
129
210
|
async function handleGetSemiDocument(args) {
|
|
130
211
|
const componentName = args?.componentName;
|
|
131
212
|
const version = args?.version || 'latest';
|
|
213
|
+
const getPath = args?.get_path || false;
|
|
132
214
|
try {
|
|
133
215
|
if (componentName) {
|
|
134
216
|
const result = await getComponentDocuments(componentName, version);
|
|
@@ -149,6 +231,60 @@ async function handleGetSemiDocument(args) {
|
|
|
149
231
|
}
|
|
150
232
|
]
|
|
151
233
|
};
|
|
234
|
+
const documentsWithLines = result.documents.map((doc)=>({
|
|
235
|
+
...doc,
|
|
236
|
+
lines: doc.content.split('\n').length
|
|
237
|
+
}));
|
|
238
|
+
const hasLargeDocument = documentsWithLines.some((doc)=>doc.lines > 888);
|
|
239
|
+
const userExplicitlySetGetPath = 'get_path' in args;
|
|
240
|
+
const shouldUsePath = getPath || hasLargeDocument && !userExplicitlySetGetPath;
|
|
241
|
+
if (shouldUsePath) {
|
|
242
|
+
const baseTempDir = tmpdir();
|
|
243
|
+
const tempDirName = `semi-docs-${componentName.toLowerCase()}-${version}-${Date.now()}`;
|
|
244
|
+
const tempDir = join(baseTempDir, tempDirName);
|
|
245
|
+
await mkdir(tempDir, {
|
|
246
|
+
recursive: true
|
|
247
|
+
});
|
|
248
|
+
const filePaths = [];
|
|
249
|
+
for (const doc of result.documents){
|
|
250
|
+
const filePath = join(tempDir, doc.name);
|
|
251
|
+
await writeFile(filePath, doc.content, 'utf-8');
|
|
252
|
+
filePaths.push(filePath);
|
|
253
|
+
}
|
|
254
|
+
const largeDocs = documentsWithLines.filter((doc)=>doc.lines > 888);
|
|
255
|
+
let message = `文档已保存到临时目录: ${tempDir}\n请使用文件读取工具查看文档内容。`;
|
|
256
|
+
if (hasLargeDocument && !userExplicitlySetGetPath) {
|
|
257
|
+
const largeDocNames = largeDocs.map((doc)=>`${doc.name} (${doc.lines.toLocaleString()} 行)`).join(', ');
|
|
258
|
+
message = `文档已保存到临时目录: ${tempDir}\n注意:以下文档文件较大,已自动保存到临时目录:${largeDocNames}\n请使用文件读取工具查看文档内容。`;
|
|
259
|
+
} else if (hasLargeDocument) {
|
|
260
|
+
const largeDocNames = largeDocs.map((doc)=>`${doc.name} (${doc.lines.toLocaleString()} 行)`).join(', ');
|
|
261
|
+
message = `文档已保存到临时目录: ${tempDir}\n注意:以下文档文件较大:${largeDocNames}\n请使用文件读取工具查看文档内容。`;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: JSON.stringify({
|
|
268
|
+
componentName: componentName.toLowerCase(),
|
|
269
|
+
version,
|
|
270
|
+
category: result.category,
|
|
271
|
+
tempDirectory: tempDir,
|
|
272
|
+
files: documentsWithLines.map((doc)=>({
|
|
273
|
+
name: doc.name,
|
|
274
|
+
path: join(tempDir, doc.name),
|
|
275
|
+
contentLength: doc.content.length,
|
|
276
|
+
lines: doc.lines
|
|
277
|
+
})),
|
|
278
|
+
count: result.documents.length,
|
|
279
|
+
message,
|
|
280
|
+
autoGetPath: hasLargeDocument && !userExplicitlySetGetPath,
|
|
281
|
+
allComponents,
|
|
282
|
+
allComponentsCount: allComponents.length
|
|
283
|
+
}, null, 2)
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
152
288
|
return {
|
|
153
289
|
content: [
|
|
154
290
|
{
|
|
@@ -157,7 +293,16 @@ async function handleGetSemiDocument(args) {
|
|
|
157
293
|
componentName: componentName.toLowerCase(),
|
|
158
294
|
version,
|
|
159
295
|
category: result.category,
|
|
160
|
-
documents: result.documents
|
|
296
|
+
documents: result.documents.map((doc)=>({
|
|
297
|
+
name: doc.name,
|
|
298
|
+
path: doc.path,
|
|
299
|
+
contentLength: doc.content.length
|
|
300
|
+
})),
|
|
301
|
+
contents: result.documents.map((doc)=>({
|
|
302
|
+
name: doc.name,
|
|
303
|
+
path: doc.path,
|
|
304
|
+
content: doc.content
|
|
305
|
+
})),
|
|
161
306
|
count: result.documents.length,
|
|
162
307
|
allComponents,
|
|
163
308
|
allComponentsCount: allComponents.length
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* 从 unpkg 或 npmmirror 获取目录列表
|
|
3
3
|
* 同时向两个数据源发送请求,使用第一个成功返回的结果
|
|
4
4
|
*/
|
|
5
|
+
export declare const UNPKG_BASE_URL = "https://unpkg.com";
|
|
6
|
+
export declare const NPMMIRROR_BASE_URL = "https://registry.npmmirror.com";
|
|
7
|
+
/**
|
|
8
|
+
* 从单个源获取目录列表
|
|
9
|
+
* 导出用于测试
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchDirectoryListFromSource(baseUrl: string, packageName: string, version: string, path: string, isNpmMirror?: boolean): Promise<Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
type: string;
|
|
14
|
+
}>>;
|
|
5
15
|
/**
|
|
6
16
|
* 从 unpkg 或 npmmirror 获取目录列表
|
|
7
17
|
* 同时向两个数据源发送请求,使用第一个成功返回的结果
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* 从 unpkg 或 npmmirror 获取具体文件内容
|
|
3
3
|
* 同时向两个数据源发送请求,使用第一个成功返回的结果
|
|
4
4
|
*/
|
|
5
|
+
export declare const UNPKG_BASE_URL = "https://unpkg.com";
|
|
6
|
+
export declare const NPMMIRROR_BASE_URL = "https://registry.npmmirror.com";
|
|
7
|
+
/**
|
|
8
|
+
* 从单个源获取文件内容
|
|
9
|
+
* 导出用于测试
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchFileContentFromSource(baseUrl: string, packageName: string, version: string, filePath: string, isNpmMirror?: boolean): Promise<string>;
|
|
5
12
|
/**
|
|
6
13
|
* 从 unpkg 或 npmmirror 获取具体文件内容
|
|
7
14
|
* 同时向两个数据源发送请求,使用第一个成功返回的结果
|
package/package.json
CHANGED