@rinova/playground-mcp 0.0.0-stage → 0.1.0

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 CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # @rinova/playground-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把 rinova-playground 生成的契约清单(`.playground/manifest.json`)暴露成 MCP 工具,让 Claude Code / Cursor 这类客户端能「读组件能力、看场景、查覆盖」。
4
+
5
+ ## 前置
6
+
7
+ 项目里要先生成清单,也就是 `playground.config.ts` 配了 `components`,然后跑过 `pnpm dev` 或 `pnpm build`。
8
+
9
+ ```ts
10
+ export default defineConfig({
11
+ components: ['src/components/**/*.vue'],
12
+ })
13
+ ```
14
+
15
+ ## 接入
16
+
17
+ **Claude Code** —— 项目根的 `.mcp.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "rinova-playground": {
23
+ "command": "npx",
24
+ "args": ["-y", "@rinova/playground-mcp", "--project", "."]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ **Cursor** —— `~/.cursor/mcp.json` 同结构。
31
+
32
+ **本地仓库调试**:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "rinova-playground": {
38
+ "command": "node",
39
+ "args": ["packages/playground-mcp/dist/index.js", "--project", "examples/minimal-lib"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ 参数:`--project <dir>`(默认当前目录)、`--manifest <file>`(默认 `<project>/.playground/manifest.json`)。
46
+
47
+ ## 工具
48
+
49
+ | 工具 | 入参 | 返回 |
50
+ |---|---|---|
51
+ | `capability_search` | `query`、`limit?` | 命中的组件 + prop 名 + 场景数 |
52
+ | `capability_get` | `component` | props/events/slots/exposed 全量契约,每个 prop 带 `文件:行号`,附相关场景与未覆盖 props |
53
+ | `scenario_list` | `component?` | 场景 id、标题、分组、文件路径 |
54
+ | `coverage_report` | — | 未覆盖 props 清单 + 覆盖率 |
55
+
56
+ 只读:不改项目文件,也不启动浏览器。写场景仍由人或 AI 直接写 `.vue` 文件(「文件即场景」主线不变)。
57
+
58
+ ## 验证
59
+
60
+ ```bash
61
+ node --experimental-strip-types scripts/probe-mcp.ts # 仓库根执行,7/7 通过
62
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { z } from 'zod';
7
+ const toPosix = (value) => value.split(path.sep).join('/');
8
+ /** 在源文件里定位某个标识符首次出现的行,产出 `path:line` 锚点 */
9
+ const locate = (root, rel, needle) => {
10
+ const abs = path.join(root, rel);
11
+ if (!fs.existsSync(abs))
12
+ return rel;
13
+ const lines = fs.readFileSync(abs, 'utf8').split('\n');
14
+ const pattern = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
15
+ const index = lines.findIndex((line) => pattern.test(line));
16
+ return index >= 0 ? `${rel}:${index + 1}` : rel;
17
+ };
18
+ const parseArgs = (argv) => {
19
+ const args = argv.slice(2);
20
+ const projectIndex = args.indexOf('--project');
21
+ const manifestIndex = args.indexOf('--manifest');
22
+ const project = path.resolve(projectIndex > -1 ? (args[projectIndex + 1] ?? '.') : '.');
23
+ const manifest = manifestIndex > -1
24
+ ? path.resolve(args[manifestIndex + 1] ?? '')
25
+ : path.join(project, '.playground/manifest.json');
26
+ return { project, manifest };
27
+ };
28
+ const loadManifest = (file) => {
29
+ if (!fs.existsSync(file)) {
30
+ throw new Error(`找不到契约清单:${file}\n先运行 pnpm dev 或 pnpm build 生成(需要 playground.config.ts 里配置 components)`);
31
+ }
32
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ };
34
+ const formatComponent = (root, component, manifest) => {
35
+ const lines = [
36
+ `# ${component.name}`,
37
+ `源码:${locate(root, component.rel, component.name)}`,
38
+ '',
39
+ '## Props',
40
+ ];
41
+ if (!component.props.length)
42
+ lines.push('- (无对外 props)');
43
+ for (const prop of component.props) {
44
+ const flags = [prop.required ? 'required' : '', prop.default ? `default=${prop.default}` : '']
45
+ .filter(Boolean)
46
+ .join(', ');
47
+ lines.push(`- ${prop.name}: ${prop.type}${flags ? ` (${flags})` : ''} ← ${locate(root, component.rel, prop.name)}`);
48
+ if (prop.description)
49
+ lines.push(` ${prop.description}`);
50
+ }
51
+ if (component.events.length)
52
+ lines.push('', `## Events`, ...component.events.map((name) => `- ${name}`));
53
+ if (component.slots.length)
54
+ lines.push('', `## Slots`, ...component.slots.map((name) => `- ${name}`));
55
+ if (component.exposed.length)
56
+ lines.push('', `## Exposed`, ...component.exposed.map((name) => `- ${name}`));
57
+ const scenes = manifest.scenes.filter((scene) => component.scenes.includes(scene.id));
58
+ lines.push('', '## 相关场景');
59
+ if (!scenes.length)
60
+ lines.push('- (暂无场景,建议补一个)');
61
+ for (const scene of scenes) {
62
+ lines.push(`- ${scene.group} / ${scene.title} ← ${scene.rel}`);
63
+ }
64
+ if (component.uncoveredProps.length) {
65
+ lines.push('', `## 未覆盖 props`, ...component.uncoveredProps.map((name) => `- ${name}`));
66
+ }
67
+ return lines.join('\n');
68
+ };
69
+ const matches = (component, query) => {
70
+ const needle = query.toLowerCase();
71
+ return (component.name.toLowerCase().includes(needle) ||
72
+ component.rel.toLowerCase().includes(needle) ||
73
+ component.props.some((prop) => prop.name.toLowerCase().includes(needle)) ||
74
+ component.scenes.some((scene) => scene.toLowerCase().includes(needle)));
75
+ };
76
+ const main = async () => {
77
+ const { project, manifest: manifestPath } = parseArgs(process.argv);
78
+ let manifest;
79
+ try {
80
+ manifest = loadManifest(manifestPath);
81
+ }
82
+ catch (error) {
83
+ console.error(error instanceof Error ? error.message : error);
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const server = new McpServer({ name: 'rinova-playground', version: '0.1.0' });
88
+ server.registerTool('capability_search', {
89
+ title: '搜索组件能力',
90
+ description: '按组件名 / prop 名 / 场景 id 搜索,返回带文件行号的摘要',
91
+ inputSchema: { query: z.string().min(1).describe('关键词'), limit: z.number().int().min(1).max(20).optional() },
92
+ }, ({ query, limit }) => {
93
+ const hits = manifest.components.filter((component) => matches(component, query)).slice(0, limit ?? 5);
94
+ if (!hits.length) {
95
+ return { content: [{ type: 'text', text: `没有匹配 "${query}" 的组件。已知组件:${manifest.components.map((item) => item.name).join(', ')}` }] };
96
+ }
97
+ const text = hits
98
+ .map((component) => `- ${component.name}(${component.rel})props: ${component.props.map((prop) => prop.name).join(', ') || '无'}|场景 ${component.scenes.length} 个`)
99
+ .join('\n');
100
+ return { content: [{ type: 'text', text }] };
101
+ });
102
+ server.registerTool('capability_get', {
103
+ title: '获取组件契约',
104
+ description: '返回组件的 props / events / slots / exposed、源码锚点与相关场景',
105
+ inputSchema: { component: z.string().min(1).describe('组件名,如 MyButton') },
106
+ }, ({ component }) => {
107
+ const found = manifest.components.find((item) => item.name.toLowerCase() === component.toLowerCase() || item.rel.endsWith(component));
108
+ if (!found) {
109
+ return {
110
+ content: [
111
+ { type: 'text', text: `没找到组件 "${component}"。可用:${manifest.components.map((item) => item.name).join(', ')}` },
112
+ ],
113
+ isError: true,
114
+ };
115
+ }
116
+ return { content: [{ type: 'text', text: formatComponent(project, found, manifest) }] };
117
+ });
118
+ server.registerTool('scenario_list', {
119
+ title: '列出场景',
120
+ description: '列出全部场景;传 component 则只列该组件相关的场景',
121
+ inputSchema: { component: z.string().optional().describe('组件名,可选') },
122
+ }, ({ component }) => {
123
+ const scenes = component
124
+ ? manifest.scenes.filter((scene) => {
125
+ const target = manifest.components.find((item) => item.name === component);
126
+ return target ? target.scenes.includes(scene.id) : false;
127
+ })
128
+ : manifest.scenes;
129
+ if (!scenes.length)
130
+ return { content: [{ type: 'text', text: '没有场景。' }] };
131
+ const text = scenes
132
+ .map((scene) => `- ${scene.group} / ${scene.title} id=${scene.id} ← ${scene.rel}`)
133
+ .join('\n');
134
+ return { content: [{ type: 'text', text }] };
135
+ });
136
+ server.registerTool('coverage_report', {
137
+ title: 'props 覆盖报告',
138
+ description: '列出没有场景覆盖的 props,作为补场景的待办',
139
+ inputSchema: {},
140
+ }, () => {
141
+ const rows = manifest.components.flatMap((component) => component.uncoveredProps.map((prop) => `- ${component.name}.${prop} ← ${locate(project, component.rel, prop)}`));
142
+ const total = manifest.components.reduce((sum, component) => sum + component.props.length, 0);
143
+ const uncovered = rows.length;
144
+ const head = `覆盖 ${total - uncovered}/${total}|清单生成时间 ${manifest.generatedAt}`;
145
+ return { content: [{ type: 'text', text: rows.length ? `${head}\n\n${rows.join('\n')}` : `${head}\n\n全部 props 都有场景覆盖。` }] };
146
+ });
147
+ const transport = new StdioServerTransport();
148
+ await server.connect(transport);
149
+ console.error(`[rinova-playground-mcp] 已启动,清单:${manifestPath}(${manifest.components.length} 个组件)`);
150
+ };
151
+ await main();
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@rinova/playground-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "rinova-playground 的 MCP server:把契约清单暴露给 AI 编码工具",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/jove-rina/rinova-playground"
10
+ },
11
+ "bin": {
12
+ "rinova-playground-mcp": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "start": "node dist/index.js"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "1.32.2",
24
+ "zod": "4.6.7"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "26.6.6",
28
+ "typescript": "5.9.3"
29
+ }
30
+ }