Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,164 @@ const INDEX_HTML_PATH = path.join(
// Placeholder CSS - will be replaced by dynamic theme injection at runtime
const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
<script>
// === Clipboard paste bridge ===
// Cross-origin iframes inside VS Code webviews cannot use the Clipboard API
// (microsoft/vscode#129178, #182642). We intercept Cmd/Ctrl+V over paste-capable
// fields and ask the parent webview for the clipboard text, then insert it ourselves.
(function () {
var pasteRequestId = 0;
var pendingPasteRequests = new Map();

function isPasteableInput(el) {
if (!el) return false;
if (el.isContentEditable) return true;
if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;
if (el.tagName === 'INPUT') {
var t = (el.type || 'text').toLowerCase();
var pasteableTypes = ['text', 'search', 'url', 'email', 'password', 'tel', 'number'];
return pasteableTypes.indexOf(t) !== -1 && !el.disabled && !el.readOnly;
}
return false;
}

function insertTextAt(el, text) {
if (!el || !text) return;
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
var start = el.selectionStart != null ? el.selectionStart : el.value.length;
var end = el.selectionEnd != null ? el.selectionEnd : el.value.length;
var nativeSetter = Object.getOwnPropertyDescriptor(
el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
'value'
).set;
// Use the native setter so React's onChange fires (React tracks the prior value)
nativeSetter.call(el, el.value.slice(0, start) + text + el.value.slice(end));
var caret = start + text.length;
el.setSelectionRange(caret, caret);
Comment thread
dan-niles marked this conversation as resolved.
Outdated
el.dispatchEvent(new Event('input', { bubbles: true }));
} else if (el.isContentEditable) {
document.execCommand('insertText', false, text);
}
}

function selectAll(el) {
if (!el) return;
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
try { el.select(); } catch (_) { /* ignore */ }
} else if (el.isContentEditable) {
var range = document.createRange();
range.selectNodeContents(el);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
}

function getSelectionContext() {
var ae = document.activeElement;
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA')) {
var s = ae.selectionStart;
var en = ae.selectionEnd;
if (s != null && en != null && s !== en) {
return { text: ae.value.slice(s, en), inInput: true, el: ae, start: s, end: en };
}
}
var sel = window.getSelection();
var selText = sel ? sel.toString() : '';
if (selText) return { text: selText, inInput: false };
return null;
}

function deleteSelection(ctx) {
if (!ctx) return;
if (ctx.inInput && ctx.el && !ctx.el.disabled && !ctx.el.readOnly) {
var nativeSetter = Object.getOwnPropertyDescriptor(
ctx.el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
'value'
).set;
nativeSetter.call(ctx.el, ctx.el.value.slice(0, ctx.start) + ctx.el.value.slice(ctx.end));
ctx.el.setSelectionRange(ctx.start, ctx.start);
ctx.el.dispatchEvent(new Event('input', { bubbles: true }));
} else {
try { document.execCommand('delete'); } catch (_) { /* ignore */ }
}
}

document.addEventListener('keydown', function (e) {
if (!(e.metaKey || e.ctrlKey)) return;
if (e.shiftKey || e.altKey) return;
var k = e.key && e.key.toLowerCase();

// Copy / Cut: work on any selection in the document (form field or UI text).
if (k === 'c' || k === 'x') {
var ctx = getSelectionContext();
if (!ctx || !ctx.text) return;
e.preventDefault();
e.stopPropagation();
if (typeof window.__mcpInspectorWriteClipboard === 'function') {
window.__mcpInspectorWriteClipboard(ctx.text).catch(function () { /* swallow */ });
}
if (k === 'x') deleteSelection(ctx);
return;
Comment thread
dan-niles marked this conversation as resolved.
}

// Paste / Select-all: only meaningful when a paste-capable field is focused.
var ae = document.activeElement;
if (!isPasteableInput(ae)) return;
if (k === 'v') {
// Prevent the (broken) native paste path; do our own via the parent webview.
e.preventDefault();
e.stopPropagation();
var id = ++pasteRequestId;
pendingPasteRequests.set(id, ae);
Comment thread
dan-niles marked this conversation as resolved.
window.parent.postMessage({ type: 'mcp-inspector-request-paste', requestId: id }, '*');
} else if (k === 'a') {
// VS Code's webview swallows the native Select All; do it manually.
e.preventDefault();
e.stopPropagation();
selectAll(ae);
}
}, true);

// Bridge for writing to the system clipboard (copy direction).
// Cross-origin iframes can't use navigator.clipboard.writeText() either,
// so we ask the extension host to do it via vscode.env.clipboard.writeText().
var copyRequestId = 0;
var pendingCopyRequests = new Map();
window.__mcpInspectorWriteClipboard = function (text) {
return new Promise(function (resolve, reject) {
var id = ++copyRequestId;
pendingCopyRequests.set(id, { resolve: resolve, reject: reject });
window.parent.postMessage({
type: 'mcp-inspector-request-clipboard-write',
requestId: id,
text: text == null ? '' : String(text)
}, '*');
setTimeout(function () {
if (pendingCopyRequests.has(id)) {
pendingCopyRequests.delete(id);
reject(new Error('Clipboard write timed out'));
}
}, 5000);
});
};

window.addEventListener('message', function (e) {
var msg = e.data;
if (!msg) return;
if (msg.type === 'mcp-inspector-paste-response') {
var target = pendingPasteRequests.get(msg.requestId);
pendingPasteRequests.delete(msg.requestId);
if (target && msg.text) insertTextAt(target, msg.text);
} else if (msg.type === 'mcp-inspector-clipboard-write-result') {
var pending = pendingCopyRequests.get(msg.requestId);
if (!pending) return;
pendingCopyRequests.delete(msg.requestId);
if (msg.ok) pending.resolve();
else pending.reject(new Error(msg.error || 'Clipboard write failed'));
}
});
Comment thread
dan-niles marked this conversation as resolved.
})();

