From f7fb8d73e57adb18f5f774b1bae4bded37de04c3 Mon Sep 17 00:00:00 2001 From: Mickael Farina Date: Thu, 16 Jul 2026 14:31:28 +0200 Subject: [PATCH 1/2] fix(connector): stop lying about state, and say why sign-in failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things the Connector tab got wrong, all reported from live use: 1. GitHub's "Sign in" did nothing. Its MCP server publishes no OAuth metadata at all, so fastmcp's Dynamic Client Registration 404s and the flow dies BEFORE a browser opens. The error was swallowed. signin_server now maps every failure to an operator-readable message naming the cause and the fix (GitHub needs a PAT under "headers", not OAuth). 2. Hugging Face showed "Connected" though nobody ever signed in. It's an OPEN endpoint (auth: none) — there is no session, which is also why it offered no way out. Saying "Connected" reads as "someone signed in here". It now reads "Ready — open endpoint, no sign-in needed". 3. Failures surfaced as toasts that vanish before they can be read. Sign-in errors now stay ON the card until the next attempt. Also renames Disconnect -> Sign out, matching what the button actually ends. Co-Authored-By: Claude Opus 4.8 --- codec_dashboard.html | 69 ++++++++++++++++++++++++++++++++----------- routes/mcp.py | 5 +++- skills/mcp_connect.py | 39 ++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/codec_dashboard.html b/codec_dashboard.html index f254347..0acc714 100644 --- a/codec_dashboard.html +++ b/codec_dashboard.html @@ -756,6 +756,12 @@

MENU

.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} @@ -1094,27 +1100,38 @@

MENU

var lock = ''; 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 = '
' + - '' + lock + 'Needs ' + kind + '' + - (s.auth === 'oauth' - ? '' - : '') + + 'Signed in' + + '' + '
'; - } else if (s.enabled && s.connected) { + } else if (s.enabled && s.auth === 'oauth') { state = '
' + - 'Connected' + - (s.auth === 'oauth' - ? '' - : '') + + '' + lock + 'Needs OAuth sign-in' + + '' + + '
'; + } else if (s.enabled && s.auth === 'api_key') { + state = s.connected + ? '
' + + 'Connected via API key
' + : '
' + lock + + 'Needs an API key in mcp_servers.json
'; + } else if (s.enabled) { + state = '
' + + 'Ready' + + 'Open endpoint — no sign-in needed' + '
'; } + state += ''; return '
' + '
' + '
' + nm + '' + @@ -1175,8 +1192,20 @@

MENU

// 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', { @@ -1184,10 +1213,14 @@

MENU

}); 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; } diff --git a/routes/mcp.py b/routes/mcp.py index a31b09d..ed4559d 100644 --- a/routes/mcp.py +++ b/routes/mcp.py @@ -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}"} diff --git a/skills/mcp_connect.py b/skills/mcp_connect.py index f81488c..58acd89 100644 --- a/skills/mcp_connect.py +++ b/skills/mcp_connect.py @@ -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 \"}}) " + 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: From d01917264660be82ca9fb817af063b4aa70c8851 Mon Sep 17 00:00:00 2001 From: Mickael Farina Date: Thu, 16 Jul 2026 14:32:30 +0200 Subject: [PATCH 2/2] chore(skills): regenerate D-1 trusted-skill manifest for mcp_connect.py Co-Authored-By: Claude Opus 4.8 --- skills/.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/.manifest.json b/skills/.manifest.json index 22952dd..639498b 100644 --- a/skills/.manifest.json +++ b/skills/.manifest.json @@ -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",