@douyinfe/semi-mcp 1.0.19 → 1.0.21

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/stdio.js ADDED
@@ -0,0 +1,1454 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { existsSync as external_fs_existsSync, readFileSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join as external_path_join } from "path";
8
+ import { homedir } from "os";
9
+ import { mkdir, readFile, writeFile } from "fs/promises";
10
+ import { lt } from "semver";
11
+ import { parseSync } from "oxc-parser";
12
+ import axios from "axios";
13
+ function getCacheDir() {
14
+ return external_path_join(homedir(), '.semi-mcp', 'cache');
15
+ }
16
+ function file_cache_getDirectoryListCacheDir() {
17
+ return external_path_join(getCacheDir(), 'directory-list');
18
+ }
19
+ function file_cache_getFileContentCacheDir() {
20
+ return external_path_join(getCacheDir(), 'file-content');
21
+ }
22
+ function keyToFileName(key) {
23
+ return key.replace(/@/g, '_at_').replace(/\//g, '_').replace(/\\/g, '_').replace(/:/g, '_').replace(/\*/g, '_').replace(/\?/g, '_').replace(/"/g, '_').replace(/</g, '_').replace(/>/g, '_').replace(/\|/g, '_');
24
+ }
25
+ async function ensureDir(dir) {
26
+ if (!external_fs_existsSync(dir)) await mkdir(dir, {
27
+ recursive: true
28
+ });
29
+ }
30
+ async function readCache(cacheDir, key) {
31
+ try {
32
+ const fileName = keyToFileName(key);
33
+ const filePath = external_path_join(cacheDir, fileName);
34
+ if (!external_fs_existsSync(filePath)) return null;
35
+ const content = await readFile(filePath, 'utf-8');
36
+ return content;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function writeCache(cacheDir, key, content) {
42
+ try {
43
+ await ensureDir(cacheDir);
44
+ const fileName = keyToFileName(key);
45
+ const filePath = external_path_join(cacheDir, fileName);
46
+ await writeFile(filePath, content, 'utf-8');
47
+ } catch {}
48
+ }
49
+ const MIN_SUPPORTED_VERSION = '2.90.2';
50
+ function getVersionCacheDir() {
51
+ return external_path_join(getCacheDir(), 'version');
52
+ }
53
+ function getVersionCacheKey(packageName, tag) {
54
+ return `${packageName}@${tag}`;
55
+ }
56
+ function getCurrentDate() {
57
+ const now = new Date();
58
+ const year = now.getFullYear();
59
+ const month = String(now.getMonth() + 1).padStart(2, '0');
60
+ const day = String(now.getDate()).padStart(2, '0');
61
+ return `${year}-${month}-${day}`;
62
+ }
63
+ async function fetchVersionFromRegistry(packageName, tag) {
64
+ const registries = [
65
+ `https://registry.npmmirror.com/${packageName}/${tag}`,
66
+ `https://registry.npmjs.org/${packageName}/${tag}`
67
+ ];
68
+ let lastError = null;
69
+ for (const url of registries)try {
70
+ const response = await fetch(url, {
71
+ headers: {
72
+ Accept: 'application/json'
73
+ }
74
+ });
75
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
76
+ const data = await response.json();
77
+ if (data && data.version) return data.version;
78
+ throw new Error('响应中没有 version 字段');
79
+ } catch (error) {
80
+ lastError = error instanceof Error ? error : new Error(String(error));
81
+ }
82
+ throw new Error(`无法获取 ${packageName}@${tag} 的版本号: ${lastError?.message}`);
83
+ }
84
+ async function resolveVersion(packageName, version) {
85
+ if (/^\d+\.\d+\.\d+/.test(version)) if (!lt(version, MIN_SUPPORTED_VERSION)) return version;
86
+ else version = 'latest';
87
+ const cacheDir = getVersionCacheDir();
88
+ const cacheKey = getVersionCacheKey(packageName, version);
89
+ const today = getCurrentDate();
90
+ const cachedContent = await readCache(cacheDir, cacheKey);
91
+ if (cachedContent) try {
92
+ const cached = JSON.parse(cachedContent);
93
+ if (cached.date === today) return cached.version;
94
+ } catch {}
95
+ const resolvedVersion = await fetchVersionFromRegistry(packageName, version);
96
+ const cacheData = {
97
+ version: resolvedVersion,
98
+ date: today
99
+ };
100
+ await writeCache(cacheDir, cacheKey, JSON.stringify(cacheData));
101
+ return resolvedVersion;
102
+ }
103
+ const UNPKG_BASE_URL = 'https://unpkg.com';
104
+ const NPMMIRROR_BASE_URL = 'https://registry.npmmirror.com';
105
+ function getCacheKey(packageName, version, path) {
106
+ return `${packageName}@${version}/${path}`;
107
+ }
108
+ function flattenDirectoryStructure(item, result = []) {
109
+ result.push({
110
+ path: item.path,
111
+ type: item.type,
112
+ size: item.size
113
+ });
114
+ if (item.files && Array.isArray(item.files)) for (const file of item.files)flattenDirectoryStructure(file, result);
115
+ return result;
116
+ }
117
+ async function fetchNpmMirrorDirectoryRecursive(baseUrl, packageName, version, path, maxDepth = 10) {
118
+ if (maxDepth <= 0) return [];
119
+ const url = `${baseUrl}/${packageName}/${version}/files/${path}/?meta`;
120
+ const response = await fetch(url, {
121
+ headers: {
122
+ Accept: 'application/json'
123
+ }
124
+ });
125
+ if (!response.ok) throw new Error(`获取目录列表失败: ${response.status} ${response.statusText}`);
126
+ const contentType = response.headers.get('content-type') || '';
127
+ if (!contentType.includes('application/json')) throw new Error(`API 返回了非 JSON 格式: ${contentType}`);
128
+ const data = await response.json();
129
+ const normalizeType = (item)=>{
130
+ const path = item.path;
131
+ if (path.endsWith('/')) return {
132
+ path,
133
+ type: 'directory'
134
+ };
135
+ if (item.type && item.type.includes('/')) return {
136
+ path,
137
+ type: 'file'
138
+ };
139
+ if ('directory' === item.type) return {
140
+ path,
141
+ type: 'directory'
142
+ };
143
+ return {
144
+ path,
145
+ type: 'file'
146
+ };
147
+ };
148
+ const result = [];
149
+ if (data && 'object' == typeof data && 'files' in data && Array.isArray(data.files)) {
150
+ const promises = [];
151
+ for (const item of data.files){
152
+ const normalized = normalizeType(item);
153
+ result.push(normalized);
154
+ if ('directory' !== normalized.type || item.files && 0 !== item.files.length) {
155
+ if (item.files && Array.isArray(item.files) && item.files.length > 0) {
156
+ const flattened = [];
157
+ flattenDirectoryStructure(item, flattened);
158
+ const subFiles = flattened.filter((f)=>f.path !== normalized.path).map(normalizeType);
159
+ result.push(...subFiles);
160
+ }
161
+ } else {
162
+ const subPath = normalized.path.startsWith('/') ? normalized.path.slice(1) : normalized.path;
163
+ promises.push(fetchNpmMirrorDirectoryRecursive(baseUrl, packageName, version, subPath, maxDepth - 1).then((subFiles)=>subFiles.filter((f)=>f.path !== normalized.path)).catch(()=>[]));
164
+ }
165
+ }
166
+ if (promises.length > 0) {
167
+ const subResults = await Promise.all(promises);
168
+ for (const subFiles of subResults)result.push(...subFiles);
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+ async function fetchDirectoryListFromSource(baseUrl, packageName, version, path, isNpmMirror = false) {
174
+ if (isNpmMirror) return fetchNpmMirrorDirectoryRecursive(baseUrl, packageName, version, path);
175
+ const url = `${baseUrl}/${packageName}@${version}/${path}/?meta`;
176
+ const response = await fetch(url, {
177
+ headers: {
178
+ Accept: 'application/json'
179
+ }
180
+ });
181
+ if (!response.ok) throw new Error(`获取目录列表失败: ${response.status} ${response.statusText}`);
182
+ const contentType = response.headers.get('content-type') || '';
183
+ if (!contentType.includes('application/json')) throw new Error(`API 返回了非 JSON 格式: ${contentType}`);
184
+ const data = await response.json();
185
+ const normalizeType = (item)=>{
186
+ const path = item.path;
187
+ if (path.endsWith('/')) return {
188
+ path,
189
+ type: 'directory'
190
+ };
191
+ if (item.type && item.type.includes('/')) return {
192
+ path,
193
+ type: 'file'
194
+ };
195
+ if ('directory' === item.type) return {
196
+ path,
197
+ type: 'directory'
198
+ };
199
+ return {
200
+ path,
201
+ type: 'file'
202
+ };
203
+ };
204
+ if (Array.isArray(data)) return data.map(normalizeType);
205
+ if (data && 'object' == typeof data && 'files' in data) {
206
+ const filesData = data;
207
+ if (Array.isArray(filesData.files)) return filesData.files.map(normalizeType);
208
+ }
209
+ if (data && 'object' == typeof data && 'path' in data) {
210
+ const singleItem = data;
211
+ if (singleItem.files && Array.isArray(singleItem.files)) {
212
+ const flattened = [];
213
+ flattenDirectoryStructure(singleItem, flattened);
214
+ return flattened.map(normalizeType);
215
+ }
216
+ return [
217
+ normalizeType(singleItem)
218
+ ];
219
+ }
220
+ throw new Error('无法解析目录列表数据格式');
221
+ }
222
+ async function fetchDirectoryList(packageName, version, path) {
223
+ const resolvedVersion = await resolveVersion(packageName, version);
224
+ const cacheKey = getCacheKey(packageName, resolvedVersion, path);
225
+ const cacheDir = file_cache_getDirectoryListCacheDir();
226
+ const cachedContent = await readCache(cacheDir, cacheKey);
227
+ if (cachedContent) try {
228
+ const cachedResult = JSON.parse(cachedContent);
229
+ return cachedResult;
230
+ } catch {}
231
+ const unpkgPromise = fetchDirectoryListFromSource(UNPKG_BASE_URL, packageName, resolvedVersion, path, false);
232
+ const npmmirrorPromise = fetchDirectoryListFromSource(NPMMIRROR_BASE_URL, packageName, resolvedVersion, path, true);
233
+ const results = await Promise.allSettled([
234
+ unpkgPromise,
235
+ npmmirrorPromise
236
+ ]);
237
+ const successfulResults = [];
238
+ const errors = [];
239
+ if ('fulfilled' === results[0].status) successfulResults.push({
240
+ source: 'unpkg',
241
+ files: results[0].value
242
+ });
243
+ else errors.push(results[0].reason instanceof Error ? results[0].reason : new Error(String(results[0].reason)));
244
+ if ('fulfilled' === results[1].status) successfulResults.push({
245
+ source: 'npmmirror',
246
+ files: results[1].value
247
+ });
248
+ else errors.push(results[1].reason instanceof Error ? results[1].reason : new Error(String(results[1].reason)));
249
+ if (0 === successfulResults.length) throw new Error(`所有数据源都失败了: ${errors.map((e)=>e.message).join('; ')}`);
250
+ successfulResults.sort((a, b)=>{
251
+ if (b.files.length !== a.files.length) return b.files.length - a.files.length;
252
+ return 'unpkg' === a.source ? -1 : 1;
253
+ });
254
+ const result = successfulResults[0].files;
255
+ await writeCache(cacheDir, cacheKey, JSON.stringify(result));
256
+ return result;
257
+ }
258
+ async function getComponentList(version) {
259
+ const packageName = '@douyinfe/semi-ui';
260
+ const files = await fetchDirectoryList(packageName, version, 'lib');
261
+ if (!files || 0 === files.length) return [];
262
+ const componentSet = new Set();
263
+ for (const file of files){
264
+ const path = file.path;
265
+ const pathWithoutLib = path.replace(/^\/lib\//, '').replace(/^lib\//, '');
266
+ const parts = pathWithoutLib.split('/');
267
+ if (parts.length >= 2 && ('cjs' === parts[0] || 'es' === parts[0])) {
268
+ const componentName = parts[1];
269
+ if (componentName && 'lib' !== componentName) componentSet.add(componentName.toLowerCase());
270
+ } else if (parts.length >= 1) {
271
+ const componentName = parts[0];
272
+ if (componentName && 'lib' !== componentName && 'cjs' !== componentName && 'es' !== componentName) componentSet.add(componentName.toLowerCase());
273
+ }
274
+ }
275
+ return Array.from(componentSet).sort();
276
+ }
277
+ const fetch_file_content_UNPKG_BASE_URL = 'https://unpkg.com';
278
+ const fetch_file_content_NPMMIRROR_BASE_URL = 'https://registry.npmmirror.com';
279
+ function fetch_file_content_getCacheKey(packageName, version, filePath) {
280
+ return `${packageName}@${version}/${filePath}`;
281
+ }
282
+ async function fetchFileContentFromSource(baseUrl, packageName, version, filePath, isNpmMirror = false) {
283
+ const url = isNpmMirror ? `${baseUrl}/${packageName}/${version}/files/${filePath}` : `${baseUrl}/${packageName}@${version}/${filePath}`;
284
+ const response = await fetch(url, {
285
+ headers: {
286
+ Accept: 'text/plain, application/json, */*'
287
+ }
288
+ });
289
+ if (!response.ok) throw new Error(`获取文件失败: ${response.status} ${response.statusText}`);
290
+ const content = await response.text();
291
+ if (content.trim().startsWith('<!DOCTYPE html>') || content.includes('npmmirror 镜像站')) throw new Error('返回了 HTML 错误页面');
292
+ return content;
293
+ }
294
+ async function fetchFileContent(packageName, version, filePath) {
295
+ const resolvedVersion = await resolveVersion(packageName, version);
296
+ const cacheKey = fetch_file_content_getCacheKey(packageName, resolvedVersion, filePath);
297
+ const cacheDir = file_cache_getFileContentCacheDir();
298
+ const cachedContent = await readCache(cacheDir, cacheKey);
299
+ if (cachedContent) return cachedContent;
300
+ const unpkgPromise = fetchFileContentFromSource(fetch_file_content_UNPKG_BASE_URL, packageName, resolvedVersion, filePath, false);
301
+ const npmmirrorPromise = fetchFileContentFromSource(fetch_file_content_NPMMIRROR_BASE_URL, packageName, resolvedVersion, filePath, true);
302
+ const unpkgWithFallback = unpkgPromise.catch(()=>new Promise(()=>{}));
303
+ const npmmirrorWithFallback = npmmirrorPromise.catch(()=>new Promise(()=>{}));
304
+ const raceResult = await Promise.race([
305
+ unpkgWithFallback,
306
+ npmmirrorWithFallback
307
+ ]).catch(()=>null);
308
+ if (raceResult) {
309
+ await writeCache(cacheDir, cacheKey, raceResult);
310
+ return raceResult;
311
+ }
312
+ const results = await Promise.allSettled([
313
+ unpkgPromise,
314
+ npmmirrorPromise
315
+ ]);
316
+ const errors = [];
317
+ for (const result of results)if ('rejected' === result.status) errors.push(result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
318
+ throw new Error(`所有数据源都失败了: ${errors.map((e)=>e.message).join('; ')}`);
319
+ }
320
+ async function getComponentDocuments(componentName, version = 'latest') {
321
+ const packageName = '@douyinfe/semi-ui';
322
+ const componentNameLower = componentName.toLowerCase();
323
+ const contentFiles = await fetchDirectoryList(packageName, version, 'content');
324
+ if (!contentFiles || 0 === contentFiles.length) return null;
325
+ const componentFiles = contentFiles.filter((file)=>{
326
+ if ('file' !== file.type) return false;
327
+ const path = file.path.toLowerCase();
328
+ const pathPattern = new RegExp(`/content/[^/]+/${componentNameLower}/index\\.md$`);
329
+ return pathPattern.test(path);
330
+ });
331
+ if (0 === componentFiles.length) return null;
332
+ const firstPath = componentFiles[0].path;
333
+ const pathParts = firstPath.split('/');
334
+ let categoryIndex = -1;
335
+ for(let i = 0; i < pathParts.length; i++)if ('content' === pathParts[i].toLowerCase()) {
336
+ categoryIndex = i + 1;
337
+ break;
338
+ }
339
+ if (-1 === categoryIndex || categoryIndex >= pathParts.length) return null;
340
+ const category = pathParts[categoryIndex];
341
+ const documentPromises = componentFiles.map(async (file)=>{
342
+ const filePath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
343
+ const parts = file.path.split('/');
344
+ const fileName = parts[parts.length - 1];
345
+ try {
346
+ const content = await fetchFileContent(packageName, version, filePath);
347
+ return {
348
+ name: fileName,
349
+ path: file.path,
350
+ content: content
351
+ };
352
+ } catch (error) {
353
+ const errorMessage = error instanceof Error ? error.message : String(error);
354
+ return {
355
+ name: fileName,
356
+ path: file.path,
357
+ content: `获取文档内容失败: ${errorMessage}`
358
+ };
359
+ }
360
+ });
361
+ const documents = await Promise.all(documentPromises);
362
+ return {
363
+ category,
364
+ documents: documents.sort((a, b)=>a.name.localeCompare(b.name))
365
+ };
366
+ }
367
+ const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;
368
+ function extractCodeBlocks(content) {
369
+ const matches = content.match(CODE_BLOCK_REGEX);
370
+ return matches || [];
371
+ }
372
+ function replaceCodeBlocksWithPlaceholders(content, componentName) {
373
+ let index = 0;
374
+ return content.replace(CODE_BLOCK_REGEX, ()=>{
375
+ index++;
376
+ return `\`\`\`text
377
+ [代码块 #${index} 已隐藏]
378
+ 要查看此代码,请使用 get_semi_code_block 工具,传入参数:
379
+ - componentName: "${componentName}"
380
+ - codeBlockIndex: ${index}
381
+ \`\`\``;
382
+ });
383
+ }
384
+ const getSemiDocumentTool = {
385
+ name: 'get_semi_document',
386
+ description: '获取 Semi Design 组件文档、额外文档或组件列表。支持获取:1) 组件文档(如 Button、Input 等组件);2) 额外文档,包括:advanced 分类下的文档(customize-theme、dark-mode、design-source、design-to-code)、ecosystem 分类下的文档(changelog、faq、react19、tailwind、update-to-v2、web-components)、experience 分类下的文档(accessibility、content-guidelines、internationalization)、start 分类下的文档(getting-started、introduction、overview)。注意:changelog 文档较大,需要使用分页格式获取,传入 changelog-1(第1页,最新内容)、changelog-2(第2页)等格式,每页300行。对于大型文档,代码块会被替换为占位符,需要使用 get_semi_code_block 工具获取具体代码',
387
+ inputSchema: {
388
+ type: 'object',
389
+ properties: {
390
+ componentName: {
391
+ type: 'string',
392
+ description: '组件名称或文档名称。组件名称例如:Button、Input、Table 等;额外文档名称例如:customize-theme、dark-mode、design-source、design-to-code、changelog-1(changelog第1页,最新)、changelog-2(changelog第2页)等、faq、react19、tailwind、update-to-v2、web-components、accessibility、content-guidelines、internationalization、getting-started、introduction、overview 等。注意:changelog 必须使用分页格式(changelog-1、changelog-2等),不能直接传入 changelog。如果不提供,则返回组件列表'
393
+ },
394
+ version: {
395
+ type: 'string',
396
+ description: '版本号,例如 2.89.1。如果不提供,默认使用 latest'
397
+ }
398
+ },
399
+ required: []
400
+ }
401
+ };
402
+ const LARGE_DOCUMENT_THRESHOLD = 888;
403
+ const CHANGELOG_PAGE_SIZE = 300;
404
+ function generateDocumentUrl(docPath) {
405
+ const pathParts = docPath.split('/');
406
+ let contentIndex = -1;
407
+ for(let i = 0; i < pathParts.length; i++)if ('content' === pathParts[i].toLowerCase()) {
408
+ contentIndex = i;
409
+ break;
410
+ }
411
+ if (-1 === contentIndex || contentIndex + 3 >= pathParts.length) return '';
412
+ const category = pathParts[contentIndex + 1];
413
+ const componentName = pathParts[contentIndex + 2];
414
+ if (!category || !componentName) return '';
415
+ return `https://semi.design/zh-CN/${category}/${componentName}`;
416
+ }
417
+ function parseChangelogPage(componentName) {
418
+ const changelogMatch = componentName.match(/^(changelog)(?:-(\d+))?$/);
419
+ if (changelogMatch) {
420
+ const baseName = changelogMatch[1];
421
+ const pageStr = changelogMatch[2];
422
+ if (!pageStr) return {
423
+ isChangelog: true,
424
+ baseName
425
+ };
426
+ {
427
+ const page = parseInt(pageStr, 10);
428
+ return {
429
+ isChangelog: true,
430
+ page,
431
+ baseName
432
+ };
433
+ }
434
+ }
435
+ return {
436
+ isChangelog: false
437
+ };
438
+ }
439
+ function paginateChangelog(content, page) {
440
+ const lines = content.split('\n');
441
+ const totalLines = lines.length;
442
+ const totalPages = Math.ceil(totalLines / CHANGELOG_PAGE_SIZE);
443
+ const startIndex = (page - 1) * CHANGELOG_PAGE_SIZE;
444
+ const endIndex = Math.min(startIndex + CHANGELOG_PAGE_SIZE, totalLines);
445
+ const pageLines = lines.slice(startIndex, endIndex);
446
+ const pageContent = pageLines.join('\n');
447
+ return {
448
+ content: pageContent,
449
+ totalPages,
450
+ currentPage: page
451
+ };
452
+ }
453
+ async function handleGetSemiDocument(args) {
454
+ const componentName = args?.componentName;
455
+ const version = args?.version || 'latest';
456
+ try {
457
+ if (componentName) {
458
+ const changelogInfo = parseChangelogPage(componentName);
459
+ if (changelogInfo.isChangelog && !changelogInfo.page) return {
460
+ content: [
461
+ {
462
+ type: 'text',
463
+ text: `changelog 文档较大,需要使用分页方式获取。\n\n请使用以下格式获取:\n- changelog-1(第1页,最新内容)\n- changelog-2(第2页)\n- changelog-3(第3页)\n- ...\n\n页码从1开始,1为最新内容。`
464
+ }
465
+ ]
466
+ };
467
+ const actualComponentName = changelogInfo.baseName || componentName;
468
+ const result = await getComponentDocuments(actualComponentName, version);
469
+ const allComponents = await getComponentList(version);
470
+ if (!result) return {
471
+ content: [
472
+ {
473
+ type: 'text',
474
+ text: `未找到组件 "${componentName}" 的文档 (版本 ${version})。\n\n可用组件列表:${allComponents.join(', ')}`
475
+ }
476
+ ]
477
+ };
478
+ if (changelogInfo.isChangelog && changelogInfo.page) {
479
+ const page = changelogInfo.page;
480
+ if (0 === result.documents.length) return {
481
+ content: [
482
+ {
483
+ type: 'text',
484
+ text: `未找到 changelog 文档 (版本 ${version})`
485
+ }
486
+ ],
487
+ isError: true
488
+ };
489
+ const doc = result.documents[0];
490
+ const paginated = paginateChangelog(doc.content, page);
491
+ if (page < 1 || page > paginated.totalPages) return {
492
+ content: [
493
+ {
494
+ type: 'text',
495
+ text: `页码 ${page} 超出范围。changelog 文档共有 ${paginated.totalPages} 页,请使用 changelog-1 到 changelog-${paginated.totalPages}。`
496
+ }
497
+ ],
498
+ isError: true
499
+ };
500
+ const nextPageHint = page < paginated.totalPages ? `使用 changelog-${page + 1} 获取下一页` : '';
501
+ const prevPageHint = page > 1 ? `使用 changelog-${page - 1} 获取上一页` : '';
502
+ const pageHints = [
503
+ nextPageHint,
504
+ prevPageHint
505
+ ].filter(Boolean).join(',');
506
+ const header = `===== ${doc.name} (第 ${page}/${paginated.totalPages} 页) =====${pageHints ? `\n[提示: ${pageHints}]` : ''}`;
507
+ const footer = `\n\n[当前页: ${page}/${paginated.totalPages} | 总行数: ${doc.content.split('\n').length} | 每页: ${CHANGELOG_PAGE_SIZE} 行]`;
508
+ const url = generateDocumentUrl(doc.path);
509
+ const urlFooter = url ? `\n\n文档链接: ${url}` : '';
510
+ return {
511
+ content: [
512
+ {
513
+ type: 'text',
514
+ text: `${header}\n\n${paginated.content}${footer}${urlFooter}`
515
+ }
516
+ ]
517
+ };
518
+ }
519
+ const documentsWithLines = result.documents.map((doc)=>({
520
+ ...doc,
521
+ lines: doc.content.split('\n').length
522
+ }));
523
+ const hasLargeDocument = documentsWithLines.some((doc)=>doc.lines > LARGE_DOCUMENT_THRESHOLD);
524
+ if (hasLargeDocument) {
525
+ const processedDocs = result.documents.map((doc)=>{
526
+ const lines = doc.content.split('\n').length;
527
+ if (lines > LARGE_DOCUMENT_THRESHOLD) {
528
+ const codeBlocks = extractCodeBlocks(doc.content);
529
+ const processedContent = replaceCodeBlocksWithPlaceholders(doc.content, componentName);
530
+ return {
531
+ ...doc,
532
+ content: processedContent,
533
+ codeBlockCount: codeBlocks.length,
534
+ originalLines: lines
535
+ };
536
+ }
537
+ return {
538
+ ...doc,
539
+ codeBlockCount: 0,
540
+ originalLines: lines
541
+ };
542
+ });
543
+ const docContents = processedDocs.map((doc)=>{
544
+ let header = `===== ${doc.name} =====`;
545
+ if (doc.codeBlockCount > 0) header += `\n[注意: 此文档原有 ${doc.originalLines} 行,包含 ${doc.codeBlockCount} 个代码块已被隐藏。使用 get_semi_code_block 工具查看具体代码]`;
546
+ const url = generateDocumentUrl(doc.path);
547
+ const urlFooter = url ? `\n\n文档链接: ${url}` : '';
548
+ return `${header}\n\n${doc.content}${urlFooter}`;
549
+ }).join('\n\n');
550
+ return {
551
+ content: [
552
+ {
553
+ type: 'text',
554
+ text: docContents
555
+ }
556
+ ]
557
+ };
558
+ }
559
+ const docContents = result.documents.map((doc)=>{
560
+ const url = generateDocumentUrl(doc.path);
561
+ const urlFooter = url ? `\n\n文档链接: ${url}` : '';
562
+ return `===== ${doc.name} =====\n\n${doc.content}${urlFooter}`;
563
+ }).join('\n\n');
564
+ return {
565
+ content: [
566
+ {
567
+ type: 'text',
568
+ text: docContents
569
+ }
570
+ ]
571
+ };
572
+ }
573
+ {
574
+ const components = await getComponentList(version);
575
+ if (0 === components.length) return {
576
+ content: [
577
+ {
578
+ type: 'text',
579
+ text: `未找到组件列表,请检查版本号 ${version} 是否正确`
580
+ }
581
+ ],
582
+ isError: true
583
+ };
584
+ return {
585
+ content: [
586
+ {
587
+ type: 'text',
588
+ text: `Semi Design 组件列表 (版本 ${version}),共 ${components.length} 个组件:\n\n${components.join(', ')}`
589
+ }
590
+ ]
591
+ };
592
+ }
593
+ } catch (error) {
594
+ const errorMessage = error instanceof Error ? error.message : String(error);
595
+ return {
596
+ content: [
597
+ {
598
+ type: 'text',
599
+ text: `获取文档失败: ${errorMessage}`
600
+ }
601
+ ],
602
+ isError: true
603
+ };
604
+ }
605
+ }
606
+ const getSemiCodeBlockTool = {
607
+ name: 'get_semi_code_block',
608
+ description: '获取 Semi Design 组件文档中的特定代码块。当文档较大时,代码块会被隐藏,使用此工具可以获取指定序号的代码块内容',
609
+ inputSchema: {
610
+ type: 'object',
611
+ properties: {
612
+ componentName: {
613
+ type: 'string',
614
+ description: '组件名称,例如 Button、Input、Form 等'
615
+ },
616
+ codeBlockIndex: {
617
+ type: 'number',
618
+ description: '代码块序号(从 1 开始)'
619
+ },
620
+ version: {
621
+ type: 'string',
622
+ description: '版本号,例如 2.89.1。如果不提供,默认使用 latest'
623
+ }
624
+ },
625
+ required: [
626
+ 'componentName',
627
+ 'codeBlockIndex'
628
+ ]
629
+ }
630
+ };
631
+ async function handleGetSemiCodeBlock(args) {
632
+ const componentName = args?.componentName;
633
+ const codeBlockIndex = args?.codeBlockIndex;
634
+ const version = args?.version || 'latest';
635
+ if (!componentName) return {
636
+ content: [
637
+ {
638
+ type: 'text',
639
+ text: '错误: 必须提供 componentName 参数'
640
+ }
641
+ ],
642
+ isError: true
643
+ };
644
+ if (null == codeBlockIndex) return {
645
+ content: [
646
+ {
647
+ type: 'text',
648
+ text: '错误: 必须提供 codeBlockIndex 参数'
649
+ }
650
+ ],
651
+ isError: true
652
+ };
653
+ if ('number' != typeof codeBlockIndex || codeBlockIndex < 1 || !Number.isInteger(codeBlockIndex)) return {
654
+ content: [
655
+ {
656
+ type: 'text',
657
+ text: '错误: codeBlockIndex 必须是大于等于 1 的整数'
658
+ }
659
+ ],
660
+ isError: true
661
+ };
662
+ try {
663
+ const result = await getComponentDocuments(componentName, version);
664
+ if (!result) return {
665
+ content: [
666
+ {
667
+ type: 'text',
668
+ text: `错误: 未找到组件 "${componentName}" 的文档 (版本 ${version})`
669
+ }
670
+ ],
671
+ isError: true
672
+ };
673
+ const allCodeBlocks = [];
674
+ for (const doc of result.documents){
675
+ const codeBlocks = extractCodeBlocks(doc.content);
676
+ codeBlocks.forEach((block, idx)=>{
677
+ allCodeBlocks.push({
678
+ block,
679
+ docName: doc.name,
680
+ indexInDoc: idx + 1
681
+ });
682
+ });
683
+ }
684
+ if (0 === allCodeBlocks.length) return {
685
+ content: [
686
+ {
687
+ type: 'text',
688
+ text: `组件 "${componentName}" 的文档中没有代码块`
689
+ }
690
+ ]
691
+ };
692
+ if (codeBlockIndex > allCodeBlocks.length) return {
693
+ content: [
694
+ {
695
+ type: 'text',
696
+ text: `错误: 代码块序号 ${codeBlockIndex} 超出范围。组件 "${componentName}" 的文档中共有 ${allCodeBlocks.length} 个代码块 (序号范围: 1-${allCodeBlocks.length})`
697
+ }
698
+ ],
699
+ isError: true
700
+ };
701
+ const targetBlock = allCodeBlocks[codeBlockIndex - 1];
702
+ return {
703
+ content: [
704
+ {
705
+ type: 'text',
706
+ text: `组件 ${componentName} 代码块 #${codeBlockIndex} (来自 ${targetBlock.docName}):\n\n${targetBlock.block}`
707
+ }
708
+ ]
709
+ };
710
+ } catch (error) {
711
+ const errorMessage = error instanceof Error ? error.message : String(error);
712
+ return {
713
+ content: [
714
+ {
715
+ type: 'text',
716
+ text: `获取代码块失败: ${errorMessage}`
717
+ }
718
+ ],
719
+ isError: true
720
+ };
721
+ }
722
+ }
723
+ const FOUNDATION_PACKAGE = '@douyinfe/semi-foundation';
724
+ const UI_PACKAGE = '@douyinfe/semi-ui';
725
+ const EXCLUDE_DIRS = [
726
+ '/lib/',
727
+ '/es/',
728
+ '/dist/',
729
+ '/cjs/',
730
+ '/__test__/',
731
+ '/__tests__/',
732
+ '/_story/',
733
+ '/_stories/',
734
+ '/node_modules/'
735
+ ];
736
+ function shouldExcludePath(filePath) {
737
+ for (const excludeDir of EXCLUDE_DIRS)if (filePath.includes(excludeDir)) return true;
738
+ return false;
739
+ }
740
+ function findComponentDirectory(directories, componentName) {
741
+ const normalizedName = componentName.toLowerCase();
742
+ for (const dir of directories){
743
+ if ('directory' !== dir.type) continue;
744
+ const dirPath = dir.path.replace(/^\//, '').replace(/\/$/, '');
745
+ const dirName = dirPath.split('/').pop() || '';
746
+ if (dirName.toLowerCase() === normalizedName) return dirPath;
747
+ }
748
+ return null;
749
+ }
750
+ async function getPackageComponentFiles(packageName, componentName, version) {
751
+ try {
752
+ const rootFiles = await fetchDirectoryList(packageName, version, '');
753
+ const componentDir = findComponentDirectory(rootFiles, componentName);
754
+ if (!componentDir) return [];
755
+ const componentDirPrefix = `/${componentDir}/`;
756
+ const files = [];
757
+ for (const file of rootFiles)if ('file' === file.type && file.path.startsWith(componentDirPrefix)) {
758
+ if (!shouldExcludePath(file.path)) {
759
+ const relativePath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
760
+ files.push(`${packageName}/${relativePath}`);
761
+ }
762
+ }
763
+ return files;
764
+ } catch (error) {
765
+ console.error(`获取包 ${packageName} 的组件文件列表失败:`, error);
766
+ return [];
767
+ }
768
+ }
769
+ const getComponentFileListTool = {
770
+ name: 'get_component_file_list',
771
+ description: `获取 Semi Design 组件的所有文件路径列表。
772
+
773
+ 返回组件在 @douyinfe/semi-foundation 和 @douyinfe/semi-ui 中的所有文件路径。
774
+
775
+ 路径格式示例:
776
+ - @douyinfe/semi-foundation/table/foundation.ts
777
+ - @douyinfe/semi-ui/table/index.tsx
778
+ - @douyinfe/semi-ui/table/Body/BaseRow.tsx
779
+
780
+ 使用场景:
781
+ 1. 先调用此工具获取组件文件列表
782
+ 2. 再使用 get_file_code 获取感兴趣的文件代码
783
+ 3. 如需查看具体函数实现,使用 get_function_code`,
784
+ inputSchema: {
785
+ type: 'object',
786
+ properties: {
787
+ componentName: {
788
+ type: 'string',
789
+ description: '组件名称,如 Table、DatePicker、Modal 等(大小写不敏感)'
790
+ },
791
+ version: {
792
+ type: 'string',
793
+ description: '版本号,默认为 "2.89.2-alpha.3"。注意:latest 版本可能只有编译后的代码'
794
+ }
795
+ },
796
+ required: [
797
+ 'componentName'
798
+ ]
799
+ }
800
+ };
801
+ async function handleGetComponentFileList(args) {
802
+ const componentName = args?.componentName;
803
+ const version = args?.version || '2.89.2-alpha.3';
804
+ if (!componentName) return {
805
+ content: [
806
+ {
807
+ type: 'text',
808
+ text: '错误:请提供组件名称 (componentName)'
809
+ }
810
+ ],
811
+ isError: true
812
+ };
813
+ try {
814
+ const [foundationFiles, uiFiles] = await Promise.all([
815
+ getPackageComponentFiles(FOUNDATION_PACKAGE, componentName, version),
816
+ getPackageComponentFiles(UI_PACKAGE, componentName, version)
817
+ ]);
818
+ if (0 === foundationFiles.length && 0 === uiFiles.length) return {
819
+ content: [
820
+ {
821
+ type: 'text',
822
+ text: `未找到组件 "${componentName}" 的文件。请检查组件名称是否正确。`
823
+ }
824
+ ],
825
+ isError: true
826
+ };
827
+ const allFiles = [
828
+ ...foundationFiles,
829
+ ...uiFiles
830
+ ];
831
+ const stats = {
832
+ ts: allFiles.filter((f)=>f.endsWith('.ts') && !f.endsWith('.d.ts')).length,
833
+ tsx: allFiles.filter((f)=>f.endsWith('.tsx')).length,
834
+ dts: allFiles.filter((f)=>f.endsWith('.d.ts')).length,
835
+ scss: allFiles.filter((f)=>f.endsWith('.scss')).length,
836
+ other: allFiles.filter((f)=>!f.match(/\.(tsx?|d\.ts|scss)$/)).length
837
+ };
838
+ const output = [
839
+ `组件: ${componentName}`,
840
+ `版本: ${version}`,
841
+ `总文件数: ${allFiles.length}`,
842
+ "",
843
+ `文件类型统计:`,
844
+ ` .ts: ${stats.ts}`,
845
+ ` .tsx: ${stats.tsx}`,
846
+ ` .d.ts: ${stats.dts}`,
847
+ ` .scss: ${stats.scss}`,
848
+ ` 其他: ${stats.other}`,
849
+ "",
850
+ `===== 文件列表 =====`,
851
+ "",
852
+ ...allFiles,
853
+ "",
854
+ `提示: 使用 get_file_code 工具传入上述路径获取文件代码`
855
+ ];
856
+ return {
857
+ content: [
858
+ {
859
+ type: 'text',
860
+ text: output.join('\n')
861
+ }
862
+ ]
863
+ };
864
+ } catch (error) {
865
+ return {
866
+ content: [
867
+ {
868
+ type: 'text',
869
+ text: `获取组件文件列表失败: ${error instanceof Error ? error.message : String(error)}`
870
+ }
871
+ ],
872
+ isError: true
873
+ };
874
+ }
875
+ }
876
+ function traverse(node, callback) {
877
+ if (!node) return;
878
+ if (Array.isArray(node)) {
879
+ for (const child of node)traverse(child, callback);
880
+ return;
881
+ }
882
+ callback(node);
883
+ const childKeys = [
884
+ 'body',
885
+ 'declarations',
886
+ 'init',
887
+ 'expression',
888
+ 'left',
889
+ 'right',
890
+ 'properties',
891
+ 'elements',
892
+ 'argument',
893
+ 'arguments',
894
+ 'params',
895
+ 'consequent',
896
+ 'alternate',
897
+ 'test',
898
+ 'object',
899
+ 'property',
900
+ 'callee',
901
+ 'value',
902
+ 'key',
903
+ 'computed',
904
+ 'members',
905
+ 'cases',
906
+ 'discriminant',
907
+ 'handler',
908
+ 'block',
909
+ 'finalizer',
910
+ 'param',
911
+ 'declaration',
912
+ 'specifiers',
913
+ 'source',
914
+ 'exported',
915
+ 'local',
916
+ 'imported',
917
+ 'superClass',
918
+ 'decorators',
919
+ 'typeAnnotation',
920
+ 'returnType',
921
+ 'typeParameters',
922
+ 'implements',
923
+ 'extends'
924
+ ];
925
+ for (const key of childKeys){
926
+ const child = node[key];
927
+ if (child && 'object' == typeof child) traverse(child, callback);
928
+ }
929
+ }
930
+ function extractFunctionsFromAST(ast, code) {
931
+ const functions = [];
932
+ traverse(ast, (node)=>{
933
+ let funcName;
934
+ let bodyStart;
935
+ let bodyEnd;
936
+ let funcStart;
937
+ let funcEnd;
938
+ switch(node.type){
939
+ case 'FunctionDeclaration':
940
+ if (node.id?.name && node.body && 'object' == typeof node.body && !Array.isArray(node.body)) {
941
+ funcName = node.id.name;
942
+ bodyStart = node.body.start;
943
+ bodyEnd = node.body.end;
944
+ funcStart = node.start;
945
+ funcEnd = node.end;
946
+ }
947
+ break;
948
+ case 'FunctionExpression':
949
+ if (node.body && 'object' == typeof node.body && !Array.isArray(node.body)) {
950
+ funcName = node.id?.name;
951
+ bodyStart = node.body.start;
952
+ bodyEnd = node.body.end;
953
+ funcStart = node.start;
954
+ funcEnd = node.end;
955
+ }
956
+ break;
957
+ case 'ArrowFunctionExpression':
958
+ if (node.body && 'object' == typeof node.body && !Array.isArray(node.body) && 'BlockStatement' === node.body.type) {
959
+ bodyStart = node.body.start;
960
+ bodyEnd = node.body.end;
961
+ funcStart = node.start;
962
+ funcEnd = node.end;
963
+ }
964
+ break;
965
+ case 'MethodDefinition':
966
+ if (node.key && node.value && 'object' == typeof node.value) {
967
+ const keyNode = node.key;
968
+ funcName = keyNode.name || keyNode.value;
969
+ const valueNode = node.value;
970
+ if (valueNode.body && 'object' == typeof valueNode.body && !Array.isArray(valueNode.body)) {
971
+ bodyStart = valueNode.body.start;
972
+ bodyEnd = valueNode.body.end;
973
+ funcStart = node.start;
974
+ funcEnd = node.end;
975
+ }
976
+ }
977
+ break;
978
+ case 'PropertyDefinition':
979
+ if (node.key && node.value && 'object' == typeof node.value) {
980
+ const keyNode = node.key;
981
+ const valueNode = node.value;
982
+ if ('ArrowFunctionExpression' === valueNode.type || 'FunctionExpression' === valueNode.type) {
983
+ funcName = keyNode.name || keyNode.value;
984
+ if (valueNode.body && 'object' == typeof valueNode.body && !Array.isArray(valueNode.body) && 'BlockStatement' === valueNode.body.type) {
985
+ bodyStart = valueNode.body.start;
986
+ bodyEnd = valueNode.body.end;
987
+ funcStart = node.start;
988
+ funcEnd = node.end;
989
+ }
990
+ }
991
+ }
992
+ break;
993
+ case 'Property':
994
+ if (node.key && node.value && 'object' == typeof node.value) {
995
+ const keyNode = node.key;
996
+ const valueNode = node.value;
997
+ if ('FunctionExpression' === valueNode.type || 'ArrowFunctionExpression' === valueNode.type) {
998
+ funcName = keyNode.name || keyNode.value;
999
+ if (valueNode.body && 'object' == typeof valueNode.body && !Array.isArray(valueNode.body) && 'BlockStatement' === valueNode.body.type) {
1000
+ bodyStart = valueNode.body.start;
1001
+ bodyEnd = valueNode.body.end;
1002
+ funcStart = node.start;
1003
+ funcEnd = node.end;
1004
+ }
1005
+ }
1006
+ }
1007
+ break;
1008
+ }
1009
+ if (void 0 !== bodyStart && void 0 !== bodyEnd && void 0 !== funcStart && void 0 !== funcEnd) functions.push({
1010
+ name: funcName || '<anonymous>',
1011
+ bodyStart,
1012
+ bodyEnd,
1013
+ fullCode: code.slice(funcStart, funcEnd),
1014
+ start: funcStart,
1015
+ end: funcEnd
1016
+ });
1017
+ });
1018
+ functions.sort((a, b)=>a.start - b.start);
1019
+ return functions;
1020
+ }
1021
+ function extractVariableDeclarationFunctions(ast, code, existingFunctions) {
1022
+ const existingStarts = new Set(existingFunctions.map((f)=>f.bodyStart));
1023
+ traverse(ast, (node)=>{
1024
+ if ('VariableDeclaration' === node.type && node.declarations) {
1025
+ for (const decl of node.declarations)if ('VariableDeclarator' === decl.type && decl.id && decl.init) {
1026
+ const idNode = decl.id;
1027
+ const initNode = decl.init;
1028
+ if (('ArrowFunctionExpression' === initNode.type || 'FunctionExpression' === initNode.type) && initNode.body && 'object' == typeof initNode.body && !Array.isArray(initNode.body) && 'BlockStatement' === initNode.body.type) {
1029
+ const bodyStart = initNode.body.start;
1030
+ for (const func of existingFunctions)if (func.bodyStart === bodyStart && '<anonymous>' === func.name) {
1031
+ func.name = idNode.name || '<anonymous>';
1032
+ break;
1033
+ }
1034
+ if (!existingStarts.has(bodyStart)) existingFunctions.push({
1035
+ name: idNode.name || '<anonymous>',
1036
+ bodyStart,
1037
+ bodyEnd: initNode.body.end,
1038
+ fullCode: code.slice(node.start, node.end),
1039
+ start: node.start,
1040
+ end: node.end
1041
+ });
1042
+ }
1043
+ }
1044
+ }
1045
+ });
1046
+ }
1047
+ function findAllFunctions(code, filename = 'code.tsx') {
1048
+ try {
1049
+ const result = parseSync(filename, code);
1050
+ if (result.errors && result.errors.length > 0) console.warn('解析代码时有错误:', result.errors);
1051
+ const ast = result.program;
1052
+ const functions = extractFunctionsFromAST(ast, code);
1053
+ extractVariableDeclarationFunctions(ast, code, functions);
1054
+ functions.sort((a, b)=>a.start - b.start);
1055
+ return functions;
1056
+ } catch (error) {
1057
+ console.error('解析代码失败:', error);
1058
+ return [];
1059
+ }
1060
+ }
1061
+ function removeFunctionBodies(code, filename = 'code.tsx') {
1062
+ const functions = findAllFunctions(code, filename);
1063
+ if (0 === functions.length) return code;
1064
+ const sortedByBodyStart = [
1065
+ ...functions
1066
+ ].sort((a, b)=>b.bodyStart - a.bodyStart);
1067
+ let result = code;
1068
+ const replacedRanges = [];
1069
+ for (const func of sortedByBodyStart){
1070
+ const isNested = replacedRanges.some((range)=>func.bodyStart >= range.start && func.bodyEnd <= range.end);
1071
+ if (isNested) continue;
1072
+ const before = result.slice(0, func.bodyStart);
1073
+ const after = result.slice(func.bodyEnd);
1074
+ result = before + '{ ... }' + after;
1075
+ replacedRanges.push({
1076
+ start: func.bodyStart,
1077
+ end: func.bodyEnd
1078
+ });
1079
+ }
1080
+ return result;
1081
+ }
1082
+ function extractFunction(code, functionName, filename = 'code.tsx') {
1083
+ const functions = findAllFunctions(code, filename);
1084
+ const targetFunction = functions.find((f)=>f.name === functionName);
1085
+ if (!targetFunction) return null;
1086
+ return targetFunction.fullCode;
1087
+ }
1088
+ function getFunctionNames(code, filename = 'code.tsx') {
1089
+ const functions = findAllFunctions(code, filename);
1090
+ const names = functions.map((f)=>f.name).filter((name)=>'<anonymous>' !== name);
1091
+ return Array.from(new Set(names));
1092
+ }
1093
+ function parseFilePath(fullPath) {
1094
+ const match = fullPath.match(/^(@douyinfe\/semi-(?:foundation|ui))\/(.+)$/);
1095
+ if (!match) return null;
1096
+ return {
1097
+ packageName: match[1],
1098
+ filePath: match[2]
1099
+ };
1100
+ }
1101
+ function isTypeScriptFile(filePath) {
1102
+ return filePath.endsWith('.ts') || filePath.endsWith('.tsx');
1103
+ }
1104
+ const LINE_THRESHOLD = 500;
1105
+ const getFileCodeTool = {
1106
+ name: 'get_file_code',
1107
+ description: `获取 Semi Design 组件文件的代码内容。
1108
+
1109
+ 输入文件路径(从 get_component_file_list 工具获取),返回文件代码。
1110
+
1111
+ 默认行为:
1112
+ - .ts/.tsx 文件且行数 >= ${LINE_THRESHOLD}:函数体被替换为 "{ ... }",只显示代码结构
1113
+ - .ts/.tsx 文件且行数 < ${LINE_THRESHOLD}:显示完整代码
1114
+ - 其他文件(.scss 等):显示完整内容
1115
+
1116
+ 可通过 fullCode 参数强制获取完整代码(包含函数体)。
1117
+
1118
+ 路径格式示例:
1119
+ - @douyinfe/semi-foundation/table/foundation.ts
1120
+ - @douyinfe/semi-ui/table/index.tsx`,
1121
+ inputSchema: {
1122
+ type: 'object',
1123
+ properties: {
1124
+ filePath: {
1125
+ type: 'string',
1126
+ description: '文件完整路径,如 @douyinfe/semi-ui/table/index.tsx'
1127
+ },
1128
+ version: {
1129
+ type: 'string',
1130
+ description: '版本号,默认为 "2.89.2-alpha.3"'
1131
+ },
1132
+ fullCode: {
1133
+ type: 'boolean',
1134
+ description: '是否获取完整代码(包含函数体),默认为 false'
1135
+ }
1136
+ },
1137
+ required: [
1138
+ 'filePath'
1139
+ ]
1140
+ }
1141
+ };
1142
+ async function handleGetFileCode(args) {
1143
+ const filePath = args?.filePath;
1144
+ const version = args?.version || '2.89.2-alpha.3';
1145
+ const fullCode = args?.fullCode || false;
1146
+ if (!filePath) return {
1147
+ content: [
1148
+ {
1149
+ type: 'text',
1150
+ text: '错误:请提供文件路径 (filePath)'
1151
+ }
1152
+ ],
1153
+ isError: true
1154
+ };
1155
+ const parsed = parseFilePath(filePath);
1156
+ if (!parsed) return {
1157
+ content: [
1158
+ {
1159
+ type: 'text',
1160
+ text: `错误:无效的文件路径格式。路径应为 @douyinfe/semi-foundation/xxx 或 @douyinfe/semi-ui/xxx 格式。\n\n提供的路径: ${filePath}`
1161
+ }
1162
+ ],
1163
+ isError: true
1164
+ };
1165
+ try {
1166
+ const content = await fetchFileContent(parsed.packageName, version, parsed.filePath);
1167
+ const lineCount = content.split('\n').length;
1168
+ let outputContent = content;
1169
+ let processInfo = '';
1170
+ const shouldFilterFunctionBodies = isTypeScriptFile(filePath) && !fullCode && lineCount >= LINE_THRESHOLD;
1171
+ if (shouldFilterFunctionBodies) {
1172
+ outputContent = removeFunctionBodies(content);
1173
+ processInfo = '(代码较长,函数体已替换为 "{ ... }",推荐使用 get_function_code 工具读取具体函数实现)';
1174
+ }
1175
+ const output = [
1176
+ `文件: ${filePath}`,
1177
+ `版本: ${version}`,
1178
+ `行数: ${lineCount}`,
1179
+ `大小: ${content.length} 字符`,
1180
+ processInfo ? `处理: ${processInfo}` : '',
1181
+ '',
1182
+ '='.repeat(60),
1183
+ '',
1184
+ outputContent
1185
+ ].filter(Boolean);
1186
+ return {
1187
+ content: [
1188
+ {
1189
+ type: 'text',
1190
+ text: output.join('\n')
1191
+ }
1192
+ ]
1193
+ };
1194
+ } catch (error) {
1195
+ return {
1196
+ content: [
1197
+ {
1198
+ type: 'text',
1199
+ text: `获取文件内容失败: ${error instanceof Error ? error.message : String(error)}\n\n文件路径: ${filePath}\n版本: ${version}`
1200
+ }
1201
+ ],
1202
+ isError: true
1203
+ };
1204
+ }
1205
+ }
1206
+ function get_function_code_parseFilePath(fullPath) {
1207
+ const match = fullPath.match(/^(@douyinfe\/semi-(?:foundation|ui))\/(.+)$/);
1208
+ if (!match) return null;
1209
+ return {
1210
+ packageName: match[1],
1211
+ filePath: match[2]
1212
+ };
1213
+ }
1214
+ const getFunctionCodeTool = {
1215
+ name: 'get_function_code',
1216
+ description: `获取 Semi Design 组件文件中指定函数的完整实现。
1217
+
1218
+ 输入文件路径和函数名,返回函数的完整代码(包含函数体)。
1219
+
1220
+ 支持的函数类型:
1221
+ - 普通函数声明: function foo() {}
1222
+ - 箭头函数: const foo = () => {}
1223
+ - 类方法: class Foo { bar() {} }
1224
+ - getter/setter: get foo() {} / set foo() {}
1225
+
1226
+ 路径格式示例:
1227
+ - @douyinfe/semi-foundation/table/foundation.ts
1228
+ - @douyinfe/semi-ui/table/Table.tsx`,
1229
+ inputSchema: {
1230
+ type: 'object',
1231
+ properties: {
1232
+ filePath: {
1233
+ type: 'string',
1234
+ description: '文件完整路径,如 @douyinfe/semi-ui/table/Table.tsx'
1235
+ },
1236
+ functionName: {
1237
+ type: 'string',
1238
+ description: '函数名称,如 render、handleClick、adapter 等'
1239
+ },
1240
+ version: {
1241
+ type: 'string',
1242
+ description: '版本号,默认为 "2.89.2-alpha.3"'
1243
+ }
1244
+ },
1245
+ required: [
1246
+ 'filePath',
1247
+ 'functionName'
1248
+ ]
1249
+ }
1250
+ };
1251
+ async function handleGetFunctionCode(args) {
1252
+ const filePath = args?.filePath;
1253
+ const functionName = args?.functionName;
1254
+ const version = args?.version || '2.89.2-alpha.3';
1255
+ if (!filePath) return {
1256
+ content: [
1257
+ {
1258
+ type: 'text',
1259
+ text: '错误:请提供文件路径 (filePath)'
1260
+ }
1261
+ ],
1262
+ isError: true
1263
+ };
1264
+ if (!functionName) return {
1265
+ content: [
1266
+ {
1267
+ type: 'text',
1268
+ text: '错误:请提供函数名称 (functionName)'
1269
+ }
1270
+ ],
1271
+ isError: true
1272
+ };
1273
+ const parsed = get_function_code_parseFilePath(filePath);
1274
+ if (!parsed) return {
1275
+ content: [
1276
+ {
1277
+ type: 'text',
1278
+ text: `错误:无效的文件路径格式。路径应为 @douyinfe/semi-foundation/xxx 或 @douyinfe/semi-ui/xxx 格式。\n\n提供的路径: ${filePath}`
1279
+ }
1280
+ ],
1281
+ isError: true
1282
+ };
1283
+ try {
1284
+ const content = await fetchFileContent(parsed.packageName, version, parsed.filePath);
1285
+ const functionCode = extractFunction(content, functionName);
1286
+ if (!functionCode) {
1287
+ const allFunctions = getFunctionNames(content);
1288
+ return {
1289
+ content: [
1290
+ {
1291
+ type: 'text',
1292
+ text: [
1293
+ `未找到函数 "${functionName}"`,
1294
+ '',
1295
+ `文件: ${filePath}`,
1296
+ `版本: ${version}`,
1297
+ '',
1298
+ `文件中可用的函数/方法 (共 ${allFunctions.length} 个):`,
1299
+ ...allFunctions.map((name)=>` - ${name}`)
1300
+ ].join('\n')
1301
+ }
1302
+ ],
1303
+ isError: true
1304
+ };
1305
+ }
1306
+ const output = [
1307
+ `文件: ${filePath}`,
1308
+ `函数: ${functionName}`,
1309
+ `版本: ${version}`,
1310
+ '',
1311
+ '='.repeat(60),
1312
+ '',
1313
+ functionCode
1314
+ ];
1315
+ return {
1316
+ content: [
1317
+ {
1318
+ type: 'text',
1319
+ text: output.join('\n')
1320
+ }
1321
+ ]
1322
+ };
1323
+ } catch (error) {
1324
+ return {
1325
+ content: [
1326
+ {
1327
+ type: 'text',
1328
+ text: `获取函数代码失败: ${error instanceof Error ? error.message : String(error)}\n\n文件路径: ${filePath}\n函数名: ${functionName}\n版本: ${version}`
1329
+ }
1330
+ ],
1331
+ isError: true
1332
+ };
1333
+ }
1334
+ }
1335
+ const tools = [
1336
+ getSemiDocumentTool,
1337
+ getSemiCodeBlockTool,
1338
+ getComponentFileListTool,
1339
+ getFileCodeTool,
1340
+ getFunctionCodeTool
1341
+ ];
1342
+ const toolHandlers = {
1343
+ [getSemiDocumentTool.name]: handleGetSemiDocument,
1344
+ [getSemiCodeBlockTool.name]: handleGetSemiCodeBlock,
1345
+ [getComponentFileListTool.name]: handleGetComponentFileList,
1346
+ [getFileCodeTool.name]: handleGetFileCode,
1347
+ [getFunctionCodeTool.name]: handleGetFunctionCode
1348
+ };
1349
+ const server_filename = fileURLToPath(import.meta.url);
1350
+ const server_dirname = dirname(server_filename);
1351
+ function getPackageVersion() {
1352
+ try {
1353
+ const packageJsonPath = external_path_join(server_dirname, '../package.json');
1354
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
1355
+ return packageJson.version;
1356
+ } catch {
1357
+ try {
1358
+ const packageJsonPath = external_path_join(server_dirname, '../../package.json');
1359
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
1360
+ return packageJson.version;
1361
+ } catch {
1362
+ return '1.0.0';
1363
+ }
1364
+ }
1365
+ }
1366
+ function createMCPServer() {
1367
+ const version = getPackageVersion();
1368
+ const server = new Server({
1369
+ name: 'semi-mcp',
1370
+ version
1371
+ }, {
1372
+ capabilities: {
1373
+ tools: {},
1374
+ resources: {}
1375
+ }
1376
+ });
1377
+ server.setRequestHandler(ListToolsRequestSchema, async ()=>({
1378
+ tools: tools
1379
+ }));
1380
+ server.setRequestHandler(CallToolRequestSchema, async (request)=>{
1381
+ const { name, arguments: args } = request.params;
1382
+ const handler = toolHandlers[name];
1383
+ if (!handler) throw new Error(`未知的工具: ${name}`);
1384
+ return handler(args || {});
1385
+ });
1386
+ server.setRequestHandler(ListResourcesRequestSchema, async ()=>({
1387
+ resources: [
1388
+ {
1389
+ uri: 'semi://components',
1390
+ name: 'Semi Components',
1391
+ description: 'Semi Design 组件列表',
1392
+ mimeType: 'application/json'
1393
+ }
1394
+ ]
1395
+ }));
1396
+ server.setRequestHandler(ReadResourceRequestSchema, async (request)=>{
1397
+ const { uri } = request.params;
1398
+ if ('semi://components' === uri || uri.startsWith('semi://components')) {
1399
+ const version = 'latest';
1400
+ try {
1401
+ const components = await getComponentList(version);
1402
+ return {
1403
+ contents: [
1404
+ {
1405
+ uri,
1406
+ mimeType: 'application/json',
1407
+ text: JSON.stringify({
1408
+ version,
1409
+ components,
1410
+ count: components.length,
1411
+ description: 'Semi Design 组件列表',
1412
+ note: '如需指定版本,请使用 get_semi_document 工具'
1413
+ }, null, 2)
1414
+ }
1415
+ ]
1416
+ };
1417
+ } catch (error) {
1418
+ const errorMessage = error instanceof Error ? error.message : String(error);
1419
+ return {
1420
+ contents: [
1421
+ {
1422
+ uri,
1423
+ mimeType: 'application/json',
1424
+ text: JSON.stringify({
1425
+ version,
1426
+ error: errorMessage,
1427
+ components: [],
1428
+ count: 0
1429
+ }, null, 2)
1430
+ }
1431
+ ]
1432
+ };
1433
+ }
1434
+ }
1435
+ throw new Error(`未知的资源 URI: ${uri}`);
1436
+ });
1437
+ return server;
1438
+ }
1439
+ async function main() {
1440
+ const res = await axios.get("https://semi.design");
1441
+ if ("1" === res.headers['x-user-internal']) {
1442
+ console.log("Semi MCP Server (stdio) 启动失败: 字节跳动内部用户请使用内网 MCP 服务, 在字节云 MCP market 搜索 Semi");
1443
+ console.log("Semi MCP Server (stdio) Load failed: ByteDance internal users please use the internal MCP service, search Semi in ByteCloud MCP market");
1444
+ process.exit(1);
1445
+ }
1446
+ const server = createMCPServer();
1447
+ const transport = new StdioServerTransport();
1448
+ await server.connect(transport);
1449
+ }
1450
+ main().catch((error)=>{
1451
+ const errorMessage = error instanceof Error ? error.message : String(error);
1452
+ process.stderr.write(`Semi MCP Server (stdio) 启动失败: ${errorMessage}\n`);
1453
+ process.exit(1);
1454
+ });