Skip to content
PDFDispatch

MCP server for the PDFDispatch API

Ask your AI assistant for a PDF, and get one.

Connect Claude Code, Cursor, VS Code or your own agent to PDFDispatch. The assistant writes the HTML, our server makes the PDF through the API with your key, and the file lands in your project. Nothing to install.

  • One URL and one header
  • 100 PDFs a month for free
  • Files stored in the EU

Add the MCP server

https://mcp.pdfdispatch.com/mcp

Claude Code

# In Claude Code: the plugin asks for your key and teaches Claude print CSS
/plugin marketplace add soriresitve/pdfdispatch-ai
/plugin install pdfdispatch@pdfdispatch

# Or only the MCP server, from a shell, with the key in PDD_KEY:
claude mcp add --transport http pdfdispatch https://mcp.pdfdispatch.com/mcp \
  --header "Authorization: Bearer $PDD_KEY"

Cursor

// ~/.cursor/mcp.json. Cursor fills in PDD_KEY from your environment.
{
  "mcpServers": {
    "pdfdispatch": {
      "url": "https://mcp.pdfdispatch.com/mcp",
      "headers": { "Authorization": "Bearer ${env:PDD_KEY}" }
    }
  }
}

VS Code

// .vscode/mcp.json. VS Code asks for the key once and keeps it secret.
{
  "inputs": [
    { "type": "promptString", "id": "pdd-key", "description": "PDFDispatch key", "password": true }
  ],
  "servers": {
    "pdfdispatch": {
      "type": "http",
      "url": "https://mcp.pdfdispatch.com/mcp",
      "headers": { "Authorization": "Bearer ${input:pdd-key}" }
    }
  }
}

Other clients

# Any MCP client that can send a header to a remote server (Streamable HTTP):
URL:     https://mcp.pdfdispatch.com/mcp
Header:  Authorization: Bearer pdd_...

"Add to Cursor" reads your key from PDD_KEY; VS Code asks for it once. Claude Desktop and other clients: setup for every client. Cursor rule and instructions for other assistants: pdfdispatch-ai.

What you can ask for

Say what you need, in your own words

The assistant picks the tools and fills in the options. These requests work as written.

An invoice from your data

“Make a PDF invoice for the order in fixtures/order-1042.json. A4, the total in bold. Save it as out/invoice-1042.pdf.”

html_to_pdf

Check the layout first

“Build a one-page certificate in HTML. Take a screenshot and check that the name is centred. Fix it, then make a landscape PDF.”

screenshot, html_to_pdf

A Word file on your computer

“Convert docs/contract-2026.docx to PDF and save it next to the original.”

upload_file, office_to_pdf

One report from many files

“Make a cover page titled "Q3 report", then merge it with reports/q3-figures.pdf into out/q3-report.pdf.”

html_to_pdf, merge_pdfs

Each one, step by step: Make PDFs from Claude Code, Cursor and VS Code.

Tools

Seven tools for documents

html_to_pdf
An HTML page to PDF, with headers and footers on every page.
markdown_to_pdf
Markdown to a PDF with a clean built-in style.
url_to_pdf
A public web page to PDF.
office_to_pdf
Word, Excel, PowerPoint and OpenDocument files to PDF.
merge_pdfs
Two or more PDFs into one, in order.
screenshot
HTML or a web page as an image, so the assistant can see its layout.
upload_file
Sends a file from your computer to the other tools.

Every input, default and limit: the tools in the docs. PDF/A, passwords and the other PDF options go through extra_fields.

Your own agent

Building an agent? Use MCP or the API

Agent frameworks that support remote MCP servers, such as the Claude API, the OpenAI Agents SDK and LangChain, can use the server. Or call the REST API from your code, one request per PDF.

  • Other MCP clients: the URL, the header and what each client needs.
  • The REST API: every route and option, with examples.
  • llms.txt: this site in one plain-text file, for your assistant to read.

Then in your code

Same API, same key, from your app

The MCP server makes every PDF with the PDFDispatch API. When the assistant has the layout right, keep the HTML and make the same PDF from your app with one HTTP request. Same key, same plan, same price.

HTML to PDF in the docs · Guides for your language

POST /v1/convert/html

Bash (curl)

# Saves out.pdf, or prints the error: {"code": ..., "error": "..."}
curl https://api.pdfdispatch.com/v1/convert/html \
  --header "Authorization: Bearer pdd_..." \
  --form files=@index.html \
  --fail-with-body --output out.pdf \
  || { cat out.pdf >&2; rm -f out.pdf; false; }

