Sırdaş

Sırdaş MCP server

For Claude Code, Claude Desktop, Cursor, VS Code and any MCP client: 27 local tools.

Start here

claude mcp add sirdas -- npx -y @sirdaspdf/mcp

For your AI agent

Paste this into Claude, ChatGPT or Cursor and the agent installs and configures itself by reading the Markdown version of this page.

A local Model Context Protocol server over stdio. It reads and writes files on the machine where it runs and never opens a network connection. Every tool declares openWorldHint: false.

Install

Claude Code (one line):

claude mcp add sirdas -- npx -y @sirdaspdf/mcp

Claude Desktop, Cursor, VS Code, Windsurf and any MCP client:

{
  "mcpServers": {
    "sirdas": { "command": "npx", "args": ["-y", "@sirdaspdf/mcp"] }
  }
}

From source (to hack on it):

git clone https://github.com/Brayan15p/SIRDAS-APP-PDF.git
cd SIRDAS-APP-PDF
npm install
npm run build -w packages/core -w packages/mcp
# server entry point: <repo>/packages/mcp/dist/cli.js
{
  "mcpServers": {
    "sirdas": { "command": "node", "args": ["/absolute/path/to/SIRDAS-APP-PDF/packages/mcp/dist/cli.js"] }
  }
}

Where to put that JSON:

  • Claude Desktopclaude_desktop_config.json (Settings → Developer → Edit Config).
  • Claude Codeclaude mcp add sirdas -- npx -y @sirdaspdf/mcp (or -- node /path/to/cli.js).
  • Cursor.cursor/mcp.json in the project, or the global MCP settings.

Requires Node.js ≥ 20. The server prints one line to stderr when ready; stdout is reserved for JSON-RPC.

Where it can read and write

The paths this server touches are chosen by a language model, not typed by a person, so it only reads and writes inside one allowed root. A path that resolves outside it is refused before any file is opened: .. escapes, absolute paths elsewhere, symlinks pointing out of the root, and files that do not exist yet whose parent directory is outside it. Paths are checked after realpath, so a symlink cannot smuggle a write out, and the root itself may be a symlink.

VariableSIRDAS_ROOT
Defaultthe working directory of the server process — the folder your MCP client launches it from
Relative pathsresolved from the root
Opt outSIRDAS_ROOT=/ allows the whole filesystem

The default is the working directory rather than something like ~/Documents because that is the folder the client already scoped the session to, and because a default that widens over time is how a local tool becomes an arbitrary file writer. SIRDAS_ROOT=/ restores the old, unconfined behaviour; it is a deliberate choice and never the default.

{
  "mcpServers": {
    "sirdas": {
      "command": "npx",
      "args": ["-y", "@sirdaspdf/mcp"],
      "env": { "SIRDAS_ROOT": "/Users/you/Documents/pdfs" }
    }
  }
}

A refusal comes back as a normal tool error naming the root and the variable:

Path is outside the allowed root: /etc/passwd. This Sırdaş MCP server only reads
and writes inside /Users/you/Documents/pdfs. Restart it with SIRDAS_ROOT=<directory>
to point it somewhere else, or SIRDAS_ROOT=/ to allow the whole filesystem (it then
writes anywhere the process can).

The server also prints its allowed root on the stderr line it writes at startup.

Tools

Seventeen tools. All PDF inputs accept either path (absolute, or relative to the allowed root) or base64 (raw PDF bytes) — never both. Tools that write files refuse to overwrite unless overwrite: true.

pdf_to_markdown — read-only

Convert a text PDF to Markdown for an LLM.

ArgumentTypeDefaultNotes
path / base64stringOne of the two is required.
anonymizebooleantrueReplace personal data with placeholders.
max_charsintegerTruncate the Markdown.

Returns the Markdown as the text content, plus structuredContent: pages, characters, tokens_estimate, anonymized, redacted (count per entity type), truncated, has_text, source. A scanned PDF yields has_text: false and an explanatory line instead of content.

Entity types: NOMBRE, DOCUMENTO, NIT, EMAIL, TELEFONO, FECHA, DIRECCION, TARJETA, HISTORIA_CLINICA.

anonymize_text — read-only

ArgumentTypeDefaultNotes
textstringAny text.
include_mappingbooleanfalseAlso return [placeholder → original] so the agent can de-anonymize an answer locally.

