@odingard/guardian-mcp 0.3.0 → 0.3.2
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 +38 -14
- package/dist/index.js +146 -4
- package/package.json +1 -1
- package/server.json +4 -4
package/README.md
CHANGED
|
@@ -31,11 +31,11 @@ npm i -g @odingard/guardian-mcp
|
|
|
31
31
|
|
|
32
32
|
## Tools
|
|
33
33
|
|
|
34
|
-
| Tool
|
|
35
|
-
|
|
|
34
|
+
| Tool | What it does |
|
|
35
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
36
36
|
| `guardian_invoke_tool` | Propose a tool call. ALLOW executes and returns the receipt; ASK returns a `requestId` held for human approval; DENY refuses with a reason code. |
|
|
37
|
-
| `guardian_claim`
|
|
38
|
-
| `guardian_receipt`
|
|
37
|
+
| `guardian_claim` | Claim an approved ASK by `requestId`, execute, return the receipt. |
|
|
38
|
+
| `guardian_receipt` | Fetch the machine-readable evidence receipts for a request. |
|
|
39
39
|
|
|
40
40
|
Nothing executes without a Guardian capability — the model's output is a proposal, never an action.
|
|
41
41
|
|
|
@@ -62,11 +62,11 @@ upstream server's tools unchanged, and every call is decided first:
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
| Decision | What happens
|
|
66
|
-
| --------- |
|
|
67
|
-
| **ALLOW** | The call is forwarded to the upstream server, then the capability is redeemed and the receipt recorded.
|
|
65
|
+
| Decision | What happens |
|
|
66
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| **ALLOW** | The call is forwarded to the upstream server, then the capability is redeemed and the receipt recorded. |
|
|
68
68
|
| **ASK** | Nothing is forwarded. The exact call is held under a `requestId`; after a human approves, `guardian_claim` forwards exactly that call, once. |
|
|
69
|
-
| **DENY** | Nothing is forwarded, ever. The same applies if Guardian is unreachable.
|
|
69
|
+
| **DENY** | Nothing is forwarded, ever. The same applies if Guardian is unreachable. |
|
|
70
70
|
|
|
71
71
|
- `guardian_claim` and `guardian_receipt` are added to the tool list.
|
|
72
72
|
- The upstream server never receives `GUARDIAN_*` settings, so it never sees
|
|
@@ -74,18 +74,42 @@ upstream server's tools unchanged, and every call is decided first:
|
|
|
74
74
|
- **Decision-gated:** an allowed call runs upstream before the capability is
|
|
75
75
|
redeemed. A refusal at redemption is reported as an incident
|
|
76
76
|
(`localEffectRan: true`), never as success.
|
|
77
|
-
- **First-run setup:** connect a tool server
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
- **First-run setup:** in the Guardian console, connect a tool server and add
|
|
78
|
+
each upstream tool as "My own code (record-only)". Then allow those tools
|
|
79
|
+
for the agent.
|
|
80
|
+
- Since 0.3.1 the proxy does not try to register tools itself.
|
|
81
|
+
- `GUARDIAN_AUTO_REGISTER=true` turns that back on, but it is denied unless
|
|
82
|
+
the agent's grant pins `mcp.install`.
|
|
83
|
+
- When Guardian refuses, the result says what to fix.
|
|
83
84
|
- Held calls live in the proxy process. Restarting the proxy drops them, and
|
|
84
85
|
nothing is forwarded.
|
|
85
86
|
|
|
86
87
|
`guardian-mcp --help` prints usage. A missing setting prints a one-line
|
|
87
88
|
message naming it instead of a stack trace.
|
|
88
89
|
|
|
90
|
+
## See what you are about to govern: `guardian-mcp tools`
|
|
91
|
+
|
|
92
|
+
Before putting a server behind the proxy, list its tools along with a suggested Guardian mode for each. This needs no Guardian credential:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx -y @odingard/guardian-mcp tools -- npx -y @modelcontextprotocol/server-filesystem ./data
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Tool Suggested Why
|
|
100
|
+
read_file Allow server marks it read-only
|
|
101
|
+
write_file Ask me first "write" may change something
|
|
102
|
+
delete_branch Ask me first server marks it destructive
|
|
103
|
+
createIssue Ask me first "create" may change something ⚠ name not accepted by Guardian
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
How the suggestions are made:
|
|
107
|
+
|
|
108
|
+
- The server's own MCP annotations (`readOnlyHint`, `destructiveHint`) are used first.
|
|
109
|
+
- Otherwise the tool's verb decides. `get`, `list` and `search` suggest Allow; everything else suggests Ask.
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
89
113
|
## License
|
|
90
114
|
|
|
91
115
|
Apache-2.0. Canonical development happens in the [Odingard Guardian monorepo](https://github.com/Odingard/guardian).
|
package/dist/index.js
CHANGED
|
@@ -18,14 +18,18 @@ var USAGE = `guardian-mcp \u2014 route an agent's tool calls through Guardian (A
|
|
|
18
18
|
Usage:
|
|
19
19
|
guardian-mcp Guardian tools (guardian_invoke_tool, guardian_claim, guardian_receipt)
|
|
20
20
|
guardian-mcp proxy -- <command> [args...] Govern every tool of an existing stdio MCP server
|
|
21
|
+
guardian-mcp tools [--json] -- <command> [args...]
|
|
22
|
+
List a server's tools with a suggested Guardian mode
|
|
23
|
+
for each (no Guardian credential needed)
|
|
21
24
|
|
|
22
25
|
Environment:
|
|
23
26
|
GUARDIAN_BASE_URL required, e.g. https://guardian-api.odingard.com
|
|
24
27
|
GUARDIAN_AGENT_CREDENTIAL required, the agent credential (gac1.\u2026) from the Guardian console
|
|
25
28
|
GUARDIAN_CONNECTION_ID tool connection id (required for proxy mode)
|
|
26
29
|
GUARDIAN_ADAPTER_ID optional adapter override
|
|
27
|
-
GUARDIAN_AUTO_REGISTER proxy: "
|
|
28
|
-
|
|
30
|
+
GUARDIAN_AUTO_REGISTER proxy: "true" to propose registering each upstream tool on first
|
|
31
|
+
use (mcp.install). Default off: register the tools once in the
|
|
32
|
+
console as "My own code (record-only)" and allow them for the agent
|
|
29
33
|
|
|
30
34
|
Example (proxy):
|
|
31
35
|
guardian-mcp proxy -- npx -y @modelcontextprotocol/server-filesystem ./data
|
|
@@ -35,6 +39,29 @@ function parseCli(argv, env) {
|
|
|
35
39
|
if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
36
40
|
return { ok: false, message: USAGE };
|
|
37
41
|
}
|
|
42
|
+
if (argv[0] === "tools") {
|
|
43
|
+
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
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
config: {
|
|
58
|
+
baseUrl: "",
|
|
59
|
+
credential: "",
|
|
60
|
+
autoRegister: false,
|
|
61
|
+
mode: { kind: "tools", command, args, json }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
38
65
|
if (argv[0] === "proxy") {
|
|
39
66
|
const rest = argv[1] === "--" ? argv.slice(2) : argv.slice(1);
|
|
40
67
|
const [command, ...args] = rest;
|
|
@@ -76,7 +103,9 @@ ${USAGE}`
|
|
|
76
103
|
credential: env.GUARDIAN_AGENT_CREDENTIAL,
|
|
77
104
|
...env.GUARDIAN_CONNECTION_ID ? { connectionId: env.GUARDIAN_CONNECTION_ID } : {},
|
|
78
105
|
...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {},
|
|
79
|
-
|
|
106
|
+
// WO-050: off by default — production denies first-use registration
|
|
107
|
+
// unless the grant pins mcp.install. Opt in with GUARDIAN_AUTO_REGISTER=true.
|
|
108
|
+
autoRegister: /^(true|1|yes|on)$/i.test(env.GUARDIAN_AUTO_REGISTER ?? ""),
|
|
80
109
|
mode
|
|
81
110
|
}
|
|
82
111
|
};
|
|
@@ -225,6 +254,89 @@ async function handleToolCall(options, name, arguments_) {
|
|
|
225
254
|
return errorResult(`unknown tool: ${name}`);
|
|
226
255
|
}
|
|
227
256
|
|
|
257
|
+
// src/presets.ts
|
|
258
|
+
var GUARDIAN_TOOL_NAME = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
|
|
259
|
+
var READ_VERBS = /* @__PURE__ */ new Set([
|
|
260
|
+
"get",
|
|
261
|
+
"list",
|
|
262
|
+
"search",
|
|
263
|
+
"read",
|
|
264
|
+
"describe",
|
|
265
|
+
"show",
|
|
266
|
+
"view",
|
|
267
|
+
"find",
|
|
268
|
+
"lookup",
|
|
269
|
+
"count",
|
|
270
|
+
"inspect",
|
|
271
|
+
"status",
|
|
272
|
+
"fetch"
|
|
273
|
+
]);
|
|
274
|
+
function firstWord(name) {
|
|
275
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).find(Boolean) ?? "";
|
|
276
|
+
}
|
|
277
|
+
function suggestTool(tool) {
|
|
278
|
+
const governable = GUARDIAN_TOOL_NAME.test(tool.name) && tool.name.length <= 64;
|
|
279
|
+
const annotations = tool.annotations ?? {};
|
|
280
|
+
if (annotations.destructiveHint === true) {
|
|
281
|
+
return {
|
|
282
|
+
name: tool.name,
|
|
283
|
+
suggestedMode: "ask",
|
|
284
|
+
why: "server marks it destructive",
|
|
285
|
+
governable
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (annotations.readOnlyHint === true) {
|
|
289
|
+
return {
|
|
290
|
+
name: tool.name,
|
|
291
|
+
suggestedMode: "allow",
|
|
292
|
+
why: "server marks it read-only",
|
|
293
|
+
governable
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const verb = firstWord(tool.name);
|
|
297
|
+
if (READ_VERBS.has(verb)) {
|
|
298
|
+
return {
|
|
299
|
+
name: tool.name,
|
|
300
|
+
suggestedMode: "allow",
|
|
301
|
+
why: `name starts with "${verb}" (not annotated; check it really only reads)`,
|
|
302
|
+
governable
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
name: tool.name,
|
|
307
|
+
suggestedMode: "ask",
|
|
308
|
+
why: verb ? `"${verb}" may change something` : "unclear what it does",
|
|
309
|
+
governable
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function renderSuggestions(serverCommand, tools) {
|
|
313
|
+
const rows = tools.map(suggestTool);
|
|
314
|
+
const width = Math.max(4, ...rows.map((r) => r.name.length));
|
|
315
|
+
const lines = [
|
|
316
|
+
`Tools of: ${serverCommand}`,
|
|
317
|
+
"",
|
|
318
|
+
`${"Tool".padEnd(width)} Suggested Why`,
|
|
319
|
+
...rows.map(
|
|
320
|
+
(r) => `${r.name.padEnd(width)} ${(r.suggestedMode === "ask" ? "Ask me first" : "Allow").padEnd(12)} ${r.why}${r.governable ? "" : " \u26A0 name not accepted by Guardian"}`
|
|
321
|
+
),
|
|
322
|
+
"",
|
|
323
|
+
"Register these in the Guardian console (Connections \u2192 Register a tool server \u2192",
|
|
324
|
+
"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."
|
|
326
|
+
];
|
|
327
|
+
const ungovernable = rows.filter((r) => !r.governable);
|
|
328
|
+
if (ungovernable.length > 0) {
|
|
329
|
+
lines.push(
|
|
330
|
+
"",
|
|
331
|
+
`\u26A0 ${ungovernable.length} tool name(s) don't match Guardian's naming rule`,
|
|
332
|
+
" (lowercase letters/digits, separated by . _ or -; at most 64 chars).",
|
|
333
|
+
" Calls to them will be denied. Leave them out, or use a server version",
|
|
334
|
+
" with compatible names."
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
228
340
|
// src/proxy.ts
|
|
229
341
|
import {
|
|
230
342
|
createGuardianContext,
|
|
@@ -333,7 +445,7 @@ function createGovernedProxy(options) {
|
|
|
333
445
|
mode: "inProcess",
|
|
334
446
|
onAsk: "result",
|
|
335
447
|
onDeny: "result",
|
|
336
|
-
installRecordTransport: options.autoRegister ??
|
|
448
|
+
installRecordTransport: options.autoRegister ?? false,
|
|
337
449
|
...options.adapterId ? { adapterId: options.adapterId } : {},
|
|
338
450
|
...options.agentName ? { agentName: options.agentName } : {}
|
|
339
451
|
});
|
|
@@ -519,6 +631,36 @@ if (!parsed.ok) {
|
|
|
519
631
|
process.exit(parsed.message.startsWith("guardian-mcp \u2014") ? 0 : 2);
|
|
520
632
|
}
|
|
521
633
|
var config = parsed.config;
|
|
634
|
+
if (config.mode.kind === "tools") {
|
|
635
|
+
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);
|
|
655
|
+
await lister.close();
|
|
656
|
+
const serverCommand = [mode.command, ...mode.args].join(" ");
|
|
657
|
+
process.stdout.write(
|
|
658
|
+
mode.json ? `${JSON.stringify({ server: serverCommand, tools: tools.map(suggestTool) }, null, 2)}
|
|
659
|
+
` : `${renderSuggestions(serverCommand, tools)}
|
|
660
|
+
`
|
|
661
|
+
);
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
522
664
|
var client = createGuardianClient({
|
|
523
665
|
baseUrl: config.baseUrl,
|
|
524
666
|
credential: config.credential,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odingard/guardian-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"mcpName": "io.github.Odingard/guardian-mcp",
|
|
5
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 outcome carries a verifiable receipt.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/server.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
|
3
3
|
"name": "io.github.Odingard/guardian-mcp",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "0.3.
|
|
4
|
+
"description": "Routes agent tool calls through Guardian for ALLOW/ASK/DENY before execution.",
|
|
5
|
+
"version": "0.3.2",
|
|
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.2",
|
|
30
30
|
"transport": {
|
|
31
31
|
"type": "stdio"
|
|
32
32
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"name": "GUARDIAN_AGENT_CREDENTIAL",
|
|
41
|
-
"description": "Agent credential (
|
|
41
|
+
"description": "Agent credential (gak_…)",
|
|
42
42
|
"isRequired": true,
|
|
43
43
|
"isSecret": true
|
|
44
44
|
},
|