Skip to content

Latest commit

 

History

62 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PDFiTT

PDFiTT is an open-source PDF MCP for rendering Markdown into polished PDF documents and converting PDFs back to Markdown. It ships as a browser editor, direct HTTP APIs, a remote Streamable HTTP MCP server, and a portable local agent skill.

Use the hosted instance, fork it, or self-host your own branded Markdown↔PDF tool.

Deployed endpoint:

https://pdf-i-tt.vercel.app/mcp

LLM entry point:

https://pdf-i-tt.vercel.app/llms.txt

Deploy with Vercel

What It Provides

  • Open-source Markdown↔PDF conversion under the MIT License.
  • Remote MCP tools for agents and MCP clients.
  • Direct Vercel Function APIs for apps and scripts.
  • Browser editor with live preview, PDF download, PDF upload, optional Clerk save, /sign-in, /dashboard, /account, and a Sign / Autofill panel.
  • Portable skill at skills/pdfitt-markdown-pdf.
  • Shared Markdown→PDF renderer across browser, API, and MCP workflows.
  • Classify-then-route PDF→Markdown (text, image/scan, form, multi-column, hybrid).

Features

  • GitHub Flavored Markdown parsing with marked.
  • PDF generation with pdfmake.
  • Support for headings, lists, task lists, blockquotes, code blocks, inline code, links, tables, horizontal rules, and image placeholders.
  • Safer export behavior for raw HTML and script content.
  • Filename derivation from the first H1 heading.
  • 250,000-character Markdown input limit.
  • Editor/in-browser convert accepts up to 300,000,000 bytes and 500 pages. Node/API/MCP stay at 8,000,000 bytes (40,000,000 for source_url fetch) and the first 15 pages. Hosted POST bodies (file / pdf_base64) still 413 around 4.5MB on Vercel Hobby — drop the file in the editor or use source_url.
  • Image-only pages OCR with tesseract.js WASM after extracting embedded page images. Hosted POST /api/markdown ocr:auto on a tiny scan (golden-tiny) is HTTP 200 in about 2s (in-process WASM from #27) — Hobby OCR is not dead. Text-layer pages are never rasterized, even when a full-bleed cover image sits underneath. Empty or hung OCR on one page (8s extract/raster, 45s page including OCR) falls back to that page's text layer or skips the page; later text pages still convert. The editor dropzone shows Converting page N of M… and yields between pages so a 370-page book can finish in this browser.

Documentation

The full system documentation is available in both source and rendered form:

  • llms.txt
  • AGENTS.md
  • SELF_HOSTING.md
  • CHANGELOG.md
  • docs/PDFiTT-System-Documentation.md
  • docs/PDFiTT-System-Documentation.pdf

If you are pointing an LLM at this repository, start with llms.txt.

Quick Start

npm install
npm run dev

Build for production:

npm run build

Quality checks:

npm run lint
npm test
npm run build
npm audit

Direct PDF API

curl -X POST "https://pdf-i-tt.vercel.app/api/pdf" \
  -H "Content-Type: application/json" \
  --output document.pdf \
  --data '{"markdown":"# Hello\n\nRendered by PDFiTT."}'

Request JSON accepts one of:

  • markdown
  • markdown_base64
  • source_url

Optional:

  • filename — used as the download name. A trailing .pdf is kept as a single extension (hello.pdf stays hello.pdf; hello becomes hello.pdf). Inner dots are preserved (my.invoice.v2.pdf).
  • style — optional pdfmake overlay applied after the frozen default. Convert never looks up the user. omit/null/non-object/content/unknown keys leave LETTER / Roboto 11 / current margins unchanged.

Example overlay:

{
  "pageMargins": [72, 72, 72, 72],
  "defaultStyle": { "fontSize": 12 },
  "styles": { "h1": { "color": "#1d4ed8" } },
  "header": { "text": "Acme Legal", "alignment": "center", "fontSize": 9 },
  "footer": { "left": "{{title}}", "right": "Page {{page}} of {{pages}}" }
}

Direct Markdown API

curl -X POST "https://pdf-i-tt.vercel.app/api/markdown" \
  -H "Content-Type: application/json" \
  -d '{"source_url":"https://www.irs.gov/pub/irs-pdf/fw9.pdf"}'
curl -X POST "https://pdf-i-tt.vercel.app/api/markdown" \
  -F file=@scan.pdf

The same route accepts JSON or multipart. Provide exactly one PDF source:

  • pdf_base64 (JSON)
  • source_url — public http or https only. file: and local paths are rejected.
  • file — multipart field used by PdfImport (curl -F file=@scan.pdf).

Optional ocr, document_type, and filename are accepted on the JSON body and as multipart form fields:

  • ocrauto (default), always, or never. always still OCRs image pages only; text-layer pages are never rasterized. Image-only PDFs with ocr: "never" return an error, not empty Markdown.
  • document_typeauto (default) or a forced path: text, image, hybrid, form, multi-column.
  • filename — requested .md name.

Successful JSON always includes markdown, page_count, ocr_used, warnings, filename, and document_class (plus page_classes and ocr_confidence). The editor dropzone converts local files in the browser and is never answered with 404 or 415. Hosted file / pdf_base64 POST bodies still cap at about 4.5MB.

W-9 and similar forms keep readable instructions and labeled fields. Expect form-fields-approximate after widgets are read; checkboxes become - [ ] / - [x]. Image-only scans OCR embedded page images. Low-confidence OCR adds low-ocr-confidence instead of failing with HTTP 500.

Documents API (save only)

GET / POST / DELETE /api/documents require a Clerk session. Convert stays public.

  • No CLERK_SECRET_KEY: HTTP 501. Convert still works.
  • No session: HTTP 401. Never HTTP 402.
  • After login the editor hydrates users.styleTheme into the download overlay. Free plan keeps styleTheme null. Convert never looks up the user.

When VITE_CLERK_PUBLISHABLE_KEY is set, the header Sign in control goes to /sign-in. /sign-up, /dashboard, and /account are also first-class pages (afterSignInUrl /dashboard, afterSignOutUrl /). /dashboard lists saved documents (title, updated time, kind) with Open and Delete. The convert editor on / stays usable logged out.

DocuSign autofill (product)

POST /api/docusign/autofill is public. Send { "markdown": "..." } and get { tabs, warnings }. The editor Autofill sign fields button renders that tab list (label, type, value or empty + reason). It does not call live DocuSign and never invents a signed PDF.

POST /api/docusign/envelopes creates a real envelope through the existing DocuSign library. Clerk session required (401 without a token, 501 without Clerk). Missing DOCUSIGN_* is HTTP 501 with the existing not-configured message — never HTTP 200 with a fake envelope. The editor Send for signature button stays disabled with that 501 message when DocuSign is not configured.

Remote MCP

Connect any MCP client that supports remote Streamable HTTP servers to:

https://pdf-i-tt.vercel.app/mcp

The MCP server exposes:

render_markdown_pdf
convert_pdf_to_markdown
get_setup_instructions
docusign_create_envelope
docusign_send_envelope
docusign_get_envelope
docusign_download_documents
docusign_void_envelope

render_markdown_pdf returns JSON containing filename, mimeType, byteLength, and base64 PDF bytes. Optional style is the same overlay as POST /api/pdf. Convert tools do not require a Clerk session and never return HTTP 402.

When CLERK_SECRET_KEY is set, /mcp also registers save_document, list_documents, get_document, and delete_document. Those require a Clerk bearer token. No session is HTTP 401, never 402. Without Clerk, save is HTTP 501 and convert stays public.

convert_pdf_to_markdown accepts exactly one of pdf_base64 or a public source_url (XOR; empty both or both set is JSON-RPC -32602). Multipart file is HTTP POST /api/markdown only, not this tool. Optional ocr is auto | always | never. Optional document_type is auto | text | image | hybrid | form | multi-column. It classifies the PDF and returns Markdown plus document_class, page_classes, ocr_used, warnings, ocr_confidence, and a .md filename. PdfConvertError / bad input returns HTTP 400 { code: -32602, message: "..." } with the request id and never leaks a TypeError.

get_setup_instructions returns generic MCP, Claude Code, OpenCode, Codex, local skill, script, and direct API setup instructions. A normal browser or curl GET to /mcp returns the same setup payload.

DocuSign fill-and-sign v1 creates a real DocuSign envelope from a filled unsigned PDF. It is not a PNG stamp. Tabs are placed with anchorString only. dateSigned uses the DocuSign clock. docusign_send_envelope refuses envelopes that have no signHere tab. docusign_download_documents works only when status is completed. Name / SSN-adjacent / FTC-sensitive tabs stay required and unlocked unless the sender explicitly locks them. A tab value that does not appear in convert markdown is left empty with reason: value_mismatch. Missing DOCUSIGN_INTEGRATION_KEY, DOCUSIGN_USER_ID, DOCUSIGN_ACCOUNT_ID, or DOCUSIGN_RSA_KEY returns HTTP 503 — envelopes are not faked.

curl https://pdf-i-tt.vercel.app/mcp

Claude Code remote HTTP setup:

claude mcp add --transport http pdfitt https://pdf-i-tt.vercel.app/mcp

Codex remote MCP setup:

codex mcp add pdfitt --url https://pdf-i-tt.vercel.app/mcp

OpenCode remote MCP setup:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "pdfitt": {
      "type": "remote",
      "url": "https://pdf-i-tt.vercel.app/mcp",
      "enabled": true
    }
  }
}

