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