From e39409e0f8aba9b13cf9e67dd5532dc51667dc51 Mon Sep 17 00:00:00 2001 From: Arden de Raaij Date: Fri, 13 Nov 2026 17:40:52 +0000 Subject: [PATCH 1/2] Fix the coach setup instructions found in review of #17 Node.js 20 is the minimum for the Claude Desktop path: `npx mcp-remote` always takes the latest release, which depends on undici 7, and that needs Node 20.18.1. The dashboard and the README both said 18. The Claude Code command gains `--scope user`. The default scope registers the server for the folder the command is run in, so running it in the home folder and then opening a project showed no server. In the dashboard the command is also one line now: backslash continuations only work in bash and zsh, and it gets pasted into PowerShell and cmd. Every http address gets --allow-http. mcp-remote exempts only `localhost` and `127.0.0.1`, not `[::1]`, and the flag is harmless on those, so the dashboard no longer mirrors that list. Once a token exists the client choice is locked. The token is named after the client it was created for, and switching afterwards showed another client's setup carrying that name, so the token list misreported what was using it. Another app now means another token. The health test compared the response with the constant the route reads, so it could not fail; it now patches both values. Two CSS rules that .cli-box was overriding take effect, and the client chips report their pressed state. --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- backend/tests/test_health.py | 9 ++++++--- frontend/src/lib/coachSetup.ts | 19 +++++++++++++------ frontend/src/screens/Settings.tsx | 14 ++++++++++---- frontend/src/styles/settings.css | 3 ++- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c875bf4..1ede59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,15 @@ System screen. and it creates a token and shows the setup with the server address and token already filled in, ready to copy. Hidden for admins and when `MCP_ENABLED=false`; `/api/health` now reports `mcp` so the dashboard can - tell. + tell. Each app gets its own token, named after it, so one can be revoked + without touching the others. + +### Fixed + +- **Setup instructions for Claude Desktop and Claude Code.** Node.js 20 is the + minimum (the `mcp-remote` helper no longer runs on 18), and the Claude Code + command now uses `--scope user`: without it the server was registered only + for the folder the command happened to be run in. ## [0.1.17] — 2026-11-12 diff --git a/README.md b/README.md index b974bfe..2c0698a 100644 --- a/README.md +++ b/README.md @@ -342,7 +342,7 @@ The quickest way: in the dashboard, **Settings → AI coach → Connect** create **Claude Code** ```bash -claude mcp add training --transport http \ +claude mcp add training --scope user --transport http \ http://:8001/mcp \ --header "Authorization: Bearer " ``` @@ -351,7 +351,7 @@ claude mcp add training --transport http \ Claude Desktop cannot send a bearer token to a remote server by itself, so a small helper ([`mcp-remote`](https://github.com/geelen/mcp-remote)) does it. Do not use **Settings → Connectors → Add custom connector**: that flow connects from Anthropic's servers and expects OAuth, so it does not work with this server (see [Reachability](#reachability)). -1. **Install [Node.js](https://nodejs.org)** (18 or newer) if you don't have it. The helper runs through `npx`, which comes with Node. +1. **Install [Node.js](https://nodejs.org)** (20 or newer) if you don't have it. The helper runs through `npx`, which comes with Node. 2. **Create a token.** In the Loopback dashboard go to **Settings → New token**, name it (e.g. "Claude Desktop") and copy it. It is shown only once. 3. **Open the config file.** In Claude Desktop: **Settings → Developer → Edit config**. This opens `claude_desktop_config.json`: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index 2633ac8..cc7e0e3 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -1,5 +1,6 @@ """The unauthenticated health endpoint — the iOS app's version handshake.""" +import pytest from fastapi.testclient import TestClient from app.main import app @@ -15,8 +16,10 @@ def test_health_reports_version(): assert body["database"] == "ok" -def test_health_reports_whether_the_coach_endpoint_is_served(): +@pytest.mark.parametrize("enabled", [True, False]) +def test_health_reports_whether_the_coach_endpoint_is_served(monkeypatch, enabled): # The dashboard hides its "connect your coach" setup when this is false. - from app.mcp_mount import MCP_ENABLED + # Patched where the route reads it: health.py imports the value by name. + monkeypatch.setattr("app.routes.health.MCP_ENABLED", enabled) - assert TestClient(app).get("/api/health").json()["mcp"] is MCP_ENABLED + assert TestClient(app).get("/api/health").json()["mcp"] is enabled diff --git a/frontend/src/lib/coachSetup.ts b/frontend/src/lib/coachSetup.ts index f13241b..80f19bc 100644 --- a/frontend/src/lib/coachSetup.ts +++ b/frontend/src/lib/coachSetup.ts @@ -33,11 +33,14 @@ export function coachUrl(origin: string): string { return `${origin.replace(/\/+$/, '')}/mcp` } -/** mcp-remote refuses plain http to anything but the local machine. */ +/** + * mcp-remote refuses plain http unless told otherwise. It exempts only the + * names `localhost` and `127.0.0.1` (not `[::1]`), and the flag is harmless + * there, so every http address gets it rather than mirroring that list. + */ export function needsAllowHttp(url: string): boolean { try { - const u = new URL(url) - return u.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(u.hostname) + return new URL(url).protocol === 'http:' } catch { return false } @@ -58,7 +61,7 @@ export function buildCoachSetup(client: CoachClientId, url: string, token: strin if (needsAllowHttp(url)) args.push('--allow-http') return { steps: [ - 'Install Node.js 18 or newer from nodejs.org if you don’t have it.', + 'Install Node.js 20 or newer from nodejs.org if you don’t have it.', 'In Claude Desktop, open Settings → Developer → Edit config.', 'Paste the block below. If the file already has "mcpServers", add only the "training" entry inside it.', 'Quit Claude Desktop completely and reopen it. Settings → Developer should show "training" as Running.', @@ -73,8 +76,12 @@ export function buildCoachSetup(client: CoachClientId, url: string, token: strin } case 'claude-code': return { - steps: ['Run this in a terminal.', 'Start Claude Code and ask “What did I run this week?”'], - snippet: `claude mcp add ${SERVER_NAME} --transport http \\\n ${url} \\\n --header "Authorization: ${bearer}"`, + steps: ['Run this in a terminal.', 'Start Claude Code in any folder and ask “What did I run this week?”'], + // One line, no continuations: a trailing backslash only means "next + // line" in bash and zsh, and this is pasted into PowerShell and cmd + // too. --scope user, because the default registers the server for the + // current folder only. + snippet: `claude mcp add ${SERVER_NAME} --scope user --transport http ${url} --header "Authorization: ${bearer}"`, snippetHasToken: true, } case 'cursor': diff --git a/frontend/src/screens/Settings.tsx b/frontend/src/screens/Settings.tsx index 4750981..fbb613e 100644 --- a/frontend/src/screens/Settings.tsx +++ b/frontend/src/screens/Settings.tsx @@ -276,7 +276,7 @@ function CopyButton({ text, label }: { text: string; label: string }) { function ConnectCoachModal({ onClose }: { onClose: () => void }) { const mint = useMintToken() const [client, setClient] = useState('claude-desktop') - const clientLabel = COACH_CLIENTS.find((c) => c.id === client)?.label ?? '' + const clientName = COACH_CLIENTS.find((c) => c.id === client)?.label ?? '' const url = coachUrl(window.location.origin) const token = mint.data?.token const setup = token ? buildCoachSetup(client, url, token) : null @@ -295,6 +295,12 @@ function ConnectCoachModal({ onClose }: { onClose: () => void }) { diff --git a/frontend/src/styles/settings.css b/frontend/src/styles/settings.css index 5e59d91..6807814 100644 --- a/frontend/src/styles/settings.css +++ b/frontend/src/styles/settings.css @@ -164,7 +164,8 @@ .connect-steps li + li { margin-top: 5px; } -.connect-snippet { +/* Doubled class: these override .cli-box, which is declared further down. */ +.cli-box.connect-snippet { margin-top: 0; max-height: 260px; overflow: auto; From 19650fc28a42d43ef382d686075b4bb4bae55564 Mon Sep 17 00:00:00 2001 From: Arden de Raaij Date: Fri, 13 Nov 2026 17:46:18 +0000 Subject: [PATCH 2/2] Close two gaps the second review found in the coach setup The client chips locked only once the token existed, so switching client while the create request was in flight still produced one client's setup under a token named after another. They now lock from the moment the request is sent. With --scope user, running `claude mcp add training` a second time is refused everywhere ("already exists in user config"), which is exactly what reconnecting after rotating a token runs into. The dashboard steps and the README now say to remove the old entry first. The Node.js minimum is stated as 20.18, which is what mcp-remote's dependency declares, and the changelog no longer claims the helper was observed failing on 18. --- CHANGELOG.md | 5 +++-- README.md | 4 +++- frontend/src/lib/coachSetup.ts | 8 ++++++-- frontend/src/screens/Settings.tsx | 12 +++++++----- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ede59f..fa17644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,9 @@ System screen. ### Fixed -- **Setup instructions for Claude Desktop and Claude Code.** Node.js 20 is the - minimum (the `mcp-remote` helper no longer runs on 18), and the Claude Code +- **Setup instructions for Claude Desktop and Claude Code.** Node.js 20.18 is + the stated minimum (what the `mcp-remote` helper's dependencies now declare; + the instructions said 18), and the Claude Code command now uses `--scope user`: without it the server was registered only for the folder the command happened to be run in. diff --git a/README.md b/README.md index 2c0698a..e061249 100644 --- a/README.md +++ b/README.md @@ -347,11 +347,13 @@ claude mcp add training --scope user --transport http \ --header "Authorization: Bearer " ``` +`--scope user` makes the server available in every folder. To replace an existing entry (for example after rotating the token), run `claude mcp remove training --scope user` first; `add` refuses to overwrite. + **Claude Desktop** Claude Desktop cannot send a bearer token to a remote server by itself, so a small helper ([`mcp-remote`](https://github.com/geelen/mcp-remote)) does it. Do not use **Settings → Connectors → Add custom connector**: that flow connects from Anthropic's servers and expects OAuth, so it does not work with this server (see [Reachability](#reachability)). -1. **Install [Node.js](https://nodejs.org)** (20 or newer) if you don't have it. The helper runs through `npx`, which comes with Node. +1. **Install [Node.js](https://nodejs.org)** (20.18 or newer) if you don't have it. The helper runs through `npx`, which comes with Node. 2. **Create a token.** In the Loopback dashboard go to **Settings → New token**, name it (e.g. "Claude Desktop") and copy it. It is shown only once. 3. **Open the config file.** In Claude Desktop: **Settings → Developer → Edit config**. This opens `claude_desktop_config.json`: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` diff --git a/frontend/src/lib/coachSetup.ts b/frontend/src/lib/coachSetup.ts index 80f19bc..ba32148 100644 --- a/frontend/src/lib/coachSetup.ts +++ b/frontend/src/lib/coachSetup.ts @@ -61,7 +61,7 @@ export function buildCoachSetup(client: CoachClientId, url: string, token: strin if (needsAllowHttp(url)) args.push('--allow-http') return { steps: [ - 'Install Node.js 20 or newer from nodejs.org if you don’t have it.', + 'Install a current Node.js (20.18 or newer) from nodejs.org if you don’t have it.', 'In Claude Desktop, open Settings → Developer → Edit config.', 'Paste the block below. If the file already has "mcpServers", add only the "training" entry inside it.', 'Quit Claude Desktop completely and reopen it. Settings → Developer should show "training" as Running.', @@ -76,7 +76,11 @@ export function buildCoachSetup(client: CoachClientId, url: string, token: strin } case 'claude-code': return { - steps: ['Run this in a terminal.', 'Start Claude Code in any folder and ask “What did I run this week?”'], + steps: [ + 'Run this in a terminal.', + 'If it says "training" already exists, run "claude mcp remove training --scope user" and try again.', + 'Start Claude Code in any folder and ask “What did I run this week?”', + ], // One line, no continuations: a trailing backslash only means "next // line" in bash and zsh, and this is pasted into PowerShell and cmd // too. --scope user, because the default registers the server for the diff --git a/frontend/src/screens/Settings.tsx b/frontend/src/screens/Settings.tsx index fbb613e..a520c94 100644 --- a/frontend/src/screens/Settings.tsx +++ b/frontend/src/screens/Settings.tsx @@ -280,6 +280,7 @@ function ConnectCoachModal({ onClose }: { onClose: () => void }) { const url = coachUrl(window.location.origin) const token = mint.data?.token const setup = token ? buildCoachSetup(client, url, token) : null + const locked = token != null || mint.isPending return ( @@ -296,11 +297,12 @@ function ConnectCoachModal({ onClose }: { onClose: () => void }) { key={c.id} className={`filter-chip${client === c.id ? ' on' : ''}`} aria-pressed={client === c.id} - // The token is named after the client it was created for, so once - // it exists the choice is fixed: another app gets its own token, - // which keeps each one identifiable and revocable on its own. - disabled={token != null && client !== c.id} - style={token != null && client !== c.id ? { opacity: 0.4, cursor: 'default' } : undefined} + // The token is named after the client it was created for, so from + // the moment the request is sent the choice is fixed: another app + // gets its own token, which keeps each one identifiable and + // revocable on its own. + disabled={locked && client !== c.id} + style={locked && client !== c.id ? { opacity: 0.4, cursor: 'default' } : undefined} onClick={() => setClient(c.id)} > {c.label}