list_printers
list_printers
Lists all configured printers with a canonical status (idle/processing/stopped/disabled/offline/error/unknown), canonical type (network/local/virtual), the host (IP/hostname when derivable), and current queue depth. Use at the start of any printer troubleshooting workflow.
Metadata
Name
list_printers
Updated
3 weeks ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/listPrinters.ts — list_printers skill
*
* Lists all configured printers with their status, type (local/network), and
* current queue depth. Use at the start of any printer troubleshooting workflow.
*
* Platform strategy
* -----------------
* darwin `lpstat -p -d` for printer list and default printer,
* `lpstat -a` for acceptance status
* win32 PowerShell Get-Printer with status and job count
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/listPrinters.ts
*/
import * as os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { parsePrinterStatuses } from "./_shared/lpstatStatus";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "list_printers",
description:
"Lists all configured printers with a canonical status (idle/processing/stopped/" +
"disabled/offline/error/unknown), canonical type (network/local/virtual), the " +
"host (IP/hostname when derivable), and current queue depth. Use at the start of " +
"any printer troubleshooting workflow.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {} as Record<string, z.ZodTypeAny>,
} as const;
// -- Types --------------------------------------------------------------------
interface PrinterEntry {
name: string;
/** Canonical across platforms: idle | processing | stopped | disabled | offline | error | unknown */
status: string;
isDefault: boolean;
/** Canonical across platforms: network | local | virtual | unknown */
type: string;
/** IP/hostname when derivable from the device URI/port (network printers); null for USB/virtual/Bonjour. */
host: string | null;
queueDepth: number;
}
interface ListPrintersResult {
printers: PrinterEntry[];
defaultPrinter: string | null;
total: number;
}
// -- PowerShell helper --------------------------------------------------------
async function runPS(script: string): Promise<string> {
const encoded = Buffer.from(script, "utf16le").toString("base64");
const { stdout } = await execAsync(
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
{ maxBuffer: 20 * 1024 * 1024 },
);
return stdout.trim();
}
// -- darwin implementation ----------------------------------------------------
async function listPrintersDarwin(): Promise<ListPrintersResult> {
// Get printer status lines
let lpstatOut = "";
try {
({ stdout: lpstatOut } = await execAsync("lpstat -p -d 2>/dev/null", {
maxBuffer: 5 * 1024 * 1024,
}));
} catch (err) {
lpstatOut = (err as { stdout?: string }).stdout ?? "";
}
// Get acceptance status
let acceptOut = "";
try {
({ stdout: acceptOut } = await execAsync("lpstat -a 2>/dev/null", {
maxBuffer: 5 * 1024 * 1024,
}));
} catch { /* ignore */ }
// Get queue depths via lpstat -o
let queueOut = "";
try {
({ stdout: queueOut } = await execAsync("lpstat -o 2>/dev/null", {
maxBuffer: 5 * 1024 * 1024,
}));
} catch { /* ignore */ }
// Parse default printer
let defaultPrinter: string | null = null;
const defaultMatch = lpstatOut.match(/system default destination:\s+(\S+)/);
if (defaultMatch) defaultPrinter = defaultMatch[1];
// Count jobs per printer
const queueDepths: Map<string, number> = new Map();
for (const line of queueOut.split("\n").filter(Boolean)) {
// job lines: "PrinterName-NNN owner size date"
const jobMatch = line.match(/^([^-\s]+)-\d+\s/);
if (jobMatch) {
const pname = jobMatch[1];
queueDepths.set(pname, (queueDepths.get(pname) ?? 0) + 1);
}
}
// Parse lpstat -p output via the shared canonical parser (handles the
// "disabled since" / no-"is" wording that the old inline regex missed —
// see _shared/lpstatStatus.ts). Kept in lockstep with check_print_queue.
const printers: PrinterEntry[] = [];
const statuses = parsePrinterStatuses(lpstatOut);
const printerLines = lpstatOut.split("\n").filter((l) => l.startsWith("printer "));
for (const line of printerLines) {
const nameMatch = line.match(/^printer\s+(\S+)\s/);
if (!nameMatch) continue;
const name = nameMatch[1];
const status = statuses.get(name) ?? "unknown";
// Resolve canonical type + the host from the device URI. The URI is read
// but only the host (IP/hostname) is surfaced — the full URI carries printer
// serial numbers (PII + Layer-4 entropy). For ipp/ipps/socket/lpd the host
// is right there in the authority; Bonjour (dnssd://) resolves via mDNS so
// the URI has no literal host (left null → Step 3 fallback prompt).
let type = "unknown";
let host: string | null = null;
try {
const { stdout: uriOut } = await execAsync(
`lpstat -v \