Skip to main content
Glama
huy97

printagent

by huy97

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

http://<host>:7788/api

Web apps, backends, point-of-sale

WebSocket

ws://<host>:7788/ws

Apps that need job events in real time

MCP

http://<host>:7788/mcp (HTTP) or stdio

Claude Code, Claude Desktop, AI agents

Web UI

http://<host>:7788

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 install downloads Puppeteer's Chrome build, and the setup wizard downloads it again if it is missing. If the machine already has Chrome, point render.chromePath at 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 | iex

The 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 run

Run it once without installing:

npx @hyydev/printagent start

Or 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 run

The 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 ~/.printagent directory

yes

created, write permission checked

Print driver

no

Linux: installs cups cups-client and starts the service

Printer

no

discovers IPP printers over mDNS (dns-sd/avahi-browse) and adds them to CUPS

Chromium

no

downloads the Puppeteer build, otherwise installs Google Chrome through brew/winget/apt

SumatraPDF (Windows)

no

downloads the portable build into ~/.printagent/tools (no admin rights), falls back to winget/choco, then writes printing.sumatraPath

API key

yes

creates a default key

Tunnel

no

installs cloudflared/ngrok when tunnel.provider is not none

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 browser

The 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 stdio

Language

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.locale and decides which starter template set is created.

  • Terminal wizard: printagent setup asks for the language first and stores the answer in the configuration.

  • API: each request picks its language from the x-locale header, the ?lang= query, the accept-language header, then agent.locale from the configuration (default en). Responses carry a content-language header, and every error also carries a stable key plus params so a client can translate it itself.

  • CLI, logs and the terminal setup screen: the PRINTAGENT_LANG environment variable, falling back to agent.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/printers

Authentication

The agent creates an API key on the first run (node bin/printagent.js key). Send it with every request:

x-api-key: pa_xxx

or 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 Host header 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-Site is same-origin or none, and Origin (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 no Origin.

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 with printing.allowLocalFilePath = true and declare printing.allowedFileRoots to limit the folders.

  • url accepts http/https only, blocks loopback and private network ranges (re-enable with printing.allowPrivateNetworkUrl), and caps the size at printing.maxDownloadMb (default 64MB).

  • CORS allows no origin by default; add your domain to server.corsOrigins if 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, where Origin matches Host).

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-after header. REST and the WebSocket share one counter, so reopening a WebSocket connection is not a way around it. The address comes from cf-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 = false or while no API key exists, with a message saying what to fix.

  • auth.enabled cannot be turned off while the tunnel is running; stop the tunnel first.

  • Every response carries x-content-type-options: nosniff, referrer-policy: no-referrer and x-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

GET /.well-known/printagent.json

Discovery endpoint: version, where to read the docs, the three REST/WebSocket/MCP entry points, how to authenticate

GET /openapi.json

OpenAPI 3.1 specification of all 36 REST endpoints, ready to feed a client generator or a tool definition

GET /llms.txt

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

/api/health

Agent status (no key needed)

GET

/api/info

Detailed information, renderer check

GET

/api/printers?refresh=1

Printer list (rescans when refresh=1)

POST

/api/printers/scan

Rescan printers

GET

/api/printers/:name

Printer details and options

POST

/api/printers/:name/default

Set the default printer

POST

/api/printers/:name/test

Print a test page

GET/POST

/api/templates

List / create a template

GET/PUT/DELETE

/api/templates/:id

Read / update / delete a template

POST

/api/templates/:id/preview

Render a template into a PDF

GET

/api/templates/seeds

List the templates in a starter set (?lang=vi|en)

POST

/api/templates/seed

Create the starter set for a language ({"locale":"en"})

GET

/api/jobs

Job list (?status=, ?limit=)

GET

/api/jobs/:id

Job details

GET

/api/jobs/:id/file

Download the printed file

POST

/api/jobs/:id/cancel

Cancel a job

POST

/api/jobs/:id/retry

Print a job again

GET/PUT

/api/settings

Read / update the configuration

GET/POST/DELETE

/api/apikeys

Manage API keys

GET

/api/tunnel

Tunnel status and tooling

POST

/api/tunnel/start, /api/tunnel/stop

Start / stop the tunnel

GET

/api/setup

Initial setup result and background service status

POST

/api/setup/run

Run setup in the background, returning progress immediately (local machine only)

GET

/api/setup/progress

Progress of the current run: current step, logs, result (local machine only)

POST

/api/setup/service

{"action":"install"|"uninstall"} (local machine only)

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

invoice-a4-en

A4

Sales invoice with QR code and amount in words

bill-80mm-en

80mm

Thermal receipt rendered through PDF

receipt-escpos-en

raw

ESC/POS receipt, plain text

tax-invoice-a4-en

A4

Tax invoice: VAT numbers on both sides, per-line tax rate, signature blocks

pos-receipt-80mm-en

80mm

POS tax receipt with amount paid and change

cash-receipt-a5-en

A5

Cash receipt: book and receipt number, five signature blocks

The Vietnamese set:

Id

Size

Used for

invoice-a4

A4

Simple sales invoice

bill-80mm

80mm

Thermal printer receipt rendered through PDF

receipt-escpos

raw

Plain text receipt sent straight to a thermal printer

vat-invoice-a4

A4

VAT invoice, printed representation of an e-invoice

pos-invoice-80mm

80mm

E-invoice issued from a cash register

cash-receipt-a5

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, now and amountWords follow the agent language unless the template passes locale= / currency=: en-US and USD for English, vi-VN and VND for 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 uses dateStyle/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 html template with page.width = 80mm, printed like any PDF.

  • Option 2: a text template with options.raw = true; the agent sends raw bytes with ESC/POS commands. The escpos options are init, cut, feed, codepage, encoding, openDrawer.

  • On Windows, raw printing needs the printer shared and printing.rawShareName set (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 cloudflared and an empty token. The agent runs cloudflared tunnel --url http://127.0.0.1:7788 and picks up the *.trycloudflare.com URL (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 at http://127.0.0.1:7788 (or http://localhost:7788). Without that step the hostname answers 502/1033 even though the connector reports a successful connection.

  • ngrok: needs ngrok and 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 logs

Running 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   # remove

It 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)

~/Library/LaunchAgents/com.printagent.agent.plist

Linux

systemd user unit + loginctl enable-linger

~/.config/systemd/user/printagent.service

Windows

Task Scheduler, ONLOGON trigger

the PrintAgent task

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 has choco, 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 over 127.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.rawShareName set, for example \\localhost\POS58.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables 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.
    6
    13 npm
    12
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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 npm
    1
    ISC
  • A
    license
    A
    quality
    D
    maintenance
    Connect AI agents to physical printers. Print receipts, shipping labels, and packing slips to your existing BizPrint-connected printers from Claude and other MCP clients.
    7
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to manage multiple 3D printers via MCP, including monitoring temperatures and progress, uploading and slicing models, controlling prints, and viewing cameras.
    19 npm
    MIT