@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.
- package/README.md +63 -3
- package/dist/audit.d.ts +9 -4
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +82 -2
- package/dist/cli.js.map +1 -1
- package/dist/gateway.d.ts +22 -2
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +189 -14
- package/dist/gateway.js.map +1 -1
- package/dist/hooks/claudeCode.d.ts +43 -0
- package/dist/hooks/claudeCode.d.ts.map +1 -0
- package/dist/hooks/claudeCode.js +148 -0
- package/dist/hooks/claudeCode.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/preset.d.ts +16 -0
- package/dist/hooks/preset.d.ts.map +1 -0
- package/dist/hooks/preset.js +32 -0
- package/dist/hooks/preset.js.map +1 -0
- package/dist/hooks/run.d.ts +9 -0
- package/dist/hooks/run.d.ts.map +1 -0
- package/dist/hooks/run.js +44 -0
- package/dist/hooks/run.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/init/index.d.ts +4 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +4 -0
- package/dist/init/index.js.map +1 -0
- package/dist/init/presets.d.ts +30 -0
- package/dist/init/presets.d.ts.map +1 -0
- package/dist/init/presets.js +34 -0
- package/dist/init/presets.js.map +1 -0
- package/dist/init/run.d.ts +18 -0
- package/dist/init/run.d.ts.map +1 -0
- package/dist/init/run.js +60 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/suggest.d.ts +50 -0
- package/dist/init/suggest.d.ts.map +1 -0
- package/dist/init/suggest.js +82 -0
- package/dist/init/suggest.js.map +1 -0
- package/dist/startup.d.ts +13 -2
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +25 -6
- package/dist/startup.js.map +1 -1
- package/package.json +3 -3
- package/src/audit.ts +10 -5
- package/src/cli.ts +75 -2
- package/src/gateway.ts +238 -13
- package/src/hooks/claudeCode.ts +194 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/preset.ts +47 -0
- package/src/hooks/run.ts +49 -0
- package/src/index.ts +2 -0
- package/src/init/index.ts +3 -0
- package/src/init/presets.ts +64 -0
- package/src/init/run.ts +79 -0
- package/src/init/suggest.ts +127 -0
- 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
|
-
-
|
|
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,
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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. */
|
package/dist/audit.d.ts.map
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;
|
|
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;
|
|
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 (
|
|
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
|
-
|
|
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":";AACA
|
|
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.
|
|
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;
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
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
|
|
98
|
-
for (const
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
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);
|