@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/README.md +24 -41
- package/meta/components.json +315 -112
- package/meta/examples.json +1579 -1137
- package/meta/manifest.json +15 -0
- package/meta/props.json +4711 -3162
- package/meta/types.json +18857 -0
- package/package.json +3 -2
- package/src/index.mjs +356 -209
package/src/index.mjs
CHANGED
|
@@ -2,326 +2,473 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @zat-design/sisyphus-react MCP server。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
18
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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[
|
|
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((
|
|
116
|
-
.
|
|
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((
|
|
186
|
+
.map((item) => item.name);
|
|
119
187
|
}
|
|
120
188
|
|
|
121
|
-
function
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
208
|
+
(component) =>
|
|
209
|
+
`- ${component.name}(${component.category}):${preferChinese(component.description) || '—'}`,
|
|
143
210
|
);
|
|
144
211
|
return textResult(
|
|
145
|
-
`sisyphus-react v${VERSION}
|
|
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
|
-
|
|
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
|
|
163
|
-
if (!
|
|
164
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
255
|
+
// 兼容旧工具:仍按组件/关键词返回所有匹配源码。新流程优先 list-examples + get-example。
|
|
217
256
|
server.registerTool(
|
|
218
257
|
'sisyphus-get-examples',
|
|
219
258
|
{
|
|
220
|
-
title: '
|
|
221
|
-
description: '
|
|
259
|
+
title: '查询组件示例(兼容)',
|
|
260
|
+
description: '按组件名和可选关键词返回 demo 源码;大量示例请优先使用 list/get-example。',
|
|
222
261
|
inputSchema: z.object({
|
|
223
|
-
name: z.string()
|
|
224
|
-
keyword: z.string().optional()
|
|
262
|
+
name: z.string(),
|
|
263
|
+
keyword: z.string().optional(),
|
|
225
264
|
}),
|
|
226
265
|
},
|
|
227
266
|
async ({ name, keyword }) => {
|
|
228
|
-
const
|
|
229
|
-
if (!
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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-
|
|
283
|
+
'sisyphus-list-examples',
|
|
260
284
|
{
|
|
261
|
-
title: '
|
|
262
|
-
description: '
|
|
285
|
+
title: '分页列出示例',
|
|
286
|
+
description: '只返回 demo 索引,不内联源码。',
|
|
263
287
|
inputSchema: z.object({
|
|
264
|
-
|
|
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 ({
|
|
268
|
-
const
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
321
|
-
|
|
466
|
+
console.error(
|
|
467
|
+
`[sisyphus-mcp] 已启动 v${VERSION},${COMPONENTS.length} 组件 / ${EXAMPLES.length} demo / ${TYPES.length} 类型`,
|
|
468
|
+
);
|
|
322
469
|
}
|
|
323
470
|
|
|
324
|
-
main().catch((
|
|
325
|
-
console.error('[sisyphus-mcp] 启动失败:',
|
|
471
|
+
main().catch((error) => {
|
|
472
|
+
console.error('[sisyphus-mcp] 启动失败:', error.message);
|
|
326
473
|
process.exit(1);
|
|
327
474
|
});
|