diff --git a/CHANGELOG.md b/CHANGELOG.md index c875bf4..fa17644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,16 @@ 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.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. ## [0.1.17] — 2026-11-12 diff --git a/README.md b/README.md index b974bfe..e061249 100644 --- a/README.md +++ b/README.md @@ -342,16 +342,18 @@ 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 " ``` +`--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)** (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.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/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..ba32148 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 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.', @@ -73,8 +76,16 @@ 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.', + '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 + // 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..a520c94 100644 --- a/frontend/src/screens/Settings.tsx +++ b/frontend/src/screens/Settings.tsx @@ -276,10 +276,11 @@ 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 + const locked = token != null || mint.isPending return ( @@ -295,6 +296,13 @@ 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;