@multiagentcoordinationprotocol/mcp 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/multiagentcoordinationprotocol/macp-mcp-typescript/compare/v0.2.0...v0.3.0) (2026-10-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * **gate:** macp-mcp gate — quorum-gated upstream tool calls (W3.8) ([#5](https://github.com/multiagentcoordinationprotocol/macp-mcp-typescript/issues/5)) ([503f5f4](https://github.com/multiagentcoordinationprotocol/macp-mcp-typescript/commit/503f5f413ee77de1d95d9e7802f647313d6e1df7))
9
+
3
10
  ## [0.2.0](https://github.com/multiagentcoordinationprotocol/macp-mcp-typescript/compare/v0.1.0...v0.2.0) (2026-10-25)
4
11
 
5
12
 
package/README.md CHANGED
@@ -6,6 +6,8 @@ session. It is the TypeScript twin of the Python
6
6
  [macp-mcp](https://github.com/multiagentcoordinationprotocol/macp-mcp). Both
7
7
  expose the same tool surface: the same names, input schemas and outputs,
8
8
  pinned by one shared manifest ([Tool contract](#tool-contract)).
9
+ Package versions are independent of the Python macp-mcp; tool parity is
10
+ enforced by `make verify-contract`, not by matching version numbers.
9
11
 
10
12
  MCP decides which tools a model can call. MACP decides who may use them and
11
13
  when, and records what was decided. The model drives every step through
@@ -69,8 +71,8 @@ claude mcp add macp \
69
71
  -- npx -y @multiagentcoordinationprotocol/mcp
70
72
  ```
71
73
 
72
- Then ask the model something like: *"Start a MACP decision with alice on
73
- which deployment plan to use, propose a canary rollout, and vote for it."*
74
+ Then ask the model something like: _"Start a MACP decision with alice on
75
+ which deployment plan to use, propose a canary rollout, and vote for it."_
74
76
 
75
77
  ### Against a production runtime
76
78
 
@@ -89,31 +91,31 @@ this agent:
89
91
 
90
92
  These are the same variables, with the same defaults, as the Python server.
91
93
 
92
- | Variable | Default | Meaning |
93
- |---|---|---|
94
- | `MACP_RUNTIME_TARGET` | `127.0.0.1:50051` | Runtime gRPC address (`host:port`). |
95
- | `MACP_AGENT_ID` | `mcp-agent` | The MACP agent this server acts as. With dev-agent auth it is also the bearer token. With `MACP_AUTH_TOKEN` it is the identity the runtime must bind the token to. |
96
- | `MACP_AUTH_TOKEN` | unset | Bearer token issued by the runtime. When unset, dev-agent auth is used, which needs a runtime started with `MACP_ALLOW_INSECURE=1`. |
97
- | `MACP_ALLOW_INSECURE` | off | `1`/`true`/`yes`/`on` uses plaintext gRPC. For a local runtime only. |
98
- | `MACP_TLS_CA_FILE` | system trust | PEM bundle for a private CA. Cannot be combined with `MACP_ALLOW_INSECURE`. |
99
- | `MACP_DEFAULT_TTL_MS` | `300000` | Session TTL used when `start_session` is called without `ttl_ms`. |
100
- | `MACP_REPLAY_IDLE_MS` | `1000` | How long a history read waits for the next envelope before treating the replay as complete. |
94
+ | Variable | Default | Meaning |
95
+ | --------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
96
+ | `MACP_RUNTIME_TARGET` | `127.0.0.1:50051` | Runtime gRPC address (`host:port`). |
97
+ | `MACP_AGENT_ID` | `mcp-agent` | The MACP agent this server acts as. With dev-agent auth it is also the bearer token. With `MACP_AUTH_TOKEN` it is the identity the runtime must bind the token to. |
98
+ | `MACP_AUTH_TOKEN` | unset | Bearer token issued by the runtime. When unset, dev-agent auth is used, which needs a runtime started with `MACP_ALLOW_INSECURE=1`. |
99
+ | `MACP_ALLOW_INSECURE` | off | `1`/`true`/`yes`/`on` uses plaintext gRPC. For a local runtime only. |
100
+ | `MACP_TLS_CA_FILE` | system trust | PEM bundle for a private CA. Cannot be combined with `MACP_ALLOW_INSECURE`. |
101
+ | `MACP_DEFAULT_TTL_MS` | `300000` | Session TTL used when `start_session` is called without `ttl_ms`. |
102
+ | `MACP_REPLAY_IDLE_MS` | `1000` | How long a history read waits for the next envelope before treating the replay as complete. |
101
103
 
102
104
  The server logs to stderr only, because stdout carries the MCP protocol. A bad
103
105
  configuration exits with status 2 and a one-line message.
104
106
 
105
107
  ## Tools
106
108
 
107
- | Tool | Kind | What it does |
108
- |---|---|---|
109
- | `start_session` | write | Start a Decision session with this agent as initiator. Takes `intent`, `participants` and optional `ttl_ms` and `context_id`. Returns the `session_id` to share. |
110
- | `propose` | write | Put an option (`proposal_id`, `option`, `rationale`) up for decision. |
111
- | `evaluate` | write | Record a scored assessment (`APPROVE` / `REVIEW` / `BLOCK` / `REJECT`, confidence 0..1) before voting. |
112
- | `vote` | write | Cast this agent's `APPROVE` / `REJECT` / `ABSTAIN` vote on a proposal. |
113
- | `commit` | write | Resolve the session with a binding Commitment (`action`, `authority_scope`, `reason`). Under the default policy only the initiator can commit. |
114
- | `get_session` | read | The session's state, participants, proposals, evaluations, votes, approve totals and majority winner. |
115
- | `get_commitment` | read | The resolved session's binding Commitment, the exact payload bytes (`payload_b64`) and its RFC-MACP-0013 `commitment_hash`. |
116
- | `verify` | read | Recompute a commitment hash with the SDK verifier, either from the runtime's recorded history (`session_id`) or from bytes you hold (`payload_b64`). Returns `match`, `mismatch` or `cannot_verify`. |
109
+ | Tool | Kind | What it does |
110
+ | ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `start_session` | write | Start a Decision session with this agent as initiator. Takes `intent`, `participants` and optional `ttl_ms` and `context_id`. Returns the `session_id` to share. |
112
+ | `propose` | write | Put an option (`proposal_id`, `option`, `rationale`) up for decision. |
113
+ | `evaluate` | write | Record a scored assessment (`APPROVE` / `REVIEW` / `BLOCK` / `REJECT`, confidence 0..1) before voting. |
114
+ | `vote` | write | Cast this agent's `APPROVE` / `REJECT` / `ABSTAIN` vote on a proposal. |
115
+ | `commit` | write | Resolve the session with a binding Commitment (`action`, `authority_scope`, `reason`). Under the default policy only the initiator can commit. |
116
+ | `get_session` | read | The session's state, participants, proposals, evaluations, votes, approve totals and majority winner. |
117
+ | `get_commitment` | read | The resolved session's binding Commitment, the exact payload bytes (`payload_b64`) and its RFC-MACP-0013 `commitment_hash`. |
118
+ | `verify` | read | Recompute a commitment hash with the SDK verifier, either from the runtime's recorded history (`session_id`) or from bytes you hold (`payload_b64`). Returns `match`, `mismatch` or `cannot_verify`. |
117
119
 
118
120
  Errors the model can act on, such as a runtime rejection or a bad argument,
119
121
  come back as tool errors with the text `"<CODE>: <message>"`, for example
@@ -124,6 +126,87 @@ the gRPC status name.
124
126
  The server is stateless. Every read replays the session from the runtime, so a
125
127
  restart loses nothing, and several servers (agents) can share one session.
126
128
 
129
+ ## Gate upstream tool calls (`macp-mcp gate`)
130
+
131
+ `macp-mcp gate` is a second mode of the same binary. It runs as an MCP server
132
+ in front of another (upstream) MCP server, and puts the tool calls you name
133
+ behind a MACP session: a matching call reaches the upstream only after a
134
+ Quorum or Decision session commits `approve`. Every other call passes straight
135
+ through.
136
+
137
+ ```bash
138
+ macp-mcp gate --policy gate.json --upstream npx -y @modelcontextprotocol/server-filesystem /srv/data
139
+ ```
140
+
141
+ `--upstream` takes the rest of the command line as the upstream's stdio
142
+ command. The gate uses the same environment variables as the base server
143
+ (`MACP_AGENT_ID` is the gate's identity: it initiates and commits every
144
+ session). The upstream inherits the gate's environment minus
145
+ `MACP_AUTH_TOKEN`, so the gate's credential never reaches it.
146
+
147
+ `gate.json` names the rules; the first rule whose `tool` matches wins (exact
148
+ name, or a glob with `*` and `?`):
149
+
150
+ ```json
151
+ {
152
+ "version": 1,
153
+ "server": "filesystem",
154
+ "rules": [
155
+ { "tool": "write_*", "mode": "macp.mode.quorum.v1", "participants": ["alice", "bob", "carol"], "threshold": 2 },
156
+ { "tool": "move_file", "mode": "macp.mode.decision.v1", "participants": ["alice", "bob"], "ttl_ms": 60000 }
157
+ ]
158
+ }
159
+ ```
160
+
161
+ | Field | Meaning |
162
+ | ---------------- | ------------------------------------------------------------------------------------------------- |
163
+ | `server` | The upstream's name, bound into every gated call's `context_id`. |
164
+ | `tool` | Exact tool name or glob. |
165
+ | `mode` | `macp.mode.quorum.v1` (ApprovalRequest + ballots) or `macp.mode.decision.v1` (Proposal + votes). |
166
+ | `participants` | The approvers. Must not include the gate's own agent id. |
167
+ | `threshold` | APPROVE ballots needed, in either mode. Default: a strict majority of `participants`. |
168
+ | `ttl_ms` | Session TTL. Default `300000`. |
169
+ | `policy_version` | Optional RFC-MACP-0012 policy to bind the session to, for example one of the spec's policy packs. |
170
+
171
+ For a gated call the gate:
172
+
173
+ 1. Starts a session whose `context_id` is `sha256:` + the hex SHA-256 of the
174
+ RFC 8785 (JCS) form of `{"arguments", "server", "tool"}`. The same canonical
175
+ JSON travels in the ApprovalRequest `details` (Quorum) or the Proposal
176
+ `rationale` (Decision), so an approver can recompute the binding.
177
+ 2. Commits `approve` as soon as the approvers' latest ballots reach the
178
+ threshold, or `deny` as soon as they no longer can (REJECT and ABSTAIN both
179
+ count as not-approve).
180
+ 3. On `approve`, forwards the call and adds the commitment to the result,
181
+ beside any `_meta` the upstream set:
182
+
183
+ ```json
184
+ "_meta": {
185
+ "io.multiagentcoordinationprotocol/commitment": {
186
+ "session_id": "…", "mode": "macp.mode.quorum.v1", "outcome": "approve",
187
+ "commitment_hash": "sha256:…"
188
+ }
189
+ }
190
+ ```
191
+
192
+ `commitment_hash` is the RFC-MACP-0013 hash; the base server's `verify`
193
+ tool recomputes it from the runtime's history.
194
+
195
+ 4. Otherwise refuses with a tool error, and never calls the upstream:
196
+ `POLICY_DENIED: …` when the session commits `deny` (or a bound policy
197
+ refuses the commitment), `SESSION_NOT_OPEN: …` when the session expires or
198
+ is cancelled without a commitment. If the session is still open 5 s after
199
+ its TTL, the gate cancels it. A call whose arguments hold an integer
200
+ outside ±(2^53−1) is refused with `INVALID_ARGUMENT: …` before any session
201
+ opens: JSON may already have rounded it, so it has no canonical form.
202
+
203
+ Approvers are ordinary MACP agents, for example a base `macp-mcp` server
204
+ driven by a person or another model, voting through the runtime. See
205
+ [examples/gated-tool-call.md](examples/gated-tool-call.md) for a full
206
+ transcript. The policy schema, CLI flags, wire values and `context_id` test
207
+ vectors live in [`contract/gate/`](contract/gate), byte-identical to the
208
+ Python macp-mcp's.
209
+
127
210
  ## Tool contract
128
211
 
129
212
  [`contract/tools.json`](contract/tools.json) pins the tool names,
@@ -135,7 +218,8 @@ arguments against it with Ajv, so the advertised surface cannot drift from
135
218
  the file. The file also ships in the npm package as
136
219
  `@multiagentcoordinationprotocol/mcp/contract/tools.json`.
137
220
 
138
- CI fails if the copy drifts from the Python repo's (`make verify-contract`).
221
+ The gate's contract (`contract/gate/`) is vendored the same way. CI fails if
222
+ any of these files drifts from the Python repo's (`make verify-contract`).
139
223
  After a deliberate surface change lands in macp-mcp, run `make sync-contract`
140
224
  and implement the change here.
141
225
 
@@ -0,0 +1,32 @@
1
+ {
2
+ "contract_version": "1",
3
+ "command": "macp-mcp gate",
4
+ "usage": "macp-mcp gate --policy <gate.json> --upstream <command> [args...]",
5
+ "flags": [
6
+ {
7
+ "name": "--policy",
8
+ "required": true,
9
+ "value": "PATH",
10
+ "description": "The gate.json policy file (contract/gate/gate.schema.json)."
11
+ },
12
+ {
13
+ "name": "--upstream",
14
+ "required": true,
15
+ "value": "COMMAND [ARGS...]",
16
+ "description": "The upstream MCP server's stdio command line. Must be the last option: every argument after it belongs to the upstream command."
17
+ }
18
+ ],
19
+ "environment": {
20
+ "read": ["MACP_RUNTIME_TARGET", "MACP_AGENT_ID", "MACP_AUTH_TOKEN", "MACP_ALLOW_INSECURE", "MACP_TLS_CA_FILE"],
21
+ "withheld_from_upstream": ["MACP_AUTH_TOKEN"],
22
+ "note": "The upstream process inherits the gate's environment minus withheld_from_upstream. The gate is the MACP agent MACP_AGENT_ID; it initiates and commits every approval session."
23
+ },
24
+ "exit_codes": {
25
+ "2": "Unusable configuration: bad MACP_* environment, an invalid gate.json, or a missing --upstream command. One line on stderr."
26
+ },
27
+ "server": {
28
+ "name": "macp-mcp-gate",
29
+ "proxies": ["tools/list", "tools/call"],
30
+ "instructions": "Some tools on this server are gated by MACP: a call to one opens a MACP session and runs only after the named approvers approve it. A refused call returns an error starting POLICY_DENIED or SESSION_NOT_OPEN; do not retry it unchanged."
31
+ }
32
+ }
@@ -0,0 +1,164 @@
1
+ {
2
+ "contract_version": "1",
3
+ "description": "macp-mcp gate context_id vectors. context_id = \"sha256:\" + lowercase hex SHA-256 of the RFC 8785 (JCS) canonical JSON (UTF-8) of {\"arguments\": input.arguments or {}, \"server\": input.server, \"tool\": input.tool}. preimage is that canonical JSON. Numbers are IEEE-754 doubles, as JSON.parse reads them. A vector with expected_error instead of preimage and context_id is refused with exactly that text, before any session is opened.",
4
+ "vectors": [
5
+ {
6
+ "name": "arguments-absent",
7
+ "note": "Absent arguments hash as {}.",
8
+ "input": {
9
+ "server": "filesystem",
10
+ "tool": "list_allowed_directories"
11
+ },
12
+ "preimage": "{\"arguments\":{},\"server\":\"filesystem\",\"tool\":\"list_allowed_directories\"}",
13
+ "context_id": "sha256:d79291c4a1ee28e189486c939345523709718be643a11d098f2254eae7b3f63a"
14
+ },
15
+ {
16
+ "name": "spec-worked-example",
17
+ "note": "The worked example in the spec repo's docs/mcp-tool-gating.md.",
18
+ "input": {
19
+ "server": "filesystem",
20
+ "tool": "write_file",
21
+ "arguments": {
22
+ "mode": "w",
23
+ "path": "/etc/hosts"
24
+ }
25
+ },
26
+ "preimage": "{\"arguments\":{\"mode\":\"w\",\"path\":\"/etc/hosts\"},\"server\":\"filesystem\",\"tool\":\"write_file\"}",
27
+ "context_id": "sha256:e8272f181497f6286a735f2bd9a2bd535b65e377fc85b6ece9d3a6bac5b73057"
28
+ },
29
+ {
30
+ "name": "simple",
31
+ "note": "Flat string arguments; keys sorted.",
32
+ "input": {
33
+ "server": "filesystem",
34
+ "tool": "write_file",
35
+ "arguments": {
36
+ "path": "/srv/reports/q3.md",
37
+ "content": "Q3 revenue: up 4%"
38
+ }
39
+ },
40
+ "preimage": "{\"arguments\":{\"content\":\"Q3 revenue: up 4%\",\"path\":\"/srv/reports/q3.md\"},\"server\":\"filesystem\",\"tool\":\"write_file\"}",
41
+ "context_id": "sha256:330ccb8df9d72d67b7d4835a591e313b5882bf79f69726d9fbfd06d896d77196"
42
+ },
43
+ {
44
+ "name": "nested",
45
+ "note": "Nested objects and arrays, booleans and null; array order is kept.",
46
+ "input": {
47
+ "server": "github",
48
+ "tool": "create_issue",
49
+ "arguments": {
50
+ "repo": "acme/api",
51
+ "labels": [
52
+ "bug",
53
+ "p1"
54
+ ],
55
+ "assignee": null,
56
+ "draft": false,
57
+ "body": {
58
+ "steps": [
59
+ 3,
60
+ 1,
61
+ 2
62
+ ],
63
+ "env": {
64
+ "os": "linux",
65
+ "arch": "arm64"
66
+ }
67
+ }
68
+ }
69
+ },
70
+ "preimage": "{\"arguments\":{\"assignee\":null,\"body\":{\"env\":{\"arch\":\"arm64\",\"os\":\"linux\"},\"steps\":[3,1,2]},\"draft\":false,\"labels\":[\"bug\",\"p1\"],\"repo\":\"acme/api\"},\"server\":\"github\",\"tool\":\"create_issue\"}",
71
+ "context_id": "sha256:d7f3bdf9be0c206ff4afa7b605f9f3e9b8386a850b76e2933a09f125dc8b1ce2"
72
+ },
73
+ {
74
+ "name": "numbers",
75
+ "note": "ECMAScript number formatting: 1.0 -> 1, -0.0 -> 0, small exponents, shortest round-trip, the largest safe integer.",
76
+ "input": {
77
+ "server": "payments",
78
+ "tool": "transfer",
79
+ "arguments": {
80
+ "amount": 1250.5,
81
+ "fee": 0.1,
82
+ "whole": 1.0,
83
+ "negzero": -0.0,
84
+ "small": 1.5e-07,
85
+ "tiny": 1e-07,
86
+ "edge": 1e-06,
87
+ "thirds": 333333333.3333333,
88
+ "max_safe": 9007199254740991,
89
+ "neg": -42
90
+ }
91
+ },
92
+ "preimage": "{\"arguments\":{\"amount\":1250.5,\"edge\":0.000001,\"fee\":0.1,\"max_safe\":9007199254740991,\"neg\":-42,\"negzero\":0,\"small\":1.5e-7,\"thirds\":333333333.3333333,\"tiny\":1e-7,\"whole\":1},\"server\":\"payments\",\"tool\":\"transfer\"}",
93
+ "context_id": "sha256:f5b6ab689bbd1d567b0729312865fc20a4666dc39644a65fdc41636740793482"
94
+ },
95
+ {
96
+ "name": "strings-escapes",
97
+ "note": "Escapes: quote, backslash, \\b \\f \\n \\r \\t, other controls as \\u00XX; DEL, U+2028, U+2029, '/' and non-ASCII stay literal.",
98
+ "input": {
99
+ "server": "mail",
100
+ "tool": "send",
101
+ "arguments": {
102
+ "subject": "Re: \"Q3\" \\ final",
103
+ "body": "line1\nline2\ttab\u0007bell\u0001soh\b\f\rdel
ls
ps",
104
+ "url": "https://example.com/a/b",
105
+ "to": "zoë@example.com",
106
+ "greeting": "שלום 😀"
107
+ }
108
+ },
109
+ "preimage": "{\"arguments\":{\"body\":\"line1\\nline2\\ttab\\u0007bell\\u0001soh\\b\\f\\rdel
ls
ps\",\"greeting\":\"שלום 😀\",\"subject\":\"Re: \\\"Q3\\\" \\\\ final\",\"to\":\"zoë@example.com\",\"url\":\"https://example.com/a/b\"},\"server\":\"mail\",\"tool\":\"send\"}",
110
+ "context_id": "sha256:fe91dc79441555be100f7e181af5b940e071aeebb8d89eb6bac9127f71f812b9"
111
+ },
112
+ {
113
+ "name": "key-order-utf16",
114
+ "note": "Keys sort by UTF-16 code units, not code points: U+20AC < U+1F600 (D83D) < U+FB01 < U+FB33.",
115
+ "input": {
116
+ "server": "kv",
117
+ "tool": "put",
118
+ "arguments": {
119
+ "דּ": 4,
120
+ "fi": 3,
121
+ "😀": 2,
122
+ "€": 1,
123
+ "ö": 0,
124
+ "€": 0,
125
+ "b": 0,
126
+ "B": 0,
127
+ "_": 0,
128
+ "1": 0,
129
+ "\r": 0,
130
+ "": 0
131
+ }
132
+ },
133
+ "preimage": "{\"arguments\":{\"\":0,\"\\r\":0,\"1\":0,\"B\":0,\"_\":0,\"b\":0,\"€\":0,\"ö\":0,\"€\":1,\"😀\":2,\"fi\":3,\"דּ\":4},\"server\":\"kv\",\"tool\":\"put\"}",
134
+ "context_id": "sha256:199c7c5bcee590b3c55b44e33c1fed2fcfac4b7d4e54e8f76b4ddb3d8d1ef355"
135
+ },
136
+ {
137
+ "name": "unsafe-integer",
138
+ "note": "An integer outside ±(2^53−1) has no canonical form: JSON.parse may already have rounded it. The call is refused with expected_error; no session is opened.",
139
+ "input": {
140
+ "server": "ledger",
141
+ "tool": "post_entry",
142
+ "arguments": {
143
+ "entry_id": 9007199254740993
144
+ }
145
+ },
146
+ "expected_error": "INVALID_ARGUMENT: tool call post_entry arguments contain an integer outside ±(2^53−1), which has no canonical JSON form"
147
+ },
148
+ {
149
+ "name": "unsafe-integer-exponent",
150
+ "note": "The same holds for an integral value written with an exponent (1e21), here nested in an array.",
151
+ "input": {
152
+ "server": "ledger",
153
+ "tool": "post_entry",
154
+ "arguments": {
155
+ "ids": [
156
+ 1,
157
+ 1e+21
158
+ ]
159
+ }
160
+ },
161
+ "expected_error": "INVALID_ARGUMENT: tool call post_entry arguments contain an integer outside ±(2^53−1), which has no canonical JSON form"
162
+ }
163
+ ]
164
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "server": "filesystem",
4
+ "rules": [
5
+ {
6
+ "tool": "write_file",
7
+ "mode": "macp.mode.quorum.v1",
8
+ "participants": ["alice", "bob", "carol"],
9
+ "threshold": 2,
10
+ "ttl_ms": 120000,
11
+ "policy_version": "policy.packs.two-agent-rule",
12
+ "description": "Four eyes on every write: two of three reviewers approve."
13
+ },
14
+ {
15
+ "tool": "move_*",
16
+ "mode": "macp.mode.decision.v1",
17
+ "participants": ["alice", "bob"],
18
+ "ttl_ms": 60000,
19
+ "description": "Moves and renames need a majority vote (2 of 2)."
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://multiagentcoordinationprotocol.io/schemas/macp-mcp/gate.schema.json",
4
+ "title": "macp-mcp gate policy (gate.json)",
5
+ "description": "Which tool calls on one upstream MCP server need a MACP session, and how that session is run. Rules are tried in order; the first whose tool pattern matches decides. A call that matches no rule is forwarded to the upstream untouched. Loaders also enforce two rules a schema cannot: threshold <= the rule's participant count, and participants never contains the gate's own MACP_AGENT_ID.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["version", "server", "rules"],
9
+ "properties": {
10
+ "version": {
11
+ "description": "gate.json format version.",
12
+ "const": 1
13
+ },
14
+ "server": {
15
+ "description": "The upstream's configured name. It is the \"server\" member of the context_id preimage, so changing it changes every context_id.",
16
+ "type": "string",
17
+ "minLength": 1
18
+ },
19
+ "rules": {
20
+ "type": "array",
21
+ "items": { "$ref": "#/$defs/rule" }
22
+ }
23
+ },
24
+ "$defs": {
25
+ "rule": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["tool", "mode", "participants"],
29
+ "properties": {
30
+ "tool": {
31
+ "description": "Tool name: an exact name, or a glob where * matches any run of characters (including none) and ? matches exactly one. No other wildcard syntax; matching is case-sensitive and covers the whole name.",
32
+ "type": "string",
33
+ "minLength": 1
34
+ },
35
+ "mode": {
36
+ "description": "The MACP mode of the approval session.",
37
+ "enum": ["macp.mode.quorum.v1", "macp.mode.decision.v1"]
38
+ },
39
+ "participants": {
40
+ "description": "MACP agent ids of the approvers. The gate itself is the session initiator and never one of them.",
41
+ "type": "array",
42
+ "minItems": 1,
43
+ "uniqueItems": true,
44
+ "items": { "type": "string", "minLength": 1 }
45
+ },
46
+ "threshold": {
47
+ "description": "APPROVE ballots (Quorum) or votes (Decision) needed to forward the call. Defaults to a strict majority of participants: floor(n/2) + 1. Must not exceed the participant count. With a bound policy that overrides the threshold (for example policy.packs.two-agent-rule), set the same value here.",
48
+ "type": "integer",
49
+ "minimum": 1
50
+ },
51
+ "ttl_ms": {
52
+ "description": "Session TTL. A session still open after ttl_ms plus a 5000 ms grace is cancelled by the gate and the call refused.",
53
+ "type": "integer",
54
+ "minimum": 1,
55
+ "default": 300000
56
+ },
57
+ "policy_version": {
58
+ "description": "An RFC-MACP-0012 policy registered with the runtime, bound to the session at SessionStart and evaluated at commitment. The gate must keep commitment authority (initiator_only or any_participant); a designated_role policy such as policy.packs.human-signoff cannot be used. Omitted means the runtime default policy.",
59
+ "type": "string",
60
+ "minLength": 1
61
+ },
62
+ "description": {
63
+ "description": "Free text for humans; ignored by the gate.",
64
+ "type": "string",
65
+ "minLength": 1
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "contract_version": "1",
3
+ "description": "What a gated tool call puts on the MACP wire, and what the MCP caller gets back. Strings with {placeholders} are templates: substitute literally, no other formatting. {server} is gate.json's server, {tool} the tool name, {context_id} and {preimage} as in context-id-vectors.json, {gate} the gate's MACP_AGENT_ID, {threshold} the rule's effective threshold, {session_id} the approval session's id, {state} its SessionState name without the SESSION_STATE_ prefix (EXPIRED, CANCELLED), {runtime_message} the runtime's Ack failure message.",
4
+ "session_start": {
5
+ "intent": "Approve MCP tool call {server}/{tool}",
6
+ "context_id": "{context_id}",
7
+ "ttl_ms": "rule.ttl_ms",
8
+ "policy_version": "rule.policy_version, or the runtime default when omitted",
9
+ "initiator": "{gate}"
10
+ },
11
+ "modes": {
12
+ "macp.mode.quorum.v1": {
13
+ "participants": "rule.participants",
14
+ "first_message": {
15
+ "message_type": "ApprovalRequest",
16
+ "request_id": "{context_id}",
17
+ "action": "tools/call:{tool}",
18
+ "summary": "{server}/{tool}",
19
+ "details": "UTF-8 bytes of {preimage}",
20
+ "required_approvals": "{threshold}"
21
+ },
22
+ "ballots": "The first Approve / Reject / Abstain from each approver for request_id {context_id} counts (the runtime rejects a second one); Approve is an approval."
23
+ },
24
+ "macp.mode.decision.v1": {
25
+ "participants": "[{gate}] followed by rule.participants; the gate never votes and is not counted",
26
+ "first_message": {
27
+ "message_type": "Proposal",
28
+ "proposal_id": "call",
29
+ "option": "tools/call:{tool}",
30
+ "rationale": "{preimage}"
31
+ },
32
+ "ballots": "The first Vote from each approver on proposal_id call counts (the runtime rejects a second one); APPROVE (case-insensitive) is an approval, REJECT and ABSTAIN are not."
33
+ }
34
+ },
35
+ "outcome": {
36
+ "approve_when": "approvals >= {threshold}",
37
+ "deny_when": "approvals + approvers_yet_to_vote < {threshold}",
38
+ "counted": "Only ballots from rule.participants."
39
+ },
40
+ "commitment": {
41
+ "approve": {
42
+ "action": "macp.gate.approve",
43
+ "outcome_positive": true
44
+ },
45
+ "deny": {
46
+ "action": "macp.gate.deny",
47
+ "outcome_positive": false
48
+ },
49
+ "authority_scope": "tools/call:{server}/{tool}",
50
+ "reason": "Free text; not part of the contract.",
51
+ "reported": "The Commitment envelope the runtime records for the session (its payload bytes are what commitment_hash covers)."
52
+ },
53
+ "forwarded_result": {
54
+ "meta_key": "io.multiagentcoordinationprotocol/commitment",
55
+ "meta_value": {
56
+ "session_id": "the approval session id",
57
+ "mode": "rule.mode",
58
+ "outcome": "approve",
59
+ "commitment_hash": "RFC-MACP-0013 hash of the recorded Commitment payload"
60
+ },
61
+ "note": "Added to the upstream's CallToolResult _meta; every key the upstream set is preserved. outcome is \"approve\" or \"deny\"; only forwarded calls carry the key, so it is always \"approve\" there."
62
+ },
63
+ "refusals": {
64
+ "note": "The upstream is never called. The result is an MCP tool error (isError: true) whose only content is one text block with exactly the rendered template.",
65
+ "templates": {
66
+ "denied": "POLICY_DENIED: tool call {tool} denied by session {session_id}",
67
+ "policy_denied": "POLICY_DENIED: {runtime_message}",
68
+ "not_open": "SESSION_NOT_OPEN: session {session_id} ended {state} without a commitment",
69
+ "unsafe_integer": "INVALID_ARGUMENT: tool call {tool} arguments contain an integer outside ±(2^53−1), which has no canonical JSON form"
70
+ },
71
+ "cases": {
72
+ "denied": "The session committed outcome_positive = false (the threshold became unreachable).",
73
+ "policy_denied": "The runtime rejected the gate's positive Commitment with POLICY_DENIED (a bound RFC-0012 policy); the gate cancels the session.",
74
+ "not_open": "The session ended EXPIRED or CANCELLED with no Commitment. A session still OPEN at ttl_ms + expiry_grace_ms is cancelled by the gate first, so {state} is CANCELLED there.",
75
+ "unsafe_integer": "The arguments hold a number that is integral with |v| > 2^53−1 (Python: an int, or a float with is_integer(); TypeScript: Number.isInteger(v) && !Number.isSafeInteger(v)), at any depth. Checked before any session is opened.",
76
+ "other": "Any other failure (runtime unreachable, a rejected SessionStart, ...) is \"<CODE>: <message>\" with the base server's code precedence: Ack failure code, then the macp-error-code trailer, then the gRPC status name. Not a template; codes and messages vary."
77
+ }
78
+ },
79
+ "timing": {
80
+ "expiry_grace_ms": 5000
81
+ }
82
+ }
package/dist/bridge.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * `get_commitment` reflect every participant's messages, not just this
10
10
  * agent's. Mirrors the Python macp-mcp `bridge.py` call for call.
11
11
  */
12
- import { MacpClient, type Envelope } from 'macp-sdk-typescript';
12
+ import { MacpClient, type CommitmentPayload, type Envelope } from 'macp-sdk-typescript';
13
13
  import { type Settings } from './config';
14
14
  import type { CommitAckResult, CommitmentResult, ProposalAckResult, SessionResult, StartSessionResult, VerifyResult, VoteAckResult } from './results';
15
15
  /** RFC-MACP-0013 hash label, the `label` part of the hash preimage. */
@@ -26,6 +26,8 @@ export type ClientFactory = (settings: Settings) => BridgeClient;
26
26
  export declare function defaultClientFactory(settings: Settings): BridgeClient;
27
27
  /** SessionState enum name without the `SESSION_STATE_` prefix (OPEN, RESOLVED, ...). */
28
28
  export declare function stateName(state: string | undefined): string;
29
+ /** A decoded CommitmentPayload with proto3 defaults filled in (protobufjs omits them). Commitment is a core message, so any mode decodes it. */
30
+ export declare function decodeCommitment(client: BridgeClient, wire: Buffer): CommitmentPayload;
29
31
  /** Decision-mode actions for one MACP agent identity. */
30
32
  export declare class MacpBridge {
31
33
  readonly settings: Settings;
package/dist/bridge.js CHANGED
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.MacpBridge = exports.ToolArgumentError = exports.REPLAY_MAX_MS = exports.COMMITMENT_HASH_LABEL = void 0;
15
15
  exports.defaultClientFactory = defaultClientFactory;
16
16
  exports.stateName = stateName;
17
+ exports.decodeCommitment = decodeCommitment;
17
18
  const macp_sdk_typescript_1 = require("macp-sdk-typescript");
18
19
  const config_1 = require("./config");
19
20
  const version_1 = require("./version");
@@ -56,7 +57,7 @@ function ackDict(ack) {
56
57
  function int64(value) {
57
58
  return value === undefined || value === '' ? 0 : Number(value);
58
59
  }
59
- /** A decoded CommitmentPayload with proto3 defaults filled in (protobufjs omits them). */
60
+ /** A decoded CommitmentPayload with proto3 defaults filled in (protobufjs omits them). Commitment is a core message, so any mode decodes it. */
60
61
  function decodeCommitment(client, wire) {
61
62
  const raw = client.protoRegistry.decodeKnownPayload(macp_sdk_typescript_1.MODE_DECISION, 'Commitment', wire) ?? {};
62
63
  const str = (key) => (typeof raw[key] === 'string' ? raw[key] : '');
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  #!/usr/bin/env node
2
- /** Entry point: `macp-mcp` (stdio transport). stdout carries only the MCP protocol; everything else goes to stderr. */
2
+ /**
3
+ * Entry point (stdio transport): `macp-mcp` serves the Decision tools;
4
+ * `macp-mcp gate ...` proxies an upstream MCP server behind gate rules.
5
+ * stdout carries only the MCP protocol; everything else goes to stderr.
6
+ */
3
7
  export {};
package/dist/cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- /** Entry point: `macp-mcp` (stdio transport). stdout carries only the MCP protocol; everything else goes to stderr. */
3
+ /**
4
+ * Entry point (stdio transport): `macp-mcp` serves the Decision tools;
5
+ * `macp-mcp gate ...` proxies an upstream MCP server behind gate rules.
6
+ * stdout carries only the MCP protocol; everything else goes to stderr.
7
+ */
4
8
  Object.defineProperty(exports, "__esModule", { value: true });
5
9
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
10
  const macp_sdk_typescript_1 = require("macp-sdk-typescript");
7
11
  const config_1 = require("./config");
12
+ const cli_1 = require("./gate/cli");
8
13
  const server_1 = require("./server");
9
14
  async function main() {
10
15
  // The SDK's default sink sends debug/info to console.log, i.e. stdout; route every level to stderr.
@@ -12,8 +17,15 @@ async function main() {
12
17
  sink: (level, args) => process.stderr.write(`[macp-sdk] ${level} ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}\n`),
13
18
  });
14
19
  let settings;
20
+ const [command, ...rest] = process.argv.slice(2);
15
21
  try {
16
22
  settings = (0, config_1.loadSettings)();
23
+ if (command === 'gate') {
24
+ process.stderr.write(`macp-mcp gate: agent=${settings.agentId} runtime=${settings.target} ` +
25
+ `auth=${(0, config_1.authMode)(settings)} tls=${settings.allowInsecure ? 'off' : 'on'}\n`);
26
+ await (0, cli_1.runGate)(rest, settings);
27
+ return;
28
+ }
17
29
  }
18
30
  catch (err) {
19
31
  if (err instanceof config_1.ConfigError) {