@xiaou66/vite-plugin-vue-mcp-next 1.0.3 → 1.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 +54 -1
- package/dist/index.cjs +804 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +277 -5
- package/dist/index.d.ts +277 -5
- package/dist/index.js +755 -57
- package/dist/index.js.map +1 -1
- package/dist/runtime/client.cjs +639 -1
- package/dist/runtime/client.cjs.map +1 -1
- package/dist/runtime/client.js +639 -1
- package/dist/runtime/client.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,11 @@ var DEFAULT_MASK_HEADERS = [
|
|
|
18
18
|
var DEFAULT_MCP_PATH = "/__mcp";
|
|
19
19
|
var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
|
|
20
20
|
var DEFAULT_SCREENSHOT_SAVE_DIR = ".vite-mcp/screenshot";
|
|
21
|
+
var DEFAULT_PERFORMANCE_SAVE_DIR = ".vite-mcp/performance";
|
|
22
|
+
var DEFAULT_PERFORMANCE_MAX_REPORTS = 100;
|
|
23
|
+
var DEFAULT_PERFORMANCE_MAX_DURATION_MS = 3e4;
|
|
24
|
+
var DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS = 250;
|
|
25
|
+
var DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS = 50;
|
|
21
26
|
var MCP_TOOL_NAMES = {
|
|
22
27
|
listPages: "list_pages",
|
|
23
28
|
reloadPage: "reload_page",
|
|
@@ -30,6 +35,11 @@ var MCP_TOOL_NAMES = {
|
|
|
30
35
|
getNetworkRequests: "get_network_requests",
|
|
31
36
|
getNetworkRequestDetail: "get_network_request_detail",
|
|
32
37
|
clearNetworkRequests: "clear_network_requests",
|
|
38
|
+
recordPerformance: "record_performance",
|
|
39
|
+
startPerformanceRecording: "start_performance_recording",
|
|
40
|
+
stopPerformanceRecording: "stop_performance_recording",
|
|
41
|
+
getPerformanceReport: "get_performance_report",
|
|
42
|
+
takeHeapSnapshot: "take_heap_snapshot",
|
|
33
43
|
getComponentTree: "get_component_tree",
|
|
34
44
|
getComponentState: "get_component_state",
|
|
35
45
|
editComponentState: "edit_component_state",
|
|
@@ -98,6 +108,19 @@ var DEFAULT_OPTIONS = {
|
|
|
98
108
|
options: {},
|
|
99
109
|
plugins: []
|
|
100
110
|
}
|
|
111
|
+
},
|
|
112
|
+
performance: {
|
|
113
|
+
mode: "auto",
|
|
114
|
+
maxDurationMs: DEFAULT_PERFORMANCE_MAX_DURATION_MS,
|
|
115
|
+
sampleIntervalMs: DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS,
|
|
116
|
+
longTaskThresholdMs: DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS,
|
|
117
|
+
saveDir: DEFAULT_PERFORMANCE_SAVE_DIR,
|
|
118
|
+
memory: {
|
|
119
|
+
enabled: true
|
|
120
|
+
},
|
|
121
|
+
stacks: {
|
|
122
|
+
enabled: true
|
|
123
|
+
}
|
|
101
124
|
}
|
|
102
125
|
};
|
|
103
126
|
function mergeMcpClientOptions(cursorConfig, mcpClients) {
|
|
@@ -167,6 +190,18 @@ function mergeOptions(options = {}) {
|
|
|
167
190
|
...DEFAULT_OPTIONS.screenshot.snapdom.plugins
|
|
168
191
|
]
|
|
169
192
|
}
|
|
193
|
+
},
|
|
194
|
+
performance: {
|
|
195
|
+
...DEFAULT_OPTIONS.performance,
|
|
196
|
+
...options.performance,
|
|
197
|
+
memory: {
|
|
198
|
+
...DEFAULT_OPTIONS.performance.memory,
|
|
199
|
+
...options.performance?.memory
|
|
200
|
+
},
|
|
201
|
+
stacks: {
|
|
202
|
+
...DEFAULT_OPTIONS.performance.stacks,
|
|
203
|
+
...options.performance?.stacks
|
|
204
|
+
}
|
|
170
205
|
}
|
|
171
206
|
};
|
|
172
207
|
}
|
|
@@ -194,24 +229,64 @@ function createRingBuffer(capacity) {
|
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
// src/context.ts
|
|
232
|
+
var MINUTE_MS = 60 * 1e3;
|
|
233
|
+
var DISCONNECTED_RUNTIME_TARGET_RETENTION_MS = 5 * MINUTE_MS;
|
|
234
|
+
function shouldPruneDisconnectedRuntimeTarget(target, now) {
|
|
235
|
+
return target.source === "runtime" && !target.connected && typeof target.disconnectedAt === "number" && now - target.disconnectedAt > DISCONNECTED_RUNTIME_TARGET_RETENTION_MS;
|
|
236
|
+
}
|
|
237
|
+
function pruneDisconnectedRuntimeTargets(targets, now) {
|
|
238
|
+
for (const [pageId, target] of targets) {
|
|
239
|
+
if (shouldPruneDisconnectedRuntimeTarget(target, now)) {
|
|
240
|
+
targets.delete(pageId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function markTargetDisconnected(target, now) {
|
|
245
|
+
return {
|
|
246
|
+
...target,
|
|
247
|
+
connected: false,
|
|
248
|
+
disconnectedAt: target.disconnectedAt ?? now
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function shouldListPageTarget(target, options) {
|
|
252
|
+
return options.includeDisconnected === true || target.source !== "runtime" || target.connected;
|
|
253
|
+
}
|
|
254
|
+
function disconnectPreviousRuntimeClientTarget(targets, target, now) {
|
|
255
|
+
if (target.source !== "runtime" || !target.runtimeClientId) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
for (const [pageId, currentTarget] of targets) {
|
|
259
|
+
if (pageId === target.pageId || currentTarget.source !== "runtime" || currentTarget.runtimeClientId !== target.runtimeClientId || !currentTarget.connected) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
targets.set(pageId, markTargetDisconnected(currentTarget, now));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
197
265
|
function createPageTargetRegistry() {
|
|
198
266
|
const targets = /* @__PURE__ */ new Map();
|
|
199
267
|
return {
|
|
200
|
-
upsert(target) {
|
|
268
|
+
upsert(target, now = Date.now()) {
|
|
269
|
+
pruneDisconnectedRuntimeTargets(targets, now);
|
|
270
|
+
disconnectPreviousRuntimeClientTarget(targets, target, now);
|
|
201
271
|
targets.set(target.pageId, target);
|
|
202
272
|
},
|
|
203
273
|
get(pageId) {
|
|
204
274
|
return targets.get(pageId);
|
|
205
275
|
},
|
|
206
|
-
list() {
|
|
207
|
-
|
|
276
|
+
list(options = {}) {
|
|
277
|
+
const now = options.now ?? Date.now();
|
|
278
|
+
pruneDisconnectedRuntimeTargets(targets, now);
|
|
279
|
+
return [...targets.values()].filter(
|
|
280
|
+
(target) => shouldListPageTarget(target, options)
|
|
281
|
+
);
|
|
208
282
|
},
|
|
209
|
-
disconnect(pageId) {
|
|
283
|
+
disconnect(pageId, now = Date.now()) {
|
|
284
|
+
pruneDisconnectedRuntimeTargets(targets, now);
|
|
210
285
|
const target = targets.get(pageId);
|
|
211
286
|
if (!target) {
|
|
212
287
|
return;
|
|
213
288
|
}
|
|
214
|
-
targets.set(pageId,
|
|
289
|
+
targets.set(pageId, markTargetDisconnected(target, now));
|
|
215
290
|
}
|
|
216
291
|
};
|
|
217
292
|
}
|
|
@@ -222,7 +297,11 @@ function createVueMcpNextContext(options) {
|
|
|
222
297
|
rpcServer: void 0,
|
|
223
298
|
pages: createPageTargetRegistry(),
|
|
224
299
|
consoleRecords: createRingBuffer(options.console.maxRecords),
|
|
225
|
-
networkRecords: createRingBuffer(options.network.maxRecords)
|
|
300
|
+
networkRecords: createRingBuffer(options.network.maxRecords),
|
|
301
|
+
performanceReports: createRingBuffer(
|
|
302
|
+
DEFAULT_PERFORMANCE_MAX_REPORTS
|
|
303
|
+
),
|
|
304
|
+
performanceSessions: /* @__PURE__ */ new Map()
|
|
226
305
|
};
|
|
227
306
|
}
|
|
228
307
|
|
|
@@ -340,13 +419,13 @@ async function requestRuntimeData(ctx, trigger) {
|
|
|
340
419
|
return { ok: false, error: "runtime bridge is not connected" };
|
|
341
420
|
}
|
|
342
421
|
const event = nanoid();
|
|
343
|
-
return new Promise((
|
|
422
|
+
return new Promise((resolve2) => {
|
|
344
423
|
const timeout = setTimeout(() => {
|
|
345
|
-
|
|
424
|
+
resolve2({ ok: false, error: "runtime bridge response timed out" });
|
|
346
425
|
}, 5e3);
|
|
347
426
|
ctx.hooks.hookOnce(event, (data) => {
|
|
348
427
|
clearTimeout(timeout);
|
|
349
|
-
|
|
428
|
+
resolve2(data);
|
|
350
429
|
});
|
|
351
430
|
trigger(event);
|
|
352
431
|
});
|
|
@@ -625,9 +704,595 @@ function registerNetworkTools(server, ctx) {
|
|
|
625
704
|
);
|
|
626
705
|
}
|
|
627
706
|
|
|
628
|
-
// src/mcp/tools/
|
|
707
|
+
// src/mcp/tools/performance.ts
|
|
629
708
|
import { z as z5 } from "zod";
|
|
630
709
|
|
|
710
|
+
// src/performance/summary.ts
|
|
711
|
+
function buildPerformanceSummary(input) {
|
|
712
|
+
const memory = buildMemorySummary(input.memorySamples);
|
|
713
|
+
const blockedTimeMs = input.longTasks.reduce((total, task) => {
|
|
714
|
+
return total + Math.max(0, task.durationMs - DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS);
|
|
715
|
+
}, 0);
|
|
716
|
+
const longTaskCount = input.longTasks.length;
|
|
717
|
+
const durations = input.longTasks.map((task) => task.durationMs);
|
|
718
|
+
const maxTaskDurationMs = durations.length ? Math.max(...durations) : 0;
|
|
719
|
+
const averageTaskDurationMs = durations.length ? Math.round(
|
|
720
|
+
durations.reduce((total, duration) => total + duration, 0) / durations.length
|
|
721
|
+
) : void 0;
|
|
722
|
+
const suspectedJank = blockedTimeMs > 0 || memory.trend === "growing" || longTaskCount > 0;
|
|
723
|
+
const severity = resolveSeverity({
|
|
724
|
+
blockedTimeMs,
|
|
725
|
+
longTaskCount,
|
|
726
|
+
memoryTrend: memory.trend
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
blockedTimeMs,
|
|
730
|
+
longTaskCount,
|
|
731
|
+
maxTaskDurationMs,
|
|
732
|
+
averageTaskDurationMs,
|
|
733
|
+
suspectedJank,
|
|
734
|
+
severity
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function buildMemorySummary(samples) {
|
|
738
|
+
const first = samples[0]?.usedJSHeapSize;
|
|
739
|
+
const last = samples.at(-1)?.usedJSHeapSize;
|
|
740
|
+
const peak = samples.reduce((currentPeak, sample) => {
|
|
741
|
+
if (typeof sample.usedJSHeapSize !== "number") {
|
|
742
|
+
return currentPeak;
|
|
743
|
+
}
|
|
744
|
+
return Math.max(currentPeak, sample.usedJSHeapSize);
|
|
745
|
+
}, 0);
|
|
746
|
+
const trend = resolveMemoryTrend(first, last);
|
|
747
|
+
return {
|
|
748
|
+
samples,
|
|
749
|
+
initialUsedJSHeapSize: first,
|
|
750
|
+
finalUsedJSHeapSize: last,
|
|
751
|
+
peakUsedJSHeapSize: peak || void 0,
|
|
752
|
+
deltaUsedJSHeapSize: typeof first === "number" && typeof last === "number" ? last - first : void 0,
|
|
753
|
+
trend
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function buildStackSummary(frames, options = {}) {
|
|
757
|
+
return {
|
|
758
|
+
topFrames: [...frames].sort(sortByHotness).slice(0, 10),
|
|
759
|
+
rawProfilePath: options.rawProfilePath,
|
|
760
|
+
limitation: options.limitation
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function resolveSeverity(input) {
|
|
764
|
+
if (input.blockedTimeMs >= 1e3 || input.longTaskCount >= 10) {
|
|
765
|
+
return "critical";
|
|
766
|
+
}
|
|
767
|
+
if (input.blockedTimeMs > 0 || input.memoryTrend === "growing") {
|
|
768
|
+
return "warning";
|
|
769
|
+
}
|
|
770
|
+
return "ok";
|
|
771
|
+
}
|
|
772
|
+
function resolveMemoryTrend(first, last) {
|
|
773
|
+
if (typeof first !== "number" || typeof last !== "number") {
|
|
774
|
+
return "unknown";
|
|
775
|
+
}
|
|
776
|
+
if (last > first) {
|
|
777
|
+
return "growing";
|
|
778
|
+
}
|
|
779
|
+
return "stable";
|
|
780
|
+
}
|
|
781
|
+
function sortByHotness(left, right) {
|
|
782
|
+
return compareNumber(right.totalTimeMs, left.totalTimeMs) || compareNumber(right.selfTimeMs, left.selfTimeMs) || compareNumber(right.hitCount, left.hitCount);
|
|
783
|
+
}
|
|
784
|
+
function compareNumber(left, right) {
|
|
785
|
+
return (left ?? -1) - (right ?? -1);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/performance/output.ts
|
|
789
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
790
|
+
import { relative, resolve } from "path";
|
|
791
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
792
|
+
async function writePerformanceArtifact(options) {
|
|
793
|
+
const root = resolve(options.root ?? process.cwd());
|
|
794
|
+
const dir = resolveOutputDirectory(root, options.saveDir);
|
|
795
|
+
const path8 = resolve(dir, createSafeFileName(options.fileName));
|
|
796
|
+
const data = typeof options.data === "string" ? Buffer.from(options.data) : options.data;
|
|
797
|
+
await mkdir(dir, { recursive: true });
|
|
798
|
+
await writeFile(path8, data);
|
|
799
|
+
return {
|
|
800
|
+
kind: options.kind,
|
|
801
|
+
path: path8,
|
|
802
|
+
relativePath: relative(root, path8),
|
|
803
|
+
byteLength: data.byteLength,
|
|
804
|
+
source: "cdp"
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function resolveOutputDirectory(root, saveDir) {
|
|
808
|
+
return resolve(root, saveDir);
|
|
809
|
+
}
|
|
810
|
+
function createSafeFileName(fileName) {
|
|
811
|
+
const trimmed = fileName.trim();
|
|
812
|
+
if (!trimmed) {
|
|
813
|
+
return `${String(Date.now())}-${nanoid2()}`;
|
|
814
|
+
}
|
|
815
|
+
const suffix = nanoid2(6);
|
|
816
|
+
const dotIndex = trimmed.lastIndexOf(".");
|
|
817
|
+
if (dotIndex === -1) {
|
|
818
|
+
return `${trimmed}-${suffix}`;
|
|
819
|
+
}
|
|
820
|
+
const base = trimmed.slice(0, dotIndex);
|
|
821
|
+
const extension = trimmed.slice(dotIndex);
|
|
822
|
+
return `${base}-${suffix}${extension}`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/cdp/cdpPerformance.ts
|
|
826
|
+
async function recordCdpPerformance(options) {
|
|
827
|
+
const session = await startCdpPerformanceRecording(options);
|
|
828
|
+
await waitForDuration(options.durationMs);
|
|
829
|
+
return stopCdpPerformanceRecording({
|
|
830
|
+
client: options.client,
|
|
831
|
+
session
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async function startCdpPerformanceRecording(options) {
|
|
835
|
+
await options.client.Performance.enable();
|
|
836
|
+
await options.client.Profiler.enable();
|
|
837
|
+
await options.client.Profiler.start();
|
|
838
|
+
return {
|
|
839
|
+
recordingId: createRecordingId(options.pageId),
|
|
840
|
+
pageId: options.pageId,
|
|
841
|
+
startedAt: Date.now(),
|
|
842
|
+
includeMemory: options.includeMemory,
|
|
843
|
+
includeStacks: options.includeStacks,
|
|
844
|
+
saveDir: options.saveDir
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async function stopCdpPerformanceRecording(options) {
|
|
848
|
+
const endedAt = Date.now();
|
|
849
|
+
const [metricsResult, profileResult] = await Promise.all([
|
|
850
|
+
options.client.Performance.getMetrics(),
|
|
851
|
+
options.client.Profiler.stop()
|
|
852
|
+
]);
|
|
853
|
+
const metrics = toMetricMap(metricsResult.metrics);
|
|
854
|
+
const longTasks = toLongTaskRecords(metrics);
|
|
855
|
+
const memorySamples = options.session.includeMemory ? [toMemorySample(metrics)] : [];
|
|
856
|
+
const memory = options.session.includeMemory ? buildMemorySummary(memorySamples) : void 0;
|
|
857
|
+
const stacks = options.session.includeStacks ? buildStackSummary(aggregateCpuProfile(profileResult.profile)) : void 0;
|
|
858
|
+
const artifact = await writeCpuProfileArtifact(
|
|
859
|
+
{
|
|
860
|
+
pageId: options.session.pageId,
|
|
861
|
+
saveDir: options.session.saveDir
|
|
862
|
+
},
|
|
863
|
+
profileResult.profile
|
|
864
|
+
);
|
|
865
|
+
const report = {
|
|
866
|
+
recordingId: options.session.recordingId,
|
|
867
|
+
pageId: options.session.pageId,
|
|
868
|
+
source: "cdp",
|
|
869
|
+
startedAt: options.session.startedAt,
|
|
870
|
+
endedAt,
|
|
871
|
+
durationMs: endedAt - options.session.startedAt,
|
|
872
|
+
summary: buildPerformanceSummary({
|
|
873
|
+
longTasks,
|
|
874
|
+
memorySamples
|
|
875
|
+
}),
|
|
876
|
+
longTasks,
|
|
877
|
+
memory,
|
|
878
|
+
stacks,
|
|
879
|
+
artifacts: [artifact],
|
|
880
|
+
limitations: [
|
|
881
|
+
"CDP path returns sampled CPU profile data, not a full instruction trace"
|
|
882
|
+
]
|
|
883
|
+
};
|
|
884
|
+
return report;
|
|
885
|
+
}
|
|
886
|
+
async function takeHeapSnapshot(options) {
|
|
887
|
+
const chunks = [];
|
|
888
|
+
await options.client.HeapProfiler.enable();
|
|
889
|
+
options.client.HeapProfiler.addHeapSnapshotChunk((event) => {
|
|
890
|
+
const payload = event;
|
|
891
|
+
if (payload.chunk) {
|
|
892
|
+
chunks.push(payload.chunk);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
await options.client.HeapProfiler.takeHeapSnapshot({
|
|
896
|
+
reportProgress: false
|
|
897
|
+
});
|
|
898
|
+
const artifact = await writePerformanceArtifact({
|
|
899
|
+
root: process.cwd(),
|
|
900
|
+
saveDir: options.saveDir,
|
|
901
|
+
fileName: `${options.pageId}-heap-snapshot.heapsnapshot`,
|
|
902
|
+
kind: "heap-snapshot",
|
|
903
|
+
data: Buffer.from(chunks.join(""))
|
|
904
|
+
});
|
|
905
|
+
return artifact;
|
|
906
|
+
}
|
|
907
|
+
function createRecordingId(pageId) {
|
|
908
|
+
return `cdp-${pageId}-${String(Date.now())}`;
|
|
909
|
+
}
|
|
910
|
+
function waitForDuration(durationMs) {
|
|
911
|
+
return new Promise((resolve2) => {
|
|
912
|
+
setTimeout(() => {
|
|
913
|
+
resolve2();
|
|
914
|
+
}, durationMs);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
function toMetricMap(metrics) {
|
|
918
|
+
return new Map(metrics.map((metric) => [metric.name, metric.value]));
|
|
919
|
+
}
|
|
920
|
+
function toLongTaskRecords(metrics) {
|
|
921
|
+
const taskDuration = metrics.get("TaskDuration") ?? 0;
|
|
922
|
+
if (taskDuration <= 0) {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
return [
|
|
926
|
+
{
|
|
927
|
+
startTime: 0,
|
|
928
|
+
durationMs: taskDuration,
|
|
929
|
+
name: "TaskDuration",
|
|
930
|
+
source: "cpu-profile"
|
|
931
|
+
}
|
|
932
|
+
];
|
|
933
|
+
}
|
|
934
|
+
function toMemorySample(metrics) {
|
|
935
|
+
return {
|
|
936
|
+
timestamp: Date.now(),
|
|
937
|
+
usedJSHeapSize: metrics.get("JSHeapUsedSize"),
|
|
938
|
+
totalJSHeapSize: metrics.get("JSHeapTotalSize"),
|
|
939
|
+
jsHeapSizeLimit: metrics.get("JSHeapSizeLimit")
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function aggregateCpuProfile(profile) {
|
|
943
|
+
const nodes = profile.nodes ?? [];
|
|
944
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
945
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
946
|
+
nodes.forEach((node) => nodeIndex.set(node.id, node));
|
|
947
|
+
(profile.samples ?? []).forEach((nodeId, index) => {
|
|
948
|
+
const node = nodeIndex.get(nodeId);
|
|
949
|
+
if (!node) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const delta = profile.timeDeltas?.[index] ?? 0;
|
|
953
|
+
const current = nodeMap.get(nodeId) ?? {
|
|
954
|
+
functionName: node.callFrame.functionName || "<anonymous>",
|
|
955
|
+
url: node.callFrame.url,
|
|
956
|
+
lineNumber: node.callFrame.lineNumber,
|
|
957
|
+
columnNumber: node.callFrame.columnNumber,
|
|
958
|
+
selfTimeMs: 0,
|
|
959
|
+
totalTimeMs: 0,
|
|
960
|
+
hitCount: 0
|
|
961
|
+
};
|
|
962
|
+
nodeMap.set(nodeId, {
|
|
963
|
+
...current,
|
|
964
|
+
selfTimeMs: current.selfTimeMs + delta,
|
|
965
|
+
totalTimeMs: current.totalTimeMs + delta,
|
|
966
|
+
hitCount: current.hitCount + 1
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
return [...nodeMap.values()];
|
|
970
|
+
}
|
|
971
|
+
async function writeCpuProfileArtifact(options, profile) {
|
|
972
|
+
return writePerformanceArtifact({
|
|
973
|
+
root: process.cwd(),
|
|
974
|
+
saveDir: options.saveDir ?? ".vite-mcp/performance",
|
|
975
|
+
fileName: `${options.pageId}-cpu-profile.cpuprofile`,
|
|
976
|
+
kind: "cpu-profile",
|
|
977
|
+
data: Buffer.from(JSON.stringify(profile, null, 2))
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/mcp/tools/performance.ts
|
|
982
|
+
var PERFORMANCE_NOT_AVAILABLE_ERROR = "Performance diagnostics are disabled by configuration";
|
|
983
|
+
var activeCdpPerformanceClients = /* @__PURE__ */ new Map();
|
|
984
|
+
function registerPerformanceTools(server, ctx) {
|
|
985
|
+
server.registerTool(
|
|
986
|
+
MCP_TOOL_NAMES.recordPerformance,
|
|
987
|
+
{
|
|
988
|
+
description: "Record a performance sample for the selected page.",
|
|
989
|
+
inputSchema: {
|
|
990
|
+
pageId: z5.string().optional(),
|
|
991
|
+
durationMs: z5.number().optional(),
|
|
992
|
+
includeMemory: z5.boolean().optional(),
|
|
993
|
+
includeStacks: z5.boolean().optional()
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
async (input) => handleRecordPerformance(ctx, input)
|
|
997
|
+
);
|
|
998
|
+
server.registerTool(
|
|
999
|
+
MCP_TOOL_NAMES.startPerformanceRecording,
|
|
1000
|
+
{
|
|
1001
|
+
description: "Start a performance recording session.",
|
|
1002
|
+
inputSchema: {
|
|
1003
|
+
pageId: z5.string().optional(),
|
|
1004
|
+
includeMemory: z5.boolean().optional(),
|
|
1005
|
+
includeStacks: z5.boolean().optional()
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
async (input) => handleStartPerformanceRecording(ctx, input)
|
|
1009
|
+
);
|
|
1010
|
+
server.registerTool(
|
|
1011
|
+
MCP_TOOL_NAMES.stopPerformanceRecording,
|
|
1012
|
+
{
|
|
1013
|
+
description: "Stop a performance recording session.",
|
|
1014
|
+
inputSchema: {
|
|
1015
|
+
recordingId: z5.string()
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
async (input) => handleStopPerformanceRecording(ctx, input)
|
|
1019
|
+
);
|
|
1020
|
+
server.registerTool(
|
|
1021
|
+
MCP_TOOL_NAMES.getPerformanceReport,
|
|
1022
|
+
{
|
|
1023
|
+
description: "Get cached performance reports and active sessions.",
|
|
1024
|
+
inputSchema: {
|
|
1025
|
+
pageId: z5.string().optional(),
|
|
1026
|
+
recordingId: z5.string().optional(),
|
|
1027
|
+
limit: z5.number().optional()
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
(input) => handleGetPerformanceReport(ctx, input)
|
|
1031
|
+
);
|
|
1032
|
+
server.registerTool(
|
|
1033
|
+
MCP_TOOL_NAMES.takeHeapSnapshot,
|
|
1034
|
+
{
|
|
1035
|
+
description: "Take a heap snapshot with CDP.",
|
|
1036
|
+
inputSchema: {
|
|
1037
|
+
pageId: z5.string().optional()
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
async (input) => handleTakeHeapSnapshot(ctx, input)
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
function appendPerformanceReport(ctx, report) {
|
|
1044
|
+
if (ctx.performanceReports.all().some((item) => item.recordingId === report.recordingId)) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
ctx.performanceReports.push(report);
|
|
1048
|
+
}
|
|
1049
|
+
async function handleRecordPerformance(ctx, input) {
|
|
1050
|
+
const durationMs = input.durationMs ?? ctx.options.performance.maxDurationMs;
|
|
1051
|
+
const includeMemory = input.includeMemory ?? ctx.options.performance.memory.enabled;
|
|
1052
|
+
const includeStacks = input.includeStacks ?? ctx.options.performance.stacks.enabled;
|
|
1053
|
+
if (ctx.options.performance.mode === "off") {
|
|
1054
|
+
return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
|
|
1055
|
+
}
|
|
1056
|
+
if (ctx.options.performance.mode !== "hook") {
|
|
1057
|
+
const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
|
|
1058
|
+
const cdp = cdpResult.cdp;
|
|
1059
|
+
if (cdp) {
|
|
1060
|
+
try {
|
|
1061
|
+
const report = await recordCdpPerformance({
|
|
1062
|
+
client: cdp.client,
|
|
1063
|
+
pageId: input.pageId ?? "cdp",
|
|
1064
|
+
durationMs,
|
|
1065
|
+
includeMemory,
|
|
1066
|
+
includeStacks,
|
|
1067
|
+
saveDir: ctx.options.performance.saveDir
|
|
1068
|
+
});
|
|
1069
|
+
appendPerformanceReport(ctx, report);
|
|
1070
|
+
return createToolResponse(toStructuredRecord(report));
|
|
1071
|
+
} finally {
|
|
1072
|
+
await closeCdpClient(cdp.client);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (ctx.options.performance.mode === "cdp") {
|
|
1076
|
+
return createToolError(
|
|
1077
|
+
formatCdpUnavailableError(
|
|
1078
|
+
"CDP performance collection is unavailable",
|
|
1079
|
+
cdpResult.error
|
|
1080
|
+
)
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
const result = await requestRuntimeData(ctx, (event) => {
|
|
1085
|
+
void ctx.rpcServer?.recordPerformance({
|
|
1086
|
+
event,
|
|
1087
|
+
durationMs,
|
|
1088
|
+
includeMemory,
|
|
1089
|
+
includeStacks
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
if (isPerformanceReport(result)) {
|
|
1093
|
+
appendPerformanceReport(ctx, result);
|
|
1094
|
+
}
|
|
1095
|
+
if (isPlainRecord(result)) {
|
|
1096
|
+
return createToolResponse(toStructuredRecord(result));
|
|
1097
|
+
}
|
|
1098
|
+
return createToolError("runtime bridge returned an invalid response");
|
|
1099
|
+
}
|
|
1100
|
+
async function handleStartPerformanceRecording(ctx, input) {
|
|
1101
|
+
const includeMemory = input.includeMemory ?? ctx.options.performance.memory.enabled;
|
|
1102
|
+
const includeStacks = input.includeStacks ?? ctx.options.performance.stacks.enabled;
|
|
1103
|
+
if (ctx.options.performance.mode === "off") {
|
|
1104
|
+
return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
|
|
1105
|
+
}
|
|
1106
|
+
if (ctx.options.performance.mode !== "hook") {
|
|
1107
|
+
const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
|
|
1108
|
+
const cdp = cdpResult.cdp;
|
|
1109
|
+
if (cdp) {
|
|
1110
|
+
try {
|
|
1111
|
+
const session = await startCdpPerformanceRecording({
|
|
1112
|
+
client: cdp.client,
|
|
1113
|
+
pageId: input.pageId ?? "cdp",
|
|
1114
|
+
includeMemory,
|
|
1115
|
+
includeStacks,
|
|
1116
|
+
saveDir: ctx.options.performance.saveDir
|
|
1117
|
+
});
|
|
1118
|
+
ctx.performanceSessions.set(session.recordingId, {
|
|
1119
|
+
recordingId: session.recordingId,
|
|
1120
|
+
pageId: session.pageId,
|
|
1121
|
+
source: "cdp",
|
|
1122
|
+
startedAt: session.startedAt,
|
|
1123
|
+
includeMemory,
|
|
1124
|
+
includeStacks,
|
|
1125
|
+
mode: ctx.options.performance.mode
|
|
1126
|
+
});
|
|
1127
|
+
activeCdpPerformanceClients.set(session.recordingId, cdp.client);
|
|
1128
|
+
return createToolResponse(
|
|
1129
|
+
toStructuredRecord({
|
|
1130
|
+
...session,
|
|
1131
|
+
source: "cdp"
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
await closeCdpClient(cdp.client);
|
|
1136
|
+
return createToolError(
|
|
1137
|
+
error instanceof Error ? error.message : String(error)
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (ctx.options.performance.mode === "cdp") {
|
|
1142
|
+
return createToolError(
|
|
1143
|
+
formatCdpUnavailableError(
|
|
1144
|
+
"CDP performance collection is unavailable",
|
|
1145
|
+
cdpResult.error
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const result = await requestRuntimeData(ctx, (event) => {
|
|
1151
|
+
void ctx.rpcServer?.startPerformanceRecording({
|
|
1152
|
+
event,
|
|
1153
|
+
includeMemory,
|
|
1154
|
+
includeStacks
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
if (isPerformanceStartResult(result)) {
|
|
1158
|
+
ctx.performanceSessions.set(result.recordingId, {
|
|
1159
|
+
recordingId: result.recordingId,
|
|
1160
|
+
pageId: input.pageId ?? "runtime",
|
|
1161
|
+
source: "hook",
|
|
1162
|
+
startedAt: result.startedAt,
|
|
1163
|
+
includeMemory,
|
|
1164
|
+
includeStacks,
|
|
1165
|
+
mode: ctx.options.performance.mode
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (isPlainRecord(result)) {
|
|
1169
|
+
return createToolResponse(toStructuredRecord(result));
|
|
1170
|
+
}
|
|
1171
|
+
return createToolError("runtime bridge returned an invalid response");
|
|
1172
|
+
}
|
|
1173
|
+
async function handleStopPerformanceRecording(ctx, input) {
|
|
1174
|
+
const session = ctx.performanceSessions.get(input.recordingId);
|
|
1175
|
+
if (!session) {
|
|
1176
|
+
return createToolError(`Performance recording not found: ${input.recordingId}`);
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
if (session.source === "cdp") {
|
|
1180
|
+
const client = activeCdpPerformanceClients.get(input.recordingId);
|
|
1181
|
+
if (!client) {
|
|
1182
|
+
return createToolError(
|
|
1183
|
+
`CDP client not found for recording: ${input.recordingId}`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const report = await stopCdpPerformanceRecording({
|
|
1187
|
+
client,
|
|
1188
|
+
session
|
|
1189
|
+
});
|
|
1190
|
+
appendPerformanceReport(ctx, report);
|
|
1191
|
+
return createToolResponse(toStructuredRecord(report));
|
|
1192
|
+
}
|
|
1193
|
+
const result = await requestRuntimeData(ctx, (event) => {
|
|
1194
|
+
void ctx.rpcServer?.stopPerformanceRecording({
|
|
1195
|
+
event,
|
|
1196
|
+
recordingId: input.recordingId
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
if (isPerformanceReport(result)) {
|
|
1200
|
+
appendPerformanceReport(ctx, result);
|
|
1201
|
+
}
|
|
1202
|
+
if (isPlainRecord(result)) {
|
|
1203
|
+
return createToolResponse(toStructuredRecord(result));
|
|
1204
|
+
}
|
|
1205
|
+
return createToolError("runtime bridge returned an invalid response");
|
|
1206
|
+
} finally {
|
|
1207
|
+
ctx.performanceSessions.delete(input.recordingId);
|
|
1208
|
+
const client = activeCdpPerformanceClients.get(input.recordingId);
|
|
1209
|
+
if (client) {
|
|
1210
|
+
activeCdpPerformanceClients.delete(input.recordingId);
|
|
1211
|
+
await closeCdpClient(client);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function handleGetPerformanceReport(ctx, input) {
|
|
1216
|
+
const reports = ctx.performanceReports.all().filter((report) => !input.pageId || report.pageId === input.pageId).filter(
|
|
1217
|
+
(report) => !input.recordingId || report.recordingId === input.recordingId
|
|
1218
|
+
);
|
|
1219
|
+
const limit = input.limit ?? reports.length;
|
|
1220
|
+
return createToolResponse(toStructuredRecord({
|
|
1221
|
+
report: reports.at(-1) ?? null,
|
|
1222
|
+
reports: reports.slice(-limit),
|
|
1223
|
+
sessions: [...ctx.performanceSessions.values()].filter(
|
|
1224
|
+
(session) => (!input.pageId || session.pageId === input.pageId) && (!input.recordingId || session.recordingId === input.recordingId)
|
|
1225
|
+
)
|
|
1226
|
+
}));
|
|
1227
|
+
}
|
|
1228
|
+
async function handleTakeHeapSnapshot(ctx, input) {
|
|
1229
|
+
if (ctx.options.performance.mode === "off" || ctx.options.performance.mode === "hook") {
|
|
1230
|
+
return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
|
|
1231
|
+
}
|
|
1232
|
+
const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
|
|
1233
|
+
const cdp = cdpResult.cdp;
|
|
1234
|
+
if (!cdp) {
|
|
1235
|
+
return createToolError(
|
|
1236
|
+
formatCdpUnavailableError(
|
|
1237
|
+
"CDP heap snapshot is unavailable",
|
|
1238
|
+
cdpResult.error
|
|
1239
|
+
)
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
return createToolResponse(
|
|
1244
|
+
toStructuredRecord(await takeHeapSnapshot({
|
|
1245
|
+
client: cdp.client,
|
|
1246
|
+
pageId: input.pageId ?? "cdp",
|
|
1247
|
+
saveDir: ctx.options.performance.saveDir
|
|
1248
|
+
}))
|
|
1249
|
+
);
|
|
1250
|
+
} finally {
|
|
1251
|
+
await closeCdpClient(cdp.client);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function connectPerformanceCdp(ctx, pageId) {
|
|
1255
|
+
try {
|
|
1256
|
+
return { cdp: await connectCdpForPage(ctx, pageId) };
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
return {
|
|
1259
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function formatCdpUnavailableError(message, reason) {
|
|
1264
|
+
if (!reason) {
|
|
1265
|
+
return message;
|
|
1266
|
+
}
|
|
1267
|
+
return `${message}: ${reason}`;
|
|
1268
|
+
}
|
|
1269
|
+
function isPerformanceReport(value) {
|
|
1270
|
+
if (!value || typeof value !== "object") {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
const report = value;
|
|
1274
|
+
return typeof report.recordingId === "string" && typeof report.pageId === "string" && (report.source === "hook" || report.source === "cdp") && typeof report.startedAt === "number" && typeof report.endedAt === "number" && typeof report.durationMs === "number" && Boolean(report.summary) && Array.isArray(report.longTasks) && Array.isArray(report.limitations);
|
|
1275
|
+
}
|
|
1276
|
+
function isPerformanceStartResult(value) {
|
|
1277
|
+
if (!value || typeof value !== "object") {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
const result = value;
|
|
1281
|
+
return typeof result.recordingId === "string" && typeof result.startedAt === "number";
|
|
1282
|
+
}
|
|
1283
|
+
function isPlainRecord(value) {
|
|
1284
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1285
|
+
}
|
|
1286
|
+
function toStructuredRecord(value) {
|
|
1287
|
+
if (isPlainRecord(value)) {
|
|
1288
|
+
return value;
|
|
1289
|
+
}
|
|
1290
|
+
return {};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/mcp/tools/pages.ts
|
|
1294
|
+
import { z as z6 } from "zod";
|
|
1295
|
+
|
|
631
1296
|
// src/plugin/entryDiscovery.ts
|
|
632
1297
|
import fs from "fs";
|
|
633
1298
|
import path from "path";
|
|
@@ -651,10 +1316,10 @@ function walkHtmlEntries(root, dir, entries) {
|
|
|
651
1316
|
if (!item.isFile() || !item.name.endsWith(".html")) {
|
|
652
1317
|
continue;
|
|
653
1318
|
}
|
|
654
|
-
const
|
|
1319
|
+
const relative2 = normalizePath(path.relative(root, fullPath));
|
|
655
1320
|
entries.push({
|
|
656
|
-
file:
|
|
657
|
-
pathname:
|
|
1321
|
+
file: relative2,
|
|
1322
|
+
pathname: relative2 === "index.html" ? "/" : `/${relative2}`
|
|
658
1323
|
});
|
|
659
1324
|
}
|
|
660
1325
|
}
|
|
@@ -664,9 +1329,12 @@ function registerPageTools(server, ctx, vite) {
|
|
|
664
1329
|
server.registerTool(
|
|
665
1330
|
MCP_TOOL_NAMES.listPages,
|
|
666
1331
|
{
|
|
667
|
-
description: "List Vite page entries and connected runtime/CDP targets."
|
|
1332
|
+
description: "List Vite page entries and connected runtime/CDP targets.",
|
|
1333
|
+
inputSchema: {
|
|
1334
|
+
includeDisconnected: z6.boolean().optional()
|
|
1335
|
+
}
|
|
668
1336
|
},
|
|
669
|
-
async () => {
|
|
1337
|
+
async (input) => {
|
|
670
1338
|
const cdpResult = await listCdpPageTargets(ctx);
|
|
671
1339
|
for (const target of cdpResult.pages) {
|
|
672
1340
|
ctx.pages.upsert(target);
|
|
@@ -674,7 +1342,9 @@ function registerPageTools(server, ctx, vite) {
|
|
|
674
1342
|
}
|
|
675
1343
|
return createToolResponse({
|
|
676
1344
|
entries: discoverHtmlEntries(vite),
|
|
677
|
-
pages: ctx.pages.list(
|
|
1345
|
+
pages: ctx.pages.list({
|
|
1346
|
+
includeDisconnected: input.includeDisconnected
|
|
1347
|
+
}),
|
|
678
1348
|
cdpError: cdpResult.error
|
|
679
1349
|
});
|
|
680
1350
|
}
|
|
@@ -684,8 +1354,8 @@ function registerPageTools(server, ctx, vite) {
|
|
|
684
1354
|
{
|
|
685
1355
|
description: "Reload the selected page. CDP uses ignoreCache; Runtime Hook falls back to normal reload.",
|
|
686
1356
|
inputSchema: {
|
|
687
|
-
pageId:
|
|
688
|
-
ignoreCache:
|
|
1357
|
+
pageId: z6.string().optional(),
|
|
1358
|
+
ignoreCache: z6.boolean().optional()
|
|
689
1359
|
}
|
|
690
1360
|
},
|
|
691
1361
|
async (input) => {
|
|
@@ -744,16 +1414,16 @@ function resolveRuntimeReloadTarget(ctx, pageId) {
|
|
|
744
1414
|
function waitForRuntimePageReconnect(ctx) {
|
|
745
1415
|
let timeout;
|
|
746
1416
|
let cleanup;
|
|
747
|
-
const promise = new Promise((
|
|
1417
|
+
const promise = new Promise((resolve2) => {
|
|
748
1418
|
timeout = setTimeout(() => {
|
|
749
1419
|
cleanup?.();
|
|
750
|
-
|
|
1420
|
+
resolve2(null);
|
|
751
1421
|
}, 5e3);
|
|
752
1422
|
cleanup = ctx.hooks.hookOnce(RUNTIME_PAGE_RECONNECTED_EVENT, (payload) => {
|
|
753
1423
|
if (timeout) {
|
|
754
1424
|
clearTimeout(timeout);
|
|
755
1425
|
}
|
|
756
|
-
|
|
1426
|
+
resolve2(isPageTarget(payload) ? payload : null);
|
|
757
1427
|
});
|
|
758
1428
|
});
|
|
759
1429
|
return {
|
|
@@ -837,7 +1507,7 @@ function getPathname(url) {
|
|
|
837
1507
|
}
|
|
838
1508
|
|
|
839
1509
|
// src/mcp/tools/screenshot.ts
|
|
840
|
-
import { z as
|
|
1510
|
+
import { z as z7 } from "zod";
|
|
841
1511
|
|
|
842
1512
|
// src/cdp/cdpScreenshot.ts
|
|
843
1513
|
async function cdpCaptureScreenshot(options) {
|
|
@@ -943,16 +1613,16 @@ function isElementRect(value) {
|
|
|
943
1613
|
|
|
944
1614
|
// src/mcp/tools/screenshotOutput.ts
|
|
945
1615
|
import { randomUUID } from "crypto";
|
|
946
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
1616
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
947
1617
|
import path2 from "path";
|
|
948
1618
|
async function createScreenshotOutput(ctx, payload) {
|
|
949
1619
|
if (ctx.options.screenshot.type === "base64") {
|
|
950
1620
|
return payload;
|
|
951
1621
|
}
|
|
952
1622
|
const saveDir = resolveScreenshotSaveDir(ctx);
|
|
953
|
-
await
|
|
1623
|
+
await mkdir2(saveDir, { recursive: true });
|
|
954
1624
|
const filePath = path2.join(saveDir, createScreenshotFileName(payload));
|
|
955
|
-
await
|
|
1625
|
+
await writeFile2(filePath, Buffer.from(payload.data, "base64"));
|
|
956
1626
|
return {
|
|
957
1627
|
source: payload.source,
|
|
958
1628
|
target: payload.target,
|
|
@@ -997,14 +1667,14 @@ function createProjectRelativePath(ctx, filePath) {
|
|
|
997
1667
|
var DEFAULT_SCREENSHOT_TARGET = "viewport";
|
|
998
1668
|
var DEFAULT_SCREENSHOT_FORMAT = "png";
|
|
999
1669
|
var screenshotInputSchema = {
|
|
1000
|
-
pageId:
|
|
1001
|
-
target:
|
|
1002
|
-
selector:
|
|
1003
|
-
format:
|
|
1004
|
-
prefer:
|
|
1005
|
-
quality:
|
|
1006
|
-
scale:
|
|
1007
|
-
snapdom:
|
|
1670
|
+
pageId: z7.string().optional(),
|
|
1671
|
+
target: z7.enum(["viewport", "fullPage", "element"]).optional(),
|
|
1672
|
+
selector: z7.string().optional(),
|
|
1673
|
+
format: z7.enum(["png", "jpeg", "webp"]).optional(),
|
|
1674
|
+
prefer: z7.enum(["auto", "cdp", "runtime"]).optional(),
|
|
1675
|
+
quality: z7.number().optional(),
|
|
1676
|
+
scale: z7.number().optional(),
|
|
1677
|
+
snapdom: z7.record(z7.string(), z7.unknown()).optional()
|
|
1008
1678
|
};
|
|
1009
1679
|
function registerScreenshotTools(server, ctx) {
|
|
1010
1680
|
server.registerTool(
|
|
@@ -1077,7 +1747,7 @@ async function createRuntimeScreenshot(ctx, input, normalized) {
|
|
|
1077
1747
|
`screenshot is too large: ${String(result.byteLength)} bytes`
|
|
1078
1748
|
);
|
|
1079
1749
|
}
|
|
1080
|
-
if (!
|
|
1750
|
+
if (!isPlainRecord2(result)) {
|
|
1081
1751
|
return createToolError("runtime screenshot returned an invalid response");
|
|
1082
1752
|
}
|
|
1083
1753
|
if (result.ok === false) {
|
|
@@ -1106,9 +1776,9 @@ async function createScreenshotResponse(ctx, result) {
|
|
|
1106
1776
|
}
|
|
1107
1777
|
}
|
|
1108
1778
|
function isScreenshotTooLarge(ctx, result) {
|
|
1109
|
-
return
|
|
1779
|
+
return isPlainRecord2(result) && "byteLength" in result && typeof result.byteLength === "number" && result.byteLength > ctx.options.screenshot.maxBytes;
|
|
1110
1780
|
}
|
|
1111
|
-
function
|
|
1781
|
+
function isPlainRecord2(value) {
|
|
1112
1782
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1113
1783
|
}
|
|
1114
1784
|
function createMimeType(format) {
|
|
@@ -1123,8 +1793,8 @@ function isScreenshotImagePayload(result) {
|
|
|
1123
1793
|
}
|
|
1124
1794
|
|
|
1125
1795
|
// src/mcp/tools/vue.ts
|
|
1126
|
-
import { nanoid as
|
|
1127
|
-
import { z as
|
|
1796
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
1797
|
+
import { z as z8 } from "zod";
|
|
1128
1798
|
function registerVueTools(server, ctx) {
|
|
1129
1799
|
server.registerTool(
|
|
1130
1800
|
MCP_TOOL_NAMES.getComponentTree,
|
|
@@ -1137,7 +1807,7 @@ function registerVueTools(server, ctx) {
|
|
|
1137
1807
|
MCP_TOOL_NAMES.getComponentState,
|
|
1138
1808
|
{
|
|
1139
1809
|
description: "Get Vue component state.",
|
|
1140
|
-
inputSchema: { componentName:
|
|
1810
|
+
inputSchema: { componentName: z8.string() }
|
|
1141
1811
|
},
|
|
1142
1812
|
async ({ componentName }) => requestVueData(ctx, (event) => {
|
|
1143
1813
|
void ctx.rpcServer?.getInspectorState({ event, componentName });
|
|
@@ -1148,10 +1818,10 @@ function registerVueTools(server, ctx) {
|
|
|
1148
1818
|
{
|
|
1149
1819
|
description: "Edit Vue component state.",
|
|
1150
1820
|
inputSchema: {
|
|
1151
|
-
componentName:
|
|
1152
|
-
path:
|
|
1153
|
-
value:
|
|
1154
|
-
valueType:
|
|
1821
|
+
componentName: z8.string(),
|
|
1822
|
+
path: z8.array(z8.string()),
|
|
1823
|
+
value: z8.string(),
|
|
1824
|
+
valueType: z8.enum(["string", "number", "boolean", "object", "array"])
|
|
1155
1825
|
}
|
|
1156
1826
|
},
|
|
1157
1827
|
({ componentName, path: path8, value, valueType }) => {
|
|
@@ -1171,7 +1841,7 @@ function registerVueTools(server, ctx) {
|
|
|
1171
1841
|
MCP_TOOL_NAMES.highlightComponent,
|
|
1172
1842
|
{
|
|
1173
1843
|
description: "Highlight a Vue component.",
|
|
1174
|
-
inputSchema: { componentName:
|
|
1844
|
+
inputSchema: { componentName: z8.string() }
|
|
1175
1845
|
},
|
|
1176
1846
|
({ componentName }) => {
|
|
1177
1847
|
if (!ctx.rpcServer) {
|
|
@@ -1199,7 +1869,7 @@ function registerVueTools(server, ctx) {
|
|
|
1199
1869
|
MCP_TOOL_NAMES.getPiniaState,
|
|
1200
1870
|
{
|
|
1201
1871
|
description: "Get Pinia store state.",
|
|
1202
|
-
inputSchema: { storeName:
|
|
1872
|
+
inputSchema: { storeName: z8.string() }
|
|
1203
1873
|
},
|
|
1204
1874
|
async ({ storeName }) => requestVueData(ctx, (event) => {
|
|
1205
1875
|
void ctx.rpcServer?.getPiniaState({ event, storeName });
|
|
@@ -1210,7 +1880,7 @@ async function requestVueData(ctx, trigger) {
|
|
|
1210
1880
|
if (!ctx.rpcServer) {
|
|
1211
1881
|
return vueBridgeUnavailable();
|
|
1212
1882
|
}
|
|
1213
|
-
const event =
|
|
1883
|
+
const event = nanoid3();
|
|
1214
1884
|
const data = await waitForVueHook(ctx, event, () => {
|
|
1215
1885
|
trigger(event);
|
|
1216
1886
|
});
|
|
@@ -1219,13 +1889,13 @@ async function requestVueData(ctx, trigger) {
|
|
|
1219
1889
|
};
|
|
1220
1890
|
}
|
|
1221
1891
|
function waitForVueHook(ctx, event, trigger) {
|
|
1222
|
-
return new Promise((
|
|
1892
|
+
return new Promise((resolve2) => {
|
|
1223
1893
|
const timeout = setTimeout(() => {
|
|
1224
|
-
|
|
1894
|
+
resolve2({ ok: false, error: "Vue runtime bridge response timed out" });
|
|
1225
1895
|
}, 5e3);
|
|
1226
1896
|
ctx.hooks.hookOnce(event, (data) => {
|
|
1227
1897
|
clearTimeout(timeout);
|
|
1228
|
-
|
|
1898
|
+
resolve2(data);
|
|
1229
1899
|
});
|
|
1230
1900
|
trigger();
|
|
1231
1901
|
});
|
|
@@ -1246,6 +1916,7 @@ function createMcpServer(ctx, vite) {
|
|
|
1246
1916
|
registerConsoleTools(server, ctx);
|
|
1247
1917
|
registerEvaluateTools(server, ctx);
|
|
1248
1918
|
registerNetworkTools(server, ctx);
|
|
1919
|
+
registerPerformanceTools(server, ctx);
|
|
1249
1920
|
registerVueTools(server, ctx);
|
|
1250
1921
|
return server;
|
|
1251
1922
|
}
|
|
@@ -1333,6 +2004,18 @@ function createServerVueRuntimeRpc(ctx) {
|
|
|
1333
2004
|
onScreenshotTaken: (event, data) => {
|
|
1334
2005
|
void ctx.hooks.callHook(event, data);
|
|
1335
2006
|
},
|
|
2007
|
+
recordPerformance: () => void 0,
|
|
2008
|
+
onPerformanceRecorded: (event, data) => {
|
|
2009
|
+
void ctx.hooks.callHook(event, data);
|
|
2010
|
+
},
|
|
2011
|
+
startPerformanceRecording: () => void 0,
|
|
2012
|
+
onPerformanceRecordingStarted: (event, data) => {
|
|
2013
|
+
void ctx.hooks.callHook(event, data);
|
|
2014
|
+
},
|
|
2015
|
+
stopPerformanceRecording: () => void 0,
|
|
2016
|
+
onPerformanceRecordingStopped: (event, data) => {
|
|
2017
|
+
void ctx.hooks.callHook(event, data);
|
|
2018
|
+
},
|
|
1336
2019
|
getInspectorTree: () => void 0,
|
|
1337
2020
|
onInspectorTreeUpdated: (event, data) => {
|
|
1338
2021
|
void ctx.hooks.callHook(event, data);
|
|
@@ -1359,7 +2042,7 @@ function createServerVueRuntimeRpc(ctx) {
|
|
|
1359
2042
|
}
|
|
1360
2043
|
|
|
1361
2044
|
// src/cdp/cdpConsole.ts
|
|
1362
|
-
import { nanoid as
|
|
2045
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
1363
2046
|
|
|
1364
2047
|
// src/shared/serialization.ts
|
|
1365
2048
|
function safeStringify(value) {
|
|
@@ -1388,7 +2071,7 @@ async function startCdpConsole(options) {
|
|
|
1388
2071
|
await options.client.Runtime.enable();
|
|
1389
2072
|
options.client.Runtime.consoleAPICalled((event) => {
|
|
1390
2073
|
options.push({
|
|
1391
|
-
id:
|
|
2074
|
+
id: nanoid4(),
|
|
1392
2075
|
pageId: options.pageId,
|
|
1393
2076
|
source: "cdp",
|
|
1394
2077
|
level: normalizeConsoleLevel(event.type),
|
|
@@ -1408,7 +2091,7 @@ function normalizeConsoleLevel(level) {
|
|
|
1408
2091
|
}
|
|
1409
2092
|
|
|
1410
2093
|
// src/cdp/cdpNetwork.ts
|
|
1411
|
-
import { nanoid as
|
|
2094
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
1412
2095
|
|
|
1413
2096
|
// src/shared/sanitize.ts
|
|
1414
2097
|
function maskHeaders(headers = {}, maskNames = []) {
|
|
@@ -1448,7 +2131,7 @@ async function startCdpNetwork(options) {
|
|
|
1448
2131
|
await options.client.Network.enable();
|
|
1449
2132
|
options.client.Network.requestWillBeSent((event) => {
|
|
1450
2133
|
records.set(event.requestId, {
|
|
1451
|
-
id:
|
|
2134
|
+
id: nanoid5(),
|
|
1452
2135
|
pageId: options.pageId,
|
|
1453
2136
|
source: "cdp",
|
|
1454
2137
|
url: event.request.url,
|
|
@@ -1856,11 +2539,11 @@ import path4 from "path";
|
|
|
1856
2539
|
async function updateJsonMcpClientConfig(options) {
|
|
1857
2540
|
try {
|
|
1858
2541
|
const config = await readJsonConfig(options.configPath);
|
|
1859
|
-
if (!
|
|
2542
|
+
if (!isPlainRecord3(config)) {
|
|
1860
2543
|
warnConfigFailure(options, "config root must be a JSON object");
|
|
1861
2544
|
return;
|
|
1862
2545
|
}
|
|
1863
|
-
const mcpServers =
|
|
2546
|
+
const mcpServers = isPlainRecord3(config.mcpServers) ? config.mcpServers : {};
|
|
1864
2547
|
if (Object.hasOwn(mcpServers, options.serverName)) {
|
|
1865
2548
|
return;
|
|
1866
2549
|
}
|
|
@@ -1913,7 +2596,7 @@ async function readOptionalTextFile2(filePath) {
|
|
|
1913
2596
|
throw error;
|
|
1914
2597
|
}
|
|
1915
2598
|
}
|
|
1916
|
-
function
|
|
2599
|
+
function isPlainRecord3(value) {
|
|
1917
2600
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1918
2601
|
}
|
|
1919
2602
|
function warnConfigFailure(options, reason) {
|
|
@@ -2255,6 +2938,14 @@ function vueMcpNext(userOptions = {}) {
|
|
|
2255
2938
|
}
|
|
2256
2939
|
}
|
|
2257
2940
|
);
|
|
2941
|
+
server.ws.on(
|
|
2942
|
+
"vite-plugin-vue-mcp-next:performance-record",
|
|
2943
|
+
(payload) => {
|
|
2944
|
+
if (isPerformanceReport2(payload)) {
|
|
2945
|
+
appendPerformanceReport(ctx, payload);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
);
|
|
2258
2949
|
const port = String(server.config.server.port || 5173);
|
|
2259
2950
|
const mcpSseUrl = `http://${options.host}:${port}${options.mcpPath}/sse`;
|
|
2260
2951
|
const mcpStreamableHttpUrl = `http://${options.host}:${port}${options.mcpPath}/mcp`;
|
|
@@ -2298,7 +2989,7 @@ function isRuntimePageTarget(payload) {
|
|
|
2298
2989
|
return false;
|
|
2299
2990
|
}
|
|
2300
2991
|
const target = payload;
|
|
2301
|
-
return target.source === "runtime" && typeof target.pageId === "string" && typeof target.url === "string" && typeof target.pathname === "string" && typeof target.connected === "boolean";
|
|
2992
|
+
return target.source === "runtime" && typeof target.pageId === "string" && typeof target.url === "string" && typeof target.pathname === "string" && typeof target.connected === "boolean" && (target.runtimeClientId === void 0 || typeof target.runtimeClientId === "string");
|
|
2302
2993
|
}
|
|
2303
2994
|
function isConsoleRecord(payload) {
|
|
2304
2995
|
if (!payload || typeof payload !== "object") {
|
|
@@ -2317,6 +3008,13 @@ function isNetworkRecord(payload) {
|
|
|
2317
3008
|
const record = payload;
|
|
2318
3009
|
return typeof record.id === "string" && typeof record.pageId === "string" && record.source === "hook" && typeof record.url === "string" && typeof record.method === "string" && typeof record.startedAt === "number";
|
|
2319
3010
|
}
|
|
3011
|
+
function isPerformanceReport2(payload) {
|
|
3012
|
+
if (!payload || typeof payload !== "object") {
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
const report = payload;
|
|
3016
|
+
return typeof report.recordingId === "string" && typeof report.pageId === "string" && (report.source === "hook" || report.source === "cdp") && Boolean(report.summary) && Array.isArray(report.longTasks) && Array.isArray(report.limitations);
|
|
3017
|
+
}
|
|
2320
3018
|
export {
|
|
2321
3019
|
vueMcpNext as default,
|
|
2322
3020
|
vueMcpNext
|