teppi-check 0.2.0 → 0.3.0

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
@@ -1,6 +1,7 @@
1
1
  # teppi-check
2
2
 
3
- Send one unpaid request to any paid endpoint and see what it actually advertises.
3
+ Send one unpaid request to any paid endpoint and see what it actually advertises, and what is
4
+ already broken about the listing before you pay anything.
4
5
 
5
6
  ```bash
6
7
  npx teppi-check https://some-seller.example/v1/extract
@@ -36,13 +37,16 @@ in the record something you can check rather than something you are asked to acc
36
37
  payment terms are not cached fail
37
38
  no cache-control, so a proxy may serve stale terms
38
39
 
40
+ prose_in_the_url defect
41
+ a query value spells out what to put there instead of putting it
42
+
39
43
  advertised
40
44
  10000 0x2222...2222 on eip155:8453 via exact
41
45
 
42
46
  request sha256:ba6f7ff0...0878
43
47
  response sha256:0a83a213...2496
44
48
 
45
- 2 checks failed
49
+ 3 problems found
46
50
  ```
47
51
 
48
52
  ## Why this exists
@@ -71,6 +75,23 @@ takes a purchase, which the full prober does on a schedule.
71
75
  Terms are read from the `payment-required` header (base64 or plain JSON) or from the response
72
76
  body, because deployed servers use both.
73
77
 
78
+ ## What it finds wrong with the listing
79
+
80
+ A check reads the answer. A defect reads the listing itself, and an endpoint can answer perfectly
81
+ while being impossible to buy from.
82
+
83
+ | Defect | Means |
84
+ |---|---|
85
+ | `prose_in_the_url` | a query value describes the parameter instead of filling it in, or a path segment is still `:id` or `{id}`, or an address is forty repeated characters |
86
+ | `path_not_found` | the listed path answered 404 or 410, so there is nothing there to buy |
87
+
88
+ These are the names the public record uses, with the same sentence beside them. A test runs this
89
+ rule and the one the record is built from over the same list of urls and fails if they ever give
90
+ different answers, so the id you read in a Teppi entry is the id you get here.
91
+
92
+ Nothing our own bad request causes is a defect. A 400, a 422 or a 429 says something about the
93
+ caller, not the seller, and none of them appear.
94
+
74
95
  ## Remote MCP servers
75
96
 
76
97
  `--mcp` runs the handshake instead: `initialize`, the initialized notification, then `tools/list`
@@ -97,14 +118,15 @@ which is what makes it useful for noticing that a server's instructions changed
97
118
 
98
119
  ```
99
120
  --mcp handshake a remote mcp server instead of an x402 endpoint
100
- --method <verb> default POST
121
+ --method <verb> tries GET then POST when not given
101
122
  --body <json|@file> default {}
102
123
  --json machine readable output
103
124
  --timeout <ms> default 15000
104
125
  --user-agent <ua> default node
105
126
  ```
106
127
 
107
- Exit codes: `0` every check passed, `1` a check failed, `2` the endpoint could not be reached.
128
+ Exit codes: `0` nothing to report, `1` a check failed or a defect was found, `2` the endpoint
129
+ could not be reached.
108
130
 
109
131
  Requests go over HTTPS only, apart from localhost.
110
132
 
@@ -123,7 +145,10 @@ the failure mode nobody notices until buyers quietly stop calling.
123
145
  import { probe } from 'teppi-check';
124
146
 
125
147
  const result = await probe({ url: 'https://some-seller.example/v1/extract' });
