@scopebond/mcp 0.3.3 → 1.0.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
@@ -30,6 +30,43 @@ pass through — so a server that exposes data as resources rather than tools is
30
30
  covered by a tool policy. Govern such data at the server, or front only servers
31
31
  whose sensitive operations are tools.
32
32
 
33
+ ## Typed adapter (optional, off by default)
34
+
35
+ `--typed typed.json` adds an adapter that describes each `tools/call` from the request the proxy actually
36
+ forwards (the proxy digests and dispatches one private copy of the message) and can refuse what it cannot
37
+ bind before anything reaches the upstream:
38
+
39
+ ```json
40
+ {
41
+ "mode": "enforce",
42
+ "requireResourceBinding": true,
43
+ "approvedResources": { "repository": ["acme/widgets"] },
44
+ "manifest": {
45
+ "hash": "sha256:…",
46
+ "tools": {
47
+ "get_issue": { "operation_class": "read_only", "resources": [{ "arg": "repository", "kind": "repository" }] },
48
+ "delete_branch": { "operation_class": "mutation", "resources": [{ "arg": "repository", "kind": "repository" }] }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ - `manifest.hash` pins the server's tool list (`manifestHash(tools)`). The proxy reads the live list (from the
55
+ client's own `tools/list` or by asking the upstream) and treats the manifest as valid only while it still
56
+ hashes to the pin; a changed server is `unverified` and every tool on it is `unknown`.
57
+ - Under `enforce`, an unknown tool or revision is denied. Under `monitor` (use it to start) nothing is denied
58
+ and unknowns are recorded as unknown.
59
+ - A manifest says what a tool is, not which resource a call touches, so it cannot authorize a resource-specific
60
+ call on its own. With `requireResourceBinding`, a mutation, or a tool that names resources, is denied
61
+ unless every resource named at its argument paths was read from the dispatched arguments and is in
62
+ `approvedResources` for its kind.
63
+ - When the hook is installed and enrolled for observations (`--observations-dir` or `SCOPEBOND_HOOK_DIR`), a
64
+ `tool_intent` (before dispatch, also for a denied call) and a `tool_outcome` (after a forwarded call) go to its
65
+ outbox as a closed `mcp` operation: server, tool, manifest revision, operation class, keyed resource ids and an
66
+ HMAC request digest under the hook's installation-local key. Without the hook the adapter still runs with a
67
+ local binding key beside your signing key and nothing is queued. The library takes any sink with an
68
+ `emit(draft)` method, so no vendor SDK is involved.
69
+
33
70
  ## Policy
34
71
 
35
72
  Bound MCP calls with the Action Taxonomy's `mcp.tool.call` type — "read-only
package/dist/cli.js CHANGED
@@ -7,7 +7,23 @@
7
7
  //
8
8
  // scopebond-mcp init --server <id> scaffold a key + starter policy
9
9
  // scopebond-mcp --server <id> [--policy p.json] [--key k.pem] [--principal sub] \
10
- // [--receipts log.jsonl] -- <upstream-command...>
10
+ // [--receipts log.jsonl] [--typed typed.json] [--dispatch-dir dir] [--delegation id] -- <upstream-command...>
11
+ //
12
+ // --dispatch-dir turns on the dispatch boundary (off by default): the directory holds dispatch.json
13
+ // (require_approval, approver_keys, budgets), an approvals/ inbox and the shared dispatch.db. Each
14
+ // tools/call that policy allows then needs its single-use approval, its delegated scope and its
15
+ // action-budget slot before it is forwarded; without them it is denied and never sent upstream.
16
+ // --delegation (or SCOPEBOND_DELEGATION) runs the proxy under a delegated child scope.
17
+ //
18
+ // --typed turns on the typed adapter (off by default): a JSON file with
19
+ // { "mode": "monitor" | "enforce", "manifest": { "hash": "sha256:…", "tools": { "<tool>":
20
+ // { "operation_class": "read_only" | "mutation", "resources": [{ "arg": "repository", "kind": "repository" }] } } },
21
+ // "requireResourceBinding": true, "approvedResources": { "repository": ["owner/name"] } }
22
+ // Under "enforce" a tool the pinned manifest does not list, a server whose tool list no longer
23
+ // matches its hash, and (with requireResourceBinding) a call whose resources cannot be bound
24
+ // from the dispatched arguments or are not approved, are denied before dispatch. When the
25
+ // hook is installed and enrolled for observations (SCOPEBOND_HOOK_DIR or --observations-dir),
26
+ // tool_intent and tool_outcome observations are queued in its outbox.
11
27
  //
12
28
  // Env: SCOPEBOND_MCP_POLICY, SCOPEBOND_MCP_KEY, SCOPEBOND_MCP_SERVER,
13
29
  // SCOPEBOND_MCP_PRINCIPAL, SCOPEBOND_MCP_RECEIPTS.
@@ -16,7 +32,12 @@ import { readFileSync, appendFileSync } from "node:fs";
16
32
  import { spawn } from "node:child_process";
17
33
  import { createInterface } from "node:readline";
18
34
  import { createMcpProxy } from "./proxy.js";
35
+ import { requestBinderFromHex } from "./typed.js";
36
+ import { existsSync, writeFileSync } from "node:fs";
37
+ import { randomBytes } from "node:crypto";
38
+ import { resolve } from "node:path";
19
39
  import { scaffold } from "./init.js";
40
+ import { openApprovalBinder, openDispatchGuard } from "@scopebond/gateway/node";
20
41
  import { connectCloud, loadMcpConnection, connectionFileFor, openExporter } from "./cloud.js";
21
42
  function arg(name, fallback) {
22
43
  const i = process.argv.indexOf(name);
@@ -101,9 +122,11 @@ else {
101
122
  const child = spawn(upstreamCmd[0], upstreamCmd.slice(1), { stdio: ["pipe", "pipe", "inherit"] });
102
123
  child.on("error", (e) => die(`could not start the upstream server: ${e.message}`));
103
124
  const shutdown = (code) => {
104
- if (!exporter)
105
- process.exit(code);
106
- void exporter.flush().finally(() => { exporter.stop(); process.exit(code); });
125
+ const done = () => process.exit(code);
126
+ void Promise.allSettled([exporter?.flush(), observations?.flush()]).finally(() => { exporter?.stop(); try {
127
+ observations?.close();
128
+ }
129
+ catch { /* closing is best effort */ } done(); });
107
130
  };
108
131
  child.on("exit", (code) => shutdown(code ?? 0));
109
132
  process.on("SIGINT", () => shutdown(0));
@@ -141,9 +164,73 @@ else {
141
164
  });
142
165
  },
143
166
  };
167
+ // The typed adapter is off unless a typed config is given. Its binding key and observation
168
+ // outbox come from the hook when it is installed and enrolled; otherwise the key is a local
169
+ // file beside the signing key (never uploaded) and nothing is queued.
170
+ const typedPath = arg("--typed", process.env.SCOPEBOND_MCP_TYPED);
171
+ let typed;
172
+ let observations;
173
+ if (typedPath) {
174
+ let raw;
175
+ try {
176
+ raw = JSON.parse(readFileSync(typedPath, "utf8"));
177
+ }
178
+ catch (e) {
179
+ die(`could not read the typed config ${typedPath}: ${e.message}`);
180
+ }
181
+ if (raw.mode !== "monitor" && raw.mode !== "enforce")
182
+ die('the typed config needs "mode": "monitor" or "enforce"');
183
+ let binder;
184
+ let sink;
185
+ const hookDir = arg("--observations-dir", process.env.SCOPEBOND_HOOK_DIR);
186
+ if (hookDir) {
187
+ try {
188
+ const hook = await import("@scopebond/hook");
189
+ const opened = hook.openObservations(resolve(hookDir), { adapterVersion: "scopebond-mcp" });
190
+ if (opened.emitter) {
191
+ sink = opened.emitter;
192
+ binder = opened.emitter.binding;
193
+ observations = opened.emitter;
194
+ }
195
+ else
196
+ process.stderr.write(`scopebond-mcp: observations are not on (${opened.status.reason ?? opened.status.state}); the typed adapter runs without them\n`);
197
+ }
198
+ catch (e) {
199
+ process.stderr.write(`scopebond-mcp: the hook package is not available for observations (${e.message})\n`);
200
+ }
201
+ }
202
+ if (!binder) {
203
+ const file = `${keyPath}.binding`;
204
+ let hex = existsSync(file) ? readFileSync(file, "utf8").trim() : "";
205
+ if (!/^[0-9a-f]{64}$/.test(hex)) {
206
+ hex = randomBytes(32).toString("hex");
207
+ writeFileSync(file, hex + "\n", { mode: 0o600 });
208
+ }
209
+ binder = requestBinderFromHex(hex);
210
+ }
211
+ typed = { ...raw, mode: raw.mode, binder, ...(sink ? { sink } : {}) };
212
+ }
213
+ // The dispatch boundary is opt-in. A directory that is named but cannot be read is a setup error: it says what may be spent.
214
+ const dispatchDir = arg("--dispatch-dir", process.env.SCOPEBOND_DISPATCH_DIR);
215
+ const delegationId = arg("--delegation", process.env.SCOPEBOND_DELEGATION);
216
+ let dispatch;
217
+ if (delegationId && !dispatchDir)
218
+ die("--delegation needs --dispatch-dir (a delegation is checked against its shared store)");
219
+ if (dispatchDir) {
220
+ try {
221
+ const guard = openDispatchGuard(dispatchDir ?? ".", { delegated: !!delegationId });
222
+ if (guard) {
223
+ const binder = openApprovalBinder(dispatchDir ?? ".");
224
+ dispatch = { guard, ...(delegationId ? { delegationId } : {}), ...(binder ? { binder } : {}) };
225
+ }
226
+ }
227
+ catch (e) {
228
+ die(`could not read the dispatch settings in ${dispatchDir ?? "."}: ${e.message}`);
229
+ }
230
+ }
144
231
  const proxy = createMcpProxy({
145
232
  policy, principal: { subject: `client:${principal}`, issuer: "scopebond:mcp-proxy" }, server: server,
146
- attesterKeyPem, upstream,
233
+ attesterKeyPem, upstream, ...(dispatch ? { dispatch } : {}), ...(typed ? { typed, adapterVersion: "scopebond-mcp" } : {}),
147
234
  onReceipt: (r) => {
148
235
  if (receiptsPath) {
149
236
  try {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// scopebond-mcp — a stdio Model Context Protocol proxy. It sits between an MCP\n// client and an upstream server: the client speaks MCP to this process, and this\n// process speaks MCP to the upstream command (everything after `--`). Every\n// tools/call is checked against policy before it is forwarded; a denial returns a\n// JSON-RPC error and is never sent upstream.\n//\n// scopebond-mcp init --server <id> scaffold a key + starter policy\n// scopebond-mcp --server <id> [--policy p.json] [--key k.pem] [--principal sub] \\\n// [--receipts log.jsonl] -- <upstream-command...>\n//\n// Env: SCOPEBOND_MCP_POLICY, SCOPEBOND_MCP_KEY, SCOPEBOND_MCP_SERVER,\n// SCOPEBOND_MCP_PRINCIPAL, SCOPEBOND_MCP_RECEIPTS.\n// MCP stdio is newline-delimited JSON-RPC. Fail closed: any setup error exits 1.\n\nimport { readFileSync, appendFileSync } from \"node:fs\";\nimport { spawn } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport type { SignedReceipt, CloudExporter } from \"@scopebond/gateway\";\nimport { createMcpProxy } from \"./proxy.js\";\nimport type { JsonRpcMessage, McpUpstream } from \"./proxy.js\";\nimport { scaffold } from \"./init.js\";\nimport { connectCloud, loadMcpConnection, connectionFileFor, openExporter } from \"./cloud.js\";\n\nfunction arg(name: string, fallback?: string): string | undefined {\n const i = process.argv.indexOf(name);\n return i >= 0 ? process.argv[i + 1] : fallback;\n}\nfunction die(message: string): never { process.stderr.write(`scopebond-mcp: ${message}\\n`); process.exit(1); }\n\n// `init`: scaffold a key + a starter policy for one server, then exit.\nif (process.argv[2] === \"init\") {\n const server = arg(\"--server\", process.env.SCOPEBOND_MCP_SERVER);\n if (!server) die(\"usage: scopebond-mcp init --server <upstream-server-id>\");\n const { keyFile, policyFile } = scaffold(server as string, { force: process.argv.includes(\"--force\") });\n console.log(`Scopebond MCP proxy enrolled for server \"${server}\":`);\n console.log(` key ${keyFile}`);\n console.log(` policy ${policyFile} (starter — edit the tool bounds)`);\n console.log(\"\");\n console.log(\"Point your MCP client at the proxy:\");\n console.log(` scopebond-mcp --server ${server} -- <your real MCP server command>`);\n process.exit(0);\n}\n\n// `connect`: enroll the proxy's key with a workspace and store a scoped credential.\nif (process.argv[2] === \"connect\") {\n const url = process.argv[3];\n const bundleFile = process.argv[4];\n const keyPath = arg(\"--key\", process.env.SCOPEBOND_MCP_KEY ?? \"scopebond-agent.key\")!;\n if (!url || !bundleFile) die(\"usage: scopebond-mcp connect <workspace-url> <enrollment-bundle.json> [--key k.pem]\");\n let bundle;\n try { bundle = JSON.parse(readFileSync(bundleFile, \"utf8\")); }\n catch (e) { die(`could not read the enrollment bundle ${bundleFile}: ${(e as Error).message}`); }\n connectCloud(keyPath, url, bundle).then((c) => {\n console.log(`Connected to ${c.url} (org ${c.organization_id} · env ${c.environment_id})`);\n console.log(` credential stored in ${connectionFileFor(keyPath)} — a secret, do not commit`);\n console.log(\"Run the proxy as usual; every governed tool call is now mirrored to your workspace.\");\n process.exit(0);\n }).catch((e) => die(`connect failed: ${(e as Error).message}`));\n} else {\n\nconst dashDash = process.argv.indexOf(\"--\");\nif (dashDash < 0 || dashDash === process.argv.length - 1) die(\"provide the upstream command after `--`\");\nconst upstreamCmd = process.argv.slice(dashDash + 1);\n\nconst server = arg(\"--server\", process.env.SCOPEBOND_MCP_SERVER);\nconst policyPath = arg(\"--policy\", process.env.SCOPEBOND_MCP_POLICY ?? \"scopebond.policy.json\");\nconst keyPath = arg(\"--key\", process.env.SCOPEBOND_MCP_KEY ?? \"scopebond-agent.key\");\nconst principal = arg(\"--principal\", process.env.SCOPEBOND_MCP_PRINCIPAL ?? \"mcp-client\");\nconst receiptsPath = arg(\"--receipts\", process.env.SCOPEBOND_MCP_RECEIPTS);\nif (!server) die(\"--server (the upstream server id) is required\");\n\nlet policy: unknown;\nlet attesterKeyPem: string;\ntry { policy = JSON.parse(readFileSync(policyPath as string, \"utf8\")); } catch (e) { die(`could not read policy ${policyPath}: ${(e as Error).message}`); }\ntry { attesterKeyPem = readFileSync(keyPath as string, \"utf8\"); } catch (e) { die(`could not read signing key ${keyPath}: ${(e as Error).message}`); }\n\n// When connected to a workspace, mirror the PEP-authorized receipts to the portal\n// through a durable outbox. The proxy is long-running, so the exporter's own timer\n// delivers; a final flush runs on shutdown.\nconst connection = loadMcpConnection(keyPath as string);\nlet exporter: CloudExporter | undefined;\nif (connection) {\n try { exporter = openExporter(keyPath as string, connection); }\n catch (e) { die(`durable Cloud outbox could not open: ${(e as Error).message}`); }\n}\n\n// Spawn the upstream server and correlate its responses by id.\nconst child = spawn(upstreamCmd[0], upstreamCmd.slice(1), { stdio: [\"pipe\", \"pipe\", \"inherit\"] });\nchild.on(\"error\", (e) => die(`could not start the upstream server: ${e.message}`));\nconst shutdown = (code: number) => {\n if (!exporter) process.exit(code);\n void exporter.flush().finally(() => { exporter!.stop(); process.exit(code); });\n};\nchild.on(\"exit\", (code) => shutdown(code ?? 0));\nprocess.on(\"SIGINT\", () => shutdown(0));\nprocess.on(\"SIGTERM\", () => shutdown(0));\n\nconst pending = new Map<string, (m: JsonRpcMessage) => void>();\nconst key = (id: unknown) => JSON.stringify(id ?? null);\n\ncreateInterface({ input: child.stdout }).on(\"line\", (line) => {\n if (!line.trim()) return;\n let msg: JsonRpcMessage;\n try { msg = JSON.parse(line); } catch { return; }\n const waiter = msg.id !== undefined ? pending.get(key(msg.id)) : undefined;\n if (waiter) { pending.delete(key(msg.id)); waiter(msg); return; }\n process.stdout.write(line + \"\\n\"); // upstream-initiated notification → pass to the client\n});\n\nconst upstream: McpUpstream = {\n call(message) {\n return new Promise((resolve) => {\n if (message.id === undefined || message.id === null) { // notification: fire and forget\n child.stdin.write(JSON.stringify(message) + \"\\n\");\n resolve({ jsonrpc: \"2.0\", result: null });\n return;\n }\n pending.set(key(message.id), resolve);\n child.stdin.write(JSON.stringify(message) + \"\\n\");\n });\n },\n};\n\nconst proxy = createMcpProxy({\n policy, principal: { subject: `client:${principal}`, issuer: \"scopebond:mcp-proxy\" }, server: server as string,\n attesterKeyPem, upstream,\n onReceipt: (r: SignedReceipt) => {\n if (receiptsPath) { try { appendFileSync(receiptsPath, JSON.stringify(r) + \"\\n\"); } catch { /* best effort */ } }\n exporter?.enqueue(r); // mirror to the workspace when connected\n },\n});\n\ncreateInterface({ input: process.stdin }).on(\"line\", async (line) => {\n if (!line.trim()) return;\n let msg: JsonRpcMessage;\n try { msg = JSON.parse(line); } catch { return; }\n try {\n const response = await proxy.handle(msg);\n if (msg.id !== undefined && msg.id !== null) process.stdout.write(JSON.stringify(response) + \"\\n\");\n } catch (e) {\n // Fail closed: a proxy error becomes a JSON-RPC error, never a silent forward.\n if (msg.id !== undefined && msg.id !== null) {\n process.stdout.write(JSON.stringify({ jsonrpc: \"2.0\", id: msg.id, error: { code: -32000, message: `scopebond-mcp failed closed: ${(e as Error).message}` } }) + \"\\n\");\n }\n }\n});\n}\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// scopebond-mcp — a stdio Model Context Protocol proxy. It sits between an MCP\n// client and an upstream server: the client speaks MCP to this process, and this\n// process speaks MCP to the upstream command (everything after `--`). Every\n// tools/call is checked against policy before it is forwarded; a denial returns a\n// JSON-RPC error and is never sent upstream.\n//\n// scopebond-mcp init --server <id> scaffold a key + starter policy\n// scopebond-mcp --server <id> [--policy p.json] [--key k.pem] [--principal sub] \\\n// [--receipts log.jsonl] [--typed typed.json] [--dispatch-dir dir] [--delegation id] -- <upstream-command...>\n//\n// --dispatch-dir turns on the dispatch boundary (off by default): the directory holds dispatch.json\n// (require_approval, approver_keys, budgets), an approvals/ inbox and the shared dispatch.db. Each\n// tools/call that policy allows then needs its single-use approval, its delegated scope and its\n// action-budget slot before it is forwarded; without them it is denied and never sent upstream.\n// --delegation (or SCOPEBOND_DELEGATION) runs the proxy under a delegated child scope.\n//\n// --typed turns on the typed adapter (off by default): a JSON file with\n// { \"mode\": \"monitor\" | \"enforce\", \"manifest\": { \"hash\": \"sha256:…\", \"tools\": { \"<tool>\":\n// { \"operation_class\": \"read_only\" | \"mutation\", \"resources\": [{ \"arg\": \"repository\", \"kind\": \"repository\" }] } } },\n// \"requireResourceBinding\": true, \"approvedResources\": { \"repository\": [\"owner/name\"] } }\n// Under \"enforce\" a tool the pinned manifest does not list, a server whose tool list no longer\n// matches its hash, and (with requireResourceBinding) a call whose resources cannot be bound\n// from the dispatched arguments or are not approved, are denied before dispatch. When the\n// hook is installed and enrolled for observations (SCOPEBOND_HOOK_DIR or --observations-dir),\n// tool_intent and tool_outcome observations are queued in its outbox.\n//\n// Env: SCOPEBOND_MCP_POLICY, SCOPEBOND_MCP_KEY, SCOPEBOND_MCP_SERVER,\n// SCOPEBOND_MCP_PRINCIPAL, SCOPEBOND_MCP_RECEIPTS.\n// MCP stdio is newline-delimited JSON-RPC. Fail closed: any setup error exits 1.\n\nimport { readFileSync, appendFileSync } from \"node:fs\";\nimport { spawn } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport type { SignedReceipt, CloudExporter } from \"@scopebond/gateway\";\nimport { createMcpProxy } from \"./proxy.js\";\nimport { requestBinderFromHex, type ObservationSink, type RequestBinder, type TypedAdapterConfig } from \"./typed.js\";\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { randomBytes } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport type { JsonRpcMessage, McpUpstream } from \"./proxy.js\";\nimport { scaffold } from \"./init.js\";\nimport { openApprovalBinder, openDispatchGuard } from \"@scopebond/gateway/node\";\nimport { connectCloud, loadMcpConnection, connectionFileFor, openExporter } from \"./cloud.js\";\n\nfunction arg(name: string, fallback?: string): string | undefined {\n const i = process.argv.indexOf(name);\n return i >= 0 ? process.argv[i + 1] : fallback;\n}\ninterface RequestBinderHolder { binding: RequestBinder }\nfunction die(message: string): never { process.stderr.write(`scopebond-mcp: ${message}\\n`); process.exit(1); }\n\n// `init`: scaffold a key + a starter policy for one server, then exit.\nif (process.argv[2] === \"init\") {\n const server = arg(\"--server\", process.env.SCOPEBOND_MCP_SERVER);\n if (!server) die(\"usage: scopebond-mcp init --server <upstream-server-id>\");\n const { keyFile, policyFile } = scaffold(server as string, { force: process.argv.includes(\"--force\") });\n console.log(`Scopebond MCP proxy enrolled for server \"${server}\":`);\n console.log(` key ${keyFile}`);\n console.log(` policy ${policyFile} (starter — edit the tool bounds)`);\n console.log(\"\");\n console.log(\"Point your MCP client at the proxy:\");\n console.log(` scopebond-mcp --server ${server} -- <your real MCP server command>`);\n process.exit(0);\n}\n\n// `connect`: enroll the proxy's key with a workspace and store a scoped credential.\nif (process.argv[2] === \"connect\") {\n const url = process.argv[3];\n const bundleFile = process.argv[4];\n const keyPath = arg(\"--key\", process.env.SCOPEBOND_MCP_KEY ?? \"scopebond-agent.key\")!;\n if (!url || !bundleFile) die(\"usage: scopebond-mcp connect <workspace-url> <enrollment-bundle.json> [--key k.pem]\");\n let bundle;\n try { bundle = JSON.parse(readFileSync(bundleFile, \"utf8\")); }\n catch (e) { die(`could not read the enrollment bundle ${bundleFile}: ${(e as Error).message}`); }\n connectCloud(keyPath, url, bundle).then((c) => {\n console.log(`Connected to ${c.url} (org ${c.organization_id} · env ${c.environment_id})`);\n console.log(` credential stored in ${connectionFileFor(keyPath)} — a secret, do not commit`);\n console.log(\"Run the proxy as usual; every governed tool call is now mirrored to your workspace.\");\n process.exit(0);\n }).catch((e) => die(`connect failed: ${(e as Error).message}`));\n} else {\n\nconst dashDash = process.argv.indexOf(\"--\");\nif (dashDash < 0 || dashDash === process.argv.length - 1) die(\"provide the upstream command after `--`\");\nconst upstreamCmd = process.argv.slice(dashDash + 1);\n\nconst server = arg(\"--server\", process.env.SCOPEBOND_MCP_SERVER);\nconst policyPath = arg(\"--policy\", process.env.SCOPEBOND_MCP_POLICY ?? \"scopebond.policy.json\");\nconst keyPath = arg(\"--key\", process.env.SCOPEBOND_MCP_KEY ?? \"scopebond-agent.key\");\nconst principal = arg(\"--principal\", process.env.SCOPEBOND_MCP_PRINCIPAL ?? \"mcp-client\");\nconst receiptsPath = arg(\"--receipts\", process.env.SCOPEBOND_MCP_RECEIPTS);\nif (!server) die(\"--server (the upstream server id) is required\");\n\nlet policy: unknown;\nlet attesterKeyPem: string;\ntry { policy = JSON.parse(readFileSync(policyPath as string, \"utf8\")); } catch (e) { die(`could not read policy ${policyPath}: ${(e as Error).message}`); }\ntry { attesterKeyPem = readFileSync(keyPath as string, \"utf8\"); } catch (e) { die(`could not read signing key ${keyPath}: ${(e as Error).message}`); }\n\n// When connected to a workspace, mirror the PEP-authorized receipts to the portal\n// through a durable outbox. The proxy is long-running, so the exporter's own timer\n// delivers; a final flush runs on shutdown.\nconst connection = loadMcpConnection(keyPath as string);\nlet exporter: CloudExporter | undefined;\nif (connection) {\n try { exporter = openExporter(keyPath as string, connection); }\n catch (e) { die(`durable Cloud outbox could not open: ${(e as Error).message}`); }\n}\n\n// Spawn the upstream server and correlate its responses by id.\nconst child = spawn(upstreamCmd[0], upstreamCmd.slice(1), { stdio: [\"pipe\", \"pipe\", \"inherit\"] });\nchild.on(\"error\", (e) => die(`could not start the upstream server: ${e.message}`));\nconst shutdown = (code: number) => {\n const done = (): never => process.exit(code);\n void Promise.allSettled([exporter?.flush(), observations?.flush()]).finally(() => { exporter?.stop(); try { observations?.close(); } catch { /* closing is best effort */ } done(); });\n};\nchild.on(\"exit\", (code) => shutdown(code ?? 0));\nprocess.on(\"SIGINT\", () => shutdown(0));\nprocess.on(\"SIGTERM\", () => shutdown(0));\n\nconst pending = new Map<string, (m: JsonRpcMessage) => void>();\nconst key = (id: unknown) => JSON.stringify(id ?? null);\n\ncreateInterface({ input: child.stdout }).on(\"line\", (line) => {\n if (!line.trim()) return;\n let msg: JsonRpcMessage;\n try { msg = JSON.parse(line); } catch { return; }\n const waiter = msg.id !== undefined ? pending.get(key(msg.id)) : undefined;\n if (waiter) { pending.delete(key(msg.id)); waiter(msg); return; }\n process.stdout.write(line + \"\\n\"); // upstream-initiated notification → pass to the client\n});\n\nconst upstream: McpUpstream = {\n call(message) {\n return new Promise((resolve) => {\n if (message.id === undefined || message.id === null) { // notification: fire and forget\n child.stdin.write(JSON.stringify(message) + \"\\n\");\n resolve({ jsonrpc: \"2.0\", result: null });\n return;\n }\n pending.set(key(message.id), resolve);\n child.stdin.write(JSON.stringify(message) + \"\\n\");\n });\n },\n};\n\n// The typed adapter is off unless a typed config is given. Its binding key and observation\n// outbox come from the hook when it is installed and enrolled; otherwise the key is a local\n// file beside the signing key (never uploaded) and nothing is queued.\nconst typedPath = arg(\"--typed\", process.env.SCOPEBOND_MCP_TYPED);\nlet typed: TypedAdapterConfig | undefined;\nlet observations: { flush(): Promise<unknown>; close(): void } | undefined;\nif (typedPath) {\n let raw: Record<string, unknown>;\n try { raw = JSON.parse(readFileSync(typedPath, \"utf8\")); } catch (e) { die(`could not read the typed config ${typedPath}: ${(e as Error).message}`); }\n if (raw.mode !== \"monitor\" && raw.mode !== \"enforce\") die('the typed config needs \"mode\": \"monitor\" or \"enforce\"');\n let binder: RequestBinder | undefined;\n let sink: ObservationSink | undefined;\n const hookDir = arg(\"--observations-dir\", process.env.SCOPEBOND_HOOK_DIR);\n if (hookDir) {\n try {\n const hook = await import(\"@scopebond/hook\") as unknown as { openObservations(dir: string, o?: object): { status: { state: string; reason?: string }; emitter?: ObservationSink & RequestBinderHolder & { flush(): Promise<unknown>; close(): void } } };\n const opened = hook.openObservations(resolve(hookDir), { adapterVersion: \"scopebond-mcp\" });\n if (opened.emitter) { sink = opened.emitter; binder = opened.emitter.binding; observations = opened.emitter; }\n else process.stderr.write(`scopebond-mcp: observations are not on (${opened.status.reason ?? opened.status.state}); the typed adapter runs without them\\n`);\n } catch (e) { process.stderr.write(`scopebond-mcp: the hook package is not available for observations (${(e as Error).message})\\n`); }\n }\n if (!binder) {\n const file = `${keyPath}.binding`;\n let hex = existsSync(file) ? readFileSync(file, \"utf8\").trim() : \"\";\n if (!/^[0-9a-f]{64}$/.test(hex)) { hex = randomBytes(32).toString(\"hex\"); writeFileSync(file, hex + \"\\n\", { mode: 0o600 }); }\n binder = requestBinderFromHex(hex);\n }\n typed = { ...(raw as object), mode: raw.mode, binder, ...(sink ? { sink } : {}) } as TypedAdapterConfig;\n}\n\n// The dispatch boundary is opt-in. A directory that is named but cannot be read is a setup error: it says what may be spent.\nconst dispatchDir = arg(\"--dispatch-dir\", process.env.SCOPEBOND_DISPATCH_DIR);\nconst delegationId = arg(\"--delegation\", process.env.SCOPEBOND_DELEGATION);\nlet dispatch: { guard: NonNullable<ReturnType<typeof openDispatchGuard>>; delegationId?: string; binder?: ReturnType<typeof openApprovalBinder> & object } | undefined;\nif (delegationId && !dispatchDir) die(\"--delegation needs --dispatch-dir (a delegation is checked against its shared store)\");\nif (dispatchDir) {\n try {\n const guard = openDispatchGuard(dispatchDir ?? \".\", { delegated: !!delegationId });\n if (guard) { const binder = openApprovalBinder(dispatchDir ?? \".\"); dispatch = { guard, ...(delegationId ? { delegationId } : {}), ...(binder ? { binder } : {}) }; }\n } catch (e) { die(`could not read the dispatch settings in ${dispatchDir ?? \".\"}: ${(e as Error).message}`); }\n\n}\n\nconst proxy = createMcpProxy({\n policy, principal: { subject: `client:${principal}`, issuer: \"scopebond:mcp-proxy\" }, server: server as string,\n attesterKeyPem, upstream, ...(dispatch ? { dispatch } : {}), ...(typed ? { typed, adapterVersion: \"scopebond-mcp\" } : {}),\n onReceipt: (r: SignedReceipt) => {\n if (receiptsPath) { try { appendFileSync(receiptsPath, JSON.stringify(r) + \"\\n\"); } catch { /* best effort */ } }\n exporter?.enqueue(r); // mirror to the workspace when connected\n },\n});\n\ncreateInterface({ input: process.stdin }).on(\"line\", async (line) => {\n if (!line.trim()) return;\n let msg: JsonRpcMessage;\n try { msg = JSON.parse(line); } catch { return; }\n try {\n const response = await proxy.handle(msg);\n if (msg.id !== undefined && msg.id !== null) process.stdout.write(JSON.stringify(response) + \"\\n\");\n } catch (e) {\n // Fail closed: a proxy error becomes a JSON-RPC error, never a silent forward.\n if (msg.id !== undefined && msg.id !== null) {\n process.stdout.write(JSON.stringify({ jsonrpc: \"2.0\", id: msg.id, error: { code: -32000, message: `scopebond-mcp failed closed: ${(e as Error).message}` } }) + \"\\n\");\n }\n }\n});\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export type { McpProxy, McpProxyConfig, McpUpstream, JsonRpcMessage } from "./pr
