printagent
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@printagentprint the PDF file ~/documents/report.pdf to the default printer"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
PrintAgent
English · Tiếng Việt
A print agent that runs on a local machine and exposes an API other systems can call to print. It prints a PDF file directly, or takes a template plus JSON variables and renders the PDF on the agent side before printing.
Four ways to drive it:
Interface | Address | Used for |
REST API |
| Web apps, backends, point-of-sale |
WebSocket |
| Apps that need job events in real time |
MCP |
| Claude Code, Claude Desktop, AI agents |
Web UI |
| Setup, printer scanning, template management, queue view |
Requirements
Node.js >= 22
macOS/Linux: CUPS (
lp,lpstat- already present on macOS)Windows: installing SumatraPDF is recommended for silent PDF printing
Chromium:
yarn installdownloads Puppeteer's Chrome build, and the setup wizard downloads it again if it is missing. If the machine already has Chrome, pointrender.chromePathat it; the agent also detects a system Chrome when the Puppeteer build fails.
The setup screen in the web UI (and the printagent setup command) checks all of the above and installs what is missing, so nothing has to be assembled by hand.
Related MCP server: Memobird MCP Server
Installation
The procedure is the same on macOS, Windows and Linux.
Bare machine, no Node yet
A single command. No Node, no git, no admin rights required:
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/huy97/printagent/main/install.sh | bash# Windows (PowerShell)
irm https://raw.githubusercontent.com/huy97/printagent/main/install.ps1 | iexThe script downloads a portable Node LTS build (verified against its SHA-256) into ~/.printagent/runtime, installs the agent from npm, creates a printagent command and opens the setup screen. If the machine already has Node >= 22 it uses that one and downloads nothing.
Everything lives under ~/.printagent (Windows: %USERPROFILE%\.printagent); uninstalling means deleting that folder. Re-run the same command to update.
Optional environment variables: PRINTAGENT_LANG=vi for Vietnamese output, PRINTAGENT_NO_START=1 to install without starting, PRINTAGENT_HOME to install elsewhere, PRINTAGENT_NODE_TRACK to pick another Node line (default v22.x).
Machine that already has Node >= 22
npm install -g @hyydev/printagent
printagent start # the agent starts immediately and opens the setup screen on first runRun it once without installing:
npx @hyydev/printagent startOr run it from source:
git clone https://github.com/huy97/printagent.git
cd printagent
yarn install
yarn start # the agent starts immediately and opens the setup screen on first runThe web interface always comes up first. On the first run the browser opens straight onto the setup screen: press Install automatically once, the steps run in order and the progress shows on screen. The terminal is never needed.
The installer fixes whatever it can instead of asking the user to install things by hand. It only stops when a required step is still broken after the fix attempt; every other step just warns and the agent still starts.
Step | Required | Handled automatically when missing |
Node.js >= 22 | yes | installed through nvm (macOS/Linux) or winget/choco (Windows), then re-run with the new Node |
The | yes | created, write permission checked |
Print driver | no | Linux: installs |
Printer | no | discovers IPP printers over mDNS ( |
Chromium | no | downloads the Puppeteer build, otherwise installs Google Chrome through brew/winget/apt |
SumatraPDF (Windows) | no | downloads the portable build into |
API key | yes | creates a |
Tunnel | no | installs cloudflared/ngrok when |
Background service | no | registers the service when chosen |
Package installation uses whatever package manager is available: brew on macOS, winget/choco on Windows, apt-get/dnf/pacman/zypper/apk on Linux (through sudo -n, only prompting for a password when running in a terminal).
Non-interactive runs (CI, mass installation scripts):
node bin/printagent.js setup --service # also register the background service
node bin/printagent.js setup --no-service # skip the background service
node bin/printagent.js setup --no-download # do not download Chromium
node bin/printagent.js start --no-open # do not open the browserThe setup command runs exactly those steps in the terminal and exits with code 1 when a required step fails, so it works inside a script. The start command never blocks: the agent comes up first and whatever setup is left is a button away in the web UI.
The interface lives at http://127.0.0.1:7788. Data (config, templates, jobs, logs) is stored under ~/.printagent (change it with the PRINTAGENT_DATA_DIR environment variable); the result of the last setup run is in ~/.printagent/setup.json (readable with printagent doctor).
CLI commands
node bin/printagent.js setup [--service|--no-service|--no-download]
node bin/printagent.js start [--port 7788] [--host 0.0.0.0] [--no-open]
node bin/printagent.js service [status|install|uninstall]
node bin/printagent.js doctor # result of the last setup run
node bin/printagent.js printers # list the connected printers
node bin/printagent.js test --printer "Printer_Name"
node bin/printagent.js key # show the API key
node bin/printagent.js config set-default-printer "Printer_Name"
node bin/printagent.js tunnel check # check cloudflared/ngrok
node bin/printagent.js mcp # run the MCP server over stdioLanguage
The agent speaks Vietnamese and English.
Web UI: the language button in the title bar, next to Refresh. The choice is stored per browser.
First-run setup screen: a language button in the top left corner. The language picked there becomes
agent.localeand decides which starter template set is created.Terminal wizard:
printagent setupasks for the language first and stores the answer in the configuration.API: each request picks its language from the
x-localeheader, the?lang=query, theaccept-languageheader, thenagent.localefrom the configuration (defaulten). Responses carry acontent-languageheader, and every error also carries a stablekeyplusparamsso a client can translate it itself.CLI, logs and the terminal setup screen: the
PRINTAGENT_LANGenvironment variable, falling back toagent.locale(changeable in the Settings tab).Documentation: this file and llms.txt; Vietnamese versions are README.vi.md and llms.vi.txt, and a running agent serves
GET /llms.txt?lang=vi.
PRINTAGENT_LANG=vi node bin/printagent.js printers
curl -H "x-locale: vi" http://127.0.0.1:7788/api/printersAuthentication
The agent creates an API key on the first run (node bin/printagent.js key). Send it with every request:
x-api-key: pa_xxxor Authorization: Bearer pa_xxx, or ?apiKey=pa_xxx (used by the WebSocket).
By default requests coming from the machine running the agent skip the key (auth.allowLocalhostWithoutKey). That exception applies only when all four conditions hold, and each one is independent, so defeating a single check is not enough:
the TCP connection comes from a loopback address (
127.0.0.1,::1);the
Hostheader is loopback - a request through a tunnel always carries the public hostname and is rejected right there, even when the attacker deliberately sends no proxy header at all;no proxy header is present (
cf-connecting-ip,x-forwarded-for,x-real-ip,cf-ray, ...);Sec-Fetch-Siteissame-originornone, andOrigin(when present) is the agent itself - this blocks any random website from quietly calling the agent on a user's machine, including through<img>/<script>tags that send noOrigin.
Browsers set those three headers themselves and a web page cannot forge them; command line tools on the local machine do not send them at all, so they keep skipping the key as before.
Settings that decide which binary the agent runs, or that turn authentication off (render.chromePath, printing.sumatraPath, printing.allowLocalFilePath, printing.allowedFileRoots, auth.enabled, auth.allowLocalhostWithoutKey, tunnel.*.binPath), can only be changed from the local machine; a remote request carrying those fields has them ignored and listed in rejectedFields.
Document source limits
filePath(printing a file already on the agent machine) is off by default. Turn it on withprinting.allowLocalFilePath = trueand declareprinting.allowedFileRootsto limit the folders.urlacceptshttp/httpsonly, blocks loopback and private network ranges (re-enable withprinting.allowPrivateNetworkUrl), and caps the size atprinting.maxDownloadMb(default 64MB).CORS allows no origin by default; add your domain to
server.corsOriginsif your web app calls the agent straight from the browser. The WebSocket uses the same list, plus an exception for the page the agent itself serves (including through a tunnel, whereOriginmatchesHost).
Protection when exposed to the Internet
Ten wrong API keys within a minute block that address for five minutes with HTTP 429 and a
retry-afterheader. REST and the WebSocket share one counter, so reopening a WebSocket connection is not a way around it. The address comes fromcf-connecting-ip/x-real-ip/x-forwarded-for, so each client behind a tunnel is counted separately, and one successful authentication clears the counter. Requests from the local machine are never counted.The tunnel refuses to start while
auth.enabled = falseor while no API key exists, with a message saying what to fix.auth.enabledcannot be turned off while the tunnel is running; stop the tunnel first.Every response carries
x-content-type-options: nosniff,referrer-policy: no-referrerandx-frame-options: SAMEORIGIN.Authenticated requests are not rate limited: throttle them at the Cloudflare layer (WAF, Access) or at a reverse proxy if you need it.
Documentation for AI agents
The agent describes itself through three addresses, none of which need an API key, so an agent can read them as soon as it connects:
Address | Content |
| Discovery endpoint: version, where to read the docs, the three REST/WebSocket/MCP entry points, how to authenticate |
| OpenAPI 3.1 specification of all 36 REST endpoints, ready to feed a client generator or a tool definition |
| The condensed plain-text version; also in the repo as llms.txt and llms.vi.txt |
The fastest way for an agent to use PrintAgent is to plug in MCP (see MCP): the 13 tools already carry descriptions and schemas, no REST documentation needed.
For a REST integration, the suggested order is: read /.well-known/printagent.json, GET /api/printers for printer names, GET /api/templates/:id and read sampleData to learn the data shape, then POST /api/print/template.
REST API
Printing a PDF file
POST /api/print/pdf - takes one of three sources: content (base64), url, filePath, or a multipart upload in the file field.
# base64
curl -X POST http://127.0.0.1:7788/api/print/pdf \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"content":"JVBERi0xLj...","printer":"HP_LaserJet","copies":2,"wait":true}'
# upload a file
curl -X POST http://127.0.0.1:7788/api/print/pdf \
-H "x-api-key: $KEY" -F file=@invoice.pdf -F printer=HP_LaserJet
# download from a URL, then print
curl -X POST http://127.0.0.1:7788/api/print/pdf \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"url":"https://example.com/invoice.pdf"}'Print options: copies, title, options.duplex (none|long|short), options.paperSize, options.orientation, options.fitToPage, options.raw, options.extraOptions (an array of CUPS -o parameters, each written as "key=value"; Linux/macOS only, ignored on Windows). Add "wait": true to make the API return only once printing has finished.
Printing from a template + JSON variables
POST /api/print/template
curl -X POST http://127.0.0.1:7788/api/print/template \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"templateId": "invoice-a4",
"data": { "code": "HD001", "shop": {"name":"ABC Store"}, "items": [{"name":"Coffee","qty":2,"price":35000}], "total": 70000 },
"printer": "HP_LaserJet",
"copies": 1,
"wait": true
}'A template can be passed inline instead of templateId:
{ "template": "<h1>{{code}}</h1>", "engine": "html", "data": { "code": "HD001" } }Rendering without printing
POST /api/print/render returns the PDF (add ?format=html to see the HTML, ?format=base64 to get JSON).
Other endpoints
Method | Path | Description |
GET |
| Agent status (no key needed) |
GET |
| Detailed information, renderer check |
GET |
| Printer list (rescans when |
POST |
| Rescan printers |
GET |
| Printer details and options |
POST |
| Set the default printer |
POST |
| Print a test page |
GET/POST |
| List / create a template |
GET/PUT/DELETE |
| Read / update / delete a template |
POST |
| Render a template into a PDF |
GET |
| List the templates in a starter set ( |
POST |
| Create the starter set for a language ( |
GET |
| Job list ( |
GET |
| Job details |
GET |
| Download the printed file |
POST |
| Cancel a job |
POST |
| Print a job again |
GET/PUT |
| Read / update the configuration |
GET/POST/DELETE |
| Manage API keys |
GET |
| Tunnel status and tooling |
POST |
| Start / stop the tunnel |
GET |
| Initial setup result and background service status |
POST |
| Run setup in the background, returning progress immediately (local machine only) |
GET |
| Progress of the current run: current step, logs, result (local machine only) |
POST |
|
|
WebSocket
const ws = new WebSocket('ws://127.0.0.1:7788/ws?apiKey=pa_xxx');
ws.onopen = () => {
ws.send(JSON.stringify({ id: '1', type: 'printers.list' }));
ws.send(JSON.stringify({
id: '2',
type: 'print.template',
payload: { templateId: 'invoice-a4', data: { code: 'HD001' }, wait: true },
}));
};
ws.onmessage = (event) => console.log(JSON.parse(event.data));Supported commands: ping, status, printers.list, printers.scan, templates.list, templates.get, render.template, print.pdf, print.template, jobs.list, job.get, job.cancel, tunnel.status, subscribe.
Pushed events ({"type":"event"}): job.created, job.updated, printer.changed, tunnel.changed, log. Choose the channels with {"type":"subscribe","payload":{"events":["job","printer","tunnel","log"]}}.
MCP
Streamable HTTP
claude mcp add --transport http printagent http://127.0.0.1:7788/mcp --header "x-api-key: pa_xxx"stdio
{
"mcpServers": {
"printagent": {
"command": "node",
"args": ["/path/to/printagent/bin/printagent.js", "mcp"],
"env": {
"PRINTAGENT_URL": "http://127.0.0.1:7788",
"PRINTAGENT_API_KEY": "pa_xxx"
}
}
}
}The stdio mode calls the running agent by default. Add --standalone if MCP should handle printing itself without a background agent.
Tools: list_printers, agent_status, print_pdf, print_template, render_template, list_templates, get_template, save_template, delete_template, list_jobs, get_job, cancel_job, print_test_page. Their titles and descriptions follow the agent language.
Templates
Templates use Handlebars. Two kinds:
html: renders HTML into a PDF with Chromium, then sends it to the printer. Used for A4 invoices and 80mm receipts.text: renders plain text, wraps it in ESC/POS commands (init, cut, open drawer) and sends it straight to the printer in raw mode.
The agent does not create templates on startup. The setup screen asks first and only creates them if you agree; create them later at any time with the Starter set button on the Templates tab, POST /api/templates/seed, or printagent setup --templates.
There are two sets of 6 templates each, created in the language selected on the setup screen.
The English set, in USD with en-US formatting:
Id | Size | Used for |
| A4 | Sales invoice with QR code and amount in words |
| 80mm | Thermal receipt rendered through PDF |
| raw | ESC/POS receipt, plain text |
| A4 | Tax invoice: VAT numbers on both sides, per-line tax rate, signature blocks |
| 80mm | POS tax receipt with amount paid and change |
| A5 | Cash receipt: book and receipt number, five signature blocks |
The Vietnamese set:
Id | Size | Used for |
| A4 | Simple sales invoice |
| 80mm | Thermal printer receipt rendered through PDF |
| raw | Plain text receipt sent straight to a thermal printer |
| A4 | VAT invoice, printed representation of an e-invoice |
| 80mm | E-invoice issued from a cash register |
| A5 | Cash receipt, Vietnamese form 01-TT |
The two sets use different ids, so both can live on the same machine; creating a set again never overwrites an existing template.
The last three Vietnamese ones follow current Vietnamese regulations: Decree 123/2020/ND-CP (amended by Decree 70/2025/ND-CP) and Circular 78/2021/TT-BTC for e-invoices, Circular 133/2016/TT-BTC for cash receipts. They use a single tax rate for the whole invoice (vatRate, vatAmount); an invoice with several rates needs the summary table reworked. The English set is a plain commercial layout, not tied to any country's statutory form.
Built-in helpers: currency, formatNumber, formatDate, vndWords, enWords, amountWords, now, add, sub, mul, div, inc, sum, eq, ne, gt, lt, and, or, upper, lower, padStart, padEnd, repeat, concat, cols (label left, number right, aligned to a column count), ascii (strips Vietnamese diacritics for thermal printers), json, qr, qrDataUri.
currency,formatNumber,formatDate,nowandamountWordsfollow the agent language unless the template passeslocale=/currency=:en-USandUSDfor English,vi-VNandVNDfor Vietnamese. The bundled templates always pass them explicitly, so they render the same whatever the agent language.{{vndWords 8855000}}spells an amount out in Vietnamese:Tám triệu tám trăm năm mươi lăm nghìn.{{enWords 979.76 currency="USD"}}spells it out in English with the currency name:Nine hundred and seventy-nine US dollars and seventy-six cents only.{{amountWords total locale="en" currency="USD"}}picks the spelling by language, handy when one template serves both.{{formatDate date day="2-digit" month="2-digit" year="numeric"}}picks individual date parts; with no parts given it usesdateStyle/timeStyle.
<h1>{{shop.name}}</h1>
{{#each items}}
<div>{{inc @index}}. {{name}} - {{currency (mul qty price)}}</div>
{{/each}}
<p>Total: {{currency total}}</p>
{{qr code size=90}}Page settings for an html template: page.format (A4, A5, Letter...), page.width/page.height (custom sizes such as 80mm), page.landscape, page.marginTop|marginRight|marginBottom|marginLeft.
Set page.height = "auto" together with page.width for roll paper: the agent measures the content height and cuts there instead of feeding a whole 297mm sheet. Add page.autoHeightPadding (default 2mm) when the printer needs extra paper at the bottom.
Thermal printers / ESC/POS
Option 1 (recommended): an
htmltemplate withpage.width = 80mm, printed like any PDF.Option 2: a
texttemplate withoptions.raw = true; the agent sends raw bytes with ESC/POS commands. Theescposoptions areinit,cut,feed,codepage,encoding,openDrawer.On Windows, raw printing needs the printer shared and
printing.rawShareNameset (for example\\localhost\POS58).
Public tunnel
Turn it on in the Tunnel tab of the web UI, or through the API.
Cloudflare quick tunnel: needs
cloudflaredand an empty token. The agent runscloudflared tunnel --url http://127.0.0.1:7788and picks up the*.trycloudflare.comURL (it changes on every run).Cloudflare named tunnel: create the tunnel in Cloudflare Zero Trust, then fill in the token and hostname. The agent runs
cloudflared tunnel --url http://127.0.0.1:7788 run --token <token>, but a tunnel created from the dashboard always takes its ingress from there, so you must open Zero Trust > Networks > Tunnels > your tunnel > Public Hostname and point the hostname athttp://127.0.0.1:7788(orhttp://localhost:7788). Without that step the hostname answers 502/1033 even though the connector reports a successful connection.ngrok: needs
ngrokand an authtoken. A fixed domain can be attached.
Turn on tunnel.autoStart to have the agent open the tunnel on every start.
Once the tunnel is up, the agent calls https://<hostname>/api/health with a random header and checks the response, so it knows whether the public address really reaches this agent. When it does not, the warning field of GET /api/tunnel (and the Tunnel tab) says why. The agent also warns when another cloudflared process on the machine runs the same tunnel, because two connectors on one tunnel make Cloudflare split requests between them.
When exposing the agent to the Internet, always keep auth.enabled = true and share API keys only with systems that need them.
Web interface
The interface is built with React + TypeScript + Vite + Tailwind v4 + shadcn/ui; the source is in ui/, the build output in web/, served by the agent at http://<host>:7788.
yarn build # build the UI into web/
yarn ui:dev # run the Vite dev server (proxies /api and /ws to port 7788)When reached through a tunnel, the interface asks for an API key and stores it in the browser's localStorage. Sensitive actions (viewing/creating/deleting API keys, editing local-only settings) only work when the interface is opened on the agent machine.
Data layout
~/.printagent/
├── config.json # the whole configuration
├── templates/<id>/ # template.hbs|template.txt + meta.json
├── files/ # PDF/raw files of the jobs (cleaned up per queue.keepFilesHours)
├── printagent.db # job history (SQLite, plus -wal/-shm files)
└── logs/ # daily logsRunning in the background
One command for all three operating systems, with no administrator rights and no NSSM:
printagent service install # start together with the machine
printagent service status # check the status
printagent service uninstall # removeIt can also be toggled in the Settings tab of the web UI. Underneath it uses:
Operating system | Mechanism | File/task created |
macOS | launchd (per-user LaunchAgent) |
|
Linux | systemd user unit + |
|
Windows | Task Scheduler, ONLOGON trigger | the |
On macOS the background logs go to ~/.printagent/logs/service.log. On Linux, enable-linger lets the agent run even before a graphical session is opened. On Windows the task is registered from an XML file and runs through wscript, so no console window pops up at every logon; the shim file is at ~/.printagent/printagent-service.vbs.
Windows specifics
Package installation uses
winget(available since Windows 10 21H2). On a machine that only haschoco, PowerShell must run as administrator, otherwise the install command is refused and that step only warns.The first time the agent listens on
0.0.0.0, Windows Firewall asks for permission. Pressing Cancel still works over127.0.0.1; only access from other machines on the LAN is lost.New machines usually ship with "Microsoft Print to PDF", so the printer step does not block. Network printers must be added through Settings > Printers & scanners; the agent does not scan mDNS on Windows.
PDF printing prefers SumatraPDF (the wizard installs it). Without it the agent falls back to PowerShell
PrintTo, which is slower and ignores most print options.Raw printing (ESC/POS) needs the printer shared and
printing.rawShareNameset, for example\\localhost\POS58.
This server cannot be deployed
Maintenance
Related MCP Connectors
OCR, transcription, file extraction, and image generation for AI agents via MCP.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Your org's AI agents, tasks, runs, search, and brain files as MCP tools and resources.
MCP tools for AI agents: render URLs to image/PDF, check link health, convert HTML/CSV/JSON.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI assistants to print documents, manage print queues, and control printers on macOS/Linux systems via the CUPS printing system. Supports printing PDFs, text files, and other formats with options like duplex printing, landscape orientation, and multiple copies.613 npm12MIT
- AlicenseNot gradedqualityDmaintenanceEnables interaction with Memobird thermal printers to print text, HTML, web pages, and images directly from MCP-enabled clients. It includes tools for device binding, image conversion, and monitoring print job status.9 npm1ISC
- AlicenseAqualityDmaintenanceConnect AI agents to physical printers. Print receipts, shipping labels, and packing slips to your existing BizPrint-connected printers from Claude and other MCP clients.71MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to manage multiple 3D printers via MCP, including monitoring temperatures and progress, uploading and slicing models, controlling prints, and viewing cameras.19 npmMIT