@mcpc-tech/unplugin-dev-inspector-mcp 0.1.30 → 0.1.32

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.
@@ -2,6 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client";
2
2
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { createClientExecClient } from "@mcpc-tech/cmcp";
5
+ import { toPng } from "html-to-image";
5
6
  import { ListPromptsResultSchema, PromptListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
6
7
 
7
8
  //#region src/tool-schemas.ts
@@ -102,6 +103,72 @@ Returns: source location, DOM hierarchy, computed styles, dimensions, and user n
102
103
  } },
103
104
  required: ["code"]
104
105
  }
106
+ },
107
+ capture_area_context: {
108
+ name: "capture_area_context",
109
+ description: `Capture area context by activating visual area selection mode.
110
+
111
+ User draws a rectangle on the page to select multiple elements at once. After selection, returns context for all elements in the area including source locations, DOM info, and screenshot.
112
+
113
+ **Flow**:
114
+ 1. Activates area selection mode (user sees crosshair cursor)
115
+ 2. User draws rectangle around target elements
116
+ 3. Returns: primary element + related elements with source locations, DOM hierarchy, and screenshot`,
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {}
120
+ }
121
+ },
122
+ get_network_requests: {
123
+ name: "get_network_requests",
124
+ description: `Get network requests from browser for debugging.
125
+
126
+ Returns list of HTTP requests with ID, method, URL, and status code. Use reqid parameter to get full details of a specific request including headers, body, and timing.
127
+
128
+ **Usage**:
129
+ - Call without parameters to list all requests
130
+ - Call with reqid to get specific request details`,
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: { reqid: {
134
+ type: "number",
135
+ description: "Optional. Request ID to get full details. If omitted, returns list of all requests."
136
+ } }
137
+ }
138
+ },
139
+ get_console_messages: {
140
+ name: "get_console_messages",
141
+ description: `Get console messages from browser for debugging.
142
+
143
+ Returns list of console logs with ID, level (log/warn/error/info), and message content. Use msgid parameter to get full details of a specific message.
144
+
145
+ **Usage**:
146
+ - Call without parameters to list all messages
147
+ - Call with msgid to get specific message details`,
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: { msgid: {
151
+ type: "number",
152
+ description: "Optional. Message ID to get full details. If omitted, returns list of all messages."
153
+ } }
154
+ }
155
+ },
156
+ get_stdio_messages: {
157
+ name: "get_stdio_messages",
158
+ description: `Get stdio (stdout/stderr) terminal messages from dev server process.
159
+
160
+ Returns list of terminal output with ID, stream type (stdout/stderr), and content. Use stdioid parameter to get full details of a specific message.
161
+
162
+ **Usage**:
163
+ - Call without parameters to list all stdio messages
164
+ - Call with stdioid to get specific message details`,
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: { stdioid: {
168
+ type: "number",
169
+ description: "Optional. Stdio message ID to get full details. If omitted, returns list of all messages."
170
+ } }
171
+ }
105
172
  }
106
173
  };
107
174
 
@@ -293,7 +360,7 @@ function getDevServerBaseUrl() {
293
360
  if (injectedConfig?.baseUrl && typeof injectedConfig.baseUrl === "string") return injectedConfig.baseUrl.replace(/\/$/, "");
294
361
  if (injectedConfig?.host && injectedConfig?.port) return `http://${injectedConfig.host}:${injectedConfig.port}${base}`.replace(/\/$/, "");
295
362
  if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
296
- return `http://localhost:5137`;
363
+ return `http://localhost:6137`;
297
364
  }
