npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cybrlab/preclick-mcp-client

v0.1.0

Published

JavaScript client for the PreClick URL security scanning service. Assess target URLs for potential threats and alignment with the user's browsing intent before navigation.

Readme

@cybrlab/preclick-mcp-client

Simple JavaScript/Node.js client for the PreClick URL security scanning service. Assess target URLs for potential threats and alignment with the user's browsing intent before navigation.

Publisher: CybrLab.ai | Service: PreClick

Scan-oriented public API: two primary methods (scan, scanWithIntent) plus five job methods for explicit control. No protocol vocabulary. No connect() required. No polling boilerplate for the common case.

Free to use — up to 100 requests per day with no API key and no sign-up required. For higher limits, configure an API key (see Configuration).


Install

npm install @cybrlab/preclick-mcp-client

Requires Node.js >= 20.

Quick start

import { PreClickClient } from "@cybrlab/preclick-mcp-client";

const client = new PreClickClient({
  apiKey: process.env.PRECLICK_API_KEY, // optional; trial mode if omitted
});

const result = await client.scan("https://example.com");

console.log(result.agent_access_directive); // ALLOW | DENY | RETRY_LATER | REQUIRE_CREDENTIALS
console.log(result.agent_access_reason);

await client.close();

That's it. The first call auto-connects to the PreClick service and returns the scan result as soon as it's ready (typically 70–80 seconds). close() cleans up when you're done.

See examples/ for runnable scripts: basic-scan.mjs, intent-scan.mjs, long-running-scan.mjs, and manual-polling.mjs.

Intent-aware scanning

When the user has stated their purpose (login, purchase, download, booking, etc.), use scanWithIntent so the scanner can evaluate destination alignment in addition to threat signals:

const result = await client.scanWithIntent(
  "https://example.com",
  "log in to my bank",
);

console.log(result.agent_access_directive);
console.log(result.intent_alignment);
// misaligned | no_mismatch_detected | inconclusive | not_provided

Interpreting results

Every completed scan returns an object with these fields:

| Field | Type | Description | |--------------------------|-----------------|-------------------------------------------------------------------------| | risk_score | float (0.0–1.0) | Threat probability | | confidence | float (0.0–1.0) | Analysis confidence | | analysis_complete | boolean | Whether the analysis finished fully | | agent_access_directive | string | ALLOW, DENY, RETRY_LATER, or REQUIRE_CREDENTIALS | | agent_access_reason | string | Normalized reason code for the directive | | intent_alignment | string | misaligned, no_mismatch_detected, inconclusive, or not_provided |

Use agent_access_directive for navigation decisions:

  • ALLOW — Proceed with navigation.
  • DENY — Do not navigate. Check agent_access_reason for the cause.
  • RETRY_LATER — Verification could not complete (temporary issue). Retry.
  • REQUIRE_CREDENTIALS — The target requires authentication. Ask the user how to proceed.

Scans typically take around 70–80 seconds on current production traffic. Both scan() and scanWithIntent() handle the polling internally; the default wait window is 10 minutes. Pass { maxWaitMs } to override:

await client.scan("https://example.com", { maxWaitMs: 120_000 });

Cancellation

Pass an AbortSignal via options.signal to cancel submission or a long-running wait. Abort works for scan(), scanWithIntent(), and waitForScan():

const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000); // give up after 30s

try {
  const result = await client.scan("https://example.com", {
    signal: controller.signal,
  });
} catch (err) {
  if (err.name === "AbortError") {
    // If submission had already completed, err.scanId is attached so you
    // can resume later via client.waitForScan(err.scanId).
    if (err.scanId) console.log("Cancelled; can resume scan", err.scanId);
  } else {
    throw err;
  }
}

Any error thrown after a scan is submitted (timeout exhaustion, abort, transport failure) carries err.scanId, so the submitted and already quota-charged scan is never orphaned — you can resume it with waitForScan(scanId).

Long-running scans (explicit control)

For the common case, scan() / scanWithIntent() is all you need. Use the job methods below when you need explicit control over the submission/wait lifecycle — for example, reporting progress to a UI, integrating with an existing job queue, or persisting a scan ID across processes or workers.

Submit once, wait later

const submission = await client.startScan("https://example.com");
// ... hand `submission.scanId` off to another function/worker/process ...
const result = await client.waitForScan(submission.scanId);
console.log(result.agent_access_directive);

scan(url) is literally startScan(url) followed by waitForScan(submission.scanId) — splitting it lets you control the two steps independently.

Submit and poll manually

