Skip to content

Commit 78f18d5

Browse files
committed
Fix clipboard pasting issues in MCP inspector
1 parent 3c92a95 commit 78f18d5

3 files changed

Lines changed: 126 additions & 1 deletion

File tree

workspaces/mcp-inspector/mcp-inspector-extension/scripts/inject-theme.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,88 @@ const INDEX_HTML_PATH = path.join(
2323
// Placeholder CSS - will be replaced by dynamic theme injection at runtime
2424
const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
2525
<script>
26+
// === Clipboard paste bridge ===
27+
// Cross-origin iframes inside VS Code webviews cannot use the Clipboard API
28+
// (microsoft/vscode#129178, #182642). We intercept Cmd/Ctrl+V over paste-capable
29+
// fields and ask the parent webview for the clipboard text, then insert it ourselves.
30+
(function () {
31+
var pasteRequestId = 0;
32+
var pendingPasteRequests = new Map();
33+
34+
function isPasteableInput(el) {
35+
if (!el) return false;
36+
if (el.isContentEditable) return true;
37+
if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;
38+
if (el.tagName === 'INPUT') {
39+
var t = (el.type || 'text').toLowerCase();
40+
var pasteableTypes = ['text', 'search', 'url', 'email', 'password', 'tel', 'number'];
41+
return pasteableTypes.indexOf(t) !== -1 && !el.disabled && !el.readOnly;
42+
}
43+
return false;
44+
}
45+
46+
function insertTextAt(el, text) {
47+
if (!el || !text) return;
48+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
49+
var start = el.selectionStart != null ? el.selectionStart : el.value.length;
50+
var end = el.selectionEnd != null ? el.selectionEnd : el.value.length;
51+
var nativeSetter = Object.getOwnPropertyDescriptor(
52+
el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
53+
'value'
54+
).set;
55+
// Use the native setter so React's onChange fires (React tracks the prior value)
56+
nativeSetter.call(el, el.value.slice(0, start) + text + el.value.slice(end));
57+
var caret = start + text.length;
58+
el.setSelectionRange(caret, caret);
59+
el.dispatchEvent(new Event('input', { bubbles: true }));
60+
} else if (el.isContentEditable) {
61+
document.execCommand('insertText', false, text);
62+
}
63+
}
64+
65+
function selectAll(el) {
66+
if (!el) return;
67+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
68+
try { el.select(); } catch (_) { /* ignore */ }
69+
} else if (el.isContentEditable) {
70+
var range = document.createRange();
71+
range.selectNodeContents(el);
72+
var sel = window.getSelection();
73+
sel.removeAllRanges();
74+
sel.addRange(range);
75+
}
76+
}
77+
78+
document.addEventListener('keydown', function (e) {
79+
if (!(e.metaKey || e.ctrlKey)) return;
80+
if (e.shiftKey || e.altKey) return;
81+
var ae = document.activeElement;
82+
if (!isPasteableInput(ae)) return;
83+
var k = e.key && e.key.toLowerCase();
84+
if (k === 'v') {
85+
// Prevent the (broken) native paste path; do our own via the parent webview.
86+
e.preventDefault();
87+
e.stopPropagation();
88+
var id = ++pasteRequestId;
89+
pendingPasteRequests.set(id, ae);
90+
window.parent.postMessage({ type: 'mcp-inspector-request-paste', requestId: id }, '*');
91+
} else if (k === 'a') {
92+
// VS Code's webview swallows the native Select All; do it manually.
93+
e.preventDefault();
94+
e.stopPropagation();
95+
selectAll(ae);
96+
}
97+
}, true);
98+
99+
window.addEventListener('message', function (e) {
100+
var msg = e.data;
101+
if (!msg || msg.type !== 'mcp-inspector-paste-response') return;
102+
var target = pendingPasteRequests.get(msg.requestId);
103+
pendingPasteRequests.delete(msg.requestId);
104+
if (target && msg.text) insertTextAt(target, msg.text);
105+
});
106+
})();
107+
26108
// Function to convert color (hex or rgb/rgba) to HSL format
27109
function colorToHsl(color) {
28110
let r, g, b;

workspaces/mcp-inspector/mcp-inspector-extension/src/MCPInspectorViewProvider.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,40 @@ export class MCPInspectorViewProvider implements vscode.WebviewViewProvider {
2525
enableScripts: WebviewConfig.ENABLE_SCRIPTS,
2626
};
2727

28+
MCPInspectorViewProvider.attachClipboardBridge(webviewView.webview);
2829
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
2930
} catch (error) {
3031
Logger.error('Failed to resolve webview view', error);
3132
throw error;
3233
}
3334
}
3435

