Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 25 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,11 @@
<button class="fchip" id="mcpToggle">Start</button>
</div>
<div class="mcp-popbody">
<div class="mcp-row" id="mcpPublicRow" hidden>
<span class="mcp-label">Public URL</span>
<code class="mcp-value" id="mcpPublicUrl"></code>
<button type="button" class="fchip" id="mcpPublicCopy">Copy</button>
</div>
<div class="mcp-row">
<span class="mcp-label">Connector URL</span>
<code class="mcp-value" id="mcpUrl">http://127.0.0.1:5758/mcp</code>
Expand Down Expand Up @@ -902,7 +907,11 @@ <h2>Messages</h2><span class="count" id="cMessages"></span>
const menu = $('#connectorMenu');
menu.hidden = !open;
$('#connectorBtn').setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) loadBridge();
if (open) {
bridgeInfo.publicUrl = null;
renderBridge({running: bridgeInfo.running});
loadBridge();
}
}

/* ================= mock state (preview fallback) ================= */
Expand Down Expand Up @@ -1069,7 +1078,7 @@ <h2>Messages</h2><span class="count" id="cMessages"></span>
const expanded = new Set(); // recent ids with expanded summary
const seenMsg = new Map(); // msg id -> pollN when first seen
let drawerTaskId = null;
let bridgeInfo = {running: false, port: 5758, localEndpoint: 'http://127.0.0.1:5758/mcp', connectorUrl: 'http://127.0.0.1:5758/mcp', token: null};
let bridgeInfo = {running: false, port: 5758, localEndpoint: 'http://127.0.0.1:5758/mcp', connectorUrl: 'http://127.0.0.1:5758/mcp', publicUrl: null, token: null};
let mcpTokenVisible = false;

function changed(key, sig){
Expand Down Expand Up @@ -1135,6 +1144,15 @@ <h2>Messages</h2><span class="count" id="cMessages"></span>
function bridgeConnectorUrl(){
return bridgeInfo.connectorUrl || bridgeEndpoint();
}
function bridgePublicUrl(){
return bridgeInfo.publicUrl || null;
}
function bridgeDisplayPublicUrl(){
const full = bridgePublicUrl();
if (!full) return null;
if (mcpTokenVisible) return full;
return full.replace(/([?&]key=)[^&]*/i, '$1...');
}
function renderBridge(state){
const running = typeof state?.running === 'boolean' ? state.running : bridgeInfo.running === true;
bridgeInfo.running = running;
Expand All @@ -1145,6 +1163,10 @@ <h2>Messages</h2><span class="count" id="cMessages"></span>
status.className = 'pill ' + (running ? 'st-ok live' : 'st-mut');
status.innerHTML = '<i></i>' + (running ? 'running' : 'stopped');
$('#mcpToggle').textContent = running ? 'Stop' : 'Start';
const publicUrl = bridgePublicUrl();
const publicRow = $('#mcpPublicRow');
publicRow.hidden = !publicUrl;
$('#mcpPublicUrl').textContent = bridgeDisplayPublicUrl() || '';
Comment thread
aiedwardyi marked this conversation as resolved.
$('#mcpUrl').textContent = bridgeDisplayUrl();
const token = mcpTokenVisible ? (bridgeInfo.token || 'Unavailable') : 'Hidden';
$('#mcpToken').textContent = token;
Expand Down Expand Up @@ -1726,6 +1748,7 @@ <h2>Messages</h2><span class="count" id="cMessages"></span>
renderMessages(lastState?.messages);
});
$('#mcpToggle').addEventListener('click', toggleBridge);
$('#mcpPublicCopy').addEventListener('click', e => copyText(bridgePublicUrl(), e.currentTarget));
$('#mcpUrlCopy').addEventListener('click', e => copyText(bridgeConnectorUrl(), e.currentTarget));
$('#mcpReveal').addEventListener('click', toggleMcpToken);
$('#mcpTokenCopy').addEventListener('click', e => copyText(bridgeInfo.token, e.currentTarget));
Expand Down
76 changes: 73 additions & 3 deletions src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,86 @@ function readBridgeToken() {
}
}

function bridgeDetails() {
const quickTunnelPorts = [20241, 20242, 20243, 20244, 20245];
const quickTunnelProbeMs = 400;

function probeQuickTunnelPort(port) {
return new Promise((resolve) => {
let settled = false;
let req;
const done = (value) => {
if (settled) return;
settled = true;
clearTimeout(wall);
resolve(value);
};
const wall = setTimeout(() => {
try { req.destroy(); } catch { /* already closed */ }
done(null);
}, quickTunnelProbeMs);

req = http.get({
host: '127.0.0.1',
port,
path: '/quicktunnel',
timeout: quickTunnelProbeMs,
}, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
body += chunk;
if (body.length > 4096) {
req.destroy();
done(null);
}
});
res.on('end', () => {
try {
const data = JSON.parse(body);
const raw = typeof data?.hostname === 'string' ? data.hostname.trim() : '';
if (!raw) {
done(null);
return;
}
if (raw.includes('://')) {
done(new URL(raw).hostname || null);
return;
}
done(raw.replace(/\/+$/, '') || null);
} catch {
done(null);
}
});
});
req.on('error', () => done(null));
req.on('timeout', () => {
req.destroy();
done(null);
});
});
}

async function detectQuickTunnelHostname() {
const results = await Promise.all(quickTunnelPorts.map(probeQuickTunnelPort));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return results.find((hostname) => hostname) || null;
Comment thread
aiedwardyi marked this conversation as resolved.
}
Comment thread
aiedwardyi marked this conversation as resolved.

async function bridgeDetails() {
const port = bridgePort();
const token = readBridgeToken();
const localEndpoint = `http://127.0.0.1:${port}/mcp`;
const connectorUrl = token ? `${localEndpoint}?key=${encodeURIComponent(token)}` : localEndpoint;
const hostname = await detectQuickTunnelHostname();
const publicUrl = hostname
? (token ? `https://${hostname}/mcp?key=${encodeURIComponent(token)}` : `https://${hostname}/mcp`)
: null;

return {
running: bridgeRunning(),
port,
localEndpoint,
connectorUrl: token ? `${localEndpoint}?key=${encodeURIComponent(token)}` : localEndpoint,
connectorUrl,
publicUrl,
token,
};
}
Expand Down Expand Up @@ -1955,7 +2025,7 @@ async function handleRequest(req, res) {
}

if (req.method === 'GET' && requestPath === '/api/bridge') {
sendJson(res, 200, bridgeDetails());
sendJson(res, 200, await bridgeDetails());
return;
}

Expand Down