@odingard/guardian-mcp 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 | What happens |
66
- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
67
- | **ALLOW** | The call is forwarded to the upstream server, then the capability is redeemed and the receipt recorded. |
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. |
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,14 +92,128 @@ 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
 
123
+ ## Govern a remote MCP server: `guardian-mcp proxy --url` (0.3.3 and later)
124
+
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.
131
+
132
+ The proxy can also front an MCP server that runs online, such as Composio.
133
+ The agent still starts `guardian-mcp` over stdio like any
134
+ local server; guardian-mcp connects to the remote server over **Streamable
135
+ HTTP** (the default) or **SSE** (`--transport sse`).
136
+
137
+ ```json
138
+ {
139
+ "mcpServers": {
140
+ "composio-governed": {
141
+ "command": "guardian-mcp",
142
+ "args": [
143
+ "proxy",
144
+ "--url",
145
+ "<the URL Composio's generate step returned>",
146
+ "--header",
147
+ "x-api-key: ${COMPOSIO_API_KEY}"
148
+ ],
149
+ "env": {
150
+ "GUARDIAN_BASE_URL": "https://guardian-api.odingard.com",
151
+ "GUARDIAN_AGENT_CREDENTIAL": "gac1.…",
152
+ "GUARDIAN_CONNECTION_ID": "pcn_…",
153
+ "COMPOSIO_API_KEY": "…"
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ A server that takes a Bearer key: `--url https://mcp.example.com/mcp --header
161
+ 'Authorization: Bearer ${REMOTE_MCP_API_KEY}'`.
162
+
163
+ - Every `tools/call` is decided by Guardian exactly as for a stdio server:
164
+ ALLOW forwards it, ASK holds it for `guardian_claim`, DENY never sends it.
165
+ `tools/list` passes through, with `guardian_claim` and `guardian_receipt`
166
+ added.
167
+ - **Secrets stay in the proxy.** Write header values as `${ENV_VAR}`;
168
+ guardian-mcp expands them itself, so no shell is needed and the key never
169
+ appears in the agent's view. In a shell, use single quotes so the shell
170
+ does not expand it onto the command line. The URL and header values are
171
+ never printed; error messages show the server's origin only.
172
+ - `${…}` can only name a variable that is set, and never a `GUARDIAN_*`
173
+ one, so the agent credential cannot be sent to the remote server.
174
+ - The URL must be `https://`; plain `http://` is accepted only for
175
+ `localhost`. User names or passwords in the URL are refused; send
176
+ credentials as headers. Redirects to another origin are not followed.
177
+ - **Fails closed.** If the remote server cannot be reached or rejects the key
178
+ at startup, guardian-mcp exits with a one-line message and serves no tools.
179
+ If the remote fails after Guardian allowed a call, the result is an error
180
+ that says the call may or may not have run, and the capability is not
181
+ redeemed. It is never reported as a success.
182
+ - `guardian-mcp tools --url …` lists a remote server's tools with suggested
183
+ modes, the same as for a local one.
184
+ - OAuth sign-in flows are not supported; use an API key or bearer token
185
+ header.
186
+ - **The upstream cannot speak for Guardian.** A remote server's result is
187
+ passed to the agent as the server's own output, whatever fields it
188
+ carries. A result that contains a `guardian: {decision, requestId}` field
189
+ is never read as a Guardian decision, so it cannot hold, swap or hide a
190
+ call.
191
+ - If `tools/list` fails upstream, the agent gets a generic "tool list is
192
+ unavailable" error. The upstream's error text (which can quote its URL
193
+ path, response body or headers) is not shown to the agent or logged;
194
+ `guardian-mcp tools --url` prints it with the URL, its path identifiers,
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.
197
+
198
+ What is **not** governed: a remote server the agent connects to directly
199
+ (for example an MCP client's own `"url"` entry, or a model provider's hosted
200
+ MCP connector), and anything the agent does without MCP. Also not covered:
201
+
202
+ - **Lost results after ALLOW.** If the remote times out (the MCP SDK's
203
+ default request timeout is 60 seconds) or drops the session after
204
+ Guardian allowed a call, the call may have run, but the capability is not
205
+ redeemed and no receipt is written. Receipts can then understate what
206
+ happened; check the remote system itself.
207
+ - **Keys in the same process tree.** guardian-mcp keeps the upstream key out
208
+ of the agent's MCP view. It cannot stop other code that runs as the same
209
+ user, in the same container, from reading the key from the environment it
210
+ was set in and calling the remote server directly. Recipes:
211
+ [ZeroClaw](../../examples/zeroclaw/README.md) and
212
+ [your own container (CrewAI, LangGraph, AutoGen)](../../examples/byo/README.md).
213
+ Runnable example: `examples/remote-mcp.mjs` (SIMULATED by default, LIVE with
214
+ Guardian settings). `--url` is in the published `@odingard/guardian-mcp`
215
+ from 0.3.3.
216
+
90
217
  ## See what you are about to govern: `guardian-mcp tools`
91
218
 
92
219
  Before putting a server behind the proxy, list its tools along with a suggested Guardian mode for each. This needs no Guardian credential:
@@ -105,11 +232,13 @@ createIssue Ask me first "create" may change something ⚠ name not accept
105
232
 
106
233
  How the suggestions are made:
107
234
 
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.
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`.
110
239
 
111
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.
112
241
 
113
242
  ## License
114
243
 
115
- Apache-2.0. Canonical development happens in the [Odingard Guardian monorepo](https://github.com/Odingard/guardian).
244
+ Apache-2.0.