Returns the anonymized text; structuredContent has total, redacted per type, tokens_estimate and, if requested, mapping.

Typical pattern: anonymize locally → send the placeholder text to a cloud model → map placeholders back in the answer, on the user's machine.

deanonymize_text — read-only

The return trip. Give it a text with placeholders and the mapping you kept, and it puts the real values back.

ArgumentTypeNotes
textstringTypically the AI's answer.
mappingobject{"[NOMBRE_1]": "Ana Gómez"}, from anonymize_text with include_mapping.

Returns the restored text; structuredContent has restored (how many were replaced) and unknown (placeholders absent from the mapping — usually invented by the model — left untouched rather than silently dropped).

Nothing is ever deleted. The original document is not modified and the mapping stays on the user's machine.

read_document — read-only · start here

One call that does what a person would, in order: detect encryption (restricted files are unlocked with no password; password opens protected ones), OCR if the PDF is a scan, identify the type, extract the key fields for that type, anonymize personal data consistently across text, fields and tables, detect tables, split cited chunks.

ArgumentTypeDefault
path / base64string
passwordstring
anonymizebooleantrue
ocrbooleantrue
max_tokensinteger1500
includemarkdown · chunks · tables · fields["fields","tables","chunks"]

structuredContent: status (ok · needs_password · wrong_password · scan_without_ocr), pages, steps (what was done — tell the user), document_type, type_match, type_signals, fields[] (name, value, page, evidence, personal), tables[], chunks[], redacted, tokens, next_steps[] (tool, why — offer them), bytes_sent: 0. Sends progress notifications.

Fields by type — factura: numero_factura, cufe, nit_emisor, fecha_emision, fecha_vencimiento, subtotal, iva, total, resolucion_dian · historia_clinica: paciente, documento_paciente, fecha_atencion, motivo_consulta, diagnostico_cie10 (all) · rut: nit, razon_social, direccion_seccional, actividad_principal · cedula · extracto_bancario · contrato · certificado. Personal fields are replaced with the same placeholders as the text; business fields (dates, totals) are kept.

extract_fields — read-only

text, optional type (detected if omitted). Same fields as above, from text you already have.

convert_to_markdown — read-only

path, anonymize (default false). DOCX, XLSX, PPTX, ODT, ODS, ODP, EPUB, HTML, CSV/TSV, EML, RTF, JSON, Markdown or text → Markdown, plus tables (rows) and warnings (e.g. formulas using cached values). Content, not layout.

split_documents — writes files only with write: true

path, write (default false), output_dir, overwrite. Finds where each document starts inside one PDF — page numbering back to 1, an opening heading (FACTURA, HISTORIA CLÍNICA, CERTIFICADO…), a strong change of document type — and returns segments (from, to, type, reasons). With write: true it saves <name>-01-factura.pdf, <name>-02-historia_clinica.pdf… Show the segments to the user before writing when it matters.

pdf_forms — read-only

path / base64, anonymize. Fillable (AcroForm) fields — text, checkbox, radio, dropdown, list — with page, plus checkboxes written as characters (☒ ☐, [x], ( )). Hand-ticked boxes on scans are not detected (that needs a vision model).

pdf_to_chunks — read-only

Convert a PDF to Markdown and split it into pieces that fit a model's context. Each chunk breaks at a heading and carries that heading, so it stands on its own — for feeding a long document to a chat in order, or for RAG indexing.

ArgumentTypeDefault
path / base64string
max_tokensinteger2000
anonymizebooleantrue

Returns the chunks as text and, in structuredContent, chunks[] with:

FieldMeaning
idContent hash (fnv1a:…). Same text → same id: use it as the vector id so re-indexing updates instead of duplicating.
pages[first, last] source pages, 1-based. Cite them.
sectionFull heading path, e.g. ["Contrato", "Cláusula tercera"].
heading, index, tokens, textAs before. tokens is an estimate (~4 chars/token), not a model tokenizer.

Plus total_chunks, doc_type / doc_type_confidence (see pdf_classify) and bytes_sent: 0.

pdf_classify — read-only

path / base64. Detects the document type with transparent rules, no model: factura, contrato, historia_clinica, cedula, rut, extracto_bancario, certificado, hoja_de_vida or desconocido. Returns type, confidence (share of matched signal weight — not a calibrated probability), the signals that matched and a runner_up. A scan without text returns has_text: false: run pdf_ocr first.

pdf_tables — read-only

