Skip to content

A page that hands you a working connect key - #598

Merged
mrviduus merged 1 commit into
mainfrom
feat/connect-assistant-page
Sep 10, 2026
Merged

A page that hands you a working connect key#598
mrviduus merged 1 commit into
mainfrom
feat/connect-assistant-page

Conversation

@mrviduus

Copy link
Copy Markdown
Owner

#596 shipped the connect-key routes with nothing in front of them, so a key could
only be minted with curl and mcp_access_keys stayed empty on production. This is
the missing half.

Where it lives, and why

On the /mcp landing page, not in profile settings — 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,
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.json with the key already substituted — a config the
reader 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

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; refresh
clears nothing.

McpKeyEndpointTests — the one I owed twice

Mint → authenticate with the key → revoke → refuse, against a live API, plus
another account's key returning 404 rather than 403.

It 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, 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/register returns the token in a cookie unless X-Client: mobile is
sent, 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

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
mrviduus merged commit 2c91c17 into main Sep 10, 2026
10 checks passed
@mrviduus
mrviduus deleted the feat/connect-assistant-page branch September 10, 2026 22:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant