add_printer
add_printer
Adds a network printer by IP address or hostname using IPP (recommended) or other protocols. Use after remove_printer to re-add a printer with fresh configuration.
Metadata
Name
add_printer
Updated
3 weeks ago
Source
Risk
High
Requires consent
true
Affected scope
system
Code
/**
* mcp/skills/addPrinter.ts — add_printer skill
*
* Adds a network printer by IP address or hostname using IPP (recommended)
* or other protocols. Use after remove_printer to re-add a printer with
* fresh configuration.
*
* Platform strategy
* -----------------
* darwin Constructs URI and runs `lpadmin -p -E -v {uri} -m everywhere`
* (or -P {driverPpd} if provided)
* win32 PowerShell Add-PrinterPort + Add-Printer
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/addPrinter.ts
*/
import * as os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { expandTilde } from "./_shared/expandTilde";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "add_printer",
description:
"Adds a network printer by IP address or hostname using IPP (recommended) " +
"or other protocols. " +
"Use after remove_printer to re-add a printer with fresh configuration.",
riskLevel: "high",
destructive: false,
requiresConsent: true,
supportsDryRun: false,
affectedScope: ["system"],
auditRequired: true,
escalationHint: {
darwin: "sudo lpadmin -p \"<name>\" -E -v \"ipp://<host>\" -m everywhere # substitute display name and printer IP/hostname",
win32: "Add-PrinterPort -Name \"<name>_Port\" -PrinterHostAddress \"<host>\"; Add-Printer -Name \"<name>\" -PortName \"<name>_Port\" -DriverName \"Microsoft IPP Class Driver\" # run from elevated PowerShell",
},
schema: {
// Keys match the privileged helper's struct Params exactly (snake_case):
// `printer_name`, `device_uri`, `driver_name`. G4 forwards executor params
// verbatim to the helper (HELPER-IPC-PROTOCOL.md / HELPER-HANDLERS.md). The
// caller supplies a COMPOSED device URI rather than host+protocol so the
// local dry-run path and the helper's real run agree on one input shape.
printer_name: z
.string()
.describe("Display name for the printer"),
device_uri: z
.string()
.describe(
"Full device URI. Compose from the printer's IP/hostname: " +
"`ipp://<host>/ipp/print` (recommended), `lpd://<host>/`, or " +
"`socket://<host>:9100`. Scheme must be ipp / lpd / socket.",
),
driver_name: z
.string()
.optional()
.describe("PPD file path or driver model. Omit for IPP Everywhere / the Microsoft IPP Class Driver (auto-driver)."),
},
} as const;
// -- Types --------------------------------------------------------------------
interface AddPrinterResult {
name: string;
uri: string;
added: boolean;
message: string;
}
// -- 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: 10 * 1024 * 1024 },
);
return stdout.trim();
}
// -- Helpers ------------------------------------------------------------------
/** Extract the host from a device URI (e.g. ipp://host/ipp/print → host). */
function hostFromUri(uri: string): string | null {
return uri.match(/^[a-z]+:\/\/([^/:\s]+)/i)?.[1] ?? null;
}
// -- darwin implementation ----------------------------------------------------
async function addPrinterDarwin(
name: string,
uri: string,
driverName?: string,
): Promise<AddPrinterResult> {
const safeName = name.replace(/'/g, "'\''");
const driverPart = driverName
? `-P '${driverName.replace(/'/g, "'\''")}''`
: "-m everywhere";
const cmd = `lpadmin -p '${safeName}' -E -v '${uri}' ${driverPart}`;
let added = false;
let errorMsg = "";
try {
await execAsync(cmd);
added = true;
} catch (err) {
errorMsg = (err as Error).message ?? String(err);
added = false;
}
const message = added
? `Printer \"${name}\" added successfully using URI: ${uri}`
: `Failed to add printer \"${name}\": ${errorMsg}. You may need administrator privileges.`;
return { name, uri, added, message };
}
// -- win32 implementation -----------------------------------------------------
async function addPrinterWin32(
name: string,
uri: string,
_driverName?: string,
): Promise<AddPrinterResult> {
const host = hostFromUri(uri);
if (!host) {
return { name, uri, added: false, message: `Could not parse a host from device URI \"${uri}\".` };}
const safeName = name.replace(/'/g, "''");
const safeHost = host.replace(/'/g, "''");
const safePort = \
`${safeName}_Port`;
const ps = `
$ErrorActionPreference = 'Stop'
# Add a TCP/IP port pointing at the printer (idempotent).
try { Add-PrinterPort -Name '${safePort}' -PrinterHostAddress '${safeHost}' } catch {}
Add-Printer -Name '${safeName}' -DriverName 'Microsoft IPP Class Driver' -PortName '${safePort}'
'success`.trim();
let added = false;
let errorMsg = "";
try {
const result = await runPS(ps);
added = result.toLowerCase().includes("success");
} catch (err) {
errorMsg = (err as Error).message ?? String(err);
added = false;
}
const message = added
? `Printer \"${name}\" added successfully. URI would be: ${uri}`
: `Failed to add printer \"${name}\": ${errorMsg}. Ensure you have administrator privileges; the Microsoft IPP Class Driver ships with Windows 10/11.`;
return { name, uri, added, message };
}
// -- Exported run function ----------------------------------------------------
export async function run({
printer_name: name,
device_uri: uri,
driver_name: driverName,
}: {
printer_name: string;
device_uri: string;
driver_name?: string;
}) {
const resolvedDriver = expandTilde(driverName);
const platform = os.platform();
return platform === "win32"
? addPrinterWin32(name, uri, resolvedDriver)
: addPrinterDarwin(name, uri, resolvedDriver);
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({ printer_name: "TestPrinter", device_uri: "ipp://192.168.1.100/ipp/print" })
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}