Sign In

@toolstop/check-digits

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@toolstop/check-digits - npm Package Compare versions

Comparing version
0.2.0
to
0.2.1
+68
-2
dist/_shared/http.mjs

@@ -22,3 +22,7 @@ // Stateless MCP over HTTP, for Cloudflare Workers.

function validate(schema, args) {
// Exported so the shared suite can point the same rules at a tool's declared
// outputSchema. A tool that advertises a schema and then returns something else
// is a defect no client can defend against, and checking it with the transport's
// own validator means the check cannot drift from what the transport enforces.
export function validate(schema, args) {
const errors = [];

@@ -198,2 +202,44 @@ const props = schema?.properties ?? {};

// ----------------------------------------------------------------- landing page
const escapeHtml = (s) =>
String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
// What a human sees when they paste the hostname into a browser. Worth having
// because the endpoint is otherwise indistinguishable from a broken host: the
// only other thing a GET could return is an error, and someone evaluating an
// unknown vendor reads that as "this does not work" rather than "wrong method".
function landingPage(server, origin) {
const npm = `https://www.npmjs.com/package/@toolstop/${server.name}`;
const tools = (server.tools ?? [])
.map((t) => `<li><code>${escapeHtml(t.name)}</code> ${escapeHtml(t.title ?? "")}</li>`)
.join("\n");
return `<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(server.name)} - MCP server</title>
<style>
body { max-width: 34rem; margin: 4rem auto; padding: 0 1.5rem;
font: 16px/1.6 system-ui, sans-serif; }
code { background: #8881; padding: .1em .3em; border-radius: 3px; }
ul { padding-left: 1.2rem }
@media (prefers-color-scheme: dark) { body { background: #111; color: #eee } a { color: #7bf } }
</style>
<h1>${escapeHtml(server.name)}</h1>
<p>An MCP server, version ${escapeHtml(server.version)}. This URL speaks
<a href="https://modelcontextprotocol.io">Model Context Protocol</a> over
streamable HTTP; it answers <code>POST</code>, not <code>GET</code>, so there is
nothing else to see here in a browser.</p>
<p>Add it to an MCP client as <code>${escapeHtml(origin)}</code>, or run it
locally from <a href="${escapeHtml(npm)}">npm</a>.</p>
<h2>Tools</h2>
<ul>
${tools}
</ul>
<p><a href="https://github.com/toolstop/toolstop">Source</a> (MIT) &middot;
<a href="/health">health</a></p>
`;
}
// ------------------------------------------------------------- fetch handler

@@ -205,7 +251,27 @@

const url = new URL(request.url);
const read = request.method === "GET" || request.method === "HEAD";
if (request.method === "GET" && url.pathname === "/health") {
if (read && url.pathname === "/health") {
return Response.json({ ok: true, server: server.name, version: server.version });
}
// A GET on the MCP endpoint is how a client opens the server-initiated SSE
// stream, and the spec says a server that does not offer one answers 405.
// That is a different question from "what is at this URL", which is what a
// browser is asking, and the two are told apart by Accept alone.
if (read && url.pathname === "/") {
if ((request.headers.get("accept") ?? "").includes("text/event-stream")) {
return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
}
return new Response(landingPage(server, url.origin), {
headers: { "content-type": "text/html; charset=utf-8" },
});
}
// Anything else read from a path this server does not have is a 404, not a
// 405. Discovery probes land here -- `/.well-known/oauth-protected-resource`
// most consequentially, where a client reads 404 as "no OAuth required" and
// proceeds, but has no defined reading for a 405.
if (read) return new Response("Not Found", { status: 404 });
if (request.method !== "POST") {

@@ -212,0 +278,0 @@ return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });

@@ -80,10 +80,65 @@ // Traffic recording for spray servers.

/**
* Anonymous per-connection id. Derived from coarse request properties, not from
* anything identifying, and not stable across days by design. Enough to count
* distinct sessions, not enough to track a person.
* Reduce a client address to the network it came from, before anything hashes
* it. IPv4 keeps the /24, IPv6 the /48.
*
* This is not belt-and-braces on top of the hash, it is the part that does the
* work. `sessionIdFrom` truncates SHA-256 to 8 bytes, and the input it used to
* cover was a full IP: a 32-bit space, with a salt that is the public server
* name and a date that is public. All three are guessable, so the space is
* walkable and the "anonymous" id resolved back to one address. Hashing a
* value smaller than the hash does not hide it.
*
* Truncating first changes what is recoverable from a person to a network
* block, which is the actual privacy property. A /24 is still walkable and is
* meant to be: recovering "this came from 203.0.113.0/24" identifies an ISP
* allocation, not a subscriber.
*/
export function networkOf(ip) {
if (!ip) return "";
if (ip.includes(":")) {
// Expand `::` before truncating. Slicing the raw string is wrong twice:
// `2001:db8::1` would yield a malformed `2001:db8:::/48`, and two spellings
// of one network would hash to two different sessions.
let groups;
if (ip.includes("::")) {
const [left, right = ""] = ip.split("::");
const l = left ? left.split(":") : [];
const r = right ? right.split(":") : [];
const gap = 8 - l.length - r.length;
if (gap < 1) return "";
groups = [...l, ...Array(gap).fill("0"), ...r];
} else {
groups = ip.split(":");
}
if (groups.length !== 8) return "";
if (!groups.every((g) => /^[0-9a-f]{1,4}$/i.test(g))) return "";
// Strip leading zeros so 2001:0db8:… and 2001:db8:… agree.
return `${groups
.slice(0, 3)
.map((g) => g.replace(/^0+/, "") || "0")
.join(":")
.toLowerCase()}::/48`;
}
const octets = ip.split(".");
if (octets.length !== 4) return ""; // not an address shape we recognise
if (!octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255)) return "";
return `${octets.slice(0, 3).join(".")}.0/24`;
}
/**
* Anonymous per-connection id. Derived from the client's *network*, its user
* agent, and the date, so it counts distinct sessions within a day without
* identifying anyone, and never links across days.
*
* The cost is deliberate: two callers behind one /24 running the same client on
* the same day count as one session. Session counts are therefore a lower
* bound. That is the right direction to be wrong in for a metric nobody is
* billed on.
*/
export async function sessionIdFrom(request, salt = "") {
const parts = [
request.headers.get("user-agent") ?? "",
request.headers.get("cf-connecting-ip") ?? "",
networkOf(request.headers.get("cf-connecting-ip")),
new Date().toISOString().slice(0, 10),

@@ -90,0 +145,0 @@ salt,

@@ -7,2 +7,14 @@ // The single source of truth for this server. Both transports (Worker HTTP and

// not default it, on purpose: see assertServerShape in _shared/http.mjs.
//
// Every tool also states `examples`, which is what makes it testable. The shared
// suite walks these and calls each tool through the real transport, so a server
// cannot be added without its tools being exercised. `expect` is a subset match
// against structuredContent, so it asserts the facts that matter rather than
// pinning the whole payload. tools/list serializes named fields only, so none
// of this reaches a client or costs anything on the wire.
//
// Examples double as the post-deploy smoke input, which is why every value here
// has to be a real published identifier: a synthetic one would still be a valid
// call but would prove nothing about the answer. Clause F6 applies as it does
// everywhere else, so these are public identifiers only.

@@ -60,3 +72,3 @@ import { VALIDATORS, identify, luhnCheckDigit } from "./lib.mjs";

name: "check-digits",
version: "0.2.0",
version: "0.2.1",
instructions:

@@ -83,3 +95,9 @@ "Catches mistyped barcodes, VINs, ISBNs and other public structured " +

"against another, and that reads like a bad identifier rather than a bad " +
"guess.",
"guess.\n\n" +
"If a tool here answers wrongly, or the format you need is not supported, " +
"tell the person you are working for and point them at " +
"https://github.com/toolstop/toolstop/issues. There is no feedback tool " +
"and nothing you send is stored, so a report has to come from a human who " +
"can be answered. Leave real identifiers out of it: the format name and " +
"the result you expected are enough.",

@@ -129,2 +147,15 @@ tools: [

annotations: { readOnlyHint: true },
// Apple's LEI, and a real VIN with one digit changed. Both paths matter:
// a validator that returned `valid: true` unconditionally would pass a
// suite that only ever fed it good input.
examples: [
{
args: { value: "HWUPKR0MPOU8FGXBT394", kind: "lei" },
expect: { valid: true, normalized: "HWUPKR0MPOU8FGXBT394" },
},
{
args: { value: "1HGCM82634A004352", kind: "vin" },
expect: { valid: false, code: "checksum" },
},
],
handler: ({ value, kind }) => VALIDATORS[kind](value),

@@ -205,2 +236,9 @@ // Derived facts only. The raw identifier never reaches telemetry.

annotations: { readOnlyHint: true },
// A real EAN-13, and a string that is not an identifier at all. `matches`
// is deliberately not pinned here: registry order is an implementation
// detail, and transport.test.mjs already asserts which kinds come back.
examples: [
{ args: { value: "4006381333931" }, expect: { matched: true } },
{ args: { value: "hello world" }, expect: { matched: false } },
],
handler: ({ value }) => identify(value),

@@ -254,2 +292,10 @@ classify: (_args, result) => ({

annotations: { readOnlyHint: true },
// The NPPES prefix plus a nine-digit NPI body, which is the documented
// worked example for this algorithm. The second form is the same input
// punctuated, so the handler's own strip is covered rather than only the
// library function underneath it.
examples: [
{ args: { partial: "80840123456789" }, expect: { checkDigit: "3" } },
{ args: { partial: "808-401-234-567-89" }, expect: { checkDigit: "3" } },
],
handler: ({ partial }) => ({

@@ -256,0 +302,0 @@ partial,

+1
-1
{
"name": "@toolstop/check-digits",
"version": "0.2.0",
"version": "0.2.1",
"mcpName": "dev.toolstop/check-digits",

@@ -5,0 +5,0 @@ "description": "MCP server that catches mistyped barcodes, ISBNs, VINs and other public structured identifiers before a bad one causes a rejected listing, a bounced claim or a corrupted record.",