If you need a progress UI or you can't block on a single call, use getScanResult() in your own loop. Handle terminal non-completed statuses explicitly so you don't loop forever on a failed / cancelled / expired envelope, and validate retryAfterMs before using it so a malformed hint can't trigger a hot loop:

const submission = await client.startScan("https://example.com");

while (true) {
  const envelope = await client.getScanResult(submission.scanId);

  if (envelope.status === "completed") {
    if (!envelope.result) {
      throw new Error("scan completed but result is missing");
    }
    console.log(envelope.result.agent_access_directive);
    break;
  }

  if (envelope.status !== "working") {
    throw new Error(`scan ended with status: ${envelope.status}`);
  }

  // Respect the server's recommended poll interval, but fall back if
  // retryAfterMs is missing or malformed.
  const hint = envelope.retryAfterMs;
  const waitMs =
    typeof hint === "number" && Number.isFinite(hint) && hint > 0
      ? hint
      : 2000;
  await new Promise((r) => setTimeout(r, waitMs));
}

(This is exactly what waitForScan() does internally — copy the pattern above only if you need to customize one of those steps.)

getScanStatus(scanId) is the cheaper status-only variant — it returns the current status without the result payload.

API reference

Primary scan API (convenience wrappers)

// Scan a URL and return the result. Handles submission + polling internally.
await client.scan(url);
await client.scan(url, { maxWaitMs: 120_000 });
await client.scan(url, { signal: controller.signal });

// Intent-aware variant. Use when the user has stated their purpose.
await client.scanWithIntent(url, intent);
await client.scanWithIntent(url, intent, { maxWaitMs: 120_000 });
await client.scanWithIntent(url, intent, { signal: controller.signal });

Both return a PreClickScanResult directly (see Return shapes below). Once submission has completed, timeout, abort, or transport failures carry err.scanId so you can resume polling without re-submitting — see Cancellation.

Job methods (explicit control)

// Submit a URL for scanning. Returns a submission envelope with `scanId`.
await client.startScan(url);
await client.startScan(url, { signal: controller.signal });

// Submit with user intent.
await client.startScanWithIntent(url, intent);
await client.startScanWithIntent(url, intent, { signal: controller.signal });

// Non-blocking status check for a scan.
await client.getScanStatus(scanId);
await client.getScanStatus(scanId, { signal: controller.signal });

// Non-blocking result fetch. Returns `{ status, result, retryAfterMs, ... }`.
// `result` is populated when `status === "completed"`.
await client.getScanResult(scanId);
await client.getScanResult(scanId, { signal: controller.signal });

// Wait for a scan to complete and return the inner scan result payload.
// Polls internally at the server-recommended cadence. Pass `signal` to
// cancel; any error thrown after submission carries `err.scanId` for resume.
await client.waitForScan(scanId);
await client.waitForScan(scanId, { maxWaitMs: 600_000 });
await client.waitForScan(scanId, { signal: controller.signal });

All seven scan and job methods accept { signal } for cancellation. On abort, an AbortError propagates; after a scan has been submitted, err.scanId is attached so you can resume polling without re-submitting. The five job methods reject unknown option keys — you cannot pass maxWaitMs to startScan or getScanResult, only signal.

Lifecycle

await client.close();          // clean up (alias for disconnect)
client.isConnected;            // boolean
await client.connect();        // optional; first scan call auto-connects
await client.disconnect();     // same as close(); close() is preferred

Return shapes

scan / scanWithIntent / waitForScan

All three return the inner scan result directly:

{
  risk_score: 0.15,          // float 0.0–1.0
  confidence: 0.92,          // float 0.0–1.0
  analysis_complete: true,
  agent_access_directive: "ALLOW",  // ALLOW | DENY | RETRY_LATER | REQUIRE_CREDENTIALS
  agent_access_reason: "no_immediate_risk_detected",
  intent_alignment: "not_provided"  // misaligned | no_mismatch_detected | inconclusive | not_provided
}

The job methods below return scan-job envelopes instead — see each for the shape.

startScan / startScanWithIntent

{
  scanId: "550e8400-e29b-41d4-a716-446655440000",
  status: "working",
  statusMessage: "Queued for processing",
  createdAt: "2026-01-18T12:00:00Z",
  updatedAt: "2026-01-18T12:00:00Z",
  ttlMs: 720000,
  pollIntervalMs: 2000,
  message: "Scan submitted."
}

getScanStatus

{
  scanId: "...",
  status: "working" | "completed" | "failed" | "cancelled",
  statusMessage: "...",
  createdAt: "...",
  updatedAt: "...",
  ttlMs: 720000,
  pollIntervalMs: 2000
}