Merge that block into ~/.config/opencode/opencode.json for global use or opencode.json in a project.

Self-Host Your Own PDF Tool

PDFiTT is designed so other developers and teams can host their own PDF generation MCP.

  1. Fork this repository.
  2. Deploy it to Vercel.
  3. Set PDFITT_PUBLIC_URL to your deployed origin, for example https://your-pdf-tool.vercel.app.
  4. Optionally set PDFITT_ALLOWED_ORIGINS to restrict browser/API callers.
  5. Connect MCP clients to https://your-pdf-tool.vercel.app/mcp.
  6. Use the direct APIs at https://your-pdf-tool.vercel.app/api/pdf and https://your-pdf-tool.vercel.app/api/markdown.

For local development:

npm install
npm run dev

For a production check:

npm run lint
npm run build

See SELF_HOSTING.md for a fuller deployment checklist and customization notes.

After deployment, replace every https://pdf-i-tt.vercel.app example with your own origin.

Agent Skill

The portable skill lives at:

skills/pdfitt-markdown-pdf

Codex local skill setup:

mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
cp -R skills/pdfitt-markdown-pdf "${CODEX_HOME:-$HOME/.codex}/skills/"

Use it from Codex, OpenCode, or any agent runtime that supports local skills by copying that folder into the relevant skills directory. The included agents/openai.yaml and agents/opencode.json templates point at the hosted MCP endpoint.