// Function to convert color (hex or rgb/rgba) to HSL format
function colorToHsl(color) {
let r, g, b;
Expand Down Expand Up @@ -69,7 +227,44 @@ const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
return Math.round(h * 360) + ' ' + Math.round(s * 100) + '% ' + Math.round(l * 100) + '%';
}

// Remove existing theme styles when updating
// Fallback to execCommand when webview blocks navigator.clipboard.writeText.
(function patchClipboard() {
const execCopy = (text) => {
const ta = document.createElement('textarea');
ta.value = text == null ? '' : String(text);
ta.setAttribute('readonly', '');
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
document.body.appendChild(ta);
const sel = document.getSelection();
const prevRange = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
ta.select();
ta.setSelectionRange(0, ta.value.length);
let ok = false;
try { ok = document.execCommand('copy'); } catch (_) { ok = false; }
document.body.removeChild(ta);
if (prevRange && sel) { sel.removeAllRanges(); sel.addRange(prevRange); }
return ok;
};
const native = navigator.clipboard && navigator.clipboard.writeText
? navigator.clipboard.writeText.bind(navigator.clipboard)
: null;
const writeText = async (text) => {
if (native) {
try { return await native(text); } catch (_) {}
}
// VS Code webview path: ask the extension host to write the clipboard.
if (typeof window.__mcpInspectorWriteClipboard === 'function') {
try { await window.__mcpInspectorWriteClipboard(text); return; } catch (_) {}
}
if (execCopy(text)) return;
throw new Error('Clipboard copy failed');
};
if (!navigator.clipboard) {
Object.defineProperty(navigator, 'clipboard', { value: {}, configurable: true });
}
try { navigator.clipboard.writeText = writeText; } catch (_) {}
})();

let themeStyleElement = null;

// Listen for theme color messages from parent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,59 @@
enableScripts: WebviewConfig.ENABLE_SCRIPTS,
};

MCPInspectorViewProvider.attachClipboardBridge(webviewView.webview);
Comment thread
dan-niles marked this conversation as resolved.
Outdated
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
} catch (error) {
Logger.error('Failed to resolve webview view', error);
throw error;
}
}