298
365
  /**
299
366
  * Merge custom agent with default agent properties
@@ -352,24 +419,257 @@ async function getDefaultAgent() {
352
419
  //#endregion
353
420
  //#region client/utils/format.ts
354
421
  /**
422
+ * Format DOM element info only (for Code tab)
423
+ */
424
+ function formatDomElement(elementInfo) {
425
+ if (!elementInfo) return "";
426
+ const { tagName, textContent, className, id: elemId, domPath, boundingBox } = elementInfo;
427
+ let output = `### DOM Element
428
+ \`\`\`
429
+ Tag: <${tagName}${elemId ? ` id="${elemId}"` : ""}${className ? ` class="${className}"` : ""}>
430
+ Text: ${textContent || "(empty)"}
431
+ Path: ${domPath || "N/A"}
432
+ \`\`\`
433
+ `;
434
+ if (boundingBox) output += `
435
+ ### Position & Size
436
+ - **Position**: (${Math.round(boundingBox.x)}, ${Math.round(boundingBox.y)})
437
+ - **Size**: ${Math.round(boundingBox.width)}px × ${Math.round(boundingBox.height)}px
438
+ `;
439
+ return output;
440
+ }
441
+ /**
442
+ * Format computed styles only (for Styles tab)
443
+ */
444
+ function formatComputedStyles(elementInfo) {
445
+ if (!elementInfo) return "";
446
+ const { computedStyles, styles } = elementInfo;
447
+ if (computedStyles) return `### Computed Styles
448
+
449
+ **Layout**:
450
+ - display: ${computedStyles.layout.display}
451
+ - position: ${computedStyles.layout.position}
452
+ - z-index: ${computedStyles.layout.zIndex}
453
+
454
+ **Typography**:
455
+ - font: ${computedStyles.typography.fontSize} ${computedStyles.typography.fontFamily}
456
+ - color: ${computedStyles.typography.color}
457
+ - text-align: ${computedStyles.typography.textAlign}
458
+
459
+ **Spacing**:
460
+ - padding: ${computedStyles.spacing.padding}
461
+ - margin: ${computedStyles.spacing.margin}
462
+
463
+ **Background & Border**:
464
+ - background: ${computedStyles.background.backgroundColor}
465
+ - border: ${computedStyles.border.border}
466
+ - border-radius: ${computedStyles.border.borderRadius}
467
+ `;
468
+ else if (styles) return `### Key Styles
469
+ - display: ${styles.display}
470
+ - color: ${styles.color}
471
+ - background: ${styles.backgroundColor}
472
+ - font-size: ${styles.fontSize}
473
+ `;
474
+ return "";
475
+ }
476
+ /**
477
+ * Format source location
478
+ */
479
+ function formatSourceInfo(sourceInfo) {
480
+ return `## Source Code
481
+ - **File**: ${sourceInfo.file}
482
+ - **Line**: ${sourceInfo.line}:${sourceInfo.column}
483
+ - **Component**: ${sourceInfo.component}
484
+ `;
485
+ }
486
+ /**
487
+ * Format console messages
488
+ */
489
+ function formatConsoleMessages(messages) {
490
+ if (messages.length === 0) return "";
491
+ const formatted = messages.map((msg) => {
492
+ return `- ${msg.level === "error" ? "❌" : msg.level === "warn" ? "⚠️" : "📝"} [${msg.level}] ${msg.text}`;
493
+ }).join("\n");
494
+ return `## Console Messages (${messages.length})
495
+ ${formatted}
496
+ `;
497
+ }
498
+ /**
499
+ * Format network requests
500
+ */
501
+ function formatNetworkRequests(requests) {
502
+ if (requests.length === 0) return "";
503
+ const formatted = requests.map((req) => {
504
+ let entry = `### ${req.method} ${req.url}
505
+ - **Status**: ${req.status}
506
+ `;
507
+ if (req.details && req.details !== "(expand request to load details)") entry += `
508
+ #### Details
509
+ \`\`\`
510
+ ${req.details}
511
+ \`\`\`
512
+ `;
513
+ return entry;
514
+ }).join("\n");
515
+ return `## Network Requests (${requests.length})
516
+ ${formatted}
517
+ `;
518
+ }
519
+ /**
520
+ * Format stdio messages
521
+ */
522
+ function formatStdioMessages(messages) {
523
+ if (messages.length === 0) return "";
524
+ const formatted = messages.map((msg) => {
525
+ return `- [${msg.stream}] ${msg.data}`;
526
+ }).join("\n");
527
+ return `## Terminal Logs (${messages.length})
528
+ ${formatted}
529
+ `;
530
+ }
531
+ /**
532
+ * Format page information
533
+ */
534
+ function formatPageInfo(pageInfo) {
535
+ return `## Page Information
536
+ - **URL**: ${pageInfo.url}
537
+ - **Title**: ${pageInfo.title}
538
+ - **Viewport**: ${pageInfo.viewport.width} × ${pageInfo.viewport.height}
539
+ - **Language**: ${pageInfo.language}
540
+ `;
541
+ }
542
+ /**
355
543
  * Format element annotations (notes) from primary and related elements
356
544
  */