Find the tables in a PDF and return them as CSV and Markdown.

ArgumentTypeDefaultNotes
path / base64string
delimiter, ; \t,Use ; for Excel in Spanish locales.
min_rowsinteger3Rows, header included, for a block to count as a table.

Columns are reconstructed from the whitespace that recurs across rows, not from where each cell starts — that is what separates right-aligned numeric columns (invoice amounts, statement balances) correctly. structuredContent returns tables[] with page, columns, rows and csv, plus total.

Needs a text layer: a scan returns total: 0. Run OCR first for those.

compare_pdfs — read-only

Compare two versions of a document line by line.

ArgumentTypeDefaultNotes
beforestringPath to the earlier version.
afterstringPath to the newer version.
changes_onlybooleantrueOnly changed lines plus two lines of context.
ignore_casebooleanfalse

The diff runs on visual lines, not paragraphs, so a one-word edit reports as one changed line instead of flagging the whole block. structuredContent returns added, removed, unchanged, similarity (0–1), changed_tokens and identical. Leave changes_only on before asking a model to explain a revision: it is what keeps the context small.

pdf_to_dataset — read-only

Turn PDFs into a JSONL training set.

ArgumentTypeDefaultNotes
pathsstring[]PDFs or any supported file (DOCX, XLSX, PPTX, HTML, EPUB, CSV…).
formattext chat prompt-completion alpaca sharegpt langchain llamaindex embeddingstextFine-tuning (TRL, OpenAI, Axolotl, LLaMA-Factory, Unsloth) or RAG shapes.
output_dirstringWrites train/validation/test.jsonl plus a Hugging Face dataset card (README.md).
max_tokensinteger2000Tokens per record before chunking.
anonymizebooleantrue
dedupebooleantrueDrop near-duplicate records.
splitbooleanfalseTrain/validation/test by document.
template / systemstringFor chat and prompt-completion.

Every record carries sirdas: {document, chunk, heading, tokens, redacted}, so a bad example traces back to a page. Trainers ignore unknown fields, so leaving it in costs nothing.

The split is by document, never by chunk: two chunks of one contract on opposite sides of the split means the model already saw the template, and the metric flatters you. It is also stable — adding a document does not move the existing ones, so you can regenerate without invalidating an earlier evaluation.

No model is called. Records come from the documents by rules, so there is no question-answer generation here; see pdf_to_rl_pairs for what can be built without one.

find_duplicates — read-only

MinHash + LSH over 5-word shingles. threshold (default 0.8) is the minimum Jaccard similarity; the LSH banding is derived from it so that lowering the threshold actually finds more, and the final check is exact Jaccard rather than the signature estimate. The first occurrence is always the one kept.

Duplicated text gets memorized rather than learned — Lee et al. (arXiv:2107.06499) measured that deduplication cuts verbatim emission tenfold — and a folder of real documents is full of repeats: the same template, the same report twice.

check_contamination — read-only

ArgumentTypeDefault
train / evaluationstring[]
ninteger13

Reports which training documents share an n-gram with the evaluation set, with the literal fragments so the finding can be checked. n-grams appearing in more than 10 reference documents are ignored, which is GPT-3's rule against legal boilerplate — it matters in corporate corpora where boilerplate dominates. Use n: 8 for Llama 3's criterion.

pdf_to_rl_pairs — read-only

Builds (prompt, verifiable answer) pairs from tables, for RL with verifiable rewards. Output is JSONL with prompt and ground_truth, the column names GRPOTrainer and the reference RLVR datasets use; extra columns reach the reward function as kwargs. reward_function: true also returns the matching Python reward function.

Generates cell lookups, row counts and column extremes. Anything ambiguous is skipped — a repeated row key or a tie for the maximum produces no question, because a pair with two right answers punishes the model for being correct. Numbers compare as numbers, so 1.120.000 and 1120000 match.

find_boilerplate — read-only

Find the paragraphs that repeat across a set of documents: letterheads, footers, confidentiality notices, the data-protection clause identical in every contract.

ArgumentTypeDefault
pathsstring[]
min_wordsinteger5
min_repeatsinteger2
across_documents_onlybooleantrue

CCNet measured duplicated paragraphs at 70% of its corpus; in office documents the pattern is sharper. A model learns that text by heart and learns nothing from it. Matching follows CCNet's recipe — lowercase, digits to zero, accents and punctuation stripped — so "Invoice 1042" and "Invoice 1043" are the same template paragraph.