3
3
  export { starterMcpPolicy } from "./init.js";
4
4
  export { connectCloud, loadMcpConnection, openExporter, connectionFileFor } from "./cloud.js";
5
5
  export type { McpConnection } from "./cloud.js";
6
+ export { requestBinderFromHex, describeToolCall, manifestHash, intentDraft, outcomeDraft, sourceReceiptHash, REQUEST_BINDING_DOMAIN } from "./typed.js";
7
+ export type { RequestBinder, ObservationSink, ObservationDraft, PinnedManifest, PinnedTool, TypedAdapterConfig, TypedDescription, OperationClass, ExitCategory } from "./typed.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC9F,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC9F,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACxJ,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -4,4 +4,5 @@
4
4
  export { createMcpProxy, mapMcpToolCall } from "./proxy.js";
5
5
  export { starterMcpPolicy } from "./init.js";
6
6
  export { connectCloud, loadMcpConnection, openExporter, connectionFileFor } from "./cloud.js";
7
+ export { requestBinderFromHex, describeToolCall, manifestHash, intentDraft, outcomeDraft, sourceReceiptHash, REQUEST_BINDING_DOMAIN } from "./typed.js";
7
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,iFAAiF;AACjF,iFAAiF;AAEjF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC","sourcesContent":["// @scopebond/mcp — the Model Context Protocol proxy connector. The library\n// surface is the deterministic mapper and the transport-agnostic proxy core; the\n// `scopebond-mcp` binary provides the stdio transport to a real upstream server.\n\nexport { createMcpProxy, mapMcpToolCall } from \"./proxy.js\";\nexport type { McpProxy, McpProxyConfig, McpUpstream, JsonRpcMessage } from \"./proxy.js\";\nexport { starterMcpPolicy } from \"./init.js\";\nexport { connectCloud, loadMcpConnection, openExporter, connectionFileFor } from \"./cloud.js\";\nexport type { McpConnection } from \"./cloud.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,iFAAiF;AACjF,iFAAiF;AAEjF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9F,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC","sourcesContent":["// @scopebond/mcp — the Model Context Protocol proxy connector. The library\n// surface is the deterministic mapper and the transport-agnostic proxy core; the\n// `scopebond-mcp` binary provides the stdio transport to a real upstream server.\n\nexport { createMcpProxy, mapMcpToolCall } from \"./proxy.js\";\nexport type { McpProxy, McpProxyConfig, McpUpstream, JsonRpcMessage } from \"./proxy.js\";\nexport { starterMcpPolicy } from \"./init.js\";\nexport { connectCloud, loadMcpConnection, openExporter, connectionFileFor } from \"./cloud.js\";\nexport type { McpConnection } from \"./cloud.js\";\nexport { requestBinderFromHex, describeToolCall, manifestHash, intentDraft, outcomeDraft, sourceReceiptHash, REQUEST_BINDING_DOMAIN } from \"./typed.js\";\nexport type { RequestBinder, ObservationSink, ObservationDraft, PinnedManifest, PinnedTool, TypedAdapterConfig, TypedDescription, OperationClass, ExitCategory } from \"./typed.js\";\n"]}
package/dist/proxy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { SignedReceipt } from "@scopebond/gateway";
1
+ import type { SignedReceipt, DispatchGuard } from "@scopebond/gateway";
2
+ import { type TypedAdapterConfig } from "./typed.js";
2
3
  export interface JsonRpcMessage {
3
4
  jsonrpc?: string;
4
5
  id?: string | number | null;
@@ -51,6 +52,27 @@ export interface McpProxyConfig {
51
52
  * rate_limit, spend_limit and sequence clauses see the real history rather than
52
53
  * an empty one. */
53
54
  history?: CountableCall[];
55
+ /** The typed adapter: binds the server, tool, pinned manifest revision, operation class and
56
+ * resources from the dispatched request, decides before dispatch under `enforce`, and emits
57
+ * tool_intent and tool_outcome observations to its sink. Absent, the proxy behaves as before. */
58
+ typed?: TypedAdapterConfig;
59
+ /** Recorded as the adapter version on tool_outcome observations. */
60
+ adapterVersion?: string;
61
+ /** The dispatch boundary: single-use approvals, the session's delegated scope and per-agent action
62
+ * budgets, decided once per `tools/call` after policy allows it and immediately before it is
63
+ * forwarded. A denial is never forwarded and spends nothing. Absent, the proxy behaves as before. */
64
+ dispatch?: {
65
+ guard: DispatchGuard; /** The delegation this session runs under, if it is a delegated child. */
66
+ delegationId?: string;
67
+ /** Adds the approval hash and target id the guard uses to a typed operation, so a consumed approval can be correlated to its intent. */
68
+ binder?: {
69
+ bindDispatched<T extends Record<string, unknown>>(operation: T, intent: {
70
+ action_type: string;
71
+ target: string;
72
+ request: unknown;
73
+ }): T;
74
+ };
75
+ };
54
76
  }
55
77
  export interface McpProxy {
56
78
  handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,oBAAoB,CAAC;AAIlE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID;6BAC6B;AAC7B,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAKpJ;AAED;mCACmC;AACnC,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD;AAED;uFACuF;AACvF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnG,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,WAAW,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;wBAGoB;IACpB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1D;AAED;uDACuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAiC/D"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAY,aAAa,EAAoB,MAAM,oBAAoB,CAAC;AAGnG,OAAO,EAAgF,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnI,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID;6BAC6B;AAC7B,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAKpJ;AAED;mCACmC;AACnC,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD;AAED;uFACuF;AACvF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnG,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,WAAW,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;wBAGoB;IACpB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B;;sGAEkG;IAClG,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;0GAEsG;IACtG,QAAQ,CAAC,EAAE;QACT,KAAK,EAAE,aAAa,CAAC,CAAC,0EAA0E;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QACvH,wIAAwI;QACxI,MAAM,CAAC,EAAE;YAAE,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE;gBAAE,WAAW,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAC;gBAAC,OAAO,EAAE,OAAO,CAAA;aAAE,GAAG,CAAC,CAAA;SAAE,CAAC;KACpJ,CAAC;CACH;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1D;AAED;uDACuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CA8H/D"}
package/dist/proxy.js CHANGED
@@ -7,8 +7,10 @@
7
7
  // receipts are pep_authorized (§15) — the identity is the configured principal.
8
8
  import { violates } from "@scopebond/verify";
9
9
  import { buildPepReceipt, attesterFromPrivateKeyPem } from "@scopebond/gateway";
10
+ import { requestHash } from "@scopebond/gateway";
10
11
  import { canonical } from "@scopebond/policy-schema/canonical";
11
12
  import { createHash } from "node:crypto";
13
+ import { describeToolCall, intentDraft, manifestHash, outcomeDraft } from "./typed.js";
12
14
  const digest = (value) => "sha256:" + createHash("sha256").update(canonical(value)).digest("hex");
13
15
  /** Map an MCP `tools/call` to a normalized `mcp.tool.call` action. Arguments are
14
16
  * digested, never stored. */
@@ -25,27 +27,138 @@ export function createMcpProxy(config) {
25
27
  // Session history of authorized calls. A forwarded call actually runs, so it
26
28
  // counts as executed for windowed clauses (rate_limit, sequence, spend_limit).
27
29
  const history = [...(config.history ?? [])];
30
+ const typed = config.typed;
31
+ const nowMs = () => (config.now ? Date.parse(config.now()) : Date.now());
32
+ // The live tool list is read from the upstream itself, and again after the recheck interval,
33
+ // so a manifest pinned to one revision is not trusted for a server that has since changed.
34
+ let verifiedAt = -Infinity;
35
+ let verified = false;
36
+ let liveHash;
37
+ let probe = 0;
38
+ const readToolList = async () => {
39
+ const tools = [];
40
+ let cursor;
41
+ for (let page = 0; page < 20; page++) {
42
+ const response = await config.upstream.call({ jsonrpc: "2.0", id: `scopebond-tools-${++probe}`, method: "tools/list", ...(cursor ? { params: { cursor } } : {}) });
43
+ const result = response?.result;
44
+ if (!result || !Array.isArray(result.tools))
45
+ return undefined;
46
+ tools.push(...result.tools);
47
+ if (typeof result.nextCursor !== "string" || result.nextCursor === "")
48
+ return manifestHash(tools);
49
+ cursor = result.nextCursor;
50
+ }
51
+ return undefined;
52
+ };
53
+ const manifestVerified = async () => {
54
+ if (!typed?.manifest)
55
+ return false;
56
+ const recheck = typed.manifestRecheckMs ?? 60_000;
57
+ if (nowMs() - verifiedAt < recheck)
58
+ return verified;
59
+ try {
60
+ liveHash = await readToolList();
61
+ }
62
+ catch {
63
+ liveHash = undefined;
64
+ }
65
+ verified = liveHash !== undefined && liveHash === typed.manifest.hash;
66
+ verifiedAt = nowMs();
67
+ return verified;
68
+ };
28
69
  return {
29
70
  async handle(message) {
71
+ if (message?.method === "tools/list" && typed?.manifest) {
72
+ // A client listing tools shows the live revision: learn it from the answer it gets.
73
+ const response = await config.upstream.call(message);
74
+ const result = response?.result;
75
+ if (!message.params?.cursor && result && Array.isArray(result.tools) && !result.nextCursor) {
76
+ liveHash = manifestHash(result.tools);
77
+ verified = liveHash === typed.manifest.hash;
78
+ verifiedAt = nowMs();
79
+ }
80
+ return response;
81
+ }
30
82
  if (message?.method !== "tools/call")
31
83
  return config.upstream.call(message);
32
- const intent = mapMcpToolCall(config.server, message.params);
84
+ // Everything below is decided on, digested and forwarded from one private copy of the
85
+ // request, so what the binding covers is exactly what reaches the upstream.
86
+ const dispatched = JSON.parse(JSON.stringify(message));
87
+ const intent = mapMcpToolCall(config.server, dispatched.params);
33
88
  const timestamp = config.now?.() ?? new Date().toISOString();
34
89
  const claimed = { intent, executed: true, timestamp, intent_hash: digest(intent).slice(7) };
35
90
  const verdict = violates(config.policy, history, claimed, { at: timestamp });
36
- const decision = verdict.violated ? "deny" : "allow";
91
+ // The adapter's own decision: unknown tools, revisions and unbound resources.
92
+ const description = typed ? describeToolCall(typed, config.server, dispatched, await manifestVerified()) : undefined;
93
+ const adapterDeny = typed?.mode === "enforce" && description !== undefined && !description.allow;
94
+ let decision = verdict.violated || adapterDeny ? "deny" : "allow";
95
+ // The boundary, for a call policy has allowed. The exact request forwarded below is what an approval is bound to.
96
+ let boundary;
97
+ const guardTarget = `${config.server}/${String(intent.params.tool)}`;
98
+ const guardRequest = { server: config.server, method: "tools/call", params: dispatched.params ?? {} };
99
+ if (decision === "allow" && config.dispatch) {
100
+ const target = guardTarget;
101
+ const request = guardRequest;
102
+ // One transport request keeps one group, so a retry of it is not a second dispatch; a new invocation is.
103
+ const group = `mcp:${requestHash({ id: dispatched.id ?? null, request, session: config.dispatch.delegationId ?? null })}`;
104
+ try {
105
+ boundary = await config.dispatch.guard.authorize({
106
+ actor: config.principal.subject, action_group: dispatched.id === undefined || dispatched.id === null ? `mcp:${globalThis.crypto.randomUUID()}` : group,
107
+ policy_digest: digest(config.policy).slice(7), intents: [{ action_type: "mcp.tool.call", target, request }],
108
+ ...(config.dispatch.delegationId ? { delegation_id: config.dispatch.delegationId } : {}),
109
+ });
110
+ }
111
+ catch (error) {
112
+ boundary = { allow: false, reason: "counter_unavailable", detail: error.message, consumed_approvals: [], budgets: [] };
113
+ }
114
+ if (!boundary.allow)
115
+ decision = "deny";
116
+ }
37
117
  const receipt = await buildPepReceipt({ intent, policy: config.policy, principal: config.principal, realtimeResult: decision, now: config.now }, attester);
38
118
  await config.onReceipt?.(receipt);
119
+ // The intent is recorded before dispatch, whatever the decision. Emission can never
120
+ // change the decision or fail the call.
121
+ const startedAt = nowMs();
122
+ const plainOperation = description?.operation ?? null;
123
+ const operation = plainOperation && config.dispatch?.binder
124
+ ? config.dispatch.binder.bindDispatched(plainOperation, { action_type: "mcp.tool.call", target: guardTarget, request: guardRequest }) : plainOperation;
125
+ if (typed?.sink && operation) {
126
+ try {
127
+ typed.sink.emit(intentDraft(operation, startedAt, receipt));
128
+ }
129
+ catch { /* observations are best effort */ }
130
+ }
39
131
  if (decision === "deny") {
132
+ const why = verdict.violated ? (verdict.explanation || "out of policy")
133
+ : boundary && !boundary.allow ? `dispatch boundary: ${boundary.reason}${boundary.detail ? ` (${boundary.detail})` : ""}`
134
+ : `the typed adapter could not bind it (${description.reasons.join("; ")})`;
40
135
  return {
41
136
  jsonrpc: "2.0",
42
137
  id: message.id ?? null,
43
- error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${verdict.explanation || "out of policy"}` },
138
+ error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${why}` },
44
139
  };
45
140
  }
46
141
  // Authorized and about to be forwarded: record it so it counts toward the window.
47
142
  history.push(claimed);
48
- return config.upstream.call(message);
143
+ let exit = "unknown";
144
+ try {
145
+ const response = await config.upstream.call(dispatched);
146
+ const result = response?.result;
147
+ exit = response?.error !== undefined || result?.isError === true ? "error" : "ok";
148
+ return response;
149
+ }
150
+ catch (error) {
151
+ exit = "error";
152
+ throw error;
153
+ }
154
+ finally {
155
+ if (typed?.sink && operation) {
156
+ try {
157
+ typed.sink.emit(outcomeDraft(operation, exit, config.adapterVersion ?? "unknown", nowMs(), startedAt, receipt));
158
+ }
159
+ catch { /* best effort */ }
160
+ }
161
+ }
49
162
  },
50
163
  };
51
164
  }
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["// The MCP proxy core: sit in front of an upstream MCP server, check each\n// `tools/call` against policy before forwarding it, and emit a signed\n// PEP-authorized receipt. A denied call is never forwarded. Deterministic and\n// transport-agnostic — the upstream is injected, so the decision path is\n// conformance-tested directly; the `scopebond-mcp` CLI provides the real stdio\n// transport. The proxy decides the caller's request (no agent signature), so its\n// receipts are pep_authorized (§15) — the identity is the configured principal.\n\nimport { violates } from \"@scopebond/verify\";\nimport { buildPepReceipt, attesterFromPrivateKeyPem } from \"@scopebond/gateway\";\nimport type { SignedReceipt, Attester } from \"@scopebond/gateway\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { createHash } from \"node:crypto\";\n\nexport interface JsonRpcMessage {\n jsonrpc?: string;\n id?: string | number | null;\n method?: string;\n params?: Record<string, unknown>;\n result?: unknown;\n error?: unknown;\n}\n\nconst digest = (value: unknown): string => \"sha256:\" + createHash(\"sha256\").update(canonical(value as never)).digest(\"hex\");\n\n/** Map an MCP `tools/call` to a normalized `mcp.tool.call` action. Arguments are\n * digested, never stored. */\nexport function mapMcpToolCall(server: string, params: Record<string, unknown> | undefined): { action_type: string; params: Record<string, unknown> } {\n return {\n action_type: \"mcp.tool.call\",\n params: { server, tool: String(params?.name ?? \"\"), args_digest: digest(params?.arguments ?? {}) },\n };\n}\n\n/** A pre-authenticated forwarder to the upstream MCP server. Tests inject it; the\n * CLI implements it over stdio. */\nexport interface McpUpstream {\n call(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** The minimal receipt shape a windowed clause needs to count a prior authorized\n * call. The proxy keeps these for the session so rate_limit/sequence clauses work. */\nexport interface CountableCall {\n intent: { action_type: string; params?: Record<string, unknown>; asset?: string; amount?: number };\n executed: boolean;\n timestamp: string;\n intent_hash: string;\n}\n\nexport interface McpProxyConfig {\n policy: unknown;\n /** The validated caller identity recorded on every receipt. */\n principal: { subject: string; issuer: string };\n /** The upstream server id, used as the action's `server` parameter. */\n server: string;\n /** PEM of the key that signs the PEP receipts (the enrolled proxy key). */\n attesterKeyPem: string;\n upstream: McpUpstream;\n now?: () => string;\n /** Sink for each emitted receipt (e.g. a local log / exporter). */\n onReceipt?: (receipt: SignedReceipt) => void | Promise<void>;\n /** Prior authorized calls to seed the window with (e.g. loaded from a durable log\n * at startup). The proxy also accumulates in-session calls on top of these so\n * rate_limit, spend_limit and sequence clauses see the real history rather than\n * an empty one. */\n history?: CountableCall[];\n}\n\nexport interface McpProxy {\n handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** Build a proxy handler. Every `tools/call` is decided against policy; anything\n * else is passed through to the upstream unchanged. */\nexport function createMcpProxy(config: McpProxyConfig): McpProxy {\n const attester: Attester = attesterFromPrivateKeyPem(config.attesterKeyPem);\n // Session history of authorized calls. A forwarded call actually runs, so it\n // counts as executed for windowed clauses (rate_limit, sequence, spend_limit).\n const history: CountableCall[] = [...(config.history ?? [])];\n return {\n async handle(message: JsonRpcMessage): Promise<JsonRpcMessage> {\n if (message?.method !== \"tools/call\") return config.upstream.call(message);\n\n const intent = mapMcpToolCall(config.server, message.params);\n const timestamp = config.now?.() ?? new Date().toISOString();\n const claimed = { intent, executed: true, timestamp, intent_hash: digest(intent).slice(7) };\n const verdict = violates(config.policy as never, history as never, claimed as never, { at: timestamp });\n const decision = verdict.violated ? \"deny\" : \"allow\";\n\n const receipt = await buildPepReceipt(\n { intent, policy: config.policy as never, principal: config.principal, realtimeResult: decision, now: config.now },\n attester,\n );\n await config.onReceipt?.(receipt);\n\n if (decision === \"deny\") {\n return {\n jsonrpc: \"2.0\",\n id: message.id ?? null,\n error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${verdict.explanation || \"out of policy\"}` },\n };\n }\n // Authorized and about to be forwarded: record it so it counts toward the window.\n history.push(claimed);\n return config.upstream.call(message);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["// The MCP proxy core: sit in front of an upstream MCP server, check each\n// `tools/call` against policy before forwarding it, and emit a signed\n// PEP-authorized receipt. A denied call is never forwarded. Deterministic and\n// transport-agnostic — the upstream is injected, so the decision path is\n// conformance-tested directly; the `scopebond-mcp` CLI provides the real stdio\n// transport. The proxy decides the caller's request (no agent signature), so its\n// receipts are pep_authorized (§15) — the identity is the configured principal.\n\nimport { violates } from \"@scopebond/verify\";\nimport { buildPepReceipt, attesterFromPrivateKeyPem } from \"@scopebond/gateway\";\nimport { requestHash } from \"@scopebond/gateway\";\nimport type { SignedReceipt, Attester, DispatchGuard, DispatchDecision } from \"@scopebond/gateway\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { createHash } from \"node:crypto\";\nimport { describeToolCall, intentDraft, manifestHash, outcomeDraft, type ExitCategory, type TypedAdapterConfig } from \"./typed.js\";\n\nexport interface JsonRpcMessage {\n jsonrpc?: string;\n id?: string | number | null;\n method?: string;\n params?: Record<string, unknown>;\n result?: unknown;\n error?: unknown;\n}\n\nconst digest = (value: unknown): string => \"sha256:\" + createHash(\"sha256\").update(canonical(value as never)).digest(\"hex\");\n\n/** Map an MCP `tools/call` to a normalized `mcp.tool.call` action. Arguments are\n * digested, never stored. */\nexport function mapMcpToolCall(server: string, params: Record<string, unknown> | undefined): { action_type: string; params: Record<string, unknown> } {\n return {\n action_type: \"mcp.tool.call\",\n params: { server, tool: String(params?.name ?? \"\"), args_digest: digest(params?.arguments ?? {}) },\n };\n}\n\n/** A pre-authenticated forwarder to the upstream MCP server. Tests inject it; the\n * CLI implements it over stdio. */\nexport interface McpUpstream {\n call(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** The minimal receipt shape a windowed clause needs to count a prior authorized\n * call. The proxy keeps these for the session so rate_limit/sequence clauses work. */\nexport interface CountableCall {\n intent: { action_type: string; params?: Record<string, unknown>; asset?: string; amount?: number };\n executed: boolean;\n timestamp: string;\n intent_hash: string;\n}\n\nexport interface McpProxyConfig {\n policy: unknown;\n /** The validated caller identity recorded on every receipt. */\n principal: { subject: string; issuer: string };\n /** The upstream server id, used as the action's `server` parameter. */\n server: string;\n /** PEM of the key that signs the PEP receipts (the enrolled proxy key). */\n attesterKeyPem: string;\n upstream: McpUpstream;\n now?: () => string;\n /** Sink for each emitted receipt (e.g. a local log / exporter). */\n onReceipt?: (receipt: SignedReceipt) => void | Promise<void>;\n /** Prior authorized calls to seed the window with (e.g. loaded from a durable log\n * at startup). The proxy also accumulates in-session calls on top of these so\n * rate_limit, spend_limit and sequence clauses see the real history rather than\n * an empty one. */\n history?: CountableCall[];\n /** The typed adapter: binds the server, tool, pinned manifest revision, operation class and\n * resources from the dispatched request, decides before dispatch under `enforce`, and emits\n * tool_intent and tool_outcome observations to its sink. Absent, the proxy behaves as before. */\n typed?: TypedAdapterConfig;\n /** Recorded as the adapter version on tool_outcome observations. */\n adapterVersion?: string;\n /** The dispatch boundary: single-use approvals, the session's delegated scope and per-agent action\n * budgets, decided once per `tools/call` after policy allows it and immediately before it is\n * forwarded. A denial is never forwarded and spends nothing. Absent, the proxy behaves as before. */\n dispatch?: {\n guard: DispatchGuard; /** The delegation this session runs under, if it is a delegated child. */ delegationId?: string;\n /** Adds the approval hash and target id the guard uses to a typed operation, so a consumed approval can be correlated to its intent. */\n binder?: { bindDispatched<T extends Record<string, unknown>>(operation: T, intent: { action_type: string; target: string; request: unknown }): T };\n };\n}\n\nexport interface McpProxy {\n handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** Build a proxy handler. Every `tools/call` is decided against policy; anything\n * else is passed through to the upstream unchanged. */\nexport function createMcpProxy(config: McpProxyConfig): McpProxy {\n const attester: Attester = attesterFromPrivateKeyPem(config.attesterKeyPem);\n // Session history of authorized calls. A forwarded call actually runs, so it\n // counts as executed for windowed clauses (rate_limit, sequence, spend_limit).\n const history: CountableCall[] = [...(config.history ?? [])];\n const typed = config.typed;\n const nowMs = (): number => (config.now ? Date.parse(config.now()) : Date.now());\n\n // The live tool list is read from the upstream itself, and again after the recheck interval,\n // so a manifest pinned to one revision is not trusted for a server that has since changed.\n let verifiedAt = -Infinity;\n let verified = false;\n let liveHash: string | undefined;\n let probe = 0;\n const readToolList = async (): Promise<string | undefined> => {\n const tools: unknown[] = [];\n let cursor: string | undefined;\n for (let page = 0; page < 20; page++) {\n const response = await config.upstream.call({ jsonrpc: \"2.0\", id: `scopebond-tools-${++probe}`, method: \"tools/list\", ...(cursor ? { params: { cursor } } : {}) });\n const result = response?.result as { tools?: unknown; nextCursor?: unknown } | undefined;\n if (!result || !Array.isArray(result.tools)) return undefined;\n tools.push(...result.tools);\n if (typeof result.nextCursor !== \"string\" || result.nextCursor === \"\") return manifestHash(tools);\n cursor = result.nextCursor;\n }\n return undefined;\n };\n const manifestVerified = async (): Promise<boolean> => {\n if (!typed?.manifest) return false;\n const recheck = typed.manifestRecheckMs ?? 60_000;\n if (nowMs() - verifiedAt < recheck) return verified;\n try { liveHash = await readToolList(); } catch { liveHash = undefined; }\n verified = liveHash !== undefined && liveHash === typed.manifest.hash;\n verifiedAt = nowMs();\n return verified;\n };\n\n return {\n async handle(message: JsonRpcMessage): Promise<JsonRpcMessage> {\n if (message?.method === \"tools/list\" && typed?.manifest) {\n // A client listing tools shows the live revision: learn it from the answer it gets.\n const response = await config.upstream.call(message);\n const result = response?.result as { tools?: unknown; nextCursor?: unknown } | undefined;\n if (!message.params?.cursor && result && Array.isArray(result.tools) && !result.nextCursor) {\n liveHash = manifestHash(result.tools);\n verified = liveHash === typed.manifest.hash;\n verifiedAt = nowMs();\n }\n return response;\n }\n if (message?.method !== \"tools/call\") return config.upstream.call(message);\n\n // Everything below is decided on, digested and forwarded from one private copy of the\n // request, so what the binding covers is exactly what reaches the upstream.\n const dispatched = JSON.parse(JSON.stringify(message)) as JsonRpcMessage;\n const intent = mapMcpToolCall(config.server, dispatched.params);\n const timestamp = config.now?.() ?? new Date().toISOString();\n const claimed = { intent, executed: true, timestamp, intent_hash: digest(intent).slice(7) };\n const verdict = violates(config.policy as never, history as never, claimed as never, { at: timestamp });\n\n // The adapter's own decision: unknown tools, revisions and unbound resources.\n const description = typed ? describeToolCall(typed, config.server, dispatched, await manifestVerified()) : undefined;\n const adapterDeny = typed?.mode === \"enforce\" && description !== undefined && !description.allow;\n let decision: \"allow\" | \"deny\" = verdict.violated || adapterDeny ? \"deny\" : \"allow\";\n\n // The boundary, for a call policy has allowed. The exact request forwarded below is what an approval is bound to.\n let boundary: DispatchDecision | undefined;\n const guardTarget = `${config.server}/${String(intent.params.tool)}`;\n const guardRequest = { server: config.server, method: \"tools/call\", params: dispatched.params ?? {} };\n if (decision === \"allow\" && config.dispatch) {\n const target = guardTarget;\n const request = guardRequest;\n // One transport request keeps one group, so a retry of it is not a second dispatch; a new invocation is.\n const group = `mcp:${requestHash({ id: dispatched.id ?? null, request, session: config.dispatch.delegationId ?? null })}`;\n try {\n boundary = await config.dispatch.guard.authorize({\n actor: config.principal.subject, action_group: dispatched.id === undefined || dispatched.id === null ? `mcp:${globalThis.crypto.randomUUID()}` : group,\n policy_digest: digest(config.policy).slice(7), intents: [{ action_type: \"mcp.tool.call\", target, request }],\n ...(config.dispatch.delegationId ? { delegation_id: config.dispatch.delegationId } : {}),\n });\n } catch (error) {\n boundary = { allow: false, reason: \"counter_unavailable\", detail: (error as Error).message, consumed_approvals: [], budgets: [] };\n }\n if (!boundary.allow) decision = \"deny\";\n }\n\n const receipt = await buildPepReceipt(\n { intent, policy: config.policy as never, principal: config.principal, realtimeResult: decision, now: config.now },\n attester,\n );\n await config.onReceipt?.(receipt);\n\n // The intent is recorded before dispatch, whatever the decision. Emission can never\n // change the decision or fail the call.\n const startedAt = nowMs();\n const plainOperation = description?.operation ?? null;\n const operation = plainOperation && config.dispatch?.binder\n ? config.dispatch.binder.bindDispatched(plainOperation as Record<string, unknown>, { action_type: \"mcp.tool.call\", target: guardTarget, request: guardRequest }) : plainOperation;\n if (typed?.sink && operation) { try { typed.sink.emit(intentDraft(operation, startedAt, receipt)); } catch { /* observations are best effort */ } }\n\n if (decision === \"deny\") {\n const why = verdict.violated ? (verdict.explanation || \"out of policy\")\n : boundary && !boundary.allow ? `dispatch boundary: ${boundary.reason}${boundary.detail ? ` (${boundary.detail})` : \"\"}`\n : `the typed adapter could not bind it (${description!.reasons.join(\"; \")})`;\n return {\n jsonrpc: \"2.0\",\n id: message.id ?? null,\n error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${why}` },\n };\n }\n // Authorized and about to be forwarded: record it so it counts toward the window.\n history.push(claimed);\n let exit: ExitCategory = \"unknown\";\n try {\n const response = await config.upstream.call(dispatched);\n const result = response?.result as { isError?: unknown } | undefined;\n exit = response?.error !== undefined || result?.isError === true ? \"error\" : \"ok\";\n return response;\n } catch (error) {\n exit = \"error\";\n throw error;\n } finally {\n if (typed?.sink && operation) { try { typed.sink.emit(outcomeDraft(operation, exit, config.adapterVersion ?? \"unknown\", nowMs(), startedAt, receipt)); } catch { /* best effort */ } }\n }\n },\n };\n}\n"]}
@@ -0,0 +1,88 @@
1
+ /** Request binding and keyed opaque ids. The hook's binding key satisfies this structurally. */
2
+ export interface RequestBinder {
3
+ /** Opaque id of the key generation; safe to upload. */
4
+ readonly generation: string;
5
+ /** HMAC-SHA-256 of `scopebond:request-binding/v1\n` + the canonical request, as 64 hex. */
6
+ requestDigest(request: unknown): string;
7
+ /** A keyed opaque id for a resource. */
8
+ resourceId(kind: string, value: string): string;
9
+ }
10
+ export declare const REQUEST_BINDING_DOMAIN = "scopebond:request-binding/v1\n";
11
+ /** The same binder the hook builds from the installation-local key: identical digests and ids
12
+ * for identical input, so an MCP proxy and a hook on one installation correlate. */
13
+ export declare function requestBinderFromHex(hex: string): RequestBinder;
14
+ /** What the adapter hands an observation outbox. The hook's `ObservationEmitter.emit` takes
15
+ * exactly this, so the hook's outbox is a sink; any other implementation works too. */
16
+ export interface ObservationDraft {
17
+ kind: "tool_intent" | "tool_outcome";
18
+ occurredAt: number;
19
+ sessionId?: string;
20
+ parentActionId?: string;
21
+ sourceReceiptHash?: string;
22
+ data: Record<string, unknown>;
23
+ }
24
+ export interface ObservationSink {
25
+ emit(draft: ObservationDraft): unknown;
26
+ }
27
+ export type OperationClass = "read_only" | "mutation" | "unknown";
28
+ export type ExitCategory = "ok" | "error" | "timeout" | "cancelled" | "unknown";
29
+ /** One tool of a pinned manifest. `resources` names the dispatched-argument paths (dotted,
30
+ * for example `repository` or `target.id`) whose values are the resources the call touches. */
31
+ export interface PinnedTool {
32
+ operation_class: "read_only" | "mutation";
33
+ resources?: Array<{
34
+ arg: string;
35
+ kind: string;
36
+ }>;
37
+ }
38
+ /** A reviewed description of one upstream server, tied to the tool list it was reviewed
39
+ * against. `hash` is `manifestHash(tools)` of that list. */
40
+ export interface PinnedManifest {
41
+ hash: string;
42
+ tools: Record<string, PinnedTool>;
43
+ }
44
+ export interface TypedAdapterConfig {
45
+ /** `enforce` denies before dispatch what it cannot bind; `monitor` records and forwards. */
46
+ mode: "enforce" | "monitor";
47
+ manifest?: PinnedManifest;
48
+ /** Deny (enforce) a mutation, or a tool that declares resources, whose resources could not
49
+ * be bound from the dispatched arguments or are not approved. */
50
+ requireResourceBinding?: boolean;
51
+ /** Approved raw resource values per kind. Under `requireResourceBinding` a kind with no
52
+ * entry approves nothing. */
53
+ approvedResources?: Record<string, string[]>;
54
+ binder: RequestBinder;
55
+ sink?: ObservationSink;
56
+ /** How long a verified tool-list hash is trusted before it is read again. Default 60 s. */
57
+ manifestRecheckMs?: number;
58
+ referenceSetVersion?: string;
59
+ }
60
+ /** SHA-256 of a tool list: the canonical form of each tool's name, description, input schema
61
+ * and annotations, in name order. Presentation-only fields do not change it. */
62
+ export declare function manifestHash(tools: unknown[]): string;
63
+ export declare const UNVERIFIED = "unverified";
64
+ export declare const MAX_RESOURCES = 100;
65
+ export interface TypedDescription {
66
+ /** The closed `mcp` operation, or null when the server or tool name is not a plain identifier. */
67
+ operation: Record<string, unknown> | null;
68
+ operation_class: OperationClass;
69
+ /** Whether every resource the manifest declares for the tool was bound. */
70
+ resources_bound: boolean;
71
+ allow: boolean;
72
+ /** Why an enforcing adapter denies; empty when it allows. Plain words, no raw values. */
73
+ reasons: string[];
74
+ }
75
+ /**
76
+ * Describe one dispatched `tools/call`. `dispatched` is the exact message the proxy will
77
+ * forward (the caller passes a copy it owns), `manifestVerified` whether the upstream's live
78
+ * tool list still hashes to the pinned revision.
79
+ */
80
+ export declare function describeToolCall(config: TypedAdapterConfig, server: string, dispatched: {
81
+ method?: string;
82
+ params?: Record<string, unknown>;
83
+ }, manifestVerified: boolean): TypedDescription;
84
+ /** `scopebond:source-receipt/v1\n` + canonical signed receipt, SHA-256: the hash that links an observation to its receipt. */
85
+ export declare const sourceReceiptHash: (receipt: unknown) => string;
86
+ export declare const intentDraft: (operation: Record<string, unknown>, at: number, receipt: unknown) => ObservationDraft;
87
+ export declare const outcomeDraft: (operation: Record<string, unknown>, exit: ExitCategory, adapterVersion: string, at: number, startedAt: number, receipt: unknown) => ObservationDraft;
88
+ //# sourceMappingURL=typed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.d.ts","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"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"}
package/dist/typed.js ADDED
@@ -0,0 +1,155 @@
1
+ // The typed MCP adapter: the closed `mcp` operation for one `tools/call`, built from the
2
+ // request the proxy actually dispatches, plus the decision an enforcing adapter takes on it.
3
+ //
4
+ // What is bound, and from where:
5
+ // server the configured server id of this proxy
6
+ // tool the tool name in the dispatched request
7
+ // revision the pinned manifest hash, and only while the upstream's live tool list still
8
+ // hashes to it; a tool list that changed since it was pinned is `unverified`
9
+ // class read_only or mutation from the pinned manifest, `unknown` for a tool it does
10
+ // not list or a revision that no longer matches
11
+ // resources opaque ids of the values found at the argument paths the pinned manifest
12
+ // names for that tool, read from the dispatched arguments themselves
13
+ // digest an HMAC over the canonical dispatched request under the installation-local
14
+ // key; the proxy forwards a copy of exactly what was digested
15
+ //
16
+ // A server or tool manifest says what a tool is, never which resource a call touches, so it
17
+ // cannot by itself authorize a resource-specific call: with `requireResourceBinding`, an
18
+ // enforcing adapter denies a call whose resources it could not bind, or that are not in the
19
+ // approved set for their kind. In monitor mode nothing is denied; unknown facts are recorded
20
+ // as unknown.
21
+ import { createHash, createHmac } from "node:crypto";
22
+ import { canonical } from "@scopebond/policy-schema/canonical";
23
+ import { SOURCE_RECEIPT_DOMAIN } from "@scopebond/policy-schema";
24
+ export const REQUEST_BINDING_DOMAIN = "scopebond:request-binding/v1\n";
25
+ const RESOURCE_ID_DOMAIN = "scopebond:resource-id/v1\n";
26
+ const KEY_GENERATION_DOMAIN = "scopebond:request-binding-key/v1\n";
27
+ /** The same binder the hook builds from the installation-local key: identical digests and ids
28
+ * for identical input, so an MCP proxy and a hook on one installation correlate. */
29
+ export function requestBinderFromHex(hex) {
30
+ if (!/^[0-9a-f]{64}$/.test(hex))
31
+ throw new TypeError("invalid request-binding key");
32
+ const key = Buffer.from(hex, "hex");
33
+ const mac = (text) => createHmac("sha256", key).update(text, "utf8").digest("hex");
34
+ return {
35
+ generation: `bk_${createHash("sha256").update(KEY_GENERATION_DOMAIN).update(key).digest("hex").slice(0, 16)}`,
36
+ requestDigest: (request) => mac(REQUEST_BINDING_DOMAIN + canonical(request)),
37
+ resourceId: (kind, value) => `sbr_${mac(`${RESOURCE_ID_DOMAIN}${kind}\0${value}`).slice(0, 32)}`,
38
+ };
39
+ }
40
+ /** SHA-256 of a tool list: the canonical form of each tool's name, description, input schema
41
+ * and annotations, in name order. Presentation-only fields do not change it. */
42
+ export function manifestHash(tools) {
43
+ const norm = tools
44
+ .filter((t) => !!t && typeof t === "object")
45
+ .map((t) => ({ name: t.name ?? null, description: t.description ?? null, inputSchema: t.inputSchema ?? null, annotations: t.annotations ?? null }))
46
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
47
+ return `sha256:${createHash("sha256").update(canonical(norm)).digest("hex")}`;
48
+ }
49
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,199}$/;
50
+ export const UNVERIFIED = "unverified";
51
+ export const MAX_RESOURCES = 100;
52
+ /** The value(s) at a dotted path of the dispatched arguments; undefined when absent or not
53
+ * a string, number, or list of them. */
54
+ function valuesAt(args, path) {
55
+ let node = args;
56
+ for (const part of path.split(".")) {
57
+ if (!node || typeof node !== "object" || Array.isArray(node) || !Object.hasOwn(node, part))
58
+ return undefined;
59
+ node = node[part];
60
+ }
61
+ const scalar = (v) => (typeof v === "string" && v !== "" ? v : typeof v === "number" && Number.isFinite(v) ? String(v) : undefined);
62
+ if (Array.isArray(node)) {
63
+ const values = node.map(scalar);
64
+ return node.length > 0 && values.every((v) => v !== undefined) ? values : undefined;
65
+ }
66
+ const one = scalar(node);
67
+ return one === undefined ? undefined : [one];
68
+ }
69
+ /**
70
+ * Describe one dispatched `tools/call`. `dispatched` is the exact message the proxy will
71
+ * forward (the caller passes a copy it owns), `manifestVerified` whether the upstream's live
72
+ * tool list still hashes to the pinned revision.
73
+ */
74
+ export function describeToolCall(config, server, dispatched, manifestVerified) {
75
+ const params = dispatched.params ?? {};
76
+ const tool = typeof params.name === "string" ? params.name : "";
77
+ const pinned = config.manifest && manifestVerified && Object.hasOwn(config.manifest.tools, tool) ? config.manifest.tools[tool] : undefined;
78
+ const operation_class = pinned ? pinned.operation_class : "unknown";
79
+ const reasons = [];
80
+ if (!NAME.test(tool) || !NAME.test(server))
81
+ reasons.push("the server or tool name is not a plain identifier");
82
+ if (!config.manifest)
83
+ reasons.push("no pinned manifest is configured, so the tool is unknown");
84
+ else if (!manifestVerified)
85
+ reasons.push("the upstream tool list no longer matches the pinned manifest revision");
86
+ else if (!pinned)
87
+ reasons.push("the tool is not in the pinned manifest");
88
+ // Bind resources from the dispatched arguments, never from anything the model summarised.
89
+ const declared = pinned?.resources ?? [];
90
+ const resourceIds = [];
91
+ const bound = [];
92
+ let resourcesBound = true;
93
+ for (const { arg, kind } of declared) {
94
+ const values = valuesAt(params.arguments, arg);
95
+ if (!values) {
96
+ resourcesBound = false;
97
+ continue;
98
+ }
99
+ for (const value of values) {
100
+ bound.push({ kind, value });
101
+ resourceIds.push(config.binder.resourceId(`mcp:${kind}`, value));
102
+ }
103
+ }
104
+ if (resourceIds.length > MAX_RESOURCES) {
105
+ resourcesBound = false;
106
+ resourceIds.length = 0;
107
+ bound.length = 0;
108
+ }
109
+ if (config.requireResourceBinding) {
110
+ const resourceSpecific = pinned && (pinned.operation_class === "mutation" || declared.length > 0);
111
+ if (resourceSpecific && declared.length === 0)
112
+ reasons.push("the tool is a mutation and its manifest names no resource to bind");
113
+ else if (resourceSpecific && !resourcesBound)
114
+ reasons.push("a resource the manifest names could not be read from the dispatched arguments");
115
+ else if (resourceSpecific) {
116
+ const approved = config.approvedResources ?? {};
117
+ if (bound.some((b) => !(approved[b.kind] ?? []).includes(b.value)))
118
+ reasons.push("a bound resource is not in the approved set for its kind");
119
+ }
120
+ }
121
+ const request = { action_type: "mcp.tool.call", server, request: { method: dispatched.method ?? "tools/call", params } };
122
+ const operation = NAME.test(tool) && NAME.test(server) ? {
123
+ type: "mcp", resource_id: config.binder.resourceId("mcp", `${server}\0${tool}`),
124
+ environment_class: "unknown", reference_set_version: config.referenceSetVersion ?? "mcp-1",
125
+ request_digest: config.binder.requestDigest(request), digest_key_generation: config.binder.generation,
126
+ server_id: server, tool_name: tool, manifest_version: manifestVerified && config.manifest ? config.manifest.hash : UNVERIFIED,
127
+ operation_class, resource_ids: resourceIds,
128
+ } : null;
129
+ return { operation, operation_class, resources_bound: resourcesBound, allow: reasons.length === 0, reasons };
130
+ }
131
+ /** `scopebond:source-receipt/v1\n` + canonical signed receipt, SHA-256: the hash that links an observation to its receipt. */
132
+ export const sourceReceiptHash = (receipt) => createHash("sha256").update(SOURCE_RECEIPT_DOMAIN + canonical(receipt), "utf8").digest("hex");
133
+ export const intentDraft = (operation, at, receipt) => ({
134
+ kind: "tool_intent", occurredAt: at, ...linkOf(receipt), data: { event: "requested", operation, request_digest: operation.request_digest, ...linkData(receipt) },
135
+ });
136
+ export const outcomeDraft = (operation, exit, adapterVersion, at, startedAt, receipt) => ({
137
+ kind: "tool_outcome", occurredAt: at, ...linkOf(receipt),
138
+ data: {
139
+ event: exit === "ok" ? "completed" : "failed", operation, exit_category: exit, adapter_version: adapterVersion.slice(0, 200),
140
+ duration_ms: Math.max(0, Math.min(86_400_000, Math.round(at - startedAt))), ...linkData(receipt),
141
+ },
142
+ });
143
+ function actionIdOf(receipt) {
144
+ const id = receipt?.payload?.action_ref?.action_id;
145
+ return typeof id === "string" && id !== "" && id.length <= 200 ? id : undefined;
146
+ }
147
+ const linkOf = (receipt) => {
148
+ const parentActionId = actionIdOf(receipt);
149
+ return parentActionId ? { parentActionId, sourceReceiptHash: sourceReceiptHash(receipt) } : {};
150
+ };
151
+ const linkData = (receipt) => {
152
+ const link = linkOf(receipt);
153
+ return link.parentActionId ? { parent_action_id: link.parentActionId, source_receipt_hash: link.sourceReceiptHash } : {};
154
+ };
155
+ //# sourceMappingURL=typed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.js","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"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","sourcesContent":["// The typed MCP adapter: the closed `mcp` operation for one `tools/call`, built from the\n// request the proxy actually dispatches, plus the decision an enforcing adapter takes on it.\n//\n// What is bound, and from where:\n// server the configured server id of this proxy\n// tool the tool name in the dispatched request\n// revision the pinned manifest hash, and only while the upstream's live tool list still\n// hashes to it; a tool list that changed since it was pinned is `unverified`\n// class read_only or mutation from the pinned manifest, `unknown` for a tool it does\n// not list or a revision that no longer matches\n// resources opaque ids of the values found at the argument paths the pinned manifest\n// names for that tool, read from the dispatched arguments themselves\n// digest an HMAC over the canonical dispatched request under the installation-local\n// key; the proxy forwards a copy of exactly what was digested\n//\n// A server or tool manifest says what a tool is, never which resource a call touches, so it\n// cannot by itself authorize a resource-specific call: with `requireResourceBinding`, an\n// enforcing adapter denies a call whose resources it could not bind, or that are not in the\n// approved set for their kind. In monitor mode nothing is denied; unknown facts are recorded\n// as unknown.\n\nimport { createHash, createHmac } from \"node:crypto\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { SOURCE_RECEIPT_DOMAIN } from \"@scopebond/policy-schema\";\n\n/** Request binding and keyed opaque ids. The hook's binding key satisfies this structurally. */\nexport interface RequestBinder {\n /** Opaque id of the key generation; safe to upload. */\n readonly generation: string;\n /** HMAC-SHA-256 of `scopebond:request-binding/v1\\n` + the canonical request, as 64 hex. */\n requestDigest(request: unknown): string;\n /** A keyed opaque id for a resource. */\n resourceId(kind: string, value: string): string;\n}\n\nexport const REQUEST_BINDING_DOMAIN = \"scopebond:request-binding/v1\\n\";\nconst RESOURCE_ID_DOMAIN = \"scopebond:resource-id/v1\\n\";\nconst KEY_GENERATION_DOMAIN = \"scopebond:request-binding-key/v1\\n\";\n\n/** The same binder the hook builds from the installation-local key: identical digests and ids\n * for identical input, so an MCP proxy and a hook on one installation correlate. */\nexport function requestBinderFromHex(hex: string): RequestBinder {\n if (!/^[0-9a-f]{64}$/.test(hex)) throw new TypeError(\"invalid request-binding key\");\n const key = Buffer.from(hex, \"hex\");\n const mac = (text: string): string => createHmac(\"sha256\", key).update(text, \"utf8\").digest(\"hex\");\n return {\n generation: `bk_${createHash(\"sha256\").update(KEY_GENERATION_DOMAIN).update(key).digest(\"hex\").slice(0, 16)}`,\n requestDigest: (request) => mac(REQUEST_BINDING_DOMAIN + canonical(request as never)),\n resourceId: (kind, value) => `sbr_${mac(`${RESOURCE_ID_DOMAIN}${kind}\\0${value}`).slice(0, 32)}`,\n };\n}\n\n/** What the adapter hands an observation outbox. The hook's `ObservationEmitter.emit` takes\n * exactly this, so the hook's outbox is a sink; any other implementation works too. */\nexport interface ObservationDraft {\n kind: \"tool_intent\" | \"tool_outcome\";\n occurredAt: number;\n sessionId?: string;\n parentActionId?: string;\n sourceReceiptHash?: string;\n data: Record<string, unknown>;\n}\nexport interface ObservationSink { emit(draft: ObservationDraft): unknown }\n\nexport type OperationClass = \"read_only\" | \"mutation\" | \"unknown\";\nexport type ExitCategory = \"ok\" | \"error\" | \"timeout\" | \"cancelled\" | \"unknown\";\n\n/** One tool of a pinned manifest. `resources` names the dispatched-argument paths (dotted,\n * for example `repository` or `target.id`) whose values are the resources the call touches. */\nexport interface PinnedTool {\n operation_class: \"read_only\" | \"mutation\";\n resources?: Array<{ arg: string; kind: string }>;\n}\n\n/** A reviewed description of one upstream server, tied to the tool list it was reviewed\n * against. `hash` is `manifestHash(tools)` of that list. */\nexport interface PinnedManifest {\n hash: string;\n tools: Record<string, PinnedTool>;\n}\n\nexport interface TypedAdapterConfig {\n /** `enforce` denies before dispatch what it cannot bind; `monitor` records and forwards. */\n mode: \"enforce\" | \"monitor\";\n manifest?: PinnedManifest;\n /** Deny (enforce) a mutation, or a tool that declares resources, whose resources could not\n * be bound from the dispatched arguments or are not approved. */\n requireResourceBinding?: boolean;\n /** Approved raw resource values per kind. Under `requireResourceBinding` a kind with no\n * entry approves nothing. */\n approvedResources?: Record<string, string[]>;\n binder: RequestBinder;\n sink?: ObservationSink;\n /** How long a verified tool-list hash is trusted before it is read again. Default 60 s. */\n manifestRecheckMs?: number;\n referenceSetVersion?: string;\n}\n\n/** SHA-256 of a tool list: the canonical form of each tool's name, description, input schema\n * and annotations, in name order. Presentation-only fields do not change it. */\nexport function manifestHash(tools: unknown[]): string {\n const norm = tools\n .filter((t): t is Record<string, unknown> => !!t && typeof t === \"object\")\n .map((t) => ({ name: t.name ?? null, description: t.description ?? null, inputSchema: t.inputSchema ?? null, annotations: t.annotations ?? null }))\n .sort((a, b) => String(a.name).localeCompare(String(b.name)));\n return `sha256:${createHash(\"sha256\").update(canonical(norm as never)).digest(\"hex\")}`;\n}\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,199}$/;\nexport const UNVERIFIED = \"unverified\";\nexport const MAX_RESOURCES = 100;\n\n/** The value(s) at a dotted path of the dispatched arguments; undefined when absent or not\n * a string, number, or list of them. */\nfunction valuesAt(args: unknown, path: string): string[] | undefined {\n let node: unknown = args;\n for (const part of path.split(\".\")) {\n if (!node || typeof node !== \"object\" || Array.isArray(node) || !Object.hasOwn(node, part)) return undefined;\n node = (node as Record<string, unknown>)[part];\n }\n const scalar = (v: unknown): string | undefined => (typeof v === \"string\" && v !== \"\" ? v : typeof v === \"number\" && Number.isFinite(v) ? String(v) : undefined);\n if (Array.isArray(node)) {\n const values = node.map(scalar);\n return node.length > 0 && values.every((v) => v !== undefined) ? (values as string[]) : undefined;\n }\n const one = scalar(node);\n return one === undefined ? undefined : [one];\n}\n\nexport interface TypedDescription {\n /** The closed `mcp` operation, or null when the server or tool name is not a plain identifier. */\n operation: Record<string, unknown> | null;\n operation_class: OperationClass;\n /** Whether every resource the manifest declares for the tool was bound. */\n resources_bound: boolean;\n allow: boolean;\n /** Why an enforcing adapter denies; empty when it allows. Plain words, no raw values. */\n reasons: string[];\n}\n\n/**\n * Describe one dispatched `tools/call`. `dispatched` is the exact message the proxy will\n * forward (the caller passes a copy it owns), `manifestVerified` whether the upstream's live\n * tool list still hashes to the pinned revision.\n */\nexport function describeToolCall(\n config: TypedAdapterConfig, server: string, dispatched: { method?: string; params?: Record<string, unknown> }, manifestVerified: boolean,\n): TypedDescription {\n const params = dispatched.params ?? {};\n const tool = typeof params.name === \"string\" ? params.name : \"\";\n const pinned = config.manifest && manifestVerified && Object.hasOwn(config.manifest.tools, tool) ? config.manifest.tools[tool] : undefined;\n const operation_class: OperationClass = pinned ? pinned.operation_class : \"unknown\";\n const reasons: string[] = [];\n if (!NAME.test(tool) || !NAME.test(server)) reasons.push(\"the server or tool name is not a plain identifier\");\n if (!config.manifest) reasons.push(\"no pinned manifest is configured, so the tool is unknown\");\n else if (!manifestVerified) reasons.push(\"the upstream tool list no longer matches the pinned manifest revision\");\n else if (!pinned) reasons.push(\"the tool is not in the pinned manifest\");\n\n // Bind resources from the dispatched arguments, never from anything the model summarised.\n const declared = pinned?.resources ?? [];\n const resourceIds: string[] = [];\n const bound: Array<{ kind: string; value: string }> = [];\n let resourcesBound = true;\n for (const { arg, kind } of declared) {\n const values = valuesAt(params.arguments, arg);\n if (!values) { resourcesBound = false; continue; }\n for (const value of values) { bound.push({ kind, value }); resourceIds.push(config.binder.resourceId(`mcp:${kind}`, value)); }\n }\n if (resourceIds.length > MAX_RESOURCES) { resourcesBound = false; resourceIds.length = 0; bound.length = 0; }\n if (config.requireResourceBinding) {\n const resourceSpecific = pinned && (pinned.operation_class === \"mutation\" || declared.length > 0);\n if (resourceSpecific && declared.length === 0) reasons.push(\"the tool is a mutation and its manifest names no resource to bind\");\n else if (resourceSpecific && !resourcesBound) reasons.push(\"a resource the manifest names could not be read from the dispatched arguments\");\n else if (resourceSpecific) {\n const approved = config.approvedResources ?? {};\n if (bound.some((b) => !(approved[b.kind] ?? []).includes(b.value))) reasons.push(\"a bound resource is not in the approved set for its kind\");\n }\n }\n\n const request = { action_type: \"mcp.tool.call\", server, request: { method: dispatched.method ?? \"tools/call\", params } };\n const operation = NAME.test(tool) && NAME.test(server) ? {\n type: \"mcp\", resource_id: config.binder.resourceId(\"mcp\", `${server}\\0${tool}`),\n environment_class: \"unknown\", reference_set_version: config.referenceSetVersion ?? \"mcp-1\",\n request_digest: config.binder.requestDigest(request), digest_key_generation: config.binder.generation,\n server_id: server, tool_name: tool, manifest_version: manifestVerified && config.manifest ? config.manifest.hash : UNVERIFIED,\n operation_class, resource_ids: resourceIds,\n } : null;\n return { operation, operation_class, resources_bound: resourcesBound, allow: reasons.length === 0, reasons };\n}\n\n/** `scopebond:source-receipt/v1\\n` + canonical signed receipt, SHA-256: the hash that links an observation to its receipt. */\nexport const sourceReceiptHash = (receipt: unknown): string =>\n createHash(\"sha256\").update(SOURCE_RECEIPT_DOMAIN + canonical(receipt as never), \"utf8\").digest(\"hex\");\n\nexport const intentDraft = (operation: Record<string, unknown>, at: number, receipt: unknown): ObservationDraft => ({\n kind: \"tool_intent\", occurredAt: at, ...linkOf(receipt), data: { event: \"requested\", operation, request_digest: operation.request_digest, ...linkData(receipt) },\n});\n\nexport const outcomeDraft = (operation: Record<string, unknown>, exit: ExitCategory, adapterVersion: string, at: number, startedAt: number, receipt: unknown): ObservationDraft => ({\n kind: \"tool_outcome\", occurredAt: at, ...linkOf(receipt),\n data: {\n event: exit === \"ok\" ? \"completed\" : \"failed\", operation, exit_category: exit, adapter_version: adapterVersion.slice(0, 200),\n duration_ms: Math.max(0, Math.min(86_400_000, Math.round(at - startedAt))), ...linkData(receipt),\n },\n});\n\nfunction actionIdOf(receipt: unknown): string | undefined {\n const id = (receipt as { payload?: { action_ref?: { action_id?: unknown } } } | undefined)?.payload?.action_ref?.action_id;\n return typeof id === \"string\" && id !== \"\" && id.length <= 200 ? id : undefined;\n}\nconst linkOf = (receipt: unknown): { parentActionId?: string; sourceReceiptHash?: string } => {\n const parentActionId = actionIdOf(receipt);\n return parentActionId ? { parentActionId, sourceReceiptHash: sourceReceiptHash(receipt) } : {};\n};\nconst linkData = (receipt: unknown): Record<string, string> => {\n const link = linkOf(receipt);\n return link.parentActionId ? { parent_action_id: link.parentActionId, source_receipt_hash: link.sourceReceiptHash as string } : {};\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scopebond/mcp",
3
- "version": "0.3.3",
3
+ "version": "1.0.0",
4
4
  "description": "Scopebond MCP proxy — one policy for every Model Context Protocol tool call, in front of the servers an agent uses. In-path enforcement with a signed PEP-authorized receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Avouro LLC",
@@ -40,13 +40,24 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@scopebond/policy-schema": "^0.4.1",
44
- "@scopebond/verify": "^0.4.1",
45
- "@scopebond/gateway": "^0.8.0"
43
+ "@scopebond/policy-schema": "^0.5.0",
44
+ "@scopebond/verify": "^0.4.2",
45
+ "@scopebond/gateway": "^0.9.0"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
+ "devDependencies": {
51
+ "@scopebond/hook": "^0.10.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@scopebond/hook": ">=0.10.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "@scopebond/hook": {
58
+ "optional": true
59
+ }
60
+ },
50
61
  "scripts": {
51
62
  "build": "tsc -p tsconfig.json",
52
63
  "test": "tsc -p tsconfig.json && node --test test/*.test.mjs"