rsbuild-plugin-vue-mcp 0.1.1 → 0.1.2

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
  }
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
  }
package/dist/396.js ADDED
@@ -0,0 +1,237 @@
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.2"
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.registerTool('get-app-record-status', {
44
+ title: 'App Record Status',
45
+ description: "Get the current Devtools App status (appRecords and activeAppRecord). Must be called first before any other MCP tools. An empty content response usually means the user needs to open a web page with an app to debug. If multiple content items are returned, the user has multiple app pages open and should close all but one to avoid interference. If a content contains multiple appRecords, it means multiple Vue App instances exist on the page. `toggle-app` to switch activeAppRecord. "
46
+ }, async ()=>{
47
+ const res = await ctx.rpcServer.getAppRecordStatus();
48
+ return {
49
+ content: res.map((r)=>({
50
+ type: 'text',
51
+ text: stringify(r)
52
+ }))
53
+ };
54
+ });
55
+ server.registerTool('toggle-app', {
56
+ title: 'Toggle App',
57
+ 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.',
58
+ inputSchema: {
59
+ id: z.string()
60
+ }
61
+ }, async ({ id })=>{
62
+ await ctx.rpcServer.toggleApp({
63
+ id
64
+ });
65
+ return {
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: 'ok'
70
+ }
71
+ ]
72
+ };
73
+ });
74
+ server.registerTool('get-component-tree', {
75
+ 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.).',
76
+ inputSchema: {
77
+ componentName: z.string().optional().describe('query by componentName (the component\'s name, not uid), default query all components')
78
+ }
79
+ }, async ({ componentName })=>{
80
+ const res = await ctx.rpcServer.getInspectorTree({
81
+ componentName
82
+ });
83
+ return {
84
+ content: res.map((r)=>({
85
+ type: 'text',
86
+ text: stringify(r)
87
+ }))
88
+ };
89
+ });
90
+ server.registerTool('get-component-state', {
91
+ description: 'Get the Vue component state in JSON structure format.',
92
+ inputSchema: {
93
+ componentName: z.string().describe('query by componentName (the component\'s name, not uid)')
94
+ }
95
+ }, async ({ componentName })=>{
96
+ const res = await ctx.rpcServer.getInspectorState({
97
+ componentName
98
+ });
99
+ return {
100
+ content: res.map((r)=>({
101
+ type: 'text',
102
+ text: stringify(r)
103
+ }))
104
+ };
105
+ });
106
+ server.registerTool('edit-component-state', {
107
+ description: 'Edit the Vue component state.',
108
+ inputSchema: {
109
+ componentName: z.string().describe('by componentName (the component\'s name, not uid)'),
110
+ path: z.array(z.string()),
111
+ value: z.string(),
112
+ valueType: z["enum"]([
113
+ 'string',
114
+ 'number',
115
+ 'boolean',
116
+ 'object',
117
+ 'array'
118
+ ])
119
+ }
120
+ }, async ({ componentName, path, value, valueType })=>{
121
+ await ctx.rpcServer.editComponentState({
122
+ componentName,
123
+ path,
124
+ value,
125
+ valueType
126
+ });
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text',
131
+ text: 'ok'
132
+ }
133
+ ]
134
+ };
135
+ });
136
+ server.registerTool('highlight-component', {
137
+ description: 'Highlight the Vue component.',
138
+ inputSchema: {
139
+ componentName: z.string().describe('by componentName (the component\'s name, not uid)')
140
+ }
141
+ }, async ({ componentName })=>{
142
+ await ctx.rpcServer.highlightComponent({
143
+ componentName
144
+ });
145
+ return {
146
+ content: [
147
+ {
148
+ type: 'text',
149
+ text: 'ok'
150
+ }
151
+ ]
152
+ };
153
+ });
154
+ server.registerTool('get-router-info', {
155
+ description: 'Get the Vue router info in JSON structure format.'
156
+ }, async ()=>{
157
+ const res = await ctx.rpcServer.getRouterInfo();
158
+ return {
159
+ content: res.map((r)=>({
160
+ type: 'text',
161
+ text: stringify(r)
162
+ }))
163
+ };
164
+ });
165
+ server.registerTool('get-pinia-state', {
166
+ description: 'Get the Pinia state in JSON structure format.',
167
+ inputSchema: {
168
+ storeName: z.string()
169
+ }
170
+ }, async ({ storeName })=>{
171
+ const res = await ctx.rpcServer.getPiniaState({
172
+ storeName
173
+ });
174
+ return {
175
+ content: res.map((r)=>({
176
+ type: 'text',
177
+ text: stringify(r)
178
+ }))
179
+ };
180
+ });
181
+ server.registerTool('get-pinia-tree', {
182
+ description: 'Get the Pinia tree in JSON structure format.'
183
+ }, async ()=>{
184
+ const res = await ctx.rpcServer.getPiniaTree();
185
+ return {
186
+ content: res.map((r)=>({
187
+ type: 'text',
188
+ text: stringify(r)
189
+ }))
190
+ };
191
+ });
192
+ return server;
193
+ }
194
+ async function mcpHandlerStateless(server, req, res) {
195
+ try {
196
+ const transport = new StreamableHTTPServerTransport({
197
+ sessionIdGenerator: void 0
198
+ });
199
+ await server.connect(transport);
200
+ await transport.handleRequest(req, res);
201
+ res.on('close', ()=>{
202
+ transport.close();
203
+ server.close();
204
+ });
205
+ } catch (error) {
206
+ console.error('Error handling MCP request:', error);
207
+ if (!res.headersSent) {
208
+ res.statusCode = 500;
209
+ res.setHeader('content-type', 'application/json');
210
+ res.end(JSON.stringify({
211
+ jsonrpc: '2.0',
212
+ error: {
213
+ code: -32603,
214
+ message: 'Internal server error'
215
+ },
216
+ id: null
217
+ }));
218
+ }
219
+ }
220
+ }
221
+ function setupRoutes(base, getServer, dev) {
222
+ dev.middlewares.use(`${base}/mcp`, async (req, res)=>{
223
+ const server = await getServer();
224
+ await mcpHandlerStateless(server, req, res);
225
+ });
226
+ }
227
+ function setupRspackRoutes(base, getServer, middlewares) {
228
+ middlewares.push({
229
+ name: 'vue-devtools-mcp',
230
+ path: `${base}/mcp`,
231
+ async middleware (req, res) {
232
+ const server = await getServer();
233
+ await mcpHandlerStateless(server, req, res);
234
+ }
235
+ });
236
+ }
237
+ export { createMcpServerDefault, createRPCServer, createVueMcpContext, setupRoutes, setupRspackRoutes };
package/dist/index.cjs CHANGED
@@ -48,45 +48,11 @@ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_no
48
48
  const promises_namespaceObject = require("node:fs/promises");
