teppi-check 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,7 @@ Send one unpaid request to any paid endpoint and see what it actually advertises
4
4
 
5
5
  ```bash
6
6
  npx teppi-check https://some-seller.example/v1/extract
7
+ npx teppi-check --mcp https://mcp.some-seller.example/mcp
7
8
  ```
8
9
 
9
10
  No install and no dependencies: the published package is a single bundled file, so the validator
@@ -70,9 +71,32 @@ takes a purchase, which the full prober does on a schedule.
70
71
  Terms are read from the `payment-required` header (base64 or plain JSON) or from the response
71
72
  body, because deployed servers use both.
72
73
 
74
+ ## Remote MCP servers
75
+
76
+ `--mcp` runs the handshake instead: `initialize`, the initialized notification, then `tools/list`
77
+ over Streamable HTTP.
78
+
79
+ | Check | Fails when |
80
+ |---|---|
81
+ | server answered | connection refused, timeout, or a server that never replies |
82
+ | named a protocol version | it never got that far |
83
+ | named itself | it never got that far |
84
+ | listed its tools | it never got that far |
85
+ | every tool declared an input schema | a tool an agent would have to guess the arguments for |
86
+ | every tool said what it does | a tool with no description |
87
+ | tool list digest | never, it prints the digest so you can compare it against a record |
88
+
89
+ A server asking for credentials or for payment is reported as such and does **not** fail: that is
90
+ what a private or paid server is entitled to say. Only a server that did not answer fails.
91
+
92
+ The digest is a sha256 over each tool's name, description and whether it declared an input
93
+ schema, sorted by name. Reordering the list does not change it and rewording a description does,
94
+ which is what makes it useful for noticing that a server's instructions changed under you.
95
+
73
96
  ## Options
74
97
 
