Skip to content

feat(assets): PDF text extraction via unpdf#352

Merged
aliasunder merged 19 commits into
mainfrom
claude/vault-bootstrap-setup-nfpewt
Jul 20, 2026
Merged

feat(assets): PDF text extraction via unpdf#352
aliasunder merged 19 commits into
mainfrom
claude/vault-bootstrap-setup-nfpewt

Conversation

@aliasunder

@aliasunder aliasunder commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Adds PDF text extraction to vault_read_asset via unpdf — a MIT-licensed, zero-dep, serverless-first PDF.js bundle. When a PDF is read, its text content is extracted and returned as a text content block, just like .svg, .json, and other text formats.

Scanned/image-only PDFs that yield no extractable text throw a structured error naming the file, its size, and page count — guiding the caller toward the limitation rather than returning empty content silently.

Key implementation details:

  • unpdf's extractText requires Uint8Array input (rejects Buffer), so the dispatch creates a zero-copy view via the shared ArrayBuffer
  • mergePages: true returns all pages as a single string
  • Output goes through the existing assertTextWithinCap guard
  • The .pdf case in the unsupported-type error is removed; .pdf added to the readable-types list in that error message
  • Tool descriptions updated to list PDF as a supported format

Type of change

  • New MCP tool
  • Bug fix
  • Refactoring
  • Documentation
  • CI / workflow change
  • Infrastructure (SST, Docker)
  • Other: New asset type support (PDF text extraction)

Checklist

  • npm test passes
  • npm run lint passes
  • npm run prettier:check passes
  • npm run build succeeds
  • New MCP tools follow the naming and description conventions in AGENTS.md
  • README or ARCHITECTURE.md updated (if user-facing behavior changed)

Note: No new tools — this extends the existing vault_read_asset tool's dispatch. One pre-existing flaky test (fit-image-to-byte-budget alpha WebP timeout) is unrelated to this change.

Summary by CodeRabbit

  • New Features

    • Added PDF text extraction for readable PDF assets.
    • Added clear errors for scanned or image-only PDFs without extractable text.
    • Added configurable limits for asset input size and image output size.
  • Documentation

    • Updated asset-handling guidance to describe PDF support, extraction behavior, and size-limit settings.
    • Updated supported asset types to include PDF files.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @aliasunder, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@socket-security

socket-security Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedunpdf@​1.6.210010010088100

View full report

@umm-actually

umm-actually Bot commented Jul 20, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle PDF extraction errors gracefully

The extractText call is not wrapped in a try/catch. If unpdf throws (e.g., on a
corrupted or password-protected PDF), the error propagates as an unhandled
rejection, bypassing the structured error path and potentially crashing the tool
handler. Wrap the call in a try/catch and re-throw a descriptive error that includes
the path and original cause.

src/vault-mcp/vault-operations/asset-operations.ts [137-146]

-const extracted = await extractText(pdfData, { mergePages: true })
+let extracted: Awaited<ReturnType<typeof extractText>>
+try {
+  extracted = await extractText(pdfData, { mergePages: true })
+} catch (cause) {
+  throw new Error(
+    `PDF extraction failed: "${path}" exists (${asset.bytes} bytes) but could not be read`,
+    { cause },
+  )
+}
 const text = typeof extracted.text === "string" ? extracted.text : ""
 if (!text.trim()) {
   throw new Error(
     `PDF has no extractable text: "${path}" exists ` +
       `(${asset.bytes} bytes, ${extracted.totalPages} pages) but ` +
       `contains no text content — it may be a scanned document or ` +
       `image-only PDF`,
   )
 }
Suggestion importance[1-10]: 5

__

Why: Adding error handling for extractText provides a more descriptive error message, but the existing code already throws errors and the suggestion is a minor improvement.

Low
General
Tighten error assertion to prevent false passes

The test for whitespace-only PDF content uses .rejects.toThrow("PDF has no
extractable text") without a more specific assertion. If the code throws a different
error (e.g., a generic "text output too large" or an unrelated exception), this test
would still pass because the error message contains the substring. Use an exact
match or a regex anchored to the start of the message to ensure the correct error is
thrown.

src/vault-mcp/vault-operations/tests/asset-operations.test.ts [80-97]

 it("throws for PDFs with only whitespace content", async () => {
   mockedReadAsset.mockResolvedValue({
     buffer: Buffer.from("fake-pdf"),
     bytes: 1_000,
     extension: ".pdf",
   })
   mockedExtractText.mockResolvedValue({
     totalPages: 1,
     text: "   \n\t  \n  ",
   })
 
   await expect(
     assetOperations.readAssetContent(
       { ...defaultParams, path: "empty.pdf" },
       logger,
     ),
-  ).rejects.toThrow("PDF has no extractable text")
+  ).rejects.toThrow(/^PDF has no extractable text/)
 })
Suggestion importance[1-10]: 5

__

Why: Tightening the error assertion to a regex anchored at the start improves test specificity and prevents false passes, a minor but valid enhancement.

Low

Comment thread src/vault-mcp/vault-operations/asset-operations.ts Outdated
Comment thread src/vault-mcp/vault-operations/__tests__/asset-operations.test.ts
@aliasunder

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e898cc5-d23a-4cbf-b3a1-27694459a05d

📥 Commits

