@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
- async function fetchFromSource(baseUrl, packageName, version, path, isNpmMirror = false) {
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 && Array.isArray(data.files)) return data.files.map(normalizeType);
39
- if (data && 'object' == typeof data && 'path' in data) return [
40
- normalizeType(data)
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 = fetchFromSource(UNPKG_BASE_URL, packageName, version, path, false);
46
- const npmmirrorPromise = fetchFromSource(NPMMIRROR_BASE_URL, packageName, version, path, true);
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 documents = componentFiles.map((file)=>{
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
- return parts[parts.length - 1].toLowerCase();
123
- }).filter((name)=>name);
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: Array.from(new Set(documents)).sort()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Semi Design MCP Server - Model Context Protocol server for Semi Design components and documentation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",