49
49
  var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
50
50
  const external_ws_namespaceObject = require("ws");
51
- const external_hookable_namespaceObject = require("hookable");
52
51
  function createVueMcpContext() {
53
52
  return {
54
- hooks: (0, external_hookable_namespaceObject.createHooks)(),
55
- rpc: null,
56
53
  rpcServer: null
57
54
  };
58
55
  }
59
- function createServerRpcFunc(ctx) {
60
- return {
61
- getAppRecordStatus: (_)=>({}),
62
- onAppRecordStatusUpdate: (event, data)=>{
63
- ctx.hooks.callHook(event, data).finally();
64
- },
65
- toggleApp: (_)=>({}),
66
- getInspectorTree: (_)=>({}),
67
- onInspectorTreeUpdated: (event, data)=>{
68
- ctx.hooks.callHook(event, data).finally();
69
- },
70
- getInspectorState: (_)=>({}),
71
- onInspectorStateUpdated: (event, data)=>{
72
- ctx.hooks.callHook(event, data).finally();
73
- },
74
- editComponentState (_) {},
75
- highlightComponent (_) {},
76
- getRouterInfo: (_)=>({}),
77
- onRouterInfoUpdated: (event, data)=>{
78
- ctx.hooks.callHook(event, data).finally();
79
- },
80
- getPiniaTree: (_)=>({}),
81
- onPiniaTreeUpdated: (event, data)=>{
82
- ctx.hooks.callHook(event, data).finally();
83
- },
84
- getPiniaState: (_)=>({}),
85
- onPiniaInfoUpdated: (event, data)=>{
86
- ctx.hooks.callHook(event, data).finally();
87
- }
88
- };
89
- }
90
56
  const external_birpc_namespaceObject = require("birpc");
