@rinova/playground-mcp 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +94 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,19 +22,74 @@ const locate = (root, rel, needle) => {
|
|
|
22
22
|
codeLines.find((item) => word.test(item.line));
|
|
23
23
|
return hit ? `${rel}:${hit.index + 1}` : rel;
|
|
24
24
|
};
|
|
25
|
+
const USAGE = `rinova-playground-mcp —— 把契约清单暴露给 AI 编码工具(只读)
|
|
26
|
+
|
|
27
|
+
用法:
|
|
28
|
+
rinova-playground-mcp [--project <目录>] [--manifest <文件>]
|
|
29
|
+
|
|
30
|
+
选项:
|
|
31
|
+
--project <目录> 项目根(默认:当前目录,并向上找 .playground/manifest.json)
|
|
32
|
+
--manifest <文件> 直接指定清单文件
|
|
33
|
+
-h, --help 显示帮助
|
|
34
|
+
|
|
35
|
+
清单由 @rinova/playground-vite 在 pnpm dev / pnpm build 时生成,需要 playground.config.ts 里配置 components。
|
|
36
|
+
`;
|
|
37
|
+
/** 从 start 向上最多 6 层找 .playground/manifest.json(客户端启动目录常常是包内子目录) */
|
|
38
|
+
const findManifestUp = (start) => {
|
|
39
|
+
let dir = path.resolve(start);
|
|
40
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
41
|
+
const candidate = path.join(dir, '.playground/manifest.json');
|
|
42
|
+
if (fs.existsSync(candidate))
|
|
43
|
+
return candidate;
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
break;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
const readStamp = (file) => {
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(file);
|
|
54
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 'missing';
|
|
58
|
+
}
|
|
59
|
+
};
|
|
25
60
|
const parseArgs = (argv) => {
|
|
26
61
|
const args = argv.slice(2);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
63
|
+
process.stdout.write(USAGE);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
const readFlag = (flag) => {
|
|
67
|
+
const index = args.indexOf(flag);
|
|
68
|
+
return index > -1 ? args[index + 1] : undefined;
|
|
69
|
+
};
|
|
70
|
+
const manifestArg = readFlag('--manifest');
|
|
71
|
+
if (manifestArg) {
|
|
72
|
+
const manifest = path.resolve(manifestArg);
|
|
73
|
+
return { project: path.dirname(path.dirname(manifest)), manifest, searchFrom: path.dirname(manifest) };
|
|
74
|
+
}
|
|
75
|
+
const projectArg = readFlag('--project');
|
|
76
|
+
const base = projectArg ? path.resolve(projectArg) : process.cwd();
|
|
77
|
+
const found = findManifestUp(base);
|
|
78
|
+
return {
|
|
79
|
+
project: found ? path.dirname(path.dirname(found)) : base,
|
|
80
|
+
manifest: found ?? path.join(base, '.playground/manifest.json'),
|
|
81
|
+
searchFrom: base,
|
|
82
|
+
};
|
|
34
83
|
};
|
|
35
|
-
const loadManifest = (file) => {
|
|
84
|
+
const loadManifest = (file, searchFrom) => {
|
|
36
85
|
if (!fs.existsSync(file)) {
|
|
37
|
-
throw new Error(
|
|
86
|
+
throw new Error([
|
|
87
|
+
`找不到契约清单:${file}`,
|
|
88
|
+
`(从 ${searchFrom} 向上找了 6 层)`,
|
|
89
|
+
'',
|
|
90
|
+
'生成方式:在项目里配置 playground.config.ts 的 components(可选 modules),然后跑一次 pnpm dev 或 pnpm build。',
|
|
91
|
+
'也可以在客户端配置里用 --project <项目根> 或 --manifest <.playground/manifest.json 的绝对路径> 显式指定。',
|
|
92
|
+
].join('\n'));
|
|
38
93
|
}
|
|
39
94
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
40
95
|
};
|
|
@@ -107,22 +162,40 @@ const matches = (component, query) => {
|
|
|
107
162
|
component.scenes.some((scene) => scene.toLowerCase().includes(needle)));
|
|
108
163
|
};
|
|
109
164
|
const main = async () => {
|
|
110
|
-
const { project, manifest: manifestPath } = parseArgs(process.argv);
|
|
165
|
+
const { project, manifest: manifestPath, searchFrom } = parseArgs(process.argv);
|
|
111
166
|
let manifest;
|
|
112
167
|
try {
|
|
113
|
-
manifest = loadManifest(manifestPath);
|
|
168
|
+
manifest = loadManifest(manifestPath, searchFrom);
|
|
114
169
|
}
|
|
115
170
|
catch (error) {
|
|
116
171
|
console.error(error instanceof Error ? error.message : error);
|
|
117
172
|
process.exitCode = 1;
|
|
118
173
|
return;
|
|
119
174
|
}
|
|
120
|
-
|
|
175
|
+
/**
|
|
176
|
+
* 清单是构建产物,dev server 会在场景 / 组件变化后重写它。
|
|
177
|
+
* 每次工具调用前比一下 mtime+size,变了就重读 —— 否则 MCP 会一直拿启动那一刻的页面表。
|
|
178
|
+
*/
|
|
179
|
+
let manifestStamp = readStamp(manifestPath);
|
|
180
|
+
const refresh = () => {
|
|
181
|
+
const stamp = readStamp(manifestPath);
|
|
182
|
+
if (stamp === manifestStamp)
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
manifest = loadManifest(manifestPath, searchFrom);
|
|
186
|
+
manifestStamp = stamp;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// 清单正在被写(JSON 半个文件)或暂时不可读:沿用上一次的,下次调用再试
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const server = new McpServer({ name: 'rinova-playground', version: '0.7.1' });
|
|
121
193
|
server.registerTool('capability_search', {
|
|
122
194
|
title: '搜索组件能力',
|
|
123
195
|
description: '按组件名 / prop 名 / 场景 id 搜索,返回带文件行号的摘要',
|
|
124
196
|
inputSchema: { query: z.string().min(1).describe('关键词'), limit: z.number().int().min(1).max(20).optional() },
|
|
125
197
|
}, ({ query, limit }) => {
|
|
198
|
+
refresh();
|
|
126
199
|
const hits = manifest.components.filter((component) => matches(component, query)).slice(0, limit ?? 5);
|
|
127
200
|
if (!hits.length) {
|
|
128
201
|
return { content: [{ type: 'text', text: `没有匹配 "${query}" 的组件。已知组件:${manifest.components.map((item) => item.name).join(', ')}` }] };
|
|
@@ -137,6 +210,7 @@ const main = async () => {
|
|
|
137
210
|
description: '返回组件的 props / events / slots / exposed、源码锚点与相关场景',
|
|
138
211
|
inputSchema: { component: z.string().min(1).describe('组件名,如 MyButton') },
|
|
139
212
|
}, ({ component }) => {
|
|
213
|
+
refresh();
|
|
140
214
|
const found = manifest.components.find((item) => item.name.toLowerCase() === component.toLowerCase() || item.rel.endsWith(component));
|
|
141
215
|
if (!found) {
|
|
142
216
|
return {
|
|
@@ -153,6 +227,7 @@ const main = async () => {
|
|
|
153
227
|
description: '列出全部场景;传 component 则只列该组件相关的场景',
|
|
154
228
|
inputSchema: { component: z.string().optional().describe('组件名,可选') },
|
|
155
229
|
}, ({ component }) => {
|
|
230
|
+
refresh();
|
|
156
231
|
const scenes = component
|
|
157
232
|
? manifest.scenes.filter((scene) => {
|
|
158
233
|
const target = manifest.components.find((item) => item.name === component);
|
|
@@ -171,6 +246,7 @@ const main = async () => {
|
|
|
171
246
|
description: '列出没有场景覆盖的 props,作为补场景的待办',
|
|
172
247
|
inputSchema: {},
|
|
173
248
|
}, () => {
|
|
249
|
+
refresh();
|
|
174
250
|
const rows = manifest.components.flatMap((component) => component.uncoveredProps.map((prop) => `- ${component.name}.${prop} ← ${locate(project, component.rel, prop)}`));
|
|
175
251
|
const total = manifest.components.reduce((sum, component) => sum + component.props.length, 0);
|
|
176
252
|
const uncovered = rows.length;
|
|
@@ -231,7 +307,10 @@ const main = async () => {
|
|
|
231
307
|
title: '演示环境',
|
|
232
308
|
description: '返回 globals / 插件 / 分组 / 页面清单与外壳能力,写场景前先读它',
|
|
233
309
|
inputSchema: {},
|
|
234
|
-
}, () =>
|
|
310
|
+
}, () => {
|
|
311
|
+
refresh();
|
|
312
|
+
return { content: [{ type: 'text', text: formatEnv(manifest) }] };
|
|
313
|
+
});
|
|
235
314
|
server.registerTool('scene_scaffold', {
|
|
236
315
|
title: '生成场景骨架',
|
|
237
316
|
description: '按环境规则算出该建的文件路径与内容(不落盘,写入交给客户端的文件工具)',
|
|
@@ -246,6 +325,7 @@ const main = async () => {
|
|
|
246
325
|
slug: z.string().optional().describe('场景文件名(不含扩展名);中文标题请显式传,默认取标题的英文缩写'),
|
|
247
326
|
},
|
|
248
327
|
}, ({ title, page, group, component, lang, section, api, slug }) => {
|
|
328
|
+
refresh();
|
|
249
329
|
const env = manifest.env;
|
|
250
330
|
if (!env) {
|
|
251
331
|
return {
|
|
@@ -360,6 +440,7 @@ const main = async () => {
|
|
|
360
440
|
path: z.string().optional().describe('只校验这个文件(相对项目根);不传则全量校验'),
|
|
361
441
|
},
|
|
362
442
|
}, ({ path: target }) => {
|
|
443
|
+
refresh();
|
|
363
444
|
if (!manifest.env) {
|
|
364
445
|
return {
|
|
365
446
|
content: [{ type: 'text', text: '这份清单里没有 env 段,无法校验。先跑一次 pnpm dev 重新生成清单。' }],
|