126
- console.log(result.verdict, result.checks);
148
+ console.log(result.verdict, result.checks, result.defects);
127
149
  ```
128
150
 
151
+ `urlHasPlaceholder(url)` and `defectsOf(url, httpStatus)` are exported on their own, for reading a
152
+ list of urls without sending anything.
153
+
129
154
  MIT.
package/npm/checks.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type PaymentTerms } from './terms.js';
2
+ import type { Defect } from './defects.js';
2
3
  export type CheckStatus = 'pass' | 'fail' | 'skip';
3
4
  export type Check = {
4
5
  readonly id: string;
@@ -16,5 +17,5 @@ export type HandshakeInput = {
16
17
  };
17
18
  export declare function runHandshakeChecks(input: HandshakeInput): Check[];
18
19
  export declare function unreachable(reason: string): Check[];
19
- export declare function verdictOf(checks: readonly Check[]): 'pass' | 'fail';
20
+ export declare function verdictOf(checks: readonly Check[], defects: readonly Defect[]): 'pass' | 'fail';
20
21
  //# sourceMappingURL=checks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2B,KAAK,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AAGnG,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAqBF,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,KAAK,CAa7E;AAED,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB,CAAC;AA+BF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,EAAE,CA4CjE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,CAEnD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,GAAG,MAAM,CAEnE"}
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2B,KAAK,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AAEnG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAqBF,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,KAAK,CAa7E;AAED,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB,CAAC;AA+BF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,EAAE,CA4CjE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,CAEnD;AAGD,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,MAAM,CAG/F"}
package/npm/cli.js CHANGED
@@ -6955,7 +6955,8 @@ function toolsFrom(result) {
6955
6955
  {
6956
6956
  name,
6957
6957
  description: text(tool.description),
6958
- hasInputSchema: typeof tool.inputSchema === "object" && tool.inputSchema !== null
6958
+ inputSchema: tool.inputSchema ?? null,
6959
+ outputSchema: tool.outputSchema ?? null
6959
6960
  }
6960
6961
  ];
6961
6962
  });
@@ -6981,6 +6982,8 @@ async function turn(response, method) {
6981
6982
  };
6982
6983
  }
6983
6984
  var DEADLINE_MS = 45e3;
6985
+ var MAX_PAGES = 20;
6986
+ var MAX_TOOLS = 2e3;
6984
6987
  async function probeMcp(url, options = {}) {
6985
6988
  const clockFor = options.now ?? Date.now;
6986
6989
  const startedFor = clockFor();
@@ -6996,7 +6999,10 @@ async function probeMcp(url, options = {}) {
6996
6999
  protocolVersion: null,
6997
7000
  serverName: null,
6998
7001
  serverVersion: null,
7002
+ instructions: null,
6999
7003
  tools: null,
7004
+ pages: null,
7005
+ truncated: false,
7000
7006
  note: "never answered and never gave up"
7001
7007
  }),
7002
7008
  options.deadlineMs ?? DEADLINE_MS
@@ -7008,6 +7014,36 @@ async function probeMcp(url, options = {}) {
7008
7014
  clearTimeout(timer);
7009
7015
  }
7010
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
+ }
7011
7047
  async function handshake(url, options) {
7012
7048
  const doFetch = options.fetchImpl ?? fetch;
7013
7049
  const clock = options.now ?? Date.now;
@@ -7019,7 +7055,10 @@ async function handshake(url, options) {
7019
7055
  protocolVersion: null,
7020
7056
  serverName: null,
7021
7057
  serverVersion: null,
7022
- tools: null
7058
+ instructions: null,
7059
+ tools: null,
7060
+ pages: null,
7061
+ truncated: false
7023
7062
  };
7024
7063
  const send = (body, sessionId) => doFetch(url, {
7025
7064
  ...initFor(body, sessionId, userAgent),
@@ -7053,34 +7092,28 @@ async function handshake(url, options) {
7053
7092
  ...base,
7054
7093
  protocolVersion: text(hello.value.protocolVersion),
7055
7094
  serverName: text(info.name),
7056
- serverVersion: text(info.version)
7095
+ serverVersion: text(info.version),
7096
+ instructions: text(hello.value.instructions)
7057
7097
  };
7058
7098
  await send(notification("notifications/initialized"), session).catch(() => void 0);
7059
- const listed = await turn(await send(request(2, "tools/list"), session), "tools/list");
7060
- if (listed.kind === "refused") {
7099
+ const walked = await walkTools(send, session);
7100
+ if (walked.kind !== "listed") {
7061
7101
  return {
7062
7102
  ...said,
7063
- reach: listed.reach,
7064
- httpStatus: listed.status,
7103
+ reach: walked.kind === "refused" ? walked.reach : "error",
7104
+ httpStatus: walked.status,
7065
7105
  handshakeMs: took(),
7066
- note: "it opened but would not list"
7067
- };
7068
- }
7069
- if (listed.kind === "broken") {
7070
- return {
7071
- ...said,
7072
- reach: "error",
7073
- httpStatus: listed.status,
7074
- handshakeMs: took(),
7075
- note: listed.note
7106
+ note: walked.note
7076
7107
  };
7077
7108
  }
7078
7109
  return {
7079
7110
  ...said,
7080
7111
  reach: "readable",
7081
- httpStatus: listed.status,
7112
+ httpStatus: walked.status,
7082
7113
  handshakeMs: took(),
7083
- tools: toolsFrom(listed.value),
7114
+ tools: walked.tools,
7115
+ pages: walked.pages,
7116
+ truncated: walked.truncated,
7084
7117
  note: null
7085
7118
  };
7086
7119
  } catch (err) {
@@ -7096,130 +7129,11 @@ async function handshake(url, options) {
7096
7129
  }
7097
7130
  function toolsDigest(tools) {
7098
7131
  const canonical = JSON.stringify(
7099
- [...tools].map((t) => [t.name, t.description ?? "", t.hasInputSchema]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
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)
7100
7133
  );
7101
7134
  return createHash("sha256").update(canonical).digest("hex");
7102
7135
  }
7103
7136
 
7104
- // src/mcp.ts
7105
- function sha256(input) {
7106
- return `sha256:${createHash2("sha256").update(input).digest("hex")}`;
7107
- }
7108
- var pass = (id, label, detail) => detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7109
- var fail = (id, label, detail) => ({
7110
- id,
7111
- label,
7112
- status: "fail",
7113
- detail
7114
- });
7115
- var skip = (id, label, detail) => ({
7116
- id,
7117
- label,
7118
- status: "skip",
7119
- detail
7120
- });
7121
- var REACH_LABEL = {
7122
- readable: "listed its tools",
7123
- auth_required: "asked for credentials",
7124
- payment_required: "asked for payment",
7125
- dead: "did not answer",
7126
- error: "answered with something else"
7127
- };
7128
- function checksFor(seen) {
7129
- const checks = [];
7130
- checks.push(
7131
- seen.reach === "dead" ? fail("reachable", "server answered", seen.note ?? "no answer") : pass("reachable", "server answered", REACH_LABEL[seen.reach])
7132
- );
7133
- if (seen.reach === "dead") return checks;
7134
- checks.push(
7135
- seen.protocolVersion === null ? skip("protocol", "named a protocol version", "it never got as far as saying") : pass("protocol", "named a protocol version", seen.protocolVersion)
7136
- );
7137
- checks.push(
7138
- seen.serverName === null ? skip("identity", "named itself", "it never got as far as saying") : pass(
7139
- "identity",
7140
- "named itself",
7141
- `${seen.serverName}${seen.serverVersion === null ? "" : ` ${seen.serverVersion}`}`
7142
- )
7143
- );
7144
- if (seen.tools === null) {
7145
- checks.push(skip("tools", "listed its tools", REACH_LABEL[seen.reach]));
7146
- return checks;
7147
- }
7148
- checks.push(pass("tools", "listed its tools", `${seen.tools.length} tool(s)`));
7149
- const bare = seen.tools.filter((t) => !t.hasInputSchema).map((t) => t.name);
7150
- checks.push(
7151
- bare.length === 0 ? pass("input-schemas", "every tool declared an input schema") : fail(
7152
- "input-schemas",
7153
- "every tool declared an input schema",
7154
- `${bare.length} without one: ${bare.slice(0, 5).join(", ")}`
7155
- )
7156
- );
7157
- const unnamed = seen.tools.filter((t) => t.description === null).map((t) => t.name);
7158
- checks.push(
7159
- unnamed.length === 0 ? pass("descriptions", "every tool said what it does") : fail(
7160
- "descriptions",
7161
- "every tool said what it does",
7162
- `${unnamed.length} without one: ${unnamed.slice(0, 5).join(", ")}`
7163
- )
7164
- );
7165
- checks.push(pass("digest", "tool list digest", toolsDigest(seen.tools)));
7166
- return checks;
7167
- }
7168
- async function probeMcpEndpoint(options) {
7169
- const seen = await probeMcp(options.url, {
7170
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
7171
- ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
7172
- ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7173
- });
7174
- const checks = checksFor(seen);
7175
- return {
7176
- url: options.url,
7177
- method: "MCP",
7178
- observedAt: seen.observedAt,
7179
- reachable: seen.reach !== "dead",
7180
- httpStatus: seen.httpStatus,
7181
- handshakeMs: seen.handshakeMs,
7182
- termsSource: seen.reach === "payment_required" ? "mcp 402" : "none",
7183
- advertised: [],
7184
- requestHash: sha256(`MCP ${options.url}`),
7185
- responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7186
- checks,
7187
- verdict: checks.some((c) => c.status === "fail") ? "fail" : "pass"
7188
- };
7189
- }
7190
-
7191
- // src/probe.ts
7192
- import { createHash as createHash3 } from "node:crypto";
7193
-
7194
- // ../x402/src/handshake.ts
7195
- var HANDSHAKE_TIMEOUT_MS2 = 3e4;
7196
- var sendsBody = (method) => method !== "GET" && method !== "HEAD";
7197
- function handshakeInit(request2) {
7198
- const carries = sendsBody(request2.method);
7199
- const headers = { accept: "application/json" };
7200
- if (carries) headers["content-type"] = "application/json";
7201
- if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7202
- if (request2.payment) headers[request2.payment.header] = request2.payment.value;
7203
- return {
7204
- method: request2.method,
7205
- headers,
7206
- ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7207
- signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
7208
- };
7209
- }
7210
- async function handshake2(request2, deps = {}) {
7211
- const doFetch = deps.fetchImpl ?? fetch;
7212
- const now = deps.now ?? Date.now;
7213
- const started = now();
7214
- const since = () => Math.max(0, now() - started);
7215
- try {
7216
- const response = await doFetch(request2.url, handshakeInit(request2));
7217
- return { ok: true, response, body: await response.text(), latencyMs: since() };
7218
- } catch (err) {
7219
- return { ok: false, reason: err.message, latencyMs: since() };
7220
- }
7221
- }
7222
-
7223
7137
  // ../x402/src/terms.ts
7224
7138
  var HEADER_NAMES = ["payment-required", "x-payment-required", "www-authenticate-payment"];
7225
7139
  function asTerms(value) {
@@ -7291,24 +7205,24 @@ var ajv = new import_ajv.Ajv({
7291
7205
  validateSchema: true,
7292
7206
  validateFormats: false
7293
7207
  });
7294
- function pass2(id, label, detail) {
7208
+ function pass(id, label, detail) {
7295
7209
  return detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7296
7210
  }
7297
- function fail2(id, label, detail) {
7211
+ function fail(id, label, detail) {
7298
7212
  return { id, label, status: "fail", detail };
7299
7213
  }
7300
7214
  function schemaCheck(id, label, schema) {
7301
7215
  if (schema === null || schema === void 0) {
7302
- return fail2(id, label, "not declared, so no response can ever be checked against it");
7216
+ return fail(id, label, "not declared, so no response can ever be checked against it");
7303
7217
  }
7304
7218
  if (typeof schema !== "object") {
7305
- return fail2(id, label, `declared as ${typeof schema}, not an object`);
7219
+ return fail(id, label, `declared as ${typeof schema}, not an object`);
7306
7220
  }
7307
7221
  try {
7308
7222
  ajv.compile(schema);
7309
- return pass2(id, label);
7223
+ return pass(id, label);
7310
7224
  } catch (err) {
7311
- return fail2(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
7225
+ return fail(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
7312
7226
  }
7313
7227
  }
7314
7228
  function requirementChecks(terms) {
@@ -7316,7 +7230,7 @@ function requirementChecks(terms) {
7316
7230
  const report = (id, label, has) => {
7317
7231
  const gaps = terms.accepts.map((r, i) => has(r) ? -1 : i).filter((i) => i >= 0);
7318
7232
  out.push(
7319
- gaps.length === 0 ? pass2(id, label) : fail2(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7233
+ gaps.length === 0 ? pass(id, label) : fail(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7320
7234
  );
7321
7235
  };
7322
7236
  report("price_declared", "price is stated", (r) => priceOf(r) !== null);
@@ -7333,29 +7247,29 @@ function requirementChecks(terms) {
7333
7247
  return out;
7334
7248
  }
7335
7249
  function runHandshakeChecks(input) {
7336
- const checks = [pass2("reachable", "endpoint answered")];
7250
+ const checks = [pass("reachable", "endpoint answered")];
7337
7251
  checks.push(
7338
- input.status === 402 ? pass2("status_402", "asks for payment") : fail2("status_402", "asks for payment", `returned ${input.status}, not 402`)
7252
+ input.status === 402 ? pass("status_402", "asks for payment") : fail("status_402", "asks for payment", `returned ${input.status}, not 402`)
7339
7253
  );
7340
7254
  if (input.terms === null) {
7341
7255
  checks.push(
7342
- fail2("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7256
+ fail("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7343
7257
  );
7344
7258
  return checks;
7345
7259
  }
7346
- checks.push(pass2("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7260
+ checks.push(pass("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7347
7261
  checks.push(
7348
- input.terms.accepts.length > 0 ? pass2(
7262
+ input.terms.accepts.length > 0 ? pass(
7349
7263
  "accepts_present",
7350
7264
  "offers at least one way to pay",
7351
7265
  `${input.terms.accepts.length} option(s)`
7352
- ) : fail2("accepts_present", "offers at least one way to pay", "accepts is empty")
7266
+ ) : fail("accepts_present", "offers at least one way to pay", "accepts is empty")
7353
7267
  );
7354
7268
  if (input.terms.accepts.length === 0) return checks;
7355
7269
  checks.push(...requirementChecks(input.terms));
7356
7270
  const cache = input.headers.get("cache-control") ?? "";
7357
7271
  checks.push(
7358
- /no-store|no-cache/i.test(cache) ? pass2("not_cacheable", "payment terms are not cached") : fail2(
7272
+ /no-store|no-cache/i.test(cache) ? pass("not_cacheable", "payment terms are not cached") : fail(
7359
7273
  "not_cacheable",
7360
7274
  "payment terms are not cached",
7361
7275
  cache === "" ? "no cache-control, so a proxy may serve stale terms" : `cache-control: ${cache}`
@@ -7364,12 +7278,165 @@ function runHandshakeChecks(input) {
7364
7278
  return checks;
7365
7279
  }
7366
7280
  function unreachable(reason) {
7367
- return [fail2("reachable", "endpoint answered", reason)];
7281
+ return [fail("reachable", "endpoint answered", reason)];
7368
7282
  }
7369
- function verdictOf(checks) {
7283
+ function verdictOf(checks, defects) {
7284
+ if (defects.length > 0) return "fail";
7370
7285
  return checks.some((c) => c.status === "fail") ? "fail" : "pass";
7371
7286
  }
7372
7287
 
7288
+ // src/defects.ts
7289
+ var PLACEHOLDER = [
7290
+ /[?&][^=&]+=(?:string|integer|number|boolean|array|object)(?:&|$)/,
7291
+ /=0x([0-9a-fA-F])\1{39}(?:&|$)/,
7292
+ /\/:[A-Za-z_][A-Za-z0-9_]*(?:\/|[?]|$)/,
7293
+ /\/[{][A-Za-z_][A-Za-z0-9_]*[}](?:\/|[?]|$)/,
7294
+ // An em dash and an e.g. belong to a sentence, and a value is not one.
7295
+ /[?&][^=&]+=[^&]*(?:%E2%80%94|—)/i,
7296
+ /[?&][^=&]+=[^&]*e\.g\./i,
7297
+ // Anchored at the start of the value, since a real one can mention these words later.
7298
+ /[?&][^=&]+=(?:required|optional)(?:[^a-z]|$)/i,
7299
+ // A value that spells out its own parameter name is describing it, not answering it.
7300
+ /[?&]([a-z_]{3,})=[^&]*\1[^&]*(?:\+|%20)/i
7301
+ ];
7302
+ function urlHasPlaceholder(url) {
7303
+ return PLACEHOLDER.some((marker) => marker.test(url));
7304
+ }
7305
+ var PROSE_SAYS = "a query value spells out what to put there instead of putting it";
7306
+ var GONE = /* @__PURE__ */ new Set([404, 410]);
7307
+ function pathNotFoundSays(httpStatus) {
7308
+ return `the listed path answered ${httpStatus}`;
7309
+ }
7310
+ function defectsOf(url, httpStatus) {
7311
+ const found = [];
7312
+ if (urlHasPlaceholder(url)) found.push({ defect: "prose_in_the_url", says: PROSE_SAYS });
7313
+ if (httpStatus !== null && GONE.has(httpStatus)) {
7314
+ found.push({ defect: "path_not_found", says: pathNotFoundSays(httpStatus) });
7315
+ }
7316
+ return found;
7317
+ }
7318
+
7319
+ // src/mcp.ts
7320
+ function sha256(input) {
7321
+ return `sha256:${createHash2("sha256").update(input).digest("hex")}`;
7322
+ }
7323
+ var pass2 = (id, label, detail) => detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7324
+ var fail2 = (id, label, detail) => ({
7325
+ id,
7326
+ label,
7327
+ status: "fail",
7328
+ detail
7329
+ });
7330
+ var skip = (id, label, detail) => ({
7331
+ id,
7332
+ label,
7333
+ status: "skip",
7334
+ detail
7335
+ });
7336
+ var REACH_LABEL = {
7337
+ readable: "listed its tools",
7338
+ auth_required: "asked for credentials",
7339
+ payment_required: "asked for payment",
7340
+ dead: "did not answer",
7341
+ error: "answered with something else"
7342
+ };
7343
+ function checksFor(seen) {
7344
+ const checks = [];
7345
+ checks.push(
7346
+ seen.reach === "dead" ? fail2("reachable", "server answered", seen.note ?? "no answer") : pass2("reachable", "server answered", REACH_LABEL[seen.reach])
7347
+ );
7348
+ if (seen.reach === "dead") return checks;
7349
+ checks.push(
7350
+ seen.protocolVersion === null ? skip("protocol", "named a protocol version", "it never got as far as saying") : pass2("protocol", "named a protocol version", seen.protocolVersion)
7351
+ );
7352
+ checks.push(
7353
+ seen.serverName === null ? skip("identity", "named itself", "it never got as far as saying") : pass2(
7354
+ "identity",
7355
+ "named itself",
7356
+ `${seen.serverName}${seen.serverVersion === null ? "" : ` ${seen.serverVersion}`}`
7357
+ )
7358
+ );
7359
+ if (seen.tools === null) {
7360
+ checks.push(skip("tools", "listed its tools", REACH_LABEL[seen.reach]));
7361
+ return checks;
7362
+ }
7363
+ checks.push(pass2("tools", "listed its tools", `${seen.tools.length} tool(s)`));
7364
+ const bare = seen.tools.filter((t) => t.inputSchema === null).map((t) => t.name);
7365
+ checks.push(
7366
+ bare.length === 0 ? pass2("input-schemas", "every tool declared an input schema") : fail2(
7367
+ "input-schemas",
7368
+ "every tool declared an input schema",
7369
+ `${bare.length} without one: ${bare.slice(0, 5).join(", ")}`
7370
+ )
7371
+ );
7372
+ const unnamed = seen.tools.filter((t) => t.description === null).map((t) => t.name);
7373
+ checks.push(
7374
+ unnamed.length === 0 ? pass2("descriptions", "every tool said what it does") : fail2(
7375
+ "descriptions",
7376
+ "every tool said what it does",
7377
+ `${unnamed.length} without one: ${unnamed.slice(0, 5).join(", ")}`
7378
+ )
7379
+ );
7380
+ checks.push(pass2("digest", "tool list digest", toolsDigest(seen.tools)));
7381
+ return checks;
7382
+ }
7383
+ async function probeMcpEndpoint(options) {
7384
+ const seen = await probeMcp(options.url, {
7385
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
7386
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
7387
+ ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7388
+ });
7389
+ const checks = checksFor(seen);
7390
+ const defects = defectsOf(options.url, seen.httpStatus);
7391
+ return {
7392
+ url: options.url,
7393
+ method: "MCP",
7394
+ observedAt: seen.observedAt,
7395
+ reachable: seen.reach !== "dead",
7396
+ httpStatus: seen.httpStatus,
7397
+ handshakeMs: seen.handshakeMs,
7398
+ termsSource: seen.reach === "payment_required" ? "mcp 402" : "none",
7399
+ advertised: [],
7400
+ requestHash: sha256(`MCP ${options.url}`),
7401
+ responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7402
+ checks,
7403
+ defects,
7404
+ verdict: verdictOf(checks, defects)
7405
+ };
7406
+ }
7407
+
7408
+ // src/probe.ts
7409
+ import { createHash as createHash3 } from "node:crypto";
7410
+
7411
+ // ../x402/src/handshake.ts
7412
+ var HANDSHAKE_TIMEOUT_MS2 = 3e4;
7413
+ var sendsBody = (method) => method !== "GET" && method !== "HEAD";
7414
+ function handshakeInit(request2) {
7415
+ const carries = sendsBody(request2.method);
7416
+ const headers = { accept: "application/json" };
7417
+ if (carries) headers["content-type"] = "application/json";
7418
+ if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7419
+ if (request2.payment) headers[request2.payment.header] = request2.payment.value;
7420
+ return {
7421
+ method: request2.method,
7422
+ headers,
7423
+ ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7424
+ signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
7425
+ };
7426
+ }
7427
+ async function handshake2(request2, deps = {}) {
7428
+ const doFetch = deps.fetchImpl ?? fetch;
7429
+ const now = deps.now ?? Date.now;
7430
+ const started = now();
7431
+ const since = () => Math.max(0, now() - started);
7432
+ try {
7433
+ const response = await doFetch(request2.url, handshakeInit(request2));
7434
+ return { ok: true, response, body: await response.text(), latencyMs: since() };
7435
+ } catch (err) {
7436
+ return { ok: false, reason: err.message, latencyMs: since() };
7437
+ }
7438
+ }
7439
+
7373
7440
  // src/probe.ts
7374
7441
  function sha2562(input) {
7375
7442
  return `sha256:${createHash3("sha256").update(input).digest("hex")}`;
@@ -7416,6 +7483,7 @@ ${body}`);
7416
7483
  handshakeMs: answer.latencyMs,