Reviewing files that changed from the base of the PR and between 29e3325 and d955522.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • ARCHITECTURE.md
  • DOCKERHUB.md
  • README.md
  • package.json
  • src/vault-mcp/config.ts
  • src/vault-mcp/mcp-core/__tests__/pdf-fixture.ts
  • src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts
  • src/vault-mcp/mcp-core/tools/asset-tools.ts
  • src/vault-mcp/vault-operations/__tests__/asset-operations.test.ts
  • src/vault-mcp/vault-operations/asset-operations.ts

📝 Walkthrough

Walkthrough

Changes

PDF assets are now read through unpdf, with extracted text validation, updated tool messaging, integration coverage, and documentation for PDF behavior and asset size limits.

PDF asset support

Layer / File(s) Summary
PDF extraction and validation
package.json, src/vault-mcp/vault-operations/..., src/vault-mcp/mcp-core/__tests__/...
Adds unpdf extraction, empty-text and output-size errors, extraction failure propagation, and PDF test fixtures and coverage.
Tool behavior and integration
src/vault-mcp/mcp-core/tools/asset-tools.ts, src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts, src/vault-mcp/vault-operations/__tests__/asset-operations.test.ts
Marks PDFs as readable in tool descriptions and verifies extracted text and updated readable-type errors.
Documentation and asset limits
ARCHITECTURE.md, README.md, DOCKERHUB.md, src/vault-mcp/config.ts
Documents PDF extraction, scanned/image-only PDF errors, and asset/image output byte limits.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: Review effort 3/5

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding PDF text extraction support via unpdf.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vault-bootstrap-setup-nfpewt

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

claude and others added 8 commits July 20, 2026 15:41
vault_read_asset now extracts text from PDFs using unpdf's content-stream
parser instead of returning a "not yet supported" error. Scanned or
image-only PDFs (no extractable text) get a descriptive error with page
count. Extracted text is subject to the same MAX_TEXT_OUTPUT_BYTES cap as
other text assets.

- Add unpdf dependency for serverless-first PDF.js text extraction
- Replace PDF error block in asset-operations.ts with extractText dispatch
- Convert Buffer → Uint8Array for unpdf compatibility
- Update vault_read_asset tool description (PDF section, errors, examples)
- Update vault_list_assets description (remove stale ".pdf unsupported" note)
- Fix "MCP attachment reads" → "MCP asset reads" comment in config.ts
- Add asset-operations unit tests (PDF happy path, scanned PDF, whitespace-
  only, text cap, unsupported type includes .pdf)
- Add integration test with minimal valid PDF fixture
- Update existing tool-definitions tests for new error message format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PajwhEzt5cjqQjBPVBuXHn
ARCHITECTURE.md still described PDFs as "not-yet-supported" after the
feature landed. README.md and DOCKERHUB.md listed supported asset types
without mentioning PDFs. Updated all three to reflect the new capability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The unpdf overload for mergePages:true already narrows text to string —
the typeof check was dead code and the "" fallback masked the invariant.
Added a why-comment on the non-obvious Buffer→Uint8Array pool conversion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tighten 4 substring error assertions to exact message matches (deterministic
errors should not use substring matchers — catches message drift). Add
content-length assertion to PDF integration test. Write coverage gap test
for extractText rejection propagation (corrupt/unparseable PDFs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both env vars were documented in every deploy surface (docker-compose,
.env.example, cli/src/env.ts) and referenced in the vault_read_asset
error descriptions, but missing from the README and DOCKERHUB config
tables — a pre-existing gap from PR #351.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace flat extractText with extractTextItems + getMeta + extractLinks
for richer PDF output at near-identical token cost (~7% overhead):

- Document header: title, page count, links count
- Heading hierarchy: relative font-size detection (top N-1 distinct
  sizes map to H1-H3, smallest is always body text)
- Code fences: monospace fontFamily → fenced code blocks
- Page separators: --- Page N ---
- Links footer: deduplicated hyperlinks from the document
- Proxy lifecycle: getDocumentProxy with try/finally cleanup

The structured items are an intermediate processing step — the raw
coordinates are never exposed; the delivery format is reconstructed
markdown that a model reads naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aliasunder
aliasunder force-pushed the claude/vault-bootstrap-setup-nfpewt branch from 28efeff to 79d0674 Compare July 20, 2026 19:49
aliasunder and others added 11 commits July 20, 2026 15:52
…nt, stale example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Exact error message for text-too-large (was substring match on
  deterministic output — 200024 bytes computed from header + body)
- New test: code fence at end-of-page with no sans-serif transition
  covers the trailing `if (inCodeBlock)` branch; mutation-verified

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sweep of all 9 asset tool handler integration tests — every
decomposed isError + content[0]?.text pattern replaced with a
single toEqual on the full result shape. Locks structure so
extra fields, type drift, or isError changes are caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…name sections→outputLines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tool input table listed only `path` but the tool also accepts
`raw?` — every other tool in the table lists all optional params.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
groupIntoLines: for...of with tracked lastY replaces index-based
iteration and eliminates three type-narrowing guards that could
never fire at runtime.

buildHeadingLevels: Map constructor with .map() replaces mutable
for-loop. Double slice (remove smallest, cap at 3) reads
sequentially. Pipeline naming fixed (sortedSizes, not roundedSizes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Annotate the heading-level builder (visual hierarchy, body-text
exclusion), line classifier (monospace/heading detection), fence
transitions, end-of-page fence close, sequential proxy calls
(structuredClone constraint), and scanned-PDF guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aliasunder
aliasunder merged commit e4107d5 into main Jul 20, 2026
15 checks passed
@aliasunder
aliasunder deleted the claude/vault-bootstrap-setup-nfpewt branch July 20, 2026 21:14
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.

2 participants