Skip to content

feat(ai): save_highlight MCP write tool — completes 7-tool surface (AI-048b)#346

Merged
mrviduus merged 1 commit into
mainfrom
ai-048b-save-highlight
Jun 16, 2026
Merged

feat(ai): save_highlight MCP write tool — completes 7-tool surface (AI-048b)#346
mrviduus merged 1 commit into
mainfrom
ai-048b-save-highlight

Conversation

@mrviduus

Copy link
Copy Markdown
Owner

AI-048b — save_highlight MCP 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.AnchorJson is a required jsonb field; 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 when selectedText appears in the chapter, and may not pin precisely on ambiguous/duplicate text.

Write-safety (adversarial QA — 0 P1)

  • Cannot fire unauthenticated: the POST is built inside AuthorizedRequestAsync, which throws McpUnauthorizedException before constructing the request when the token is Pending/Failed — the write physically can't leave the process (LastRequest == null on the null-token path). No retry layer → no double-write.
  • Synthesized anchor always valid JSON: JsonSerializer escapes quotes/backslash/newline/braces/emoji — 7 adversarial selectedText cases round-trip into valid jsonb. Shape is a superset of the reader's TextAnchor, so it doesn't break findTextByAnchor.
  • Body matches the endpoint DTO 1:1 (editionId, chapterId, anchorJson, color, selectedText, noteText; anchorJson sent as a string).
  • QA fix: color enum trimmed to yellow|green|blue|pink to match the web reader's palette (orange would save but render unstyled).
  • Honest tool description: it's a WRITE on the user's own account, requires sign-in.

Tests — 32 save_highlight (full unit suite 594)

tools/list now 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; no ITool leaked.

Verify

  • dotnet test tests/TextStack.UnitTests → 594 pass
  • dotnet build / dotnet format --verify-no-changes → clean

The 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

…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>
@mrviduus mrviduus merged commit 585c4b2 into main Jun 16, 2026
5 checks passed
@mrviduus mrviduus deleted the ai-048b-save-highlight branch June 16, 2026 21:00
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