7417
7484
  responseHash: null,
7418
7485
  checks: unreachable(answer.reason),
7486
+ defects: defectsOf(options.url, null),
7419
7487
  verdict: "fail"
7420
7488
  };
7421
7489
  }
@@ -7429,6 +7497,7 @@ ${body}`);
7429
7497
  parseError: parsed.parseError,
7430
7498
  source: parsed.source
7431
7499
  });
7500
+ const defects = defectsOf(options.url, response.status);
7432
7501
  return {
7433
7502
  ...base,
7434
7503
  reachable: true,
@@ -7438,9 +7507,23 @@ ${body}`);
7438
7507
  advertised: advertisedFrom(parsed.terms),
7439
7508
  responseHash: sha2562(text2),
7440
7509
  checks,
7441
- verdict: verdictOf(checks)
7510
+ defects,
7511
+ verdict: verdictOf(checks, defects)
7442
7512
  };
7443
7513
  }
7514
+ function askedForPayment(result) {
7515
+ return result.checks.some((c) => c.id === "status_402" && c.status === "pass");
7516
+ }
7517
+ async function probeEitherVerb(run, explicit) {
7518
+ const verbs = explicit === void 0 ? ["GET", "POST"] : [explicit.toUpperCase()];
7519
+ let first;
7520
+ for (const method of verbs) {
7521
+ const attempt = await run(method);
7522
+ if (askedForPayment(attempt)) return attempt;
7523
+ first ??= attempt;
7524
+ }
7525
+ return first;
7526
+ }
7444
7527
 