getScanResult (still running)

{
  scanId: "...",
  status: "working",
  statusMessage: "...",
  result: null,
  retryAfterMs: 2000,
  message: "Scan still in progress."
}

getScanResult (completed)

{
  scanId: "...",
  status: "completed",
  statusMessage: "Scan completed successfully",
  result: {
    risk_score: 0.15,
    confidence: 0.92,
    analysis_complete: true,
    agent_access_directive: "ALLOW",
    agent_access_reason: "no_immediate_risk_detected",
    intent_alignment: "not_provided"
  },
  retryAfterMs: null,
  message: "Scan completed successfully."
}

Configuration

new PreClickClient(options) accepts:

| Option | Type | Default | Description | |--------------------|----------|---------------------------|--------------------------------------------------------------------------------------------------------------| | apiKey | string | null | PreClick API key. Sent as X-API-Key. Trial mode (100 req/day) if omitted. Must be non-empty when provided. | | endpoint | string | https://preclick.ai/mcp | Override the PreClick endpoint URL. | | clientName | string | preclick-mcp-client | Reported client name. | | clientVersion | string | package version | Reported client version. Defaults to the installed package version (read at load). | | headers | object | {} | Extra HTTP headers to send with every request. Values must be strings. | | requestTimeoutMs | number | 600000 | Per-request timeout (ms). Must be a positive integer. | | logger | object | no-op | Logger with optional .info / .warn / .error functions (e.g. console). |

To obtain an API key for higher limits, contact [email protected].

Errors

The client throws three error types (all subclasses of PreClickError):

import {
  PreClickError,
  PreClickConnectionError,
  PreClickRemoteError,
} from "@cybrlab/preclick-mcp-client";

try {
  const result = await client.scan("https://example.com");
} catch (err) {
  if (err.name === "AbortError") {
    // Caller cancelled via AbortSignal. If err.scanId is present, submission
    // had already completed and you can resume with client.waitForScan(err.scanId).
  } else if (err instanceof PreClickConnectionError) {
    // Transport / connection failure (network, DNS, TLS, handshake).
    // Check err.retryable — false for malformed-endpoint config errors,
    // true for transient sockets/DNS/TLS failures.
    if (err.retryable) {
      // safe to retry with backoff
    }
  } else if (err instanceof PreClickRemoteError) {
    // Remote-side failure: scan failed / cancelled / expired, rate limit,
    // auth failure, etc. Inspect err.code and err.data for context.
    console.error(err.code, err.data);
  } else if (err instanceof PreClickError) {
    // Misuse (invalid arguments) or waitForScan maxWaitMs exhausted.
    // For waitForScan exhaustion, err.scanId holds the orphaned scan so
    // you can resume polling later instead of re-submitting.
  }
}

Troubleshooting

| Symptom | Cause | Fix | |----------------------------------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------| | PreClickConnectionError: Failed to connect ... | Endpoint unreachable | Check network; verify curl -I https://preclick.ai/mcp returns a response | | PreClickConnectionError: Invalid PreClick endpoint ... | Invalid endpoint option | Pass a valid https:// URL as the endpoint option | | PreClickRemoteError: 401 ... | API key required or invalid | Set a valid apiKey in client options | | PreClickRemoteError: 429 ... | Rate limit exceeded | Reduce frequency or add an API key for higher limits | | PreClickError: waitForScan exhausted ... | Scan did not complete within window | Pass a larger { maxWaitMs } to scan / scanWithIntent / waitForScan, or use manual polling | | Scan takes a long time | Target site is slow or complex | Wait for completion; scans typically take 70–80 seconds |

Bundled agent skill

skills/preclick/SKILL.md is a generic agent skill that instructs LLM agents on when to call scan vs scanWithIntent, and how to interpret the response. The skill only covers the primary API; the job methods (startScan / waitForScan / etc.) are mentioned briefly as an advanced option for explicit lifecycle control. Drop it into any agent runtime that loads markdown skills (Claude Code, custom harnesses, etc.) to give the agent automatic preflight URL verification behavior.

Important notice

This package is client software licensed under the Apache License 2.0. Use of the hosted PreClick service is subject to applicable service terms, acceptable-use rules, rate limits, and law. Do not use the hosted service for unauthorized, unlawful, abusive, or malicious activity.

Scan results are informational risk signals, not a guarantee that a URL is safe or unsafe. They are not a substitute for user judgment, browser and endpoint security controls, organizational security review, or legal and compliance review.

Support

License

Copyright 2026 CybrLab.ai.

Licensed under the Apache License 2.0. See LICENSE.