@odingard/guardian-mcp 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "node:module";
5
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
7
  import {
@@ -18,10 +16,21 @@ var USAGE = `guardian-mcp \u2014 route an agent's tool calls through Guardian (A
18
16
  Usage:
19
17
  guardian-mcp Guardian tools (guardian_invoke_tool, guardian_claim, guardian_receipt)
20
18
  guardian-mcp proxy -- <command> [args...] Govern every tool of an existing stdio MCP server
19
+ guardian-mcp proxy --url <https-url> [--transport streamable-http|sse] [--header "Name: value"]...
20
+ Govern every tool of a remote MCP server (Streamable
21
+ HTTP by default, or SSE). The agent still talks stdio.
21
22
  guardian-mcp tools [--json] -- <command> [args...]
23
+ guardian-mcp tools [--json] --url <https-url> [--transport ...] [--header ...]
22
24
  List a server's tools with a suggested Guardian mode
23
25
  for each (no Guardian credential needed)
24
26
 
27
+ Remote upstreams:
28
+ Write secrets as \${ENV_VAR} references, e.g. --header 'x-api-key: \${COMPOSIO_API_KEY}'
29
+ (single quotes in a shell, so the key never appears in the process list; JSON configs need
30
+ no quoting). guardian-mcp expands them itself, refuses a missing variable and any
31
+ GUARDIAN_* variable, and never prints the URL or header values. The URL must be https://
32
+ (plain http:// only for localhost).
33
+
25
34
  Environment:
26
35
  GUARDIAN_BASE_URL required, e.g. https://guardian-api.odingard.com
27
36
  GUARDIAN_AGENT_CREDENTIAL required, the agent credential (gac1.\u2026) from the Guardian console
@@ -31,8 +40,9 @@ Environment:
31
40
  use (mcp.install). Default off: register the tools once in the
32
41
  console as "My own code (record-only)" and allow them for the agent
33
42
 
34
- Example (proxy):
43
+ Examples (proxy):
35
44
  guardian-mcp proxy -- npx -y @modelcontextprotocol/server-filesystem ./data
45
+ guardian-mcp proxy --url https://mcp.example.com/mcp --header 'Authorization: Bearer \${REMOTE_MCP_API_KEY}'
36
46
  `;
37
47
  function parseCli(argv, env) {
38
48
  let mode = { kind: "dispatch" };
@@ -41,39 +51,22 @@ function parseCli(argv, env) {
41
51
  }
42
52
  if (argv[0] === "tools") {
43
53
  const json = argv[1] === "--json";
44
- const after = argv.slice(json ? 2 : 1);
45
- const rest = after[0] === "--" ? after.slice(1) : after;
46
- const [command, ...args] = rest;
47
- if (!command) {
48
- return {
49
- ok: false,
50
- message: `guardian-mcp tools: missing the MCP server command.
51
-
52
- ${USAGE}`
53
- };
54
- }
54
+ const target = parseTarget("tools", argv.slice(json ? 2 : 1), env);
55
+ if (!target.ok) return target;
55
56
  return {
56
57
  ok: true,
57
58
  config: {
58
59
  baseUrl: "",
59
60
  credential: "",
60
61
  autoRegister: false,
61
- mode: { kind: "tools", command, args, json }
62
+ mode: { kind: "tools", json, ...target.target }
62
63
  }
63
64
  };
64
65
  }
65
66
  if (argv[0] === "proxy") {
66
- const rest = argv[1] === "--" ? argv.slice(2) : argv.slice(1);
67
- const [command, ...args] = rest;
68
- if (!command) {
69
- return {
70
- ok: false,
71
- message: `guardian-mcp proxy: missing the upstream MCP server command.
72
-
73
- ${USAGE}`
74
- };
75
- }
76
- mode = { kind: "proxy", command, args };
67
+ const target = parseTarget("proxy", argv.slice(1), env);
68
+ if (!target.ok) return target;
69
+ mode = { kind: "proxy", ...target.target };
77
70
  } else if (argv.length > 0) {
78
71
  return {
79
72
  ok: false,
@@ -103,7 +96,7 @@ ${USAGE}`
103
96
  credential: env.GUARDIAN_AGENT_CREDENTIAL,
104
97
  ...env.GUARDIAN_CONNECTION_ID ? { connectionId: env.GUARDIAN_CONNECTION_ID } : {},
105
98
  ...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {},
106
- // WO-050: off by default — production denies first-use registration
99
+ // Off by default — production denies first-use registration
107
100
  // unless the grant pins mcp.install. Opt in with GUARDIAN_AUTO_REGISTER=true.
108
101
  autoRegister: /^(true|1|yes|on)$/i.test(env.GUARDIAN_AUTO_REGISTER ?? ""),
109
102
  mode
@@ -118,6 +111,168 @@ function upstreamEnv(env) {
118
111
  }
119
112
  return out;
120
113
  }
114
+ var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
115
+ var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
116
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
117
+ function expandEnv(cmd, value, env, where) {
118
+ for (const match of value.matchAll(ENV_REF)) {
119
+ const name = match[1];
120
+ if (name.startsWith("GUARDIAN_")) {
121
+ return {
122
+ ok: false,
123
+ message: `guardian-mcp ${cmd}: ${where} refers to \${${name}}; Guardian settings are never sent to the upstream server.`
124
+ };
125
+ }
126
+ if (!env[name]) {
127
+ return {
128
+ ok: false,
129
+ message: `guardian-mcp ${cmd}: ${where} refers to \${${name}}, which is not set.`
130
+ };
131
+ }
132
+ }
133
+ return {
134
+ ok: true,
135
+ value: value.replace(ENV_REF, (_, name) => env[name])
136
+ };
137
+ }
138
+ function parseTarget(cmd, argv, env) {
139
+ const fail = (message) => ({
140
+ ok: false,
141
+ message: `guardian-mcp ${cmd}: ${message}
142
+
143
+ ${USAGE}`
144
+ });
145
+ if (argv[0] === "--") argv = argv.slice(1);
146
+ else if (argv[0]?.startsWith("--")) return parseRemote(cmd, argv, env, fail);
147
+ const [command, ...args] = argv;
148
+ if (!command) {
149
+ return fail(
150
+ cmd === "proxy" ? "missing the upstream MCP server command (or --url for a remote server)." : "missing the MCP server command (or --url for a remote server)."
151
+ );
152
+ }
153
+ return { ok: true, target: { command, args } };
154
+ }
155
+ function parseRemote(cmd, argv, env, fail) {
156
+ let rawUrl;
157
+ let transport = "streamable-http";
158
+ const rawHeaders = [];
159
+ for (let i = 0; i < argv.length; i++) {
160
+ const arg = argv[i];
161
+ const eq = arg.indexOf("=");
162
+ const flag = arg.startsWith("--") && eq > 0 ? arg.slice(0, eq) : arg;
163
+ let value;
164
+ if (flag !== arg) value = arg.slice(eq + 1);
165
+ else if (flag === "--url" || flag === "--transport" || flag === "--header") {
166
+ value = argv[++i];
167
+ }
168
+ if (flag === "--url") {
169
+ if (rawUrl !== void 0) return fail("--url given more than once.");
170
+ if (!value) return fail("--url needs a value.");
171
+ rawUrl = value;
172
+ } else if (flag === "--transport") {
173
+ if (value !== "streamable-http" && value !== "sse" && value !== "http") {
174
+ return fail('--transport must be "streamable-http" or "sse".');
175
+ }
176
+ transport = value === "sse" ? "sse" : "streamable-http";
177
+ } else if (flag === "--header") {
178
+ if (!value) return fail("--header needs a value.");
179
+ rawHeaders.push(value);
180
+ } else if (flag === "--") {
181
+ return fail("give either --url or a command after --, not both.");
182
+ } else {
183
+ return fail(
184
+ `unknown option at position ${i + 1} (expected --url, --transport or --header).`
185
+ );
186
+ }
187
+ }
188
+ if (!rawUrl) return fail("--url is required with --transport or --header.");
189
+ const expandedUrl = expandEnv(cmd, rawUrl, env, "--url");
190
+ if (!expandedUrl.ok) return expandedUrl;
191
+ let url;
192
+ try {
193
+ url = new URL(expandedUrl.value);
194
+ } catch {
195
+ return fail("--url is not a valid URL.");
196
+ }
197
+ if (url.username || url.password) {
198
+ return fail(
199
+ "--url must not carry a user name or password; send credentials with --header."
200
+ );
201
+ }
202
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK.has(url.hostname))) {
203
+ return fail(
204
+ "--url must be https:// (plain http:// is accepted only for localhost)."
205
+ );
206
+ }
207
+ const headers = {};
208
+ for (const [n, raw] of rawHeaders.entries()) {
209
+ const colon = raw.indexOf(":");
210
+ const name = colon > 0 ? raw.slice(0, colon).trim() : "";
211
+ if (!name || !HEADER_NAME.test(name)) {
212
+ return fail(`--header #${n + 1} must look like "Name: value".`);
213
+ }
214
+ const expanded = expandEnv(
215
+ cmd,
216
+ raw.slice(colon + 1).trim(),
217
+ env,
218
+ `--header ${name}`
219
+ );
220
+ if (!expanded.ok) return expanded;
221
+ if (!expanded.value) return fail(`--header ${name} has an empty value.`);
222
+ if (/[\r\n]/.test(expanded.value))
223
+ return fail(`--header ${name} contains a line break.`);
224
+ headers[name] = expanded.value;
225
+ }
226
+ return {
227
+ ok: true,
228
+ target: { remote: { url: url.href, transport, headers } }
229
+ };
230
+ }
231
+ function secretsOf(target) {
232
+ if (!("remote" in target)) return [];
233
+ const { url, headers } = target.remote;
234
+ const out = /* @__PURE__ */ new Set([url]);
235
+ const parsed2 = new URL(url);
236
+ if (parsed2.search) out.add(parsed2.search.slice(1));
237
+ for (const v of parsed2.searchParams.values()) if (v.length >= 4) out.add(v);
238
+ for (const pair of parsed2.search.slice(1).split("&")) {
239
+ const raw = pair.slice(pair.indexOf("=") + 1);
240
+ if (pair.indexOf("=") < 0 || raw.length < 4) continue;
241
+ const forms = [raw, raw.replace(/\+/g, " ")];
242
+ for (const form of [...forms]) {
243
+ try {
244
+ forms.push(decodeURIComponent(form));
245
+ } catch {
246
+ }
247
+ }
248
+ for (const form of forms) if (form.length >= 4) out.add(form);
249
+ }
250
+ for (const segment of parsed2.pathname.split("/")) {
251
+ if (segment.length >= 16 || segment.length >= 8 && /\d/.test(segment)) {
252
+ out.add(segment);
253
+ out.add(decodeURIComponent(segment));
254
+ }
255
+ }
256
+ for (const value of Object.values(headers)) {
257
+ out.add(value);
258
+ const parts = value.split(/\s+/);
259
+ for (const part of parts) if (part.length >= 8) out.add(part);
260
+ }
261
+ return [...out].sort((a, b) => b.length - a.length);
262
+ }
263
+ function redact(text, secrets) {
264
+ let out = text;
265
+ for (const secret of secrets) {
266
+ if (secret) out = out.split(secret).join("[redacted]");
267
+ }
268
+ return out;
269
+ }
270
+ function describeTarget(target) {
271
+ if ("remote" in target) {
272
+ return `${new URL(target.remote.url).origin} (${target.remote.transport})`;
273
+ }
274
+ return [target.command, ...target.args].join(" ");
275
+ }
121
276
 
122
277
  // src/dispatch.ts
123
278
  var TOOLS = [
@@ -165,11 +320,26 @@ var TOOLS = [
165
320
  }
166
321
  ];
167
322
  function jsonResult(payload) {
168
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
323
+ return {
324
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
325
+ };
169
326
  }
170
327
  function errorResult(message) {
171
328
  return { isError: true, content: [{ type: "text", text: message }] };
172
329
  }
330
+ function jsonError(payload) {
331
+ return { ...jsonResult(payload), isError: true };
332
+ }
333
+ var CHECK_FIRST = "Call guardian_receipt with this requestId, or check the target system, before retrying.";
334
+ function denied(reasonCode, requestId) {
335
+ const code = reasonCode ?? "DENIED";
336
+ return jsonError({
337
+ decision: "DENY",
338
+ reasonCode: code,
339
+ ...requestId ? { requestId } : {},
340
+ message: code === "SERVICE_UNAVAILABLE" ? "Guardian could not be reached, so nothing was done. Try again later." : "Guardian denied this call. Nothing was done. Do not retry the same call."
341
+ });
342
+ }
173
343
  async function requestAndExecute(client2, result) {
174
344
  if (!result.ok) {
175
345
  const heldRequestId = result.requestId;
@@ -181,33 +351,62 @@ async function requestAndExecute(client2, result) {
181
351
  next: "Held for human approval. Call guardian_claim with this requestId after approval."
182
352
  });
183
353
  }
184
- return jsonResult({ decision: "DENY", reasonCode: result.reasonCode });
354
+ return denied(
355
+ result.reasonCode,
356
+ typeof heldRequestId === "string" && heldRequestId.length > 0 ? heldRequestId : void 0
357
+ );
185
358
  }