Paragraphs under min_words are skipped: titles, page numbers and signatures repeat by nature, and removing them would break the document's structure. Repeating inside one document is legitimate by default; repeating across five is a template.

This only reports. pdf_to_dataset with strip_boilerplate: true is what removes them, always keeping the first occurrence, so nothing disappears from the corpus entirely.

pdf_ocr — read-only

Read a scanned PDF with local OCR.

ArgumentTypeDefaultNotes
pathstringThe scanned PDF.
languagestringspa+engTesseract languages. The wrong one wrecks accuracy.
lang_pathstringFolder with the .traineddata files.
anonymizebooleantrue

Use it when pdf_to_markdown returns has_text: false: that PDF is a photo of a page, not text. Structure is rebuilt with the same rules as a text PDF, so headings and paragraphs survive.

It does not rasterize. A scanned PDF already contains one image per page, so the image is pulled straight out of the file and encoded as PNG. Rasterizing in Node would need a native canvas with compiled binaries, and npx sirdas would stop working without a toolchain.

Two things it needs, and the error explains both when they are missing: tesseract.js, an optional dependency because its WebAssembly engine is 44 MB and most PDFs have a text layer; and language data, looked up in SIRDAS_TESSDATA, then public/ocr/lang, then the user cache. sirdas ocr --download-models fetches it once — the only moment any of this touches the network, and it is a download, never an upload.

structuredContent reports recognized against pages, and pages_with_several_images when a page held more than one image and only the largest was read.

pdf_info — read-only

path / base64pages, encrypted, bytes, title, author, subject, creator, producer, created, modified.

merge_pdfs — writes a file

ArgumentTypeDefault
pathsstring[] (≥ 2)
outputstring
overwritebooleanfalse

split_pdf — writes files

ArgumentTypeDefaultNotes
pathstring
rangesstring""1-based, e.g. "1-3, 5, 8-end". Empty = one file per page.
output_dirstringinput's directory
overwritebooleanfalse

Output names follow <name>-p1-3.pdf, <name>-p5.pdf. Returns files and total pages.

compress_pdf_lossless — writes a file

path, optional output (default <name>-optimizado.pdf), overwrite. Returns before, after, saved_percent. Text stays selectable; the output is never larger than the input.

edit_pages — writes a file

ArgumentTypeDefaultNotes
pathstring
actiondelete · extract · rotate
pagesstring""1-based ranges. Required for delete/extract; empty rotates every page. Extract keeps the order given.
angle90 · 180 · 27090Clockwise, for rotate.
output / overwrite<name>-<action>.pdf / false

Deleting every page is refused. Returns pages_before, pages_after, affected.

add_page_numbers — writes a file

path, format (default {n}; {total} also available, e.g. Página {n} de {total}), position (bottom-center, bottom-right, bottom-left, top-*), start_at, skip_first (unnumbered cover pages), output, overwrite.

add_watermark — writes a file

path, text, opacity (0.03–1, default 0.15), output, overwrite. A diagonal visible label, not protection. Standard PDF font: Western European characters only; others return a clear error.

protect_pdf — writes a file

path, password, optional owner_password (random if omitted, so opening the file is not enough to lift restrictions), allow_print, allow_copy, allow_edit (all true by default), output (default <name>-protegido.pdf), overwrite. AES-256.

unlock_pdf — writes a file

path, password (default empty), output (default <name>-desbloqueado.pdf), overwrite. Try without a password first: files that open but forbid copying/printing unlock with none. Returns state (none · restricted · password). A wrong or missing open password returns isError with wrong_password: true — ask the user, never guess. Any other tool that meets an encrypted PDF says to call unlock_pdf.

Prompts

PromptArgumentsWhat it does
private_ai_answerpath, questionThe whole private round trip in one go: anonymize with mapping → answer on placeholders → deanonymize_text → reply with real names.
index_pdf_for_ragpath, max_tokens?Chunk with ids, pages and sections as vector-store metadata, and cite as «section, p. N».

Progress

pdf_ocr sends notifications/progress (0–100) when the client passes a progressToken, so a 300-page scan does not look hung.

Errors

Failures come back as a tool result with isError: true and one actionable sentence — for example a password-protected PDF (This PDF is password protected…), a file that is not a PDF, or a path outside the allowed root. The server never crashes on bad input.