75
98
  ```
99
+ --mcp handshake a remote mcp server instead of an x402 endpoint
76
100
  --method <verb> default POST
77
101
  --body <json|@file> default {}
78
102
  --json machine readable output
package/npm/cli.js CHANGED
@@ -4609,7 +4609,7 @@ var require_core = __commonJS({
4609
4609
  errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
4610
4610
  if (!errors || errors.length === 0)
4611
4611
  return "No errors";
4612
- return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg);
4612
+ return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text2, msg) => text2 + separator + msg);
4613
4613
  }
4614
4614
  $dataMetaSchema(metaSchema, keywordsJsonPointers) {
4615
4615
  const rules = this.RULES.all;
@@ -6871,32 +6871,382 @@ import { readFileSync } from "node:fs";
6871
6871
  import { argv, env, exit, stdout } from "node:process";
6872
6872
  import { parseArgs } from "node:util";
6873
6873
 
6874
- // src/probe.ts
6874
+ // src/mcp.ts
6875
+ import { createHash as createHash2 } from "node:crypto";
6876
+
6877
+ // ../mcp-probe/src/frame.ts
6878
+ var JSON_RPC = "2.0";
6879
+ var PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26"];
6880
+ var PREFERRED_VERSION = PROTOCOL_VERSIONS[0];
6881
+ function request(id, method, params) {
6882
+ return JSON.stringify({ jsonrpc: JSON_RPC, id, method, ...params ? { params } : {} });
6883
+ }
6884
+ function notification(method) {
6885
+ return JSON.stringify({ jsonrpc: JSON_RPC, method });
6886
+ }
6887
+ function lastEventData(body) {
6888
+ const payloads = body.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter((line) => line.length > 0);
6889
+ return payloads.length === 0 ? null : payloads[payloads.length - 1];
6890
+ }
6891
+ function readOne(text2) {
6892
+ let parsed;
6893
+ try {
6894
+ parsed = JSON.parse(text2);
6895
+ } catch {
6896
+ return { kind: "unreadable", why: "the answer is not json" };
6897
+ }
6898
+ const one = Array.isArray(parsed) ? parsed[0] : parsed;
6899
+ if (typeof one !== "object" || one === null) {
6900
+ return { kind: "unreadable", why: "the answer is not a json rpc object" };
6901
+ }
6902
+ const message = one;
6903
+ if (message.error !== void 0) {
6904
+ const error = message.error;
6905
+ return {
6906
+ kind: "error",
6907
+ code: typeof error.code === "number" ? error.code : 0,
6908
+ message: typeof error.message === "string" ? error.message : "no message"
6909
+ };
6910
+ }
6911
+ if (typeof message.result !== "object" || message.result === null) {
6912
+ return { kind: "unreadable", why: "the answer carries neither a result nor an error" };
6913
+ }
6914
+ return { kind: "result", value: message.result };
6915
+ }
6916
+ function readAnswer(contentType, body) {
6917
+ const streamed = (contentType ?? "").toLowerCase().includes("text/event-stream");
6918
+ if (!streamed) return readOne(body);
6919
+ const data = lastEventData(body);
6920
+ return data === null ? { kind: "unreadable", why: "the stream carried no data line" } : readOne(data);
6921
+ }
6922
+
6923
+ // ../mcp-probe/src/probe.ts
6875
6924
  import { createHash } from "node:crypto";
6925
+ var HANDSHAKE_TIMEOUT_MS = 2e4;
6926
+ var CLIENT = { name: "teppi-probe", version: "0.1.0" };
6927
+ function initFor(body, sessionId, userAgent) {
6928
+ return {
6929
+ method: "POST",
6930
+ headers: {
6931
+ "content-type": "application/json",
6932
+ accept: "application/json, text/event-stream",
6933
+ "mcp-protocol-version": PREFERRED_VERSION,
6934
+ "user-agent": userAgent,
6935
+ ...sessionId === null ? {} : { "mcp-session-id": sessionId }
6936
+ },
6937
+ body
6938
+ };
6939
+ }
6940
+ var INITIALIZE = {
6941
+ protocolVersion: PREFERRED_VERSION,
6942
+ capabilities: {},
6943
+ clientInfo: CLIENT
6944
+ };
6945
+ var text = (value) => typeof value === "string" ? value : null;
6946
+ function toolsFrom(result) {
6947
+ const raw = result.tools;
6948
+ if (!Array.isArray(raw)) return [];
6949
+ return raw.flatMap((value) => {
6950
+ if (typeof value !== "object" || value === null) return [];
6951
+ const tool = value;
6952
+ const name = text(tool.name);
6953
+ if (name === null) return [];
6954
+ return [
6955
+ {
6956
+ name,
6957
+ description: text(tool.description),
6958
+ inputSchema: tool.inputSchema ?? null,
6959
+ outputSchema: tool.outputSchema ?? null
6960
+ }
6961
+ ];
6962
+ });
6963
+ }
6964
+ function reachFor(status) {
6965
+ if (status === 401 || status === 403) return "auth_required";
6966
+ if (status === 402) return "payment_required";
6967
+ if (status >= 500 || status === 404 || status === 410) return "dead";
6968
+ if (status >= 400) return "error";
6969
+ return null;
6970
+ }
6971
+ async function turn(response, method) {
6972
+ const refused = reachFor(response.status);
6973
+ if (refused !== null) return { kind: "refused", reach: refused, status: response.status };
6974
+ const answer = readAnswer(response.headers.get("content-type"), await response.text());
6975
+ if (answer.kind === "result") {
6976
+ return { kind: "read", status: response.status, value: answer.value };
6977
+ }
6978
+ return {
6979
+ kind: "broken",
6980
+ status: response.status,
6981
+ note: answer.kind === "error" ? `${method}: ${answer.message}` : answer.why
6982
+ };
6983
+ }
6984
+ var DEADLINE_MS = 45e3;
6985
+ var MAX_PAGES = 20;
6986
+ var MAX_TOOLS = 2e3;
6987
+ async function probeMcp(url, options = {}) {
6988
+ const clockFor = options.now ?? Date.now;
6989
+ const startedFor = clockFor();
6990
+ let timer;
6991
+ const gaveUp = new Promise((resolve) => {
6992
+ timer = setTimeout(
6993
+ () => resolve({
6994
+ url,
6995
+ observedAt: new Date(startedFor).toISOString(),
6996
+ reach: "dead",
6997
+ httpStatus: null,
6998
+ handshakeMs: options.deadlineMs ?? DEADLINE_MS,
6999
+ protocolVersion: null,
7000
+ serverName: null,
7001
+ serverVersion: null,
7002
+ instructions: null,
7003
+ tools: null,
7004
+ pages: null,
7005
+ truncated: false,
7006
+ note: "never answered and never gave up"
7007
+ }),
7008
+ options.deadlineMs ?? DEADLINE_MS
7009
+ );
7010
+ });
7011
+ try {
7012
+ return await Promise.race([handshake(url, options), gaveUp]);
7013
+ } finally {
7014
+ clearTimeout(timer);
7015
+ }
7016
+ }
7017
+ async function walkTools(send, session) {
7018
+ const tools = [];
7019
+ let cursor = null;
7020
+ let pages = 0;
7021
+ let status = 200;
7022
+ for (; ; ) {
7023
+ const asked = request(2 + pages, "tools/list", cursor === null ? void 0 : { cursor });
7024
+ const listed = await turn(await send(asked, session), "tools/list");
7025
+ if (listed.kind === "refused") {
7026
+ return {
7027
+ kind: "refused",
7028
+ reach: listed.reach,
7029
+ status: listed.status,
7030
+ note: "it opened but would not list"
7031
+ };
7032
+ }
7033
+ if (listed.kind === "broken")
7034
+ return { kind: "broken", status: listed.status, note: listed.note };
7035
+ tools.push(...toolsFrom(listed.value));
7036
+ status = listed.status;
7037
+ pages += 1;
7038
+ const next = text(listed.value.nextCursor);
7039
+ if (next === null || next === cursor)
7040
+ return { kind: "listed", status, tools, pages, truncated: false };
7041
+ if (pages >= MAX_PAGES || tools.length >= MAX_TOOLS) {
7042
+ return { kind: "listed", status, tools, pages, truncated: true };
7043
+ }
7044
+ cursor = next;
7045
+ }
7046
+ }
7047
+ async function handshake(url, options) {
7048
+ const doFetch = options.fetchImpl ?? fetch;
7049
+ const clock = options.now ?? Date.now;
7050
+ const userAgent = options.userAgent ?? `${CLIENT.name}/${CLIENT.version}`;
7051
+ const startedAt = clock();
7052
+ const base = {
7053
+ url,
7054
+ observedAt: new Date(startedAt).toISOString(),
7055
+ protocolVersion: null,
7056
+ serverName: null,
7057
+ serverVersion: null,
7058
+ instructions: null,
7059
+ tools: null,
7060
+ pages: null,
7061
+ truncated: false
7062
+ };
7063
+ const send = (body, sessionId) => doFetch(url, {
7064
+ ...initFor(body, sessionId, userAgent),
7065
+ signal: AbortSignal.timeout(options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7066
+ });
7067
+ const took = () => Math.max(0, clock() - startedAt);
7068
+ try {
7069
+ const opened = await send(request(1, "initialize", INITIALIZE), null);
7070
+ const hello = await turn(opened, "initialize");
7071
+ if (hello.kind === "refused") {
7072
+ return {
7073
+ ...base,
7074
+ reach: hello.reach,
7075
+ httpStatus: hello.status,
7076
+ handshakeMs: took(),
7077
+ note: null
7078
+ };
7079
+ }
7080
+ if (hello.kind === "broken") {
7081
+ return {
7082
+ ...base,
7083
+ reach: "error",
7084
+ httpStatus: hello.status,
7085
+ handshakeMs: took(),
7086
+ note: hello.note
7087
+ };
7088
+ }
7089
+ const info = hello.value.serverInfo ?? {};
7090
+ const session = opened.headers.get("mcp-session-id");
7091
+ const said = {
7092
+ ...base,
7093
+ protocolVersion: text(hello.value.protocolVersion),
7094
+ serverName: text(info.name),
7095
+ serverVersion: text(info.version),
7096
+ instructions: text(hello.value.instructions)
7097
+ };
7098
+ await send(notification("notifications/initialized"), session).catch(() => void 0);
7099
+ const walked = await walkTools(send, session);
7100
+ if (walked.kind !== "listed") {
7101
+ return {
7102
+ ...said,
7103
+ reach: walked.kind === "refused" ? walked.reach : "error",
7104
+ httpStatus: walked.status,
7105
+ handshakeMs: took(),
7106
+ note: walked.note
7107
+ };
7108
+ }
7109
+ return {
7110
+ ...said,
7111
+ reach: "readable",
7112
+ httpStatus: walked.status,
7113
+ handshakeMs: took(),
7114
+ tools: walked.tools,
7115
+ pages: walked.pages,
7116
+ truncated: walked.truncated,
7117
+ note: null
7118
+ };
7119
+ } catch (err) {
7120
+ const name = err.name;
7121
+ return {
7122
+ ...base,
7123
+ reach: "dead",
7124
+ httpStatus: null,
7125
+ handshakeMs: took(),
7126
+ note: name === "TimeoutError" || name === "AbortError" ? "timed out" : "unreachable"
7127
+ };
7128
+ }
7129
+ }
7130
+ function toolsDigest(tools) {
7131
+ const canonical = JSON.stringify(
7132
+ [...tools].map((t) => [t.name, t.description ?? "", t.inputSchema !== null]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
7133
+ );
7134
+ return createHash("sha256").update(canonical).digest("hex");
7135
+ }
7136
+
7137
+ // src/mcp.ts
7138
+ function sha256(input) {
7139
+ return `sha256:${createHash2("sha256").update(input).digest("hex")}`;
7140
+ }
7141
+ var pass = (id, label, detail) => detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7142
+ var fail = (id, label, detail) => ({
7143
+ id,
7144
+ label,
7145
+ status: "fail",
7146
+ detail
7147
+ });
7148
+ var skip = (id, label, detail) => ({
7149
+ id,
7150
+ label,
7151
+ status: "skip",
7152
+ detail
7153
+ });
7154
+ var REACH_LABEL = {
7155
+ readable: "listed its tools",
7156
+ auth_required: "asked for credentials",
7157
+ payment_required: "asked for payment",
7158
+ dead: "did not answer",
7159
+ error: "answered with something else"
7160
+ };
7161
+ function checksFor(seen) {
7162
+ const checks = [];
7163
+ checks.push(
7164
+ seen.reach === "dead" ? fail("reachable", "server answered", seen.note ?? "no answer") : pass("reachable", "server answered", REACH_LABEL[seen.reach])
7165
+ );
7166
+ if (seen.reach === "dead") return checks;
7167
+ checks.push(
7168
+ seen.protocolVersion === null ? skip("protocol", "named a protocol version", "it never got as far as saying") : pass("protocol", "named a protocol version", seen.protocolVersion)
7169
+ );
7170
+ checks.push(
7171
+ seen.serverName === null ? skip("identity", "named itself", "it never got as far as saying") : pass(
7172
+ "identity",
7173
+ "named itself",
7174
+ `${seen.serverName}${seen.serverVersion === null ? "" : ` ${seen.serverVersion}`}`
7175
+ )
7176
+ );
7177
+ if (seen.tools === null) {
7178
+ checks.push(skip("tools", "listed its tools", REACH_LABEL[seen.reach]));
7179
+ return checks;
7180
+ }
7181
+ checks.push(pass("tools", "listed its tools", `${seen.tools.length} tool(s)`));
7182
+ const bare = seen.tools.filter((t) => t.inputSchema === null).map((t) => t.name);
7183
+ checks.push(
7184
+ bare.length === 0 ? pass("input-schemas", "every tool declared an input schema") : fail(
7185
+ "input-schemas",
7186
+ "every tool declared an input schema",
7187
+ `${bare.length} without one: ${bare.slice(0, 5).join(", ")}`
7188
+ )
7189
+ );
7190
+ const unnamed = seen.tools.filter((t) => t.description === null).map((t) => t.name);
7191
+ checks.push(
7192
+ unnamed.length === 0 ? pass("descriptions", "every tool said what it does") : fail(
7193
+ "descriptions",
7194
+ "every tool said what it does",
7195
+ `${unnamed.length} without one: ${unnamed.slice(0, 5).join(", ")}`
7196
+ )
7197
+ );
7198
+ checks.push(pass("digest", "tool list digest", toolsDigest(seen.tools)));
7199
+ return checks;
7200
+ }
7201
+ async function probeMcpEndpoint(options) {
7202
+ const seen = await probeMcp(options.url, {
7203
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
7204
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
7205
+ ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7206
+ });
7207
+ const checks = checksFor(seen);
7208
+ return {
7209
+ url: options.url,
7210
+ method: "MCP",
7211
+ observedAt: seen.observedAt,
7212
+ reachable: seen.reach !== "dead",
7213
+ httpStatus: seen.httpStatus,
7214
+ handshakeMs: seen.handshakeMs,
7215
+ termsSource: seen.reach === "payment_required" ? "mcp 402" : "none",
7216
+ advertised: [],
7217
+ requestHash: sha256(`MCP ${options.url}`),
7218
+ responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7219
+ checks,
7220
+ verdict: checks.some((c) => c.status === "fail") ? "fail" : "pass"
7221
+ };
7222
+ }
7223
+
7224
+ // src/probe.ts
7225
+ import { createHash as createHash3 } from "node:crypto";
6876
7226
 
6877
7227
  // ../x402/src/handshake.ts
6878
- var HANDSHAKE_TIMEOUT_MS = 3e4;
7228
+ var HANDSHAKE_TIMEOUT_MS2 = 3e4;
6879
7229
  var sendsBody = (method) => method !== "GET" && method !== "HEAD";
6880
- function handshakeInit(request) {
6881
- const carries = sendsBody(request.method);
7230
+ function handshakeInit(request2) {
7231
+ const carries = sendsBody(request2.method);
6882
7232
  const headers = { accept: "application/json" };
6883
7233
  if (carries) headers["content-type"] = "application/json";
6884
- if (request.userAgent !== void 0) headers["user-agent"] = request.userAgent;
6885
- if (request.payment) headers[request.payment.header] = request.payment.value;
7234
+ if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7235
+ if (request2.payment) headers[request2.payment.header] = request2.payment.value;
6886
7236
  return {
6887
- method: request.method,
7237
+ method: request2.method,
6888
7238
  headers,
6889
- ...carries ? { body: JSON.stringify(request.input ?? {}) } : {},
6890
- signal: AbortSignal.timeout(request.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7239
+ ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7240
+ signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
6891
7241
  };
6892
7242
  }
6893
- async function handshake(request, deps = {}) {
7243
+ async function handshake2(request2, deps = {}) {
6894
7244
  const doFetch = deps.fetchImpl ?? fetch;
6895
7245
  const now = deps.now ?? Date.now;
6896
7246
  const started = now();
6897
7247
  const since = () => Math.max(0, now() - started);
6898
7248
  try {
6899
- const response = await doFetch(request.url, handshakeInit(request));
7249
+ const response = await doFetch(request2.url, handshakeInit(request2));
6900
7250
  return { ok: true, response, body: await response.text(), latencyMs: since() };
6901
7251
  } catch (err) {
6902
7252
  return { ok: false, reason: err.message, latencyMs: since() };
@@ -6974,24 +7324,24 @@ var ajv = new import_ajv.Ajv({
6974
7324
  validateSchema: true,
6975
7325
  validateFormats: false
6976
7326
  });
6977
- function pass(id, label, detail) {
7327
+ function pass2(id, label, detail) {
6978
7328
  return detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
6979
7329
  }
6980
- function fail(id, label, detail) {
7330
+ function fail2(id, label, detail) {
6981
7331
  return { id, label, status: "fail", detail };
6982
7332
  }
6983
7333
  function schemaCheck(id, label, schema) {
6984
7334
  if (schema === null || schema === void 0) {
6985
- return fail(id, label, "not declared, so no response can ever be checked against it");
7335
+ return fail2(id, label, "not declared, so no response can ever be checked against it");
6986
7336
  }
6987
7337
  if (typeof schema !== "object") {
6988
- return fail(id, label, `declared as ${typeof schema}, not an object`);
7338
+ return fail2(id, label, `declared as ${typeof schema}, not an object`);
6989
7339
  }
6990
7340
  try {
6991
7341
  ajv.compile(schema);
6992
- return pass(id, label);
7342
+ return pass2(id, label);
6993
7343
  } catch (err) {
6994
- return fail(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
7344
+ return fail2(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
6995
7345
  }
6996
7346
  }
6997
7347
  function requirementChecks(terms) {
@@ -6999,7 +7349,7 @@ function requirementChecks(terms) {
6999
7349
  const report = (id, label, has) => {
7000
7350
  const gaps = terms.accepts.map((r, i) => has(r) ? -1 : i).filter((i) => i >= 0);
7001
7351
  out.push(
7002
- gaps.length === 0 ? pass(id, label) : fail(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7352
+ gaps.length === 0 ? pass2(id, label) : fail2(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7003
7353
  );
7004
7354
  };
7005
7355
  report("price_declared", "price is stated", (r) => priceOf(r) !== null);
@@ -7016,29 +7366,29 @@ function requirementChecks(terms) {
7016
7366
  return out;
7017
7367
  }
7018
7368
  function runHandshakeChecks(input) {
7019
- const checks = [pass("reachable", "endpoint answered")];
7369
+ const checks = [pass2("reachable", "endpoint answered")];
7020
7370
  checks.push(
7021
- input.status === 402 ? pass("status_402", "asks for payment") : fail("status_402", "asks for payment", `returned ${input.status}, not 402`)
7371
+ input.status === 402 ? pass2("status_402", "asks for payment") : fail2("status_402", "asks for payment", `returned ${input.status}, not 402`)
7022
7372
  );
7023
7373
  if (input.terms === null) {
7024
7374
  checks.push(
7025
- fail("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7375
+ fail2("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7026
7376
  );
7027
7377
  return checks;
7028
7378
  }
7029
- checks.push(pass("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7379
+ checks.push(pass2("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7030
7380
  checks.push(
7031
- input.terms.accepts.length > 0 ? pass(
7381
+ input.terms.accepts.length > 0 ? pass2(
7032
7382
  "accepts_present",
7033
7383
  "offers at least one way to pay",
7034
7384
  `${input.terms.accepts.length} option(s)`
7035
- ) : fail("accepts_present", "offers at least one way to pay", "accepts is empty")
7385
+ ) : fail2("accepts_present", "offers at least one way to pay", "accepts is empty")
7036
7386
  );
7037
7387
  if (input.terms.accepts.length === 0) return checks;
7038
7388
  checks.push(...requirementChecks(input.terms));
7039
7389
  const cache = input.headers.get("cache-control") ?? "";
7040
7390
  checks.push(
7041
- /no-store|no-cache/i.test(cache) ? pass("not_cacheable", "payment terms are not cached") : fail(
7391
+ /no-store|no-cache/i.test(cache) ? pass2("not_cacheable", "payment terms are not cached") : fail2(
7042
7392
  "not_cacheable",
7043
7393
  "payment terms are not cached",
7044
7394
  cache === "" ? "no cache-control, so a proxy may serve stale terms" : `cache-control: ${cache}`
@@ -7047,15 +7397,15 @@ function runHandshakeChecks(input) {
7047
7397
  return checks;
7048
7398
  }
7049
7399
  function unreachable(reason) {
7050
- return [fail("reachable", "endpoint answered", reason)];
7400
+ return [fail2("reachable", "endpoint answered", reason)];
7051
7401
  }
7052
7402
  function verdictOf(checks) {
7053
7403
  return checks.some((c) => c.status === "fail") ? "fail" : "pass";
7054
7404
  }
7055
7405
 
7056
7406
  // src/probe.ts
7057
- function sha256(input) {
7058
- return `sha256:${createHash("sha256").update(input).digest("hex")}`;
7407
+ function sha2562(input) {
7408
+ return `sha256:${createHash3("sha256").update(input).digest("hex")}`;
7059
7409
  }
7060
7410
  function advertisedFrom(terms) {
7061
7411
  if (!terms) return [];
@@ -7071,7 +7421,7 @@ async function probe(options) {
7071
7421
  const body = options.body ?? "{}";
7072
7422
  const doFetch = options.fetchImpl ?? fetch;
7073
7423
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
7074
- const requestHash = sha256(`${method} ${options.url}
7424
+ const requestHash = sha2562(`${method} ${options.url}
7075
7425
  ${body}`);
