@odingard/guardian-mcp 0.3.3 → 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/README.md +54 -17
- package/dist/index.js +429 -71
- package/package.json +4 -3
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,18 @@ npm i -g @odingard/guardian-mcp
|
|
|
39
39
|
|
|
40
40
|
Nothing executes without a Guardian capability — the model's output is a proposal, never an action.
|
|
41
41
|
|
|
42
|
+
Only a call whose receipt status is `sent-verified` is returned as a plain
|
|
43
|
+
result. Every other outcome is an MCP tool error (`isError: true`) that says
|
|
44
|
+
what happened:
|
|
45
|
+
|
|
46
|
+
- DENY, or Guardian could not be reached: nothing was done. A denial says not
|
|
47
|
+
to retry the same call; an outage says to try again later.
|
|
48
|
+
- Receipt status `not-sent`: `outcome: "not-done"`, the call was not done.
|
|
49
|
+
- Receipt status `outcome-pending` or `outcome-unconfirmed`, or an execute
|
|
50
|
+
answer that was refused or lost: `outcome: "unknown"`. The call may have
|
|
51
|
+
been done, so check (`guardian_receipt`, or the target system) before
|
|
52
|
+
retrying.
|
|
53
|
+
|
|
42
54
|
## Govern an existing MCP server: `guardian-mcp proxy` (0.3)
|
|
43
55
|
|
|
44
56
|
Put Guardian in front of any stdio MCP server you already use, such as
|
|
@@ -62,11 +74,12 @@ upstream server's tools unchanged, and every call is decided first:
|
|
|
62
74
|
}
|
|
63
75
|
```
|
|
64
76
|
|
|
65
|
-
| Decision
|
|
66
|
-
|
|
|
67
|
-
| **ALLOW**
|
|
68
|
-
| **ASK**
|
|
69
|
-
| **DENY**
|
|
77
|
+
| Decision | What happens |
|
|
78
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
79
|
+
| **ALLOW** | The call is forwarded to the upstream server, then the capability is redeemed and the receipt recorded. The upstream result is returned unchanged, with Guardian's `requestId` under `_meta["com.odingard/guardian"]`, so the client can fetch the receipt with `guardian_receipt`. |
|
|
80
|
+
| **ASK** | Nothing is forwarded. The exact call is held under a `requestId`; after a human approves, `guardian_claim` forwards exactly that call, once. |
|
|
81
|
+
| **DENY** | Nothing is forwarded, ever. The result is an MCP tool error (`isError: true`) with the reason code and, when Guardian made a record of it, the `requestId`. |
|
|
82
|
+
| **Guardian unreachable** | Nothing is forwarded. The result is a tool error (`isError: true`) whose JSON keeps `decision: "DENY"` with `reasonCode: "SERVICE_UNAVAILABLE"`, and its message says Guardian could not be reached and the call can be tried again later. A held call stays held, so `guardian_claim` can be retried. |
|
|
70
83
|
|
|
71
84
|
- `guardian_claim` and `guardian_receipt` are added to the tool list.
|
|
72
85
|
- The upstream server never receives `GUARDIAN_*` settings, so it never sees
|
|
@@ -79,20 +92,42 @@ upstream server's tools unchanged, and every call is decided first:
|
|
|
79
92
|
for the agent.
|
|
80
93
|
- Since 0.3.1 the proxy does not try to register tools itself.
|
|
81
94
|
- `GUARDIAN_AUTO_REGISTER=true` turns that back on, but it is denied unless
|
|
82
|
-
the agent's grant pins `mcp.install
|
|
95
|
+
the agent's grant pins `mcp.install`, and it only works for a tool that
|
|
96
|
+
has no entry yet. An "already registered" answer is refused
|
|
97
|
+
(`RECORD_TRANSPORT_UNVERIFIED`) and nothing is forwarded, including after
|
|
98
|
+
a proxy restart for an entry the proxy created itself. Once the entries
|
|
99
|
+
exist, run the proxy without it.
|
|
83
100
|
- When Guardian refuses, the result says what to fix.
|
|
84
101
|
- Held calls live in the proxy process. Restarting the proxy drops them, and
|
|
85
102
|
nothing is forwarded.
|
|
103
|
+
- **Cancelled requests are not forwarded.** If the client cancels a
|
|
104
|
+
`tools/call` (or it times out) before the call is sent upstream, for
|
|
105
|
+
example while Guardian is still deciding, the call is not forwarded and the
|
|
106
|
+
capability is not redeemed. A `guardian_claim` cancelled that way keeps the
|
|
107
|
+
call held with its approval, so the next `guardian_claim` forwards it
|
|
108
|
+
without asking the person again. Once a call has been sent upstream it runs
|
|
109
|
+
to its real outcome, which is recorded.
|
|
110
|
+
- **A second `guardian_claim` of the same `requestId`** says the call was
|
|
111
|
+
already forwarded and must not be proposed again. If the first claim has
|
|
112
|
+
finished, its result is returned again after that notice; if it is still
|
|
113
|
+
running, the answer says to check `guardian_receipt`. The proxy remembers
|
|
114
|
+
the last 1000 claimed requests. A `requestId` it does not hold is answered
|
|
115
|
+
with "this claim forwarded nothing", and a reminder to check
|
|
116
|
+
`guardian_receipt` if it was claimed before.
|
|
117
|
+
- An unexpected proxy error after a call was forwarded says the call may
|
|
118
|
+
have run, never that it was not forwarded.
|
|
86
119
|
|
|
87
120
|
`guardian-mcp --help` prints usage. A missing setting prints a one-line
|
|
88
121
|
message naming it instead of a stack trace.
|
|
89
122
|
|
|
90
|
-
## Govern a remote MCP server: `guardian-mcp proxy --url` (
|
|
123
|
+
## Govern a remote MCP server: `guardian-mcp proxy --url` (0.3.3 and later)
|
|
91
124
|
|
|
92
|
-
**Status:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
**Status: tested against a real Guardian; production proof pending.** It ran
|
|
126
|
+
end to end over Streamable HTTP against the real Guardian server code (allow,
|
|
127
|
+
ask then approve, deny, outage), not yet on hosted production. SSE is tested
|
|
128
|
+
against local servers and a stand-in Guardian only, and no real hosted server
|
|
129
|
+
such as Composio has been tried. Composio's `x-api-key` header is
|
|
130
|
+
unconfirmed.
|
|
96
131
|
|
|
97
132
|
The proxy can also front an MCP server that runs online, such as Composio.
|
|
98
133
|
The agent still starts `guardian-mcp` over stdio like any
|
|
@@ -157,7 +192,8 @@ A server that takes a Bearer key: `--url https://mcp.example.com/mcp --header
|
|
|
157
192
|
unavailable" error. The upstream's error text (which can quote its URL
|
|
158
193
|
path, response body or headers) is not shown to the agent or logged;
|
|
159
194
|
`guardian-mcp tools --url` prints it with the URL, its path identifiers,
|
|
160
|
-
query values and header values scrubbed.
|
|
195
|
+
query values and header values scrubbed. Each query value is scrubbed as
|
|
196
|
+
typed (a `+` stays a `+`), percent-decoded, and with `+` read as a space.
|
|
161
197
|
|
|
162
198
|
What is **not** governed: a remote server the agent connects to directly
|
|
163
199
|
(for example an MCP client's own `"url"` entry, or a model provider's hosted
|
|
@@ -175,9 +211,8 @@ MCP connector), and anything the agent does without MCP. Also not covered:
|
|
|
175
211
|
[ZeroClaw](../../examples/zeroclaw/README.md) and
|
|
176
212
|
[your own container (CrewAI, LangGraph, AutoGen)](../../examples/byo/README.md).
|
|
177
213
|
Runnable example: `examples/remote-mcp.mjs` (SIMULATED by default, LIVE with
|
|
178
|
-
Guardian settings).
|
|
179
|
-
|
|
180
|
-
0.3.2 does not have `--url` yet.
|
|
214
|
+
Guardian settings). `--url` is in the published `@odingard/guardian-mcp`
|
|
215
|
+
from 0.3.3.
|
|
181
216
|
|
|
182
217
|
## See what you are about to govern: `guardian-mcp tools`
|
|
183
218
|
|
|
@@ -197,8 +232,10 @@ createIssue Ask me first "create" may change something ⚠ name not accept
|
|
|
197
232
|
|
|
198
233
|
How the suggestions are made:
|
|
199
234
|
|
|
200
|
-
-
|
|
201
|
-
-
|
|
235
|
+
- A tool the server marks destructive (`destructiveHint`) suggests Ask.
|
|
236
|
+
- So does a tool whose name contains a verb that changes something or sends data out, anywhere in the name (`create`, `update`, `delete`, `replace`, `send`, `run`, `write` and similar), even when the server marks it read-only. `get_or_create_customer`, `find_and_replace` and `list_and_delete_old_files` suggest Ask.
|
|
237
|
+
- Otherwise a tool the server marks read-only (`readOnlyHint`) suggests Allow, and so does a name that starts with a read verb (`get`, `list`, `search`, `read`, `describe`, `show`, `view`, `find`, `lookup`, `count`, `inspect`).
|
|
238
|
+
- Everything else suggests Ask, including `fetch` (it can send data to any URL) and `status`.
|
|
202
239
|
|
|
203
240
|
They are suggestions only: you set the real modes in the console. Tool names that break Guardian's naming rule are flagged, because every call to them would be denied. Add `--json` for machine-readable output.
|
|
204
241
|
|
package/dist/index.js
CHANGED
|
@@ -235,6 +235,18 @@ function secretsOf(target) {
|
|
|
235
235
|
const parsed2 = new URL(url);
|
|
236
236
|
if (parsed2.search) out.add(parsed2.search.slice(1));
|
|
237
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
|
+
}
|
|
238
250
|
for (const segment of parsed2.pathname.split("/")) {
|
|
239
251
|
if (segment.length >= 16 || segment.length >= 8 && /\d/.test(segment)) {
|
|
240
252
|
out.add(segment);
|
|
@@ -308,11 +320,26 @@ var TOOLS = [
|
|
|
308
320
|
}
|
|
309
321
|
];
|
|
310
322
|
function jsonResult(payload) {
|
|
311
|
-
return {
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
325
|
+
};
|
|
312
326
|
}
|
|
313
327
|
function errorResult(message) {
|
|
314
328
|
return { isError: true, content: [{ type: "text", text: message }] };
|
|
315
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
|
+
}
|
|
316
343
|
async function requestAndExecute(client2, result) {
|
|
317
344
|
if (!result.ok) {
|
|
318
345
|
const heldRequestId = result.requestId;
|
|
@@ -324,33 +351,62 @@ async function requestAndExecute(client2, result) {
|
|
|
324
351
|
next: "Held for human approval. Call guardian_claim with this requestId after approval."
|
|
325
352
|
});
|
|
326
353
|
}
|
|
327
|
-
return
|
|
354
|
+
return denied(
|
|
355
|
+
result.reasonCode,
|
|
356
|
+
typeof heldRequestId === "string" && heldRequestId.length > 0 ? heldRequestId : void 0
|
|
357
|
+
);
|
|
328
358
|
}
|
|
329
359
|
if (result.decision === "ALLOW" && result.capabilityToken) {
|
|
330
360
|
const executed = await client2.execute({
|
|
331
361
|
requestId: result.requestId,
|
|
332
362
|
capabilityToken: result.capabilityToken
|
|
333
363
|
});
|
|
334
|
-
|
|
335
|
-
|
|
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({
|
|
336
376
|
decision: "ALLOW",
|
|
337
377
|
requestId: result.requestId,
|
|
338
378
|
receipt: executed.receipt
|
|
339
|
-
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (status === "not-sent") {
|
|
382
|
+
return jsonError({
|
|
340
383
|
decision: "ALLOW",
|
|
341
384
|
requestId: result.requestId,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
});
|
|
345
397
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
398
|
+
if (result.decision === "ASK") {
|
|
399
|
+
return jsonResult({
|
|
400
|
+
decision: "ASK",
|
|
401
|
+
requestId: result.requestId,
|
|
402
|
+
...result.reasonCode ? { reasonCode: result.reasonCode } : {},
|
|
351
403
|
message: "Held for human approval. Call guardian_claim with this requestId after approval."
|
|
352
|
-
}
|
|
353
|
-
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return denied(
|
|
407
|
+
result.reasonCode ?? (result.decision === "DENY" ? void 0 : "MISSING_CAPABILITY"),
|
|
408
|
+
result.requestId
|
|
409
|
+
);
|
|
354
410
|
}
|
|
355
411
|
async function handleToolCall(options, name, arguments_) {
|
|
356
412
|
const { client: client2, defaultConnectionId, adapterId } = options;
|
|
@@ -410,12 +466,85 @@ var READ_VERBS = /* @__PURE__ */ new Set([
|
|
|
410
466
|
"find",
|
|
411
467
|
"lookup",
|
|
412
468
|
"count",
|
|
413
|
-
"inspect"
|
|
414
|
-
"status",
|
|
415
|
-
"fetch"
|
|
469
|
+
"inspect"
|
|
416
470
|
]);
|
|
417
|
-
|
|
418
|
-
|
|
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"
|
|
545
|
+
]);
|
|
546
|
+
function words(name) {
|
|
547
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
419
548
|
}
|
|
420
549
|
function suggestTool(tool) {
|
|
421
550
|
const governable = GUARDIAN_TOOL_NAME.test(tool.name) && tool.name.length <= 64;
|
|
@@ -428,6 +557,16 @@ function suggestTool(tool) {
|
|
|
428
557
|
governable
|
|
429
558
|
};
|
|
430
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
|
+
}
|
|
431
570
|
if (annotations.readOnlyHint === true) {
|
|
432
571
|
return {
|
|
433
572
|
name: tool.name,
|
|
@@ -436,7 +575,7 @@ function suggestTool(tool) {
|
|
|
436
575
|
governable
|
|
437
576
|
};
|
|
438
577
|
}
|
|
439
|
-
const verb =
|
|
578
|
+
const verb = tokens[0] ?? "";
|
|
440
579
|
if (READ_VERBS.has(verb)) {
|
|
441
580
|
return {
|
|
442
581
|
name: tool.name,
|
|
@@ -492,6 +631,20 @@ import {
|
|
|
492
631
|
} from "@odingard/guardian-sdk";
|
|
493
632
|
var CLAIM_TOOL = "guardian_claim";
|
|
494
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
|
+
}
|
|
495
648
|
var PROXY_GOVERNANCE_TOOLS = [
|
|
496
649
|
{
|
|
497
650
|
name: CLAIM_TOOL,
|
|
@@ -518,6 +671,14 @@ function jsonResult2(payload, isError = false) {
|
|
|
518
671
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
519
672
|
};
|
|
520
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
|
+
}
|
|
521
682
|
function isDecisionResult(value) {
|
|
522
683
|
return typeof value === "object" && value !== null && "guardian" in value && typeof value.guardian === "object";
|
|
523
684
|
}
|
|
@@ -533,6 +694,14 @@ var UPSTREAM_RESULT = /* @__PURE__ */ Symbol("guardian-mcp.upstreamResult");
|
|
|
533
694
|
function upstreamRan(value) {
|
|
534
695
|
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, UPSTREAM_RESULT);
|
|
535
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
|
+
};
|
|
536
705
|
var UpstreamListFailed = class extends Error {
|
|
537
706
|
constructor() {
|
|
538
707
|
super(
|
|
@@ -541,16 +710,44 @@ var UpstreamListFailed = class extends Error {
|
|
|
541
710
|
this.name = "UpstreamListFailed";
|
|
542
711
|
}
|
|
543
712
|
};
|
|
544
|
-
function callUpstream(upstream, name, args) {
|
|
713
|
+
function callUpstream(upstream, name, args, signal, forwarding) {
|
|
545
714
|
return async () => {
|
|
715
|
+
if (signal?.aborted) {
|
|
716
|
+
throw new UpstreamCallCancelled(name);
|
|
717
|
+
}
|
|
718
|
+
forwarding.sent = true;
|
|
546
719
|
try {
|
|
547
|
-
return {
|
|
548
|
-
|
|
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
|
+
}
|
|
549
732
|
throw new UpstreamCallFailed(name);
|
|
550
733
|
}
|
|
551
734
|
};
|
|
552
735
|
}
|
|
553
|
-
function
|
|
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
|
+
}
|
|
554
751
|
if (err instanceof UpstreamCallFailed) {
|
|
555
752
|
return jsonResult2(
|
|
556
753
|
{
|
|
@@ -569,12 +766,16 @@ function failure(err, toolName) {
|
|
|
569
766
|
requestId: err.requestId,
|
|
570
767
|
executionFailed: err.reasonCode,
|
|
571
768
|
localEffectRan: err.localEffectRan,
|
|
572
|
-
|
|
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."
|
|
573
771
|
},
|
|
574
772
|
true
|
|
575
773
|
);
|
|
576
774
|
}
|
|
577
775
|
if (err instanceof GuardianDenied) {
|
|
776
|
+
if (err.reasonCode === UNAVAILABLE) {
|
|
777
|
+
return unreachableResult(toolName, err.requestId);
|
|
778
|
+
}
|
|
578
779
|
return jsonResult2(
|
|
579
780
|
{
|
|
580
781
|
decision: "DENY",
|
|
@@ -599,17 +800,28 @@ function failure(err, toolName) {
|
|
|
599
800
|
return jsonResult2(
|
|
600
801
|
{
|
|
601
802
|
toolName,
|
|
602
|
-
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."
|
|
603
804
|
},
|
|
604
805
|
true
|
|
605
806
|
);
|
|
606
807
|
}
|
|
808
|
+
var CLAIMED_REQUESTS_LIMIT = 1e3;
|
|
607
809
|
function createGovernedProxy(options) {
|
|
608
810
|
const { upstream, log = () => {
|
|
609
811
|
} } = options;
|
|
610
812
|
const toolInvokeRequestIds = /* @__PURE__ */ new Set();
|
|
611
813
|
const pendingCalls = /* @__PURE__ */ new Map();
|
|
612
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
|
+
};
|
|
613
825
|
const recordingClient = {
|
|
614
826
|
...options.client,
|
|
615
827
|
async request(invocation) {
|
|
@@ -631,6 +843,26 @@ function createGovernedProxy(options) {
|
|
|
631
843
|
...options.adapterId ? { adapterId: options.adapterId } : {},
|
|
632
844
|
...options.agentName ? { agentName: options.agentName } : {}
|
|
633
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
|
+
}
|
|
634
866
|
let upstreamTools = null;
|
|
635
867
|
async function loadTools() {
|
|
636
868
|
if (upstreamTools) return upstreamTools;
|
|
@@ -647,8 +879,10 @@ function createGovernedProxy(options) {
|
|
|
647
879
|
upstreamTools = tools;
|
|
648
880
|
return tools;
|
|
649
881
|
}
|
|
650
|
-
function toCallResult(toolName, args, output) {
|
|
651
|
-
if (upstreamRan(output))
|
|
882
|
+
function toCallResult(toolName, args, output, allowedRequestId) {
|
|
883
|
+
if (upstreamRan(output)) {
|
|
884
|
+
return withRequestId(output[UPSTREAM_RESULT], allowedRequestId);
|
|
885
|
+
}
|
|
652
886
|
if (!isDecisionResult(output)) {
|
|
653
887
|
return jsonResult2(
|
|
654
888
|
{
|
|
@@ -670,40 +904,92 @@ function createGovernedProxy(options) {
|
|
|
670
904
|
});
|
|
671
905
|
}
|
|
672
906
|
pendingInstalls.set(g.requestId, toolName);
|
|
907
|
+
const step = heldSetupStep(toolName, g.requestId);
|
|
673
908
|
return jsonResult2({
|
|
674
909
|
decision: "ASK",
|
|
675
910
|
toolName,
|
|
676
911
|
requestId: g.requestId,
|
|
677
|
-
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.`
|
|
678
913
|
});
|
|
679
914
|
}
|
|
680
915
|
if (g.decision === "DENY") {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
);
|
|
688
929
|
}
|
|
689
930
|
return jsonResult2({ decision: "ALLOW", toolName, requestId: g.requestId });
|
|
690
931
|
}
|
|
691
|
-
|
|
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;
|
|
692
963
|
const held = pendingCalls.get(requestId);
|
|
693
964
|
if (held) {
|
|
965
|
+
if (signal?.aborted) {
|
|
966
|
+
return cancelledResult(held.name, requestId);
|
|
967
|
+
}
|
|
694
968
|
pendingCalls.delete(requestId);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
claimed = await options.client.claim(requestId);
|
|
698
|
-
} catch {
|
|
969
|
+
const putBack = () => {
|
|
970
|
+
claimedRequests.delete(requestId);
|
|
699
971
|
pendingCalls.set(requestId, held);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
972
|
+
};
|
|
973
|
+
rememberClaim(requestId, { toolName: held.name, state: "forwarding" });
|
|
974
|
+
let claimed;
|
|
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);
|
|
704
990
|
}
|
|
705
991
|
if (claimed.ok && claimed.decision === "ASK" || !claimed.ok && (claimed.reasonCode === "APPROVAL_PENDING" || claimed.reasonCode === "APPROVAL_REQUIRED")) {
|
|
706
|
-
|
|
992
|
+
putBack();
|
|
707
993
|
return jsonResult2({
|
|
708
994
|
decision: "ASK",
|
|
709
995
|
toolName: held.name,
|
|
@@ -711,39 +997,95 @@ function createGovernedProxy(options) {
|
|
|
711
997
|
message: "Still awaiting human approval. Nothing was sent."
|
|
712
998
|
});
|
|
713
999
|
}
|
|
1000
|
+
let result;
|
|
1001
|
+
const forwarding = { sent: false };
|
|
714
1002
|
try {
|
|
1003
|
+
const call = tracked();
|
|
715
1004
|
const output = await settle(
|
|
716
|
-
ctx,
|
|
1005
|
+
call.ctx,
|
|
717
1006
|
held.name,
|
|
718
1007
|
held.args,
|
|
719
1008
|
claimed,
|
|
720
|
-
callUpstream(upstream, 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()
|
|
721
1016
|
);
|
|
722
|
-
return toCallResult(held.name, held.args, output);
|
|
723
1017
|
} catch (err) {
|
|
724
|
-
|
|
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
|
+
});
|
|
725
1035
|
}
|
|
1036
|
+
return result;
|
|
726
1037
|
}
|
|
727
1038
|
const installFor = pendingInstalls.get(requestId);
|
|
728
1039
|
if (installFor) {
|
|
729
|
-
|
|
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
|
+
}
|
|
730
1067
|
if (claimed.ok && claimed.decision === "ALLOW" && claimed.capabilityToken) {
|
|
1068
|
+
forgetHeld();
|
|
1069
|
+
pendingInstalls.delete(requestId);
|
|
731
1070
|
const executed = await options.client.execute({
|
|
732
1071
|
requestId: claimed.requestId,
|
|
733
1072
|
capabilityToken: claimed.capabilityToken
|
|
734
1073
|
});
|
|
735
|
-
|
|
736
|
-
|
|
1074
|
+
if (executed.ok && step === "enable") {
|
|
1075
|
+
ctx.ensured.add(key);
|
|
1076
|
+
} else if (executed.ok) {
|
|
1077
|
+
ctx.installed?.add(key);
|
|
1078
|
+
}
|
|
737
1079
|
return jsonResult2(
|
|
738
1080
|
executed.ok ? {
|
|
739
1081
|
decision: "ALLOW",
|
|
740
1082
|
toolName: installFor,
|
|
741
|
-
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.`
|
|
742
1084
|
} : { toolName: installFor, executionFailed: executed.reasonCode },
|
|
743
1085
|
!executed.ok
|
|
744
1086
|
);
|
|
745
1087
|
}
|
|
746
|
-
if (claimed.ok && claimed.decision === "ASK") {
|
|
1088
|
+
if (claimed.ok && claimed.decision === "ASK" || !claimed.ok && (claimed.reasonCode === "APPROVAL_PENDING" || claimed.reasonCode === "APPROVAL_REQUIRED")) {
|
|
747
1089
|
return jsonResult2({
|
|
748
1090
|
decision: "ASK",
|
|
749
1091
|
toolName: installFor,
|
|
@@ -751,17 +1093,21 @@ function createGovernedProxy(options) {
|
|
|
751
1093
|
message: "Still awaiting human approval."
|
|
752
1094
|
});
|
|
753
1095
|
}
|
|
1096
|
+
forgetHeld();
|
|
754
1097
|
pendingInstalls.delete(requestId);
|
|
755
|
-
return jsonResult2(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1098
|
+
return jsonResult2(
|
|
1099
|
+
{
|
|
1100
|
+
decision: "DENY",
|
|
1101
|
+
toolName: installFor,
|
|
1102
|
+
reasonCode: claimed.ok ? claimed.reasonCode ?? "MISSING_CAPABILITY" : claimed.reasonCode
|
|
1103
|
+
},
|
|
1104
|
+
true
|
|
1105
|
+
);
|
|
760
1106
|
}
|
|
761
1107
|
return jsonResult2(
|
|
762
1108
|
{
|
|
763
1109
|
requestId,
|
|
764
|
-
error:
|
|
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.`
|
|
765
1111
|
},
|
|
766
1112
|
true
|
|
767
1113
|
);
|
|
@@ -785,13 +1131,14 @@ function createGovernedProxy(options) {
|
|
|
785
1131
|
...PROXY_GOVERNANCE_TOOLS
|
|
786
1132
|
];
|
|
787
1133
|
},
|
|
788
|
-
async callTool(name, args) {
|
|
1134
|
+
async callTool(name, args, callOptions) {
|
|
1135
|
+
const signal = callOptions?.signal;
|
|
789
1136
|
if (name === CLAIM_TOOL) {
|
|
790
1137
|
const requestId = args.requestId;
|
|
791
1138
|
if (typeof requestId !== "string" || requestId.length === 0) {
|
|
792
1139
|
return jsonResult2({ error: "requestId is required" }, true);
|
|
793
1140
|
}
|
|
794
|
-
return claim(requestId);
|
|
1141
|
+
return claim(requestId, signal);
|
|
795
1142
|
}
|
|
796
1143
|
if (name === RECEIPT_TOOL) {
|
|
797
1144
|
const requestId = args.requestId;
|
|
@@ -819,16 +1166,21 @@ function createGovernedProxy(options) {
|
|
|
819
1166
|
if (!tools.has(name)) {
|
|
820
1167
|
return jsonResult2({ error: `unknown tool: ${name}` }, true);
|
|
821
1168
|
}
|
|
1169
|
+
if (signal?.aborted) {
|
|
1170
|
+
return cancelledResult(name);
|
|
1171
|
+
}
|
|
1172
|
+
const forwarding = { sent: false };
|
|
822
1173
|
try {
|
|
1174
|
+
const call = tracked();
|
|
823
1175
|
const output = await runThroughGuardian(
|
|
824
|
-
ctx,
|
|
1176
|
+
call.ctx,
|
|
825
1177
|
name,
|
|
826
1178
|
args,
|
|
827
|
-
callUpstream(upstream, name, args)
|
|
1179
|
+
callUpstream(upstream, name, args, signal, forwarding)
|
|
828
1180
|
);
|
|
829
|
-
return toCallResult(name, args, output);
|
|
1181
|
+
return toCallResult(name, args, output, call.allowedRequestId());
|
|
830
1182
|
} catch (err) {
|
|
831
|
-
return failure(err, name);
|
|
1183
|
+
return failure(err, name, forwarding);
|
|
832
1184
|
}
|
|
833
1185
|
}
|
|
834
1186
|
};
|
|
@@ -892,7 +1244,10 @@ function toolListFailure(target, err) {
|
|
|
892
1244
|
function clientUpstream(client2) {
|
|
893
1245
|
return {
|
|
894
1246
|
listTools: () => listAllTools(client2),
|
|
895
|
-
async callTool(name, args) {
|
|
1247
|
+
async callTool(name, args, options) {
|
|
1248
|
+
if (options?.signal?.aborted) {
|
|
1249
|
+
throw new UpstreamCallCancelled(name);
|
|
1250
|
+
}
|
|
896
1251
|
return await client2.callTool({
|
|
897
1252
|
name,
|
|
898
1253
|
arguments: args
|
|
@@ -974,9 +1329,12 @@ if (config.mode.kind === "proxy") {
|
|
|
974
1329
|
}));
|
|
975
1330
|
server.setRequestHandler(
|
|
976
1331
|
CallToolRequestSchema,
|
|
977
|
-
async (request) => await proxy.callTool(
|
|
1332
|
+
async (request, extra) => await proxy.callTool(
|
|
978
1333
|
request.params.name,
|
|
979
|
-
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 }
|
|
980
1338
|
)
|
|
981
1339
|
);
|
|
982
1340
|
} else {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odingard/guardian-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"mcpName": "io.github.Odingard/guardian-mcp",
|
|
5
|
-
"description": "MCP server that routes an agent's tool calls through Guardian — the human authority layer for autonomous AI. Every tool call gets a deterministic ALLOW/ASK/DENY decision before anything executes; ASK holds for human approval, DENY never runs, every
|
|
5
|
+
"description": "MCP server that routes an agent's tool calls through Guardian — the human authority layer for autonomous AI. Every tool call gets a deterministic ALLOW/ASK/DENY decision before anything executes; ASK holds for human approval, DENY never runs, and every allowed call that completes carries a verifiable receipt.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"author": "Odingard Security LLC",
|
|
7
8
|
"type": "module",
|
|
8
9
|
"main": "dist/index.js",
|
|
9
10
|
"bin": {
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
28
|
-
"@odingard/guardian-sdk": "^0.3.
|
|
29
|
+
"@odingard/guardian-sdk": "^0.3.9"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "^22",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
|
3
3
|
"name": "io.github.Odingard/guardian-mcp",
|
|
4
4
|
"description": "Routes agent tool calls through Guardian for ALLOW/ASK/DENY before execution.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.4",
|
|
6
6
|
"tools": [
|
|
7
7
|
{
|
|
8
8
|
"name": "guardian_invoke_tool",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{
|
|
27
27
|
"registryType": "npm",
|
|
28
28
|
"identifier": "@odingard/guardian-mcp",
|
|
29
|
-
"version": "0.3.
|
|
29
|
+
"version": "0.3.4",
|
|
30
30
|
"transport": {
|
|
31
31
|
"type": "stdio"
|
|
32
32
|
},
|