Code

/**
 * mcp/skills/checkPrinterConnectivity.ts — check_printer_connectivity skill
 *
 * Tests TCP connectivity to a network printer on standard ports:
 * 9100 (raw/JetDirect), 631 (IPP), 80 (HTTP admin).
 * Use to determine if a printer issue is network-level vs driver/software level.
 *
 * Platform strategy
 * -----------------
 * darwin & win32  Pure Node.js net.createConnection() — platform independent.
 *                 For each port, attempts connection with timeout and records
 *                 success/latency.
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkPrinterConnectivity.ts
 */
 
import * as os  from "os";
import * as net from "net";
import { z }    from "zod";
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "check_printer_connectivity",
  description:
    "Tests TCP connectivity to a network printer on standard ports: " +
    "9100 (raw/JetDirect), 631 (IPP), 80 (HTTP admin). " +
    "Use to determine if a printer issue is network-level vs driver/software level.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    host: z
      .string()
      .describe("Printer IP address or hostname"),
    ports: z
      .array(z.number())
      .optional()
      .describe("Ports to test. Default: [9100, 631, 80]"),
    timeoutMs: z
      .number()
      .optional()
      .describe("Timeout per port in ms. Default: 3000"),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface PrinterPortResult {
  port:       number;
  service:    string;
  reachable:  boolean;
  latencyMs:  number | null;
}
 
// -- Helpers ------------------------------------------------------------------
 
function portService(port: number): string {
  switch (port) {
    case 9100: return "raw/JetDirect";
    case 631:  return "IPP";
    case 80:   return "HTTP admin";
    default:   return "unknown";
  }
}
 
function testPort(host: string, port: number, timeoutMs: number): Promise<PrinterPortResult> {
  return new Promise((resolve) => {
    const service   = portService(port);
    const startedAt = Date.now();
    let   resolved  = false;
 
    const finish = (result: PrinterPortResult) => {
      if (!resolved) {
        resolved = true;
        socket.destroy();
        resolve(result);
      }
    };
 
    const socket = net.createConnection({ host, port });
    socket.setTimeout(timeoutMs);
 
    socket.on("connect", () => {
      const latencyMs = Date.now() - startedAt;
      finish({ port, service, reachable: true, latencyMs });
    });
 
    socket.on("timeout", () => {
      finish({ port, service, reachable: false, latencyMs: null });
    });
 
    socket.on("error", () => {
      finish({ port, service, reachable: false, latencyMs: null });
    });
  });
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({
  host,
  ports     = [9100, 631, 80],
  timeoutMs = 3000,
}: {
  host:       string;
  ports?:     number[];
  timeoutMs?: number;
}) {
  const results    = await Promise.all(ports.map((p) => testPort(host, p, timeoutMs)));
  const isReachable = results.some((r) => r.reachable);
 
  // Build recommendation based on which ports are open
  const ipp631 = results.find((r) => r.port === 631 && r.reachable);
  let recommendation = "";
  if (ipp631) {
    recommendation = `IPP is available. Recommended printer URI: ipp://${host}/ipp/print`;
  } else if (results.find((r) => r.port === 9100 && r.reachable)) {
    recommendation = `Raw/JetDirect is available. Recommended printer URI: socket://${host}:9100`;
  } else if (!isReachable) {
    recommendation = "Printer is not reachable on any tested port. Check network connection, IP address, and that the printer is powered on.";
  } else {
    recommendation = "Some ports are open. Check printer documentation for the correct URI.";
  }
 
  return {
    host,
    ports: results,
    isReachable,
    recommendation,
    platform: os.platform(),
  };
}
 
// -- Smoke test ---------------------------------------------------------------
 
if (false) {
  run({ host: "192.168.1.100" })
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}