@odingard/guardian-mcp 0.3.0 → 0.3.3
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 +131 -15
- package/dist/index.js +456 -56
- package/package.json +5 -4
- 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,134 @@ 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
|
+
## Govern a remote MCP server: `guardian-mcp proxy --url` (unreleased)
|
|
91
|
+
|
|
92
|
+
**Status: not yet proven live.** This is tested against local Streamable
|
|
93
|
+
HTTP and SSE MCP servers and a fake Guardian only. Nothing has run against a
|
|
94
|
+
real Guardian or a real hosted server such as Composio. Composio's
|
|
95
|
+
`x-api-key` header is unconfirmed.
|
|
96
|
+
|
|
97
|
+
The proxy can also front an MCP server that runs online, such as Composio.
|
|
98
|
+
The agent still starts `guardian-mcp` over stdio like any
|
|
99
|
+
local server; guardian-mcp connects to the remote server over **Streamable
|
|
100
|
+
HTTP** (the default) or **SSE** (`--transport sse`).
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"composio-governed": {
|
|
106
|
+
"command": "guardian-mcp",
|
|
107
|
+
"args": [
|
|
108
|
+
"proxy",
|
|
109
|
+
"--url",
|
|
110
|
+
"<the URL Composio's generate step returned>",
|
|
111
|
+
"--header",
|
|
112
|
+
"x-api-key: ${COMPOSIO_API_KEY}"
|
|
113
|
+
],
|
|
114
|
+
"env": {
|
|
115
|
+
"GUARDIAN_BASE_URL": "https://guardian-api.odingard.com",
|
|
116
|
+
"GUARDIAN_AGENT_CREDENTIAL": "gac1.…",
|
|
117
|
+
"GUARDIAN_CONNECTION_ID": "pcn_…",
|
|
118
|
+
"COMPOSIO_API_KEY": "…"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A server that takes a Bearer key: `--url https://mcp.example.com/mcp --header
|
|
126
|
+
'Authorization: Bearer ${REMOTE_MCP_API_KEY}'`.
|
|
127
|
+
|
|
128
|
+
- Every `tools/call` is decided by Guardian exactly as for a stdio server:
|
|
129
|
+
ALLOW forwards it, ASK holds it for `guardian_claim`, DENY never sends it.
|
|
130
|
+
`tools/list` passes through, with `guardian_claim` and `guardian_receipt`
|
|
131
|
+
added.
|
|
132
|
+
- **Secrets stay in the proxy.** Write header values as `${ENV_VAR}`;
|
|
133
|
+
guardian-mcp expands them itself, so no shell is needed and the key never
|
|
134
|
+
appears in the agent's view. In a shell, use single quotes so the shell
|
|
135
|
+
does not expand it onto the command line. The URL and header values are
|
|
136
|
+
never printed; error messages show the server's origin only.
|
|
137
|
+
- `${…}` can only name a variable that is set, and never a `GUARDIAN_*`
|
|
138
|
+
one, so the agent credential cannot be sent to the remote server.
|
|
139
|
+
- The URL must be `https://`; plain `http://` is accepted only for
|
|
140
|
+
`localhost`. User names or passwords in the URL are refused; send
|
|
141
|
+
credentials as headers. Redirects to another origin are not followed.
|
|
142
|
+
- **Fails closed.** If the remote server cannot be reached or rejects the key
|
|
143
|
+
at startup, guardian-mcp exits with a one-line message and serves no tools.
|
|
144
|
+
If the remote fails after Guardian allowed a call, the result is an error
|
|
145
|
+
that says the call may or may not have run, and the capability is not
|
|
146
|
+
redeemed. It is never reported as a success.
|
|
147
|
+
- `guardian-mcp tools --url …` lists a remote server's tools with suggested
|
|
148
|
+
modes, the same as for a local one.
|
|
149
|
+
- OAuth sign-in flows are not supported; use an API key or bearer token
|
|
150
|
+
header.
|
|
151
|
+
- **The upstream cannot speak for Guardian.** A remote server's result is
|
|
152
|
+
passed to the agent as the server's own output, whatever fields it
|
|
153
|
+
carries. A result that contains a `guardian: {decision, requestId}` field
|
|
154
|
+
is never read as a Guardian decision, so it cannot hold, swap or hide a
|
|
155
|
+
call.
|
|
156
|
+
- If `tools/list` fails upstream, the agent gets a generic "tool list is
|
|
157
|
+
unavailable" error. The upstream's error text (which can quote its URL
|
|
158
|
+
path, response body or headers) is not shown to the agent or logged;
|
|
159
|
+
`guardian-mcp tools --url` prints it with the URL, its path identifiers,
|
|
160
|
+
query values and header values scrubbed.
|
|
161
|
+
|
|
162
|
+
What is **not** governed: a remote server the agent connects to directly
|
|
163
|
+
(for example an MCP client's own `"url"` entry, or a model provider's hosted
|
|
164
|
+
MCP connector), and anything the agent does without MCP. Also not covered:
|
|
165
|
+
|
|
166
|
+
- **Lost results after ALLOW.** If the remote times out (the MCP SDK's
|
|
167
|
+
default request timeout is 60 seconds) or drops the session after
|
|
168
|
+
Guardian allowed a call, the call may have run, but the capability is not
|
|
169
|
+
redeemed and no receipt is written. Receipts can then understate what
|
|
170
|
+
happened; check the remote system itself.
|
|
171
|
+
- **Keys in the same process tree.** guardian-mcp keeps the upstream key out
|
|
172
|
+
of the agent's MCP view. It cannot stop other code that runs as the same
|
|
173
|
+
user, in the same container, from reading the key from the environment it
|
|
174
|
+
was set in and calling the remote server directly. Recipes:
|
|
175
|
+
[ZeroClaw](../../examples/zeroclaw/README.md) and
|
|
176
|
+
[your own container (CrewAI, LangGraph, AutoGen)](../../examples/byo/README.md).
|
|
177
|
+
Runnable example: `examples/remote-mcp.mjs` (SIMULATED by default, LIVE with
|
|
178
|
+
Guardian settings). It runs inside this repository, where the workspace
|
|
179
|
+
links the unreleased guardian-mcp; the published `@odingard/guardian-mcp`
|
|
180
|
+
0.3.2 does not have `--url` yet.
|
|
181
|
+
|
|
182
|
+
## See what you are about to govern: `guardian-mcp tools`
|
|
183
|
+
|
|
184
|
+
Before putting a server behind the proxy, list its tools along with a suggested Guardian mode for each. This needs no Guardian credential:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx -y @odingard/guardian-mcp tools -- npx -y @modelcontextprotocol/server-filesystem ./data
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
Tool Suggested Why
|
|
192
|
+
read_file Allow server marks it read-only
|
|
193
|
+
write_file Ask me first "write" may change something
|
|
194
|
+
delete_branch Ask me first server marks it destructive
|
|
195
|
+
createIssue Ask me first "create" may change something ⚠ name not accepted by Guardian
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
How the suggestions are made:
|
|
199
|
+
|
|
200
|
+
- The server's own MCP annotations (`readOnlyHint`, `destructiveHint`) are used first.
|
|
201
|
+
- Otherwise the tool's verb decides. `get`, `list` and `search` suggest Allow; everything else suggests Ask.
|
|
202
|
+
|
|
203
|
+
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
|
+
|
|
89
205
|
## License
|
|
90
206
|
|
|
91
|
-
Apache-2.0.
|
|
207
|
+
Apache-2.0.
|
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,35 +16,57 @@ 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.
|
|
22
|
+
guardian-mcp tools [--json] -- <command> [args...]
|
|
23
|
+
guardian-mcp tools [--json] --url <https-url> [--transport ...] [--header ...]
|
|
24
|
+
List a server's tools with a suggested Guardian mode
|
|
25
|
+
for each (no Guardian credential needed)
|
|
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).
|
|
21
33
|
|
|
22
34
|
Environment:
|
|
23
35
|
GUARDIAN_BASE_URL required, e.g. https://guardian-api.odingard.com
|
|
24
36
|
GUARDIAN_AGENT_CREDENTIAL required, the agent credential (gac1.\u2026) from the Guardian console
|
|
25
37
|
GUARDIAN_CONNECTION_ID tool connection id (required for proxy mode)
|
|
26
38
|
GUARDIAN_ADAPTER_ID optional adapter override
|
|
27
|
-
GUARDIAN_AUTO_REGISTER proxy: "
|
|
28
|
-
|
|
39
|
+
GUARDIAN_AUTO_REGISTER proxy: "true" to propose registering each upstream tool on first
|
|
40
|
+
use (mcp.install). Default off: register the tools once in the
|
|
41
|
+
console as "My own code (record-only)" and allow them for the agent
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
Examples (proxy):
|
|
31
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}'
|
|
32
46
|
`;
|
|
33
47
|
function parseCli(argv, env) {
|
|
34
48
|
let mode = { kind: "dispatch" };
|
|
35
49
|
if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
36
50
|
return { ok: false, message: USAGE };
|
|
37
51
|
}
|
|
52
|
+
if (argv[0] === "tools") {
|
|
53
|
+
const json = argv[1] === "--json";
|
|
54
|
+
const target = parseTarget("tools", argv.slice(json ? 2 : 1), env);
|
|
55
|
+
if (!target.ok) return target;
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
config: {
|
|
59
|
+
baseUrl: "",
|
|
60
|
+
credential: "",
|
|
61
|
+
autoRegister: false,
|
|
62
|
+
mode: { kind: "tools", json, ...target.target }
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
38
66
|
if (argv[0] === "proxy") {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
message: `guardian-mcp proxy: missing the upstream MCP server command.
|
|
45
|
-
|
|
46
|
-
${USAGE}`
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
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 };
|
|
50
70
|
} else if (argv.length > 0) {
|
|
51
71
|
return {
|
|
52
72
|
ok: false,
|
|
@@ -76,7 +96,9 @@ ${USAGE}`
|
|
|
76
96
|
credential: env.GUARDIAN_AGENT_CREDENTIAL,
|
|
77
97
|
...env.GUARDIAN_CONNECTION_ID ? { connectionId: env.GUARDIAN_CONNECTION_ID } : {},
|
|
78
98
|
...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {},
|
|
79
|
-
|
|
99
|
+
// Off by default — production denies first-use registration
|
|
100
|
+
// unless the grant pins mcp.install. Opt in with GUARDIAN_AUTO_REGISTER=true.
|
|
101
|
+
autoRegister: /^(true|1|yes|on)$/i.test(env.GUARDIAN_AUTO_REGISTER ?? ""),
|
|
80
102
|
mode
|
|
81
103
|
}
|
|
82
104
|
};
|
|
@@ -89,6 +111,156 @@ function upstreamEnv(env) {
|
|
|
89
111
|
}
|
|
90
112
|
return out;
|
|
91
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 segment of parsed2.pathname.split("/")) {
|
|
239
|
+
if (segment.length >= 16 || segment.length >= 8 && /\d/.test(segment)) {
|
|
240
|
+
out.add(segment);
|
|
241
|
+
out.add(decodeURIComponent(segment));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const value of Object.values(headers)) {
|
|
245
|
+
out.add(value);
|
|
246
|
+
const parts = value.split(/\s+/);
|
|
247
|
+
for (const part of parts) if (part.length >= 8) out.add(part);
|
|
248
|
+
}
|
|
249
|
+
return [...out].sort((a, b) => b.length - a.length);
|
|
250
|
+
}
|
|
251
|
+
function redact(text, secrets) {
|
|
252
|
+
let out = text;
|
|
253
|
+
for (const secret of secrets) {
|
|
254
|
+
if (secret) out = out.split(secret).join("[redacted]");
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function describeTarget(target) {
|
|
259
|
+
if ("remote" in target) {
|
|
260
|
+
return `${new URL(target.remote.url).origin} (${target.remote.transport})`;
|
|
261
|
+
}
|
|
262
|
+
return [target.command, ...target.args].join(" ");
|
|
263
|
+
}
|
|
92
264
|
|
|
93
265
|
// src/dispatch.ts
|
|
94
266
|
var TOOLS = [
|
|
@@ -225,6 +397,89 @@ async function handleToolCall(options, name, arguments_) {
|
|
|
225
397
|
return errorResult(`unknown tool: ${name}`);
|
|
226
398
|
}
|
|
227
399
|
|
|
400
|
+
// src/presets.ts
|
|
401
|
+
var GUARDIAN_TOOL_NAME = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
|
|
402
|
+
var READ_VERBS = /* @__PURE__ */ new Set([
|
|
403
|
+
"get",
|
|
404
|
+
"list",
|
|
405
|
+
"search",
|
|
406
|
+
"read",
|
|
407
|
+
"describe",
|
|
408
|
+
"show",
|
|
409
|
+
"view",
|
|
410
|
+
"find",
|
|
411
|
+
"lookup",
|
|
412
|
+
"count",
|
|
413
|
+
"inspect",
|
|
414
|
+
"status",
|
|
415
|
+
"fetch"
|
|
416
|
+
]);
|
|
417
|
+
function firstWord(name) {
|
|
418
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).find(Boolean) ?? "";
|
|
419
|
+
}
|
|
420
|
+
function suggestTool(tool) {
|
|
421
|
+
const governable = GUARDIAN_TOOL_NAME.test(tool.name) && tool.name.length <= 64;
|
|
422
|
+
const annotations = tool.annotations ?? {};
|
|
423
|
+
if (annotations.destructiveHint === true) {
|
|
424
|
+
return {
|
|
425
|
+
name: tool.name,
|
|
426
|
+
suggestedMode: "ask",
|
|
427
|
+
why: "server marks it destructive",
|
|
428
|
+
governable
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (annotations.readOnlyHint === true) {
|
|
432
|
+
return {
|
|
433
|
+
name: tool.name,
|
|
434
|
+
suggestedMode: "allow",
|
|
435
|
+
why: "server marks it read-only",
|
|
436
|
+
governable
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const verb = firstWord(tool.name);
|
|
440
|
+
if (READ_VERBS.has(verb)) {
|
|
441
|
+
return {
|
|
442
|
+
name: tool.name,
|
|
443
|
+
suggestedMode: "allow",
|
|
444
|
+
why: `name starts with "${verb}" (not annotated; check it really only reads)`,
|
|
445
|
+
governable
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
name: tool.name,
|
|
450
|
+
suggestedMode: "ask",
|
|
451
|
+
why: verb ? `"${verb}" may change something` : "unclear what it does",
|
|
452
|
+
governable
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function renderSuggestions(serverCommand, tools, remote = false) {
|
|
456
|
+
const rows = tools.map(suggestTool);
|
|
457
|
+
const width = Math.max(4, ...rows.map((r) => r.name.length));
|
|
458
|
+
const lines = [
|
|
459
|
+
`Tools of: ${serverCommand}`,
|
|
460
|
+
"",
|
|
461
|
+
`${"Tool".padEnd(width)} Suggested Why`,
|
|
462
|
+
...rows.map(
|
|
463
|
+
(r) => `${r.name.padEnd(width)} ${(r.suggestedMode === "ask" ? "Ask me first" : "Allow").padEnd(12)} ${r.why}${r.governable ? "" : " \u26A0 name not accepted by Guardian"}`
|
|
464
|
+
),
|
|
465
|
+
"",
|
|
466
|
+
"Register these in the Guardian console (Connections \u2192 Register a tool server \u2192",
|
|
467
|
+
"record-only), then set each mode for your agent. Run the server through",
|
|
468
|
+
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."
|
|
469
|
+
];
|
|
470
|
+
const ungovernable = rows.filter((r) => !r.governable);
|
|
471
|
+
if (ungovernable.length > 0) {
|
|
472
|
+
lines.push(
|
|
473
|
+
"",
|
|
474
|
+
`\u26A0 ${ungovernable.length} tool name(s) don't match Guardian's naming rule`,
|
|
475
|
+
" (lowercase letters/digits, separated by . _ or -; at most 64 chars).",
|
|
476
|
+
" Calls to them will be denied. Leave them out, or use a server version",
|
|
477
|
+
" with compatible names."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return lines.join("\n");
|
|
481
|
+
}
|
|
482
|
+
|
|
228
483
|
// src/proxy.ts
|
|
229
484
|
import {
|
|
230
485
|
createGuardianContext,
|
|
@@ -266,7 +521,46 @@ function jsonResult2(payload, isError = false) {
|
|
|
266
521
|
function isDecisionResult(value) {
|
|
267
522
|
return typeof value === "object" && value !== null && "guardian" in value && typeof value.guardian === "object";
|
|
268
523
|
}
|
|
524
|
+
var UpstreamCallFailed = class extends Error {
|
|
525
|
+
constructor(toolName) {
|
|
526
|
+
super("upstream call failed");
|
|
527
|
+
this.toolName = toolName;
|
|
528
|
+
this.name = "UpstreamCallFailed";
|
|
529
|
+
}
|
|
530
|
+
toolName;
|
|
531
|
+
};
|
|
532
|
+
var UPSTREAM_RESULT = /* @__PURE__ */ Symbol("guardian-mcp.upstreamResult");
|
|
533
|
+
function upstreamRan(value) {
|
|
534
|
+
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, UPSTREAM_RESULT);
|
|
535
|
+
}
|
|
536
|
+
var UpstreamListFailed = class extends Error {
|
|
537
|
+
constructor() {
|
|
538
|
+
super(
|
|
539
|
+
"guardian-mcp: the upstream MCP server's tool list is unavailable; no tools are served."
|
|
540
|
+
);
|
|
541
|
+
this.name = "UpstreamListFailed";
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
function callUpstream(upstream, name, args) {
|
|
545
|
+
return async () => {
|
|
546
|
+
try {
|
|
547
|
+
return { [UPSTREAM_RESULT]: await upstream.callTool(name, args) };
|
|
548
|
+
} catch {
|
|
549
|
+
throw new UpstreamCallFailed(name);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
269
553
|
function failure(err, toolName) {
|
|
554
|
+
if (err instanceof UpstreamCallFailed) {
|
|
555
|
+
return jsonResult2(
|
|
556
|
+
{
|
|
557
|
+
decision: "ALLOW",
|
|
558
|
+
toolName,
|
|
559
|
+
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."
|
|
560
|
+
},
|
|
561
|
+
true
|
|
562
|
+
);
|
|
563
|
+
}
|
|
270
564
|
if (err instanceof GuardianExecutionFailed) {
|
|
271
565
|
return jsonResult2(
|
|
272
566
|
{
|
|
@@ -333,7 +627,7 @@ function createGovernedProxy(options) {
|
|
|
333
627
|
mode: "inProcess",
|
|
334
628
|
onAsk: "result",
|
|
335
629
|
onDeny: "result",
|
|
336
|
-
installRecordTransport: options.autoRegister ??
|
|
630
|
+
installRecordTransport: options.autoRegister ?? false,
|
|
337
631
|
...options.adapterId ? { adapterId: options.adapterId } : {},
|
|
338
632
|
...options.agentName ? { agentName: options.agentName } : {}
|
|
339
633
|
});
|
|
@@ -354,7 +648,16 @@ function createGovernedProxy(options) {
|
|
|
354
648
|
return tools;
|
|
355
649
|
}
|
|
356
650
|
function toCallResult(toolName, args, output) {
|
|
357
|
-
if (
|
|
651
|
+
if (upstreamRan(output)) return output[UPSTREAM_RESULT];
|
|
652
|
+
if (!isDecisionResult(output)) {
|
|
653
|
+
return jsonResult2(
|
|
654
|
+
{
|
|
655
|
+
toolName,
|
|
656
|
+
error: "Guardian proxy error; the call was not forwarded."
|
|
657
|
+
},
|
|
658
|
+
true
|
|
659
|
+
);
|
|
660
|
+
}
|
|
358
661
|
const g = output.guardian;
|
|
359
662
|
if (g.decision === "ASK") {
|
|
360
663
|
if (toolInvokeRequestIds.has(g.requestId)) {
|
|
@@ -388,16 +691,19 @@ function createGovernedProxy(options) {
|
|
|
388
691
|
async function claim(requestId) {
|
|
389
692
|
const held = pendingCalls.get(requestId);
|
|
390
693
|
if (held) {
|
|
694
|
+
pendingCalls.delete(requestId);
|
|
391
695
|
let claimed;
|
|
392
696
|
try {
|
|
393
697
|
claimed = await options.client.claim(requestId);
|
|
394
698
|
} catch {
|
|
699
|
+
pendingCalls.set(requestId, held);
|
|
395
700
|
return jsonResult2(
|
|
396
701
|
{ requestId, error: "Guardian unreachable; nothing was forwarded." },
|
|
397
702
|
true
|
|
398
703
|
);
|
|
399
704
|
}
|
|
400
705
|
if (claimed.ok && claimed.decision === "ASK" || !claimed.ok && (claimed.reasonCode === "APPROVAL_PENDING" || claimed.reasonCode === "APPROVAL_REQUIRED")) {
|
|
706
|
+
pendingCalls.set(requestId, held);
|
|
401
707
|
return jsonResult2({
|
|
402
708
|
decision: "ASK",
|
|
403
709
|
toolName: held.name,
|
|
@@ -405,14 +711,13 @@ function createGovernedProxy(options) {
|
|
|
405
711
|
message: "Still awaiting human approval. Nothing was sent."
|
|
406
712
|
});
|
|
407
713
|
}
|
|
408
|
-
pendingCalls.delete(requestId);
|
|
409
714
|
try {
|
|
410
715
|
const output = await settle(
|
|
411
716
|
ctx,
|
|
412
717
|
held.name,
|
|
413
718
|
held.args,
|
|
414
719
|
claimed,
|
|
415
|
-
(
|
|
720
|
+
callUpstream(upstream, held.name, held.args)
|
|
416
721
|
);
|
|
417
722
|
return toCallResult(held.name, held.args, output);
|
|
418
723
|
} catch (err) {
|
|
@@ -463,7 +768,15 @@ function createGovernedProxy(options) {
|
|
|
463
768
|
}
|
|
464
769
|
return {
|
|
465
770
|
async listTools() {
|
|
466
|
-
|
|
771
|
+
let tools;
|
|
772
|
+
try {
|
|
773
|
+
tools = await loadTools();
|
|
774
|
+
} catch {
|
|
775
|
+
log(
|
|
776
|
+
"guardian-mcp proxy: the upstream MCP server's tool list is unavailable"
|
|
777
|
+
);
|
|
778
|
+
throw new UpstreamListFailed();
|
|
779
|
+
}
|
|
467
780
|
return [
|
|
468
781
|
...[...tools.values()].map((tool) => ({
|
|
469
782
|
...tool,
|
|
@@ -491,7 +804,18 @@ function createGovernedProxy(options) {
|
|
|
491
804
|
!result.ok
|
|
492
805
|
);
|
|
493
806
|
}
|
|
494
|
-
|
|
807
|
+
let tools;
|
|
808
|
+
try {
|
|
809
|
+
tools = await loadTools();
|
|
810
|
+
} catch {
|
|
811
|
+
return jsonResult2(
|
|
812
|
+
{
|
|
813
|
+
toolName: name,
|
|
814
|
+
error: "The upstream MCP server's tool list is unavailable; nothing was forwarded."
|
|
815
|
+
},
|
|
816
|
+
true
|
|
817
|
+
);
|
|
818
|
+
}
|
|
495
819
|
if (!tools.has(name)) {
|
|
496
820
|
return jsonResult2({ error: `unknown tool: ${name}` }, true);
|
|
497
821
|
}
|
|
@@ -500,7 +824,7 @@ function createGovernedProxy(options) {
|
|
|
500
824
|
ctx,
|
|
501
825
|
name,
|
|
502
826
|
args,
|
|
503
|
-
(
|
|
827
|
+
callUpstream(upstream, name, args)
|
|
504
828
|
);
|
|
505
829
|
return toCallResult(name, args, output);
|
|
506
830
|
} catch (err) {
|
|
@@ -510,7 +834,85 @@ function createGovernedProxy(options) {
|
|
|
510
834
|
};
|
|
511
835
|
}
|
|
512
836
|
|
|
837
|
+
// src/upstream.ts
|
|
838
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
839
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
840
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
841
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
842
|
+
var UpstreamConnectError = class extends Error {
|
|
843
|
+
constructor(message) {
|
|
844
|
+
super(message);
|
|
845
|
+
this.name = "UpstreamConnectError";
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
function upstreamTransport(target, env) {
|
|
849
|
+
if (!("remote" in target)) {
|
|
850
|
+
return new StdioClientTransport({
|
|
851
|
+
command: target.command,
|
|
852
|
+
args: target.args,
|
|
853
|
+
env: upstreamEnv(env),
|
|
854
|
+
stderr: "inherit"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const { url, transport, headers } = target.remote;
|
|
858
|
+
const options = {
|
|
859
|
+
requestInit: { headers },
|
|
860
|
+
redirectPolicy: "same-origin"
|
|
861
|
+
};
|
|
862
|
+
return transport === "sse" ? new SSEClientTransport(new URL(url), options) : new StreamableHTTPClientTransport(new URL(url), options);
|
|
863
|
+
}
|
|
864
|
+
async function connectUpstream(target, env, clientInfo, transport = upstreamTransport(target, env)) {
|
|
865
|
+
const client2 = new Client(clientInfo, { capabilities: {} });
|
|
866
|
+
try {
|
|
867
|
+
await client2.connect(transport);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
await client2.close().catch(() => {
|
|
870
|
+
});
|
|
871
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
872
|
+
throw new UpstreamConnectError(
|
|
873
|
+
`guardian-mcp: could not connect to the upstream MCP server at ${describeTarget(target)}: ${redact(detail, secretsOf(target)).slice(0, 300)}. No tools are served.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return client2;
|
|
877
|
+
}
|
|
878
|
+
async function listAllTools(client2) {
|
|
879
|
+
const tools = [];
|
|
880
|
+
let cursor;
|
|
881
|
+
do {
|
|
882
|
+
const page = await client2.listTools(cursor ? { cursor } : void 0);
|
|
883
|
+
tools.push(...page.tools);
|
|
884
|
+
cursor = page.nextCursor;
|
|
885
|
+
} while (cursor);
|
|
886
|
+
return tools;
|
|
887
|
+
}
|
|
888
|
+
function toolListFailure(target, err) {
|
|
889
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
890
|
+
return `guardian-mcp: the upstream MCP server at ${describeTarget(target)} did not return its tool list: ${redact(detail, secretsOf(target)).slice(0, 300)}.`;
|
|
891
|
+
}
|
|
892
|
+
function clientUpstream(client2) {
|
|
893
|
+
return {
|
|
894
|
+
listTools: () => listAllTools(client2),
|
|
895
|
+
async callTool(name, args) {
|
|
896
|
+
return await client2.callTool({
|
|
897
|
+
name,
|
|
898
|
+
arguments: args
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
513
904
|
// src/index.ts
|
|
905
|
+
async function connectOrExit(...args) {
|
|
906
|
+
try {
|
|
907
|
+
return await connectUpstream(...args);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
process.stderr.write(
|
|
910
|
+
`${err instanceof UpstreamConnectError ? err.message : "guardian-mcp: could not connect to the upstream MCP server. No tools are served."}
|
|
911
|
+
`
|
|
912
|
+
);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
514
916
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
515
917
|
var parsed = parseCli(process.argv.slice(2), process.env);
|
|
516
918
|
if (!parsed.ok) {
|
|
@@ -519,6 +921,31 @@ if (!parsed.ok) {
|
|
|
519
921
|
process.exit(parsed.message.startsWith("guardian-mcp \u2014") ? 0 : 2);
|
|
520
922
|
}
|
|
521
923
|
var config = parsed.config;
|
|
924
|
+
if (config.mode.kind === "tools") {
|
|
925
|
+
const mode = config.mode;
|
|
926
|
+
const lister = await connectOrExit(mode, process.env, {
|
|
927
|
+
name: "guardian-mcp-tools",
|
|
928
|
+
version: VERSION
|
|
929
|
+
});
|
|
930
|
+
let tools;
|
|
931
|
+
try {
|
|
932
|
+
tools = await listAllTools(lister);
|
|
933
|
+
} catch (err) {
|
|
934
|
+
process.stderr.write(`${toolListFailure(mode, err)}
|
|
935
|
+
`);
|
|
936
|
+
await lister.close().catch(() => {
|
|
937
|
+
});
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
await lister.close();
|
|
941
|
+
const serverCommand = describeTarget(mode);
|
|
942
|
+
process.stdout.write(
|
|
943
|
+
mode.json ? `${JSON.stringify({ server: serverCommand, tools: tools.map(suggestTool) }, null, 2)}
|
|
944
|
+
` : `${renderSuggestions(serverCommand, tools, "remote" in mode)}
|
|
945
|
+
`
|
|
946
|
+
);
|
|
947
|
+
process.exit(0);
|
|
948
|
+
}
|
|
522
949
|
var client = createGuardianClient({
|
|
523
950
|
baseUrl: config.baseUrl,
|
|
524
951
|
credential: config.credential,
|
|
@@ -529,18 +956,10 @@ var server = new Server(
|
|
|
529
956
|
{ capabilities: { tools: {} } }
|
|
530
957
|
);
|
|
531
958
|
if (config.mode.kind === "proxy") {
|
|
532
|
-
const upstreamClient =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
);
|
|
536
|
-
await upstreamClient.connect(
|
|
537
|
-
new StdioClientTransport({
|
|
538
|
-
command: config.mode.command,
|
|
539
|
-
args: config.mode.args,
|
|
540
|
-
env: upstreamEnv(process.env),
|
|
541
|
-
stderr: "inherit"
|
|
542
|
-
})
|
|
543
|
-
);
|
|
959
|
+
const upstreamClient = await connectOrExit(config.mode, process.env, {
|
|
960
|
+
name: "guardian-mcp-proxy",
|
|
961
|
+
version: VERSION
|
|
962
|
+
});
|
|
544
963
|
const proxy = createGovernedProxy({
|
|
545
964
|
client,
|
|
546
965
|
connectionId: config.connectionId,
|
|
@@ -548,26 +967,7 @@ if (config.mode.kind === "proxy") {
|
|
|
548
967
|
...config.adapterId ? { adapterId: config.adapterId } : {},
|
|
549
968
|
log: (message) => process.stderr.write(`${message}
|
|
550
969
|
`),
|
|
551
|
-
upstream:
|
|
552
|
-
async listTools() {
|
|
553
|
-
const tools = [];
|
|
554
|
-
let cursor;
|
|
555
|
-
do {
|
|
556
|
-
const page = await upstreamClient.listTools(
|
|
557
|
-
cursor ? { cursor } : void 0
|
|
558
|
-
);
|
|
559
|
-
tools.push(...page.tools);
|
|
560
|
-
cursor = page.nextCursor;
|
|
561
|
-
} while (cursor);
|
|
562
|
-
return tools;
|
|
563
|
-
},
|
|
564
|
-
async callTool(name, args) {
|
|
565
|
-
return await upstreamClient.callTool({
|
|
566
|
-
name,
|
|
567
|
-
arguments: args
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
970
|
+
upstream: clientUpstream(upstreamClient)
|
|
571
971
|
});
|
|
572
972
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
573
973
|
tools: await proxy.listTools()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odingard/guardian-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|
|
@@ -20,17 +20,18 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --external:@odingard/guardian-sdk --external:@modelcontextprotocol/sdk --outfile=dist/index.js",
|
|
22
22
|
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
23
24
|
"prepublishOnly": "npm run build && npm test"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
27
|
-
"@odingard/guardian-sdk": "^0.3.
|
|
28
|
+
"@odingard/guardian-sdk": "^0.3.8"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^22",
|
|
31
32
|
"esbuild": "^0.28.2",
|
|
32
33
|
"typescript": "^5.9.3",
|
|
33
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^5.1.0"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
|
@@ -40,5 +41,5 @@
|
|
|
40
41
|
"url": "https://github.com/Odingard/guardian-sdk.git",
|
|
41
42
|
"directory": "packages/mcp"
|
|
42
43
|
},
|
|
43
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://odingard.com/guardian"
|
|
44
45
|
}
|
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.3",
|
|
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.3",
|
|
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
|
},
|