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

@kblockai/client

v0.1.0-rc.1

Published

Connect an existing agent to the K-Block Gateway: an MCP client over Streamable HTTP with typed K-Block outcomes (completed, approval pending, denied with the policy's explanation…), approval waiting and resumption, typed errors, and an optional LangChain

Readme

@kblockai/client

Protect an existing agent with K-Block by pointing its tool traffic at the K-Block Gateway and adding an API key. The Gateway is an MCP server (Streamable HTTP) in front of your organization's MCP servers. It applies your organization's policy, holds calls for human approval, executes approved calls exactly once and records a verifiable receipt for each. This package is a thin client for it: it decides nothing itself, speaks the Gateway's MCP conventions and gives you typed outcomes.

Pre-release (0.1.0-rc.1) for invited pilot users. You need a K-Block Gateway URL and an agent's gateway API key (kb_live_… / kb_test_…, shown once when it is created in the K-Block dashboard). K-Block gives you both.

Requirements

  • Node.js 22 or later (tested on Node.js 22.17).
  • ESM only: "type": "module" in your package.json, or .mjs files.
  • Runtime dependency: @modelcontextprotocol/sdk (^1.30.1). LangChain.js support is optional: install @langchain/core 1.x next to this package to use @kblockai/client/langchain.
  • TypeScript (optional): module / moduleResolution nodenext (or bundler with a bundler), types: ["node"], and the DOM lib or skipLibCheck: true (the MCP SDK's declarations use HeadersInit). Declarations are included.

Install

npm install --save-exact @kblockai/[email protected]

With LangChain.js:

npm install --save-exact @kblockai/[email protected] @langchain/core@^1

A first program

The code on this page is taken unchanged from complete example programs (quickstart.mjs, agent.ts, langchain_agent.ts, verify_receipt.mjs). quickstart.mjs makes three calls: one the policy allows, one that needs a person's approval, one the policy denies.

import { randomUUID } from "node:crypto";
import { connectKBlock, explainResult } from "@kblockai/client";

const gatewayUrl = process.env.KBLOCK_GATEWAY_URL;
const apiKey = process.env.KBLOCK_API_KEY;
if (!gatewayUrl || !apiKey) {
  console.error("Set KBLOCK_GATEWAY_URL and KBLOCK_API_KEY to the Gateway URL and the agent's gateway API key that K-Block gave you.");
  process.exit(2);
}

const client = await connectKBlock({ gatewayUrl, apiKey });

// 1. Allowed: the Gateway executes the call exactly once and records a receipt.
const r = await client.callTool("lookup_order", { order_id: `ord_${randomUUID()}` });
if (r.status === "completed") console.log("1. completed:", r.structuredContent, "receipt", r.receiptId, `(${r.receiptObjectId})`);
else console.log("1.", explainResult(r));

// 2. Approval required: nothing runs until a person approves the call in the K-Block dashboard;
//    waitForApproval repeats the same call until the decision lands, and it then executes once.
const pending = await client.callTool("refund", { order_id: `ord_${randomUUID()}`, amount: "250.00", currency: "USD" });
console.log("2.", explainResult(pending));
if (pending.status === "approval_pending") {
  console.log(`   waiting for a person to approve request ${pending.approvalRequestId} in the K-Block dashboard`);
  const done = await client.waitForApproval(pending, { timeoutSeconds: 600, waitSecondsPerRequest: 30 });
  if (done.status === "completed") console.log("   completed:", done.structuredContent, "receipt", done.receiptId, `(${done.receiptObjectId})`);
  else console.log("  ", explainResult(done));
}

// 3. Denied: the policy's explanation; nothing was executed.
const denied = await client.callTool("refund", { order_id: `ord_${randomUUID()}`, amount: "2500.00", currency: "USD" });
console.log("3.", explainResult(denied));

await client.close();
KBLOCK_GATEWAY_URL="<Gateway URL>" KBLOCK_API_KEY="<gateway API key>" node quickstart.mjs

The client reads no environment variable itself: your application passes the Gateway URL and the key to connectKBlock, from wherever it keeps its configuration. The program assumes an organization whose tools lookup_order (action orders.read) and refund (action refund.issue, with its amount and currency) are under a policy made from the Support refunds template (support-refunds@1, default limits); with other tools, use their names, arguments and amounts. Amounts are decimal strings ("250.00"): the Gateway refuses non-integer JSON numbers, because it signs the exact arguments into the evidence.

Outcomes

callTool(name, args, { idempotencyKey?, waitSeconds?, signal?, timeoutMs? }) returns a KBlockCallResult, discriminated by status:

| status | Meaning | Useful members | |---|---|---| | completed | executed; the upstream's answer | content, structuredContent, actionId, receiptId, observe? | | approval_pending | a person must approve the exact call; nothing ran | actionId, approvalRequestId, expiresAt, policy? | | denied | refused by the policy or a check; nothing ran | code, detail, policy?, why? | | rejected / expired / cancelled | a person rejected it, it expired, or it was cancelled | code, receipt? | | unavailable | nothing ran right now | code, retryable | | upstream_error | the upstream failed (it may have acted: see detail) | code, receipt?, upstream? | | invalid_request / plan_limit / replay / unauthorized | other refusals | code, detail | | unrecognized | a refusal status this version does not know (never a success) | gatewayStatus |

In the policy's terms: a call the policy allows is executed (completed); a call that requires approval is approval_pending until a person decides (then completed, rejected or expired); a call the policy denies is denied with the code POLICY_DENIED. denied also covers checks K-Block makes before the policy, each with its own code: for example INVALID_AMOUNT when the call's amount cannot be read or, for a tool that reads the call's currency, names a currency other than the tool's, or NO_APPROVERS when approval is required but no member who may approve has a personal K-Block key. A completed result does not say whether a person approved the call first; its receipt does. observe is present when the call was executed under an observe window, which relaxes the policy's default deny (and, if the owner chose so, an approval rule without a guard) while a new policy is observed.

Every result carries call (tool, arguments, idempotency key) and raw (the Gateway's answer). policy is the explanation the organization discloses to agents: policyRef (name@version), decidedBy (the rule), conditions with thresholds (e.g. amount 2500 USD > 1000 USD) and amountBand only when the organization opts in to the conditions disclosure (the default rule sends neither; at none, policy and why are both absent), approval (roles, approver count, TTL) and a one-line message. It never contains user IDs, delegation data or argument values other than the amount. An explanation with a value this client version does not know is not interpreted: policy is then absent and why still carries the Gateway's own sentence.

explainResult(result) turns any outcome into text an LLM can act on. For approval_pending it says how to resume (see below).

Fail closed. A success without K-Block evidence (_meta.kblock with the protected action ID) or an error without a K-Block refusal is not reported as an outcome: the client throws KBlockProtocolError. That happens when gatewayUrl points at something other than the Gateway. A completed call's receiptId is undefined only when the Gateway executed the call but could not read its receipt back while answering: the call ran (do not repeat it).

Approvals: waiting and resuming

A pending call is resumed by repeating the same call: same tool, same arguments, same idempotency key. callTool sends a fresh idempotency key by default (kbc-<uuid>, in result.call.idempotencyKey), so:

  • client.resume(result) repeats it: still pending, or (once approved) executed and completed;
  • client.waitForApproval(pending, { timeoutSeconds, waitSecondsPerRequest }) resumes with the Gateway's kblock/wait-seconds, so the Gateway holds each request until the decision lands (up to the Gateway's own cap per request), and returns the first non-pending outcome (or the pending one after timeoutSeconds);
  • repeating a call that already executed returns its stored outcome: it never runs twice;
  • a plain new callTool sends a fresh key: that is a new request, which needs its own approval.

Approvers decide in the K-Block dashboard (Approvals), where they see the exact signed call. The client cannot approve.

idempotencyKey: false sends no key: the Gateway then resumes an open action of the identical call by its fingerprint, but runs an identical call again once the first one finished. Pass your own key (1–200 characters) to tie retries across processes. waitSeconds on callTool holds the first request, too.

Errors

Refusals are results; transport failures throw typed errors whose messages say what to do (the API key's secret part is never included, only its public prefix kb_live_abcdefghijkl_…). KBlockError and isKBlockError are this package's base class and guard.

| Error | When | |---|---| | KBlockAuthError (UNAUTHORIZED, status 401) | the Gateway refused the key: not a gateway key, revoked, expired, or its agent is disabled | | KBlockAuthError (MALFORMED_API_KEY) | the key is not in the kb_<live\|test>_<prefix>_<secret> format (nothing sent) | | KBlockRateLimitedError (retryAfterSeconds) | HTTP 429: the per-key rate limit | | KBlockUnavailableError (code, retryAfterSeconds, outcomeUnknown) | HTTP 503 DATABASE_UNAVAILABLE / GATEWAY_SHUTTING_DOWN (the Gateway's own answers: nothing processed), RPC_UNAVAILABLE (a JSON-RPC answer that the Gateway's database is unavailable, e.g. for listTools), HTTP_ERROR for any other 5xx (outcome unknown), NETWORK_ERROR for a connection failure, TIMEOUT when no answer came in time (outcome unknown for a tool call). outcomeUnknown: true means the request may have been processed: repeat a call only with the same idempotency key. The error's call holds the call as it was sent: when it has a key (error.call.idempotencyKey), client.resume(error.call) repeats it safely; a call sent without a key would run again, so check its effect first | | KBlockHttpError | e.g. 413 (request over 1 MiB), 404 (wrong URL), a 3xx redirect (never followed: the key goes only to the gatewayUrl's origin) | | KBlockProtocolError | not a K-Block answer (see "Fail closed"); not a receipt verification error, which is @kblockai/sdk's ProtocolError |

waitForApproval waits out rate limits and temporary unavailability only when the call has an idempotency key, so a repeat can never run it twice.

gatewayUrl must be https:, or http: to a loopback host; allowInsecureHttp: true allows plain http elsewhere (the key would travel in clear text). Otherwise connectKBlock throws a TypeError.

Receipts

Each executed call has a receipt: a portable .kbundle that anyone can verify offline. The Gateway returns the receipt's ID with each completed call (receiptId). A member of the organization downloads the bundle in the K-Block dashboard (the receipt's page, Download .kbundle), or a program downloads it from the K-Block Cloud API with a management API key that has the receipts:read scope (Authorization: Bearer <management API key>). The agent's gateway API key cannot download receipts. Rejected and cancelled calls also end with a receipt; a call denied when it was decided, and an approval request that expired, end without one. In a receipt of your Gateway, the request, the execution and the result are signed by the Gateway on behalf of your organization, and an approval by the approver's own key; the agent signs nothing.

Given your organization's ID and the Cloud API's URL, the client builds the receipt URLs:

const client = await connectKBlock({
  gatewayUrl,                            // e.g. https://gateway.example.com; locally http://127.0.0.1:<port>
  apiKey,                                // the agent's gateway API key (kb_live_… / kb_test_…)
  receipts: { organizationId, apiUrl },  // optional: lets client.receiptUrls() build receipt URLs
});
const urls = client.receiptUrls(r);                        // urls.bundle: <apiUrl>/api/v1/orgs/<org>/receipts/<receiptId>/bundle

With dashboardUrl in receipts, urls.page is the dashboard's receipt page.

To verify a bundle, install the separate package @kblockai/sdk (npm install --save-exact @kblockai/[email protected]), which works offline and does not trust K-Block:

import { renderText, verifyBundleJson } from "@kblockai/sdk/verify";
const report = verifyBundleJson(await readFile(file, "utf8")); // never throws on invalid input
console.log(renderText(report)); // the seven checks, then RESULT: VALID or RESULT: INVALID

Compare report.issuer with your Gateway's KID (K-Block gives it to you): a valid bundle signed by another identity is still valid, but not your Gateway's.

LangChain.js

import { loadKBlockTools } from "@kblockai/client/langchain";
const tools = await loadKBlockTools(client);   // pass `tools` to your agent

Each tool returns text the model can reason about (the upstream's content when completed; for a pending approval, that nothing ran and that the same call must be repeated after approval; for a denial, the code, detail and the policy's explanation) and the typed KBlockCallResult as the ToolMessage artifact. When the tool is invoked with a tool call (as agents do), every refusal (held for approval, denied, rejected, expired, unavailable, an upstream that failed…) is a ToolMessage with status: "error", so LangGraph edges, evaluations and tracing never count it as a successful tool run; refusalsAsErrors: false restores status: "success", and throwOnRefusal: true throws KBlockRefusalError instead. By default no idempotency key is sent, so when the model repeats an identical call after approval the Gateway resumes it; pass idempotencyKey: (tool, args) => … to derive one.

Everything else is MCP

client.mcp is the connected MCP Client (Streamable HTTP, Authorization: Bearer <key>), for frameworks that take one. client.listTools() lists what the agent may call (the Gateway lists only the tools the agent's delegation grants). client.close() closes the connection.

Versioning

0.1.0-rc.1 is a pre-release. Package versions follow semver and are independent of the K-Block protocol version (v0.1).

License

K-Block SDK License (proprietary); see the LICENSE file in this package.