feat(ai): save_highlight MCP write tool — completes 7-tool surface (AI-048b)#346
Merged
Conversation
…I-048b) The first MCP write tool, now safe on AI-050b's per-user device-flow token (never a shared static secret). POST /me/highlights on the user's own identity. The MCP client has no DOM, so the bridge synthesizes a W3C text-quote anchor (exact=selectedText, chapterId, source:"mcp") into the required AnchorJson jsonb field — NO backend change (the endpoint already stores any valid-JSON anchor; web/mobile still send their full DOM anchor). The highlight always saves + is listable via list_my_highlights; it re-anchors in the web reader when selectedText appears in the chapter, may not pin precisely on ambiguous text (documented). Write-safety (adversarial QA, 0 P1): the POST is built inside AuthorizedRequestAsync, which throws before constructing the request when the token is Pending/Failed — the write physically cannot fire unauthenticated (LastRequest null). Synthesized anchor is always valid JSON (JsonSerializer escapes quotes/backslash/newline/emoji — 7 adversarial selectedText cases round-trip). Request body matches CreateHighlightRequest 1:1 (anchorJson sent as a string). No retry layer → no double-write. QA fix: color enum trimmed to yellow|green|blue|pink to match the web reader's palette (orange would render unstyled). 32 save_highlight tests; full unit suite 594 green; StudyBuddy set-equality green. Completes the Phase 8 7-tool surface. Co-Authored-By: Claude Opus 4.8 (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.
AI-048b —
save_highlightMCP write tool (Phase 8, completes the 7-tool surface)The first MCP write tool, now safe on AI-050b's per-user device-flow token (never a shared static secret) — it writes to the user's own account via
POST /me/highlights.The anchor problem (approach b — no backend change)
Highlight.AnchorJsonis a requiredjsonbfield; the web reader fills it with a W3C text-quote anchor ({prefix, exact, suffix, startOffset, endOffset, chapterId}). An MCP client has no DOM, so the bridge synthesizes a valid anchor from the agent's args (exact = selectedText, empty prefix/suffix,chapterId,source:"mcp"). The endpoint already stores any valid-JSON anchor opaquely, and web/mobile still send their full DOM anchor → no backend change.Limitation (documented): the highlight always saves and is retrievable via
list_my_highlights; it re-anchors in the web reader whenselectedTextappears in the chapter, and may not pin precisely on ambiguous/duplicate text.Write-safety (adversarial QA — 0 P1)
AuthorizedRequestAsync, which throwsMcpUnauthorizedExceptionbefore constructing the request when the token isPending/Failed— the write physically can't leave the process (LastRequest == nullon the null-token path). No retry layer → no double-write.JsonSerializerescapes quotes/backslash/newline/braces/emoji — 7 adversarialselectedTextcases round-trip into validjsonb. Shape is a superset of the reader'sTextAnchor, so it doesn't breakfindTextByAnchor.editionId, chapterId, anchorJson, color, selectedText, noteText;anchorJsonsent as a string).colorenum trimmed toyellow|green|blue|pinkto match the web reader's palette (orangewould save but render unstyled).Tests — 32 save_highlight (full unit suite 594)
tools/listnow exposes 7 tools; happy-path authorized POST shape + Bearer + synthesized-anchor body + 201→id; adversarial-text anchor escaping (7 cases); arg validation (missing/bad-guid/oversized/bad-color/extra-prop → IsError, never HTTP); unauthenticated → auth-required, no write; 401/transport/empty-body → clean error; cancellation propagates. StudyBuddy set-equality green; noIToolleaked.Verify
dotnet test tests/TextStack.UnitTests→ 594 passdotnet build/dotnet format --verify-no-changes→ cleanThe 7-tool MCP surface (search_books, get_book, get_chapter, list_my_highlights, save_highlight, list_my_vocabulary, ask_book) is now complete. Remaining Phase 8: AI-049 (SSE), AI-051 (npm), AI-052 (landing), AI-053 (integration tests).
🤖 Generated with Claude Code