91
57
  const channelCache = new WeakMap();
92
58
  function createRPCServer(ws, functions, options = {}) {
@@ -110,9 +76,8 @@ function createRPCServer(ws, functions, options = {}) {
110
76
  }
111
77
  const mcp_js_namespaceObject = require("@modelcontextprotocol/sdk/server/mcp.js");
112
78
  var package_namespaceObject = {
113
- rE: "0.1.1"
79
+ rE: "0.1.2"
114
80
  };
115
- const external_nanoid_namespaceObject = require("nanoid");
116
81
  const external_zod_namespaceObject = require("zod");
117
82
  function stringify(value) {
118
83
  return "string" == typeof value ? value : JSON.stringify(value);
@@ -126,99 +91,71 @@ function createMcpServerDefault(options, ctx) {
126
91
  });
127
92
  server.registerTool('get-app-record-status', {
128
93
  title: 'App Record Status',
129
- description: 'Get the App status of Devtools on the page. This should be the first tool called before using any other MCP tools, to determine the current App status, including appRecords and activeAppRecord.'
130
- }, async ()=>new Promise((resolve, reject)=>{
131
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
132
- ctx.hooks.hookOnce(eventName, (res)=>{
133
- resolve({
134
- content: [
135
- {
136
- type: 'text',
137
- text: stringify(res)
138
- }
139
- ]
140
- });
141
- });
142
- ctx.rpcServer.getAppRecordStatus({
143
- event: eventName
144
- }).catch((e)=>{
145
- console.error(e);
146
- reject(e);
147
- });
148
- }));
94
+ description: "Get the current Devtools App status (appRecords and activeAppRecord). Must be called first before any other MCP tools. An empty content response usually means the user needs to open a web page with an app to debug. If multiple content items are returned, the user has multiple app pages open and should close all but one to avoid interference. If a content contains multiple appRecords, it means multiple Vue App instances exist on the page. `toggle-app` to switch activeAppRecord. "
95
+ }, async ()=>{
96
+ const res = await ctx.rpcServer.getAppRecordStatus();
97
+ return {
98
+ content: res.map((r)=>({
99
+ type: 'text',
100
+ text: stringify(r)
101
+ }))
102
+ };
103
+ });
149
104
  server.registerTool('toggle-app', {
150
105
  title: 'Toggle App',
151
106
  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.',
152
107
  inputSchema: {
153
108
  id: external_zod_namespaceObject.z.string()
154
109
  }
155
- }, async ({ id })=>new Promise((resolve, reject)=>{
156
- ctx.rpcServer.toggleApp({
157
- id
158
- }).then(()=>{
159
- resolve({
160
- content: [
161
- {
162
- type: 'text',
163
- text: 'ok'
164
- }
165
- ]
166
- });
167
- }).catch((e)=>{
168
- console.error(e);
169
- reject(e);
170
- });
171
- }));
110
+ }, async ({ id })=>{
111
+ await ctx.rpcServer.toggleApp({
112
+ id
113
+ });
114
+ return {
115
+ content: [
116
+ {
117
+ type: 'text',
118
+ text: 'ok'
119
+ }
120
+ ]
121
+ };
122
+ });
172
123
  server.registerTool('get-component-tree', {
173
- 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.).'
174
- }, async ()=>new Promise((resolve, reject)=>{
175
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
176
- ctx.hooks.hookOnce(eventName, (res)=>{
177
- resolve({
178
- content: [
179
- {
180
- type: 'text',
181
- text: stringify(res)
182
- }
183
- ]
184
- });
185
- });
186
- ctx.rpcServer.getInspectorTree({
187
- event: eventName
188
- }).catch((e)=>{
189
- console.error(e);
190
- reject(e);
191
- });
192
- }));
124
+ 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.).',
125
+ inputSchema: {
126
+ componentName: external_zod_namespaceObject.z.string().optional().describe('query by componentName (the component\'s name, not uid), default query all components')
127
+ }
128
+ }, async ({ componentName })=>{
129
+ const res = await ctx.rpcServer.getInspectorTree({
130
+ componentName
131
+ });
132
+ return {
133
+ content: res.map((r)=>({
134
+ type: 'text',
135
+ text: stringify(r)
136
+ }))
137
+ };
138
+ });
193
139
  server.registerTool('get-component-state', {
194
140
  description: 'Get the Vue component state in JSON structure format.',
195
141
  inputSchema: {
196
- componentName: external_zod_namespaceObject.z.string()
142
+ componentName: external_zod_namespaceObject.z.string().describe('query by componentName (the component\'s name, not uid)')
197
143
  }
198
- }, async ({ componentName })=>new Promise((resolve, reject)=>{
199
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
200
- ctx.hooks.hookOnce(eventName, (res)=>{
201
- resolve({
202
- content: [
203
- {
204
- type: 'text',
205
- text: stringify(res)
206
- }
207
- ]
208
- });
209
- });
210
- ctx.rpcServer.getInspectorState({
211
- event: eventName,
212
- componentName
213
- }).catch((e)=>{
214
- console.error(e);
215
- reject(e);
216
- });
217
- }));
144
+ }, async ({ componentName })=>{
145
+ const res = await ctx.rpcServer.getInspectorState({
146
+ componentName
147
+ });
148
+ return {
149
+ content: res.map((r)=>({
150
+ type: 'text',
151
+ text: stringify(r)
152
+ }))
153
+ };
154
+ });
218
155
  server.registerTool('edit-component-state', {
219
156
  description: 'Edit the Vue component state.',
220
157
  inputSchema: {
221
- componentName: external_zod_namespaceObject.z.string(),
158
+ componentName: external_zod_namespaceObject.z.string().describe('by componentName (the component\'s name, not uid)'),
222
159
  path: external_zod_namespaceObject.z.array(external_zod_namespaceObject.z.string()),
223
160
  value: external_zod_namespaceObject.z.string(),
224
161
  valueType: external_zod_namespaceObject.z["enum"]([
@@ -229,115 +166,78 @@ function createMcpServerDefault(options, ctx) {
229
166
  'array'
230
167
  ])
231
168
  }
232
- }, async ({ componentName, path, value, valueType })=>new Promise((resolve, reject)=>{
233
- ctx.rpcServer.editComponentState({
234
- componentName,
235
- path,
236
- value,
237
- valueType
238
- }).then(()=>{
239
- resolve({
240
- content: [
241
- {
242
- type: 'text',
243
- text: 'ok'
244
- }
245
- ]
246
- });
247
- }).catch((e)=>{
248
- console.error(e);
249
- reject(e);
250
- });
251
- }));
169
+ }, async ({ componentName, path, value, valueType })=>{
170
+ await ctx.rpcServer.editComponentState({
171
+ componentName,
172
+ path,
173
+ value,
174
+ valueType
175
+ });
176
+ return {
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: 'ok'
181
+ }
182
+ ]
183
+ };
184
+ });
252
185
  server.registerTool('highlight-component', {
253
186
  description: 'Highlight the Vue component.',
254
187
  inputSchema: {
255
- componentName: external_zod_namespaceObject.z.string()
188
+ componentName: external_zod_namespaceObject.z.string().describe('by componentName (the component\'s name, not uid)')
256
189
  }
257
- }, async ({ componentName })=>new Promise((resolve, reject)=>{
258
- ctx.rpcServer.highlightComponent({
259
- componentName
260
- }).then(()=>{
261
- resolve({
262
- content: [
263
- {
264
- type: 'text',
265
- text: 'ok'
266
- }
267
- ]
268
- });
269
- }).catch((e)=>{
270
- console.error(e);
271
- reject(e);
272
- });
273
- }));
190
+ }, async ({ componentName })=>{
191
+ await ctx.rpcServer.highlightComponent({
192
+ componentName
193
+ });
194
+ return {
195
+ content: [
196
+ {
197
+ type: 'text',
198
+ text: 'ok'
199
+ }
200
+ ]
201
+ };
202
+ });
274
203
  server.registerTool('get-router-info', {
275
204
  description: 'Get the Vue router info in JSON structure format.'
276
- }, async ()=>new Promise((resolve, reject)=>{
277
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
278
- ctx.hooks.hookOnce(eventName, (res)=>{
279
- resolve({
280
- content: [
281
- {
282
- type: 'text',
283
- text: stringify(res)
284
- }
285
- ]
286
- });
287
- });
288
- ctx.rpcServer.getRouterInfo({
289
- event: eventName
290
- }).catch((e)=>{
291
- console.error(e);
292
- reject(e);
293
- });
294
- }));
205
+ }, async ()=>{
206
+ const res = await ctx.rpcServer.getRouterInfo();
207
+ return {
208
+ content: res.map((r)=>({
209
+ type: 'text',
210
+ text: stringify(r)
211
+ }))
212
+ };
213
+ });
295
214
  server.registerTool('get-pinia-state', {
296
215
  description: 'Get the Pinia state in JSON structure format.',
297
216
  inputSchema: {
298
217
  storeName: external_zod_namespaceObject.z.string()
299
218
  }
300
- }, async ({ storeName })=>new Promise((resolve, reject)=>{
301
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
302
- ctx.hooks.hookOnce(eventName, (res)=>{
303
- resolve({
304
- content: [
305
- {
306
- type: 'text',
307
- text: stringify(res)
308
- }
309
- ]
310
- });
311
- });
312
- ctx.rpcServer.getPiniaState({
313
- event: eventName,
314
- storeName
315
- }).catch((e)=>{
316
- console.error(e);
317
- reject(e);
318
- });
319
- }));
219
+ }, async ({ storeName })=>{
220
+ const res = await ctx.rpcServer.getPiniaState({
221
+ storeName
222
+ });
223
+ return {
224
+ content: res.map((r)=>({
225
+ type: 'text',
226
+ text: stringify(r)
227
+ }))
228
+ };
229
+ });
320
230
  server.registerTool('get-pinia-tree', {
321
231
  description: 'Get the Pinia tree in JSON structure format.'
322
- }, async ()=>new Promise((resolve, reject)=>{
323
- const eventName = (0, external_nanoid_namespaceObject.nanoid)();
324
- ctx.hooks.hookOnce(eventName, (res)=>{
325
- resolve({
326
- content: [
327
- {
328
- type: 'text',
329
- text: stringify(res)
330
- }
331
- ]
332
- });
333
- });
334
- ctx.rpcServer.getPiniaTree({
335
- event: eventName
336
- }).catch((e)=>{
337
- console.error(e);
338
- reject(e);
339
- });
340
- }));
232
+ }, async ()=>{
233
+ const res = await ctx.rpcServer.getPiniaTree();
234
+ return {
235
+ content: res.map((r)=>({
236
+ type: 'text',
237
+ text: stringify(r)
238
+ }))
239
+ };
240
+ });
341
241
  return server;
342
242
  }
343
243
  const streamableHttp_js_namespaceObject = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -448,11 +348,9 @@ const pluginVueMcp = (options = {})=>({
448
348
  wss.emit('connection', client, request);
449
349
  });
450
350
  });
451
- const rpc = createServerRpcFunc(ctx);
452
- ctx.rpcServer = createRPCServer(wss, rpc, {
351
+ ctx.rpcServer = createRPCServer(wss, {}, {
453
352
  timeout: 3000
454
353
  });
455
- ctx.rpc = rpc;
456
354
  const { printUrl = true, mcpPath = '/__mcp' } = options;
457
355
  setupRoutes(mcpPath, async ()=>{
458
356
  let mcp = createMcpServerDefault(options, ctx);