intent-envelope-mcp 0.0.0 → 0.2.0
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 +180 -180
- package/dist/config.js +15 -15
- package/package.json +57 -57
- package/src/bin.ts +62 -62
- package/src/config.ts +89 -89
- package/src/index.ts +12 -12
- package/src/ledger.ts +61 -61
- package/src/relay.ts +238 -238
package/README.md
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
# intent-envelope-mcp
|
|
2
|
-
|
|
3
|
-
Enforce an intent envelope on an MCP server. A tool call outside the envelope is refused
|
|
4
|
-
before it reaches the server.
|
|
5
|
-
|
|
6
|
-
Your MCP client launches this instead of the server it wants. Every tool call crosses
|
|
7
|
-
this process, is decided against the envelope, and is forwarded only if the envelope
|
|
8
|
-
permits it. Nothing about the client changes, and no code in your agent changes.
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
your agent ──► intent-envelope-mcp ──► the MCP server
|
|
12
|
-
(decides, then forwards or refuses)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Implements [Intent Envelope 0.1](../../spec/v0.1.md). Apache-2.0.
|
|
16
|
-
|
|
17
|
-
## Why a separate process
|
|
18
|
-
|
|
19
|
-
The component that decides what an agent may do is not the component an attacker is
|
|
20
|
-
talking to. An agent can be argued into anything; it cannot argue with a process that
|
|
21
|
-
does not read its reasoning. The envelope is read from disk before the first message is
|
|
22
|
-
accepted and is never re-read, so there is no tool call, argument, document or
|
|
23
|
-
notification that changes what is permitted.
|
|
24
|
-
|
|
25
|
-
## Install
|
|
26
|
-
|
|
27
|
-
```sh
|
|
28
|
-
npm install -g intent-envelope-mcp
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Requires Node 22.18 or newer. The only dependency is
|
|
32
|
-
[`intent-envelope`](../typescript) itself.
|
|
33
|
-
|
|
34
|
-
## Use
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Everything after `--` is the server to govern. In a client config, wrap what you already
|
|
41
|
-
had:
|
|
42
|
-
|
|
43
|
-
```json
|
|
44
|
-
{
|
|
45
|
-
"mcpServers": {
|
|
46
|
-
"invoices": {
|
|
47
|
-
"command": "intent-envelope-mcp",
|
|
48
|
-
"args": [
|
|
49
|
-
"--envelope", "/etc/intent-envelopes/ap-agent.json",
|
|
50
|
-
"--ledger", "/var/log/ap-agent-decisions.jsonl",
|
|
51
|
-
"--", "npx", "-y", "@acme/invoice-mcp"
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
| Option | Meaning |
|
|
59
|
-
| --- | --- |
|
|
60
|
-
| `--envelope <path>` | the envelope to enforce, or `INTENT_ENVELOPE` |
|
|
61
|
-
| `--ledger <path>` | append decisions as JSON lines, or `INTENT_ENVELOPE_LEDGER` |
|
|
62
|
-
| `--no-filter-tools` | advertise every upstream tool, refusing the ones not granted |
|
|
63
|
-
|
|
64
|
-
An envelope that fails validation stops the process with exit code 2 before it accepts a
|
|
65
|
-
message. An enforcement point that starts with an envelope it cannot understand is worse
|
|
66
|
-
than one that does not start, because the client assumes it is running.
|
|
67
|
-
|
|
68
|
-
## What a refusal looks like
|
|
69
|
-
|
|
70
|
-
A refused call returns a JSON-RPC error rather than a tool result. The tool never ran, so
|
|
71
|
-
reporting it as a result that failed would be a lie the agent then reasons from.
|
|
72
|
-
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"jsonrpc": "2.0",
|
|
76
|
-
"id": 4,
|
|
77
|
-
"error": {
|
|
78
|
-
"code": -32000,
|
|
79
|
-
"message": "intent envelope: refused by tools.deny: issue_payment is on tools.deny, which no later grant overrides",
|
|
80
|
-
"data": {
|
|
81
|
-
"intent_envelope": {
|
|
82
|
-
"envelope_id": "invoice-reconciler",
|
|
83
|
-
"revision": 3,
|
|
84
|
-
"agent_identity": "invoice-reconciler",
|
|
85
|
-
"tool": "issue_payment",
|
|
86
|
-
"result": "deny",
|
|
87
|
-
"rule": "tools.deny",
|
|
88
|
-
"decided_at": "2026-11-03T16:09:44.812Z"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Two codes, because collapsing them would turn "a named person must decide this" into
|
|
96
|
-
"no":
|
|
97
|
-
|
|
98
|
-
| Code | Meaning |
|
|
99
|
-
| --- | --- |
|
|
100
|
-
| `-32000` | refused by the envelope |
|
|
101
|
-
| `-32001` | a named human must approve this call |
|
|
102
|
-
|
|
103
|
-
## The ledger
|
|
104
|
-
|
|
105
|
-
With `--ledger`, every decision is appended as one JSON object per line, flushed as it is
|
|
106
|
-
made. Refusals are recorded, not only successes: a log holding what succeeded cannot
|
|
107
|
-
answer the question an incident asks, which is what the agent tried to do.
|
|
108
|
-
|
|
109
|
-
Each record names the envelope, its revision, the agent identity, the tool, the result,
|
|
110
|
-
the rule that decided and when, which is enough to reconstruct a run months later against
|
|
111
|
-
the exact envelope revision in force at the time.
|
|
112
|
-
|
|
113
|
-
If the ledger cannot be written, enforcement continues and the failure is reported once
|
|
114
|
-
on stderr. Refusing correctly while unable to log is degraded; allowing calls because a
|
|
115
|
-
disk filled up would be a breach.
|
|
116
|
-
|
|
117
|
-
## See it work
|
|
118
|
-
|
|
119
|
-
```sh
|
|
120
|
-
node demo/poisoned-invoice.mjs
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
An agent reads a supplier invoice containing instructions, believes every word, and
|
|
124
|
-
carries them out in order. Nothing detects the injection, no prompt asks the model to be
|
|
125
|
-
careful, and there is no classifier. The exfiltration and the payment are refused anyway.
|
|
126
|
-
The invoice server's own stderr shows it ran only the two calls the envelope permitted.
|
|
127
|
-
|
|
128
|
-
The document also tells the agent not to involve a human. That instruction has nothing to
|
|
129
|
-
enforce it, so the mismatch escalates regardless.
|
|
130
|
-
|
|
131
|
-
## What this does not do
|
|
132
|
-
|
|
133
|
-
Worth being explicit, because a guardrail people misunderstand is worse than none.
|
|
134
|
-
|
|
135
|
-
- **It does not wait for a human.** An escalation is recorded as an escalation and the
|
|
136
|
-
call is refused with the name of who must approve it. There is no approval channel yet,
|
|
137
|
-
so `on_no_response: allow` is **not** honoured: escalated calls are refused rather than
|
|
138
|
-
allowed, and the process says so on startup if your envelope sets it. Being stricter
|
|
139
|
-
than the envelope asked is the safe direction, but it is a difference and you should
|
|
140
|
-
know about it.
|
|
141
|
-
- **Tool filtering is not the gate.** `tools/list` is filtered to what the envelope
|
|
142
|
-
grants, which reduces what the model is tempted by, but a client can call any name it
|
|
143
|
-
likes. The guarantee comes from intercepting `tools/call`, which happens whether or not
|
|
144
|
-
the tool was ever advertised. `--no-filter-tools` turns the cosmetic half off and
|
|
145
|
-
changes nothing about enforcement.
|
|
146
|
-
- **It governs one server.** Run one of these per server you want governed, which is how
|
|
147
|
-
MCP client configs are shaped anyway. Calls that reach a server by some other route are
|
|
148
|
-
not governed by it, so the envelope is only as good as the chokepoint you put it in.
|
|
149
|
-
- **It does not inspect prompts or model output.** It decides tool calls. An agent talked
|
|
150
|
-
into something that stays inside its envelope is not something this notices.
|
|
151
|
-
- **`max_runtime_seconds` is not enforced**, because this process does not own your run
|
|
152
|
-
loop and cannot tell a slow tool from a long think.
|
|
153
|
-
|
|
154
|
-
## Why a line relay rather than the MCP SDK
|
|
155
|
-
|
|
156
|
-
This understands exactly two methods and relays every other line byte for byte.
|
|
157
|
-
|
|
158
|
-
The protocol has two eras in circulation, one using `server/discover` with inline `_meta`
|
|
159
|
-
and one using the `initialize` handshake, and it keeps growing: `subscriptions/listen`,
|
|
160
|
-
`notifications/cancelled`, `InputRequiredResult`. A proxy built on a protocol library
|
|
161
|
-
would have to model all of it and would break on the next addition. A relay that forwards
|
|
162
|
-
what it does not recognise works across both eras and survives additions it has never
|
|
163
|
-
heard of. An unparseable line is passed along untouched, because this process is not the
|
|
164
|
-
arbiter of protocol validity; it is the arbiter of one question.
|
|
165
|
-
|
|
166
|
-
It also keeps the dependency count at one.
|
|
167
|
-
|
|
168
|
-
## Development
|
|
169
|
-
|
|
170
|
-
```sh
|
|
171
|
-
npm test # end to end over real pipes, against a server that records calls
|
|
172
|
-
npm run typecheck
|
|
173
|
-
npm run smoke # pack, install the tarball, run the installed binary
|
|
174
|
-
npm run build
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
The suite's central assertion is not that the client saw an error. It is that the refused
|
|
178
|
-
call is **absent from the upstream server's own record**. Breaking the interception so
|
|
179
|
-
calls are recorded and then forwarded anyway fails eight of the sixteen tests, which is
|
|
180
|
-
the behaviour a logger-pretending-to-be-a-guardrail would have.
|
|
1
|
+
# intent-envelope-mcp
|
|
2
|
+
|
|
3
|
+
Enforce an intent envelope on an MCP server. A tool call outside the envelope is refused
|
|
4
|
+
before it reaches the server.
|
|
5
|
+
|
|
6
|
+
Your MCP client launches this instead of the server it wants. Every tool call crosses
|
|
7
|
+
this process, is decided against the envelope, and is forwarded only if the envelope
|
|
8
|
+
permits it. Nothing about the client changes, and no code in your agent changes.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
your agent ──► intent-envelope-mcp ──► the MCP server
|
|
12
|
+
(decides, then forwards or refuses)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Implements [Intent Envelope 0.1](../../spec/v0.1.md). Apache-2.0.
|
|
16
|
+
|
|
17
|
+
## Why a separate process
|
|
18
|
+
|
|
19
|
+
The component that decides what an agent may do is not the component an attacker is
|
|
20
|
+
talking to. An agent can be argued into anything; it cannot argue with a process that
|
|
21
|
+
does not read its reasoning. The envelope is read from disk before the first message is
|
|
22
|
+
accepted and is never re-read, so there is no tool call, argument, document or
|
|
23
|
+
notification that changes what is permitted.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g intent-envelope-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Node 22.18 or newer. The only dependency is
|
|
32
|
+
[`intent-envelope`](../typescript) itself.
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Everything after `--` is the server to govern. In a client config, wrap what you already
|
|
41
|
+
had:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"invoices": {
|
|
47
|
+
"command": "intent-envelope-mcp",
|
|
48
|
+
"args": [
|
|
49
|
+
"--envelope", "/etc/intent-envelopes/ap-agent.json",
|
|
50
|
+
"--ledger", "/var/log/ap-agent-decisions.jsonl",
|
|
51
|
+
"--", "npx", "-y", "@acme/invoice-mcp"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Option | Meaning |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `--envelope <path>` | the envelope to enforce, or `INTENT_ENVELOPE` |
|
|
61
|
+
| `--ledger <path>` | append decisions as JSON lines, or `INTENT_ENVELOPE_LEDGER` |
|
|
62
|
+
| `--no-filter-tools` | advertise every upstream tool, refusing the ones not granted |
|
|
63
|
+
|
|
64
|
+
An envelope that fails validation stops the process with exit code 2 before it accepts a
|
|
65
|
+
message. An enforcement point that starts with an envelope it cannot understand is worse
|
|
66
|
+
than one that does not start, because the client assumes it is running.
|
|
67
|
+
|
|
68
|
+
## What a refusal looks like
|
|
69
|
+
|
|
70
|
+
A refused call returns a JSON-RPC error rather than a tool result. The tool never ran, so
|
|
71
|
+
reporting it as a result that failed would be a lie the agent then reasons from.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"jsonrpc": "2.0",
|
|
76
|
+
"id": 4,
|
|
77
|
+
"error": {
|
|
78
|
+
"code": -32000,
|
|
79
|
+
"message": "intent envelope: refused by tools.deny: issue_payment is on tools.deny, which no later grant overrides",
|
|
80
|
+
"data": {
|
|
81
|
+
"intent_envelope": {
|
|
82
|
+
"envelope_id": "invoice-reconciler",
|
|
83
|
+
"revision": 3,
|
|
84
|
+
"agent_identity": "invoice-reconciler",
|
|
85
|
+
"tool": "issue_payment",
|
|
86
|
+
"result": "deny",
|
|
87
|
+
"rule": "tools.deny",
|
|
88
|
+
"decided_at": "2026-11-03T16:09:44.812Z"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two codes, because collapsing them would turn "a named person must decide this" into
|
|
96
|
+
"no":
|
|
97
|
+
|
|
98
|
+
| Code | Meaning |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `-32000` | refused by the envelope |
|
|
101
|
+
| `-32001` | a named human must approve this call |
|
|
102
|
+
|
|
103
|
+
## The ledger
|
|
104
|
+
|
|
105
|
+
With `--ledger`, every decision is appended as one JSON object per line, flushed as it is
|
|
106
|
+
made. Refusals are recorded, not only successes: a log holding what succeeded cannot
|
|
107
|
+
answer the question an incident asks, which is what the agent tried to do.
|
|
108
|
+
|
|
109
|
+
Each record names the envelope, its revision, the agent identity, the tool, the result,
|
|
110
|
+
the rule that decided and when, which is enough to reconstruct a run months later against
|
|
111
|
+
the exact envelope revision in force at the time.
|
|
112
|
+
|
|
113
|
+
If the ledger cannot be written, enforcement continues and the failure is reported once
|
|
114
|
+
on stderr. Refusing correctly while unable to log is degraded; allowing calls because a
|
|
115
|
+
disk filled up would be a breach.
|
|
116
|
+
|
|
117
|
+
## See it work
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
node demo/poisoned-invoice.mjs
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An agent reads a supplier invoice containing instructions, believes every word, and
|
|
124
|
+
carries them out in order. Nothing detects the injection, no prompt asks the model to be
|
|
125
|
+
careful, and there is no classifier. The exfiltration and the payment are refused anyway.
|
|
126
|
+
The invoice server's own stderr shows it ran only the two calls the envelope permitted.
|
|
127
|
+
|
|
128
|
+
The document also tells the agent not to involve a human. That instruction has nothing to
|
|
129
|
+
enforce it, so the mismatch escalates regardless.
|
|
130
|
+
|
|
131
|
+
## What this does not do
|
|
132
|
+
|
|
133
|
+
Worth being explicit, because a guardrail people misunderstand is worse than none.
|
|
134
|
+
|
|
135
|
+
- **It does not wait for a human.** An escalation is recorded as an escalation and the
|
|
136
|
+
call is refused with the name of who must approve it. There is no approval channel yet,
|
|
137
|
+
so `on_no_response: allow` is **not** honoured: escalated calls are refused rather than
|
|
138
|
+
allowed, and the process says so on startup if your envelope sets it. Being stricter
|
|
139
|
+
than the envelope asked is the safe direction, but it is a difference and you should
|
|
140
|
+
know about it.
|
|
141
|
+
- **Tool filtering is not the gate.** `tools/list` is filtered to what the envelope
|
|
142
|
+
grants, which reduces what the model is tempted by, but a client can call any name it
|
|
143
|
+
likes. The guarantee comes from intercepting `tools/call`, which happens whether or not
|
|
144
|
+
the tool was ever advertised. `--no-filter-tools` turns the cosmetic half off and
|
|
145
|
+
changes nothing about enforcement.
|
|
146
|
+
- **It governs one server.** Run one of these per server you want governed, which is how
|
|
147
|
+
MCP client configs are shaped anyway. Calls that reach a server by some other route are
|
|
148
|
+
not governed by it, so the envelope is only as good as the chokepoint you put it in.
|
|
149
|
+
- **It does not inspect prompts or model output.** It decides tool calls. An agent talked
|
|
150
|
+
into something that stays inside its envelope is not something this notices.
|
|
151
|
+
- **`max_runtime_seconds` is not enforced**, because this process does not own your run
|
|
152
|
+
loop and cannot tell a slow tool from a long think.
|
|
153
|
+
|
|
154
|
+
## Why a line relay rather than the MCP SDK
|
|
155
|
+
|
|
156
|
+
This understands exactly two methods and relays every other line byte for byte.
|
|
157
|
+
|
|
158
|
+
The protocol has two eras in circulation, one using `server/discover` with inline `_meta`
|
|
159
|
+
and one using the `initialize` handshake, and it keeps growing: `subscriptions/listen`,
|
|
160
|
+
`notifications/cancelled`, `InputRequiredResult`. A proxy built on a protocol library
|
|
161
|
+
would have to model all of it and would break on the next addition. A relay that forwards
|
|
162
|
+
what it does not recognise works across both eras and survives additions it has never
|
|
163
|
+
heard of. An unparseable line is passed along untouched, because this process is not the
|
|
164
|
+
arbiter of protocol validity; it is the arbiter of one question.
|
|
165
|
+
|
|
166
|
+
It also keeps the dependency count at one.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm test # end to end over real pipes, against a server that records calls
|
|
172
|
+
npm run typecheck
|
|
173
|
+
npm run smoke # pack, install the tarball, run the installed binary
|
|
174
|
+
npm run build
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The suite's central assertion is not that the client saw an error. It is that the refused
|
|
178
|
+
call is **absent from the upstream server's own record**. Breaking the interception so
|
|
179
|
+
calls are recorded and then forwarded anyway fails eight of the sixteen tests, which is
|
|
180
|
+
the behaviour a logger-pretending-to-be-a-guardrail would have.
|
package/dist/config.js
CHANGED
|
@@ -15,21 +15,21 @@ export class ConfigError extends Error {
|
|
|
15
15
|
this.name = 'ConfigError';
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export const USAGE = `intent-envelope-mcp: enforce an intent envelope on an MCP server
|
|
19
|
-
|
|
20
|
-
intent-envelope-mcp --envelope <path> [options] -- <server-command> [server-args...]
|
|
21
|
-
|
|
22
|
-
Options
|
|
23
|
-
--envelope <path> the envelope to enforce (or INTENT_ENVELOPE)
|
|
24
|
-
--ledger <path> append decisions here as JSON lines (or INTENT_ENVELOPE_LEDGER)
|
|
25
|
-
--no-filter-tools advertise every upstream tool, refusing the ones not granted
|
|
26
|
-
--help
|
|
27
|
-
|
|
28
|
-
Everything after -- is the server to govern, run as a child process.
|
|
29
|
-
|
|
30
|
-
intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
|
|
31
|
-
|
|
32
|
-
Point your MCP client at this command instead of the server's own. Nothing else about
|
|
18
|
+
export const USAGE = `intent-envelope-mcp: enforce an intent envelope on an MCP server
|
|
19
|
+
|
|
20
|
+
intent-envelope-mcp --envelope <path> [options] -- <server-command> [server-args...]
|
|
21
|
+
|
|
22
|
+
Options
|
|
23
|
+
--envelope <path> the envelope to enforce (or INTENT_ENVELOPE)
|
|
24
|
+
--ledger <path> append decisions here as JSON lines (or INTENT_ENVELOPE_LEDGER)
|
|
25
|
+
--no-filter-tools advertise every upstream tool, refusing the ones not granted
|
|
26
|
+
--help
|
|
27
|
+
|
|
28
|
+
Everything after -- is the server to govern, run as a child process.
|
|
29
|
+
|
|
30
|
+
intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
|
|
31
|
+
|
|
32
|
+
Point your MCP client at this command instead of the server's own. Nothing else about
|
|
33
33
|
the client changes.`;
|
|
34
34
|
/**
|
|
35
35
|
* Parse argv in the shape `[options] -- command args...`.
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "intent-envelope-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Enforce an intent envelope on an MCP server: refuse a tool call before it reaches the server.",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"intent-envelope-mcp": "./dist/bin.js"
|
|
9
|
-
},
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"default": "./dist/index.js"
|
|
14
|
-
},
|
|
15
|
-
"./package.json": "./package.json"
|
|
16
|
-
},
|
|
17
|
-
"types": "./dist/index.d.ts",
|
|
18
|
-
"main": "./dist/index.js",
|
|
19
|
-
"files": [
|
|
20
|
-
"dist",
|
|
21
|
-
"src",
|
|
22
|
-
"README.md"
|
|
23
|
-
],
|
|
24
|
-
"keywords": [
|
|
25
|
-
"mcp",
|
|
26
|
-
"ai",
|
|
27
|
-
"agents",
|
|
28
|
-
"security",
|
|
29
|
-
"intent",
|
|
30
|
-
"guardrails",
|
|
31
|
-
"proxy"
|
|
32
|
-
],
|
|
33
|
-
"homepage": "https://securedai.org/agent-intent",
|
|
34
|
-
"repository": {
|
|
35
|
-
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/secured-ai/intent-envelope.git",
|
|
37
|
-
"directory": "packages/mcp-proxy"
|
|
38
|
-
},
|
|
39
|
-
"bugs": "https://github.com/secured-ai/intent-envelope/issues",
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=22.18"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"intent-envelope": ">=0.1.0 <1.0.0"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsc -p tsconfig.build.json",
|
|
48
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
49
|
-
"test": "node --test test/*.test.ts",
|
|
50
|
-
"smoke": "node tools/consumer-smoke.mjs",
|
|
51
|
-
"prepack": "npm run build"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "^22.19.1",
|
|
55
|
-
"typescript": "^5.9.3"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "intent-envelope-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Enforce an intent envelope on an MCP server: refuse a tool call before it reaches the server.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"intent-envelope-mcp": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"ai",
|
|
27
|
+
"agents",
|
|
28
|
+
"security",
|
|
29
|
+
"intent",
|
|
30
|
+
"guardrails",
|
|
31
|
+
"proxy"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://securedai.org/agent-intent",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/secured-ai/intent-envelope.git",
|
|
37
|
+
"directory": "packages/mcp-proxy"
|
|
38
|
+
},
|
|
39
|
+
"bugs": "https://github.com/secured-ai/intent-envelope/issues",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"intent-envelope": ">=0.1.0 <1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.build.json",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
49
|
+
"test": "node --test test/*.test.ts",
|
|
50
|
+
"smoke": "node tools/consumer-smoke.mjs",
|
|
51
|
+
"prepack": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.19.1",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/bin.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The command an MCP client launches in place of the server it wants governed.
|
|
4
|
-
*
|
|
5
|
-
* Exit codes: 0 if the upstream exited cleanly, the upstream's own code if it did not,
|
|
6
|
-
* and 2 for a configuration problem here. A client that cannot tell the difference
|
|
7
|
-
* between "the server failed" and "the envelope was unreadable" cannot be debugged.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { spawn } from 'node:child_process';
|
|
11
|
-
|
|
12
|
-
import { ConfigError, parse } from './config.ts';
|
|
13
|
-
import { fileLedger, memoryLedger } from './ledger.ts';
|
|
14
|
-
import { Relay } from './relay.ts';
|
|
15
|
-
|
|
16
|
-
const log = (message: string) => process.stderr.write(`[intent-envelope] ${message}\n`);
|
|
17
|
-
|
|
18
|
-
function main(): void {
|
|
19
|
-
let config;
|
|
20
|
-
try {
|
|
21
|
-
config = parse(process.argv.slice(2), process.env);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
if (error instanceof ConfigError) {
|
|
24
|
-
process.stderr.write(`${error.message}\n`);
|
|
25
|
-
process.exit(2);
|
|
26
|
-
}
|
|
27
|
-
// A schema failure arrives here. It names the field, which is the useful part.
|
|
28
|
-
process.stderr.write(`[intent-envelope] the envelope is not valid: ${(error as Error).message}\n`);
|
|
29
|
-
process.exit(2);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const { envelope, command, args } = config;
|
|
33
|
-
log(`enforcing ${envelope.id} revision ${envelope.revision ?? 1} on: ${command} ${args.join(' ')}`);
|
|
34
|
-
|
|
35
|
-
const upstream = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
36
|
-
|
|
37
|
-
upstream.on('error', (error) => {
|
|
38
|
-
log(`cannot start the server: ${error.message}`);
|
|
39
|
-
process.exit(2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const ledger = config.ledgerPath ? fileLedger(config.ledgerPath, log) : memoryLedger();
|
|
43
|
-
const relay = new Relay({
|
|
44
|
-
envelope,
|
|
45
|
-
ledger,
|
|
46
|
-
upstream,
|
|
47
|
-
input: process.stdin,
|
|
48
|
-
output: process.stdout,
|
|
49
|
-
log,
|
|
50
|
-
filterToolList: config.filterToolList,
|
|
51
|
-
});
|
|
52
|
-
relay.start();
|
|
53
|
-
|
|
54
|
-
upstream.on('exit', (code, signal) => {
|
|
55
|
-
const counts = relay.summary();
|
|
56
|
-
log(`${counts.allow} allowed, ${counts.deny} refused, ${counts.escalate} escalated`);
|
|
57
|
-
if (config.ledgerPath) log(`ledger: ${config.ledgerPath}`);
|
|
58
|
-
process.exit(signal ? 1 : (code ?? 0));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The command an MCP client launches in place of the server it wants governed.
|
|
4
|
+
*
|
|
5
|
+
* Exit codes: 0 if the upstream exited cleanly, the upstream's own code if it did not,
|
|
6
|
+
* and 2 for a configuration problem here. A client that cannot tell the difference
|
|
7
|
+
* between "the server failed" and "the envelope was unreadable" cannot be debugged.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
import { ConfigError, parse } from './config.ts';
|
|
13
|
+
import { fileLedger, memoryLedger } from './ledger.ts';
|
|
14
|
+
import { Relay } from './relay.ts';
|
|
15
|
+
|
|
16
|
+
const log = (message: string) => process.stderr.write(`[intent-envelope] ${message}\n`);
|
|
17
|
+
|
|
18
|
+
function main(): void {
|
|
19
|
+
let config;
|
|
20
|
+
try {
|
|
21
|
+
config = parse(process.argv.slice(2), process.env);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof ConfigError) {
|
|
24
|
+
process.stderr.write(`${error.message}\n`);
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
// A schema failure arrives here. It names the field, which is the useful part.
|
|
28
|
+
process.stderr.write(`[intent-envelope] the envelope is not valid: ${(error as Error).message}\n`);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { envelope, command, args } = config;
|
|
33
|
+
log(`enforcing ${envelope.id} revision ${envelope.revision ?? 1} on: ${command} ${args.join(' ')}`);
|
|
34
|
+
|
|
35
|
+
const upstream = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
36
|
+
|
|
37
|
+
upstream.on('error', (error) => {
|
|
38
|
+
log(`cannot start the server: ${error.message}`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ledger = config.ledgerPath ? fileLedger(config.ledgerPath, log) : memoryLedger();
|
|
43
|
+
const relay = new Relay({
|
|
44
|
+
envelope,
|
|
45
|
+
ledger,
|
|
46
|
+
upstream,
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout,
|
|
49
|
+
log,
|
|
50
|
+
filterToolList: config.filterToolList,
|
|
51
|
+
});
|
|
52
|
+
relay.start();
|
|
53
|
+
|
|
54
|
+
upstream.on('exit', (code, signal) => {
|
|
55
|
+
const counts = relay.summary();
|
|
56
|
+
log(`${counts.allow} allowed, ${counts.deny} refused, ${counts.escalate} escalated`);
|
|
57
|
+
if (config.ledgerPath) log(`ledger: ${config.ledgerPath}`);
|
|
58
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|