A page that hands you a working connect key - #598
Merged
Conversation
The routes shipped in #596 with nothing in front of them, so a key could only be minted with curl and mcp_access_keys stayed empty. This is the missing half. It lives on the /mcp landing page rather than in profile settings, because that is where someone already is when they want to connect. Signed out, it asks for sign-in and never calls the API — a key is per-account and listing without one is a guaranteed 401. The key is shown once, in a panel that stays put rather than a toast, because the server keeps only a SHA-256 and "copy it now" is a statement of fact. Under it, a claude_desktop_config.json with the key already substituted: a config the reader has to hand-edit is the terminal step this feature exists to remove. Revoking the key on screen clears the panel with it — a dead string left there invites pasting something that authenticates nothing. Naming is skipped, not asked: a date-stamped default beats putting a form between the reader and the thing they came for. The component tests found a real defect, not just covered one. `refresh` cleared `error` on success, so a create that failed at 200ms had its message wiped by the mount listing that succeeded at 300ms — a button that visibly did nothing. Each action now clears the error it is about to replace, and refresh clears nothing. McpKeyEndpointTests covers the lifecycle against a live API — mint, authenticate, revoke, refuse — and asserts `lastUsedAt` actually transitions from null. That assertion is the point of the file: GuestActivityMiddleware is dead code in this repo precisely because nobody ever checked that its stamp was written. Verified locally against a rebuilt container, and it caught two things while I wrote it: /auth/register returns the token in a cookie unless X-Client: mobile is sent, and my first "secret is never echoed" assertion was wrong because the display prefix legitimately starts with tsk_. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
mrviduus
added a commit
that referenced
this pull request
Sep 10, 2026
* fix(web): the insights section never rendered, and its test hid that
`BookInsightsSection` called `@textstack/shared`'s `insightsApi`. Every shared API
client routes through an `authFetch` configured by `initApi()`, and only the
mobile app calls `initApi` — the web has its own client because its token lives in
a cookie, not a header. So the call threw "API not initialized" before reaching
the network, the component swallowed it with `.catch(() => {})`, and
`insights.length === 0` returned null.
The конспект section — the destination of the entire assistant handoff, the place
a conclusion written by an outside assistant is supposed to appear — has never
rendered on the web at all. Silently, for its whole life. Proved with a throwaway
test asserting the shared call rejects on web before fixing it.
Its own unit test was green throughout, because it mocked `@textstack/shared` and
thereby replaced the broken dependency with a working one. A test that mocks the
thing that is broken cannot see it.
Fixed with a web-local `api/insights.ts` on the web's own authFetch, matching how
every other web API module works. The component test now mocks that instead.
Added `noSharedApiOnWeb.test.ts`, which fails if any non-test web source imports a
shared API client — the class of bug rather than this instance. Mutation-tested:
re-adding the original import makes it fail with the file and symbol named, and
removing it makes it pass.
The `.catch(() => {})` stays: silence is right for a network failure on a
supplementary panel. It just cannot tell that apart from a wiring mistake, which
is what the import guard is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* docs(handoff): the state of play at the end of 2026-09-10
Four PRs merged and deployed, and an honest list of what is left in the order it
should be done — plus the three things only the owner can do, one of which
(Android developer verification, 30 Sep) is unrelated to this feature and more
urgent than all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* feat(mobile): a connect screen on the device where the reading happens
The web page shipped in #598; the phone had nothing. The key is account-level, so
one minted here works in Claude Desktop and vice versa — which is why this is
worth having regardless of how the open question about mobile connector support
lands.
`canConnectAssistant` is a new named capability rather than an inline check.
capabilityLiterals.test.ts fails the build on `user?.isGuest`, and rightly: the
reason this one is account-only is sharper than the others. A key reaches the
whole library from outside the app and lives until revoked, while a guest's
identity is three SecureStore keys that vanish with the app — a key minted by one
would outlive every means of revoking it.
apps/mobile/vitest.config.ts is narrowed to `src/lib/**/*.test.ts` and says
outright that components are not in scope, so a test for this screen would not
run. Everything worth asserting therefore moved to
`packages/shared/src/lib/mcpConnect.ts` — the config template, the default key
name, the live-key filter — where both platforms use it and it is covered. The
screen stays dumb. Its tests parse the generated config as JSON rather than
matching substrings: a snippet that is subtly malformed fails the client at
launch with nothing naming the cause.
That move also removes a real risk of drift. The template and the default name
existed only in the web component; two platforms handing out two different
snippets for the same key is two chances to be wrong.
Networking stays per-platform on purpose. Mobile uses the shared api client it
initialises; web keeps its own because its token is a cookie. Mixing those two
layers is exactly the defect that kept the insights section from ever rendering
on the web, and `noSharedApiOnWeb.test.ts` now guards that boundary from the
other side.
Strings went into shared, so the golden fixtures on both sides needed the hand
edit their guards demand — 20 keys, visible in the diff rather than regenerated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#596 shipped the connect-key routes with nothing in front of them, so a key could
only be minted with curl and
mcp_access_keysstayed empty on production. This isthe missing half.
Where it lives, and why
On the
/mcplanding page, not in profile settings — that is where someonealready is when they want to connect.
Signed out it asks for sign-in and never calls the API: a key is per-account,
so listing without one is a guaranteed 401.
The key is shown once, in a panel that stays put rather than a toast, because
the server keeps only a SHA-256 and "copy it now" is a statement of fact. Under it
sits a
claude_desktop_config.jsonwith the key already substituted — a config thereader has to hand-edit is the terminal step this whole feature exists to remove.
Revoking a key clears its panel: a dead string left on screen invites pasting
something that authenticates nothing.
Naming is skipped rather than asked. A date-stamped default beats putting a form
between the reader and the thing they came for.
The tests found a defect rather than covering one
refreshclearederroron success, so a create that failed at 200ms had itsmessage wiped by the mount listing that succeeded at 300ms — a button that visibly
did nothing. Each action now clears the error it is about to replace; refresh
clears nothing.
McpKeyEndpointTests— the one I owed twiceMint → authenticate with the key → revoke → refuse, against a live API, plus
another account's key returning 404 rather than 403.
It asserts
lastUsedAtactually transitions from null. That assertion is thepoint of the file:
GuestActivityMiddlewareis dead code in this repo preciselybecause nobody ever checked that its stamp was written, and the connect key is the
same shape — a middleware that resolves a bearer and stamps a column.
Run locally against a rebuilt container, 3/3. Writing it caught two things:
/auth/registerreturns the token in a cookie unlessX-Client: mobileissent, and my first "the secret is never echoed" assertion was wrong because the
display prefix legitimately starts with
tsk_.Verified
705 web tests, tsc clean, integration 3/3 locally against a live API.
Still missing after this
No mobile screen — the web page exists, the phone has nothing, and the phone is
where the reading happens. That is gated on a question only the owner can answer:
whether the Claude and ChatGPT mobile apps accept custom MCP connectors at all.
🤖 Generated with Claude Code
https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E