' +
'
' + 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/.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",
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: