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
69 changes: 51 additions & 18 deletions codec_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,12 @@ <h2>MENU</h2>
.mcp-btn-primary:hover:not(:disabled){filter:brightness(1.08)}
.mcp-connected{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;color:#3fa45b}
.mcp-connected .dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
.mcp-hint{font-size:11.5px;color:var(--text-muted)}
/* Sign-in failures stay ON the card. A toast disappears before the user has
read it, which is how "GitHub sign-in does nothing" went unexplained. */
.mcp-err{margin-top:8px;padding:8px 11px;border-radius:6px;font-size:11.5px;line-height:1.55;
background:rgba(212,76,53,.09);border:1px solid rgba(212,76,53,.35);color:#c2452f}
.mcp-err[hidden]{display:none}
.mcp-switch{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0}
.mcp-switch input{opacity:0;width:0;height:0}
.mcp-slider{position:absolute;cursor:pointer;inset:0;background:var(--surface-3);border:1px solid var(--border);border-radius:24px;transition:.2s}
Expand Down Expand Up @@ -1094,27 +1100,38 @@ <h2>MENU</h2>
var lock = '<svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>';
var nm = escHtml(s.name);
// State machine, in the order a user actually experiences it:
// off → nothing (just the toggle)
// on + needs auth + !conn → why + how to fix it (Sign in)
// on + connected → Connected + a way back out (Disconnect)
// on + no auth needed → Connected (nothing to sign into)
// off → nothing (just the toggle)
// on + oauth + !signed in → why + how to fix it (Sign in)
// on + oauth + signed in → Signed in + the way back out (Sign out)
// on + api key → Connected via API key (no session to end)
// on + open endpoint → "No sign-in needed" — NOT "Connected".
// Saying "Connected" for an anonymous endpoint reads as "someone
// signed in here", which is a lie: there is no session, which is also
// why there's no Sign out to offer.
var state = '';
if (s.enabled && s.needs_auth && !s.connected) {
var kind = s.auth === 'api_key' ? 'API key' : 'OAuth sign-in';
if (s.enabled && s.auth === 'oauth' && s.connected) {
state = '<div class="mcp-state">' +
'<span class="mcp-auth">' + lock + 'Needs ' + kind + '</span>' +
(s.auth === 'oauth'
? '<button class="mcp-btn mcp-btn-primary" data-signin="' + nm + '">Sign in</button>'
: '') +
'<span class="mcp-connected"><span class="dot"></span>Signed in</span>' +
'<button class="mcp-btn" data-disconnect="' + nm + '">Sign out</button>' +
'</div>';
} else if (s.enabled && s.connected) {
} else if (s.enabled && s.auth === 'oauth') {
state = '<div class="mcp-state">' +
'<span class="mcp-connected"><span class="dot"></span>Connected</span>' +
(s.auth === 'oauth'
? '<button class="mcp-btn" data-disconnect="' + nm + '">Disconnect</button>'
: '') +
'<span class="mcp-auth">' + lock + 'Needs OAuth sign-in</span>' +
'<button class="mcp-btn mcp-btn-primary" data-signin="' + nm + '">Sign in</button>' +
'</div>';
} else if (s.enabled && s.auth === 'api_key') {
state = s.connected
? '<div class="mcp-state"><span class="mcp-connected"><span class="dot"></span>' +
'Connected via API key</span></div>'
: '<div class="mcp-state"><span class="mcp-auth">' + lock +
'Needs an API key in mcp_servers.json</span></div>';
} else if (s.enabled) {
state = '<div class="mcp-state">' +
'<span class="mcp-connected"><span class="dot"></span>Ready</span>' +
'<span class="mcp-hint">Open endpoint — no sign-in needed</span>' +
'</div>';
}
state += '<div class="mcp-err" data-err="' + nm + '" hidden></div>';
return '<div class="mcp-card">' +
'<div class="mcp-card-main">' +
'<div class="mcp-card-top"><span class="mcp-name">' + nm + '</span>' +
Expand Down Expand Up @@ -1175,19 +1192,35 @@ <h2>MENU</h2>

// OAuth sign-in: opens the server's authorize page in the browser (fastmcp runs
// the flow + caches the token). Can take a while — the user has to authorize.
// Show a sign-in failure ON the card and keep it there. Toasts vanish in a few
// seconds, so a failure the user needs to act on (add a token, retry the browser
// approval) was effectively invisible.
function mcpShowError(name, msg) {
var el = document.querySelector('.mcp-err[data-err="' + (window.CSS && CSS.escape ? CSS.escape(name) : name) + '"]');
if (!el) { showToast(msg, true); return; }
el.textContent = msg;
el.hidden = false;
}

async function mcpSignin(name, btn) {
btn.disabled = true; var orig = btn.textContent; btn.textContent = 'Opening browser…';
var errBox = document.querySelector('.mcp-err[data-err="' + (window.CSS && CSS.escape ? CSS.escape(name) : name) + '"]');
if (errBox) errBox.hidden = true;
try {
var csrf = (document.cookie.match(/codec_csrf=([^;]+)/) || [])[1] || '';
var r = await fetch('/api/mcp/servers/' + encodeURIComponent(name) + '/signin', {
method: 'POST', headers: {'Content-Type': 'application/json', 'x-csrf-token': csrf}
});
var d = await r.json().catch(function() { return {}; });
var okay = r.ok && d.ok !== false;
showToast(d.message || (okay ? 'Signed in.' : 'Sign-in failed.'), !okay);
if (okay) renderConnectors();
if (okay) {
showToast(d.message || 'Signed in.');
renderConnectors();
} else {
mcpShowError(name, d.message || 'Sign-in failed.');
}
} catch (e) {
showToast('Sign-in failed: ' + e, true);
mcpShowError(name, 'Sign-in failed: ' + e);
} finally {
btn.disabled = false; btn.textContent = orig;
}
Expand Down
5 changes: 4 additions & 1 deletion routes/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ async def mcp_signin(name: str):
import asyncio
try:
msg = await asyncio.to_thread(lambda: _mcp_connect().signin_server(name))
ok = not msg.lower().startswith("no mcp server")
# signin_server never raises — it returns an operator-readable message
# for every failure mode (no such server, DCR unsupported, timeout).
# Only the success path starts with "Signed in to".
ok = msg.lower().startswith("signed in to")
return {"ok": ok, "message": msg}
except Exception as e:
return {"ok": False, "message": f"Sign-in failed: {e}"}
Expand Down
2 changes: 1 addition & 1 deletion skills/.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"health_check.py": "01ac95da13770e2419460b4aaf1bd79c6628b6bc5413a5608153b81feee81568",
"imessage_send.py": "862f7031a525faed4cec7cb703ef32f5d8bdc1045326f880d50455baf4cfcf86",
"json_formatter.py": "42a770a1455a574d33e2f96f34c63ae7a83845f70789862e5e5198d0b429fde2",
"mcp_connect.py": "55fd270026b7c83dbe1c9f7653921537050f3e1086f84e35e3f2327daa224e17",
"mcp_connect.py": "099dab866fb826732299ad590148dd8ef58be1c35fd6130bc0e2289c6222036c",
"memory_entities.py": "252ad5861d614b916504b8af6300c4a8c62622c9f6c15238b11d6c6a052bada7",
"memory_history.py": "a2762c03c325517d10907f8aa9511103a5716a29f9e956d48473b817105fb65c",
"memory_save.py": "3d801338bfd0818aaf1e65e692e404af0a0fba6886d26150f0fb66e4b4fde424",
Expand Down
39 changes: 37 additions & 2 deletions skills/mcp_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,46 @@ def _target():
finally:
loop.close()

with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
tools = ex.submit(_target).result(timeout=180)
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
tools = ex.submit(_target).result(timeout=180)
except concurrent.futures.TimeoutError:
return (f"Sign-in to {name} timed out after 3 minutes — the browser "
f"authorization was never completed. Click Sign in and approve "
f"the request in the window that opens.")
except Exception as e:
return _signin_error_message(name, server, e)
return f"Signed in to {name} — {len(tools)} tool(s) available. Drive it by voice/chat: \"list tools on {name}\"."


def _signin_error_message(name: str, server: dict, exc: Exception) -> str:
"""Turn a raw OAuth stack trace into something the operator can act on.

The failure that motivated this: GitHub's MCP server publishes no OAuth
metadata at all, so fastmcp's Dynamic Client Registration call 404s and the
flow dies BEFORE a browser ever opens. The user saw a Sign in button do
literally nothing. Name the cause and the fix instead."""
detail = f"{type(exc).__name__}: {exc}"
log.warning("mcp_connect: sign-in to %s failed — %s", name, detail)
low = detail.lower()
label = server.get("name", name)

if "registration" in low and "404" in low:
return (f"{label} doesn't support automatic app registration, so CODEC "
f"can't sign in through the browser — the sign-in fails before a "
f"window can open. This server needs a personal access token "
f"instead: add it under \"headers\" for \"{name}\" in "
f"{CONFIG_PATH} (e.g. {{\"Authorization\": \"Bearer <token>\"}}) "
f"and set \"auth\": \"api_key\".")
if "registration" in low:
return (f"{label} rejected CODEC's app registration, so the browser "
f"sign-in can't start. Details: {detail[:180]}")
if "timeout" in low or "timed out" in low:
return (f"{label} didn't respond in time. Check the URL in {CONFIG_PATH} "
f"and that you're online.")
return f"Sign-in to {label} failed — {detail[:220]}"


def _fmt_tools(tools) -> str:
out = []
for t in tools:
Expand Down