@zat-design/sisyphus-react-mcp 1.0.2 → 4.5.7

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/src/index.mjs CHANGED
@@ -2,326 +2,473 @@
2
2
  /**
3
3
  * @zat-design/sisyphus-react MCP server。
4
4
  *
5
- * stdio 传输,启动时加载包内随附的 AI 元数据 JSON(meta/),离线可用。
6
- * AI 编码助手暴露四个查询工具:
7
- * sisyphus-list-components / sisyphus-get-component-api / sisyphus-get-examples / sisyphus-search
8
- * 返回内容经脱水压缩(去呈现噪声、双语保留中文、去重),降低 token 占用。
5
+ * 服务读取包内同版本 AI 元数据,并校验当前业务项目安装的
6
+ * @zat-design/sisyphus-react MCP 完全同版。
9
7
  */
8
+ import crypto from 'node:crypto';
10
9
  import fs from 'node:fs';
11
10
  import path from 'node:path';
11
+ import { createRequire } from 'node:module';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
14
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
15
  import { z } from 'zod';
16
16
 
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const META_DIR = path.resolve(__dirname, '..', 'meta');
17
+ const SUPPORTED_SCHEMA_VERSION = 2;
18
+ const PACKAGE_ROOT = path.dirname(fileURLToPath(new URL('../package.json', import.meta.url)));
19
+ const PACKAGE_VERSION = JSON.parse(
20
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
21
+ ).version;
19
22
 
20
- // ---------- 元数据加载(缺失/损坏时显式报错,不静默返回空)----------
23
+ function resolveMetaDir() {
24
+ if (process.env.SISYPHUS_META_DIR) return path.resolve(process.env.SISYPHUS_META_DIR);
25
+ return path.join(PACKAGE_ROOT, 'meta');
26
+ }
21
27
 
