@adtl/gateway 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +63 -3
  2. package/dist/audit.d.ts +9 -4
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +1 -1
  5. package/dist/audit.js.map +1 -1
  6. package/dist/cli.js +82 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/gateway.d.ts +22 -2
  9. package/dist/gateway.d.ts.map +1 -1
  10. package/dist/gateway.js +189 -14
  11. package/dist/gateway.js.map +1 -1
  12. package/dist/hooks/claudeCode.d.ts +43 -0
  13. package/dist/hooks/claudeCode.d.ts.map +1 -0
  14. package/dist/hooks/claudeCode.js +148 -0
  15. package/dist/hooks/claudeCode.js.map +1 -0
  16. package/dist/hooks/index.d.ts +4 -0
  17. package/dist/hooks/index.d.ts.map +1 -0
  18. package/dist/hooks/index.js +4 -0
  19. package/dist/hooks/index.js.map +1 -0
  20. package/dist/hooks/preset.d.ts +16 -0
  21. package/dist/hooks/preset.d.ts.map +1 -0
  22. package/dist/hooks/preset.js +32 -0
  23. package/dist/hooks/preset.js.map +1 -0
  24. package/dist/hooks/run.d.ts +9 -0
  25. package/dist/hooks/run.d.ts.map +1 -0
  26. package/dist/hooks/run.js +44 -0
  27. package/dist/hooks/run.js.map +1 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/init/index.d.ts +4 -0
  33. package/dist/init/index.d.ts.map +1 -0
  34. package/dist/init/index.js +4 -0
  35. package/dist/init/index.js.map +1 -0
  36. package/dist/init/presets.d.ts +30 -0
  37. package/dist/init/presets.d.ts.map +1 -0
  38. package/dist/init/presets.js +34 -0
  39. package/dist/init/presets.js.map +1 -0
  40. package/dist/init/run.d.ts +18 -0
  41. package/dist/init/run.d.ts.map +1 -0
  42. package/dist/init/run.js +60 -0
  43. package/dist/init/run.js.map +1 -0
  44. package/dist/init/suggest.d.ts +50 -0
  45. package/dist/init/suggest.d.ts.map +1 -0
  46. package/dist/init/suggest.js +82 -0
  47. package/dist/init/suggest.js.map +1 -0
  48. package/dist/startup.d.ts +13 -2
  49. package/dist/startup.d.ts.map +1 -1
  50. package/dist/startup.js +25 -6
  51. package/dist/startup.js.map +1 -1
  52. package/package.json +3 -3
  53. package/src/audit.ts +10 -5
  54. package/src/cli.ts +75 -2
  55. package/src/gateway.ts +238 -13
  56. package/src/hooks/claudeCode.ts +194 -0
  57. package/src/hooks/index.ts +3 -0
  58. package/src/hooks/preset.ts +47 -0
  59. package/src/hooks/run.ts +49 -0
  60. package/src/index.ts +2 -0
  61. package/src/init/index.ts +3 -0
  62. package/src/init/presets.ts +64 -0
  63. package/src/init/run.ts +79 -0
  64. package/src/init/suggest.ts +127 -0
  65. package/src/startup.ts +25 -7
package/README.md CHANGED
@@ -58,7 +58,25 @@ One file holds both the labels and the gateway settings.
58
58
  | `confidentiality` | `public`, `private`, `secret` | Who may see the tool's *result*? |
59
59
  | `egress` | `none`, `internal`, `external`, `public` | Where *calling* the tool can send data. |
60
60
 
61
- Tools without a rule use the defaults (`untrusted`, `private`, egress `unknown`). That's safe but strict, so describe the tools you use.
61
+ Tools without a rule use the defaults (`untrusted`, `private`, egress `unknown`). That's safe but strict, so describe the tools you use. Resources and prompts use the server's `defaults`.
62
+
63
+ #### Drafting rules with `init`
64
+
65
+ List your servers under `gateway.upstreams`, then let `init` draft a rule for every tool:
66
+
67
+ ```sh
68
+ adtl-gateway init -c adtl.yaml # print the updated config, change nothing
69
+ adtl-gateway init -c adtl.yaml --write # update the file (original kept as adtl.yaml.bak)
70
+ ```
71
+
72
+ `init` connects to each upstream and lists its tools.
73
+
74
+ - **Presets** cover the GitHub (`github-mcp-server`), filesystem and fetch servers. A preset says what the server's reads and writes mean; the server's `readOnlyHint` decides which tools are reads, so new tools are covered too.
75
+ - **Other servers** get rules from their MCP tool hints. Hints only set egress, because they are the server's own claims: every result stays `untrusted` and `private` until you change it. These lines are marked `review`.
76
+ - Entries already in the file are never changed, so you can re-run `init` after a server adds tools. Tools the server no longer offers are reported, not removed.
77
+ - Edits keep your comments, but the YAML may be reformatted slightly (e.g. `[a]` becomes `[ a ]`).
78
+
79
+ Always review the result: a preset is a starting point, not a judgment about your data.
62
80
 
