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 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-mcp": {
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 | Description | Inputs |
133
- |------------------------|-----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
134
- | `get-component-tree` | Get the Vue component tree. The result is returned as a **JSON** text payload. | — |
135
- | `get-component-state` | Get a component's state as JSON (data, props, computed, refs…). | `componentName: string` |
136
- | `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'` |
137
- | `highlight-component` | Highlight a component on the page (auto-clears after 5s). | `componentName: string` |
138
- | `get-router-info` | Get the current Vue Router info as JSON (route, matched records, params, query…). | — |
139
- | `get-pinia-tree` | Get the Pinia store tree as JSON. | — |
140
- | `get-pinia-state` | Get a single Pinia store's state as JSON. | `storeName: string` |
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 of the `cart` Pinia store."
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-mcp": {
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-component-tree` | 获取 Vue 组件树。返回结果为 **JSON** 文本。 | — |
131
- | `get-component-state` | 以 JSON 格式获取组件状态(data、props、computed、refs…)。 | `componentName: string` |
132
- | `edit-component-state` | 编辑组件状态中的某个值(实时、响应式)。 | `componentName: string`、`path: string[]`、`value: string`、`valueType: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` |
133
- | `highlight-component` | 在页面中高亮某个组件(5 秒后自动取消)。 | `componentName: string` |
134
- | `get-router-info` | 以 JSON 格式获取当前 Vue Router 信息(路由、匹配记录、params、query…)。 | — |
135
- | `get-pinia-tree` | 以 JSON 格式获取 Pinia store 树。 | — |
136
- | `get-pinia-state` | 以 JSON 格式获取单个 Pinia store 的状态。 | `storeName: string` |
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 };