/**
* Wires up the parent-webview side of the clipboard paste bridge.
* The iframe cannot read the system clipboard directly because it's cross-origin
* to the VS Code webview shell; we relay the request through the extension host.
*/
public static attachClipboardBridge(webview: vscode.Webview): vscode.Disposable {
return webview.onDidReceiveMessage(async (msg) => {
if (!msg) return;

Check warning on line 43 in workspaces/mcp-inspector/mcp-inspector-extension/src/MCPInspectorViewProvider.ts

View workflow job for this annotation

GitHub Actions / Build / Build repo

Expected { after 'if' condition
if (msg.type === 'mcp-inspector-request-clipboard-text') {
try {
const text = await vscode.env.clipboard.readText();
webview.postMessage({
type: 'mcp-inspector-clipboard-text',
requestId: msg.requestId,
text,
});
} catch (error) {
Logger.error('Failed to read clipboard for inspector paste', error);
webview.postMessage({
type: 'mcp-inspector-clipboard-text',
requestId: msg.requestId,
text: '',
});
}
} else if (msg.type === 'mcp-inspector-request-clipboard-write') {
try {
await vscode.env.clipboard.writeText(typeof msg.text === 'string' ? msg.text : '');
webview.postMessage({
type: 'mcp-inspector-clipboard-write-result',
requestId: msg.requestId,
ok: true,
});
} catch (error) {
Logger.error('Failed to write clipboard for inspector copy', error);
webview.postMessage({
type: 'mcp-inspector-clipboard-write-result',
requestId: msg.requestId,
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
});
}

/**
* Get HTML content for the webview
*/
Expand Down Expand Up @@ -199,7 +245,7 @@
</style>
</head>
<body>
<iframe id="inspector-iframe" src="${inspectorUrl}" sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
<iframe id="inspector-iframe" src="${inspectorUrl}" sandbox="allow-scripts allow-forms allow-same-origin" allow="clipboard-read; clipboard-write"></iframe>
<script>
(function() {
const iframe = document.getElementById('inspector-iframe');
Expand Down Expand Up @@ -344,6 +390,23 @@
setTimeout(sendThemeColors, 200);
};

// === Clipboard bridge (parent webview side) ===
// Iframe asks us for clipboard read/write -> we ask the extension -> we forward back to iframe.
const vscodeApi = acquireVsCodeApi();
window.addEventListener('message', function(e) {
const msg = e.data;
if (!msg || typeof msg !== 'object') return;
if (msg.type === 'mcp-inspector-request-paste' && e.source === iframe.contentWindow) {
vscodeApi.postMessage({ type: 'mcp-inspector-request-clipboard-text', requestId: msg.requestId });
} else if (msg.type === 'mcp-inspector-request-clipboard-write' && e.source === iframe.contentWindow) {
vscodeApi.postMessage({ type: 'mcp-inspector-request-clipboard-write', requestId: msg.requestId, text: msg.text });
} else if (msg.type === 'mcp-inspector-clipboard-text' && iframe.contentWindow) {
iframe.contentWindow.postMessage({ type: 'mcp-inspector-paste-response', requestId: msg.requestId, text: msg.text }, '*');
} else if (msg.type === 'mcp-inspector-clipboard-write-result' && iframe.contentWindow) {
iframe.contentWindow.postMessage({ type: 'mcp-inspector-clipboard-write-result', requestId: msg.requestId, ok: msg.ok, error: msg.error }, '*');
Comment thread
dan-niles marked this conversation as resolved.
Outdated
}
});

// Listen for VSCode theme changes
const observer = new MutationObserver((mutations) => {
sendThemeColors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ function openInspectorPanel(
dark: vscode.Uri.joinPath(context.extensionUri, 'resources', 'icon-light.svg'),
};

// Wire up the paste bridge so iframe inputs can paste from the system clipboard
context.subscriptions.push(MCPInspectorViewProvider.attachClipboardBridge(currentPanel.webview));

Comment thread
dan-niles marked this conversation as resolved.
Outdated
// Set initial loading content
currentPanel.webview.html = provider.getLoadingHtml();

Expand Down
Loading