63
81
  Labels only get stricter as a session goes on. A session that has read anything untrusted stays untrusted until the session ends.
64
82
 
@@ -120,6 +138,46 @@ adtl-gateway --config adtl.yaml --audit
120
138
 
121
139
  or set `gateway.mode: audit` in the file. In audit mode every call goes through and results are not changed. Labels are still tracked, so the decisions are the same as in active mode. Each line in the audit log gets `"mode": "audit"`, and a call the policy would have stopped is logged as `"outcome": "forwarded"` with `"decision": "ask"` or `"block"`, the reason and the matching rules. Once the log looks right, remove the flag.
122
140
 
141
+ ## Claude Code: cover the built-in tools too
142
+
143
+ The gateway only sees MCP calls. Claude Code's own tools (`WebFetch`, `WebSearch`, `Bash`, `Read`, ...) don't go through it, so a page fetched with `WebFetch` would not taint the session, and `Bash` could send data anywhere. `adtl-gateway hook` closes that gap. Add it to `.claude/settings.json` (or `~/.claude/settings.json` for every project):
144
+
145
+ ```json
146
+ {
147
+ "hooks": {
148
+ "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "npx -y @adtl/gateway hook --config /absolute/path/to/adtl.yaml", "timeout": 10, "onFailure": "block" }] }],
149
+ "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "npx -y @adtl/gateway hook --config /absolute/path/to/adtl.yaml", "timeout": 10 }] }],
150
+ "PostToolUseFailure": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "npx -y @adtl/gateway hook --config /absolute/path/to/adtl.yaml", "timeout": 10 }] }]
151
+ }
152
+ }
153
+ ```
154
+
155
+ (A global install, `npm i -g @adtl/gateway` and then `adtl-gateway hook ...`, starts faster than `npx`.)
156
+
157
+ What it does:
158
+
159
+ - **After each tool runs**, it labels the output and adds it to the session label. Built-in tools are labeled as the server `claude-code`.
160
+ - **Before each tool runs**, it checks the call against the same policy as the gateway. `block` denies the call. `ask` shows Claude Code's own permission prompt, so you approve in the terminal. `allow` gives no answer, so your normal Claude Code permissions still apply. ADTL never approves a call on its own.
161
+ - **It shares one session label with the gateway.** Claude Code gives the gateway its session id (`CLAUDE_CODE_SESSION_ID`), and both keep the label in `~/.adtl/sessions/<id>.jsonl` (set `ADTL_SESSIONS_DIR` to change the directory). A `WebFetch` taints the session for `github__add_comment`, and a private file read through the gateway counts against a later `Bash` call. Labels last for the whole Claude Code session, including `--resume`.
162
+ - **It covers MCP servers that skip the gateway**, such as connectors added in claude.ai. Their tools are labeled as their server name (`mcp__<server>__<tool>` → server `<server>`), so you can describe them under `servers:` like any upstream. The gateway's own tools (`mcp__adtl__*`) are skipped; pass `--gateway-server <name>` if you registered the gateway under another name.
163
+ - **It fails closed.** If the hook can't decide, it exits with code 2, which stops the call.
164
+ - **It writes to the same audit log** (`gateway.audit`) with `"via": "claude-code-hook"`, and follows `gateway.mode: audit`. The config needs no `gateway:` section if you only use the hooks.
165
+
166
+ Built-in labels, which you can override under `servers.claude-code.tools`:
167
+
168
+ | Tools | Result | Egress |
169
+ |---|---|---|
170
+ | `Read`, `Grep`, `Glob` | trusted, private | none |
171
+ | `WebFetch`, `WebSearch` | untrusted, public | external (a URL or query can carry data out) |
172
+ | `Bash`, `Monitor` | unknown, private | unknown |
173
+ | `Agent` / `Task` | unknown, private | none (the sub-agent's own calls are checked too) |
174
+ | `Write`, `Edit`, `TodoWrite`, other bookkeeping tools | trusted, public | none |
175
+ | anything else | your `defaults` | unknown |
176
+
177
+ With the [example policy](../../examples/adtl.yaml), `Bash` and `WebFetch` run freely in a fresh session, but ask first once the session has read untrusted content, because `no-public-write-after-untrusted` matches `tool.egress: [public, external, unknown]`.
178
+
179
+ The hook also refuses `Write` and `Edit` on your ADTL config and session files, and asks before shell commands that mention the session directory, so the agent can't simply clear its own taint. This is a speed bump, not a sandbox.
180
+
123
181
  ## How it works
124
182
 
125
183
  ```
@@ -134,9 +192,11 @@ The gateway fails closed. If an interceptor errors or times out, the call is sto
134
192
 
135
193
  ## Limits of this version
136
194
 
137
- - Only tools are proxied. Resources and prompts are not passed through yet.
195
+ - Tools, resources and prompts are proxied. Resource subscriptions, `list_changed` notifications and completions are not passed through yet.
196
+ - Resources and prompts are labeled by server defaults (`servers.<id>.defaults`) or the global defaults; there are no per-resource or per-prompt rules yet. A resource URI that two upstreams could serve is refused rather than guessed.
138
197
  - Approvals come from one push channel (ntfy). Other channels, approvals inside the MCP client, and approver roles are planned.
139
- - Labels last for one gateway process, which is one agent session. They are not carried into files, memory or other agents yet; that's planned for v0.4.
198
+ - Labels last for one agent session: one gateway process, or under Claude Code one Claude Code session (shared with the hooks). They are not carried into files, memory or other agents yet; that's planned for v0.4. A file written after reading untrusted content reads back as trusted.
199
+ - The Claude Code hooks label tools by name, not by what they touched: a `Read` of a downloaded file is as trusted as a `Read` of your own code.
140
200
  - Labels are set by your config. Servers' own trust claims can only make labels stricter, unless you set `acceptServerClaims: true`.
141
201
 
142
202
  ## Links
package/dist/audit.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  export interface AuditEntry {
2
2
  ts: string;
3
3
  sessionId: string;
4
- event: "tools/call";
4
+ event: "tools/call" | "resources/read" | "prompts/get";
5
5
  server: string;
6
- tool: string;
7
- /** Final outcome of the call. */
8
- outcome: "forwarded" | "blocked" | "upstream_error";
6
+ /** Exactly one of tool, resource or prompt is set, matching `event`. */
7
+ tool?: string;
8
+ resource?: string;
9
+ prompt?: string;
10
+ /** Final outcome of the call. "asked": handed to Claude Code's permission prompt by the hook. */
11
+ outcome: "forwarded" | "blocked" | "asked" | "upstream_error";
12
+ /** Present when the entry was written by the Claude Code hook rather than the gateway. */
13
+ via?: "claude-code-hook";
9
14
  /** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
10
15
  decision?: string;
11
16
  /** Present when the gateway ran in audit mode: nothing was stopped or changed. */
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACpD,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,qBAAa,aAAc,YAAW,SAAS;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;IAIjC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B;AAED,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAM;IACpC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,GAAG,gBAAgB,GAAG,aAAa,CAAC;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,gBAAgB,CAAC;IAC9D,0FAA0F;IAC1F,GAAG,CAAC,EAAE,kBAAkB,CAAC;IACzB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,qBAAa,aAAc,YAAW,SAAS;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;IAIjC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B;AAED,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAM;IACpC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B"}
package/dist/audit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Append-only JSONL audit log. One line per tool call.
2
+ * Append-only JSONL audit log. One line per tool call, resource read or prompt get.
3
3
  *
4
4
  * Arguments and results are never written; the call is identified by its
5
5
  * hash, so the log can be shared without leaking the data it describes.
package/dist/audit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,MAAM,OAAO,aAAa;IACH;IAArB,YAAqB,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QAC/B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,KAAiB;QACrB,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IAChB,OAAO,GAAiB,EAAE,CAAC;IACpC,KAAK,CAAC,KAAiB;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;CACF"}
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkCpC,MAAM,OAAO,aAAa;IACH;IAArB,YAAqB,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QAC/B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,KAAiB;QACrB,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IAChB,OAAO,GAAiB,EAAE,CAAC;IACpC,KAAK,CAAC,KAAiB;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;CACF"}
package/dist/cli.js CHANGED
@@ -4,6 +4,8 @@
4
4
  * servers through ADTL's interceptors.
5
5
  *
6
6
  * adtl-gateway --config adtl.yaml [--audit]
7
+ * adtl-gateway init --config adtl.yaml [--write]
8
+ * adtl-gateway hook --config adtl.yaml [--gateway-server adtl]
7
9
  *
8
10
  * Point your MCP client (e.g. Claude Code's .mcp.json) at this command.
9
11
  * Logs go to stderr; stdout carries MCP JSON-RPC only.
@@ -11,8 +13,12 @@
11
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
14
  import { EnvExpansionError } from "./config.js";
13
15
  import { GATEWAY_VERSION } from "./gateway.js";
16
+ import { runHook } from "./hooks/run.js";
17
+ import { runInit } from "./init/run.js";
14
18
  import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
15
19
  const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
20
+ adtl-gateway init --config <adtl.yaml> [--write]
21
+ adtl-gateway hook --config <adtl.yaml> [--gateway-server <name>]
16
22
 
17
23
  Runs an MCP server over stdio that forwards tool calls to the upstream
18
24
  servers in the config, checking and labeling each call with ADTL.
@@ -22,12 +28,36 @@ Options:
22
28
  --audit Log what would be stopped, but stop nothing
23
29
  (same as gateway.mode: audit).
24
30
  -v, --version Print the version and exit.
31
+
32
+ init:
33
+ Connects to every upstream, lists its tools, and drafts a labeling rule
34
+ for each tool the config doesn't cover yet (presets for GitHub,
35
+ filesystem and fetch; MCP tool hints otherwise). Existing entries are
36
+ never changed. Prints the updated config; review it before using it.
37
+ --write Update the config file in place (original kept as .bak).
38
+
39
+ hook:
40
+ The Claude Code hook for PreToolUse, PostToolUse and PostToolUseFailure.
41
+ Labels built-in tools (WebFetch, Read, Bash, ...) and MCP servers outside
42
+ the gateway, and runs the same policy on them, sharing the session label
43
+ with the gateway. Reads the hook event from stdin.
44
+ --gateway-server <name>
45
+ The gateway's name in Claude Code's MCP config; its
46
+ tools are checked by the gateway and skipped here.
47
+ Default: adtl.
48
+
25
49
  -h, --help Show this help.`;
26
50
  function parseArgs(argv) {
27
51
  const out = {};
28
52
  for (let i = 0; i < argv.length; i++) {
29
53
  const a = argv[i];
30
- if (a === "-h" || a === "--help")
54
+ if (i === 0 && a === "init")
55
+ out.init = true;
56
+ else if (i === 0 && a === "hook")
57
+ out.hook = true;
58
+ else if (a === "--gateway-server" && out.hook)
59
+ out.gatewayServer = argv[++i];
60
+ else if (a === "-h" || a === "--help")
31
61
  out.help = true;
32
62
  else if (a === "-v" || a === "--version")
33
63
  out.version = true;
@@ -37,6 +67,8 @@ function parseArgs(argv) {
37
67
  out.config = a.slice("--config=".length);
38
68
  else if (a === "--audit")
39
69
  out.audit = true;
70
+ else if (a === "--write" && out.init)
71
+ out.write = true;
40
72
  else
41
73
  throw new GatewayConfigError(`Unknown argument: ${a}\n\n${USAGE}`);
42
74
  }
@@ -51,6 +83,10 @@ async function main() {
51
83
  return void console.log(GATEWAY_VERSION);
52
84
  if (!args.config)
53
85
  throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
86
+ if (args.init)
87
+ return init(args.config, args.write === true);
88
+ if (args.hook)
89
+ return hook(args.config, args.gatewayServer);
54
90
  const loaded = await loadGatewayConfig(args.config);
55
91
  if (args.audit)
56
92
  loaded.gateway.mode = "audit";
@@ -74,9 +110,53 @@ async function main() {
74
110
  await server.connect(new StdioServerTransport());
75
111
  console.error(`[adtl-gateway] ${GATEWAY_VERSION} ready (session ${started.gateway.sessionId})`);
76
112
  }
113
+ function summarize(r, path, write) {
114
+ const lines = r.reports.map((s) => {
115
+ const parts = [`${s.added.length} tools added`];
116
+ if (s.kept.length)
117
+ parts.push(`${s.kept.length} kept`);
118
+ if (s.addedDefaults)
119
+ parts.push("defaults added");
120
+ if (s.stale.length)
121
+ parts.push(`no longer offered: ${s.stale.join(", ")}`);
122
+ return ` ${s.id} (${s.preset ? `preset ${s.preset}` : "from tool hints, review"}): ${parts.join("; ")}`;
123
+ });
124
+ for (const f of r.failed)
125
+ lines.push(` ${f.id}: could not connect, skipped (${f.error})`);
126
+ const changed = r.reports.some((s) => s.added.length || s.addedDefaults);
127
+ const tail = !changed
128
+ ? "Nothing to add."
129
+ : write
130
+ ? `Updated ${path} (original saved as ${path}.bak). Review every line marked "review".`
131
+ : "Nothing written. Review the config above, then re-run with --write.";
132
+ return `${lines.join("\n")}\n${tail}`;
133
+ }
134
+ async function readStdin() {
135
+ const chunks = [];
136
+ for await (const c of process.stdin)
137
+ chunks.push(c);
138
+ return Buffer.concat(chunks).toString("utf8");
139
+ }
140
+ async function hook(config, gatewayServer) {
141
+ const out = await runHook({ configPath: config, stdin: await readStdin(), ...(gatewayServer ? { gatewayServer } : {}) });
142
+ if (out.stdout)
143
+ process.stdout.write(`${JSON.stringify(out.stdout)}\n`);
144
+ if (out.stderr)
145
+ process.stderr.write(`${out.stderr}\n`);
146
+ process.exitCode = out.exitCode;
147
+ }
148
+ async function init(path, write) {
149
+ const result = await runInit({ path, write });
150
+ if (!write)
151
+ process.stdout.write(result.text);
152
+ console.error(`[adtl-gateway] init\n${summarize(result, path, write)}`);
153
+ if (result.failed.length && !result.reports.length)
154
+ process.exitCode = 1;
155
+ }
77
156
  main().catch((err) => {
78
157
  const known = err instanceof GatewayConfigError || err instanceof EnvExpansionError;
79
158
  console.error(known ? `[adtl-gateway] ${err.message}` : err);
80
- process.exit(1);
159
+ // For Claude Code hooks only exit code 2 stops the call; any other code lets it run.
160
+ process.exit(process.argv[2] === "hook" ? 2 : 1);
81
161
  });
82
162
  //# sourceMappingURL=cli.js.map
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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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/gateway.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import { type CallToolResult, type GetPromptResult, type Prompt, type ReadResourceResult, type Resource, type ResourceTemplate, type Tool } from "@modelcontextprotocol/sdk/types.js";
4
4
  import type { ApprovalBroker } from "./approvals.js";
5
5
  import type { AuditSink } from "./audit.js";
6
6
  import type { InterceptorInvoker } from "./invoker.js";
7
- export declare const GATEWAY_VERSION = "0.2.0";
7
+ export declare const GATEWAY_VERSION = "0.3.1";
8
8
  /** `_meta` key on gateway-generated error results describing why a call was stopped. */
9
9
  export declare const GATEWAY_DECISION_KEY = "adtl/gateway";
10
10
  export interface GatewayLogger {
@@ -33,10 +33,30 @@ export declare class AdtlGateway {
33
33
  private readonly log;
34
34
  private readonly approvals;
35
35
  private routes;
36
+ private promptRoutes;
37
+ /** Resource URI → the upstreams that list it. */
38
+ private resourceRoutes;
39
+ private templateRoutes;
36
40
  constructor(opts: AdtlGatewayOptions);
37
41
  /** The downstream MCP server the agent connects to. */
38
42
  createServer(): Server;
43
+ /** Every upstream's items from one list method, following cursors. Upstreams that fail are skipped with a warning. */
44
+ private listAll;
39
45
  listTools(): Promise<Tool[]>;
46
+ listPrompts(): Promise<Prompt[]>;
47
+ listResources(): Promise<Resource[]>;
48
+ listResourceTemplates(): Promise<ResourceTemplate[]>;
49
+ /** The one upstream that serves `uri`: an exact listing wins, then template matches. */
50
+ private resourceUpstream;
51
+ readResource(uri: string): Promise<ReadResourceResult>;
52
+ getPrompt(name: string, args?: Record<string, string>): Promise<GetPromptResult>;
53
+ /**
54
+ * Forward a resources/read or prompts/get request through the interceptor
55
+ * chain: request interceptors → upstream → response interceptors (adtl.label).
56
+ * These requests have no tool result to carry an error, so a stopped request
57
+ * becomes a JSON-RPC error.
58
+ */
59
+ private forwardLabeled;
40
60
  callTool(name: string, args: Record<string, unknown>, opts?: CallOptions): Promise<CallToolResult>;
41
61
  /** Run an approval request, reporting progress to the client while it waits. */
42
62
  private waitForApproval;
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,IAAI,EACV,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,KAAK,EAAyB,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAE9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAQnD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAOD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAmCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,MAAM,CAA4B;gBAE9B,IAAI,EAAE,kBAAkB;IASpC,uDAAuD;IACvD,YAAY,IAAI,MAAM;IAuBhB,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAuB5B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,cAAc,CAAC;IA2G5G,gFAAgF;YAClE,eAAe;CAY9B"}
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAEnE,OAAO,EAUL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EAErB,KAAK,IAAI,EACV,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,KAAK,EAAyB,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAE9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAuBnD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAOD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAmCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,YAAY,CAAkC;IACtD,iDAAiD;IACjD,OAAO,CAAC,cAAc,CAAkC;IACxD,OAAO,CAAC,cAAc,CAAuB;gBAEjC,IAAI,EAAE,kBAAkB;IASpC,uDAAuD;IACvD,YAAY,IAAI,MAAM;IAuCtB,sHAAsH;YACxG,OAAO;IAsBf,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAe5B,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAehC,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAkBpC,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiB1D,wFAAwF;YAC1E,gBAAgB;IAkBxB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQtD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBtF;;;;;OAKG;YACW,cAAc;IAwDtB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,cAAc,CAAC;IA2G5G,gFAAgF;YAClE,eAAe;CAY9B"}
package/dist/gateway.js CHANGED
@@ -5,6 +5,11 @@
5
5
  * tools/list → every upstream's tools, renamed <upstream>__<tool>
6
6
  * tools/call → request interceptors (adtl.policy) → upstream → response
7
7
  * interceptors (adtl.label) → agent
8
+ * prompts/* → like tools: renamed <upstream>__<prompt>, results labeled
9
+ * resources/* → URIs pass through unchanged and are routed to the upstream
10
+ * that lists them (or whose template matches). Results are
11
+ * labeled. A URI more than one upstream could serve is refused,
12
+ * since ADTL could not tell which server to label it as.
8
13
  *
9
14
  * When a call is stopped, the agent receives a normal tool result with
10
15
  * `isError: true` explaining why, so the model can tell the user.
@@ -19,9 +24,10 @@
19
24
  */
20
25
  import { randomUUID } from "node:crypto";
21
26
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
27
+ import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
28
+ import { CallToolRequestSchema, ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
29
  import { TOOL_SEPARATOR } from "./config.js";
24
- export const GATEWAY_VERSION = "0.2.0";
30
+ export const GATEWAY_VERSION = "0.3.1";
25
31
  /** `_meta` key on gateway-generated error results describing why a call was stopped. */
26
32
  export const GATEWAY_DECISION_KEY = "adtl/gateway";
27
33
  const stderrLogger = {
@@ -62,6 +68,10 @@ export class AdtlGateway {
62
68
  log;
63
69
  approvals;
64
70
  routes = new Map();
71
+ promptRoutes = new Map();
72
+ /** Resource URI → the upstreams that list it. */
73
+ resourceRoutes = new Map();
74
+ templateRoutes = [];
65
75
  constructor(opts) {
66
76
  this.upstreams = opts.upstreams;
67
77
  this.invoker = opts.invoker;
@@ -72,8 +82,15 @@ export class AdtlGateway {
72
82
  }
73
83
  /** The downstream MCP server the agent connects to. */
74
84
  createServer() {
85
+ // Offer resources and prompts only if some upstream does (the SDK refuses handlers for undeclared capabilities).
86
+ const offers = (c) => [...this.upstreams.values()].some((u) => u.getServerCapabilities()?.[c]);
87
+ const capabilities = { tools: {} };
88
+ if (offers("resources"))
89
+ capabilities.resources = {};
90
+ if (offers("prompts"))
91
+ capabilities.prompts = {};
75
92
  const server = new Server({ name: "adtl-gateway", version: GATEWAY_VERSION }, {
76
- capabilities: { tools: {} },
93
+ capabilities,
77
94
  instructions: "Tools from several MCP servers, named <server>__<tool>. ADTL checks each call against trust labels; " +
78
95
  "if a call is stopped, the result explains why.",
79
96
  });
@@ -85,31 +102,189 @@ export class AdtlGateway {
85
102
  : (progress, message) => extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress, message } });
86
103
  return this.callTool(req.params.name, req.params.arguments ?? {}, progress ? { progress } : {});
87
104
  });
105
+ if (capabilities.resources) {
106
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await this.listResources() }));
107
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
108
+ resourceTemplates: await this.listResourceTemplates(),
109
+ }));
110
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => this.readResource(req.params.uri));
111
+ }
112
+ if (capabilities.prompts) {
113
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await this.listPrompts() }));
114
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => this.getPrompt(req.params.name, req.params.arguments));
115
+ }
88
116
  return server;
