An MCP server that drives VitalSource Bookshelf through a real Chromium browser via Playwright. It exposes tools, resources, and a prompt so an agent (e.g. Claude Code) can list your books, read chapter text, and save notes — all against textbooks you already own through VitalSource.
There is no public VitalSource API for personal libraries, so this works by automating the web reader. Selectors live in one file (src/vitalsource/selectors.ts) and may need patching if VitalSource ships a UI change.
Disclaimer — read before using
This project is for personal and educational use only. It is not affiliated with, endorsed by, or in any way connected to VitalSource Technologies. Automating a browser against a paid subscription service may violate VitalSource's Terms of Service. You use this software entirely at your own risk.
Requires Node 20+.
npm install
npx playwright install chromium # one-time — Playwright needs its own Chromium
npm run buildThe first time you use the server you need to sign in interactively so the session is persisted to disk:
- Have your MCP client call the
logintool (or call it without arguments; it defaults toautomode). - A visible Chromium window opens and navigates to the VitalSource login page.
- Sign in normally — Google/Apple SSO and two-factor auth all work because you are driving a real browser.
- The window closes automatically once you reach the library shelf. The session is written to
user-data/auth-state.json.
Subsequent tool calls reuse the persisted session and run headless. If the session expires, call login again.
If your VitalSource account uses a native email + password (no SSO, no 2FA), you can set credentials in the server environment:
VS_MCP_EMAIL=you@example.com
VS_MCP_PASSWORD=your_password
When both are set, ensureLoggedIn() signs in automatically without opening a window. This mode does not work for accounts that use Google, Apple, or institution SSO — use the interactive flow instead.
Every tool returns both a text content block and a structuredContent object.
| Tool | Inputs | Structured output |
|---|---|---|
login |
mode? ("auto" | "credential" | "interactive", default "auto") |
{ status: string, mode: "credential" | "interactive" } |
list_books |
— | { books: [{ bookId, title, edition, href }] } |
get_toc |
bookId |
{ bookId, entries: [{ chapterId, title, page }] } |
read_chapter |
bookId, chapterId, format? ("md" | "text", default "md"), maxPages? (int, max 1000), maxChars? (int, max 1 000 000) |
{ bookId, chapterId, pageCount, charCount, truncated, content } |
save_notes |
filename, content, subdir?, book?, chapter? |
{ path, bytes } |
login — "auto" mode uses credential login when VS_MCP_EMAIL + VS_MCP_PASSWORD are set, otherwise falls back to interactive. Pass "credential" or "interactive" to force a mode.
get_toc — chapterId is a tocIndexN uuid (e.g. "tocIndex9"), not a CFI. This same id is what read_chapter expects.
read_chapter — navigates to the chapter and walks its pages via ArrowRight. maxPages caps how many page turns are attempted (default 200). maxChars caps the returned text (default 100 000 chars). truncated is true if either limit was hit.
save_notes — writes a UTF-8 markdown file under the notes root (default <project>/notes/). book and chapter are added to YAML frontmatter if provided. subdir is restricted to inside the notes root; path traversal is rejected.
Resources expose the same data as tools, usable without explicit tool calls.
| URI | MIME type | Returns |
|---|---|---|
vitalsource://library |
application/json |
Full book list (same shape as list_books) |
vitalsource://book/{bookId}/toc |
application/json |
TOC entries for the given book |
vitalsource://book/{bookId}/chapter/{chapterId} |
text/markdown |
Chapter content as Markdown |
study_notes(book, chapter) — generates a user-role prompt message that instructs the agent to call list_books → get_toc → read_chapter in sequence, then write lean cheat-sheet-style notes (flat #### headings, bold-label bullets) and optionally save them with save_notes.
claude mcp add-json vitalsource '{"command":"node","args":["C:\\vitalsource-mcp\\dist\\index.js"]}'Using add-json is more reliable on Windows than the -- node ... form because it avoids shell argument-splitting issues.
For development without a build step:
claude mcp add-json vitalsource '{"command":"npx","args":["tsx","C:\\vitalsource-mcp\\src\\index.ts"]}'A typical agent flow looks like:
"Take notes on chapter 2 of Linear Algebra Done Right."
The agent chains list_books → get_toc → read_chapter → drafts notes → save_notes.
- DRM / image-only books. Some titles disable text extraction or are scan-only. Chapter content comes back empty or as garbled markup. No workaround short of OCR.
- Selector drift. All CSS selectors and URL patterns live in
src/vitalsource/selectors.ts. When a tool returns empty results after a Bookshelf update, that file is the first place to look. Each selector is a comma-joined fallback chain in priority order; bump the "Last verified" date in the header when you revalidate. - Single browser context / serialized calls. All tool calls share one
BrowserContext.runExclusivequeues them so they do not run in true parallel. Two simultaneousread_chaptercalls will execute back-to-back, not concurrently. - Interactive login needs a desktop. The headed browser window cannot open on a headless server with no display. Use credential mode (
VS_MCP_EMAIL/VS_MCP_PASSWORD) for unattended environments — but only if your account is a native email + password account. - First-open cold load. A book that has never been opened in this browser profile must download assets on first access.
waitForTocretries once after a reload; the whole sequence can take up to ~90 seconds.
| Variable | Default | Purpose |
|---|---|---|
VS_MCP_USER_DATA_DIR |
<project>/user-data |
Where Playwright persists the browser profile. |
VS_MCP_NOTES_DIR |
<project>/notes |
Where save_notes writes markdown files. |
VS_MCP_EMAIL |
— | Email for headless credential login. |
VS_MCP_PASSWORD |
— | Password for headless credential login. Native accounts only — not SSO. |
npm run dev # run MCP server from source via tsx (speaks stdio — attach an MCP client)
npm test # Vitest unit tests (pure-function coverage; browser paths verified manually)
npm run typecheck # tsc --noEmit
npm run prettier:fixsrc/
index.ts MCP entry: registers 5 tools, 3 resources, 1 prompt; connects stdio
config.ts paths + env overrides
browser.ts BrowserManager singleton (persistent context + runExclusive mutex)
notes.ts saveNotes() — sanitized filename/subdir, YAML frontmatter, isInside guard
vitalsource/
selectors.ts all CSS selectors + URL builders (single source of truth)
auth.ts ensureLoggedIn(), interactiveLogin(), credentialLogin()
library.ts listBooks()
toc.ts getToc(bookId)
reader.ts readChapter() — iframe discovery, page-turn loop, boundContent
page-helpers.ts waitForToc() — cold-load retry helper