{
  "name": "cputools",
  "homepage": "https://cputools.relaystation.ai",
  "endpoint": "https://api.relaystation.ai/mcp",
  "protocol": "mcp",
  "tools": [
    {
      "name": "pdf_merge",
      "description": "Merge 2+ PDFs into one (pdf-lib). Input: `files` — an array of sources (each { inline } base64 ≤4 MB | { inputKey } ≤50 MB). Billed PER PAGE (~$0.001/page at launch; POST unauthenticated for a live 402 price quote). Returns the merged PDF in the uniform output envelope (inline base64, or outputKey + presigned URL if large)."
    },
    {
      "name": "pdf_split",
      "description": "Split one PDF into multiple (pdf-lib). Input: { file, ranges?: \"1-3,5\", burst?: true } — burst/no-ranges = one file per page. Billed PER SOURCE PAGE (~$0.001/page at launch; 402-probe for a live quote). Capped at 100 output files. Returns a manifest of presigned-GET image/PDF refs."
    },
    {
      "name": "pdf_rotate",
      "description": "Rotate pages 90/180/270° (pdf-lib). Input: { file, degrees: \"90\"|\"180\"|\"270\", pages?: \"all\"|\"1-3,5\" } (1-based). Billed PER ROTATED PAGE (~$0.001/page at launch; 402-probe for a live quote). Returns the rotated PDF."
    },
    {
      "name": "pdf_pages",
      "description": "Delete / reorder / insert PDF pages (pdf-lib). Input is a discriminated union on `action`: {action:\"delete\",file,pages:\"2,4-5\"} | {action:\"reorder\",file,order:[3,1,2]} | {action:\"insert\",file,insert:<source>,at:1}. 1-based. Billed PER RESULT PAGE (~$0.001/page at launch; 402-probe for a live quote). Returns the edited PDF."
    },
    {
      "name": "pdf_extract_text",
      "description": "Extract text from a (text-layer) PDF via unpdf. Input: { file, pages?: \"1-3,5\" }. Billed PER SOURCE PAGE (~$0.002/page at launch; 402-probe for a live quote). Returns JSON { totalPages, pages:[{page,text}], text } — no bytes. For scanned/image PDFs use pdf_ocr."
    },
    {
      "name": "pdf_metadata",
      "description": "Read or set PDF document metadata (pdf-lib). Input: { file, set?: { title?, author?, subject?, keywords?, creator?, producer? } }. No `set` → returns the metadata JSON; with `set` → returns the modified PDF. FLAT-billed (~$0.001/call at launch; 402-probe for a live quote)."
    },
    {
      "name": "pdf_watermark",
      "description": "Stamp text on PDF pages (pdf-lib). Input: { file, text, opacity?:0-1, size?:int, color?:\"#RRGGBB\", position?:\"center\"|\"top-left\"|\"top-right\"|\"bottom-left\"|\"bottom-right\", pages?:\"all\"|\"1-3\" }. Billed PER STAMPED PAGE (~$0.001/page at launch; 402-probe for a live quote). Returns the watermarked PDF."
    },
    {
      "name": "pdf_form",
      "description": "Fill AcroForm fields and/or flatten a PDF (pdf-lib). Input: { file, fields?: { <name>: <string|boolean> }, flatten?: bool } (string→text/dropdown/radio, boolean→checkbox). Billed PER PAGE (~$0.001/page at launch; 402-probe for a live quote). Returns the filled/flattened PDF."
    },
    {
      "name": "pdf_encrypt",
      "description": "Password-protect a PDF (qpdf, 256-bit AES, worker). Input: { file, userPassword?, ownerPassword?, print?, copy?, modify?, annotate?, fillForms?, extract?, assemble?, printHighRes? } — at least one password; neither may start with \"-\". ownerPassword alone ⇒ opens freely, permissions owner-gated. The optional permission booleans (the canonical PDF permission octet) map to qpdf restriction flags (absent = allowed): print+printHighRes set the print level (none/low/full); `copy` is the live text/graphics extraction control while `extract` (accessibility extraction) is deprecated by PDF 2.0 / 256-bit and may be a no-op; modify/annotate/fillForms/assemble gate edits/comments/form-fill/page-assembly. Billed PER PAGE (~$0.001/page at launch; 402-probe for a live quote). Passwords ride only in the request — never logged. Returns the encrypted PDF."
    },
    {
      "name": "pdf_compress",
      "description": "Recompress a PDF (qpdf object+content streams, worker). Input: { file, linearize?: bool } (linearize = web \"fast view\"). Billed PER PAGE, NOT by savings — the ratio varies by document (~$0.001/page at launch; 402-probe for a live quote). Returns the recompressed PDF."
    },
    {
      "name": "pdf_render",
      "description": "Rasterize PDF pages to PNG/JPEG (poppler pdftoppm, worker). Input: { file, pages?: \"1-3,5\" (default all), dpi?: int ≤300, format?: \"png\"|\"jpeg\" }. Non-embedded standard-14 fonts render via vendored DejaVu Sans. Billed PER OUTPUT PAGE (~$0.001/page at launch; 402-probe for a live quote). CAP: ≤40 pages, ≤300 DPI. Returns a manifest of presigned-GET image refs."
    },
    {
      "name": "pdf_ocr",
      "description": "OCR a scanned/image PDF (pdftoppm → tesseract LSTM, worker). Input: { file, pages?: \"1-3,5\" (default all), lang?: <roster tag, default \"eng\"> }. The live roster is the operator-tunable cputools.ocr.langs (deployed: eng, spa, fra, deu, ita, por, nld, pol, rus, chi_sim); an off-roster lang is a free 422 UNSUPPORTED_LANG naming the roster. SYNCHRONOUS — CAP 5 pages (the call must clear a 30 s window; larger async jobs are coming). Billed PER SOURCE PAGE (~$0.003/page premium at launch; 402-probe for a live quote). Returns JSON { pages:[{page,text}], text }."
    },
    {
      "name": "pdf_ocr_searchable",
      "description": "Make a scanned PDF SEARCHABLE: per page, render → tesseract emits a page PDF with the image + an invisible text layer → qpdf merges. Input: { file, pages?: \"1-3,5\" (default all), lang?: <roster tag, default \"eng\"> } (live roster: cputools.ocr.langs — deployed eng, spa, fra, deu, ita, por, nld, pol, rus, chi_sim; off-roster → free 422 UNSUPPORTED_LANG). SYNCHRONOUS — CAP 5 pages. Billed PER SOURCE PAGE (~$0.004/page at launch — premium over plain OCR; 402-probe for a live quote). Returns the searchable PDF ALWAYS as a storage ref ({ output: { outputKey, outputUrl, sizeBytes } }) — never inline."
    },
    {
      "name": "image_ocr",
      "description": "OCR a raster image (tesseract LSTM, worker — PNG/JPEG/TIFF/BMP/WebP). Input: { file, lang?: <roster tag, default \"eng\">, tsv?: bool } (live roster: cputools.ocr.langs — deployed eng, spa, fra, deu, ita, por, nld, pol, rus, chi_sim; off-roster → free 422 UNSUPPORTED_LANG). tsv: true also returns per-word boxes + confidences. A non-image input is a refunded 422 IMAGE_PARSE_FAILED (tesseract is the parser). Billed FLAT (~$0.003 at launch; 402-probe for a live quote). Returns JSON { text, tsv? }."
    },
    {
      "name": "pdf_repair",
      "description": "Repair a damaged PDF (qpdf, worker): `qpdf --check` diagnoses (findings report), then a full rewrite reconstructs the xref + normalizes structure. Input: { file }. Billed PER PAGE (~$0.001/page at launch — compress\\"
    },
    {
      "name": "pdf_verify_signatures",
      "description": "Verify a PDF\\"
    },
    {
      "name": "pdf_from_html",
      "description": "Generate a PDF from HTML on headless Chromium (dedicated render worker) — invoices/reports/contracts/certificates. Input: { html: string, options?: { format?: \"A4\"|\"Letter\"|\"Legal\"|\"A3\"|\"A5\"|\"Tabloid\"|\"Ledger\", landscape?, printBackground?, scale?: 0.1–2, margin?: {top,right,bottom,left}, displayHeaderFooter?, headerTemplate?, footerTemplate?, pageRanges?: \"1-5, 8\", tagged? } }. Header/footer templates are HTML print furniture (date/title/url/pageNumber/totalPages classes); supplying one auto-enables displayHeaderFooter. pageRanges prints a subset; tagged (default true) emits an accessible PDF. SANDBOXED: no network and no JavaScript (header/footer templates included) — ALL assets must be inlined as `data:` URIs / inline <style> (external URLs, scripts, and file:// are blocked). Billed FLAT per render (~$0.003 at launch; 402-probe for a live quote). Returns the PDF + page count."
    },
    {
      "name": "pdf_from_office",
      "description": "Convert an Office document to PDF (LibreOffice) — docx/xlsx/pptx/odt/ods/odp/rtf/txt/csv. Input: { file, filename, options? } — the file is the cputools input convention ({ inline: \"<base64>\" } ≤4MB or { inputKey } from cputools_upload_url ≤50MB) and `filename` carries the source extension so the format is inferred. Optional `options` export controls: pageRanges (e.g. \"1-5,8\"), quality (1–100 JPEG quality for embedded images), losslessImageCompression, reduceImageResolution + maxImageResolution (DPI ceiling — one of 75/150/300/600/1200; reduceImageResolution must be true to apply), landscape. Billed FLAT per convert (~$0.005 at launch; 402-probe for a live quote). Requires an Idempotency-Key. Returns the PDF as the uniform output envelope ({ output: { inline } } when small, else { output: { outputKey, outputUrl } })."
    },
    {
      "name": "csv_convert",
      "description": "Convert between csv/tsv/json/ndjson (Papaparse). Input: { file, from?: <enum> (default csv), to: <enum>, delimiter? }. Cells are data, never evaluated (no formula execution). Billed PER MB of input, min 1 MiB (~$0.0002/MB at launch; 402-probe for a live quote). Returns the converted file."
    },
    {
      "name": "csv_dedupe",
      "description": "Drop duplicate CSV rows, order-preserving (Papaparse). Input: { file, columns?: string[] } — key columns; default whole row. Billed PER MB of input, min 1 MiB (~$0.0002/MB at launch; 402-probe for a live quote). Returns the deduplicated CSV."
    },
    {
      "name": "csv_select",
      "description": "Project/reorder CSV columns (Papaparse). Input: { file, columns?: string[] | drop?: string[] } — EXACTLY ONE; columns keeps+reorders, drop removes. Billed PER MB of input, min 1 MiB (~$0.0002/MB at launch; 402-probe for a live quote). Returns the projected CSV."
    },
    {
      "name": "qr_generate",
      "description": "Generate a QR code (PNG/SVG). Input: { data, format?: \"png\"|\"svg\", size?, margin?, ecc?: \"L\"|\"M\"|\"Q\"|\"H\" }. No input file. FLAT-billed (~$0.0002/call at launch; 402-probe for a live quote). CAP: data ≤4096 chars. Returns the QR image."
    },
    {
      "name": "barcode_generate",
      "description": "Generate a barcode (PNG/SVG; bwip-js). Input: { data, symbology, format?: \"png\"|\"svg\", scale?, height? }. `symbology` is any bwip-js bcid (111 supported — e.g. code128, ean13, qrcode, datamatrix; full enum in /openapi.json POST /v1/barcode); an unsupported one → 422 UNSUPPORTED_SYMBOLOGY, data the symbology can\\"
    },
    {
      "name": "image_resize",
      "description": "Resize an image (sharp). Input: { file, width?, height?, fit?: \"cover\"|\"contain\"|\"fill\"|\"inside\"|\"outside\", animated?: bool } (at least one of width/height). With animated:true all frames of an animated GIF/WebP are resized + preserved (billed per-frame). Billed PER MEGAPIXEL of input, min 1 (~$0.0003/MP at launch; 402-probe for a live quote). Returns the resized image."
    },
    {
      "name": "image_convert",
      "description": "Convert image format (sharp; png/jpeg/webp/avif — HEIC input supported). Input: { file, format: \"png\"|\"jpeg\"|\"webp\"|\"avif\", quality?: 1-100, animated?: bool }. With animated:true all frames of an animated input are preserved (gif/webp targets keep the stack; others flatten). Billed PER MEGAPIXEL of input, min 1 (~$0.0003/MP at launch; 402-probe for a live quote). Returns the converted image."
    },
    {
      "name": "image_compress",
      "description": "Compress an image at its current format (sharp). Input: { file, quality?: int }. Billed PER MEGAPIXEL of input, min 1 (~$0.0003/MP at launch; 402-probe for a live quote). Returns the compressed image."
    },
    {
      "name": "image_rotate",
      "description": "Rotate / flip an image (sharp). Input: { file, angle?: \"90\"|\"180\"|\"270\" (a STRING), flip?: bool, flop?: bool } — at least one of angle/flip/flop. Billed PER MEGAPIXEL of input, min 1 (~$0.0003/MP at launch; 402-probe for a live quote). Returns the rotated image."
    },
    {
      "name": "image_metadata",
      "description": "Read image dimensions/format/EXIF presence (sharp). Input: { file }. FLAT-billed (~$0.0003/call at launch; 402-probe for a live quote). Returns JSON { metadata: { width, height, format, ... } } — no bytes."
    },
    {
      "name": "image_crop",
      "description": "Crop a region from an image. Input: { file, left, top, width, height }. Per-MP billed. Output: cropped image (original format)."
    },
    {
      "name": "image_blur",
      "description": "Gaussian-blur an image. Input: { file, sigma? (0.3–1000, default 3) }. Per-MP billed."
    },
    {
      "name": "image_sharpen",
      "description": "Sharpen an image. Input: { file, sigma? (0–10, default 1) }. Per-MP billed."
    },
    {
      "name": "image_grayscale",
      "description": "Convert an image to grayscale. Input: { file }. Per-MP billed."
    },
    {
      "name": "image_exif_strip",
      "description": "Strip EXIF/metadata from an image (auto-orients first, then drops all metadata). Input: { file }. Per-MP billed."
    },
    {
      "name": "image_dominant_color",
      "description": "Get the dominant color of an image. Input: { file }. Flat-billed. Returns { dominant: {r,g,b}, hex }."
    },
    {
      "name": "image_composite",
      "description": "Overlay/watermark one image onto another. Input: { file (base), overlay, gravity? | (top,left), opacity? (0–1) }. Per-MP billed on the base."
    },
    {
      "name": "image_contact_sheet",
      "description": "Tile images into a thumbnail-grid PNG. Input: { files: <source>[] (≤100), columns?, cellWidth?, cellHeight?, gap?, background? }. Per-MP billed on the total input."
    },
    {
      "name": "image_from_html",
      "description": "Render caller-supplied HTML to an image (png/jpeg/webp) via headless-Chromium page.screenshot on the dedicated render worker. Input: { html, type?: \"png\"|\"jpeg\"|\"webp\" (default png), fullPage?: bool, clip?: { x, y, width, height }, omitBackground?: bool, width?: int, height?: int }. SANDBOXED / SSRF-WALLED like pdf_from_html: NO network, NO JavaScript — all assets must be inlined as data: URIs (provided-HTML-only). `clip` and `fullPage` are mutually exclusive (→ 400); `omitBackground` (transparent) requires png/webp, with jpeg → 400. FLAT-billed (~$0.003/call at launch; 402-probe for a live quote) — output dimensions aren\\"
    },
    {
      "name": "image_adjust",
      "description": "Adjust image color/tone (sharp modulate/negate/tint). Input: { file, brightness?: 0–10, saturation?: 0–10, hue?: -360–360 (degrees), lightness?: -100–100, negate?: bool, tint?: { r, g, b } } — at least one adjustment. Per-MP billed (~$0.0003/MP at launch; 402-probe for a live quote). Returns the adjusted image (original format)."
    },
    {
      "name": "image_trim",
      "description": "Auto-crop uniform-color borders from an image (sharp trim). Input: { file, threshold?: 0–255 (sensitivity) }. Per-MP billed (~$0.0003/MP at launch; 402-probe for a live quote). Returns the trimmed image (original format)."
    },
    {
      "name": "image_extend",
      "description": "Pad (extend) an image on any side (sharp). Input: { file, top?, bottom?, left?, right? (0–10000 px each; at least one positive), background?: \"#rrggbb\" | { r, g, b, alpha? }, extendWith?: \"background\"|\"copy\"|\"repeat\"|\"mirror\" }. Per-MP billed (~$0.0003/MP at launch; 402-probe for a live quote). Returns the extended image (original format)."
    },
    {
      "name": "pdf_images",
      "description": "Extract embedded images from a PDF → PNGs. Input: { file, pages? (\"1-3,5\") }. Per-page billed. Returns { images: [{ page, index, width, height, output }], count, truncated }."
    },
    {
      "name": "pdf_diff",
      "description": "Text-compare two PDFs (extract text → unified diff). Input: { a, b, context? }. Per-page billed on a+b. Returns { patch, changed }."
    },
    {
      "name": "pdf_bookmarks",
      "description": "Read a PDF outline / bookmarks. Input: { file }. Flat-billed. Returns { outline: [{ title, children }], count }. (Read-only; setting an outline is deferred.)"
    },
    {
      "name": "pdf_attachments",
      "description": "List (and optionally extract) embedded file attachments in a PDF. Input: { file, extract? }. Flat-billed. Returns { attachments: [{ filename, size, output? }], count }."
    },
    {
      "name": "cputools_upload_url",
      "description": "Mint a presigned POST for uploading a file up to 50 MB, returning an `inputKey` to pass to any file-taking cputools tool (the >4 MB path). FREE. Input: { ext?, contentType? }. Returns { url, fields, inputKey, expiresAt, maxBytes } — POST multipart/form-data to `url` with every `fields` entry + a `file` field, then call the op with { inputKey }. Requires your key (the upload is scoped to your namespace)."
    }
  ],
  "schema": "sep-1649@v0",
  "description": "cputools — stateless CPU utilities for agents and devs. 24 tools across PDF, CSV, image, and QR/barcode, callable over MCP at api.relaystation.ai/mcp — one endpoint that also carries Relaystation’s Courier and chassis tools, all funded from one balance with one key. Pay per call with an rs_live_* key or x402; an unauthenticated POST returns a live 402 price quote.",
  "contact": {
    "email": "hi@relaystation.ai"
  }
}