@shpbl/sdk
v0.3.1
Published
Typed client for the SHPBL API — the same fourteen tools the MCP server runs, over ordinary HTTP. No model, no magic, no hidden state.
Maintainers
Readme
@shpbl/sdk
A typed client for the SHPBL HTTP API. Same fourteen tools the MCP server runs, same gate, same meter, same ledger — only the envelope differs.
No model runs in this package. The SDK does not call an LLM, and SHPBL's server does not either. The reasoning belongs to your agent; the runtime is deterministic.
The manual is the source of truth. The method, every tool, the lanes and the
meter are documented once, at https://shpbl.com/manual. This page documents the
envelope; the client reference lives at https://shpbl.com/sdk, and the source is
packages/sdk of https://github.com/SweetKenneth/shpbl-master.
Install
npm install @shpbl/sdkNode 20+, or any runtime with fetch (browsers, Workers, Deno, Bun).
Use
import { ShpblClient, text } from "@shpbl/sdk";
const shpbl = new ShpblClient({ key: process.env.SHPBL_KEY }); // key optional
const welcome = await shpbl.welcome({ name: "Kenneth" });
console.log(text(welcome));
const audit = await shpbl.evaluateRepo({ repo: "owner/repo", brief: true });
console.log(text(audit));Without a key the free lane runs: the repository evaluation is complete and
yours, and the run stops at the composition boundary. A Practitioner key opens
run_gauntlet, library search, library_document and writes.
Know before you call
Tiers are pinned in the package, so the SDK answers "can I do this?" with no request and no round trip.
const free = new ShpblClient(); // no key
free.canCall("evaluate_repo"); // true
free.canCall("library_search"); // false
free.tierOf("write_to_repo"); // "practitioner"
free.available(); // the ten tools open to you nowThe ask
Hitting the key wall is a normal branch in a good integration, not an exception you mop up. So it is an object, not a stack trace.
const ask = free.ask("run_gauntlet");{
tool: "run_gauntlet",
tier: "practitioner",
hasKey: false,
reason: "no_key", // "no_key" | "tier" | "unknown"
headline: "`run_gauntlet` needs a Practitioner key. You do not have one set.",
unlocks: "the full conducted run: survey, evaluation, repair, batched harvest and report",
stillWorks: [ ...what the free lane does right now ],
url: "https://shpbl.com/mcp-access",
docsUrl: "https://shpbl.com/manual",
}Every ShpblAuthError carries one, with the server's own remedy line folded in
as serverNextStep. explain() renders it as plain text with no ANSI, equally
at home in a log, a GitHub issue or a modal.
try {
await shpbl.librarySearch({ query: "retry backoff" });
} catch (error) {
if (error instanceof ShpblAuthError) console.log(error.explain());
}`library_search` needs a Practitioner key. You do not have one set.
What it opens: search across the Collective Master Library, the vault and past composites
Take a key: https://shpbl.com/mcp-access
Manual: https://shpbl.com/manual
Server says: Take or check a key at https://shpbl.com/mcp-access, then run the same call again.
Working right now, with no key:
· evaluate_repo: the complete diagnosis of your repository, at full depth
· fix_repo: verbatim source and the ordered remediation protocol
· harvest_repo: what the repository can already do, and how much owned capability is offerable against it
· list_repos, method_protocol, library_index, build_intent, selfcheck_mcpRegister onAsk once and every paywall in your integration routes to your own
banner, prompt or metric. It notifies; it does not swallow the throw.
const shpbl = new ShpblClient({
key: process.env.SHPBL_KEY,
onAsk: (ask) => ui.showUpgrade(ask),
});Drift
A typed client is a promise about a contract, and contracts move. One request tells you whether this pinned release still matches the server.
const report = await shpbl.compatibility();
if (!report.ok) console.warn(report.summary);
// @shpbl/sdk 0.2.0 against server 1.36.0: the server has 1 tool(s) this SDK has
// no method for (compose_repo). 1 tool(s) accept arguments this SDK does not
// name: evaluate_repo (depth). Unknown arguments are forwarded untouched, so
// calls keep working; upgrade the SDK to get types for them.report gives you newTools, missingTools, tierChanges and per-tool
arguments drift. Log it on boot, or assert ok in CI and find out about a
server change from your own pipeline rather than a bug report. Nothing here
changes behaviour: invoke() forwards unknown arguments regardless.
Tools
| Method | Tool |
| --- | --- |
| welcome() | welcome — first call of a conversation |
| methodProtocol() | method_protocol |
| listRepos() | list_repos |
| evaluateRepo() | evaluate_repo |
| fixRepo() | fix_repo |
| harvestRepo() | harvest_repo |
| runGauntlet() | run_gauntlet (Practitioner) |
| librarySearch() | library_search |
| libraryIndex() | library_index |
| libraryDocument() | library_document |
| buildIntent() | build_intent |
| writeToRepo() | write_to_repo |
| subscriptionStatus() | subscription_status |
| selfcheck() | selfcheck_mcp |
invoke(name, args) calls anything by name, and tools() / descriptor() read
the live contract so a new server argument never needs an SDK release.
The gauntlet, without the bookkeeping
run_gauntlet is a chain, and the server holds no session. Every step past the
first must carry the ledger_digest your model folded and the signed
fold_token the previous step handed back, the run pauses for a person every
few harvest steps, and the step after the last batch is the closing step rather
than the next number. gauntlet() holds that bookkeeping — the token, the order
the server dictates, and the pause — and nothing else.
The ledger is yours. Your model reads each batch and writes one line per capability; the SDK will not invent it, and it refuses to send a step past the first without one instead of letting the server refuse the call for you.
import { ShpblClient, gauntlet } from "@shpbl/sdk";
const shpbl = new ShpblClient({ key: process.env.SHPBL_KEY });
const session = gauntlet(shpbl, { repo: "owner/repo" });
let ledger = "";
let step = await session.next(); // step 1
while (!step.done) {
console.log(step.text); // your model reads this batch
ledger += `\n${yourModelsLinesFor(step)}`; // one line per capability
if (step.checkpoint) {
// Report to your person. Only if they said yes:
session.acknowledge();
}
step = await session.next({ ledgerDigest: ledger });
}Approval is never implied. acknowledge() is the only way continue_ack is
sent, and snapshot() gives you exactly what to persist to resume in another
process. A pause is the server's own gauntlet-checkpoint stage, never wording
in the returned text — the harvest protocol prose contains the word
"checkpoint". steps() walks for you and hands control back at a checkpoint or
when the next call needs a ledger. startWithPlan opens with the run card, which
names the batch count and the closing step before anything is read. maxSteps
(default 200) is a hard ceiling on calls, so a run that never closes stops
instead of looping.
Writing back
files carry the complete new contents under text, never a diff.
await shpbl.writeToRepo({
repo: "owner/repo",
title: "Land the harvest",
summary: "Run seal and coverage.",
kind: "harvest",
files: [{ path: ".shpbl/README.md", text: "# Capability library\n" }],
});Errors
One class per situation, so you branch instead of parsing strings.
| Class | When |
| --- | --- |
| ShpblValidationError | 400 — arguments rejected; .issues lists them |
| ShpblAuthError | 401 / 402 / 403 — missing key, wrong tier |
| ShpblNotFoundError | 404 — no such tool |
| ShpblRefusalError | 422 — the tool declined; .nextStep says what to do |
| ShpblRateLimitError | 429 — .retryAfterSeconds |
| ShpblServerError | 5xx |
| ShpblNetworkError | never reached a response |
| ShpblTimeoutError | timed out or cancelled |
A refusal is a legitimate answer, not a bug. Pass { refusalsAsResult: true }
to get it back as a result with ok: false instead of a throw.
Options
new ShpblClient({
key: "shpbl_mcp_…",
baseUrl: "https://shpbl.com",
timeoutMs: 120_000, // runs are not fast
maxRetries: 2, // 429 / 5xx / network only, honours retry-after
headers: {},
fetch: customFetch,
userAgent: "my-app/1.0",
onRequest: (e) => {},
onResponse: (e) => {},
});Per-call overrides: { signal, timeoutMs, maxRetries, headers, refusalsAsResult }.
Headers you supply win. Setting authorization yourself replaces the bearer
token the client would otherwise send.
withKey(key) returns an independent client, useful per tenant.
License
The package is MIT. The service it talks to is not: the server, the libraries,
the catalogs and the reports stay proprietary under their own terms. The
wrapper is free so nobody needs a legal conversation to build against SHPBL.
See NOTICE for the exact split.
Development
npm install
npm run typecheck
npm test
npm run build