/**
* 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" ;
const execAsync = promisify (exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "list_printers" ,
description:
"Lists all configured printers with their status, type (local/network), 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 ;
status : string ;
isDefault : boolean ;
type : string ;
location : 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
// Lines: "printer NAME is idle. enabled since ..."
// "printer NAME is stopped. Reason: ..."
const printers : PrinterEntry [] = [];
const printerLines = lpstatOut. split ( " \n " ). filter (( l ) => l. startsWith ( "printer " ));
for ( const line of printerLines) {
const nameMatch = line. match ( / ^ printer \s + ( \S + ) \s / );
const statusMatch = line. match ( / is \s + (idle | stopped | processing | disabled) / i );
if ( ! nameMatch) continue ;
const name = nameMatch[ 1 ];
const status = statusMatch ? statusMatch[ 1 ] : "unknown" ;
// Determine type from acceptance output (location not reliably available via lpstat)
// We look for "IPP" or "LPD" or socket patterns in lpinfo (optional, skip if slow)
let type = "unknown" ;
let location : string | null = null ;
// Try to get device-uri for this printer to determine type
try {
const { stdout : uriOut } = await execAsync (
`lpstat -v "${ name . replace ( / " / g , ' \\ "' ) }" 2>/dev/null` ,
{ maxBuffer: 1 * 1024 * 1024 },
);
if (uriOut. includes ( "ipp://" ) || uriOut. includes ( "ipps://" )) {
type = "network (IPP)" ;
} else if (uriOut. includes ( "lpd://" )) {
type = "network (LPD)" ;
} else if (uriOut. includes ( "socket://" )) {
type = "network (socket)" ;
} else if (uriOut. includes ( "usb://" )) {
type = "local (USB)" ;
} else if (uriOut. includes ( "file:" ) || uriOut. includes ( "pdf" )) {
type = "virtual" ;
} else {
type = "local" ;
}
const locMatch = uriOut. match ( / device for [ ^ :] + : \s + ( \S + ) / );
if (locMatch) location = locMatch[ 1 ];
} catch { /* skip */ }
printers. push ({
name,
status,
isDefault: name === defaultPrinter,
type,
location,
queueDepth: queueDepths. get (name) ?? 0 ,
});
}
return { printers, defaultPrinter, total: printers. length };
}
// -- win32 implementation -----------------------------------------------------
async function listPrintersWin32 () : Promise < ListPrintersResult > {
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$printers = Get-Printer | Select-Object Name,DriverName,PortName,PrinterStatus,JobCount,Location,Shared,Type
$default = (Get-ItemProperty -Path 'HKCU: \\ Software \\ Microsoft \\ Windows NT \\ CurrentVersion \\ Windows' -Name 'Device' -ErrorAction SilentlyContinue).Device
[PSCustomObject]@{
printers = $printers | ConvertTo-Json -Depth 2 -Compress
default = if ($default) { ($default -split ',')[0].Trim() } else { $null }
} | ConvertTo-Json -Depth 3 -Compress` . trim ();
const raw = await runPS (ps);
if ( ! raw) return { printers: [], defaultPrinter: null , total: 0 };
let outer : { printers : string ; default : string | null };
try {
outer = JSON . parse (raw);
} catch {
return { printers: [], defaultPrinter: null , total: 0 };
}
const defaultPrinter = outer.default ?? null ;
let printers : PrinterEntry [] = [];
if (outer.printers) {
try {
const arr = JSON . parse (outer.printers);
const list = Array. isArray (arr) ? arr : [arr];
printers = (list as Record < string , unknown >[]). map (( p ) => ({
name: String (p.Name ?? "Unknown" ),
status: String (p.PrinterStatus ?? "Unknown" ),
isDefault: String (p.Name) === defaultPrinter,
type: p.Shared ? "network (shared)" : String (p.Type ?? "local" ),
location: p.Location ? String (p.Location) : null ,
queueDepth: typeof p.JobCount === "number" ? p.JobCount : 0 ,
}));
} catch { /* ignore */ }
}
return { printers, defaultPrinter, total: printers. length };
}
// -- Exported run function ----------------------------------------------------
export async function run ( _args : Record < string , never > = {}) : Promise < ListPrintersResult > {
const platform = os. platform ();
return platform === "win32"
? listPrintersWin32 ()
: listPrintersDarwin ();
}
// -- Smoke test ---------------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}