@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 CHANGED
@@ -31,11 +31,11 @@ npm i -g @odingard/guardian-mcp
31
31
 
32
32
  ## Tools
33
33
 
34
- | Tool | What it does |
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` | Claim an approved ASK by `requestId`, execute, return the receipt. |
38
- | `guardian_receipt` | Fetch the machine-readable evidence receipts for a request. |
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 in the Guardian console whose
78
- tools JSON lists each upstream tool as record-only
79
- (`{"name":"send_message","transport":"record"}`), and allow those tools
80
- for the agent, then set `GUARDIAN_AUTO_REGISTER=false`. Otherwise the proxy
81
- proposes an `mcp.install` registration per tool, which is denied unless the
82
- agent's grant pins `mcp.install`.
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: "false" when the upstream tools are already registered
28
- as record-only on the connection (skips mcp.install)
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
- autoRegister: !/^(false|0|no|off)$/i.test(env.GUARDIAN_AUTO_REGISTER ?? ""),
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 ?? true,
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.0",
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": "MCP server that routes an agent's tool calls through Guardian — Human Authority Infrastructure for Autonomous AI. Every tool call gets a deterministic ALLOW/ASK/DENY decision before anything executes; ASK holds the call until a human approves it, DENY never runs, and every outcome carries a verifiable receipt.",
5
- "version": "0.3.0",
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.0",
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 (gac1.…)",
41
+ "description": "Agent credential (gak_…)",
42
42
  "isRequired": true,
43
43
  "isSecret": true
44
44
  },