@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.
Files changed (2) hide show
  1. package/dist/index.js +94 -13
  2. 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
- const projectIndex = args.indexOf('--project');
28
- const manifestIndex = args.indexOf('--manifest');
29
- const project = path.resolve(projectIndex > -1 ? (args[projectIndex + 1] ?? '.') : '.');
30
- const manifest = manifestIndex > -1
31
- ? path.resolve(args[manifestIndex + 1] ?? '')
32
- : path.join(project, '.playground/manifest.json');
33
- return { project, manifest };
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(`找不到契约清单:${file}\n先运行 pnpm dev 或 pnpm build 生成(需要 playground.config.ts 里配置 components)`);
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
- const server = new McpServer({ name: 'rinova-playground', version: '0.7.0' });
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
- }, () => ({ content: [{ type: 'text', text: formatEnv(manifest) }] }));
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 重新生成清单。' }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rinova/playground-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "rinova-playground 的 MCP server:把契约清单暴露给 AI 编码工具",
5
5
  "type": "module",
6
6
  "license": "MIT",