# 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

[GitHub source](https://github.com/idemeum/skills/blob/main/listPrinters.ts)

#### Risk

Low

#### Requires consent

false

#### Affected scope

user

## Code

```typescript
/**
 * 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 \