36+
/**
37+
* Wires up the parent-webview side of the clipboard paste bridge.
38+
* The iframe cannot read the system clipboard directly because it's cross-origin
39+
* to the VS Code webview shell; we relay the request through the extension host.
40+
*/
41+
public static attachClipboardBridge(webview: vscode.Webview): vscode.Disposable {
42+
return webview.onDidReceiveMessage(async (msg) => {
43+
if (!msg || msg.type !== 'mcp-inspector-request-clipboard-text') return;
44+
try {
45+
const text = await vscode.env.clipboard.readText();
46+
webview.postMessage({
47+
type: 'mcp-inspector-clipboard-text',
48+
requestId: msg.requestId,
49+
text,
50+
});
51+
} catch (error) {
52+
Logger.error('Failed to read clipboard for inspector paste', error);
53+
webview.postMessage({
54+
type: 'mcp-inspector-clipboard-text',
55+
requestId: msg.requestId,
56+
text: '',
57+
});
58+
}
59+
});
60+
}
61+
3562
/**
3663
* Get HTML content for the webview
3764
*/
@@ -199,7 +226,7 @@ export class MCPInspectorViewProvider implements vscode.WebviewViewProvider {
199226
</style>
200227
</head>
201228
<body>
202-
<iframe id="inspector-iframe" src="${inspectorUrl}" sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
229+
<iframe id="inspector-iframe" src="${inspectorUrl}" sandbox="allow-scripts allow-forms allow-same-origin" allow="clipboard-read; clipboard-write"></iframe>
203230
<script>
204231
(function() {
205232
const iframe = document.getElementById('inspector-iframe');
@@ -344,6 +371,19 @@ export class MCPInspectorViewProvider implements vscode.WebviewViewProvider {
344371
setTimeout(sendThemeColors, 200);
345372
};
346373
374+
// === Clipboard paste bridge (parent webview side) ===
375+
// Iframe asks us for clipboard text -> we ask the extension -> we forward back to iframe.
376+
const vscodeApi = acquireVsCodeApi();
377+
window.addEventListener('message', function(e) {
378+
const msg = e.data;
379+
if (!msg || typeof msg !== 'object') return;
380+
if (msg.type === 'mcp-inspector-request-paste' && e.source === iframe.contentWindow) {
381+
vscodeApi.postMessage({ type: 'mcp-inspector-request-clipboard-text', requestId: msg.requestId });
382+
} else if (msg.type === 'mcp-inspector-clipboard-text' && iframe.contentWindow) {
383+
iframe.contentWindow.postMessage({ type: 'mcp-inspector-paste-response', requestId: msg.requestId, text: msg.text }, '*');
384+
}
385+
});
386+
347387
// Listen for VSCode theme changes
348388
const observer = new MutationObserver((mutations) => {
349389
sendThemeColors();

workspaces/mcp-inspector/mcp-inspector-extension/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ function openInspectorPanel(
119119
dark: vscode.Uri.joinPath(context.extensionUri, 'resources', 'icon-light.svg'),
120120
};
121121

122+
// Wire up the paste bridge so iframe inputs can paste from the system clipboard
123+
context.subscriptions.push(MCPInspectorViewProvider.attachClipboardBridge(currentPanel.webview));
124+
122125
// Set initial loading content
123126
currentPanel.webview.html = provider.getLoadingHtml();
124127

0 commit comments

Comments
 (0)