7076
7426
  const base = {
7077
7427
  url: options.url,
@@ -7081,7 +7431,7 @@ ${body}`);
7081
7431
  termsSource: "none",
7082
7432
  advertised: []
7083
7433
  };
7084
- const answer = await handshake(
7434
+ const answer = await handshake2(
7085
7435
  {
7086
7436
  url: options.url,
7087
7437
  method,
@@ -7102,9 +7452,9 @@ ${body}`);
7102
7452
  verdict: "fail"
7103
7453
  };
7104
7454
  }
7105
- const { response, body: text } = answer;
7455
+ const { response, body: text2 } = answer;
7106
7456
  const handshakeMs = answer.latencyMs;
7107
- const parsed = parseTerms(response.headers, text);
7457
+ const parsed = parseTerms(response.headers, text2);
7108
7458
  const checks = runHandshakeChecks({
7109
7459
  status: response.status,
7110
7460
  headers: response.headers,
@@ -7119,11 +7469,24 @@ ${body}`);
7119
7469
  handshakeMs,
7120
7470
  termsSource: parsed.source,
7121
7471
  advertised: advertisedFrom(parsed.terms),
7122
- responseHash: sha256(text),
7472
+ responseHash: sha2562(text2),
7123
7473
  checks,
7124
7474
  verdict: verdictOf(checks)
7125
7475
  };
7126
7476
  }
7477
+ function askedForPayment(result) {
7478
+ return result.checks.some((c) => c.id === "status_402" && c.status === "pass");
7479
+ }
7480
+ async function probeEitherVerb(run, explicit) {
7481
+ const verbs = explicit === void 0 ? ["GET", "POST"] : [explicit.toUpperCase()];
7482
+ let first;
7483
+ for (const method of verbs) {
7484
+ const attempt = await run(method);
7485
+ if (askedForPayment(attempt)) return attempt;
7486
+ first ??= attempt;
7487
+ }
7488
+ return first;
7489
+ }
7127
7490
 
7128
7491
  // src/report.ts
7129
7492
  var ANSI = {
@@ -7139,8 +7502,8 @@ function shouldColour(env2, isTty) {
7139
7502
  if (env2.FORCE_COLOR !== void 0 && env2.FORCE_COLOR !== "0") return true;
7140
7503
  return isTty;
7141
7504
  }
7142
- function paint(colour, text, on) {
7143
- return on ? `${ANSI[colour]}${text}${ANSI.reset}` : text;
7505
+ function paint(colour, text2, on) {
7506
+ return on ? `${ANSI[colour]}${text2}${ANSI.reset}` : text2;
7144
7507
  }
7145
7508
  var MARK = { pass: "ok", fail: "fail", skip: "skip" };
7146
7509
  var COLOUR = { pass: "green", fail: "red", skip: "yellow" };
@@ -7192,7 +7555,8 @@ var USAGE = `
7192
7555
  Sends one unpaid request and reports what the endpoint advertises, in the
7193
7556
  same form the public record uses.
7194
7557
 
7195
- --method <verb> default POST
7558
+ --mcp handshake a remote mcp server instead of an x402 endpoint
7559
+ --method <verb> tries get then post when not given
7196
7560
  --body <json|@file> default {}
7197
7561
  --json machine readable output
7198
7562
  --timeout <ms> default 15000
@@ -7200,11 +7564,26 @@ var USAGE = `
7200
7564
 
7201
7565
  Exit codes: 0 every check passed, 1 a check failed, 2 could not be reached.
7202
7566
  `;
7567
+ function refuseUnlessUsable(url) {
7568
+ let parsed;
7569
+ try {
7570
+ parsed = new URL(url);
7571
+ } catch {
7572
+ stdout.write(`not a url: ${url}
7573
+ `);
7574
+ exit(2);
7575
+ }
7576
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
7577
+ stdout.write("refusing to send a request over plain http\n");
7578
+ exit(2);
7579
+ }
7580
+ }
7203
7581
  async function main() {
7204
7582
  const { values, positionals } = parseArgs({
7205
7583
  args: argv.slice(2),
7206
7584
  allowPositionals: true,
7207
7585
  options: {
7586
+ mcp: { type: "boolean", default: false },
7208
7587
  method: { type: "string" },
7209
7588
  body: { type: "string" },
7210
7589
  json: { type: "boolean", default: false },
@@ -7219,33 +7598,25 @@ async function main() {
7219
7598
  `);
7220
7599
  exit(values.help ? 0 : 2);
7221
7600
  }
7222
- let parsedUrl;
7223
- try {
7224
- parsedUrl = new URL(url);
7225
- } catch {
7226
- stdout.write(`not a url: ${url}
7227
- `);
7228
- exit(2);
7229
- }
7230
- if (parsedUrl.protocol !== "https:" && parsedUrl.hostname !== "localhost") {
7231
- stdout.write("refusing to send a request over plain http\n");
7232
- exit(2);
7233
- }
7234
- const rawBody = values.body ?? "{}";
7235
- const body = rawBody.startsWith("@") ? readFileSync(rawBody.slice(1), "utf8") : rawBody;
7236
- const result = await probe({
7601
+ refuseUnlessUsable(url);
7602
+ const shared = {
7237
7603
  url,
7238
- method: values.method?.toUpperCase() ?? "POST",
7239
- body,
7240
7604
  ...values.timeout ? { timeoutMs: Number(values.timeout) } : {},
7241
7605
  ...values["user-agent"] ? { userAgent: values["user-agent"] } : {}
7242
- });
7606
+ };
7607
+ const rawBody = values.body ?? "{}";
7608
+ const body = rawBody.startsWith("@") ? readFileSync(rawBody.slice(1), "utf8") : rawBody;
7609
+ const found = await probeEitherVerb(
7610
+ (method) => probe({ ...shared, method, body }),
7611
+ values.method
7612
+ );
7613
+ const out = values.mcp ? await probeMcpEndpoint(shared) : found;
7243
7614
  stdout.write(
7244
- values.json ? `${formatJson(result)}
7245
- ` : formatText(result, shouldColour(env, stdout.isTTY === true))
7615
+ values.json ? `${formatJson(out)}
7616
+ ` : formatText(out, shouldColour(env, stdout.isTTY === true))
7246
7617
  );
7247
- if (!result.reachable) exit(2);
7248
- exit(result.verdict === "pass" ? 0 : 1);
7618
+ if (!out.reachable) exit(2);
7619
+ exit(out.verdict === "pass" ? 0 : 1);
7249
7620
  }
7250
7621
  main().catch((err) => {
7251
7622
  stdout.write(`${err.message}
package/npm/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { type PaymentRequirement, type PaymentTerms, parseTerms, priceOf, schemasOf, } from './terms.js';
2
- export { type Check, type CheckStatus, runHandshakeChecks, schemaCheck, verdictOf, } from './checks.ts';
3
- export { DEFAULT_USER_AGENT, type ProbeOptions, type ProbeResult, probe } from './probe.ts';
4
- export { formatJson, formatText, shouldColour } from './report.ts';
2
+ export { type Check, type CheckStatus, runHandshakeChecks, schemaCheck, verdictOf, } from './checks.js';
3
+ export { checksFor, type McpProbeOptions, probeMcpEndpoint, REACH_LABEL, } from './mcp.js';
4
+ export { DEFAULT_USER_AGENT, type ProbeOptions, type ProbeResult, probe } from './probe.js';
5
+ export { formatJson, formatText, shouldColour } from './report.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,UAAU,EACV,OAAO,EACP,SAAS,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,kBAAkB,EAClB,WAAW,EACX,SAAS,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,UAAU,EACV,OAAO,EACP,SAAS,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,kBAAkB,EAClB,WAAW,EACX,SAAS,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,SAAS,EACT,KAAK,eAAe,EACpB,gBAAgB,EAChB,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/npm/index.js CHANGED
@@ -4608,7 +4608,7 @@ var require_core = __commonJS({
4608
4608
  errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
4609
4609
  if (!errors || errors.length === 0)
4610
4610
  return "No errors";
4611
- return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg);
4611
+ return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text2, msg) => text2 + separator + msg);
4612
4612
  }
4613
4613
  $dataMetaSchema(metaSchema, keywordsJsonPointers) {
4614
4614
  const rules = this.RULES.all;
@@ -7015,32 +7015,382 @@ function verdictOf(checks) {
7015
7015
  return checks.some((c) => c.status === "fail") ? "fail" : "pass";
7016
7016
  }
7017
7017
 
7018
- // src/probe.ts
7018
+ // src/mcp.ts
7019
+ import { createHash as createHash2 } from "node:crypto";
7020
+
7021
+ // ../mcp-probe/src/frame.ts
7022
+ var JSON_RPC = "2.0";
7023
+ var PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26"];
7024
+ var PREFERRED_VERSION = PROTOCOL_VERSIONS[0];
7025
+ function request(id, method, params) {
7026
+ return JSON.stringify({ jsonrpc: JSON_RPC, id, method, ...params ? { params } : {} });
7027
+ }
7028
+ function notification(method) {
7029
+ return JSON.stringify({ jsonrpc: JSON_RPC, method });
7030
+ }
7031
+ function lastEventData(body) {
7032
+ const payloads = body.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter((line) => line.length > 0);
7033
+ return payloads.length === 0 ? null : payloads[payloads.length - 1];
7034
+ }
7035
+ function readOne(text2) {
7036
+ let parsed;
7037
+ try {
7038
+ parsed = JSON.parse(text2);
7039
+ } catch {
7040
+ return { kind: "unreadable", why: "the answer is not json" };
7041
+ }
7042
+ const one = Array.isArray(parsed) ? parsed[0] : parsed;
7043
+ if (typeof one !== "object" || one === null) {
7044
+ return { kind: "unreadable", why: "the answer is not a json rpc object" };
7045
+ }
7046
+ const message = one;
7047
+ if (message.error !== void 0) {
7048
+ const error = message.error;
7049
+ return {
7050
+ kind: "error",
7051
+ code: typeof error.code === "number" ? error.code : 0,
7052
+ message: typeof error.message === "string" ? error.message : "no message"
7053
+ };
7054
+ }
7055
+ if (typeof message.result !== "object" || message.result === null) {
7056
+ return { kind: "unreadable", why: "the answer carries neither a result nor an error" };
7057
+ }
7058
+ return { kind: "result", value: message.result };
7059
+ }
7060
+ function readAnswer(contentType, body) {
7061
+ const streamed = (contentType ?? "").toLowerCase().includes("text/event-stream");
7062
+ if (!streamed) return readOne(body);
7063
+ const data = lastEventData(body);
7064
+ return data === null ? { kind: "unreadable", why: "the stream carried no data line" } : readOne(data);
7065
+ }
7066
+
7067
+ // ../mcp-probe/src/probe.ts
7019
7068
  import { createHash } from "node:crypto";
7069
+ var HANDSHAKE_TIMEOUT_MS = 2e4;
7070
+ var CLIENT = { name: "teppi-probe", version: "0.1.0" };
7071
+ function initFor(body, sessionId, userAgent) {
7072
+ return {
7073
+ method: "POST",
7074
+ headers: {
7075
+ "content-type": "application/json",
7076
+ accept: "application/json, text/event-stream",
7077
+ "mcp-protocol-version": PREFERRED_VERSION,
7078
+ "user-agent": userAgent,
7079
+ ...sessionId === null ? {} : { "mcp-session-id": sessionId }
7080
+ },
7081
+ body
7082
+ };
7083
+ }
7084
+ var INITIALIZE = {
7085
+ protocolVersion: PREFERRED_VERSION,
7086
+ capabilities: {},
7087
+ clientInfo: CLIENT
7088
+ };
7089
+ var text = (value) => typeof value === "string" ? value : null;
7090
+ function toolsFrom(result) {
7091
+ const raw = result.tools;
7092
+ if (!Array.isArray(raw)) return [];
7093
+ return raw.flatMap((value) => {
7094
+ if (typeof value !== "object" || value === null) return [];
7095
+ const tool = value;
7096
+ const name = text(tool.name);
7097
+ if (name === null) return [];
7098
+ return [
7099
+ {
7100
+ name,
7101
+ description: text(tool.description),
7102
+ inputSchema: tool.inputSchema ?? null,
7103
+ outputSchema: tool.outputSchema ?? null
7104
+ }
7105
+ ];
7106
+ });
7107
+ }
7108
+ function reachFor(status) {
7109
+ if (status === 401 || status === 403) return "auth_required";
7110
+ if (status === 402) return "payment_required";
7111
+ if (status >= 500 || status === 404 || status === 410) return "dead";
7112
+ if (status >= 400) return "error";
7113
+ return null;
7114
+ }
7115
+ async function turn(response, method) {
7116
+ const refused = reachFor(response.status);
7117
+ if (refused !== null) return { kind: "refused", reach: refused, status: response.status };
7118
+ const answer = readAnswer(response.headers.get("content-type"), await response.text());
7119
+ if (answer.kind === "result") {
7120
+ return { kind: "read", status: response.status, value: answer.value };
7121
+ }
7122
+ return {
7123
+ kind: "broken",
7124
+ status: response.status,
7125
+ note: answer.kind === "error" ? `${method}: ${answer.message}` : answer.why
7126
+ };
7127
+ }
7128
+ var DEADLINE_MS = 45e3;
7129
+ var MAX_PAGES = 20;
7130
+ var MAX_TOOLS = 2e3;
7131
+ async function probeMcp(url, options = {}) {
7132
+ const clockFor = options.now ?? Date.now;
7133
+ const startedFor = clockFor();
7134
+ let timer;
7135
+ const gaveUp = new Promise((resolve) => {
7136
+ timer = setTimeout(
7137
+ () => resolve({
7138
+ url,
7139
+ observedAt: new Date(startedFor).toISOString(),
7140
+ reach: "dead",
7141
+ httpStatus: null,
7142
+ handshakeMs: options.deadlineMs ?? DEADLINE_MS,
7143
+ protocolVersion: null,
7144
+ serverName: null,
7145
+ serverVersion: null,
7146
+ instructions: null,
7147
+ tools: null,
7148
+ pages: null,
7149
+ truncated: false,
7150
+ note: "never answered and never gave up"
7151
+ }),
7152
+ options.deadlineMs ?? DEADLINE_MS
7153
+ );
7154
+ });
7155
+ try {
7156
+ return await Promise.race([handshake(url, options), gaveUp]);
7157
+ } finally {
7158
+ clearTimeout(timer);
7159
+ }
7160
+ }
7161
+ async function walkTools(send, session) {
7162
+ const tools = [];
7163
+ let cursor = null;
7164
+ let pages = 0;
7165
+ let status = 200;
7166
+ for (; ; ) {
7167
+ const asked = request(2 + pages, "tools/list", cursor === null ? void 0 : { cursor });
7168
+ const listed = await turn(await send(asked, session), "tools/list");
7169
+ if (listed.kind === "refused") {
7170
+ return {
7171
+ kind: "refused",
7172
+ reach: listed.reach,
7173
+ status: listed.status,
7174
+ note: "it opened but would not list"
7175
+ };
7176
+ }
7177
+ if (listed.kind === "broken")
7178
+ return { kind: "broken", status: listed.status, note: listed.note };
7179
+ tools.push(...toolsFrom(listed.value));
7180
+ status = listed.status;
7181
+ pages += 1;
7182
+ const next = text(listed.value.nextCursor);
7183
+ if (next === null || next === cursor)
7184
+ return { kind: "listed", status, tools, pages, truncated: false };
7185
+ if (pages >= MAX_PAGES || tools.length >= MAX_TOOLS) {
7186
+ return { kind: "listed", status, tools, pages, truncated: true };
7187
+ }
7188
+ cursor = next;
7189
+ }
7190
+ }
7191
+ async function handshake(url, options) {
7192
+ const doFetch = options.fetchImpl ?? fetch;
7193
+ const clock = options.now ?? Date.now;
7194
+ const userAgent = options.userAgent ?? `${CLIENT.name}/${CLIENT.version}`;
7195
+ const startedAt = clock();
7196
+ const base = {
7197
+ url,
7198
+ observedAt: new Date(startedAt).toISOString(),
7199
+ protocolVersion: null,
7200
+ serverName: null,
7201
+ serverVersion: null,
7202
+ instructions: null,
7203
+ tools: null,
7204
+ pages: null,
7205
+ truncated: false
7206
+ };
7207
+ const send = (body, sessionId) => doFetch(url, {
7208
+ ...initFor(body, sessionId, userAgent),
7209
+ signal: AbortSignal.timeout(options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7210
+ });
7211
+ const took = () => Math.max(0, clock() - startedAt);
7212
+ try {
7213
+ const opened = await send(request(1, "initialize", INITIALIZE), null);
7214
+ const hello = await turn(opened, "initialize");
7215
+ if (hello.kind === "refused") {
7216
+ return {
7217
+ ...base,
7218
+ reach: hello.reach,
7219
+ httpStatus: hello.status,
7220
+ handshakeMs: took(),
7221
+ note: null
7222
+ };
7223
+ }
7224
+ if (hello.kind === "broken") {
7225
+ return {
7226
+ ...base,
7227
+ reach: "error",
7228
+ httpStatus: hello.status,
7229
+ handshakeMs: took(),
7230
+ note: hello.note
7231
+ };
7232
+ }
7233
+ const info = hello.value.serverInfo ?? {};
7234
+ const session = opened.headers.get("mcp-session-id");
7235
+ const said = {
7236
+ ...base,
7237
+ protocolVersion: text(hello.value.protocolVersion),
7238
+ serverName: text(info.name),
7239
+ serverVersion: text(info.version),
7240
+ instructions: text(hello.value.instructions)
7241
+ };
7242
+ await send(notification("notifications/initialized"), session).catch(() => void 0);
7243
+ const walked = await walkTools(send, session);
7244
+ if (walked.kind !== "listed") {
7245
+ return {
7246
+ ...said,
7247
+ reach: walked.kind === "refused" ? walked.reach : "error",
7248
+ httpStatus: walked.status,
7249
+ handshakeMs: took(),
7250
+ note: walked.note
7251
+ };
7252
+ }
7253
+ return {
7254
+ ...said,
7255
+ reach: "readable",
7256
+ httpStatus: walked.status,
7257
+ handshakeMs: took(),
7258
+ tools: walked.tools,
7259
+ pages: walked.pages,
7260
+ truncated: walked.truncated,
7261
+ note: null
7262
+ };
7263
+ } catch (err) {
7264
+ const name = err.name;
7265
+ return {
7266
+ ...base,
7267
+ reach: "dead",
7268
+ httpStatus: null,
7269
+ handshakeMs: took(),
7270
+ note: name === "TimeoutError" || name === "AbortError" ? "timed out" : "unreachable"
7271
+ };
7272
+ }
7273
+ }
7274
+ function toolsDigest(tools) {
7275
+ const canonical = JSON.stringify(
7276
+ [...tools].map((t) => [t.name, t.description ?? "", t.inputSchema !== null]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
7277
+ );
7278
+ return createHash("sha256").update(canonical).digest("hex");
7279
+ }
7280
+
7281
+ // src/mcp.ts
7282
+ function sha256(input) {
7283
+ return `sha256:${createHash2("sha256").update(input).digest("hex")}`;
7284
+ }
7285
+ var pass2 = (id, label, detail) => detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7286
+ var fail2 = (id, label, detail) => ({
7287
+ id,
7288
+ label,
7289
+ status: "fail",
7290
+ detail
7291
+ });
7292
+ var skip = (id, label, detail) => ({
7293
+ id,
7294
+ label,
7295
+ status: "skip",
7296
+ detail
7297
+ });
7298
+ var REACH_LABEL = {
7299
+ readable: "listed its tools",
7300
+ auth_required: "asked for credentials",
7301
+ payment_required: "asked for payment",
7302
+ dead: "did not answer",
7303
+ error: "answered with something else"
7304
+ };
7305
+ function checksFor(seen) {
7306
+ const checks = [];
7307
+ checks.push(
7308
+ seen.reach === "dead" ? fail2("reachable", "server answered", seen.note ?? "no answer") : pass2("reachable", "server answered", REACH_LABEL[seen.reach])
7309
+ );
7310
+ if (seen.reach === "dead") return checks;
7311
+ checks.push(
7312
+ seen.protocolVersion === null ? skip("protocol", "named a protocol version", "it never got as far as saying") : pass2("protocol", "named a protocol version", seen.protocolVersion)
7313
+ );
7314
+ checks.push(
7315
+ seen.serverName === null ? skip("identity", "named itself", "it never got as far as saying") : pass2(
7316
+ "identity",
7317
+ "named itself",
7318
+ `${seen.serverName}${seen.serverVersion === null ? "" : ` ${seen.serverVersion}`}`
7319
+ )
7320
+ );
7321
+ if (seen.tools === null) {
7322
+ checks.push(skip("tools", "listed its tools", REACH_LABEL[seen.reach]));
7323
+ return checks;
7324
+ }
7325
+ checks.push(pass2("tools", "listed its tools", `${seen.tools.length} tool(s)`));
7326
+ const bare = seen.tools.filter((t) => t.inputSchema === null).map((t) => t.name);
7327
+ checks.push(
7328
+ bare.length === 0 ? pass2("input-schemas", "every tool declared an input schema") : fail2(
7329
+ "input-schemas",
7330
+ "every tool declared an input schema",
7331
+ `${bare.length} without one: ${bare.slice(0, 5).join(", ")}`
7332
+ )
7333
+ );
7334
+ const unnamed = seen.tools.filter((t) => t.description === null).map((t) => t.name);
7335
+ checks.push(
7336
+ unnamed.length === 0 ? pass2("descriptions", "every tool said what it does") : fail2(
7337
+ "descriptions",
7338
+ "every tool said what it does",
7339
+ `${unnamed.length} without one: ${unnamed.slice(0, 5).join(", ")}`
7340
+ )
7341
+ );
7342
+ checks.push(pass2("digest", "tool list digest", toolsDigest(seen.tools)));
7343
+ return checks;
7344
+ }
7345
+ async function probeMcpEndpoint(options) {
7346
+ const seen = await probeMcp(options.url, {
7347
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
7348
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
7349
+ ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7350
+ });
7351
+ const checks = checksFor(seen);
7352
+ return {
7353
+ url: options.url,
7354
+ method: "MCP",
7355
+ observedAt: seen.observedAt,
7356
+ reachable: seen.reach !== "dead",
7357
+ httpStatus: seen.httpStatus,
7358
+ handshakeMs: seen.handshakeMs,
7359
+ termsSource: seen.reach === "payment_required" ? "mcp 402" : "none",
7360
+ advertised: [],
7361
+ requestHash: sha256(`MCP ${options.url}`),
7362
+ responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7363
+ checks,
7364
+ verdict: checks.some((c) => c.status === "fail") ? "fail" : "pass"
7365
+ };
7366
+ }
7367
+
7368
+ // src/probe.ts
7369
+ import { createHash as createHash3 } from "node:crypto";
7020
7370
 
7021
7371
  // ../x402/src/handshake.ts
7022
- var HANDSHAKE_TIMEOUT_MS = 3e4;
7372
+ var HANDSHAKE_TIMEOUT_MS2 = 3e4;
7023
7373
  var sendsBody = (method) => method !== "GET" && method !== "HEAD";
7024
- function handshakeInit(request) {
7025
- const carries = sendsBody(request.method);
7374
+ function handshakeInit(request2) {
7375
+ const carries = sendsBody(request2.method);
7026
7376
  const headers = { accept: "application/json" };
7027
7377
  if (carries) headers["content-type"] = "application/json";
7028
- if (request.userAgent !== void 0) headers["user-agent"] = request.userAgent;
7029
- if (request.payment) headers[request.payment.header] = request.payment.value;
7378
+ if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7379
+ if (request2.payment) headers[request2.payment.header] = request2.payment.value;
7030
7380
  return {
7031
- method: request.method,
7381
+ method: request2.method,
7032
7382
  headers,
7033
- ...carries ? { body: JSON.stringify(request.input ?? {}) } : {},
7034
- signal: AbortSignal.timeout(request.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7383
+ ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7384
+ signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
7035
7385
  };
7036
7386
  }
7037
- async function handshake(request, deps = {}) {
7387
+ async function handshake2(request2, deps = {}) {
7038
7388
  const doFetch = deps.fetchImpl ?? fetch;
7039
7389
  const now = deps.now ?? Date.now;
7040
7390
  const started = now();
7041
7391
  const since = () => Math.max(0, now() - started);
7042
7392
  try {
7043
- const response = await doFetch(request.url, handshakeInit(request));
7393
+ const response = await doFetch(request2.url, handshakeInit(request2));
7044
7394
  return { ok: true, response, body: await response.text(), latencyMs: since() };
7045
7395
  } catch (err) {
7046
7396
  return { ok: false, reason: err.message, latencyMs: since() };
@@ -7049,8 +7399,8 @@ async function handshake(request, deps = {}) {
7049
7399
 
7050
7400
  // src/probe.ts
7051
7401
  var DEFAULT_USER_AGENT = "node";
7052
- function sha256(input) {
7053
- return `sha256:${createHash("sha256").update(input).digest("hex")}`;
7402
+ function sha2562(input) {
7403
+ return `sha256:${createHash3("sha256").update(input).digest("hex")}`;
7054
7404
  }
7055
7405
  function advertisedFrom(terms) {
7056
7406
  if (!terms) return [];
@@ -7066,7 +7416,7 @@ async function probe(options) {
7066
7416
  const body = options.body ?? "{}";
7067
7417
  const doFetch = options.fetchImpl ?? fetch;
7068
7418
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
7069
- const requestHash = sha256(`${method} ${options.url}
7419
+ const requestHash = sha2562(`${method} ${options.url}
7070
7420
  ${body}`);
7071
7421
  const base = {
7072
7422
  url: options.url,
@@ -7076,7 +7426,7 @@ ${body}`);
7076
7426
  termsSource: "none",
7077
7427
  advertised: []
7078
7428
  };
7079
- const answer = await handshake(
7429
+ const answer = await handshake2(
7080
7430
  {
7081
7431
  url: options.url,
7082
7432
  method,
@@ -7097,9 +7447,9 @@ ${body}`);
7097
7447
  verdict: "fail"
7098
7448
  };
7099
7449
  }
7100
- const { response, body: text } = answer;
7450
+ const { response, body: text2 } = answer;
7101
7451
  const handshakeMs = answer.latencyMs;
7102
- const parsed = parseTerms(response.headers, text);
7452
+ const parsed = parseTerms(response.headers, text2);
7103
7453
  const checks = runHandshakeChecks({
7104
7454
  status: response.status,
7105
7455
  headers: response.headers,
@@ -7114,7 +7464,7 @@ ${body}`);
7114
7464
  handshakeMs,
7115
7465
  termsSource: parsed.source,
7116
7466
  advertised: advertisedFrom(parsed.terms),
7117
- responseHash: sha256(text),
7467
+ responseHash: sha2562(text2),
7118
7468
  checks,
7119
7469
  verdict: verdictOf(checks)
7120
7470
  };
@@ -7134,8 +7484,8 @@ function shouldColour(env, isTty) {
7134
7484
  if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
7135
7485
  return isTty;
7136
7486
  }
7137
- function paint(colour, text, on) {
7138
- return on ? `${ANSI[colour]}${text}${ANSI.reset}` : text;
7487
+ function paint(colour, text2, on) {
7488
+ return on ? `${ANSI[colour]}${text2}${ANSI.reset}` : text2;
7139
7489
  }
7140
7490
  var MARK = { pass: "ok", fail: "fail", skip: "skip" };
7141
7491
  var COLOUR = { pass: "green", fail: "red", skip: "yellow" };
@@ -7181,11 +7531,14 @@ function formatJson(result) {
7181
7531
  }
7182
7532
  export {
7183
7533
  DEFAULT_USER_AGENT,
7534
+ REACH_LABEL,
7535
+ checksFor,
7184
7536
  formatJson,
7185
7537
  formatText,
7186
7538
  parseTerms,
7187
7539
  priceOf,
7188
7540
  probe,
7541
+ probeMcpEndpoint,
7189
7542
  runHandshakeChecks,
7190
7543
  schemaCheck,
7191
7544
  schemasOf,
@@ -0,0 +1,18 @@
1
+ export declare const JSON_RPC = "2.0";
2
+ export declare const PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26"];
3
+ export declare const PREFERRED_VERSION: "2025-06-18";
4
+ export type RpcResult = {
5
+ readonly kind: 'result';
6
+ readonly value: Record<string, unknown>;
7
+ } | {
8
+ readonly kind: 'error';
9
+ readonly code: number;
10
+ readonly message: string;
11
+ } | {
12
+ readonly kind: 'unreadable';
13
+ readonly why: string;
14
+ };
15
+ export declare function request(id: number, method: string, params?: Record<string, unknown>): string;
16
+ export declare function notification(method: string): string;
17
+ export declare function lastEventData(body: string): string | null;
18
+ export declare function readAnswer(contentType: string | null, body: string): RpcResult;
@@ -0,0 +1,2 @@
1
+ export { JSON_RPC, lastEventData, notification, PREFERRED_VERSION, PROTOCOL_VERSIONS, type RpcResult, readAnswer, request, } from './frame.js';
2
+ export { CLIENT, definitionDigest, definitionOf, HANDSHAKE_TIMEOUT_MS, INITIALIZE, initFor, MAX_PAGES, MAX_TOOLS, type McpObservation, type ProbeOptions, probeMcp, type Reach, reachFor, type Tool, toolsDigest, toolsFrom, } from './probe.js';
@@ -0,0 +1,52 @@
1
+ export declare const HANDSHAKE_TIMEOUT_MS = 20000;
2
+ export declare const CLIENT: {
3
+ readonly name: "teppi-probe";
4
+ readonly version: "0.1.0";
5
+ };
6
+ export type Reach = 'readable' | 'auth_required' | 'payment_required' | 'dead' | 'error';
7
+ export type Tool = {
8
+ readonly name: string;
9
+ readonly description: string | null;
10
+ readonly inputSchema: unknown;
11
+ readonly outputSchema: unknown;
12
+ };
13
+ export type McpObservation = {
14
+ readonly url: string;
15
+ readonly observedAt: string;
16
+ readonly reach: Reach;
17
+ readonly httpStatus: number | null;
18
+ readonly handshakeMs: number;
19
+ readonly protocolVersion: string | null;
20
+ readonly serverName: string | null;
21
+ readonly serverVersion: string | null;
22
+ readonly instructions: string | null;
23
+ readonly tools: readonly Tool[] | null;
24
+ readonly pages: number | null;
25
+ readonly truncated: boolean;
26
+ readonly note: string | null;
27
+ };
28
+ export type ProbeOptions = {
29
+ readonly timeoutMs?: number;
30
+ readonly deadlineMs?: number;
31
+ readonly fetchImpl?: typeof fetch;
32
+ readonly now?: () => number;
33
+ readonly userAgent?: string;
34
+ };
35
+ export declare function initFor(body: string, sessionId: string | null, userAgent: string): RequestInit;
36
+ export declare const INITIALIZE: {
37
+ protocolVersion: "2025-06-18";
38
+ capabilities: {};
39
+ clientInfo: {
40
+ readonly name: "teppi-probe";
41
+ readonly version: "0.1.0";
42
+ };
43
+ };
44
+ export declare function toolsFrom(result: Record<string, unknown>): Tool[];
45
+ export declare function reachFor(status: number): Reach | null;
46
+ export declare const DEADLINE_MS = 45000;
47
+ export declare const MAX_PAGES = 20;
48
+ export declare const MAX_TOOLS = 2000;
49
+ export declare function probeMcp(url: string, options?: ProbeOptions): Promise<McpObservation>;
50
+ export declare function toolsDigest(tools: readonly Tool[]): string;
51
+ export declare function definitionOf(seen: McpObservation): string;
52
+ export declare function definitionDigest(seen: McpObservation): string;
package/npm/mcp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type McpObservation } from './mcp-probe/index.js';
2
+ import type { Check } from './checks.js';
3
+ import type { ProbeResult } from './probe.js';
4
+ export type McpProbeOptions = {
5
+ readonly url: string;
6
+ readonly userAgent?: string;
7
+ readonly timeoutMs?: number;
8
+ readonly fetchImpl?: typeof fetch;
9
+ };
10
+ export declare const REACH_LABEL: Readonly<Record<McpObservation['reach'], string>>;
11
+ export declare function checksFor(seen: McpObservation): Check[];
12
+ export declare function probeMcpEndpoint(options: McpProbeOptions): Promise<ProbeResult>;
13
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAyB,MAAM,kBAAkB,CAAC;AAC9E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC,CAAC;AAwBF,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAMzE,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,KAAK,EAAE,CA2DvD;AAGD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAsBrF"}
package/npm/probe.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Check } from './checks.ts';
1
+ import { type Check } from './checks.js';
2
2
  export declare const DEFAULT_USER_AGENT = "node";
3
3
  export type ProbeOptions = {
4
4
  readonly url: string;
@@ -28,4 +28,6 @@ export type ProbeResult = {
28
28
  readonly verdict: 'pass' | 'fail';
29
29
  };
30
30
  export declare function probe(options: ProbeOptions): Promise<ProbeResult>;
31
+ export declare function askedForPayment(result: ProbeResult): boolean;
32
+ export declare function probeEitherVerb(run: (method: string) => Promise<ProbeResult>, explicit?: string): Promise<ProbeResult>;
31
33
  //# sourceMappingURL=probe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAA8C,MAAM,aAAa,CAAC;AAGrF,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+DvE"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAA8C,MAAM,aAAa,CAAC;AAGrF,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+DvE;AAGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAE5D;AAGD,wBAAsB,eAAe,CACpC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,EAC7C,QAAQ,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CAStB"}
package/npm/report.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ProbeResult } from './probe.ts';
1
+ import type { ProbeResult } from './probe.js';
2
2
  export declare function shouldColour(env: Record<string, string | undefined>, isTty: boolean): boolean;
3
3
  export declare function formatText(result: ProbeResult, colour: boolean): string;
4
4
  export declare function formatJson(result: ProbeResult): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teppi-check",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Probe any paid endpoint and print what it actually returns for the money. Reproduces any entry in the Teppi record.",
5
5
  "keywords": [
6
6
  "x402",
@@ -20,6 +20,7 @@
20
20
  ".": "./npm/index.js"
21
21
  },
22
22
  "devDependencies": {
23
+ "@teppi/mcp-probe": "0.0.0",
23
24
  "@teppi/x402": "0.0.0",
24
25
  "ajv": "^8.17.1",
25
26
  "esbuild": "^0.28.2",