Node.js

// convert.mjs: Node.js 20 or newer, no packages.
import { openAsBlob } from 'node:fs';
import { writeFile } from 'node:fs/promises';

const form = new FormData();
form.append('files', await openAsBlob('index.html', { type: 'text/html' }), 'index.html');

const res = await fetch('https://api.pdfdispatch.com/v1/convert/html', {
  method: 'POST',
  headers: { Authorization: 'Bearer pdd_...' },
  body: form,
});

if (!res.ok) {
  const text = await res.text();
  let message = text;
  try {
    const { code, error } = JSON.parse(text);
    message = `${code}: ${error}`;
  } catch {}
  console.error(`HTTP ${res.status} ${message}`);
  process.exit(1);
}

await writeFile('out.pdf', Buffer.from(await res.arrayBuffer()));

Bun

// convert.ts: bun convert.ts
const form = new FormData();
form.append('files', Bun.file('index.html'), 'index.html');

const res = await fetch('https://api.pdfdispatch.com/v1/convert/html', {
  method: 'POST',
  headers: { Authorization: 'Bearer pdd_...' },
  body: form,
});

if (!res.ok) {
  const text = await res.text();
  let message = text;
  try {
    const { code, error } = JSON.parse(text);
    message = `${code}: ${error}`;
  } catch {}
  console.error(`HTTP ${res.status} ${message}`);
  process.exit(1);
}

await Bun.write('out.pdf', res);

Python

import sys

import requests

with open("index.html", "rb") as f:
    res = requests.post(
        "https://api.pdfdispatch.com/v1/convert/html",
        headers={"Authorization": "Bearer pdd_..."},
        files={"files": ("index.html", f, "text/html")},
        timeout=90,
    )

if not res.ok:
    try:
        err = res.json()
        message = f"{err['code']}: {err['error']}"
    except ValueError:
        message = res.text
    sys.exit(f"HTTP {res.status_code} {message}")

with open("out.pdf", "wb") as out:
    out.write(res.content)

PHP

<?php

$ch = curl_init('https://api.pdfdispatch.com/v1/convert/html');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer pdd_...'],
    CURLOPT_POSTFIELDS => ['files' => new CURLFile('index.html', 'text/html', 'index.html')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($body === false) {
    fwrite(STDERR, 'Request failed: '.curl_error($ch).PHP_EOL);
    exit(1);
}

if ($status < 200 || $status >= 300) {
    $error = json_decode($body, true);
    $message = isset($error['code']) ? "{$error['code']}: {$error['error']}" : $body;
    fwrite(STDERR, "HTTP $status $message".PHP_EOL);
    exit(1);
}

file_put_contents('out.pdf', $body);

Go

package main

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	html, err := os.ReadFile("index.html")
	if err != nil {
		log.Fatal(err)
	}

	var body bytes.Buffer
	form := multipart.NewWriter(&body)
	part, err := form.CreateFormFile("files", "index.html")
	if err != nil {
		log.Fatal(err)
	}
	part.Write(html)
	form.Close()

	req, err := http.NewRequest(http.MethodPost, "https://api.pdfdispatch.com/v1/convert/html", &body)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer pdd_...")
	req.Header.Set("Content-Type", form.FormDataContentType())

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}

	if res.StatusCode < 200 || res.StatusCode > 299 {
		var apiErr struct {
			Code  int    `json:"code"`
			Error string `json:"error"`
		}
		if json.Unmarshal(data, &apiErr) == nil {
			log.Fatalf("HTTP %d %d: %s", res.StatusCode, apiErr.Code, apiErr.Error)
		}
		log.Fatalf("HTTP %d %s", res.StatusCode, data)
	}

	if err := os.WriteFile("out.pdf", data, 0o644); err != nil {
		log.Fatal(err)
	}
}

Java

// ConvertHtml.java: JDK 11 or newer, no libraries.
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