22
- /** 读取并校验一个元数据 JSON。 */
23
- function loadMeta(file) {
24
- const fp = path.join(META_DIR, file);
25
- if (!fs.existsSync(fp)) {
26
- throw new Error(`[sisyphus-mcp] 元数据文件缺失: ${fp}。请确认包内 meta/ 目录完整。`);
27
- }
28
- let parsed;
28
+ function resolveComponentPackage() {
29
29
  try {
30
- parsed = JSON.parse(fs.readFileSync(fp, 'utf8'));
31
- } catch (err) {
32
- throw new Error(`[sisyphus-mcp] 元数据文件损坏,无法解析 JSON: ${fp}(${err.message})`);
30
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
31
+ let current = path.dirname(projectRequire.resolve('@zat-design/sisyphus-react'));
32
+ while (current !== path.dirname(current)) {
33
+ const packageFile = path.join(current, 'package.json');
34
+ if (fs.existsSync(packageFile)) {
35
+ const packageJson = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
36
+ if (packageJson.name === '@zat-design/sisyphus-react') {
37
+ return { version: packageJson.version, file: packageFile };
38
+ }
39
+ }
40
+ current = path.dirname(current);
41
+ }
42
+ throw new Error('已解析入口,但未找到组件包 package.json');
43
+ } catch (error) {
44
+ throw new Error(
45
+ '[sisyphus-mcp] 无法从当前项目解析 @zat-design/sisyphus-react。' +
46
+ '请确认业务项目已安装组件库。' +
47
+ `原始错误:${error.message}`,
48
+ );
33
49
  }
34
- return parsed;
35
50
  }
36
51
 
37
- const COMPONENTS_META = loadMeta('components.json');
38
- const PROPS_META = loadMeta('props.json');
39
- const EXAMPLES_META = loadMeta('examples.json');
52
+ function parseJson(file) {
53
+ if (!fs.existsSync(file)) throw new Error(`[sisyphus-mcp] 元数据文件缺失: ${file}`);
54
+ try {
55
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ } catch (error) {
57
+ throw new Error(`[sisyphus-mcp] 元数据 JSON 损坏: ${file}(${error.message})`);
58
+ }
59
+ }
40
60
 
41
- const VERSION = COMPONENTS_META.version || 'unknown';
42
- const COMPONENTS = COMPONENTS_META.components || [];
43
- const PROPS = PROPS_META.components || {};
44
- const EXAMPLES = EXAMPLES_META.examples || [];
61
+ function loadMeta() {
62
+ const componentPackage = resolveComponentPackage();
63
+ const dir = resolveMetaDir();
64
+ const manifest = parseJson(path.join(dir, 'manifest.json'));
65
+ if (manifest.schemaVersion !== SUPPORTED_SCHEMA_VERSION) {
66
+ throw new Error(
67
+ `[sisyphus-mcp] 不支持元数据 schema v${manifest.schemaVersion},` +
68
+ `当前 MCP 支持 v${SUPPORTED_SCHEMA_VERSION}`,
69
+ );
70
+ }
71
+ if (manifest.version !== PACKAGE_VERSION) {
72
+ throw new Error(
73
+ `[sisyphus-mcp] 版本不一致:MCP v${PACKAGE_VERSION},` +
74
+ `内置元数据 v${manifest.version}。请重新构建并发布同版 MCP。`,
75
+ );
76
+ }
77
+ if (componentPackage.version !== PACKAGE_VERSION) {
78
+ throw new Error(
79
+ `[sisyphus-mcp] 版本不一致:MCP v${PACKAGE_VERSION},` +
80
+ `项目组件库 v${componentPackage.version}。请安装完全相同版本的三包。`,
81
+ );
82
+ }
83
+ const result = { dir, manifest, componentPackage };
84
+ for (const name of ['components', 'props', 'examples', 'types']) {
85
+ const fileName = `${name}.json`;
86
+ const full = path.join(dir, fileName);
87
+ if (!fs.existsSync(full)) throw new Error(`[sisyphus-mcp] 元数据文件缺失: ${full}`);
88
+ const raw = fs.readFileSync(full, 'utf8').trimEnd();
89
+ const hash = crypto.createHash('sha256').update(raw).digest('hex');
90
+ if (manifest.files?.[fileName] !== hash) {
91
+ throw new Error(`[sisyphus-mcp] 元数据校验失败: ${fileName}`);
92
+ }
93
+ const parsed = parseJson(full);
94
+ if (parsed.schemaVersion !== manifest.schemaVersion || parsed.version !== manifest.version) {
95
+ throw new Error(`[sisyphus-mcp] 元数据版本不一致: ${fileName}`);
96
+ }
97
+ result[name] = parsed;
98
+ }
99
+ return result;
100
+ }
45
101
 
46
- if (!COMPONENTS.length) {
47
- throw new Error('[sisyphus-mcp] 元数据为空:components.json 不含任何组件,拒绝启动。');
102
+ let META;
103
+ try {
104
+ META = loadMeta();
105
+ } catch (error) {
106
+ console.error(error.message);
107
+ process.exit(1);
48
108
  }
109
+ const VERSION = META.componentPackage.version;
110
+ const COMPONENTS = META.components.components || [];
111
+ const PROPS = META.props.components || {};
112
+ const EXAMPLES = META.examples.examples || [];
113
+ const TYPES = META.types.types || [];
49
114
 
50
- // ---------- 脱水/压缩工具(供各工具复用)----------
115
+ if (!COMPONENTS.length) throw new Error('[sisyphus-mcp] 组件元数据为空,拒绝启动');
51
116
 
52
- /** 折叠多余空行、去首尾空白。 */
53
- function dehydrateText(s) {
54
- if (!s) return '';
55
- return s
117
+ function dehydrateText(value = '') {
118
+ return value
56
119
  .replace(/\r\n/g, '\n')
57
120
  .replace(/\n{3,}/g, '\n\n')
58
121
  .trim();
59
122
  }
60
123
 
61
- /**
62
- * 双语去重:若描述同时含中英文,仅保留中文部分。
63
- * 简单策略——按句子拆分,剔除不含 CJK 字符的纯英文句子(保留含中文的)。
64
- * 全英文描述则原样保留。
65
- */
66
- function preferChinese(s) {
67
- if (!s) return '';
68
- const hasCJK = /[一-龥]/.test(s);
69
- if (!hasCJK) return s.trim();
70
- return s
71
- .split(/(?<=[。;!?\n])/)
72
- .map((seg) => seg.trim())
73
- .filter((seg) => seg && /[一-龥]/.test(seg))
74
- .join('')
75
- .trim() || s.trim();
124
+ function preferChinese(value = '') {
125
+ if (!/[\u4e00-\u9fa5]/.test(value)) return value.trim();
126
+ return (
127
+ value
128
+ .split(/(?<=[。;!?\n])/)
129
+ .map((part) => part.trim())
130
+ .filter((part) => part && /[\u4e00-\u9fa5]/.test(part))
131
+ .join('') || value.trim()
132
+ );
76
133
  }
77
134
 
78
- /** 组装一个 prop 的精简描述行。 */
79
135
  function formatProp(name, info) {
80
136
  const parts = [`- ${name}`];
81
137
  if (info.type) parts.push(`\`${info.type}\``);
82
- if (info.required) parts.push('(必填)');
83
- if (info.defaultValue !== undefined && info.defaultValue !== '')
138
+ if (info.required === true) parts.push('(必填)');
139
+ if (info.defaultValue !== undefined && info.defaultValue !== '') {
84
140
  parts.push(`默认 \`${info.defaultValue}\``);
85
- const desc = preferChinese(dehydrateText(info.description || ''));
86
- if (desc) parts.push(`— ${desc}`);
141
+ }
142
+ const description = preferChinese(dehydrateText(info.description || ''));
143
+ if (description) parts.push(`— ${description}`);
87
144
  return parts.join(' ');
88
145
  }
89
146
 
90
- /** 文本结果包装。 */
91
147
  function textResult(text) {
92
148
  return { content: [{ type: 'text', text }] };
93
149
  }
94
150
 
95
- /** Levenshtein 距离,用于「组件不存在」时给候选。 */
96
151
  function editDistance(a, b) {
97
- const m = a.length;
98
- const n = b.length;
99
- const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
100
- for (let j = 0; j <= n; j++) dp[0][j] = j;
101
- for (let i = 1; i <= m; i++) {
102
- for (let j = 1; j <= n; j++) {
152
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
153
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
154
+ for (let i = 1; i <= a.length; i++) {
155
+ for (let j = 1; j <= b.length; j++) {
103
156
  dp[i][j] =
104
157
  a[i - 1] === b[j - 1]
105
158
  ? dp[i - 1][j - 1]
106
159
  : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
107
160
  }
108
161
  }
109
- return dp[m][n];
162
+ return dp[a.length][b.length];
163
+ }
164
+
165
+ function componentNames(component) {
166
+ return [component.name, ...(component.aliases || [])];
167
+ }
168
+
169
+ function findComponent(name) {
170
+ const lower = name.toLowerCase();
171
+ return COMPONENTS.find((component) =>
172
+ componentNames(component).some((candidate) => candidate.toLowerCase() === lower),
173
+ );
110
174
  }
111
175
 
112
- /** 找最接近的组件名(用于纠错提示)。 */
113
176
  function nearestComponents(name, limit = 3) {
114
177
  const lower = name.toLowerCase();
115
- return COMPONENTS.map((c) => ({ name: c.name, d: editDistance(lower, c.name.toLowerCase()) }))
116
- .sort((a, b) => a.d - b.d)
178
+ return COMPONENTS.map((component) => ({
179
+ name: component.name,
180
+ distance: Math.min(
181
+ ...componentNames(component).map((candidate) => editDistance(lower, candidate.toLowerCase())),
182
+ ),
183
+ }))
184
+ .sort((a, b) => a.distance - b.distance)
117
185
  .slice(0, limit)
118
- .map((x) => x.name);
186
+ .map((item) => item.name);
119
187
  }
120
188
 
121
- function findComponent(name) {
122
- return COMPONENTS.find((c) => c.name.toLowerCase() === name.toLowerCase());
189
+ function exampleMatches(example, keyword) {
190
+ if (!keyword) return true;
191
+ const value = keyword.toLowerCase();
192
+ return [example.id, example.file, example.title, example.note].some((field) =>
193
+ (field || '').toLowerCase().includes(value),
194
+ );
123
195
  }
124
196
 
125
- // ---------- MCP server 与工具 ----------
197
+ const server = new McpServer({ name: '@zat-design/sisyphus-react-mcp', version: VERSION });
126
198
 
127
- const server = new McpServer({
128
- name: '@zat-design/sisyphus-react-mcp',
129
- version: VERSION,
130
- });
131
-
132
- // 1) sisyphus-list-components
133
199
  server.registerTool(
134
200
  'sisyphus-list-components',
135
201
  {
136
202
  title: '列出组件',
137
- description: `列出 @zat-design/sisyphus-react (v${VERSION}) 的全部组件及其业务域分类与一句话描述。`,
203
+ description: `列出 @zat-design/sisyphus-react (v${VERSION}) 全部稳定业务组件。`,
138
204
  inputSchema: z.object({}),
139
205
  },
140
206
  async () => {
141
207
  const lines = COMPONENTS.map(
142
- (c) => `- ${c.name}(${c.category || '未分类'}):${preferChinese(c.description) || '—'}`,
208
+ (component) =>
209
+ `- ${component.name}(${component.category}):${preferChinese(component.description) || '—'}`,
143
210
  );
144
211
  return textResult(
145
- `sisyphus-react v${VERSION} 组件清单(共 ${COMPONENTS.length}):\n\n${lines.join('\n')}`,
212
+ `sisyphus-react v${VERSION} 业务组件(${COMPONENTS.length} 个):\n\n${lines.join('\n')}`,
146
213
  );
147
214
  },
148
215
  );
149
216
 
150
- // 2) sisyphus-get-component-api
151
217
  server.registerTool(
152
218
  'sisyphus-get-component-api',
153
219
  {
154
220
  title: '查询组件 API',
155
- description:
156
- '按组件名返回其 props/fieldProps 与配置式约定枚举(type/valueType/names 等)。默认不内联示例,示例请用 sisyphus-get-examples。',
157
- inputSchema: z.object({
158
- name: z.string().describe('组件名,如 ProForm / ProSelect / ProEditTable'),
159
- }),
221
+ description: '返回组件 API 分组、TypeScript 类型、必填性、默认值与配置约定。',
222
+ inputSchema: z.object({ name: z.string().describe('组件名,如 ProForm') }),
160
223
  },
161
224
  async ({ name }) => {
162
- const comp = findComponent(name);
163
- if (!comp) {
164
- const candidates = nearestComponents(name);
165
- return textResult(
166
- `未找到组件「${name}」。最接近的候选:${candidates.join('、')}。可用 sisyphus-list-components 查看全部。`,
167
- );
168
- }
169
- const entry = PROPS[comp.name];
170
- const out = [`# ${comp.name}(${comp.category || '未分类'})v${VERSION}`];
171
- if (comp.description) out.push(preferChinese(comp.description));
172
- out.push(`导入:\`import { ${comp.name} } from '${comp.importPath}'\``);
173
-
174
- if (!entry) {
175
- out.push('\n(暂无 API 元数据)');
176
- return textResult(out.join('\n'));
225
+ const component = findComponent(name);
226
+ if (!component) {
227
+ return textResult(`未找到组件「${name}」。最接近:${nearestComponents(name).join('、')}。`);
177
228
  }
178
-
179
- // props:docs-api 分组形式 或 ts-interface 扁平形式
180
- if (entry.source === 'docs-api' && entry.groups) {
181
- for (const g of entry.groups) {
182
- const props = Object.entries(g.props || {});
183
- if (!props.length) continue;
184
- out.push(`\n## ${g.title}`);
185
- for (const [pn, info] of props) out.push(formatProp(pn, info));
186
- }
187
- } else if (entry.props) {
188
- const props = Object.entries(entry.props);
189
- if (props.length) {
190
- out.push('\n## Props');
191
- for (const [pn, info] of props) out.push(formatProp(pn, info));
192
- }
229
+ const entry = PROPS[component.name];
230
+ const output = [
231
+ `# ${component.name}(${component.category})v${VERSION}`,
232
+ preferChinese(component.description),
233
+ `导入:\`import { ${component.name} } from '${component.importPath}'\``,
234
+ `Props 类型:\`${component.propsType}\``,
235
+ ].filter(Boolean);
236
+ for (const group of entry?.groups || []) {
237
+ const props = Object.entries(group.props || {});
238
+ if (!props.length) continue;
239
+ output.push(`\n## ${group.title}${group.typeName ? `(\`${group.typeName}\`)` : ''}`);
240
+ for (const [propName, info] of props) output.push(formatProp(propName, info));
193
241
  }
194
-
195
- // 配置式约定枚举
196
- const conv = entry.conventions || {};
197
- const convKeys = Object.keys(conv);
198
- if (convKeys.length) {
199
- out.push('\n## 配置式约定');
200
- for (const key of convKeys) {
201
- const c = conv[key];
202
- out.push(`\n### ${key}`);
203
- if (c.description) out.push(preferChinese(c.description));
204
- if (c.example) out.push(`示例:\`${c.example}\``);
205
- if (c.values) {
206
- for (const v of c.values) out.push(`- \`${v.value}\`${v.desc ? ` — ${v.desc}` : ''}`);
207
- }
208
- if (c.ref) out.push(`(同 ${c.ref} 约定)`);
242
+ for (const [key, convention] of Object.entries(entry?.conventions || {})) {
243
+ output.push(`\n## 配置约定:${key}`);
244
+ if (convention.description) output.push(preferChinese(convention.description));
245
+ if (convention.example) output.push(`示例:\`${convention.example}\``);
246
+ for (const value of convention.values || []) {
247
+ output.push(`- \`${value.value}\`${value.desc ? ` — ${value.desc}` : ''}`);
209
248
  }
249
+ if (convention.ref) output.push(`参考:${convention.ref}`);
210
250
  }
211
-
212
- return textResult(dehydrateText(out.join('\n')));
251
+ return textResult(dehydrateText(output.join('\n')));
213
252
  },
214
253
  );
215
254
 
216
- // 3) sisyphus-get-examples
255
+ // 兼容旧工具:仍按组件/关键词返回所有匹配源码。新流程优先 list-examples + get-example。
217
256
  server.registerTool(
218
257
  'sisyphus-get-examples',
219
258
  {
220
- title: '查询组件示例',
221
- description: '按组件名(可选场景关键词)返回真实可运行的 demo 源码与标题。',
259
+ title: '查询组件示例(兼容)',
260
+ description: '按组件名和可选关键词返回 demo 源码;大量示例请优先使用 list/get-example。',
222
261
  inputSchema: z.object({
223
- name: z.string().describe('组件名,如 ProEditTable'),
224
- keyword: z.string().optional().describe('可选场景关键词,按示例标题/说明过滤,如「虚拟」「异步」'),
262
+ name: z.string(),
263
+ keyword: z.string().optional(),
225
264
  }),
226
265
  },
227
266
  async ({ name, keyword }) => {
228
- const comp = findComponent(name);
229
- if (!comp) {
230
- return textResult(
231
- `未找到组件「${name}」。最接近的候选:${nearestComponents(name).join('、')}。`,
232
- );
233
- }
234
- let list = EXAMPLES.filter((e) => e.component.toLowerCase() === comp.name.toLowerCase());
235
- if (keyword) {
236
- const kw = keyword.toLowerCase();
237
- list = list.filter(
238
- (e) =>
239
- (e.title || '').toLowerCase().includes(kw) ||
240
- (e.note || '').toLowerCase().includes(kw) ||
241
- (e.file || '').toLowerCase().includes(kw),
242
- );
243
- }
244
- if (!list.length) {
245
- return textResult(
246
- `${comp.name} 未找到${keyword ? `匹配「${keyword}」的` : ''}示例。可去掉关键词重试。`,
247
- );
248
- }
249
- const blocks = list.map((e) => {
250
- const header = `## ${e.title}${e.note ? `\n${preferChinese(e.note)}` : ''}\n(${e.file})`;
251
- return `${header}\n\`\`\`tsx\n${e.source.trim()}\n\`\`\``;
252
- });
253
- return textResult(`# ${comp.name} 示例(${list.length} 个)\n\n${blocks.join('\n\n')}`);
267
+ const component = findComponent(name);
268
+ if (!component) return textResult(`未找到组件「${name}」。`);
269
+ const list = EXAMPLES.filter(
270
+ (example) => example.component === component.name && exampleMatches(example, keyword),
271
+ );
272
+ if (!list.length) return textResult(`${component.name} 未找到匹配示例。`);
273
+ const blocks = list.map(
274
+ (example) =>
275
+ `## ${example.title}\nID:\`${example.id}\`${example.note ? `\n${preferChinese(example.note)}` : ''}` +
276
+ `\n\`\`\`tsx\n${example.source.trim()}\n\`\`\``,
277
+ );
278
+ return textResult(`# ${component.name} 示例(${list.length} 个)\n\n${blocks.join('\n\n')}`);
254
279
  },
255
280
  );
256
281
 
257
- // 4) search
258
282
  server.registerTool(
259
- 'sisyphus-search',
283
+ 'sisyphus-list-examples',
260
284
  {
261
- title: '关键词搜索',
262
- description: '按关键词跨组件清单、props 描述、示例标题检索,返回最相关的组件与能力定位。',
285
+ title: '分页列出示例',
286
+ description: '只返回 demo 索引,不内联源码。',
263
287
  inputSchema: z.object({
264
- query: z.string().describe('自然语言关键词,如「日期范围拆成两个字段」「异步下拉」'),
288
+ name: z.string(),
289
+ keyword: z.string().optional(),
290
+ cursor: z.string().optional().describe('上次返回的 nextCursor'),
291
+ limit: z.number().int().min(1).max(50).default(20),
265
292
  }),
266
293
  },
267
- async ({ query }) => {
268
- const kw = query.toLowerCase();
269
- const hits = [];
294
+ async ({ name, keyword, cursor, limit }) => {
295
+ const component = findComponent(name);
296
+ if (!component) return textResult(`未找到组件「${name}」。`);
297
+ const all = EXAMPLES.filter(
298
+ (example) => example.component === component.name && exampleMatches(example, keyword),
299
+ );
300
+ const offset = Number.parseInt(cursor || '0', 10);
301
+ if (!Number.isSafeInteger(offset) || offset < 0) return textResult('无效 cursor。');
302
+ const page = all.slice(offset, offset + limit);
303
+ const nextCursor = offset + page.length < all.length ? String(offset + page.length) : null;
304
+ const lines = page.map(
305
+ (example) =>
306
+ `- \`${example.id}\` — ${example.title}${example.note ? `:${preferChinese(example.note)}` : ''}`,
307
+ );
308
+ return textResult(
309
+ `# ${component.name} 示例索引(${offset + 1}-${offset + page.length} / ${all.length})\n` +
310
+ `${lines.join('\n') || '(无匹配结果)'}\n\nnextCursor: ${nextCursor || 'null'}`,
311
+ );
312
+ },
313
+ );
314
+
315
+ server.registerTool(
316
+ 'sisyphus-get-example',
317
+ {
318
+ title: '获取单个示例',
319
+ description: '按 list-examples 返回的稳定 ID 获取单个 demo 源码。',
320
+ inputSchema: z.object({ id: z.string() }),
321
+ },
322
+ async ({ id }) => {
323
+ const example = EXAMPLES.find((item) => item.id === id);
324
+ if (!example) return textResult(`未找到示例「${id}」,请先调用 sisyphus-list-examples。`);
325
+ return textResult(
326
+ `# ${example.component} / ${example.title}\nID:\`${example.id}\`` +
327
+ `${example.note ? `\n${preferChinese(example.note)}` : ''}` +
328
+ `\n\n\`\`\`tsx\n${example.source.trim()}\n\`\`\``,
329
+ );
330
+ },
331
+ );
270
332
 
271
- for (const c of COMPONENTS) {
272
- const hay = [c.name, c.category, c.description, ...(c.whenToUse || [])]
273
- .join(' ')
274
- .toLowerCase();
275
- if (hay.includes(kw)) hits.push(`组件 ${c.name}(${c.category}):${preferChinese(c.description)}`);
333
+ server.registerTool(
334
+ 'sisyphus-get-type',
335
+ {
336
+ title: '查询公开 TypeScript 类型',
337
+ description: '返回从包根导出的准确 TypeScript 声明。',
338
+ inputSchema: z.object({ name: z.string().describe('如 ProFormColumnType') }),
339
+ },
340
+ async ({ name }) => {
341
+ const type = TYPES.find((item) => item.name.toLowerCase() === name.toLowerCase());
342
+ if (!type) {
343
+ const candidates = TYPES.map((item) => item.name)
344
+ .sort(
345
+ (a, b) =>
346
+ editDistance(name.toLowerCase(), a.toLowerCase()) -
347
+ editDistance(name.toLowerCase(), b.toLowerCase()),
348
+ )
349
+ .slice(0, 3);
350
+ return textResult(`未找到公开类型「${name}」。最接近:${candidates.join('、')}。`);
276
351
  }
352
+ const properties = Object.entries(type.properties || {});
353
+ return textResult(
354
+ `# ${type.name}\n来源:\`${type.source}\`${type.description ? `\n${type.description}` : ''}` +
355
+ `\n\n\`\`\`ts\n${type.declaration}\n\`\`\`` +
356
+ (properties.length
357
+ ? `\n\n## 展开属性\n${properties.map(([propertyName, info]) => formatProp(propertyName, info)).join('\n')}`
358
+ : ''),
359
+ );
360
+ },
361
+ );
277
362
 
278
- for (const [cn, entry] of Object.entries(PROPS)) {
279
- const collectProps = entry.source === 'docs-api'
280
- ? (entry.groups || []).flatMap((g) => Object.entries(g.props || {}))
281
- : Object.entries(entry.props || {});
282
- for (const [pn, info] of collectProps) {
283
- if (`${pn} ${info.description || ''}`.toLowerCase().includes(kw)) {
284
- hits.push(`${cn}.${pn} — ${preferChinese(info.description || '')}`);
285
- }
286
- }
287
- // 约定命中:枚举值数组 + 约定描述(描述型约定如用法/反模式也可被检索)
288
- for (const [convKey, conv] of Object.entries(entry.conventions || {})) {
289
- for (const v of conv.values || []) {
290
- if (`${v.value} ${v.desc || ''}`.toLowerCase().includes(kw)) {
291
- hits.push(`${cn} 约定 ${convKey}=\`${v.value}\` — ${v.desc || ''}`);
292
- }
293
- }
294
- if (conv.description && conv.description.toLowerCase().includes(kw)) {
295
- hits.push(`${cn} 约定 ${convKey} 详见 sisyphus-get-component-api`);
363
+ function searchTerms(query) {
364
+ const normalized = query.toLowerCase().trim();
365
+ const terms = new Set([normalized, ...normalized.split(/\s+/).filter(Boolean)]);
366
+ const chinese = [...normalized].filter((char) => /[\u4e00-\u9fa5]/.test(char)).join('');
367
+ if (chinese.length >= 4) {
368
+ for (let i = 0; i < chinese.length - 1; i++) terms.add(chinese.slice(i, i + 2));
369
+ }
370
+ return [...terms].filter(Boolean);
371
+ }
372
+
373
+ server.registerTool(
374
+ 'sisyphus-search',
375
+ {
376
+ title: '跨元数据搜索',
377
+ description: '按多关键词搜索组件、API、约定、类型和示例索引。',
378
+ inputSchema: z.object({ query: z.string().min(1) }),
379
+ },
380
+ async ({ query }) => {
381
+ const terms = searchTerms(query);
382
+ const hits = [];
383
+ const add = (text, haystack, weight = 1) => {
384
+ const lower = haystack.toLowerCase();
385
+ const score = terms.reduce((total, term) => total + (lower.includes(term) ? weight : 0), 0);
386
+ if (score) hits.push({ text, score });
387
+ };
388
+ for (const component of COMPONENTS) {
389
+ add(
390
+ `组件 ${component.name}(${component.category}):${preferChinese(component.description)}`,
391
+ [
392
+ component.name,
393
+ ...(component.aliases || []),
394
+ component.category,
395
+ component.description,
396
+ ...(component.whenToUse || []),
397
+ ].join(' '),
398
+ 3,
399
+ );
400
+ }
401
+ for (const [componentName, entry] of Object.entries(PROPS)) {
402
+ for (const group of entry.groups || []) {
403
+ for (const [propName, info] of Object.entries(group.props || {})) {
404
+ add(
405
+ `${componentName}.${propName} [${group.title}] — ${preferChinese(info.description || '')}`,
406
+ `${propName} ${group.title} ${info.type} ${info.description}`,
407
+ 2,
408
+ );
296
409
  }
297
410
  }
298
- }
299
-
300
- for (const e of EXAMPLES) {
301
- if (`${e.title} ${e.note}`.toLowerCase().includes(kw)) {
302
- hits.push(`示例 ${e.component}「${e.title}」(${e.file})→ sisyphus-get-examples 获取源码`);
411
+ for (const [key, convention] of Object.entries(entry.conventions || {})) {
412
+ add(
413
+ `${componentName} 约定 ${key} 请查询组件 API`,
414
+ `${key} ${convention.description || ''} ${(convention.values || []).map((value) => `${value.value} ${value.desc}`).join(' ')}`,
415
+ 3,
416
+ );
303
417
  }
304
418
  }
305
-
306
- if (!hits.length) {
307
- return textResult(`未检索到与「${query}」相关的内容。可尝试更通用的关键词或用 sisyphus-list-components。`);
308
- }
309
- // 去重
310
- const unique = [...new Set(hits)];
311
- return textResult(`检索「${query}」命中 ${unique.length} 条:\n\n${unique.join('\n')}`);
419
+ for (const type of TYPES)
420
+ add(
421
+ `类型 ${type.name} sisyphus-get-type`,
422
+ `${type.name} ${type.description} ${type.declaration}`,
423
+ 2,
424
+ );
425
+ for (const example of EXAMPLES)
426
+ add(
427
+ `示例 ${example.component}「${example.title}」 — \`${example.id}\``,
428
+ `${example.title} ${example.note} ${example.file}`,
429
+ 1,
430
+ );
431
+ const unique = [
432
+ ...new Map(hits.sort((a, b) => b.score - a.score).map((hit) => [hit.text, hit])).values(),
433
+ ].slice(0, 30);
434
+ if (!unique.length) return textResult(`未检索到与「${query}」相关的内容。`);
435
+ return textResult(
436
+ `检索「${query}」命中 ${unique.length} 条(最多展示 30 条):\n\n${unique.map((hit) => hit.text).join('\n')}`,
437
+ );
312
438
  },
313
439
  );
314
440
 
315
- // ---------- 启动 ----------
441
+ server.registerTool(
442
+ 'sisyphus-get-meta-status',
443
+ {
444
+ title: '查询元数据状态',
445
+ description: '返回当前业务项目的组件库版本、MCP 内置元数据 schema 和覆盖统计。',
446
+ inputSchema: z.object({}),
447
+ },
448
+ async () =>
449
+ textResult(
450
+ [
451
+ '# Sisyphus AI 元数据状态',
452
+ `- MCP 版本:${PACKAGE_VERSION}`,
453
+ `- 组件库版本:${VERSION}`,
454
+ `- schemaVersion:${META.manifest.schemaVersion}`,
455
+ `- 业务组件:${META.manifest.counts.components}`,
456
+ `- 文档 demo:${META.manifest.counts.examples}`,
457
+ `- 公开类型:${META.manifest.counts.types}`,
458
+ `- 元数据来源:MCP 内置 ${META.dir}`,
459
+ ].join('\n'),
460
+ ),
461
+ );
316
462
 
317
463
  async function main() {
318
464
  const transport = new StdioServerTransport();
319
465
  await server.connect(transport);
320
- // stdio 模式下日志走 stderr,避免污染协议通道
321
- console.error(`[sisyphus-mcp] 已启动 v${VERSION},组件 ${COMPONENTS.length} 个,示例 ${EXAMPLES.length} 条`);
466
+ console.error(
467
+ `[sisyphus-mcp] 已启动 v${VERSION},${COMPONENTS.length} 组件 / ${EXAMPLES.length} demo / ${TYPES.length} 类型`,
468
+ );
322
469
  }
323
470
 
324
- main().catch((err) => {
325
- console.error('[sisyphus-mcp] 启动失败:', err);
471
+ main().catch((error) => {
472
+ console.error('[sisyphus-mcp] 启动失败:', error.message);
326
473
  process.exit(1);
327
474
  });