186
359
  if (result.decision === "ALLOW" && result.capabilityToken) {
187
360
  const executed = await client2.execute({
188
361
  requestId: result.requestId,
189
362
  capabilityToken: result.capabilityToken
190
363
  });
191
- return jsonResult(
192
- executed.ok ? {
364
+ if (!executed.ok) {
365
+ return jsonError({
366
+ decision: "ALLOW",
367
+ requestId: result.requestId,
368
+ outcome: "unknown",
369
+ executionFailed: executed.reasonCode,
370
+ message: `Guardian could not confirm the outcome (${executed.reasonCode}). The call may have been done. ${CHECK_FIRST}`
371
+ });
372
+ }
373
+ const status = executed.receipt?.status;
374
+ if (status === "sent-verified") {
375
+ return jsonResult({
193
376
  decision: "ALLOW",
194
377
  requestId: result.requestId,
195
378
  receipt: executed.receipt
196
- } : {
379
+ });
380
+ }
381
+ if (status === "not-sent") {
382
+ return jsonError({
197
383
  decision: "ALLOW",
198
384
  requestId: result.requestId,
199
- executionFailed: executed.reasonCode
200
- }
201
- );
385
+ outcome: "not-done",
386
+ receipt: executed.receipt,
387
+ message: "Guardian did not send the call (receipt status not-sent). It was not done."
388
+ });
389
+ }
390
+ return jsonError({
391
+ decision: "ALLOW",
392
+ requestId: result.requestId,
393
+ outcome: "unknown",
394
+ receipt: executed.receipt,
395
+ message: `The outcome is unknown (receipt status ${typeof status === "string" ? status : "missing"}). The call may have been done. ${CHECK_FIRST}`
396
+ });
202
397
  }