357
545
  function formatElementAnnotations(options) {
358
- const { primaryNote, primaryTag, relatedElements } = options;
546
+ const { primaryNote, primaryTag, relatedElements, elementNotes } = options;
359
547
  const notes = [];
360
548
  if (primaryNote) {
361
549
  const tag = primaryTag || "element";
362
550
  notes.push(`- **Primary element** (${tag}): ${primaryNote}`);
363
551
  }
364
552
  if (relatedElements && relatedElements.length > 0) relatedElements.forEach((el, idx) => {
365
- if (el.note) {
553
+ const note = el.note || elementNotes?.[idx];
554
+ if (note) {
366
555
  const tag = el.elementInfo?.tagName?.toLowerCase() || el.component;
367
556
  const location = el.file && el.line ? ` at ${el.file}:${el.line}` : "";
368
- notes.push(`- **Related element #${idx + 1}** (${tag}${location}): ${el.note}`);
557
+ notes.push(`- **Related element #${idx + 1}** (${tag}${location}): ${note}`);
369
558
  }
370
559
  });
371
560
  return notes.length > 0 ? `\n**Element Annotations**:\n${notes.join("\n")}\n` : "";
372
561
  }
562
+ /**
563
+ * Format complete context for Copy & Go (matches ContextPicker tab structure)
564
+ */
565
+ function formatCopyContext(options) {
566
+ const { sourceInfo, includeElement, includeStyles, includePageInfo, pageInfo, feedback, consoleMessages, networkRequests, stdioMessages, relatedElements, relatedElementIds, elementNotes } = options;
567
+ let output = "# Element Context\n\n";
568
+ const hasRelatedElements = relatedElements && relatedElements.length > 0 && relatedElementIds && relatedElementIds.length > 0;
569
+ if (sourceInfo && includeElement) {
570
+ output += "## Code\n\n";
571
+ if (hasRelatedElements) output += "### Primary Element (Best Match)\n";
572
+ output += formatSourceInfo(sourceInfo);
573
+ output += "\n";
574
+ output += formatDomElement(sourceInfo.elementInfo);
575
+ output += "\n";
576
+ }
577
+ if (relatedElements && relatedElements.length > 0 && relatedElementIds && relatedElementIds.length > 0) {
578
+ const selectedElements = relatedElements.filter((_, idx) => relatedElementIds.includes(idx));
579
+ if (selectedElements.length > 0) {
580
+ output += "## Related Elements\n\n";
581
+ const grouped = selectedElements.reduce((acc, el) => {
582
+ const file = el.file || "unknown";
583
+ if (!acc[file]) acc[file] = [];
584
+ acc[file].push(el);
585
+ return acc;
586
+ }, {});
587
+ Object.entries(grouped).forEach(([file, elements]) => {
588
+ output += `### ${file}\n`;
589
+ elements.forEach((el) => {
590
+ const originalIndex = relatedElements.indexOf(el);
591
+ const note = elementNotes ? elementNotes[originalIndex] : null;
592
+ output += `- **${el.component}** (${el.line}:${el.column})`;
593
+ if (el.elementInfo?.tagName) {
594
+ output += ` - \`<${el.elementInfo.tagName}`;
595
+ if (el.elementInfo.className) output += ` class="${el.elementInfo.className}"`;
596
+ if (el.elementInfo.id) output += ` id="${el.elementInfo.id}"`;
597
+ output += `>`;
598
+ if (el.elementInfo.textContent) {
599
+ const preview = el.elementInfo.textContent.trim().slice(0, 30);
600
+ if (preview) output += ` "${preview}${el.elementInfo.textContent.length > 30 ? "..." : ""}"`;
601
+ }
602
+ output += "`";
603
+ }
604
+ if (note) output += `\n - **USER NOTE**: "${note}"`;
605
+ output += "\n";
606
+ });
607
+ output += "\n";
608
+ });
609
+ }
610
+ }
611
+ if (sourceInfo?.elementInfo && includeStyles) {
612
+ output += "## Styles\n\n";
613
+ output += formatComputedStyles(sourceInfo.elementInfo);
614
+ output += "\n";
615
+ }
616
+ if (pageInfo && includePageInfo) {
617
+ output += formatPageInfo(pageInfo);
618
+ output += "\n";
619
+ }
620
+ if (feedback) output += `## User Request\n\n${feedback}\n\n`;
621
+ if (consoleMessages && consoleMessages.length > 0) {
622
+ output += formatConsoleMessages(consoleMessages);
623
+ output += "\n";
624
+ }
625
+ if (networkRequests && networkRequests.length > 0) {
626
+ output += formatNetworkRequests(networkRequests);
627
+ output += "\n";
628
+ }
629
+ if (stdioMessages && stdioMessages.length > 0) output += formatStdioMessages(stdioMessages);
630
+ return output.trim();
631
+ }
632
+
633
+ //#endregion
634
+ //#region client/utils/screenshot.ts
635
+ /**
636
+ * Capture screenshot of a DOM element
637
+ * @param element - The DOM element to capture
638
+ * @param options - Screenshot options
639
+ * @returns Promise<string> - Data URL of the screenshot, or empty string on error
640
+ */
641
+ async function captureElementScreenshot(element, options = {}) {
642
+ const { quality = .95, maxWidth = 1200, maxHeight = 800 } = options;
643
+ try {
644
+ const rect = element.getBoundingClientRect();
645
+ return await toPng(element, {
646
+ quality,
647
+ pixelRatio: Math.min(1, maxWidth / rect.width, maxHeight / rect.height) * (window.devicePixelRatio || 1),
648
+ cacheBust: true,
649
+ width: rect.width,
650
+ height: rect.height,
651
+ style: {
652
+ margin: "0",
653
+ transform: "none"
654
+ },
655
+ filter: (node) => {
656
+ if (node instanceof HTMLElement) {
657
+ const tagName = node.tagName.toLowerCase();
658
+ if (tagName === "iframe" || tagName === "script" || tagName === "link" || tagName === "video" || tagName === "audio" || tagName === "object" || tagName === "embed") return false;
659
+ if (tagName === "img") {
660
+ const img = node;
661
+ if (!img.complete || img.naturalWidth === 0) return false;
662
+ }
663
+ }
664
+ return true;
665
+ },
666
+ fontEmbedCSS: ""
667
+ });
668
+ } catch (error) {
669
+ console.error("[screenshot] Failed to capture element:", error);
670
+ return "";
671
+ }
672
+ }
373
673
 
374
674
  //#endregion
375
675
  //#region client/hooks/useMcp.ts
@@ -552,6 +852,13 @@ ${requests}
552
852
 
553
853
  `;
554
854
  }
855
+ const notesSection = formatElementAnnotations({
856
+ primaryNote: sourceInfo.note,
857
+ primaryTag: elementInfo?.tagName?.toLowerCase() || component,
858
+ relatedElements: sourceInfo.relatedElements,
859
+ elementNotes: selectedContext?.elementNotes
860
+ });
861
+ if (notesSection) output += notesSection;
555
862
  output += `## Your Task
556
863
  1. Investigate the issue using 'chrome_devtools' tool (check console logs, network requests, performance)
557
864
  2. Use 'execute_page_script' to query element state if needed
@@ -722,6 +1029,77 @@ After clicking, use \`list_inspections\` to view the captured element with full
722
1029
  {
723
1030
  ...TOOL_SCHEMAS.execute_page_script,
724
1031
  implementation: patchContext
1032
+ },
1033
+ {
1034
+ ...TOOL_SCHEMAS.capture_area_context,
1035
+ implementation: () => {
1036
+ cancelPendingRequest("New area capture request started");
1037
+ window.dispatchEvent(new CustomEvent("activate-area-select"));
1038
+ return new Promise((resolve, reject) => {
1039
+ pendingResolve = resolve;
1040
+ pendingReject = reject;
1041
+ const handleAreaComplete = async (event) => {
1042
+ window.removeEventListener("area-selection-complete", handleAreaComplete);
1043
+ const { sourceInfo } = event.detail || {};
1044
+ if (!sourceInfo) {
1045
+ reject(/* @__PURE__ */ new Error("No elements selected in area"));
1046
+ clearPendingRequest();
1047
+ return;
1048
+ }
1049
+ try {
1050
+ const pageInfo = {
1051
+ url: window.location.href,
1052
+ title: document.title,
1053
+ viewport: {
1054
+ width: window.innerWidth,
1055
+ height: window.innerHeight
1056
+ },
1057
+ language: document.documentElement.lang || navigator.language
1058
+ };
1059
+ let screenshot;
1060
+ if (sourceInfo.element) screenshot = await captureElementScreenshot(sourceInfo.element);
1061
+ const elementNotes = {};
1062
+ sourceInfo.relatedElements?.forEach((el, idx) => {
1063
+ if (el.note) elementNotes[idx] = el.note;
1064
+ });
1065
+ const content = [{
1066
+ type: "text",
1067
+ text: formatCopyContext({
1068
+ sourceInfo,
1069
+ includeElement: true,
1070
+ includeStyles: false,
1071
+ includePageInfo: true,
1072
+ pageInfo,
1073
+ relatedElements: sourceInfo.relatedElements,
1074
+ relatedElementIds: sourceInfo.relatedElements?.map((_, idx) => idx),
1075
+ elementNotes: Object.keys(elementNotes).length > 0 ? elementNotes : void 0
1076
+ })
1077
+ }];
1078
+ if (screenshot) {
1079
+ const base64Data = screenshot.replace(/^data:image\/\w+;base64,/, "");
1080
+ content.push({
1081
+ type: "image",
1082
+ data: base64Data,
1083
+ mimeType: "image/png"
1084
+ });
1085
+ }
1086
+ resolve({ content });
1087
+ } catch (error) {
1088
+ const errorMsg = error instanceof Error ? error.message : String(error);
1089
+ reject(/* @__PURE__ */ new Error(`Error processing area selection: ${errorMsg}`));
1090
+ }
1091
+ clearPendingRequest();
1092
+ };
1093
+ window.addEventListener("area-selection-complete", handleAreaComplete);
1094
+ setTimeout(() => {
1095
+ if (pendingReject === reject) {
1096
+ window.removeEventListener("area-selection-complete", handleAreaComplete);
1097
+ clearPendingRequest();
1098
+ reject(/* @__PURE__ */ new Error("Timeout: No area selected"));
1099
+ }
1100
+ }, TIMEOUT_MS);
1101
+ });
1102
+ }
725
1103
  }
726
1104
  ];
727
1105
  const getCustomTools = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpc-tech/unplugin-dev-inspector-mcp",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Universal dev inspector plugin for React/Vue - inspect component sources and API calls in any bundler",
5
5
  "type": "module",
6
6
  "license": "MIT",