Repository navigation
Commit 2fb53e7
The conversation moves to the reader's own assistant (#596)
* feat(mcp): a connect key a reader can actually paste
The remote MCP endpoint had no credential of its own. Its only supported bearer
was a 60-minute access token minted for a different transport, obtained by
installing a .NET CLI, running a device flow in a terminal and copying a JWT out
of a cache file — then repeating within the hour. That is the reason zero
insights were ever written, not the quality of the feature, and it is why the
whole thing is unreachable from a phone.
McpAccessKey is long-lived, revoked in place rather than deleted, and stores only
a SHA-256 of the key via the existing DeviceCodes.HashToken — the same helper
behind PasswordResetToken and DeviceAuthorization. A fast hash is right here:
this is 32 bytes of CSPRNG output, not a human-chosen password.
Resolution is middleware, not a change to GetUserId: that method is synchronous
and called from 102 places, and a key lookup needs the database. It sits ABOVE
the rate limiter, unlike GuestActivityMiddleware which is deliberately below —
highlight-write is the one policy partitioned by user id rather than IP,
precisely because MCP traffic arrives from a single container address, and it
cannot pick that partition before the key is resolved.
LastUsedAt is written at most hourly. An assistant issues one request per tool
call, so writing it per request would put an UPDATE in front of every read of the
user's own library.
The device flow is untouched. It is still correct for the local stdio tool, which
caches a refresh token and renews itself.
Still to come before this is usable: the page that creates and shows a key, on
web and mobile, and an integration test for the revoke-then-401 and LastUsedAt
paths — GuestActivityMiddleware is dead code today precisely because nobody
wrote that second test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* refactor: delete Study Buddy
Zero usage across its whole life, and unlike the other AI surfaces that is not a
product verdict — it is orphaned code. StudyBuddyPanel is referenced exactly once
in the repository, by its own declaration. The selection-toolbar button that
looks like its entry point routes to handleAskAboutThis and always has, and its
aria-label already reads reader.selectionToolbar.askAboutThis, so
reader.selectionToolbar.studyBuddy was a string nothing rendered.
The prop chain stays because the button is real and useful, but it is renamed
onStudyBuddy → onAskAbout (and handleStudyBuddy, StudyBuddyIcon with it). A prop
named after a deleted feature is how the next person concludes the feature is
still there.
BookToolTriggers survives: it is the shared detector behind the Explain
pre-router, not just the agent's tool gate. Its test kept the property it pinned
— a self-contained passage triggers nothing — with the passages inlined now that
the golden corpus is gone.
Its eval runner scored a surface with no entry point, so it goes with it. The
model registry, traces, shadow routing and AiQualityPage stay: they route
translate, explain and the SEO crews, which are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* refactor: delete the Librarian
48 runs, 3.5 cents of inference, one user. It answered "what should I read" in
natural language over the catalog — a real idea, but it is the surface an outside
assistant with the connector attached does better, over a catalog it can already
search with search_books.
Goes with it: the agent, its DTOs, RetrievedCatalog, the eval runner and golden
set, both clients (web api/hooks/components + DiscoverPage, mobile screen and the
entry card on Search), the rate-limit policy, the model route, and 25 web + 22
shared locale keys.
DiscoverMenu stays — it is the genres/authors/books navigation dropdown and only
one of its links pointed here. The /discover route and its legacy redirect go
with the page they resolved to.
LibrarySearchService stays: it is the seam behind SearchLibraryTool and
SearchLibrarySemanticTool, which are their own decision.
The librarian.openBook entry in the locale override registry goes too — it
documented a deliberate web/mobile divergence for a key that no longer exists,
and a registry that outlives its keys stops being a registry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* refactor: delete the in-app book chat
16 conversations and 26 messages over its whole life, from one person — and
book_conversation is upserted on read, so those 16 counted sheet-opens, not
conversations. The engagement number was smaller than it looked, not larger.
The reason it goes is not the usage, and not cost: measured on production, the
six chat surfaces together spent 16 cents. It goes because the conversation is
better somewhere else. The reader already pays for an assistant that beats
anything we can afford to serve, and it already holds their profile and a year of
history, none of which is copyable here. What has to come home is the conclusion,
and BookInsight already carries it.
Removed end to end: the endpoints, ConversationSummarizer, BookChatHistory, both
entities and their tables (DropBookChat is annotated with what it destroys and
the counts taken before writing it), both clients, and the reader chrome that
opened them — AskPanel and AskSheet, the top-bar button on both platforms, the
"Ask" action in the selection bars, and the citation-jump machinery that existed
only to land a chat citation on its page.
useRagIndex goes with the panel that owned it, which settles the guest-403
dead-end it had: a bare catch turned a guest's 403 into "Could not index this
book" plus a Retry that reissued the same 403. Deleting beat fixing.
ask_book and AskEndpoints are untouched. They are the MCP tool's backing route,
not this, and they are their own decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* docs(handoff): record what shipped, what is left, and four decisions the RAG cut surfaces
Written before continuing the deletion so the findings survive the session:
the similar-books rail dies with the chunks (4 editions of 1423 have an
embedding, so it is already blank on 99.7% of pages), three tools are RAG-backed
and two serve features that stay, Tutor loses its grounded example sentence, and
RagIndexStatus is 7 columns on each of two entities.
Also the honest not-done list: the key has no UI on either client, so it can only
be minted with curl today, and its revoke/LastUsedAt integration test is missing
— the exact gap that left GuestActivityMiddleware dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* refactor: delete the retrieval spine
7 books of 1498 were ever indexed — 4 catalog editions of 1423, 3 uploads of 75.
Vision PDF transcription bought those seven and cost $4.14, which is 94% of the
project's $4.39 lifetime LLM spend. Everything downstream of it was reasoning we
were paying to make possible, so that an outside assistant could do it better for
free by reading the chapter as plain text.
Gone: Ai.Rag entire, Application/Rag, Infrastructure/Rag, both chunk tables,
pgvector, editions.embedding, the seven RAG-state columns on each of Edition and
UserBook, the indexing and embedding workers, /ask and /index on both halves,
/admin/rag, the eval runners, and ask_book from the bridge (14 tools → 13).
Two things went that were NOT chat, and both were already dead:
* "Similar books" — SimilarBooksRail is fed by editions.embedding, and 4 of
1423 editions had one, so the rail was blank on 99.7% of book pages.
* Semantic catalog search (HybridCatalogSearch, semantic=true) blended FTS with
the same vectors and degraded to FTS everywhere else. Keyword search is
untouched: it runs on the Postgres FTS search_vector.
Three tools were RAG-backed. search_book was reachable from Explain's
EarlierReference signal — Explain's tool path last fired 2026-06-13, so the
signal now maps to nothing rather than to something broken.
get_example_sentence leaves Tutor without a worked example on a miss; worth
noting VocabularyWord.Sentence already stores the sentence a word was saved
from, so that capability is a rewire away with no retrieval at all.
find_earlier_definition had no live caller.
DropRagSpine is annotated with what it destroys and the counts taken first.
Reading PDFs never went through any of this: the reader renders the original
document (ADR-012) and extraction is deterministic, in TextStack.Extraction,
covered in CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* docs: say 13 tools everywhere, and stop advertising what was deleted
mcp.md is the canonical reference — the NuGet README and the /mcp landing page
both point at it — and it still described 14 tools including ask_book, told
readers a bookId passed to ask_book 404s, and printed a chain ending in it. The
landing page listed the tool by name and the entitlement comments still named
librarian / ask / book chat / study buddy as the paid-inference surface.
mcp.md now says plainly that there is no question-answering tool and why: your
assistant reads get_chapter as plain text and reasons over it better than our
retrieval did, on a subscription you already pay for.
STATUS.md's "AI platform: Phases 1-12 complete, RAG, agents" and "Book Chat: web
+ mobile at parity" sat twelve inches from assistant-handoff.md saying the
opposite. Replaced with what is actually live, plus an In flight entry carrying
the measurements, the not-done list, and the two unapplied destructive
migrations.
Also removed here: ReadingProgressGate, whose only caller was RagContextService.
MaxChapterNumber itself stays and is now documented as deliberately write-only —
it keeps accumulating correctly and get_book_progress is the reader it is waiting
for. Re-deriving that history later would be worse than carrying one int.
One correction to the previous commit's wording: pgvector the extension is NOT
gone. DropRagSpine drops the chunk tables, editions.embedding and its HNSW index;
vector columns remain on vocabulary_words.embedding and the drift centroids, so
Infrastructure still references Pgvector. Noted in assistant-handoff.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
* test: two assertions I missed when ask_book and the chat routes went
Both failed in CI's docker job, and neither is a break: the API came up healthy
and 174 integration tests passed.
McpManifestEndpointTests still expected 14 tools and listed ask_book. I updated
the drift test and the stdio smoke test and missed this one — three places
assert the tool count and only two were in my head.
GuestSessionEndpointTests' paid-inference matrix still named /ask, /me/chat and
/index. They now 404 rather than 403, so the test reported 'paid inference
reachable by a guest' when the truth was 'route deleted'. Reduced to its one
surviving entry, with a comment saying why one entry is still worth a matrix:
the next paid surface has to be added here on the day it ships, and a stale row
reads like a pass.
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>1 parent d8d6e86 commit 2fb53e7
275 files changed
Lines changed: 18548 additions & 22376 deletions
File tree
- apps
- admin/src
- api
- pages
- mobile
- app
- (tabs)
- src
- components
- librarian
- reader
- hooks
- lib
- __fixtures__
- web/src
- api
- __tests__
- components
- book
- __tests__
- librarian
- __tests__
- reader
- __tests__
- hooks
- lib
- locales
- __tests__
- __fixtures__
- pages
- __tests__
- styles
- types
- backend/src
- Ai
- TextStack.Ai.Agents
- TextStack.Ai.EvalSuite
- TextStack.Ai.Mcp
- Auth
- Http
- Tools
- TextStack.Ai.Rag
- Api
- Endpoints
- Extensions
- Middleware
- Application
- Agents
- Ai
- Auth
- BookChat
- Books
- Common/Interfaces
- Entitlements
- Rag
- Recommendations
- Search
- Tools
- UserBooks
- Contracts
- Admin
- Agents
- Books
- Mcp
- UserBooks
- Domain
- Entities
- Enums
- Infrastructure
- Migrations
- Persistence
- Rag
- Worker
- Services
- docs
- 05-features
- tests
- TextStack.Ai.Mcp.Tests
- TextStack.AiEvals
- TextStack.IntegrationTests
- TextStack.UnitTests
- Fakes
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
244 | | - | |
| 244 | + | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| |||
514 | 514 | | |
515 | 515 | | |
516 | 516 | | |
517 | | - | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
518 | 520 | | |
519 | 521 | | |
520 | 522 | | |
| |||
524 | 526 | | |
525 | 527 | | |
526 | 528 | | |
527 | | - | |
| 529 | + | |
528 | 530 | | |
529 | 531 | | |
530 | 532 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
585 | 585 | | |
586 | 586 | | |
587 | 587 | | |
588 | | - | |
589 | | - | |
590 | | - | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
601 | | - | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
611 | 588 | | |
612 | 589 | | |
613 | 590 | | |
| |||
1367 | 1344 | | |
1368 | 1345 | | |
1369 | 1346 | | |
1370 | | - | |
1371 | | - | |
1372 | | - | |
1373 | | - | |
1374 | | - | |
1375 | 1347 | | |
1376 | 1348 | | |
1377 | 1349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | 16 | | |
18 | 17 | | |
19 | 18 | | |
| |||
445 | 444 | | |
446 | 445 | | |
447 | 446 | | |
448 | | - | |
| 447 | + | |
449 | 448 | | |
450 | 449 | | |
451 | 450 | | |
| |||
807 | 806 | | |
808 | 807 | | |
809 | 808 | | |
810 | | - | |
811 | | - | |
812 | 809 | | |
813 | 810 | | |
814 | 811 | | |
| |||
896 | 893 | | |
897 | 894 | | |
898 | 895 | | |
899 | | - | |
900 | | - | |
901 | | - | |
902 | | - | |
903 | | - | |
904 | | - | |
905 | | - | |
906 | | - | |
907 | | - | |
908 | | - | |
909 | | - | |
910 | 896 | | |
911 | 897 | | |
912 | 898 | | |
| |||
958 | 944 | | |
959 | 945 | | |
960 | 946 | | |
961 | | - | |
962 | | - | |
963 | | - | |
964 | | - | |
965 | | - | |
966 | | - | |
967 | | - | |
968 | | - | |
969 | 947 | | |
970 | 948 | | |
971 | 949 | | |
| |||
1058 | 1036 | | |
1059 | 1037 | | |
1060 | 1038 | | |
1061 | | - | |
1062 | | - | |
1063 | | - | |
1064 | | - | |
1065 | | - | |
1066 | | - | |
1067 | | - | |
1068 | | - | |
1069 | | - | |
1070 | | - | |
1071 | | - | |
1072 | | - | |
1073 | | - | |
1074 | | - | |
1075 | | - | |
1076 | | - | |
1077 | | - | |
1078 | | - | |
1079 | | - | |
1080 | | - | |
1081 | | - | |
1082 | | - | |
1083 | | - | |
1084 | | - | |
1085 | | - | |
1086 | | - | |
1087 | | - | |
1088 | | - | |
1089 | | - | |
1090 | | - | |
1091 | | - | |
1092 | | - | |
1093 | | - | |
1094 | | - | |
1095 | | - | |
1096 | | - | |
1097 | | - | |
1098 | | - | |
1099 | | - | |
1100 | | - | |
1101 | | - | |
1102 | 1039 | | |
1103 | 1040 | | |
1104 | 1041 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| |||
323 | 323 | | |
324 | 324 | | |
325 | 325 | | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
348 | | - | |
349 | 326 | | |
350 | 327 | | |
351 | 328 | | |
| |||
536 | 513 | | |
537 | 514 | | |
538 | 515 | | |
539 | | - | |
540 | | - | |
541 | | - | |
542 | | - | |
543 | | - | |
544 | | - | |
545 | | - | |
546 | | - | |
547 | | - | |
548 | | - | |
549 | | - | |
550 | | - | |
551 | | - | |
552 | 516 | | |
553 | 517 | | |
554 | 518 | | |
| |||
0 commit comments