203
- return jsonResult({
204
- decision: result.decision,
205
- requestId: result.requestId,
206
- ...result.reasonCode ? { reasonCode: result.reasonCode } : {},
207
- ...result.decision === "ASK" ? {
398
+ if (result.decision === "ASK") {
399
+ return jsonResult({
400
+ decision: "ASK",
401
+ requestId: result.requestId,
402
+ ...result.reasonCode ? { reasonCode: result.reasonCode } : {},
208
403
  message: "Held for human approval. Call guardian_claim with this requestId after approval."
209
- } : {}
210
- });
404
+ });
405
+ }
406
+ return denied(
407
+ result.reasonCode ?? (result.decision === "DENY" ? void 0 : "MISSING_CAPABILITY"),
408
+ result.requestId
409
+ );
211
410
  }
212
411
  async function handleToolCall(options, name, arguments_) {
213
412
  const { client: client2, defaultConnectionId, adapterId } = options;
@@ -267,12 +466,85 @@ var READ_VERBS = /* @__PURE__ */ new Set([
267
466
  "find",
268
467
  "lookup",
269
468
  "count",
270
- "inspect",
271
- "status",
272
- "fetch"
469
+ "inspect"
470
+ ]);
471
+ var WRITE_VERBS = /* @__PURE__ */ new Set([
472
+ "add",
473
+ "append",
474
+ "apply",
475
+ "approve",
476
+ "archive",
477
+ "assign",
478
+ "buy",
479
+ "cancel",
480
+ "charge",
481
+ "clear",
482
+ "close",
483
+ "commit",
484
+ "copy",
485
+ "create",
486
+ "delete",
487
+ "deploy",
488
+ "destroy",
489
+ "disable",
490
+ "drop",
491
+ "edit",
492
+ "enable",
493
+ "exec",
494
+ "execute",
495
+ "fork",
496
+ "grant",
497
+ "import",
498
+ "insert",
499
+ "install",
500
+ "invite",
501
+ "kill",
502
+ "lock",
503
+ "merge",
504
+ "modify",
505
+ "move",
506
+ "notify",
507
+ "patch",
508
+ "pay",
509
+ "post",
510
+ "publish",
511
+ "purge",
512
+ "push",
513
+ "put",
514
+ "refund",
515
+ "remove",
516
+ "rename",
517
+ "replace",
518
+ "reply",
519
+ "reset",
520
+ "restart",
521
+ "restore",
522
+ "revoke",
523
+ "rm",
524
+ "run",
525
+ "save",
526
+ "schedule",
527
+ "sell",
528
+ "send",
529
+ "set",
530
+ "share",
531
+ "sign",
532
+ "start",
533
+ "stop",
534
+ "submit",
535
+ "sync",
536
+ "transfer",
537
+ "trigger",
538
+ "truncate",
539
+ "uninstall",
540
+ "unlock",
541
+ "update",
542
+ "upload",
543
+ "upsert",
544
+ "write"
273
545
  ]);
274
- function firstWord(name) {
275
- return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).find(Boolean) ?? "";
546
+ function words(name) {
547
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
276
548
  }
277
549
  function suggestTool(tool) {
278
550
  const governable = GUARDIAN_TOOL_NAME.test(tool.name) && tool.name.length <= 64;
@@ -285,6 +557,16 @@ function suggestTool(tool) {
285
557
  governable
286
558
  };
287
559
  }
560
+ const tokens = words(tool.name);
561
+ const writeVerb = tokens.find((token) => WRITE_VERBS.has(token));
562
+ if (writeVerb) {
563
+ return {
564
+ name: tool.name,
565
+ suggestedMode: "ask",
566
+ why: `"${writeVerb}" may change something`,
567
+ governable
568
+ };
569
+ }
288
570
  if (annotations.readOnlyHint === true) {
289
571
  return {
290
572
  name: tool.name,
@@ -293,7 +575,7 @@ function suggestTool(tool) {
293
575
  governable
294
576
  };
295
577
  }
296
- const verb = firstWord(tool.name);
578
+ const verb = tokens[0] ?? "";
297
579
  if (READ_VERBS.has(verb)) {
298
580
  return {
299
581
  name: tool.name,
@@ -309,7 +591,7 @@ function suggestTool(tool) {
309
591
  governable
310
592
  };
311
593
  }
312
- function renderSuggestions(serverCommand, tools) {
594
+ function renderSuggestions(serverCommand, tools, remote = false) {
313
595
  const rows = tools.map(suggestTool);
314
596
  const width = Math.max(4, ...rows.map((r) => r.name.length));
315
597
  const lines = [
@@ -322,7 +604,7 @@ function renderSuggestions(serverCommand, tools) {
322
604
  "",
323
605
  "Register these in the Guardian console (Connections \u2192 Register a tool server \u2192",
324
606
  "record-only), then set each mode for your agent. Run the server through",
325
- "`guardian-mcp proxy -- <same command>` so every call is decided first."
607
+ remote ? "`guardian-mcp proxy --url <same url> --header \u2026` so every call is decided first." : "`guardian-mcp proxy -- <same command>` so every call is decided first."
326
608
  ];
327
609
  const ungovernable = rows.filter((r) => !r.governable);
328
610
  if (ungovernable.length > 0) {
@@ -349,6 +631,20 @@ import {
349
631
  } from "@odingard/guardian-sdk";
350
632
  var CLAIM_TOOL = "guardian_claim";
351
633
  var RECEIPT_TOOL = "guardian_receipt";
634
+ var GUARDIAN_RESULT_META = "com.odingard/guardian";
635
+ var UNAVAILABLE = "SERVICE_UNAVAILABLE";
636
+ function unreachableResult(toolName, requestId) {
637
+ return jsonResult2(
638
+ {
639
+ decision: "DENY",
640
+ toolName,
641
+ reasonCode: UNAVAILABLE,
642
+ ...requestId ? { requestId } : {},
643
+ message: `Guardian could not be reached, so ${toolName} was not checked and nothing was forwarded. Try again later.`
644
+ },
645
+ true
646
+ );
647
+ }
352
648
  var PROXY_GOVERNANCE_TOOLS = [
353
649
  {
354
650
  name: CLAIM_TOOL,
@@ -375,10 +671,93 @@ function jsonResult2(payload, isError = false) {
375
671
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
376
672
  };
377
673
  }
674
+ function withRequestId(result, requestId) {
675
+ if (!requestId) return result;
676
+ const meta = typeof result._meta === "object" && result._meta !== null ? result._meta : {};
677
+ return {
678
+ ...result,
679
+ _meta: { ...meta, [GUARDIAN_RESULT_META]: { requestId } }
680
+ };
681
+ }
378
682
  function isDecisionResult(value) {
379
683
  return typeof value === "object" && value !== null && "guardian" in value && typeof value.guardian === "object";
380
684
  }
381
- function failure(err, toolName) {
685
+ var UpstreamCallFailed = class extends Error {
686
+ constructor(toolName) {
687
+ super("upstream call failed");
688
+ this.toolName = toolName;
689
+ this.name = "UpstreamCallFailed";
690
+ }
691
+ toolName;
692
+ };
693
+ var UPSTREAM_RESULT = /* @__PURE__ */ Symbol("guardian-mcp.upstreamResult");
694
+ function upstreamRan(value) {
695
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, UPSTREAM_RESULT);
696
+ }
697
+ var UpstreamCallCancelled = class extends Error {
698
+ constructor(toolName) {
699
+ super("the call was cancelled before it was forwarded");
700
+ this.toolName = toolName;
701
+ this.name = "UpstreamCallCancelled";
702
+ }
703
+ toolName;
704
+ };
705
+ var UpstreamListFailed = class extends Error {
706
+ constructor() {
707
+ super(
708
+ "guardian-mcp: the upstream MCP server's tool list is unavailable; no tools are served."
709
+ );
710
+ this.name = "UpstreamListFailed";
711
+ }
712
+ };
713
+ function callUpstream(upstream, name, args, signal, forwarding) {
714
+ return async () => {
715
+ if (signal?.aborted) {
716
+ throw new UpstreamCallCancelled(name);
717
+ }
718
+ forwarding.sent = true;
719
+ try {
720
+ return {
721
+ [UPSTREAM_RESULT]: await upstream.callTool(
722
+ name,
723
+ args,
724
+ signal ? { signal } : void 0
725
+ )
726
+ };
727
+ } catch (err) {
728
+ if (err instanceof UpstreamCallCancelled) {
729
+ forwarding.sent = false;
730
+ throw err;
731
+ }
732
+ throw new UpstreamCallFailed(name);
733
+ }
734
+ };
735
+ }
736
+ function cancelledResult(toolName, requestId) {
737
+ return jsonResult2(
738
+ {
739
+ toolName,
740
+ ...requestId ? { requestId } : {},
741
+ cancelled: true,
742
+ message: requestId ? `The request was cancelled before ${toolName} was forwarded. Nothing was forwarded; the approved call is still held, so call ${CLAIM_TOOL} with this requestId to forward it.` : `The request was cancelled before ${toolName} was forwarded. Nothing was forwarded.`
743
+ },
744
+ true
745
+ );
746
+ }
747
+ function failure(err, toolName, forwarding) {
748
+ if (err instanceof UpstreamCallCancelled) {
749
+ return cancelledResult(toolName);
750
+ }
751
+ if (err instanceof UpstreamCallFailed) {
752
+ return jsonResult2(
753
+ {
754
+ decision: "ALLOW",
755
+ toolName,
756
+ error: "Guardian allowed the call but the upstream MCP server did not return a result. It may or may not have run; check before retrying. The capability was not redeemed."
757
+ },
758
+ true
759
+ );
760
+ }
382
761
  if (err instanceof GuardianExecutionFailed) {
383
762
  return jsonResult2(
384
763
  {
@@ -387,12 +766,16 @@ function failure(err, toolName) {
387
766
  requestId: err.requestId,
388
767
  executionFailed: err.reasonCode,
389
768
  localEffectRan: err.localEffectRan,
390
- message: err.localEffectRan ? "The upstream call ran but Guardian refused to redeem the capability. Treat this as an incident, not a success." : "Guardian could not redeem the capability; the call was not completed."
769
+ ...err.outcomeUnknown ? { outcomeUnknown: true } : {},
770
+ message: err.outcomeUnknown ? err.localEffectRan ? `The upstream call ran, but Guardian could not confirm that it was recorded. Do not call it again; check this requestId with ${RECEIPT_TOOL}.` : `Guardian could not confirm whether the call completed. It may have run. Do not call it again; check this requestId with ${RECEIPT_TOOL}.` : err.localEffectRan ? "The upstream call ran but Guardian refused to redeem the capability. Do not call it again. Treat this as an incident, not a success." : "Guardian could not redeem the capability; the call was not completed."
391
771
  },
392
772
  true
393
773
  );
394
774
  }
395
775
  if (err instanceof GuardianDenied) {
776
+ if (err.reasonCode === UNAVAILABLE) {
777
+ return unreachableResult(toolName, err.requestId);
778
+ }
396
779
  return jsonResult2(
397
780
  {
398
781
  decision: "DENY",
@@ -417,17 +800,28 @@ function failure(err, toolName) {
417
800
  return jsonResult2(
418
801
  {
419
802
  toolName,
420
- error: "Guardian proxy error; the call was not forwarded."
803
+ error: forwarding.sent ? "Guardian proxy error after the call was forwarded. It may have run; check before retrying." : "Guardian proxy error; the call was not forwarded."
421
804
  },
422
805
  true
423
806
  );
424
807
  }
808
+ var CLAIMED_REQUESTS_LIMIT = 1e3;
425
809
  function createGovernedProxy(options) {
426
810
  const { upstream, log = () => {
427
811
  } } = options;
428
812
  const toolInvokeRequestIds = /* @__PURE__ */ new Set();
429
813
  const pendingCalls = /* @__PURE__ */ new Map();
430
814
  const pendingInstalls = /* @__PURE__ */ new Map();
815
+ const claimedCapabilities = /* @__PURE__ */ new Map();
816
+ const claimedRequests = /* @__PURE__ */ new Map();
817
+ const rememberClaim = (requestId, entry) => {
818
+ claimedRequests.delete(requestId);
819
+ claimedRequests.set(requestId, entry);
820
+ while (claimedRequests.size > CLAIMED_REQUESTS_LIMIT) {
821
+ const oldest = claimedRequests.keys().next().value;
822
+ claimedRequests.delete(oldest);
823
+ }
824
+ };
431
825
  const recordingClient = {
432
826
  ...options.client,
433
827
  async request(invocation) {
@@ -449,6 +843,26 @@ function createGovernedProxy(options) {
449
843
  ...options.adapterId ? { adapterId: options.adapterId } : {},
450
844
  ...options.agentName ? { agentName: options.agentName } : {}
451
845
  });
846
+ ctx.installed ??= /* @__PURE__ */ new Set();
847
+ function heldSetupStep(toolName, requestId) {
848
+ const held = ctx.heldSetup?.get(`${options.connectionId}:${toolName}`);
849
+ return held?.requestId === requestId ? held.step : void 0;
850
+ }
851
+ function tracked() {
852
+ let allowed;
853
+ return {
854
+ ctx: {
855
+ ...ctx,
856
+ options: {
857
+ ...ctx.options,
858
+ onReceipt: (event) => {
859
+ allowed = event.requestId;
860
+ }
861
+ }
862
+ },
863
+ allowedRequestId: () => allowed
864
+ };
865
+ }
452
866
  let upstreamTools = null;
453
867
  async function loadTools() {
454
868
  if (upstreamTools) return upstreamTools;
@@ -465,8 +879,19 @@ function createGovernedProxy(options) {
465
879
  upstreamTools = tools;
466
880
  return tools;
467
881
  }
468
- function toCallResult(toolName, args, output) {
469
- if (!isDecisionResult(output)) return output;
882
+ function toCallResult(toolName, args, output, allowedRequestId) {
883
+ if (upstreamRan(output)) {
884
+ return withRequestId(output[UPSTREAM_RESULT], allowedRequestId);
885
+ }
886
+ if (!isDecisionResult(output)) {
887
+ return jsonResult2(
888
+ {
889
+ toolName,
890
+ error: "Guardian proxy error; the call was not forwarded."
891
+ },
892
+ true
893
+ );
894
+ }
470
895
  const g = output.guardian;
471
896
  if (g.decision === "ASK") {
472
897
  if (toolInvokeRequestIds.has(g.requestId)) {
@@ -479,37 +904,92 @@ function createGovernedProxy(options) {
479
904
  });
480
905
  }
481
906
  pendingInstalls.set(g.requestId, toolName);
907
+ const step = heldSetupStep(toolName, g.requestId);
482
908
  return jsonResult2({
483
909
  decision: "ASK",
484
910
  toolName,
485
911
  requestId: g.requestId,
486
- message: `First use of ${toolName} needs a human to approve registering it with Guardian. After approval call ${CLAIM_TOOL} with this requestId, then retry the original call.`
912
+ message: step === "enable" ? `Switching on ${toolName} for this agent needs a human to approve it in Guardian. After approval call ${CLAIM_TOOL} with this requestId (or retry the original call).` : `First use of ${toolName} needs a human to approve registering it with Guardian. After approval call ${CLAIM_TOOL} with this requestId, then retry the original call.`
487
913
  });
488
914
  }
489
915
  if (g.decision === "DENY") {
490
- return jsonResult2({
491
- decision: "DENY",
492
- toolName,
493
- reasonCode: g.reasonCode,
494
- ...g.requestId ? { requestId: g.requestId } : {},
495
- message: g.message
496
- });
916
+ if (g.reasonCode === UNAVAILABLE) {
917
+ return unreachableResult(toolName, g.requestId);
918
+ }
919
+ return jsonResult2(
920
+ {
921
+ decision: "DENY",
922
+ toolName,
923
+ reasonCode: g.reasonCode,
924
+ ...g.requestId ? { requestId: g.requestId } : {},
925
+ message: g.message
926
+ },
927
+ true
928
+ );
497
929
  }
498
930
  return jsonResult2({ decision: "ALLOW", toolName, requestId: g.requestId });
499
931
  }
500
- async function claim(requestId) {
932
+ function alreadyClaimed(requestId) {
933
+ const seen = claimedRequests.get(requestId);
934
+ if (!seen) return void 0;
935
+ if (seen.state === "forwarding" || !seen.result) {
936
+ return jsonResult2(
937
+ {
938
+ toolName: seen.toolName,
939
+ requestId,
940
+ alreadyClaimed: true,
941
+ message: `This approved call is already being forwarded by an earlier ${CLAIM_TOOL}. Do not propose ${seen.toolName} again; wait, then call ${RECEIPT_TOOL} with this requestId to see what happened.`
942
+ },
943
+ true
944
+ );
945
+ }
946
+ const notice = {
947
+ type: "text",
948
+ text: `This requestId was already claimed and ${seen.toolName} was already forwarded. Do not propose it again. The earlier result follows.`
949
+ };
950
+ const earlier = seen.result;
951
+ return {
952
+ ...earlier,
953
+ isError: true,
954
+ content: [
955
+ notice,
956
+ ...Array.isArray(earlier.content) ? earlier.content : []
957
+ ]
958
+ };
959
+ }
960
+ async function claim(requestId, signal) {
961
+ const repeat = alreadyClaimed(requestId);
962
+ if (repeat) return repeat;
501
963
  const held = pendingCalls.get(requestId);
502
964
  if (held) {
965
+ if (signal?.aborted) {
966
+ return cancelledResult(held.name, requestId);
967
+ }
968
+ pendingCalls.delete(requestId);
969
+ const putBack = () => {
970
+ claimedRequests.delete(requestId);
971
+ pendingCalls.set(requestId, held);
972
+ };
973
+ rememberClaim(requestId, { toolName: held.name, state: "forwarding" });
503
974
  let claimed;
504
- try {
505
- claimed = await options.client.claim(requestId);
506
- } catch {
507
- return jsonResult2(
508
- { requestId, error: "Guardian unreachable; nothing was forwarded." },
509
- true
510
- );
975
+ const reused = claimedCapabilities.get(requestId);
976
+ if (reused) {
977
+ claimedCapabilities.delete(requestId);
978
+ claimed = reused;
979
+ } else {
980
+ try {
981
+ claimed = await options.client.claim(requestId);
982
+ } catch {
983
+ putBack();
984
+ return unreachableResult(held.name, requestId);
985
+ }
986
+ }
987
+ if (!claimed.ok && claimed.reasonCode === UNAVAILABLE) {
988
+ putBack();
989
+ return unreachableResult(held.name, requestId);
511
990
  }
512
991
  if (claimed.ok && claimed.decision === "ASK" || !claimed.ok && (claimed.reasonCode === "APPROVAL_PENDING" || claimed.reasonCode === "APPROVAL_REQUIRED")) {
992
+ putBack();
513
993
  return jsonResult2({
514
994
  decision: "ASK",
515
995
  toolName: held.name,
@@ -517,40 +997,95 @@ function createGovernedProxy(options) {
517
997
  message: "Still awaiting human approval. Nothing was sent."
518
998
  });
519
999
  }
520
- pendingCalls.delete(requestId);
1000
+ let result;
1001
+ const forwarding = { sent: false };
521
1002
  try {
1003
+ const call = tracked();
522
1004
  const output = await settle(
523
- ctx,
1005
+ call.ctx,
524
1006
  held.name,
525
1007
  held.args,
526
1008
  claimed,
527
- () => upstream.callTool(held.name, held.args)
1009
+ callUpstream(upstream, held.name, held.args, signal, forwarding)
1010
+ );
1011
+ result = toCallResult(
1012
+ held.name,
1013
+ held.args,
1014
+ output,
1015
+ call.allowedRequestId()
528
1016
  );
529
- return toCallResult(held.name, held.args, output);
530
1017
  } catch (err) {
531
- return failure(err, held.name);
1018
+ if (err instanceof UpstreamCallCancelled) {
1019
+ putBack();
1020
+ if (claimed.ok && claimed.decision === "ALLOW") {
1021
+ claimedCapabilities.set(requestId, claimed);
1022
+ }
1023
+ return cancelledResult(held.name, requestId);
1024
+ }
1025
+ result = failure(err, held.name, forwarding);
1026
+ }
1027
+ if (!claimed.ok || claimed.decision !== "ALLOW" || !claimed.capabilityToken) {
1028
+ claimedRequests.delete(requestId);
1029
+ } else {
1030
+ rememberClaim(requestId, {
1031
+ toolName: held.name,
1032
+ state: "done",
1033
+ result
1034
+ });
532
1035
  }
1036
+ return result;
533
1037
  }
534
1038
  const installFor = pendingInstalls.get(requestId);
535
1039
  if (installFor) {
536
- const claimed = await options.client.claim(requestId);
1040
+ if (signal?.aborted) {
1041
+ return jsonResult2(
1042
+ {
1043
+ toolName: installFor,
1044
+ requestId,
1045
+ cancelled: true,
1046
+ message: `The request was cancelled. Nothing was claimed; call ${CLAIM_TOOL} with this requestId again.`
1047
+ },
1048
+ true
1049
+ );
1050
+ }
1051
+ const key = `${options.connectionId}:${installFor}`;
1052
+ const step = heldSetupStep(installFor, requestId) ?? "install";
1053
+ const forgetHeld = () => {
1054
+ if (ctx.heldSetup?.get(key)?.requestId === requestId) {
1055
+ ctx.heldSetup.delete(key);
1056
+ }
1057
+ };
1058
+ let claimed;
1059
+ try {
1060
+ claimed = await options.client.claim(requestId);
1061
+ } catch {
1062
+ return unreachableResult(installFor, requestId);
1063
+ }
1064
+ if (!claimed.ok && claimed.reasonCode === UNAVAILABLE) {
1065
+ return unreachableResult(installFor, requestId);
1066
+ }
537
1067
  if (claimed.ok && claimed.decision === "ALLOW" && claimed.capabilityToken) {
1068
+ forgetHeld();
1069
+ pendingInstalls.delete(requestId);
538
1070
  const executed = await options.client.execute({
539
1071
  requestId: claimed.requestId,
540
1072
  capabilityToken: claimed.capabilityToken
541
1073
  });
542
- pendingInstalls.delete(requestId);
543
- ctx.ensured.add(`${options.connectionId}:${installFor}`);
1074
+ if (executed.ok && step === "enable") {
1075
+ ctx.ensured.add(key);
1076
+ } else if (executed.ok) {
1077
+ ctx.installed?.add(key);
1078
+ }
544
1079
  return jsonResult2(
545
1080
  executed.ok ? {
546
1081
  decision: "ALLOW",
547
1082
  toolName: installFor,
548
- message: `${installFor} is registered with Guardian. Retry the original call.`
1083
+ message: step === "enable" ? `${installFor} is switched on for this agent. Retry the original call.` : `${installFor} is registered with Guardian. Retry the original call; switching it on for this agent may need one more approval.`
549
1084
  } : { toolName: installFor, executionFailed: executed.reasonCode },
550
1085
  !executed.ok
551
1086
  );
552
1087
  }
553
- if (claimed.ok && claimed.decision === "ASK") {
1088
+ if (claimed.ok && claimed.decision === "ASK" || !claimed.ok && (claimed.reasonCode === "APPROVAL_PENDING" || claimed.reasonCode === "APPROVAL_REQUIRED")) {
554
1089
  return jsonResult2({
555
1090
  decision: "ASK",
556
1091
  toolName: installFor,
@@ -558,24 +1093,36 @@ function createGovernedProxy(options) {
558
1093
  message: "Still awaiting human approval."
559
1094
  });
560
1095
  }
1096
+ forgetHeld();
561
1097
  pendingInstalls.delete(requestId);
562
- return jsonResult2({
563
- decision: "DENY",
564
- toolName: installFor,
565
- reasonCode: claimed.ok ? claimed.reasonCode ?? "MISSING_CAPABILITY" : claimed.reasonCode
566
- });
1098
+ return jsonResult2(
1099
+ {
1100
+ decision: "DENY",
1101
+ toolName: installFor,
1102
+ reasonCode: claimed.ok ? claimed.reasonCode ?? "MISSING_CAPABILITY" : claimed.reasonCode
1103
+ },
1104
+ true
1105
+ );
567
1106
  }
568
1107
  return jsonResult2(
569
1108
  {
570
1109
  requestId,
571
- error: "No held call with this requestId in this proxy session. Nothing was forwarded."
1110
+ error: `This proxy holds no call with this requestId, so this claim forwarded nothing. If it was claimed before (in this or an earlier proxy session), the call may already have been forwarded: call ${RECEIPT_TOOL} with this requestId before proposing it again.`
572
1111
  },
573
1112
  true
574
1113
  );
575
1114
  }
576
1115
  return {
577
1116
  async listTools() {
578
- const tools = await loadTools();
1117
+ let tools;
1118
+ try {
1119
+ tools = await loadTools();
1120
+ } catch {
1121
+ log(
1122
+ "guardian-mcp proxy: the upstream MCP server's tool list is unavailable"
1123
+ );
1124
+ throw new UpstreamListFailed();
1125
+ }
579
1126
  return [
580
1127
  ...[...tools.values()].map((tool) => ({
581
1128
  ...tool,
@@ -584,13 +1131,14 @@ function createGovernedProxy(options) {
584
1131
  ...PROXY_GOVERNANCE_TOOLS
585
1132
  ];
586
1133
  },
587
- async callTool(name, args) {
1134
+ async callTool(name, args, callOptions) {
1135
+ const signal = callOptions?.signal;
588
1136
  if (name === CLAIM_TOOL) {
589
1137
  const requestId = args.requestId;
590
1138
  if (typeof requestId !== "string" || requestId.length === 0) {
591
1139
  return jsonResult2({ error: "requestId is required" }, true);
592
1140
  }
593
- return claim(requestId);
1141
+ return claim(requestId, signal);
594
1142
  }
595
1143
  if (name === RECEIPT_TOOL) {
596
1144
  const requestId = args.requestId;
@@ -603,26 +1151,123 @@ function createGovernedProxy(options) {
603
1151
  !result.ok
604
1152
  );
605
1153
  }
606
- const tools = await loadTools();
1154
+ let tools;
1155
+ try {
1156
+ tools = await loadTools();
1157
+ } catch {
1158
+ return jsonResult2(
1159
+ {
1160
+ toolName: name,
1161
+ error: "The upstream MCP server's tool list is unavailable; nothing was forwarded."
1162
+ },
1163
+ true
1164
+ );
1165
+ }
607
1166
  if (!tools.has(name)) {
608
1167
  return jsonResult2({ error: `unknown tool: ${name}` }, true);
609
1168
  }
1169
+ if (signal?.aborted) {
1170
+ return cancelledResult(name);
1171
+ }
1172
+ const forwarding = { sent: false };
610
1173
  try {
1174
+ const call = tracked();
611
1175
  const output = await runThroughGuardian(
612
- ctx,
1176
+ call.ctx,
613
1177
  name,
614
1178
  args,
615
- () => upstream.callTool(name, args)
1179
+ callUpstream(upstream, name, args, signal, forwarding)
616
1180
  );
617
- return toCallResult(name, args, output);
1181
+ return toCallResult(name, args, output, call.allowedRequestId());
618
1182
  } catch (err) {
619
- return failure(err, name);
1183
+ return failure(err, name, forwarding);
620
1184
  }
621
1185
  }
622
1186
  };
623
1187
  }
624
1188
 
1189
+ // src/upstream.ts
1190
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1191
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1192
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1193
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1194
+ var UpstreamConnectError = class extends Error {
1195
+ constructor(message) {
1196
+ super(message);
1197
+ this.name = "UpstreamConnectError";
1198
+ }
1199
+ };
1200
+ function upstreamTransport(target, env) {
1201
+ if (!("remote" in target)) {
1202
+ return new StdioClientTransport({
1203
+ command: target.command,
1204
+ args: target.args,
1205
+ env: upstreamEnv(env),
1206
+ stderr: "inherit"
1207
+ });
1208
+ }
1209
+ const { url, transport, headers } = target.remote;
1210
+ const options = {
1211
+ requestInit: { headers },
1212
+ redirectPolicy: "same-origin"
1213
+ };
1214
+ return transport === "sse" ? new SSEClientTransport(new URL(url), options) : new StreamableHTTPClientTransport(new URL(url), options);
1215
+ }
1216
+ async function connectUpstream(target, env, clientInfo, transport = upstreamTransport(target, env)) {
1217
+ const client2 = new Client(clientInfo, { capabilities: {} });
1218
+ try {
1219
+ await client2.connect(transport);
1220
+ } catch (err) {
1221
+ await client2.close().catch(() => {
1222
+ });
1223
+ const detail = err instanceof Error ? err.message : String(err);
1224
+ throw new UpstreamConnectError(
1225
+ `guardian-mcp: could not connect to the upstream MCP server at ${describeTarget(target)}: ${redact(detail, secretsOf(target)).slice(0, 300)}. No tools are served.`
1226
+ );
1227
+ }
1228
+ return client2;
1229
+ }
1230
+ async function listAllTools(client2) {
1231
+ const tools = [];
1232
+ let cursor;
1233
+ do {
1234
+ const page = await client2.listTools(cursor ? { cursor } : void 0);
1235
+ tools.push(...page.tools);
1236
+ cursor = page.nextCursor;
1237
+ } while (cursor);
1238
+ return tools;
1239
+ }
1240
+ function toolListFailure(target, err) {
1241
+ const detail = err instanceof Error ? err.message : String(err);
1242
+ return `guardian-mcp: the upstream MCP server at ${describeTarget(target)} did not return its tool list: ${redact(detail, secretsOf(target)).slice(0, 300)}.`;
1243
+ }
1244
+ function clientUpstream(client2) {
1245
+ return {
1246
+ listTools: () => listAllTools(client2),
1247
+ async callTool(name, args, options) {
1248
+ if (options?.signal?.aborted) {
1249
+ throw new UpstreamCallCancelled(name);
1250
+ }
1251
+ return await client2.callTool({
1252
+ name,
1253
+ arguments: args
1254
+ });
1255
+ }
1256
+ };
1257
+ }
1258
+
625
1259
  // src/index.ts
1260
+ async function connectOrExit(...args) {
1261
+ try {
1262
+ return await connectUpstream(...args);
1263
+ } catch (err) {
1264
+ process.stderr.write(
1265
+ `${err instanceof UpstreamConnectError ? err.message : "guardian-mcp: could not connect to the upstream MCP server. No tools are served."}
1266
+ `
1267
+ );
1268
+ process.exit(1);
1269
+ }
1270
+ }
626
1271
  var VERSION = createRequire(import.meta.url)("../package.json").version;
627
1272
  var parsed = parseCli(process.argv.slice(2), process.env);
628
1273
  if (!parsed.ok) {
@@ -633,30 +1278,25 @@ if (!parsed.ok) {
633
1278
  var config = parsed.config;
634
1279
  if (config.mode.kind === "tools") {
635
1280
  const mode = config.mode;
636
- const lister = new Client(
637
- { name: "guardian-mcp-tools", version: VERSION },
638
- { capabilities: {} }
639
- );
640
- await lister.connect(
641
- new StdioClientTransport({
642
- command: mode.command,
643
- args: mode.args,
644
- env: upstreamEnv(process.env),
645
- stderr: "inherit"
646
- })
647
- );
648
- const tools = [];
649
- let cursor;
650
- do {
651
- const page = await lister.listTools(cursor ? { cursor } : void 0);
652
- tools.push(...page.tools);
653
- cursor = page.nextCursor;
654
- } while (cursor);
1281
+ const lister = await connectOrExit(mode, process.env, {
1282
+ name: "guardian-mcp-tools",
1283
+ version: VERSION
1284
+ });
1285
+ let tools;
1286
+ try {
1287
+ tools = await listAllTools(lister);
1288
+ } catch (err) {
1289
+ process.stderr.write(`${toolListFailure(mode, err)}
1290
+ `);
1291
+ await lister.close().catch(() => {
1292
+ });
1293
+ process.exit(1);
1294
+ }
655
1295
  await lister.close();
656
- const serverCommand = [mode.command, ...mode.args].join(" ");
1296
+ const serverCommand = describeTarget(mode);
657
1297
  process.stdout.write(
658
1298
  mode.json ? `${JSON.stringify({ server: serverCommand, tools: tools.map(suggestTool) }, null, 2)}
659
- ` : `${renderSuggestions(serverCommand, tools)}
1299
+ ` : `${renderSuggestions(serverCommand, tools, "remote" in mode)}
660
1300
  `
661
1301
  );
662
1302
  process.exit(0);
@@ -671,18 +1311,10 @@ var server = new Server(
671
1311
  { capabilities: { tools: {} } }
672
1312
  );
673
1313
  if (config.mode.kind === "proxy") {
674
- const upstreamClient = new Client(
675
- { name: "guardian-mcp-proxy", version: VERSION },
676
- { capabilities: {} }
677
- );
678
- await upstreamClient.connect(
679
- new StdioClientTransport({
680
- command: config.mode.command,
681
- args: config.mode.args,
682
- env: upstreamEnv(process.env),
683
- stderr: "inherit"
684
- })
685
- );
1314
+ const upstreamClient = await connectOrExit(config.mode, process.env, {
1315
+ name: "guardian-mcp-proxy",
1316
+ version: VERSION
1317
+ });
686
1318
  const proxy = createGovernedProxy({
687
1319
  client,
688
1320
  connectionId: config.connectionId,
@@ -690,35 +1322,19 @@ if (config.mode.kind === "proxy") {
690
1322
  ...config.adapterId ? { adapterId: config.adapterId } : {},
691
1323
  log: (message) => process.stderr.write(`${message}
692
1324
  `),
693
- upstream: {
694
- async listTools() {
695
- const tools = [];
696
- let cursor;
697
- do {
698
- const page = await upstreamClient.listTools(
699
- cursor ? { cursor } : void 0
700
- );
701
- tools.push(...page.tools);
702
- cursor = page.nextCursor;
703
- } while (cursor);
704
- return tools;
705
- },
706
- async callTool(name, args) {
707
- return await upstreamClient.callTool({
708
- name,
709
- arguments: args
710
- });
711
- }
712
- }
1325
+ upstream: clientUpstream(upstreamClient)
713
1326
  });
714
1327
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
715
1328
  tools: await proxy.listTools()
716
1329
  }));
717
1330
  server.setRequestHandler(
718
1331
  CallToolRequestSchema,
719
- async (request) => await proxy.callTool(
1332
+ async (request, extra) => await proxy.callTool(
720
1333
  request.params.name,
721
- request.params.arguments ?? {}
1334
+ request.params.arguments ?? {},
1335
+ // Cancelled (notifications/cancelled) or timed-out requests are
1336
+ // not forwarded if the signal fires before the call is sent.
1337
+ { signal: extra.signal }
722
1338
  )
723
1339
  );
724
1340
  } else {