@crawlcheck/sdk 1.3.0 → 1.4.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/dist/mcp.d.ts +3 -2
- package/dist/mcp.js +27 -14
- package/package.json +1 -1
- package/test/mcp.test.js +21 -1
package/dist/mcp.d.ts
CHANGED
|
@@ -30,8 +30,9 @@ export function mcpPreflight(cc: any, opts?: {}): {
|
|
|
30
30
|
/** After initialize, with the tools the server lists. Refuses (and the wrapper closes the client) when they differ from the approved lockfile. */
|
|
31
31
|
afterConnect(tools: any): Promise<import("./index.js").McpLockCheck | null>;
|
|
32
32
|
/** Before tools/call. */
|
|
33
|
-
beforeToolCall(name: any, annotations: any): Promise<{
|
|
34
|
-
receipt:
|
|
33
|
+
beforeToolCall(name: any, annotations: any, tool: any): Promise<{
|
|
34
|
+
receipt: any;
|
|
35
|
+
tool_receipt: any;
|
|
35
36
|
}>;
|
|
36
37
|
};
|
|
37
38
|
/**
|
package/dist/mcp.js
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
// tools/call: the connect receipt must still be unexpired (else the host is decided again); a tool outside the approved
|
|
11
11
|
// lockfile is refused; a tool annotated destructive (destructiveHint true, or readOnlyHint false with
|
|
12
12
|
// openWorldHint true) asks opts.confirm when opts.confirmDestructive is set.
|
|
13
|
+
// per call: (1.4.0) a tool call gets a receipt of its own, scoped to that tool (name, digest of its definition, opts.scopes),
|
|
14
|
+
// so a connect receipt never stands in for a call to a tool that changed. opts.toolReceipts: "write" (default:
|
|
15
|
+
// every tool not annotated readOnlyHint), "all", or "none".
|
|
13
16
|
// Nothing here calls a tool, reads arguments or alters the request; a refused call throws McpPreflightRefused and the
|
|
14
17
|
// request is never sent.
|
|
15
18
|
import { normalizeDomain } from "./index.js";
|
|
16
|
-
import { checkLock } from "./index.js";
|
|
19
|
+
import { checkLock, toolOperation } from "./index.js";
|
|
17
20
|
export class McpPreflightRefused extends Error {
|
|
18
21
|
constructor(stage, why, detail) { super("MCP preflight refused " + stage + ": " + why); this.name = "McpPreflightRefused"; this.stage = stage; this.why = why; this.receipt = detail && detail.receipt || null; this.lock = detail && detail.lock || null; this.verification = detail && detail.verification || null; }
|
|
19
22
|
}
|
|
@@ -103,7 +106,7 @@ export function mcpPreflight(cc, opts = {}) {
|
|
|
103
106
|
return lc;
|
|
104
107
|
},
|
|
105
108
|
/** Before tools/call. */
|
|
106
|
-
async beforeToolCall(name, annotations) {
|
|
109
|
+
async beforeToolCall(name, annotations, tool) {
|
|
107
110
|
if (state.host && !fresh())
|
|
108
111
|
await decide(state.host);
|
|
109
112
|
if (state.approved && !state.approved.has(name))
|
|
@@ -113,7 +116,16 @@ export function mcpPreflight(cc, opts = {}) {
|
|
|
113
116
|
if (!ok)
|
|
114
117
|
throw new McpPreflightRefused("tools/call", "tool " + name + " is annotated destructive and no human confirmed", { receipt: state.receipt });
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
const mode = opts.toolReceipts || "write", ro = !!(annotations && annotations.readOnlyHint === true);
|
|
120
|
+
let callReceipt = null;
|
|
121
|
+
if (state.host && (mode === "all" || (mode === "write" && !ro))) { // op1/mcp: a receipt for this exact tool call
|
|
122
|
+
const op = tool ? await toolOperation(tool, opts.scopes) : Object.assign({ tool: String(name) }, opts.scopes && opts.scopes.length ? { scopes: opts.scopes.slice() } : {});
|
|
123
|
+
const g = await cc.guard(state.host, "connect", { template, policy: opts.policy, agent: opts.agent, confirm: opts.confirm, allowWarn: !!opts.allowWarn, onlineKeys: opts.onlineKeys, operation: op });
|
|
124
|
+
if (!g.proceed)
|
|
125
|
+
throw new McpPreflightRefused("tools/call", "no receipt for tool " + name + ": " + ((g.why || []).join("; ") || "refused"), { receipt: g.receipt, verification: g.verification });
|
|
126
|
+
callReceipt = g.receipt;
|
|
127
|
+
}
|
|
128
|
+
return { receipt: callReceipt || state.receipt, tool_receipt: callReceipt };
|
|
117
129
|
}
|
|
118
130
|
};
|
|
119
131
|
}
|
|
@@ -129,7 +141,7 @@ export function withPreflight(client, cc, opts = {}) {
|
|
|
129
141
|
client.connect = async function (transport, ...rest) {
|
|
130
142
|
await pf.beforeConnect(opts.serverUrl || transportUrl(transport));
|
|
131
143
|
const r = await connect(transport, ...rest);
|
|
132
|
-
if (opts.lock || opts.confirmDestructive) {
|
|
144
|
+
if (opts.lock || opts.confirmDestructive || (opts.toolReceipts || "write") !== "none") {
|
|
133
145
|
let list = [];
|
|
134
146
|
try {
|
|
135
147
|
const t = typeof client.listTools === "function" ? await client.listTools() : null;
|
|
@@ -140,23 +152,24 @@ export function withPreflight(client, cc, opts = {}) {
|
|
|
140
152
|
}
|
|
141
153
|
list.forEach(function (t) { if (t && t.name)
|
|
142
154
|
tools[t.name] = t; });
|
|
143
|
-
|
|
144
|
-
await pf.afterConnect(list);
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
155
|
+
if (opts.lock || opts.confirmDestructive)
|
|
147
156
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
await pf.afterConnect(list);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
try {
|
|
161
|
+
if (typeof client.close === "function")
|
|
162
|
+
await client.close();
|
|
163
|
+
}
|
|
164
|
+
catch (e2) { }
|
|
165
|
+
throw e;
|
|
150
166
|
}
|
|
151
|
-
catch (e2) { }
|
|
152
|
-
throw e;
|
|
153
|
-
}
|
|
154
167
|
}
|
|
155
168
|
return r;
|
|
156
169
|
};
|
|
157
170
|
client.callTool = async function (params, ...rest) {
|
|
158
171
|
const name = params && params.name;
|
|
159
|
-
await pf.beforeToolCall(name, tools[name] && tools[name].annotations);
|
|
172
|
+
await pf.beforeToolCall(name, tools[name] && tools[name].annotations, tools[name]);
|
|
160
173
|
return callTool(params, ...rest);
|
|
161
174
|
};
|
|
162
175
|
client.preflight = pf;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlcheck/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Typed client for the CrawlCheck API (generated from its OpenAPI document) and the zero-dependency offline verifier for evidence bundles and remediation receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/test/mcp.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { withPreflight, mcpPreflight, McpPreflightRefused, transportUrl, lockFro
|
|
|
5
5
|
function cc(table) {
|
|
6
6
|
const calls = [];
|
|
7
7
|
return { calls, async guard(host, action, opts) {
|
|
8
|
-
calls.push({ host, action, template: opts.template });
|
|
8
|
+
calls.push({ host, action, template: opts.template, tool: opts.operation && opts.operation.tool, op: opts.operation });
|
|
9
9
|
const d = table[host] || "unsupported";
|
|
10
10
|
const receipt = { kind: "crawlcheck-decision", domain: host, action, decision: d, expires_at: new Date(Date.now() + 3600e3).toISOString(), steps: [{ why: "table" }], sha256: "x", signature: { kid: "k" } };
|
|
11
11
|
let proceed = d === "allow" || (d === "warn" && !!opts.allowWarn);
|
|
@@ -79,3 +79,23 @@ test("bare hooks work without a client object", async () => {
|
|
|
79
79
|
const r = await h.beforeConnect("https://good.example/mcp"); assert.equal(r.host, "good.example");
|
|
80
80
|
assert.equal((await h.beforeToolCall("search")).receipt.decision, "allow");
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
test("1.4.0: a write tool call gets its own receipt, scoped to the tool's definition; a read-only one reuses connect", async () => {
|
|
84
|
+
const { lockToolHash } = await import("../dist/index.js");
|
|
85
|
+
const k = cc({ "good.example": "allow" }); const c = withPreflight(mcpClient(TOOLS), k);
|
|
86
|
+
await c.connect(T("https://good.example/mcp")); await c.callTool({ name: "search" }); await c.callTool({ name: "delete_repo" });
|
|
87
|
+
const per = k.calls.filter((x) => x.tool);
|
|
88
|
+
assert.deepEqual(per.map((x) => x.tool), ["delete_repo"]);
|
|
89
|
+
assert.equal(per[0].op.tool_schema_sha256, await lockToolHash(TOOLS[1]));
|
|
90
|
+
});
|
|
91
|
+
test("1.4.0: a refused tool receipt stops tools/call; toolReceipts none skips it", async () => {
|
|
92
|
+
const k = cc({ "good.example": "allow" }); const orig = k.guard;
|
|
93
|
+
k.guard = async (h, a, o) => o.operation ? { proceed: false, receipt: null, verification: null, why: ["the receipt is for another operation"] } : orig(h, a, o);
|
|
94
|
+
const cl = mcpClient(TOOLS); const c = withPreflight(cl, k);
|
|
95
|
+
await c.connect(T("https://good.example/mcp"));
|
|
96
|
+
await assert.rejects(() => c.callTool({ name: "delete_repo" }), (e) => e.stage === "tools/call" && /another operation/.test(e.why));
|
|
97
|
+
assert.ok(!cl.sent.includes("tools/call delete_repo"));
|
|
98
|
+
const k2 = cc({ "good.example": "allow" }); const c2 = withPreflight(mcpClient(TOOLS), k2, { toolReceipts: "none" });
|
|
99
|
+
await c2.connect(T("https://good.example/mcp")); await c2.callTool({ name: "delete_repo" });
|
|
100
|
+
assert.equal(k2.calls.filter((x) => x.tool).length, 0);
|
|
101
|
+
});
|