MCP Tunnel
by leomercier
Verified
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
const term = new Terminal({
cursorBlink: true,
theme: {
background: "#1e1e1e",
foreground: "#f0f0f0",
cursor: "#f0f0f0",
selectionBackground: "#565656"
},
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
fontSize: 14,
lineHeight: 1.2,
scrollback: 5000,
cursorStyle: "block"
});
// Add addons
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon());
term.open(document.getElementById("terminal-container"));
fitAddon.fit();
term.focus();
// Handle window resize
window.addEventListener("resize", () => {
fitAddon.fit();
});
// Clear button functionality
document.getElementById("clear-btn").addEventListener("click", () => {
term.clear();
});
let ws;
let commandBuffer = "";
let commandHistory = [];
let historyPosition = -1;
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
ws.onopen = () => {
term.writeln("\r\nConnected to VM terminal");
term.write("\r\n$ ");
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "output") {
term.write(data.content);
if (!data.content.endsWith("\n")) {
term.write("\r\n");
}
term.write("$ ");
}
};
ws.onclose = () => {
term.writeln("\r\nConnection lost. Reconnecting...");
setTimeout(connectWebSocket, 2000);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
term.writeln("\r\nConnection error. Please try again.");
};
}
function clearCurrentLine() {
const currentLine = commandBuffer;
for (let i = 0; i < currentLine.length; i++) {
term.write("\b \b");
}
return currentLine;
}
term.onKey(({ key, domEvent }) => {
const printable =
!domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;
if (domEvent.keyCode === 13) {
// Enter key
term.write("\r\n");
if (commandBuffer.trim() !== "") {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: "command",
command: commandBuffer
})
);
// Add to history if not duplicate
if (
commandHistory.length === 0 ||
commandHistory[commandHistory.length - 1] !== commandBuffer
) {
commandHistory.push(commandBuffer);
}
historyPosition = -1;
} else {
term.writeln("Not connected to the server.");
term.write("$ ");
}
} else {
term.write("$ ");
}
commandBuffer = "";
} else if (domEvent.keyCode === 8) {
// Backspace
if (commandBuffer.length > 0) {
commandBuffer = commandBuffer.slice(0, -1);
term.write("\b \b");
}
} else if (domEvent.keyCode === 38) {
// Up arrow - History previous
if (commandHistory.length > 0) {
if (historyPosition === -1) {
historyPosition = commandHistory.length - 1;
} else if (historyPosition > 0) {
historyPosition--;
}
clearCurrentLine();
commandBuffer = commandHistory[historyPosition];
term.write(commandBuffer);
}
} else if (domEvent.keyCode === 40) {
// Down arrow - History next
if (historyPosition !== -1) {
if (historyPosition < commandHistory.length - 1) {
historyPosition++;
clearCurrentLine();
commandBuffer = commandHistory[historyPosition];
term.write(commandBuffer);
} else {
historyPosition = -1;
clearCurrentLine();
commandBuffer = "";
}
}
} else if (domEvent.ctrlKey && key.toLowerCase() === "c") {
// Ctrl+C
term.write("^C\r\n$ ");
commandBuffer = "";
} else if (domEvent.ctrlKey && key.toLowerCase() === "l") {
// Ctrl+L (clear)
term.clear();
term.write("$ " + commandBuffer);
} else if (printable) {
commandBuffer += key;
term.write(key);
}
});
window.addEventListener("load", connectWebSocket);