teppi-check 0.1.0 → 0.2.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
@@ -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,349 @@ 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
+ hasInputSchema: typeof tool.inputSchema === "object" && tool.inputSchema !== null
6959
+ }
6960
+ ];
6961
+ });
6962
+ }
6963
+ function reachFor(status) {
6964
+ if (status === 401 || status === 403) return "auth_required";
6965
+ if (status === 402) return "payment_required";
6966
+ if (status >= 500 || status === 404 || status === 410) return "dead";
6967
+ if (status >= 400) return "error";
6968
+ return null;
6969
+ }
6970
+ async function turn(response, method) {
6971
+ const refused = reachFor(response.status);
6972
+ if (refused !== null) return { kind: "refused", reach: refused, status: response.status };
6973
+ const answer = readAnswer(response.headers.get("content-type"), await response.text());
6974
+ if (answer.kind === "result") {
6975
+ return { kind: "read", status: response.status, value: answer.value };
6976
+ }
6977
+ return {
6978
+ kind: "broken",
6979
+ status: response.status,
6980
+ note: answer.kind === "error" ? `${method}: ${answer.message}` : answer.why
6981
+ };
6982
+ }
6983
+ var DEADLINE_MS = 45e3;
6984
+ async function probeMcp(url, options = {}) {
6985
+ const clockFor = options.now ?? Date.now;
6986
+ const startedFor = clockFor();
6987
+ let timer;
6988
+ const gaveUp = new Promise((resolve) => {
6989
+ timer = setTimeout(
6990
+ () => resolve({
6991
+ url,
6992
+ observedAt: new Date(startedFor).toISOString(),
6993
+ reach: "dead",
6994
+ httpStatus: null,
6995
+ handshakeMs: options.deadlineMs ?? DEADLINE_MS,
6996
+ protocolVersion: null,
6997
+ serverName: null,
6998
+ serverVersion: null,
6999
+ tools: null,
7000
+ note: "never answered and never gave up"
7001
+ }),
7002
+ options.deadlineMs ?? DEADLINE_MS
7003
+ );
7004
+ });
7005
+ try {
7006
+ return await Promise.race([handshake(url, options), gaveUp]);
7007
+ } finally {
7008
+ clearTimeout(timer);
7009
+ }
7010
+ }
7011
+ async function handshake(url, options) {
7012
+ const doFetch = options.fetchImpl ?? fetch;
7013
+ const clock = options.now ?? Date.now;
7014
+ const userAgent = options.userAgent ?? `${CLIENT.name}/${CLIENT.version}`;
7015
+ const startedAt = clock();
7016
+ const base = {
7017
+ url,
7018
+ observedAt: new Date(startedAt).toISOString(),
7019
+ protocolVersion: null,
7020
+ serverName: null,
7021
+ serverVersion: null,
7022
+ tools: null
7023
+ };
7024
+ const send = (body, sessionId) => doFetch(url, {
7025
+ ...initFor(body, sessionId, userAgent),
7026
+ signal: AbortSignal.timeout(options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7027
+ });
7028
+ const took = () => Math.max(0, clock() - startedAt);
7029
+ try {
7030
+ const opened = await send(request(1, "initialize", INITIALIZE), null);
7031
+ const hello = await turn(opened, "initialize");
7032
+ if (hello.kind === "refused") {
7033
+ return {
7034
+ ...base,
7035
+ reach: hello.reach,
7036
+ httpStatus: hello.status,
7037
+ handshakeMs: took(),
7038
+ note: null
7039
+ };
7040
+ }
7041
+ if (hello.kind === "broken") {
7042
+ return {
7043
+ ...base,
7044
+ reach: "error",
7045
+ httpStatus: hello.status,
7046
+ handshakeMs: took(),
7047
+ note: hello.note
7048
+ };
7049
+ }
7050
+ const info = hello.value.serverInfo ?? {};
7051
+ const session = opened.headers.get("mcp-session-id");
7052
+ const said = {
7053
+ ...base,
7054
+ protocolVersion: text(hello.value.protocolVersion),
7055
+ serverName: text(info.name),
7056
+ serverVersion: text(info.version)
7057
+ };
7058
+ 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") {
7061
+ return {
7062
+ ...said,
7063
+ reach: listed.reach,
7064
+ httpStatus: listed.status,
7065
+ 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
7076
+ };
7077
+ }
7078
+ return {
7079
+ ...said,
7080
+ reach: "readable",
7081
+ httpStatus: listed.status,
7082
+ handshakeMs: took(),
7083
+ tools: toolsFrom(listed.value),
7084
+ note: null
7085
+ };
7086
+ } catch (err) {
7087
+ const name = err.name;
7088
+ return {
7089
+ ...base,
7090
+ reach: "dead",
7091
+ httpStatus: null,
7092
+ handshakeMs: took(),
7093
+ note: name === "TimeoutError" || name === "AbortError" ? "timed out" : "unreachable"
7094
+ };
7095
+ }
7096
+ }
7097
+ function toolsDigest(tools) {
7098
+ 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)
7100
+ );
7101
+ return createHash("sha256").update(canonical).digest("hex");
7102
+ }
7103
+
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";
6876
7193
 