Scripted render example:

node skills/pdfitt-markdown-pdf/scripts/render_pdf_via_mcp.mjs \
  --mcp-url https://pdf-i-tt.vercel.app/mcp \
  --markdown-file ./notes.md \
  --out ./notes.pdf

PDF to Markdown:

node skills/pdfitt-markdown-pdf/convert_pdf_via_mcp.mjs \
  --mcp-url https://pdf-i-tt.vercel.app/mcp \
  --pdf-file ./scan.pdf \
  --out ./scan.md

Deployment

vercel.json rewrites /mcp to /api/mcp, then /sign-in, /sign-up, /dashboard, and /account (including nested Clerk paths) to /index.html so those client routes are not HTTP 404. Function maxDuration / memory / OCR includeFiles stay owner-locked (api/markdown.js and api/mcp.js 60s / 1024MB; api/pdf.js 30s, no includeFiles). OCR uses tesseract.js WASM and pdf.js image extraction only — no native canvas.

Relevant environment variables:

  • PDFITT_PUBLIC_URL: base URL used in generated setup instructions.
  • PDFITT_ALLOWED_ORIGINS: comma-separated allowed origins for browser calls to /api/pdf, /api/markdown, and /mcp. When unset, same-host and localhost origins are allowed. No-Origin requests are always allowed. MCP clients that send MCP-Protocol-Version or Mcp-Method are not blocked by a foreign Origin.
  • PDFITT_MCP_URL: local skill script fallback for the MCP endpoint URL.
  • PDFITT_TESS_CACHE: writable cache directory for tesseract.js language data (default /tmp/pdfitt-tess).
  • DOCUSIGN_INTEGRATION_KEY, DOCUSIGN_USER_ID, DOCUSIGN_ACCOUNT_ID, DOCUSIGN_RSA_KEY: JWT impersonation for the five DocuSign MCP tools. Optional DOCUSIGN_AUTH_SERVER (default https://account-d.docusign.com) and DOCUSIGN_BASE_URI. Do not commit secrets. Without these, the DocuSign tools return HTTP 503.
  • VITE_CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY: optional Clerk accounts. Missing keys keep the editor and convert APIs public. Save is /api/documents (401 without a session, 501 without Clerk).
  • CONVEX_URL: optional Convex store for users (plan free|premium, styleTheme null on free), documents, and imports. Without it, signed-in saves use compact Clerk publicMetadata.

License

MIT License. See LICENSE.

About

Open-source, self-hostable Markdown-to-PDF generation MCP with a browser UI, direct API, and portable agent skill.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages