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]*@)?([^/:\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`;
 
  // Install driverless via the Microsoft IPP Class Driver (the Windows
  // equivalent of IPP Everywhere), matching this tool's documented command.
  // The previous 'Generic / Text Only' driver produced text-only garbage on
  // real printers. NOTE: win32 uses the IPP class driver regardless of
  // `protocol` — a legacy lpd/socket printer that needs a vendor driver must be
  // added with a specific PPD/INF by IT.
  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;
}) {
  // Expand ~ in driver_name so the LLM can pass "~/Downloads/HP.ppd" naturally.
  // lpadmin -P treats the path literally; without expansion the call would
  // fail with "PPD file not found" on a path like "<cwd>/~/Downloads/HP.ppd".
  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); });
}