6877
7194
  // ../x402/src/handshake.ts
6878
- var HANDSHAKE_TIMEOUT_MS = 3e4;
7195
+ var HANDSHAKE_TIMEOUT_MS2 = 3e4;
6879
7196
  var sendsBody = (method) => method !== "GET" && method !== "HEAD";
6880
- function handshakeInit(request) {
6881
- const carries = sendsBody(request.method);
7197
+ function handshakeInit(request2) {
7198
+ const carries = sendsBody(request2.method);
6882
7199
  const headers = { accept: "application/json" };
6883
7200
  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;
7201
+ if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7202
+ if (request2.payment) headers[request2.payment.header] = request2.payment.value;
6886
7203
  return {
6887
- method: request.method,
7204
+ method: request2.method,
6888
7205
  headers,
6889
- ...carries ? { body: JSON.stringify(request.input ?? {}) } : {},
6890
- signal: AbortSignal.timeout(request.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7206
+ ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7207
+ signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
6891
7208
  };
6892
7209
  }
6893
- async function handshake(request, deps = {}) {
7210
+ async function handshake2(request2, deps = {}) {
6894
7211
  const doFetch = deps.fetchImpl ?? fetch;
6895
7212
  const now = deps.now ?? Date.now;
6896
7213
  const started = now();
6897
7214
  const since = () => Math.max(0, now() - started);
6898
7215
  try {
6899
- const response = await doFetch(request.url, handshakeInit(request));
7216
+ const response = await doFetch(request2.url, handshakeInit(request2));
6900
7217
  return { ok: true, response, body: await response.text(), latencyMs: since() };
6901
7218
  } catch (err) {
6902
7219
  return { ok: false, reason: err.message, latencyMs: since() };
@@ -6974,24 +7291,24 @@ var ajv = new import_ajv.Ajv({
6974
7291
  validateSchema: true,
6975
7292
  validateFormats: false
6976
7293
  });
6977
- function pass(id, label, detail) {
7294
+ function pass2(id, label, detail) {
6978
7295
  return detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
6979
7296
  }
6980
- function fail(id, label, detail) {
7297
+ function fail2(id, label, detail) {
6981
7298
  return { id, label, status: "fail", detail };
6982
7299
  }
6983
7300
  function schemaCheck(id, label, schema) {
6984
7301
  if (schema === null || schema === void 0) {
6985
- return fail(id, label, "not declared, so no response can ever be checked against it");
7302
+ return fail2(id, label, "not declared, so no response can ever be checked against it");
6986
7303
  }
6987
7304
  if (typeof schema !== "object") {
6988
- return fail(id, label, `declared as ${typeof schema}, not an object`);
7305
+ return fail2(id, label, `declared as ${typeof schema}, not an object`);
6989
7306
  }
6990
7307
  try {
6991
7308
  ajv.compile(schema);
6992
- return pass(id, label);
7309
+ return pass2(id, label);
6993
7310
  } catch (err) {
6994
- return fail(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
7311
+ return fail2(id, label, `declared but invalid: ${err.message.split("\n")[0]}`);
6995
7312
  }
6996
7313
  }
6997
7314
  function requirementChecks(terms) {
@@ -6999,7 +7316,7 @@ function requirementChecks(terms) {
6999
7316
  const report = (id, label, has) => {
7000
7317
  const gaps = terms.accepts.map((r, i) => has(r) ? -1 : i).filter((i) => i >= 0);
7001
7318
  out.push(
7002
- gaps.length === 0 ? pass(id, label) : fail(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7319
+ gaps.length === 0 ? pass2(id, label) : fail2(id, label, `absent from accepts[${gaps.join("], accepts[")}]`)
7003
7320
  );
7004
7321
  };
7005
7322
  report("price_declared", "price is stated", (r) => priceOf(r) !== null);
@@ -7016,29 +7333,29 @@ function requirementChecks(terms) {
7016
7333
  return out;
7017
7334
  }
7018
7335
  function runHandshakeChecks(input) {
7019
- const checks = [pass("reachable", "endpoint answered")];
7336
+ const checks = [pass2("reachable", "endpoint answered")];
7020
7337
  checks.push(
7021
- input.status === 402 ? pass("status_402", "asks for payment") : fail("status_402", "asks for payment", `returned ${input.status}, not 402`)
7338
+ input.status === 402 ? pass2("status_402", "asks for payment") : fail2("status_402", "asks for payment", `returned ${input.status}, not 402`)
7022
7339
  );
7023
7340
  if (input.terms === null) {
7024
7341
  checks.push(
7025
- fail("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7342
+ fail2("terms_parseable", "payment terms parse", input.parseError ?? "no terms found")
7026
7343
  );
7027
7344
  return checks;
7028
7345
  }
7029
- checks.push(pass("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7346
+ checks.push(pass2("terms_parseable", "payment terms parse", `read from the ${input.source}`));
7030
7347
  checks.push(
7031
- input.terms.accepts.length > 0 ? pass(
7348
+ input.terms.accepts.length > 0 ? pass2(
7032
7349
  "accepts_present",
7033
7350
  "offers at least one way to pay",
7034
7351
  `${input.terms.accepts.length} option(s)`
7035
- ) : fail("accepts_present", "offers at least one way to pay", "accepts is empty")
7352
+ ) : fail2("accepts_present", "offers at least one way to pay", "accepts is empty")
7036
7353
  );
7037
7354
  if (input.terms.accepts.length === 0) return checks;
7038
7355
  checks.push(...requirementChecks(input.terms));
7039
7356
  const cache = input.headers.get("cache-control") ?? "";
7040
7357
  checks.push(
7041
- /no-store|no-cache/i.test(cache) ? pass("not_cacheable", "payment terms are not cached") : fail(
7358
+ /no-store|no-cache/i.test(cache) ? pass2("not_cacheable", "payment terms are not cached") : fail2(
7042
7359
  "not_cacheable",
7043
7360
  "payment terms are not cached",
7044
7361
  cache === "" ? "no cache-control, so a proxy may serve stale terms" : `cache-control: ${cache}`
@@ -7047,15 +7364,15 @@ function runHandshakeChecks(input) {
7047
7364
  return checks;
7048
7365
  }
7049
7366
  function unreachable(reason) {
7050
- return [fail("reachable", "endpoint answered", reason)];
7367
+ return [fail2("reachable", "endpoint answered", reason)];
7051
7368
  }
7052
7369
  function verdictOf(checks) {
7053
7370
  return checks.some((c) => c.status === "fail") ? "fail" : "pass";
7054
7371
  }
7055
7372
 
7056
7373
  // src/probe.ts
7057
- function sha256(input) {
7058
- return `sha256:${createHash("sha256").update(input).digest("hex")}`;
7374
+ function sha2562(input) {
7375
+ return `sha256:${createHash3("sha256").update(input).digest("hex")}`;
7059
7376
  }
7060
7377
  function advertisedFrom(terms) {
7061
7378
  if (!terms) return [];
@@ -7071,7 +7388,7 @@ async function probe(options) {
7071
7388
  const body = options.body ?? "{}";
7072
7389
  const doFetch = options.fetchImpl ?? fetch;
7073
7390
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
7074
- const requestHash = sha256(`${method} ${options.url}
7391
+ const requestHash = sha2562(`${method} ${options.url}
7075
7392
  ${body}`);
7076
7393
  const base = {
7077
7394
  url: options.url,
@@ -7081,7 +7398,7 @@ ${body}`);
7081
7398
  termsSource: "none",
7082
7399
  advertised: []
7083
7400
  };
7084
- const answer = await handshake(
7401
+ const answer = await handshake2(
7085
7402
  {
7086
7403
  url: options.url,
7087
7404
  method,
@@ -7102,9 +7419,9 @@ ${body}`);
7102
7419
  verdict: "fail"
7103
7420
  };
7104
7421
  }
7105
- const { response, body: text } = answer;
7422
+ const { response, body: text2 } = answer;
7106
7423
  const handshakeMs = answer.latencyMs;
7107
- const parsed = parseTerms(response.headers, text);
7424
+ const parsed = parseTerms(response.headers, text2);
7108
7425
  const checks = runHandshakeChecks({
7109
7426
  status: response.status,
7110
7427
  headers: response.headers,
@@ -7119,7 +7436,7 @@ ${body}`);
7119
7436
  handshakeMs,
7120
7437
  termsSource: parsed.source,
7121
7438
  advertised: advertisedFrom(parsed.terms),
7122
- responseHash: sha256(text),
7439
+ responseHash: sha2562(text2),
7123
7440
  checks,
7124
7441
  verdict: verdictOf(checks)
7125
7442
  };
@@ -7139,8 +7456,8 @@ function shouldColour(env2, isTty) {
7139
7456
  if (env2.FORCE_COLOR !== void 0 && env2.FORCE_COLOR !== "0") return true;
7140
7457
  return isTty;
7141
7458
  }
7142
- function paint(colour, text, on) {
7143
- return on ? `${ANSI[colour]}${text}${ANSI.reset}` : text;
7459
+ function paint(colour, text2, on) {
7460
+ return on ? `${ANSI[colour]}${text2}${ANSI.reset}` : text2;
7144
7461
  }
7145
7462
  var MARK = { pass: "ok", fail: "fail", skip: "skip" };
7146
7463
  var COLOUR = { pass: "green", fail: "red", skip: "yellow" };
@@ -7192,6 +7509,7 @@ var USAGE = `
7192
7509
  Sends one unpaid request and reports what the endpoint advertises, in the
7193
7510
  same form the public record uses.
7194
7511
 
7512
+ --mcp handshake a remote mcp server instead of an x402 endpoint
7195
7513
  --method <verb> default POST
7196
7514
  --body <json|@file> default {}
7197
7515
  --json machine readable output
@@ -7200,11 +7518,26 @@ var USAGE = `
7200
7518
 
7201
7519
  Exit codes: 0 every check passed, 1 a check failed, 2 could not be reached.
7202
7520
  `;
7521
+ function refuseUnlessUsable(url) {
7522
+ let parsed;
7523
+ try {
7524
+ parsed = new URL(url);
7525
+ } catch {
7526
+ stdout.write(`not a url: ${url}
7527
+ `);
7528
+ exit(2);
7529
+ }
7530
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
7531
+ stdout.write("refusing to send a request over plain http\n");
7532
+ exit(2);
7533
+ }
7534
+ }
7203
7535
  async function main() {
7204
7536
  const { values, positionals } = parseArgs({
7205
7537
  args: argv.slice(2),
7206
7538
  allowPositionals: true,
7207
7539
  options: {
7540
+ mcp: { type: "boolean", default: false },
7208
7541
  method: { type: "string" },
7209
7542
  body: { type: "string" },
7210
7543
  json: { type: "boolean", default: false },
@@ -7219,26 +7552,17 @@ async function main() {
7219
7552
  `);
7220
7553
  exit(values.help ? 0 : 2);
7221
7554
  }
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({
7555
+ refuseUnlessUsable(url);
7556
+ const shared = {
7237
7557
  url,
7238
- method: values.method?.toUpperCase() ?? "POST",
7239
- body,
7240
7558
  ...values.timeout ? { timeoutMs: Number(values.timeout) } : {},
7241
7559
  ...values["user-agent"] ? { userAgent: values["user-agent"] } : {}
7560
+ };
7561
+ 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
7242
7566
  });
7243
7567
  stdout.write(
7244
7568
  values.json ? `${formatJson(result)}
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,349 @@ 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
+ hasInputSchema: typeof tool.inputSchema === "object" && tool.inputSchema !== null
7103
+ }
7104
+ ];
7105
+ });
7106
+ }
7107
+ function reachFor(status) {
7108
+ if (status === 401 || status === 403) return "auth_required";
7109
+ if (status === 402) return "payment_required";
7110
+ if (status >= 500 || status === 404 || status === 410) return "dead";
7111
+ if (status >= 400) return "error";
7112
+ return null;
7113
+ }
7114
+ async function turn(response, method) {
7115
+ const refused = reachFor(response.status);
7116
+ if (refused !== null) return { kind: "refused", reach: refused, status: response.status };
7117
+ const answer = readAnswer(response.headers.get("content-type"), await response.text());
7118
+ if (answer.kind === "result") {
7119
+ return { kind: "read", status: response.status, value: answer.value };
7120
+ }
7121
+ return {
7122
+ kind: "broken",
7123
+ status: response.status,
7124
+ note: answer.kind === "error" ? `${method}: ${answer.message}` : answer.why
7125
+ };
7126
+ }
7127
+ var DEADLINE_MS = 45e3;
7128
+ async function probeMcp(url, options = {}) {
7129
+ const clockFor = options.now ?? Date.now;
7130
+ const startedFor = clockFor();
7131
+ let timer;
7132
+ const gaveUp = new Promise((resolve) => {
7133
+ timer = setTimeout(
7134
+ () => resolve({
7135
+ url,
7136
+ observedAt: new Date(startedFor).toISOString(),
7137
+ reach: "dead",
7138
+ httpStatus: null,
7139
+ handshakeMs: options.deadlineMs ?? DEADLINE_MS,
7140
+ protocolVersion: null,
7141
+ serverName: null,
7142
+ serverVersion: null,
7143
+ tools: null,
7144
+ note: "never answered and never gave up"
7145
+ }),
7146
+ options.deadlineMs ?? DEADLINE_MS
7147
+ );
7148
+ });
7149
+ try {
7150
+ return await Promise.race([handshake(url, options), gaveUp]);
7151
+ } finally {
7152
+ clearTimeout(timer);
7153
+ }
7154
+ }
7155
+ async function handshake(url, options) {
7156
+ const doFetch = options.fetchImpl ?? fetch;
7157
+ const clock = options.now ?? Date.now;
7158
+ const userAgent = options.userAgent ?? `${CLIENT.name}/${CLIENT.version}`;
7159
+ const startedAt = clock();
7160
+ const base = {
7161
+ url,
7162
+ observedAt: new Date(startedAt).toISOString(),
7163
+ protocolVersion: null,
7164
+ serverName: null,
7165
+ serverVersion: null,
7166
+ tools: null
7167
+ };
7168
+ const send = (body, sessionId) => doFetch(url, {
7169
+ ...initFor(body, sessionId, userAgent),
7170
+ signal: AbortSignal.timeout(options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7171
+ });
7172
+ const took = () => Math.max(0, clock() - startedAt);
7173
+ try {
7174
+ const opened = await send(request(1, "initialize", INITIALIZE), null);
7175
+ const hello = await turn(opened, "initialize");
7176
+ if (hello.kind === "refused") {
7177
+ return {
7178
+ ...base,
7179
+ reach: hello.reach,
7180
+ httpStatus: hello.status,
7181
+ handshakeMs: took(),
7182
+ note: null
7183
+ };
7184
+ }
7185
+ if (hello.kind === "broken") {
7186
+ return {
7187
+ ...base,
7188
+ reach: "error",
7189
+ httpStatus: hello.status,
7190
+ handshakeMs: took(),
7191
+ note: hello.note
7192
+ };
7193
+ }
7194
+ const info = hello.value.serverInfo ?? {};
7195
+ const session = opened.headers.get("mcp-session-id");
7196
+ const said = {
7197
+ ...base,
7198
+ protocolVersion: text(hello.value.protocolVersion),
7199
+ serverName: text(info.name),
7200
+ serverVersion: text(info.version)
7201
+ };
7202
+ 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") {
7214
+ return {
7215
+ ...said,
7216
+ reach: "error",
7217
+ httpStatus: listed.status,
7218
+ handshakeMs: took(),
7219
+ note: listed.note
7220
+ };
7221
+ }
7222
+ return {
7223
+ ...said,
7224
+ reach: "readable",
7225
+ httpStatus: listed.status,
7226
+ handshakeMs: took(),
7227
+ tools: toolsFrom(listed.value),
7228
+ note: null
7229
+ };
7230
+ } catch (err) {
7231
+ const name = err.name;
7232
+ return {
7233
+ ...base,
7234
+ reach: "dead",
7235
+ httpStatus: null,
7236
+ handshakeMs: took(),
7237
+ note: name === "TimeoutError" || name === "AbortError" ? "timed out" : "unreachable"
7238
+ };
7239
+ }
7240
+ }
7241
+ function toolsDigest(tools) {
7242
+ 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)
7244
+ );
7245
+ return createHash("sha256").update(canonical).digest("hex");
7246
+ }
7247
+
7248
+ // src/mcp.ts
7249
+ function sha256(input) {
7250
+ return `sha256:${createHash2("sha256").update(input).digest("hex")}`;
7251
+ }
7252
+ var pass2 = (id, label, detail) => detail === void 0 ? { id, label, status: "pass" } : { id, label, status: "pass", detail };
7253
+ var fail2 = (id, label, detail) => ({
7254
+ id,
7255
+ label,
7256
+ status: "fail",
7257
+ detail
7258
+ });
7259
+ var skip = (id, label, detail) => ({
7260
+ id,
7261
+ label,
7262
+ status: "skip",
7263
+ detail
7264
+ });
7265
+ var REACH_LABEL = {
7266
+ readable: "listed its tools",
7267
+ auth_required: "asked for credentials",
7268
+ payment_required: "asked for payment",
7269
+ dead: "did not answer",
7270
+ error: "answered with something else"
7271
+ };
7272
+ function checksFor(seen) {
7273
+ const checks = [];
7274
+ checks.push(
7275
+ seen.reach === "dead" ? fail2("reachable", "server answered", seen.note ?? "no answer") : pass2("reachable", "server answered", REACH_LABEL[seen.reach])
7276
+ );
7277
+ if (seen.reach === "dead") return checks;
7278
+ checks.push(
7279
+ seen.protocolVersion === null ? skip("protocol", "named a protocol version", "it never got as far as saying") : pass2("protocol", "named a protocol version", seen.protocolVersion)
7280
+ );
7281
+ checks.push(
7282
+ seen.serverName === null ? skip("identity", "named itself", "it never got as far as saying") : pass2(
7283
+ "identity",
7284
+ "named itself",
7285
+ `${seen.serverName}${seen.serverVersion === null ? "" : ` ${seen.serverVersion}`}`
7286
+ )
7287
+ );
7288
+ if (seen.tools === null) {
7289
+ checks.push(skip("tools", "listed its tools", REACH_LABEL[seen.reach]));
7290
+ return checks;
7291
+ }
7292
+ 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);
7294
+ checks.push(
7295
+ bare.length === 0 ? pass2("input-schemas", "every tool declared an input schema") : fail2(
7296
+ "input-schemas",
7297
+ "every tool declared an input schema",
7298
+ `${bare.length} without one: ${bare.slice(0, 5).join(", ")}`
7299
+ )
7300
+ );
7301
+ const unnamed = seen.tools.filter((t) => t.description === null).map((t) => t.name);
7302
+ checks.push(
7303
+ unnamed.length === 0 ? pass2("descriptions", "every tool said what it does") : fail2(
7304
+ "descriptions",
7305
+ "every tool said what it does",
7306
+ `${unnamed.length} without one: ${unnamed.slice(0, 5).join(", ")}`
7307
+ )
7308
+ );
7309
+ checks.push(pass2("digest", "tool list digest", toolsDigest(seen.tools)));
7310
+ return checks;
7311
+ }
7312
+ async function probeMcpEndpoint(options) {
7313
+ const seen = await probeMcp(options.url, {
7314
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
7315
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
7316
+ ...options.userAgent === void 0 ? {} : { userAgent: options.userAgent }
7317
+ });
7318
+ const checks = checksFor(seen);
7319
+ return {
7320
+ url: options.url,
7321
+ method: "MCP",
7322
+ observedAt: seen.observedAt,
7323
+ reachable: seen.reach !== "dead",
7324
+ httpStatus: seen.httpStatus,
7325
+ handshakeMs: seen.handshakeMs,
7326
+ termsSource: seen.reach === "payment_required" ? "mcp 402" : "none",
7327
+ advertised: [],
7328
+ requestHash: sha256(`MCP ${options.url}`),
7329
+ responseHash: seen.tools === null ? null : `sha256:${toolsDigest(seen.tools)}`,
7330
+ checks,
7331
+ verdict: checks.some((c) => c.status === "fail") ? "fail" : "pass"
7332
+ };
7333
+ }
7334
+
7335
+ // src/probe.ts
7336
+ import { createHash as createHash3 } from "node:crypto";
7020
7337
 
7021
7338
  // ../x402/src/handshake.ts
7022
- var HANDSHAKE_TIMEOUT_MS = 3e4;
7339
+ var HANDSHAKE_TIMEOUT_MS2 = 3e4;
7023
7340
  var sendsBody = (method) => method !== "GET" && method !== "HEAD";
7024
- function handshakeInit(request) {
7025
- const carries = sendsBody(request.method);
7341
+ function handshakeInit(request2) {
7342
+ const carries = sendsBody(request2.method);
7026
7343
  const headers = { accept: "application/json" };
7027
7344
  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;
7345
+ if (request2.userAgent !== void 0) headers["user-agent"] = request2.userAgent;
7346
+ if (request2.payment) headers[request2.payment.header] = request2.payment.value;
7030
7347
  return {
7031
- method: request.method,
7348
+ method: request2.method,
7032
7349
  headers,
7033
- ...carries ? { body: JSON.stringify(request.input ?? {}) } : {},
7034
- signal: AbortSignal.timeout(request.timeoutMs ?? HANDSHAKE_TIMEOUT_MS)
7350
+ ...carries ? { body: JSON.stringify(request2.input ?? {}) } : {},
7351
+ signal: AbortSignal.timeout(request2.timeoutMs ?? HANDSHAKE_TIMEOUT_MS2)
7035
7352
  };
7036
7353
  }
7037
- async function handshake(request, deps = {}) {
7354
+ async function handshake2(request2, deps = {}) {
7038
7355
  const doFetch = deps.fetchImpl ?? fetch;
7039
7356
  const now = deps.now ?? Date.now;
7040
7357
  const started = now();
7041
7358
  const since = () => Math.max(0, now() - started);
7042
7359
  try {
7043
- const response = await doFetch(request.url, handshakeInit(request));
7360
+ const response = await doFetch(request2.url, handshakeInit(request2));
7044
7361
  return { ok: true, response, body: await response.text(), latencyMs: since() };
7045
7362
  } catch (err) {
7046
7363
  return { ok: false, reason: err.message, latencyMs: since() };
@@ -7049,8 +7366,8 @@ async function handshake(request, deps = {}) {
7049
7366
 
7050
7367
  // src/probe.ts
7051
7368
  var DEFAULT_USER_AGENT = "node";
7052
- function sha256(input) {
7053
- return `sha256:${createHash("sha256").update(input).digest("hex")}`;
7369
+ function sha2562(input) {
7370
+ return `sha256:${createHash3("sha256").update(input).digest("hex")}`;
7054
7371
  }
7055
7372
  function advertisedFrom(terms) {
7056
7373
  if (!terms) return [];
@@ -7066,7 +7383,7 @@ async function probe(options) {
7066
7383
  const body = options.body ?? "{}";
7067
7384
  const doFetch = options.fetchImpl ?? fetch;
7068
7385
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
7069
- const requestHash = sha256(`${method} ${options.url}
7386
+ const requestHash = sha2562(`${method} ${options.url}
7070
7387
  ${body}`);
7071
7388
  const base = {
7072
7389
  url: options.url,
@@ -7076,7 +7393,7 @@ ${body}`);
7076
7393
  termsSource: "none",
7077
7394
  advertised: []
7078
7395
  };
7079
- const answer = await handshake(
7396
+ const answer = await handshake2(
7080
7397
  {
7081
7398
  url: options.url,
7082
7399
  method,
@@ -7097,9 +7414,9 @@ ${body}`);
7097
7414
  verdict: "fail"
7098
7415
  };
7099
7416
  }
7100
- const { response, body: text } = answer;
7417
+ const { response, body: text2 } = answer;
7101
7418
  const handshakeMs = answer.latencyMs;
7102
- const parsed = parseTerms(response.headers, text);
7419
+ const parsed = parseTerms(response.headers, text2);
7103
7420
  const checks = runHandshakeChecks({
7104
7421
  status: response.status,
7105
7422
  headers: response.headers,
@@ -7114,7 +7431,7 @@ ${body}`);
7114
7431
  handshakeMs,
7115
7432
  termsSource: parsed.source,
7116
7433
  advertised: advertisedFrom(parsed.terms),
7117
- responseHash: sha256(text),
7434
+ responseHash: sha2562(text2),
7118
7435
  checks,
7119
7436
  verdict: verdictOf(checks)
7120
7437
  };
@@ -7134,8 +7451,8 @@ function shouldColour(env, isTty) {
7134
7451
  if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
7135
7452
  return isTty;
7136
7453
  }
7137
- function paint(colour, text, on) {
7138
- return on ? `${ANSI[colour]}${text}${ANSI.reset}` : text;
7454
+ function paint(colour, text2, on) {
7455
+ return on ? `${ANSI[colour]}${text2}${ANSI.reset}` : text2;
7139
7456
  }
7140
7457
  var MARK = { pass: "ok", fail: "fail", skip: "skip" };
7141
7458
  var COLOUR = { pass: "green", fail: "red", skip: "yellow" };
@@ -7181,11 +7498,14 @@ function formatJson(result) {
7181
7498
  }
7182
7499
  export {
7183
7500
  DEFAULT_USER_AGENT,
7501
+ REACH_LABEL,
7502
+ checksFor,
7184
7503
  formatJson,
7185
7504
  formatText,
7186
7505
  parseTerms,
7187
7506
  priceOf,
7188
7507
  probe,
7508
+ probeMcpEndpoint,
7189
7509
  runHandshakeChecks,
7190
7510
  schemaCheck,
7191
7511
  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, HANDSHAKE_TIMEOUT_MS, INITIALIZE, initFor, type McpObservation, type ProbeOptions, probeMcp, type Reach, reachFor, type Tool, toolsDigest, toolsFrom, } from './probe.js';
@@ -0,0 +1,44 @@
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 hasInputSchema: boolean;
11
+ };
12
+ export type McpObservation = {
13
+ readonly url: string;
14
+ readonly observedAt: string;
15
+ readonly reach: Reach;
16
+ readonly httpStatus: number | null;
17
+ readonly handshakeMs: number;
18
+ readonly protocolVersion: string | null;
19
+ readonly serverName: string | null;
20
+ readonly serverVersion: string | null;
21
+ readonly tools: readonly Tool[] | null;
22
+ readonly note: string | null;
23
+ };
24
+ export type ProbeOptions = {
25
+ readonly timeoutMs?: number;
26
+ readonly deadlineMs?: number;
27
+ readonly fetchImpl?: typeof fetch;
28
+ readonly now?: () => number;
29
+ readonly userAgent?: string;
30
+ };
31
+ export declare function initFor(body: string, sessionId: string | null, userAgent: string): RequestInit;
32
+ export declare const INITIALIZE: {
33
+ protocolVersion: "2025-06-18";
34
+ capabilities: {};
35
+ clientInfo: {
36
+ readonly name: "teppi-probe";
37
+ readonly version: "0.1.0";
38
+ };
39
+ };
40
+ export declare function toolsFrom(result: Record<string, unknown>): Tool[];
41
+ export declare function reachFor(status: number): Reach | null;
42
+ export declare const DEADLINE_MS = 45000;
43
+ export declare function probeMcp(url: string, options?: ProbeOptions): Promise<McpObservation>;
44
+ export declare function toolsDigest(tools: readonly Tool[]): 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;
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.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",
@@ -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",