Skip to content

Commit 70085e3

Browse files
committed
Fix clipboard copying issues in MCP inspector
1 parent 78f18d5 commit 70085e3

2 files changed

Lines changed: 159 additions & 23 deletions

File tree

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

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,57 @@ const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
7575
}
7676
}
7777
78+
function getSelectionContext() {
79+
var ae = document.activeElement;
80+
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA')) {
81+
var s = ae.selectionStart;
82+
var en = ae.selectionEnd;
83+
if (s != null && en != null && s !== en) {
84+
return { text: ae.value.slice(s, en), inInput: true, el: ae, start: s, end: en };
85+
}
86+
}
87+
var sel = window.getSelection();
88+
var selText = sel ? sel.toString() : '';
89+
if (selText) return { text: selText, inInput: false };
90+
return null;
91+
}
92+
93+
function deleteSelection(ctx) {
94+
if (!ctx) return;
95+
if (ctx.inInput && ctx.el && !ctx.el.disabled && !ctx.el.readOnly) {
96+
var nativeSetter = Object.getOwnPropertyDescriptor(
97+
ctx.el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
98+
'value'
99+
).set;
100+
nativeSetter.call(ctx.el, ctx.el.value.slice(0, ctx.start) + ctx.el.value.slice(ctx.end));
101+
ctx.el.setSelectionRange(ctx.start, ctx.start);
102+
ctx.el.dispatchEvent(new Event('input', { bubbles: true }));
103+
} else {
104+
try { document.execCommand('delete'); } catch (_) { /* ignore */ }
105+
}
106+
}
107+
78108
document.addEventListener('keydown', function (e) {
79109
if (!(e.metaKey || e.ctrlKey)) return;
80110
if (e.shiftKey || e.altKey) return;
111+
var k = e.key && e.key.toLowerCase();
112+
113+
// Copy / Cut: work on any selection in the document (form field or UI text).
114+
if (k === 'c' || k === 'x') {
115+
var ctx = getSelectionContext();
116+
if (!ctx || !ctx.text) return;
117+
e.preventDefault();
118+
e.stopPropagation();
119+
if (typeof window.__mcpInspectorWriteClipboard === 'function') {
120+
window.__mcpInspectorWriteClipboard(ctx.text).catch(function () { /* swallow */ });
121+
}
122+
if (k === 'x') deleteSelection(ctx);
123+
return;
124+
}
125+
126+
// Paste / Select-all: only meaningful when a paste-capable field is focused.
81127
var ae = document.activeElement;
82128
if (!isPasteableInput(ae)) return;
83-
var k = e.key && e.key.toLowerCase();
84129
if (k === 'v') {
85130
// Prevent the (broken) native paste path; do our own via the parent webview.
86131
e.preventDefault();
@@ -96,12 +141,43 @@ const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
96141
}
97142
}, true);
98143
144+
// Bridge for writing to the system clipboard (copy direction).
145+
// Cross-origin iframes can't use navigator.clipboard.writeText() either,
146+
// so we ask the extension host to do it via vscode.env.clipboard.writeText().
147+
var copyRequestId = 0;
148+
var pendingCopyRequests = new Map();
149+
window.__mcpInspectorWriteClipboard = function (text) {
150+
return new Promise(function (resolve, reject) {
151+
var id = ++copyRequestId;
152+
pendingCopyRequests.set(id, { resolve: resolve, reject: reject });
153+
window.parent.postMessage({
154+
type: 'mcp-inspector-request-clipboard-write',
155+
requestId: id,
156+
text: text == null ? '' : String(text)
157+
}, '*');
158+
setTimeout(function () {
159+
if (pendingCopyRequests.has(id)) {
160+
pendingCopyRequests.delete(id);
161+
reject(new Error('Clipboard write timed out'));
162+
}
163+
}, 5000);
164+
});
165+
};
166+
99167
window.addEventListener('message', function (e) {
100168
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);
169+
if (!msg) return;
170+
if (msg.type === 'mcp-inspector-paste-response') {
171+
var target = pendingPasteRequests.get(msg.requestId);
172+
pendingPasteRequests.delete(msg.requestId);
173+
if (target && msg.text) insertTextAt(target, msg.text);
174+
} else if (msg.type === 'mcp-inspector-clipboard-write-result') {
175+
var pending = pendingCopyRequests.get(msg.requestId);
176+
if (!pending) return;
177+
pendingCopyRequests.delete(msg.requestId);
178+
if (msg.ok) pending.resolve();
179+
else pending.reject(new Error(msg.error || 'Clipboard write failed'));
180+
}
105181
});
106182
})();
107183
@@ -151,7 +227,44 @@ const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
151227
return Math.round(h * 360) + ' ' + Math.round(s * 100) + '% ' + Math.round(l * 100) + '%';
152228
}
153229
154-
// Remove existing theme styles when updating
230+
// Fallback to execCommand when webview blocks navigator.clipboard.writeText.
231+
(function patchClipboard() {
232+
const execCopy = (text) => {
233+
const ta = document.createElement('textarea');
234+
ta.value = text == null ? '' : String(text);
235+
ta.setAttribute('readonly', '');
236+
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
237+
document.body.appendChild(ta);
238+
const sel = document.getSelection();
239+
const prevRange = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
240+
ta.select();
241+
ta.setSelectionRange(0, ta.value.length);
242+
let ok = false;
243+
try { ok = document.execCommand('copy'); } catch (_) { ok = false; }
244+
document.body.removeChild(ta);
245+
if (prevRange && sel) { sel.removeAllRanges(); sel.addRange(prevRange); }
246+
return ok;
247+
};
248+
const native = navigator.clipboard && navigator.clipboard.writeText
249+
? navigator.clipboard.writeText.bind(navigator.clipboard)
250+
: null;
251+
const writeText = async (text) => {
252+
if (native) {
253+
try { return await native(text); } catch (_) {}
254+
}
255+
// VS Code webview path: ask the extension host to write the clipboard.
256+
if (typeof window.__mcpInspectorWriteClipboard === 'function') {
257+
try { await window.__mcpInspectorWriteClipboard(text); return; } catch (_) {}
258+
}
259+
if (execCopy(text)) return;
260+
throw new Error('Clipboard copy failed');
261+
};
262+
if (!navigator.clipboard) {
263+
Object.defineProperty(navigator, 'clipboard', { value: {}, configurable: true });
264+
}
265+
try { navigator.clipboard.writeText = writeText; } catch (_) {}
266+
})();
267+
155268
let themeStyleElement = null;
156269
157270
// Listen for theme color messages from parent

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

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,40 @@ export class MCPInspectorViewProvider implements vscode.WebviewViewProvider {
4040
*/
4141
public static attachClipboardBridge(webview: vscode.Webview): vscode.Disposable {
4242
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-
});
43+
if (!msg) return;
44+
if (msg.type === 'mcp-inspector-request-clipboard-text') {
45+
try {
46+
const text = await vscode.env.clipboard.readText();
47+
webview.postMessage({
48+
type: 'mcp-inspector-clipboard-text',
49+
requestId: msg.requestId,
50+
text,
51+
});
52+
} catch (error) {
53+
Logger.error('Failed to read clipboard for inspector paste', error);
54+
webview.postMessage({
55+
type: 'mcp-inspector-clipboard-text',
56+
requestId: msg.requestId,
57+
text: '',
58+
});
59+
}
60+
} else if (msg.type === 'mcp-inspector-request-clipboard-write') {
61+
try {
62+
await vscode.env.clipboard.writeText(typeof msg.text === 'string' ? msg.text : '');
63+
webview.postMessage({
64+
type: 'mcp-inspector-clipboard-write-result',
65+
requestId: msg.requestId,
66+
ok: true,
67+
});
68+
} catch (error) {
69+
Logger.error('Failed to write clipboard for inspector copy', error);
70+
webview.postMessage({
71+
type: 'mcp-inspector-clipboard-write-result',
72+
requestId: msg.requestId,
73+
ok: false,
74+
error: error instanceof Error ? error.message : String(error),
75+
});
76+
}
5877
}
5978
});
6079
}
@@ -371,16 +390,20 @@ export class MCPInspectorViewProvider implements vscode.WebviewViewProvider {
371390
setTimeout(sendThemeColors, 200);
372391
};
373392
374-
// === Clipboard paste bridge (parent webview side) ===
375-
// Iframe asks us for clipboard text -> we ask the extension -> we forward back to iframe.
393+
// === Clipboard bridge (parent webview side) ===
394+
// Iframe asks us for clipboard read/write -> we ask the extension -> we forward back to iframe.
376395
const vscodeApi = acquireVsCodeApi();
377396
window.addEventListener('message', function(e) {
378397
const msg = e.data;
379398
if (!msg || typeof msg !== 'object') return;
380399
if (msg.type === 'mcp-inspector-request-paste' && e.source === iframe.contentWindow) {
381400
vscodeApi.postMessage({ type: 'mcp-inspector-request-clipboard-text', requestId: msg.requestId });
401+
} else if (msg.type === 'mcp-inspector-request-clipboard-write' && e.source === iframe.contentWindow) {
402+
vscodeApi.postMessage({ type: 'mcp-inspector-request-clipboard-write', requestId: msg.requestId, text: msg.text });
382403
} else if (msg.type === 'mcp-inspector-clipboard-text' && iframe.contentWindow) {
383404
iframe.contentWindow.postMessage({ type: 'mcp-inspector-paste-response', requestId: msg.requestId, text: msg.text }, '*');
405+
} else if (msg.type === 'mcp-inspector-clipboard-write-result' && iframe.contentWindow) {
406+
iframe.contentWindow.postMessage({ type: 'mcp-inspector-clipboard-write-result', requestId: msg.requestId, ok: msg.ok, error: msg.error }, '*');
384407
}
385408
});
386409

0 commit comments

Comments
 (0)