public class ConvertHtml {
    public static void main(String[] args) throws Exception {
        String boundary = "pdd-" + UUID.randomUUID();
        ByteArrayOutputStream body = new ByteArrayOutputStream();
        body.write(("--" + boundary + "\r\n"
                + "Content-Disposition: form-data; name=\"files\"; filename=\"index.html\"\r\n"
                + "Content-Type: text/html\r\n\r\n").getBytes(StandardCharsets.UTF_8));
        body.write(Files.readAllBytes(Path.of("index.html")));
        body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.pdfdispatch.com/v1/convert/html"))
                .header("Authorization", "Bearer pdd_...")
                .header("Content-Type", "multipart/form-data; boundary=" + boundary)
                .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
                .build();

        HttpResponse<byte[]> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() / 100 != 2) {
            // The error as JSON: {"code": ..., "error": "..."}
            String error = new String(response.body(), StandardCharsets.UTF_8);
            System.err.println("HTTP " + response.statusCode() + " " + error);
            System.exit(1);
        }

        Files.write(Path.of("out.pdf"), response.body());
    }
}

C#

using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "pdd_...");

using var form = new MultipartFormDataContent();
var html = new ByteArrayContent(await File.ReadAllBytesAsync("index.html"));
html.Headers.ContentType = new MediaTypeHeaderValue("text/html");
form.Add(html, "files", "index.html");

using var res = await client.PostAsync("https://api.pdfdispatch.com/v1/convert/html", form);

if (!res.IsSuccessStatusCode)
{
    var message = await res.Content.ReadAsStringAsync();
    try
    {
        using var json = JsonDocument.Parse(message);
        message = $"{json.RootElement.GetProperty("code")}: {json.RootElement.GetProperty("error")}";
    }
    catch (JsonException)
    {
        // Not JSON: a plain-text message.
    }
    Console.Error.WriteLine($"HTTP {(int)res.StatusCode} {message}");
    return 1;
}

await File.WriteAllBytesAsync("out.pdf", await res.Content.ReadAsByteArrayAsync());
return 0;

Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

struct APIError: Decodable {
    let code: Int
    let error: String
}

let boundary = "pdd-\(UUID().uuidString)"
var body = Data()
body.append(Data("--\(boundary)\r\n".utf8))
body.append(Data("Content-Disposition: form-data; name=\"files\"; filename=\"index.html\"\r\n".utf8))
body.append(Data("Content-Type: text/html\r\n\r\n".utf8))
body.append(try Data(contentsOf: URL(fileURLWithPath: "index.html")))
body.append(Data("\r\n--\(boundary)--\r\n".utf8))

var request = URLRequest(url: URL(string: "https://api.pdfdispatch.com/v1/convert/html")!)
request.httpMethod = "POST"
request.setValue("Bearer pdd_...", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

let (data, response) = try await URLSession.shared.upload(for: request, from: body)
let status = (response as! HTTPURLResponse).statusCode

guard (200..<300).contains(status) else {
    if let apiError = try? JSONDecoder().decode(APIError.self, from: data) {
        fatalError("HTTP \(status) \(apiError.code): \(apiError.error)")
    }
    fatalError("HTTP \(status) \(String(decoding: data, as: UTF8.self))")
}

try data.write(to: URL(fileURLWithPath: "out.pdf"))

Dart (Flutter)

// pubspec.yaml: dependencies: http: ^1.2.0
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;

Future<void> main() async {
  final request = http.MultipartRequest(
    'POST',
    Uri.parse('https://api.pdfdispatch.com/v1/convert/html'),
  )
    ..headers['Authorization'] = 'Bearer pdd_...'
    ..files.add(await http.MultipartFile.fromPath('files', 'index.html'));

  final response = await http.Response.fromStream(await request.send());

  if (response.statusCode < 200 || response.statusCode >= 300) {
    var message = response.body;
    try {
      final error = jsonDecode(response.body) as Map<String, dynamic>;
      message = '${error['code']}: ${error['error']}';
    } on FormatException {
      // Not JSON: a plain-text message.
    }
    stderr.writeln('HTTP ${response.statusCode} $message');
    exit(1);
  }

  await File('out.pdf').writeAsBytes(response.bodyBytes);
}

Cost and privacy

One tool call, one PDF on your plan

A tool call that makes a file counts as one PDF, the same as one API request. Uploads are free.

Your key is not kept

The MCP server passes your key to the API with each call. It never stores or logs it.

Files stay in the EU

Results and uploads are stored in the EU under random names, and deleted after one day at the latest.

Links stop after an hour

Each result is a download link that works for one hour. The assistant saves what you want to keep.

The same rules as the API

Your plan, its limits and its price apply. A call that fails costs nothing.

Plans and prices · MCP privacy

Your assistant's first PDF, in two minutes

Make a key, add the server, ask for a PDF. 100 PDFs a month are free.