rsbuild-plugin-vue-mcp 0.1.1 → 0.1.3
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 +27 -11
- package/README_zh.md +27 -11
- package/dist/396.js +413 -0
- package/dist/index.cjs +289 -215
- package/dist/index.js +2 -4
- package/dist/overlay-bootstrap.js +1 -1
- package/dist/overlay.js +166 -45
- package/dist/rspack.cjs +289 -215
- package/dist/rspack.js +2 -4
- package/dist/types/types.d.ts +34 -22
- package/package.json +1 -3
- package/dist/551.js +0 -337
- package/dist/types/mcp/rpc.d.ts +0 -2
package/README.md
CHANGED
|
@@ -104,9 +104,10 @@ Add that URL to your client's MCP configuration (Cursor, Claude Desktop, VS Code
|
|
|
104
104
|
```json
|
|
105
105
|
{
|
|
106
106
|
"mcpServers": {
|
|
107
|
-
"vue-
|
|
107
|
+
"vue-devtools": {
|
|
108
108
|
"type": "streamable-http",
|
|
109
109
|
"url": "http://localhost:<YourPort>/__mcp/mcp",
|
|
110
|
+
"description": "Vue DevTools MCP - Rsbuild/Rspack MCP plugin based on Vue DevTools",
|
|
110
111
|
"disabled": false
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -129,15 +130,26 @@ Once the page is open, the AI assistant can call the tools listed below against
|
|
|
129
130
|
The plugin registers the following tools on the MCP server. Each tool talks to the app page through birpc, so the data
|
|
130
131
|
always reflects the **live** application. All tools return their results as **JSON-formatted text** (not markdown).
|
|
131
132
|
|
|
132
|
-
| Tool
|
|
133
|
-
|
|
134
|
-
| `get-
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
133
|
+
| Tool | Description | Inputs |
|
|
134
|
+
|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
|
135
|
+
| `get-app-record-status` | Get the current DevTools App status (appRecords and activeAppRecord). Must be called first before any other tool. An empty result usually means the app page needs to be opened. | — |
|
|
136
|
+
| `toggle-app` | Toggle the activeAppRecord by its id. All operations on Components/Router/Pinia are based on the activeAppRecord. | `id: string` |
|
|
137
|
+
| `get-component-tree` | Get the Vue component tree. The result is returned as a **JSON** text payload. | `componentName?: string` (optional, query a single component by name) |
|
|
138
|
+
| `get-component-state` | Get a component's state as JSON (data, props, computed, refs…). | `componentName: string` |
|
|
139
|
+
| `edit-component-state` | Edit a value inside a component's state (live, reactive). | `componentName: string`, `path: string[]`, `value: string`, `valueType: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` |
|
|
140
|
+
| `highlight-component` | Highlight a component on the page (auto-clears after 5s). | `componentName: string` |
|
|
141
|
+
| `scroll-to-component` | Scroll the page to bring a component into view. | `componentName: string` |
|
|
142
|
+
| `get-component-bounds` | Get a component's bounding rect (x/y/width/height, plus viewport size and in-viewport flag). | `componentName: string` |
|
|
143
|
+
| `get-component-dom` | Get the rendered DOM (outerHTML) of a component (truncated to 10000 chars). | `componentName: string` |
|
|
144
|
+
| `get-component-render-code` | Get the compiled render function code of a component (truncated to 10000 chars). | `componentName: string` |
|
|
145
|
+
| `pick-component` | Enter pick mode: the user clicks a component on the page, then its id and name are returned (blocks until click, ~30s timeout). | — |
|
|
146
|
+
| `cancel-pick-component` | Cancel the pick mode started by `pick-component`. | — |
|
|
147
|
+
| `open-component-in-editor` | Open the component's source file in the local editor via the dev server `__open-in-editor` endpoint (SFC only). | `componentName: string`, `baseUrl?: string` (defaults to the page origin) |
|
|
148
|
+
| `get-router-info` | Get the current Vue Router info as JSON (route, matched records, params, query…). | — |
|
|
149
|
+
| `navigate-router` | Navigate the router (like `router.push`) and return the new route info. Requires vue-router. | `path?: string`, `name?: string`, `params?: Record<string, string>`, `query?: Record<string, string>` |
|
|
150
|
+
| `get-pinia-tree` | Get the Pinia store tree as JSON. | — |
|
|
151
|
+
| `get-pinia-state` | Get a single Pinia store's state as JSON. | `storeName: string` |
|
|
152
|
+
| `edit-pinia-state` | Edit a value inside a Pinia store's state (live, reactive). | `storeName: string`, `path: string[]`, `value: string`, `valueType: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` |
|
|
141
153
|
|
|
142
154
|
### Example AI workflow
|
|
143
155
|
|
|
@@ -145,8 +157,12 @@ always reflects the **live** application. All tools return their results as **JS
|
|
|
145
157
|
- "What is the state of the `UserCard` component?"
|
|
146
158
|
- "Set `count` in `Counter` to `10`." → calls `edit-component-state` and the UI updates instantly.
|
|
147
159
|
- "Highlight the `Navbar` component." → the element flashes in the browser.
|
|
160
|
+
- "Scroll to the `Footer` component." → calls `scroll-to-component`.
|
|
161
|
+
- "Open `Footer`'s source in my editor." → calls `open-component-in-editor`.
|
|
162
|
+
- "Navigate to `/users/1` and show me its component tree." → calls `navigate-router`.
|
|
163
|
+
- "I don't know which component this button belongs to." → calls `pick-component` and the user clicks it.
|
|
148
164
|
- "What route are we on and what are its params?" → calls `get-router-info`.
|
|
149
|
-
- "Show me the state
|
|
165
|
+
- "Show me the `cart` Pinia store state and set `count` to `10`." → calls `get-pinia-state` + `edit-pinia-state`.
|
|
150
166
|
|
|
151
167
|
## How it works
|
|
152
168
|
|
package/README_zh.md
CHANGED
|
@@ -101,9 +101,10 @@ MCP 服务(基于 Streamable HTTP 传输)将在 `http://localhost:[port]/__m
|
|
|
101
101
|
```json
|
|
102
102
|
{
|
|
103
103
|
"mcpServers": {
|
|
104
|
-
"vue-
|
|
104
|
+
"vue-devtools": {
|
|
105
105
|
"type": "streamable-http",
|
|
106
106
|
"url": "http://localhost:<YourPort>/__mcp/mcp",
|
|
107
|
+
"description": "Vue DevTools MCP - Rsbuild/Rspack MCP plugin based on Vue DevTools",
|
|
107
108
|
"disabled": false
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -125,15 +126,26 @@ MCP 服务(基于 Streamable HTTP 传输)将在 `http://localhost:[port]/__m
|
|
|
125
126
|
插件会在 MCP 服务上注册以下工具。每个工具都通过 birpc 与 App 页面通信,因此数据始终是**实时**的应用状态。所有工具返回的结果均为
|
|
126
127
|
**JSON 格式的文本**(不是 markdown)。
|
|
127
128
|
|
|
128
|
-
| 工具
|
|
129
|
-
|
|
130
|
-
| `get-
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
129
|
+
| 工具 | 说明 | 入参 |
|
|
130
|
+
|-----------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
|
|
131
|
+
| `get-app-record-status` | 获取当前 DevTools App 状态(appRecords 与 activeAppRecord)。调用任何其他工具前必须先调用它。空结果通常表示需要打开应用页面。 | — |
|
|
132
|
+
| `toggle-app` | 通过 appRecord 的 id 切换 activeAppRecord。组件/路由/Pinia 等操作都基于 activeAppRecord。 | `id: string` |
|
|
133
|
+
| `get-component-tree` | 获取 Vue 组件树。返回结果为 **JSON** 文本。 | `componentName?: string`(可选,按组件名查询单个组件) |
|
|
134
|
+
| `get-component-state` | 以 JSON 格式获取组件状态(data、props、computed、refs…)。 | `componentName: string` |
|
|
135
|
+
| `edit-component-state` | 编辑组件状态中的某个值(实时、响应式)。 | `componentName: string`、`path: string[]`、`value: string`、`valueType: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` |
|
|
136
|
+
| `highlight-component` | 在页面中高亮某个组件(5 秒后自动取消)。 | `componentName: string` |
|
|
137
|
+
| `scroll-to-component` | 滚动页面使指定组件进入视口。 | `componentName: string` |
|
|
138
|
+
| `get-component-bounds` | 获取组件的包围盒(x/y/width/height,含视口尺寸与是否在视口内)。 | `componentName: string` |
|
|
139
|
+
| `get-component-dom` | 获取组件渲染后的 DOM(outerHTML,超过 10000 字符会截断)。 | `componentName: string` |
|
|
140
|
+
| `get-component-render-code` | 获取组件编译后的渲染函数代码(超过 10000 字符会截断)。 | `componentName: string` |
|
|
141
|
+
| `pick-component` | 进入组件拾取模式:用户在页面上点击某个组件后返回其 id 与名称(阻塞等待点击,约 30 秒超时)。 | — |
|
|
142
|
+
| `cancel-pick-component` | 取消 `pick-component` 开启的拾取模式。 | — |
|
|
143
|
+
| `open-component-in-editor` | 通过 dev server 的 `__open-in-editor` 端点在本地编辑器中打开组件源码(仅支持 SFC)。 | `componentName: string`、`baseUrl?: string`(默认为页面 origin) |
|
|
144
|
+
| `get-router-info` | 以 JSON 格式获取当前 Vue Router 信息(路由、匹配记录、params、query…)。 | — |
|
|
145
|
+
| `navigate-router` | 执行路由导航(类似 `router.push`),返回导航后的路由信息。需要应用安装 vue-router。 | `path?: string`、`name?: string`、`params?: Record<string, string>`、`query?: Record<string, string>` |
|
|
146
|
+
| `get-pinia-tree` | 以 JSON 格式获取 Pinia store 树。 | — |
|
|
147
|
+
| `get-pinia-state` | 以 JSON 格式获取单个 Pinia store 的状态。 | `storeName: string` |
|
|
148
|
+
| `edit-pinia-state` | 编辑 Pinia store 状态中的某个值(实时、响应式)。 | `storeName: string`、`path: string[]`、`value: string`、`valueType: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` |
|
|
137
149
|
|
|
138
150
|
### AI 使用示例
|
|
139
151
|
|
|
@@ -141,8 +153,12 @@ MCP 服务(基于 Streamable HTTP 传输)将在 `http://localhost:[port]/__m
|
|
|
141
153
|
- "`UserCard` 组件现在的状态是什么?"
|
|
142
154
|
- "把 `Counter` 组件的 `count` 改成 `10`。" → 调用 `edit-component-state`,页面 UI 立即更新。
|
|
143
155
|
- "高亮一下 `Navbar` 组件。" → 浏览器中对应元素闪烁。
|
|
156
|
+
- "滚到 `Footer` 组件那里。" → 调用 `scroll-to-component`。
|
|
157
|
+
- "把 `Footer` 的源码在编辑器里打开。" → 调用 `open-component-in-editor`。
|
|
158
|
+
- "导航到 `/users/1` 页面看看组件树。" → 调用 `navigate-router`。
|
|
159
|
+
- "我不知道这个按钮对应哪个组件。" → 调用 `pick-component`,让用户在页面上点一下。
|
|
144
160
|
- "当前路由是什么,参数是什么?" → 调用 `get-router-info`。
|
|
145
|
-
- "看看 `cart` 这个 Pinia store
|
|
161
|
+
- "看看 `cart` 这个 Pinia store 的状态,把 `count` 改成 `10`。" → 调用 `get-pinia-state` + `edit-pinia-state`。
|
|
146
162
|
|
|
147
163
|
## 工作原理
|
|
148
164
|
|
package/dist/396.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { createBirpcGroup } from "birpc";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
function createVueMcpContext() {
|
|
6
|
+
return {
|
|
7
|
+
rpcServer: null
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const channelCache = new WeakMap();
|
|
11
|
+
function createRPCServer(ws, functions, options = {}) {
|
|
12
|
+
const group = createBirpcGroup(functions, ()=>Array.from(ws?.clients || []).map((channel)=>{
|
|
13
|
+
if (channel.readyState === channel.CLOSED) return;
|
|
14
|
+
const cached = channelCache.get(channel);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
const options = {
|
|
17
|
+
on: (fn)=>channel.on('message', fn),
|
|
18
|
+
post: (data)=>channel.send(data),
|
|
19
|
+
serialize: (v)=>JSON.stringify(v),
|
|
20
|
+
deserialize: (v)=>JSON.parse(v)
|
|
21
|
+
};
|
|
22
|
+
channelCache.set(channel, options);
|
|
23
|
+
return options;
|
|
24
|
+
}).filter((c)=>!!c), options);
|
|
25
|
+
ws.on('connection', ()=>{
|
|
26
|
+
group.updateChannels();
|
|
27
|
+
});
|
|
28
|
+
return group.broadcast;
|
|
29
|
+
}
|
|
30
|
+
var package_namespaceObject = {
|
|
31
|
+
rE: "0.1.3"
|
|
32
|
+
};
|
|
33
|
+
function stringify(value) {
|
|
34
|
+
return "string" == typeof value ? value : JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
function createMcpServerDefault(options, ctx) {
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: "rsbuild-vue-mcp",
|
|
39
|
+
description: 'Rsbuild/Rspack MCP plugin based on Vue DevTools',
|
|
40
|
+
version: package_namespaceObject.rE,
|
|
41
|
+
...options.mcpServerInfo
|
|
42
|
+
});
|
|
43
|
+
server.registerPrompt('app-record-guide', {
|
|
44
|
+
title: 'App Record Guide',
|
|
45
|
+
description: 'How to read the App Record status and switch the active app instance before using other MCP tools.'
|
|
46
|
+
}, async ()=>({
|
|
47
|
+
messages: [
|
|
48
|
+
{
|
|
49
|
+
role: 'user',
|
|
50
|
+
content: {
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: "Before using any other MCP tools, call `get-app-record-status` to read the current Vue DevTools App status (appRecords and activeAppRecord).\n\nHow to interpret the result:\n- Empty content: no web page with an app is open. The user needs to open the page in a browser first.\n- Multiple content items: the user has multiple app pages open. Close all but one to avoid interference between them.\n- A content item containing multiple appRecords: multiple Vue App instances exist on the same page. Use `toggle-app` (with the appRecord `id`) to switch the activeAppRecord.\n\nAll MCP tool operations on Components, Router, Pinia, etc. are performed based on the activeAppRecord. When you need to target a different app instance, call `toggle-app` first."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}));
|
|
57
|
+
server.registerTool('get-app-record-status', {
|
|
58
|
+
title: 'App Record Status',
|
|
59
|
+
description: "Get the current Devtools App status (appRecords and activeAppRecord). Must be called first before any other MCP tools. See the `app-record-guide` prompt for how to interpret the result."
|
|
60
|
+
}, async ()=>{
|
|
61
|
+
const res = await ctx.rpcServer.getAppRecordStatus();
|
|
62
|
+
return {
|
|
63
|
+
content: res.map((r)=>({
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: stringify(r)
|
|
66
|
+
}))
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
server.registerTool('toggle-app', {
|
|
70
|
+
title: 'Toggle App',
|
|
71
|
+
description: 'Toggle the activeAppRecord by the id of the appRecord. All MCP tool operations on Components, Router, Pinia, etc. are performed based on the activeAppRecord. See the `app-record-guide` prompt for details.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
id: z.string()
|
|
74
|
+
}
|
|
75
|
+
}, async ({ id })=>{
|
|
76
|
+
await ctx.rpcServer.toggleApp({
|
|
77
|
+
id
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: 'ok'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
server.registerTool('get-component-tree', {
|
|
89
|
+
description: 'Get the Vue component tree in JSON tree syntax format (by Vue DevTools stringify, a two-dimensional serialization format that separates structure from values, replacing nested object references with IDs.).',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
componentName: z.string().optional().describe('query by componentName (the component\'s name, not uid), default query all components')
|
|
92
|
+
}
|
|
93
|
+
}, async ({ componentName })=>{
|
|
94
|
+
const res = await ctx.rpcServer.getInspectorTree({
|
|
95
|
+
componentName
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
content: res.map((r)=>({
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: stringify(r)
|
|
101
|
+
}))
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
server.registerTool('get-component-state', {
|
|
105
|
+
description: 'Get the Vue component state in JSON structure format.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
componentName: z.string().describe('query by componentName (the component\'s name, not uid)')
|
|
108
|
+
}
|
|
109
|
+
}, async ({ componentName })=>{
|
|
110
|
+
const res = await ctx.rpcServer.getInspectorState({
|
|
111
|
+
componentName
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: res.map((r)=>({
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: stringify(r)
|
|
117
|
+
}))
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
server.registerTool('edit-component-state', {
|
|
121
|
+
description: 'Edit the Vue component state.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)'),
|
|
124
|
+
path: z.array(z.string()),
|
|
125
|
+
value: z.string(),
|
|
126
|
+
valueType: z["enum"]([
|
|
127
|
+
'string',
|
|
128
|
+
'number',
|
|
129
|
+
'boolean',
|
|
130
|
+
'object',
|
|
131
|
+
'array'
|
|
132
|
+
])
|
|
133
|
+
}
|
|
134
|
+
}, async ({ componentName, path, value, valueType })=>{
|
|
135
|
+
await ctx.rpcServer.editComponentState({
|
|
136
|
+
componentName,
|
|
137
|
+
path,
|
|
138
|
+
value,
|
|
139
|
+
valueType
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: 'ok'
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
server.registerTool('highlight-component', {
|
|
151
|
+
description: 'Highlight the Vue component.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)')
|
|
154
|
+
}
|
|
155
|
+
}, async ({ componentName })=>{
|
|
156
|
+
await ctx.rpcServer.highlightComponent({
|
|
157
|
+
componentName
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: 'ok'
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
server.registerTool('scroll-to-component', {
|
|
169
|
+
description: 'Scroll the page to bring the Vue component into view.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)')
|
|
172
|
+
}
|
|
173
|
+
}, async ({ componentName })=>{
|
|
174
|
+
await ctx.rpcServer.scrollToComponent({
|
|
175
|
+
componentName
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: 'ok'
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
server.registerTool('get-component-bounds', {
|
|
187
|
+
description: 'Get the bounding rect of the Vue component (x/y/width/height relative to the viewport, plus viewport size and whether it is currently in the viewport).',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)')
|
|
190
|
+
}
|
|
191
|
+
}, async ({ componentName })=>{
|
|
192
|
+
const res = await ctx.rpcServer.getComponentBounds({
|
|
193
|
+
componentName
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
content: res.map((r)=>({
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: stringify(r)
|
|
199
|
+
}))
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
server.registerTool('get-component-dom', {
|
|
203
|
+
description: 'Get the rendered DOM (outerHTML) of the Vue component. The output is truncated to 10000 characters.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)')
|
|
206
|
+
}
|
|
207
|
+
}, async ({ componentName })=>{
|
|
208
|
+
const res = await ctx.rpcServer.getComponentDom({
|
|
209
|
+
componentName
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
content: res.map((r)=>({
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: stringify(r)
|
|
215
|
+
}))
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
server.registerTool('get-component-render-code', {
|
|
219
|
+
description: 'Get the compiled render function code of the Vue component. The output is truncated to 10000 characters.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)')
|
|
222
|
+
}
|
|
223
|
+
}, async ({ componentName })=>{
|
|
224
|
+
const res = await ctx.rpcServer.getComponentRenderCode({
|
|
225
|
+
componentName
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
content: res.map((r)=>({
|
|
229
|
+
type: 'text',
|
|
230
|
+
text: stringify(r)
|
|
231
|
+
}))
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
server.registerTool('pick-component', {
|
|
235
|
+
description: "Enter component pick mode: the user clicks a component on the page, then the tool returns its id and name. It blocks until the user clicks (may time out, birpc timeout is 30s). Use `cancel-pick-component` to cancel. Useful when you don't know the name of the target component."
|
|
236
|
+
}, async ()=>{
|
|
237
|
+
const res = await ctx.rpcServer.pickComponent();
|
|
238
|
+
return {
|
|
239
|
+
content: res.map((r)=>({
|
|
240
|
+
type: 'text',
|
|
241
|
+
text: stringify(r)
|
|
242
|
+
}))
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
server.registerTool('cancel-pick-component', {
|
|
246
|
+
description: 'Cancel the component pick mode started by `pick-component`.'
|
|
247
|
+
}, async ()=>{
|
|
248
|
+
await ctx.rpcServer.cancelPickComponent();
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: 'ok'
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
server.registerTool('open-component-in-editor', {
|
|
259
|
+
description: "Open the Vue component's source file in the local editor via the dev server `__open-in-editor` endpoint. Only works for SFC components (requires `__file` in the compiled component).",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
componentName: z.string().describe('by componentName (the component\'s name, not uid)'),
|
|
262
|
+
baseUrl: z.string().optional().describe('dev server base url, defaults to the page origin')
|
|
263
|
+
}
|
|
264
|
+
}, async ({ componentName, baseUrl })=>{
|
|
265
|
+
await ctx.rpcServer.openComponentInEditor({
|
|
266
|
+
componentName,
|
|
267
|
+
baseUrl
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: 'ok'
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
server.registerTool('get-router-info', {
|
|
279
|
+
description: 'Get the Vue router info in JSON structure format.'
|
|
280
|
+
}, async ()=>{
|
|
281
|
+
const res = await ctx.rpcServer.getRouterInfo();
|
|
282
|
+
return {
|
|
283
|
+
content: res.map((r)=>({
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: stringify(r)
|
|
286
|
+
}))
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
server.registerTool('navigate-router', {
|
|
290
|
+
description: 'Navigate the Vue Router (like router.push) and return the new route info. Requires vue-router to be installed in the app.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
path: z.string().optional().describe('target path, e.g. `/users/1`'),
|
|
293
|
+
name: z.string().optional().describe('target route name (used when `path` is not provided)'),
|
|
294
|
+
params: z.record(z.string(), z.string()).optional().describe('route params for named routes'),
|
|
295
|
+
query: z.record(z.string(), z.string()).optional().describe('query params, e.g. `{ "tab": "1" }`')
|
|
296
|
+
}
|
|
297
|
+
}, async ({ path, name, params, query })=>{
|
|
298
|
+
const res = await ctx.rpcServer.navigateRouter({
|
|
299
|
+
path,
|
|
300
|
+
name,
|
|
301
|
+
params,
|
|
302
|
+
query
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
content: res.map((r)=>({
|
|
306
|
+
type: 'text',
|
|
307
|
+
text: stringify(r)
|
|
308
|
+
}))
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
server.registerTool('get-pinia-state', {
|
|
312
|
+
description: 'Get the Pinia state in JSON structure format.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
storeName: z.string()
|
|
315
|
+
}
|
|
316
|
+
}, async ({ storeName })=>{
|
|
317
|
+
const res = await ctx.rpcServer.getPiniaState({
|
|
318
|
+
storeName
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
content: res.map((r)=>({
|
|
322
|
+
type: 'text',
|
|
323
|
+
text: stringify(r)
|
|
324
|
+
}))
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
server.registerTool('edit-pinia-state', {
|
|
328
|
+
description: 'Edit a value inside a Pinia store\'s state (live, reactive).',
|
|
329
|
+
inputSchema: {
|
|
330
|
+
storeName: z.string(),
|
|
331
|
+
path: z.array(z.string()).describe('path to the value inside the store state, e.g. `["items", "0", "name"]`'),
|
|
332
|
+
value: z.string(),
|
|
333
|
+
valueType: z["enum"]([
|
|
334
|
+
'string',
|
|
335
|
+
'number',
|
|
336
|
+
'boolean',
|
|
337
|
+
'object',
|
|
338
|
+
'array'
|
|
339
|
+
])
|
|
340
|
+
}
|
|
341
|
+
}, async ({ storeName, path, value, valueType })=>{
|
|
342
|
+
await ctx.rpcServer.editPiniaState({
|
|
343
|
+
storeName,
|
|
344
|
+
path,
|
|
345
|
+
value,
|
|
346
|
+
valueType
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
text: 'ok'
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
server.registerTool('get-pinia-tree', {
|
|
358
|
+
description: 'Get the Pinia tree in JSON structure format.'
|
|
359
|
+
}, async ()=>{
|
|
360
|
+
const res = await ctx.rpcServer.getPiniaTree();
|
|
361
|
+
return {
|
|
362
|
+
content: res.map((r)=>({
|
|
363
|
+
type: 'text',
|
|
364
|
+
text: stringify(r)
|
|
365
|
+
}))
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
return server;
|
|
369
|
+
}
|
|
370
|
+
async function mcpHandlerStateless(server, req, res) {
|
|
371
|
+
try {
|
|
372
|
+
const transport = new StreamableHTTPServerTransport({
|
|
373
|
+
sessionIdGenerator: void 0
|
|
374
|
+
});
|
|
375
|
+
await server.connect(transport);
|
|
376
|
+
await transport.handleRequest(req, res);
|
|
377
|
+
res.on('close', ()=>{
|
|
378
|
+
transport.close();
|
|
379
|
+
server.close();
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Error handling MCP request:', error);
|
|
383
|
+
if (!res.headersSent) {
|
|
384
|
+
res.statusCode = 500;
|
|
385
|
+
res.setHeader('content-type', 'application/json');
|
|
386
|
+
res.end(JSON.stringify({
|
|
387
|
+
jsonrpc: '2.0',
|
|
388
|
+
error: {
|
|
389
|
+
code: -32603,
|
|
390
|
+
message: 'Internal server error'
|
|
391
|
+
},
|
|
392
|
+
id: null
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function setupRoutes(base, getServer, dev) {
|
|
398
|
+
dev.middlewares.use(`${base}/mcp`, async (req, res)=>{
|
|
399
|
+
const server = await getServer();
|
|
400
|
+
await mcpHandlerStateless(server, req, res);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function setupRspackRoutes(base, getServer, middlewares) {
|
|
404
|
+
middlewares.push({
|
|
405
|
+
name: 'vue-devtools-mcp',
|
|
406
|
+
path: `${base}/mcp`,
|
|
407
|
+
async middleware (req, res) {
|
|
408
|
+
const server = await getServer();
|
|
409
|
+
await mcpHandlerStateless(server, req, res);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
export { createMcpServerDefault, createRPCServer, createVueMcpContext, setupRoutes, setupRspackRoutes };
|