7445
7528
  // src/report.ts
7446
7529
  var ANSI = {
@@ -7478,6 +7561,13 @@ function formatText(result, colour) {
7478
7561
  lines.push(` ${check.label.padEnd(width)}${mark}`);
7479
7562
  if (check.detail) lines.push(` ${paint("dim", check.detail, colour)}`);
7480
7563
  }
7564
+ if (result.defects.length > 0) {
7565
+ lines.push("");
7566
+ for (const found of result.defects) {
7567
+ lines.push(` ${found.defect.padEnd(width)}${paint("red", "defect", colour)}`);
7568
+ lines.push(` ${paint("dim", found.says, colour)}`);
7569
+ }
7570
+ }
7481
7571
  if (result.advertised.length > 0) {
7482
7572
  lines.push("");
7483
7573
  lines.push(` ${paint("dim", "advertised", colour)}`);
@@ -7490,9 +7580,9 @@ function formatText(result, colour) {
7490
7580
  if (result.responseHash)
7491
7581
  lines.push(` ${paint("dim", "response ", colour)}${result.responseHash}`);
7492
7582
  lines.push("");
7493
- const failed = result.checks.filter((c) => c.status === "fail").length;
7583
+ const failed = result.checks.filter((c) => c.status === "fail").length + result.defects.length;
7494
7584
  lines.push(
7495
- result.verdict === "pass" ? ` ${paint("green", "every handshake check passed", colour)}` : ` ${paint("red", `${failed} check${failed === 1 ? "" : "s"} failed`, colour)}`
7585
+ result.verdict === "pass" ? ` ${paint("green", "every handshake check passed", colour)}` : ` ${paint("red", `${failed} problem${failed === 1 ? "" : "s"} found`, colour)}`
7496
7586
  );
7497
7587
  lines.push(` ${paint("dim", "this is the unpaid half. correctness needs a purchase.", colour)}`);
7498
7588
  lines.push("");
@@ -7506,17 +7596,18 @@ function formatJson(result) {
7506
7596
  var USAGE = `
7507
7597
  teppi-check <url> [options]
7508
7598
 
7509
- Sends one unpaid request and reports what the endpoint advertises, in the
7510
- same form the public record uses.
7599
+ Sends one unpaid request and reports what the endpoint advertises, plus
7600
+ anything wrong with the listing itself, in the form the public record uses.
7511
7601
 
7512
7602
  --mcp handshake a remote mcp server instead of an x402 endpoint
7513
- --method <verb> default POST
7603
+ --method <verb> tries get then post when not given
7514
7604
  --body <json|@file> default {}
7515
7605
  --json machine readable output
7516
7606
  --timeout <ms> default 15000
7517
7607
  --user-agent <ua> default node
7518
7608
 
7519
- Exit codes: 0 every check passed, 1 a check failed, 2 could not be reached.
7609
+ Exit codes: 0 nothing to report, 1 a check failed or a defect was found,
7610
+ 2 could not be reached.
7520
7611
  `;
7521
7612
  function refuseUnlessUsable(url) {
7522
7613
  let parsed;
@@ -7559,17 +7650,18 @@ async function main() {
7559
7650
  ...values["user-agent"] ? { userAgent: values["user-agent"] } : {}
7560
7651
  };
7561
7652
  const rawBody = values.body ?? "{}";
7562
- const result = values.mcp ? await probeMcpEndpoint(shared) : await probe({
7563
- ...shared,
7564
- method: values.method?.toUpperCase() ?? "POST",
7565
- body: rawBody.startsWith("@") ? readFileSync(rawBody.slice(1), "utf8") : rawBody
7566
- });
7653
+ const body = rawBody.startsWith("@") ? readFileSync(rawBody.slice(1), "utf8") : rawBody;
7654
+ const found = await probeEitherVerb(
7655
+ (method) => probe({ ...shared, method, body }),
7656
+ values.method
7657
+ );
7658
+ const out = values.mcp ? await probeMcpEndpoint(shared) : found;
7567
7659
  stdout.write(
7568
- values.json ? `${formatJson(result)}
7569
- ` : formatText(result, shouldColour(env, stdout.isTTY === true))
7660
+ values.json ? `${formatJson(out)}
7661
+ ` : formatText(out, shouldColour(env, stdout.isTTY === true))
7570
7662
  );
7571
- if (!result.reachable) exit(2);
7572
- exit(result.verdict === "pass" ? 0 : 1);
7663
+ if (!out.reachable) exit(2);
7664
+ exit(out.verdict === "pass" ? 0 : 1);
7573
7665
  }
7574
7666
  main().catch((err) => {
7575
7667
  stdout.write(`${err.message}
@@ -0,0 +1,9 @@
1
+ export type Defect = {
2
+ readonly defect: string;
3
+ readonly says: string;
4
+ };
5
+ export declare function urlHasPlaceholder(url: string): boolean;
6
+ export declare const PROSE_SAYS = "a query value spells out what to put there instead of putting it";
7
+ export declare function pathNotFoundSays(httpStatus: number): string;
8
+ export declare function defectsOf(url: string, httpStatus: number | null): Defect[];
9
+ //# sourceMappingURL=defects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defects.d.ts","sourceRoot":"","sources":["../src/defects.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,CAAC;AAkBF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,eAAO,MAAM,UAAU,qEAAqE,CAAC;AAK7F,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAO1E"}
package/npm/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { type PaymentRequirement, type PaymentTerms, parseTerms, priceOf, schemasOf, } from './terms.js';
2
2
  export { type Check, type CheckStatus, runHandshakeChecks, schemaCheck, verdictOf, } from './checks.js';
3
+ export { type Defect, defectsOf, PROSE_SAYS, pathNotFoundSays, urlHasPlaceholder, } from './defects.js';
3
4
  export { checksFor, type McpProbeOptions, probeMcpEndpoint, REACH_LABEL, } from './mcp.js';
4
5
  export { DEFAULT_USER_AGENT, type ProbeOptions, type ProbeResult, probe } from './probe.js';
5
6
  export { formatJson, formatText, shouldColour } from './report.js';
@@ -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,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"}
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,KAAK,MAAM,EACX,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,cAAc,CAAC;AACtB,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
@@ -7011,10 +7011,42 @@ function runHandshakeChecks(input) {
7011
7011
  function unreachable(reason) {
7012
7012
  return [fail("reachable", "endpoint answered", reason)];
7013
7013
  }
7014
- function verdictOf(checks) {
7014
+ function verdictOf(checks, defects) {
7015
+ if (defects.length > 0) return "fail";
7015
7016
  return checks.some((c) => c.status === "fail") ? "fail" : "pass";
7016
7017
  }
7017
7018
 
7019
+ // src/defects.ts
7020
+ var PLACEHOLDER = [
7021
+ /[?&][^=&]+=(?:string|integer|number|boolean|array|object)(?:&|$)/,
7022
+ /=0x([0-9a-fA-F])\1{39}(?:&|$)/,
7023
+ /\/:[A-Za-z_][A-Za-z0-9_]*(?:\/|[?]|$)/,
7024
+ /\/[{][A-Za-z_][A-Za-z0-9_]*[}](?:\/|[?]|$)/,
7025
+ // An em dash and an e.g. belong to a sentence, and a value is not one.
7026
+ /[?&][^=&]+=[^&]*(?:%E2%80%94|—)/i,
7027
+ /[?&][^=&]+=[^&]*e\.g\./i,
7028
+ // Anchored at the start of the value, since a real one can mention these words later.
7029
+ /[?&][^=&]+=(?:required|optional)(?:[^a-z]|$)/i,
7030
+ // A value that spells out its own parameter name is describing it, not answering it.
7031
+ /[?&]([a-z_]{3,})=[^&]*\1[^&]*(?:\+|%20)/i
7032
+ ];
7033
+ function urlHasPlaceholder(url) {
7034
+ return PLACEHOLDER.some((marker) => marker.test(url));
7035
+ }
7036
+ var PROSE_SAYS = "a query value spells out what to put there instead of putting it";
7037
+ var GONE = /* @__PURE__ */ new Set([404, 410]);
7038
+ function pathNotFoundSays(httpStatus) {
7039
+ return `the listed path answered ${httpStatus}`;
7040
+ }
7041
+ function defectsOf(url, httpStatus) {
7042
+ const found = [];
7043
+ if (urlHasPlaceholder(url)) found.push({ defect: "prose_in_the_url", says: PROSE_SAYS });
7044
+ if (httpStatus !== null && GONE.has(httpStatus)) {
7045
+ found.push({ defect: "path_not_found", says: pathNotFoundSays(httpStatus) });
7046
+ }
7047
+ return found;
7048
+ }
7049
+
7018
7050
  // src/mcp.ts
7019
7051
  import { createHash as createHash2 } from "node:crypto";
7020
7052
 
@@ -7099,7 +7131,8 @@ function toolsFrom(result) {
7099
7131
  {
7100
7132
  name,
7101
7133
  description: text(tool.description),
7102
- hasInputSchema: typeof tool.inputSchema === "object" && tool.inputSchema !== null
7134
+ inputSchema: tool.inputSchema ?? null,
7135
+ outputSchema: tool.outputSchema ?? null
7103
7136
  }
7104
7137
  ];
7105
7138
  });
@@ -7125,6 +7158,8 @@ async function turn(response, method) {
7125
7158
  };
7126
7159
  }
7127
7160
  var DEADLINE_MS = 45e3;
7161
+ var MAX_PAGES = 20;
7162
+ var MAX_TOOLS = 2e3;
7128
7163
  async function probeMcp(url, options = {}) {
7129
7164
  const clockFor = options.now ?? Date.now;
7130
7165
  const startedFor = clockFor();
@@ -7140,7 +7175,10 @@ async function probeMcp(url, options = {}) {
7140
7175
  protocolVersion: null,
7141
7176
  serverName: null,
7142
7177
  serverVersion: null,
7178
+ instructions: null,
7143
7179
  tools: null,
7180
+ pages: null,
7181
+ truncated: false,
7144
7182
  note: "never answered and never gave up"
7145
7183
  }),
7146
7184
  options.deadlineMs ?? DEADLINE_MS
@@ -7152,6 +7190,36 @@ async function probeMcp(url, options = {}) {
7152
7190
  clearTimeout(timer);
7153
7191
  }
7154
7192
  }
7193
+ async function walkTools(send, session) {
7194
+ const tools = [];
7195
+ let cursor = null;
7196
+ let pages = 0;
7197
+ let status = 200;
7198
+ for (; ; ) {
7199
+ const asked = request(2 + pages, "tools/list", cursor === null ? void 0 : { cursor });
7200
+ const listed = await turn(await send(asked, session), "tools/list");
7201
+ if (listed.kind === "refused") {
7202
+ return {
7203
+ kind: "refused",
7204
+ reach: listed.reach,
7205
+ status: listed.status,
7206
+ note: "it opened but would not list"
7207
+ };
7208
+ }
7209
+ if (listed.kind === "broken")
7210
+ return { kind: "broken", status: listed.status, note: listed.note };
7211
+ tools.push(...toolsFrom(listed.value));
7212
+ status = listed.status;
7213
+ pages += 1;
7214
+ const next = text(listed.value.nextCursor);
7215
+ if (next === null || next === cursor)
7216
+ return { kind: "listed", status, tools, pages, truncated: false };
7217
+ if (pages >= MAX_PAGES || tools.length >= MAX_TOOLS) {
7218
+ return { kind: "listed", status, tools, pages, truncated: true };
7219
+ }
7220
+ cursor = next;
7221
+ }
7222
+ }
7155
7223
  async function handshake(url, options) {
7156
7224
  const doFetch = options.fetchImpl ?? fetch;
7157
7225
  const clock = options.now ?? Date.now;
@@ -7163,7 +7231,10 @@ async function handshake(url, options) {
7163
7231
  protocolVersion: null,
7164
7232
  serverName: null,
7165
7233
  serverVersion: null,
7166
- tools: null
7234
+ instructions: null,
7235
+ tools: null,
7236
+ pages: null,
7237
+ truncated: false
7167
7238
  };
7168
7239
  const send = (body, sessionId) => doFetch(url, {
7169
7240
  ...initFor(body, sessionId, userAgent),
@@ -7197,34 +7268,28 @@ async function handshake(url, options) {
7197
7268
  ...base,
7198
7269
  protocolVersion: text(hello.value.protocolVersion),
7199
7270
  serverName: text(info.name),
7200
- serverVersion: text(info.version)
7271
+ serverVersion: text(info.version),
7272
+ instructions: text(hello.value.instructions)
7201
7273
  };
7202
7274
  await send(notification("notifications/initialized"), session).catch(() => void 0);
7203
- const listed = await turn(await send(request(2, "tools/list"), session), "tools/list");
7204
- if (listed.kind === "refused") {
7205
- return {
7206
- ...said,
7207
- reach: listed.reach,
7208
- httpStatus: listed.status,
7209
- handshakeMs: took(),
7210
- note: "it opened but would not list"
7211
- };
7212
- }
7213
- if (listed.kind === "broken") {
7275
+ const walked = await walkTools(send, session);
7276
+ if (walked.kind !== "listed") {
7214
7277
  return {
7215
7278
  ...said,
7216
- reach: "error",
7217
- httpStatus: listed.status,
7279
+ reach: walked.kind === "refused" ? walked.reach : "error",
7280
+ httpStatus: walked.status,
7218
7281
  handshakeMs: took(),
7219
- note: listed.note
7282
+ note: walked.note
7220
7283
  };
7221
7284
  }
7222
7285
  return {
7223
7286
  ...said,
7224
7287
  reach: "readable",
7225
- httpStatus: listed.status,
7288
+ httpStatus: walked.status,
7226
7289
  handshakeMs: took(),
7227
- tools: toolsFrom(listed.value),
7290
+ tools: walked.tools,
7291
+ pages: walked.pages,
7292
+ truncated: walked.truncated,
7228
7293
  note: null
7229
7294
  };
7230
7295
  } catch (err) {
@@ -7240,7 +7305,7 @@ async function handshake(url, options) {
7240
7305
  }
7241
7306
  function toolsDigest(tools) {
7242
7307
  const canonical = JSON.stringify(
7243
- [...tools].map((t) => [t.name, t.description ?? "", t.hasInputSchema]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
7308
+ [...tools].map((t) => [t.name, t.description ?? "", t.inputSchema !== null]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
7244
7309
  );
7245
7310
  return createHash("sha256").update(canonical).digest("hex");
7246
7311
  }
@@ -7290,7 +7355,7 @@ function checksFor(seen) {
7290
7355
  return checks;
7291
7356
  }
7292
7357
  checks.push(pass2("tools", "listed its tools", `${seen.tools.length} tool(s)`));
7293
- const bare = seen.tools.filter((t) => !t.hasInputSchema).map((t) => t.name);
7358
+ const bare = seen.tools.filter((t) => t.inputSchema === null).map((t) => t.name);
7294
7359
  checks.push(
7295
7360
  bare.length === 0 ? pass2("input-schemas", "every tool declared an input schema") : fail2(
7296
7361
  "input-schemas",
@@ -7316,6 +7381,7 @@ async function probeMcpEndpoint(options) {
7316
7381
  ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7317
7382
  });
7318
7383
  const checks = checksFor(seen);
7384
+ const defects = defectsOf(options.url, seen.httpStatus);
7319
7385
  return {
7320
7386
  url: options.url,
7321
7387
  method: "MCP",
@@ -7328,7 +7394,8 @@ async function probeMcpEndpoint(options) {
7328
7394
  requestHash: sha256(`MCP ${options.url}`),
7329
7395
  responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7330
7396
  checks,
7331
- verdict: checks.some((c) => c.status === "fail") ? "fail" : "pass"
7397
+ defects,
7398
+ verdict: verdictOf(checks, defects)
7332
7399
  };
7333
7400
  }
7334
7401
 
@@ -7411,6 +7478,7 @@ ${body}`);
7411
7478
  handshakeMs: answer.latencyMs,
7412
7479
  responseHash: null,
7413
7480
  checks: unreachable(answer.reason),
7481
+ defects: defectsOf(options.url, null),
7414
7482
  verdict: "fail"
7415
7483
  };
7416
7484
  }
@@ -7424,6 +7492,7 @@ ${body}`);
7424
7492
  parseError: parsed.parseError,
7425
7493
  source: parsed.source
7426
7494
  });
7495
+ const defects = defectsOf(options.url, response.status);
7427
7496
  return {
7428
7497
  ...base,
7429
7498
  reachable: true,
@@ -7433,7 +7502,8 @@ ${body}`);
7433
7502
  advertised: advertisedFrom(parsed.terms),
7434
7503
  responseHash: sha2562(text2),
7435
7504
  checks,
7436
- verdict: verdictOf(checks)
7505
+ defects,
7506
+ verdict: verdictOf(checks, defects)
7437
7507
  };
7438
7508
  }
7439
7509
 
@@ -7473,6 +7543,13 @@ function formatText(result, colour) {
7473
7543
  lines.push(` ${check.label.padEnd(width)}${mark}`);
7474
7544
  if (check.detail) lines.push(` ${paint("dim", check.detail, colour)}`);
7475
7545
  }
7546
+ if (result.defects.length > 0) {
7547
+ lines.push("");
7548
+ for (const found of result.defects) {
7549
+ lines.push(` ${found.defect.padEnd(width)}${paint("red", "defect", colour)}`);
7550
+ lines.push(` ${paint("dim", found.says, colour)}`);
7551
+ }
7552
+ }
7476
7553
  if (result.advertised.length > 0) {
7477
7554
  lines.push("");
7478
7555
  lines.push(` ${paint("dim", "advertised", colour)}`);
@@ -7485,9 +7562,9 @@ function formatText(result, colour) {
7485
7562
  if (result.responseHash)
7486
7563
  lines.push(` ${paint("dim", "response ", colour)}${result.responseHash}`);
7487
7564
  lines.push("");
7488
- const failed = result.checks.filter((c) => c.status === "fail").length;
7565
+ const failed = result.checks.filter((c) => c.status === "fail").length + result.defects.length;
7489
7566
  lines.push(
7490
- result.verdict === "pass" ? ` ${paint("green", "every handshake check passed", colour)}` : ` ${paint("red", `${failed} check${failed === 1 ? "" : "s"} failed`, colour)}`
7567
+ result.verdict === "pass" ? ` ${paint("green", "every handshake check passed", colour)}` : ` ${paint("red", `${failed} problem${failed === 1 ? "" : "s"} found`, colour)}`
7491
7568
  );
7492
7569
  lines.push(` ${paint("dim", "this is the unpaid half. correctness needs a purchase.", colour)}`);
7493
7570
  lines.push("");
@@ -7498,11 +7575,14 @@ function formatJson(result) {
7498
7575
  }
7499
7576
  export {
7500
7577
  DEFAULT_USER_AGENT,
7578
+ PROSE_SAYS,
7501
7579
  REACH_LABEL,
7502
7580
  checksFor,
7581
+ defectsOf,
7503
7582
  formatJson,
7504
7583
  formatText,
7505
7584
  parseTerms,
7585
+ pathNotFoundSays,
7506
7586
  priceOf,
7507
7587
  probe,
7508
7588
  probeMcpEndpoint,
@@ -7510,5 +7590,6 @@ export {
7510
7590
  schemaCheck,
7511
7591
  schemasOf,
7512
7592
  shouldColour,
7593
+ urlHasPlaceholder,
7513
7594
  verdictOf
7514
7595
  };
@@ -1,2 +1,2 @@
1
1
  export { JSON_RPC, lastEventData, notification, PREFERRED_VERSION, PROTOCOL_VERSIONS, type RpcResult, readAnswer, request, } from './frame.js';
2
- export { CLIENT, HANDSHAKE_TIMEOUT_MS, INITIALIZE, initFor, type McpObservation, type ProbeOptions, probeMcp, type Reach, reachFor, type Tool, toolsDigest, toolsFrom, } from './probe.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';
@@ -7,7 +7,8 @@ export type Reach = 'readable' | 'auth_required' | 'payment_required' | 'dead' |
7
7
  export type Tool = {
8
8
  readonly name: string;
9
9
  readonly description: string | null;
10
- readonly hasInputSchema: boolean;
10
+ readonly inputSchema: unknown;
11
+ readonly outputSchema: unknown;
11
12
  };
12
13
  export type McpObservation = {
13
14
  readonly url: string;
@@ -18,7 +19,10 @@ export type McpObservation = {
18
19
  readonly protocolVersion: string | null;
19
20
  readonly serverName: string | null;
20
21
  readonly serverVersion: string | null;
22
+ readonly instructions: string | null;
21
23
  readonly tools: readonly Tool[] | null;
24
+ readonly pages: number | null;
25
+ readonly truncated: boolean;
22
26
  readonly note: string | null;
23
27
  };
24
28
  export type ProbeOptions = {
@@ -40,5 +44,9 @@ export declare const INITIALIZE: {
40
44
  export declare function toolsFrom(result: Record<string, unknown>): Tool[];
41
45
  export declare function reachFor(status: number): Reach | null;
42
46
  export declare const DEADLINE_MS = 45000;
47
+ export declare const MAX_PAGES = 20;
48
+ export declare const MAX_TOOLS = 2000;
43
49
  export declare function probeMcp(url: string, options?: ProbeOptions): Promise<McpObservation>;
44
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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type McpObservation } from './mcp-probe/index.js';
2
- import type { Check } from './checks.js';
2
+ import { type Check } from './checks.js';
3
3
  import type { ProbeResult } from './probe.js';
4
4
  export type McpProbeOptions = {
5
5
  readonly url: string;
package/npm/mcp.d.ts.map CHANGED
@@ -1 +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"}
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,EAAE,KAAK,KAAK,EAAa,MAAM,aAAa,CAAC;AAEpD,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,CAwBrF"}
package/npm/probe.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Check } from './checks.js';
2
+ import { type Defect } from './defects.js';
2
3
  export declare const DEFAULT_USER_AGENT = "node";
3
4
  export type ProbeOptions = {
4
5
  readonly url: string;
@@ -25,7 +26,10 @@ export type ProbeResult = {
25
26
  readonly requestHash: string;
26
27
  readonly responseHash: string | null;
27
28
  readonly checks: readonly Check[];
29
+ readonly defects: readonly Defect[];
28
30
  readonly verdict: 'pass' | 'fail';
29
31
  };
30
32
  export declare function probe(options: ProbeOptions): Promise<ProbeResult>;
33
+ export declare function askedForPayment(result: ProbeResult): boolean;
34
+ export declare function probeEitherVerb(run: (method: string) => Promise<ProbeResult>, explicit?: string): Promise<ProbeResult>;
31
35
  //# 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;AACrF,OAAO,EAAE,KAAK,MAAM,EAAa,MAAM,cAAc,CAAC;AAGtD,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,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAmEvE;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"}
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAI7F;AASD,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CA6CvE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEtD"}
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAI7F;AASD,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAsDvE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEtD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teppi-check",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",