89
117
  }
90
- async listTools() {
91
- const routes = new Map();
92
- const tools = [];
118
+ /** Every upstream's items from one list method, following cursors. Upstreams that fail are skipped with a warning. */
119
+ async listAll(method, capability, page) {
120
+ const out = [];
93
121
  for (const [upstream, client] of this.upstreams) {
122
+ if (capability !== "tools" && !client.getServerCapabilities()?.[capability])
123
+ continue;
94
124
  let cursor;
95
125
  try {
96
126
  do {
97
- const page = await client.listTools(cursor ? { cursor } : {});
98
- for (const t of page.tools) {
99
- const name = `${upstream}${TOOL_SEPARATOR}${t.name}`;
100
- routes.set(name, { upstream, tool: t.name, ...(t.annotations ? { annotations: t.annotations } : {}) });
101
- tools.push({ ...t, name });
102
- }
103
- cursor = page.nextCursor;
127
+ const p = await page(client, cursor);
128
+ for (const item of p.items)
129
+ out.push({ upstream, item });
130
+ cursor = p.nextCursor;
104
131
  } while (cursor);
105
132
  }
106
133
  catch (err) {
107
- this.log.warn(`tools/list failed for ${upstream}: ${err.message}`);
134
+ this.log.warn(`${method} failed for ${upstream}: ${err.message}`);
108
135
  }
109
136
  }
137
+ return out;
138
+ }
139
+ async listTools() {
140
+ const all = await this.listAll("tools/list", "tools", async (c, cursor) => {
141
+ const p = await c.listTools(cursor ? { cursor } : {});
142
+ return { items: p.tools, nextCursor: p.nextCursor };
143
+ });
144
+ const routes = new Map();
145
+ const tools = all.map(({ upstream, item: t }) => {
146
+ const name = `${upstream}${TOOL_SEPARATOR}${t.name}`;
147
+ routes.set(name, { upstream, tool: t.name, ...(t.annotations ? { annotations: t.annotations } : {}) });
148
+ return { ...t, name };
149
+ });
110
150
  this.routes = routes;
111
151
  return tools;
112
152
  }
153
+ async listPrompts() {
154
+ const all = await this.listAll("prompts/list", "prompts", async (c, cursor) => {
155
+ const p = await c.listPrompts(cursor ? { cursor } : {});
156
+ return { items: p.prompts, nextCursor: p.nextCursor };
157
+ });
158
+ const routes = new Map();
159
+ const prompts = all.map(({ upstream, item: p }) => {
160
+ const name = `${upstream}${TOOL_SEPARATOR}${p.name}`;
161
+ routes.set(name, { upstream, prompt: p.name });
162
+ return { ...p, name };
163
+ });
164
+ this.promptRoutes = routes;
165
+ return prompts;
166
+ }
167
+ async listResources() {
168
+ const all = await this.listAll("resources/list", "resources", async (c, cursor) => {
169
+ const p = await c.listResources(cursor ? { cursor } : {});
170
+ return { items: p.resources, nextCursor: p.nextCursor };
171
+ });
172
+ const routes = new Map();
173
+ for (const { upstream, item } of all) {
174
+ const ups = routes.get(item.uri) ?? new Set();
175
+ ups.add(upstream);
176
+ routes.set(item.uri, ups);
177
+ }
178
+ for (const [uri, ups] of routes) {
179
+ if (ups.size > 1)
180
+ this.log.warn(`resource ${uri} is offered by ${[...ups].join(", ")}; reading it will be refused`);
181
+ }
182
+ this.resourceRoutes = routes;
183
+ return all.map((a) => a.item);
184
+ }
185
+ async listResourceTemplates() {
186
+ const all = await this.listAll("resources/templates/list", "resources", async (c, cursor) => {
187
+ const p = await c.listResourceTemplates(cursor ? { cursor } : {});
188
+ return { items: p.resourceTemplates, nextCursor: p.nextCursor };
189
+ });
190
+ const routes = [];
191
+ for (const { upstream, item } of all) {
192
+ try {
193
+ routes.push({ upstream, template: new UriTemplate(item.uriTemplate) });
194
+ }
195
+ catch (err) {
196
+ this.log.warn(`ignoring resource template ${item.uriTemplate} from ${upstream}: ${err.message}`);
197
+ }
198
+ }
199
+ this.templateRoutes = routes;
200
+ return all.map((a) => a.item);
201
+ }
202
+ /** The one upstream that serves `uri`: an exact listing wins, then template matches. */
203
+ async resourceUpstream(uri) {
204
+ if (!this.resourceRoutes.has(uri))
205
+ await this.listResources();
206
+ let candidates = [...(this.resourceRoutes.get(uri) ?? [])];
207
+ if (candidates.length === 0) {
208
+ if (this.templateRoutes.length === 0)
209
+ await this.listResourceTemplates();
210
+ candidates = [...new Set(this.templateRoutes.filter((t) => t.template.match(uri) !== null).map((t) => t.upstream))];
211
+ }
212
+ if (candidates.length === 0)
213
+ throw new McpError(ErrorCode.InvalidParams, `Unknown resource: ${uri}`);
214
+ if (candidates.length > 1) {
215
+ throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} could come from more than one server (${candidates.join(", ")}). ` +
216
+ `ADTL can't tell which server to label it as, so it was not read.`);
217
+ }
218
+ return candidates[0];
219
+ }
220
+ async readResource(uri) {
221
+ const upstream = await this.resourceUpstream(uri);
222
+ const client = this.upstreams.get(upstream);
223
+ return this.forwardLabeled("resources/read", upstream, { resource: uri }, { uri }, (params) => client.readResource(params));
224
+ }
225
+ async getPrompt(name, args) {
226
+ let route = this.promptRoutes.get(name);
227
+ if (!route) {
228
+ await this.listPrompts();
229
+ route = this.promptRoutes.get(name);
230
+ }
231
+ if (!route)
232
+ throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`);
233
+ const client = this.upstreams.get(route.upstream);
234
+ return this.forwardLabeled("prompts/get", route.upstream, { prompt: route.prompt }, { name: route.prompt, ...(args ? { arguments: args } : {}) }, (params) => client.getPrompt(params));
235
+ }
236
+ /**
237
+ * Forward a resources/read or prompts/get request through the interceptor
238
+ * chain: request interceptors → upstream → response interceptors (adtl.label).
239
+ * These requests have no tool result to carry an error, so a stopped request
240
+ * becomes a JSON-RPC error.
241
+ */
242
+ async forwardLabeled(event, upstream, target, params, send) {
243
+ const started = performance.now();
244
+ const context = {
245
+ sessionId: this.sessionId,
246
+ timestamp: new Date().toISOString(),
247
+ principal: { type: "anonymous" },
248
+ };
249
+ const config = { server: upstream };
250
+ const audit = this.invoker.modeOverride === "audit" ? { mode: "audit" } : {};
251
+ const finish = (entry) => this.audit?.write({
252
+ ts: context.timestamp,
253
+ sessionId: this.sessionId,
254
+ event,
255
+ server: upstream,
256
+ ...target,
257
+ ...entry,
258
+ durationMs: Math.round(performance.now() - started),
259
+ });
260
+ const what = "resource" in target ? `resource ${target.resource}` : `prompt ${upstream}${TOOL_SEPARATOR}${target.prompt}`;
261
+ const req = await this.invoker.runRequest({ event, payload: { method: event, params }, config, context });
262
+ if (req.blocked) {
263
+ finish({ outcome: "blocked", decision: req.blocked.decision, reason: req.blocked.reason, warnings: req.warnings });
264
+ this.log.info(`${req.blocked.decision} ${what}: ${req.blocked.reason}`);
265
+ throw new McpError(ErrorCode.InvalidRequest, `ADTL blocked this request: ${req.blocked.reason}`);
266
+ }
267
+ const forwarded = req.payload.params;
268
+ let result;
269
+ try {
270
+ result = await send(forwarded);
271
+ }
272
+ catch (err) {
273
+ finish({ outcome: "upstream_error", reason: err.message, ...audit, warnings: req.warnings });
274
+ throw err;
275
+ }
276
+ const res = await this.invoker.runResponse({ event, payload: { method: event, params: forwarded, result }, config, context });
277
+ const warnings = [...req.warnings, ...res.warnings];
278
+ if (res.blocked) {
279
+ // Fail closed: unlabeled content must not reach the agent.
280
+ finish({ outcome: "blocked", decision: res.blocked.decision, reason: res.blocked.reason, warnings });
281
+ this.log.warn(`${what} withheld: ${res.blocked.reason}`);
282
+ throw new McpError(ErrorCode.InternalError, `ADTL could not label this content, so it was withheld: ${res.blocked.reason}`);
283
+ }
284
+ const label = infoOf(res, "adtl.label");
285
+ finish({ outcome: "forwarded", ...audit, label: label.label, sessionLabel: label.sessionLabel, warnings });
286
+ return res.payload.result;
287
+ }
113
288
  async callTool(name, args, opts = {}) {
114
289
  const started = performance.now();
115
290
  let route = this.routes.get(name);