commitlayer-mcp-recorder 0.1.0__tar.gz
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.
- commitlayer_mcp_recorder-0.1.0/LICENSE +21 -0
- commitlayer_mcp_recorder-0.1.0/PKG-INFO +212 -0
- commitlayer_mcp_recorder-0.1.0/README.md +186 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder/__init__.py +468 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder/__main__.py +201 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/PKG-INFO +212 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/SOURCES.txt +12 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/dependency_links.txt +1 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/entry_points.txt +2 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/requires.txt +3 -0
- commitlayer_mcp_recorder-0.1.0/commitlayer_mcp_recorder.egg-info/top_level.txt +1 -0
- commitlayer_mcp_recorder-0.1.0/pyproject.toml +40 -0
- commitlayer_mcp_recorder-0.1.0/setup.cfg +4 -0
- commitlayer_mcp_recorder-0.1.0/tests/test_recorder.py +613 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asterfold Labs, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commitlayer-mcp-recorder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CommitLayer MCP Recorder: a stdio proxy that records an agent's MCP tool calls as CommitLayer actions
|
|
5
|
+
Author-email: "Asterfold Labs, Inc." <hello@commitlayer.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://commitlayer.ai
|
|
8
|
+
Project-URL: Documentation, https://commitlayer.ai/docs
|
|
9
|
+
Project-URL: Changelog, https://commitlayer.ai/changelog
|
|
10
|
+
Project-URL: Source, https://github.com/kobikwelu/stateprf-svcs/tree/main/recorder/python
|
|
11
|
+
Keywords: ai-agents,verification,audit,mcp,observability
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: yaml
|
|
24
|
+
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# CommitLayer MCP Recorder (Python)
|
|
28
|
+
|
|
29
|
+
A small stdio proxy that sits between an MCP client (Claude Desktop, Claude Code, Cursor, an agent framework)
|
|
30
|
+
and any downstream MCP server (a Stripe or Zendesk MCP server, your own). It forwards every JSON-RPC message
|
|
31
|
+
unchanged in both directions, and when the client calls a mapped tool it records the intent as a CommitLayer
|
|
32
|
+
action **before** forwarding the call. Standard library only; Python 3.10+.
|
|
33
|
+
|
|
34
|
+
> The agent may declare its intent. It may read the verdict. It must never control the independent facts, the governing contract, or the final evidence.
|
|
35
|
+
|
|
36
|
+
The recorder only does the first of those three. It never blocks, reorders or alters the agent's traffic, and
|
|
37
|
+
the downstream tool's result is never sent to CommitLayer: a tool answering "refund issued" is a claim, not
|
|
38
|
+
evidence. Verification reads the systems of record.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ./recorder/python # from this repository; `pip install pyyaml` for .yaml mappings
|
|
42
|
+
export COMMITLAYER_API_KEY=spk_... # a workspace API key; without it the proxy is a passthrough
|
|
43
|
+
commitlayer-mcp-recorder --map mapping.json -- npx -y @stripe/mcp --tools=all
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Wiring it into a client
|
|
47
|
+
|
|
48
|
+
Wherever the client is told how to start the server, put the recorder in front and move the real command
|
|
49
|
+
after `--`.
|
|
50
|
+
|
|
51
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"stripe": {
|
|
57
|
+
"command": "commitlayer-mcp-recorder",
|
|
58
|
+
"args": ["--map", "/etc/commitlayer/stripe.json", "--agent-id", "support-bot", "--", "npx", "-y", "@stripe/mcp", "--tools=all"],
|
|
59
|
+
"env": {"COMMITLAYER_API_KEY": "spk_..."}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Claude Code:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude mcp add stripe -e COMMITLAYER_API_KEY=spk_... -- commitlayer-mcp-recorder --map stripe.json -- npx -y @stripe/mcp --tools=all
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Cursor and other clients use the same `command` / `args` / `env` shape. The downstream server does not know
|
|
72
|
+
the recorder is there; it sees the same bytes it would have seen.
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
commitlayer-mcp-recorder --map mapping.json [--agent-id ID] [--workflow-id ID] [--api-url URL]
|
|
78
|
+
[--timeout 5] [--no-fail-open] [--warn-unmapped] -- <downstream command and args>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Flag | Meaning |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `--map` | Mapping file, JSON or YAML (YAML needs PyYAML). Default `$COMMITLAYER_RECORDER_MAP`. |
|
|
84
|
+
| `--agent-id`, `--workflow-id` | Override `agent_id` / `workflow_id` from the mapping file. |
|
|
85
|
+
| `--api-url` | Default `$COMMITLAYER_API_URL`, then `https://api.commitlayer.ai`. |
|
|
86
|
+
| `--timeout` | Seconds to wait for the API per call (default 5). |
|
|
87
|
+
| `--no-fail-open` | When recording fails, refuse the call instead of forwarding it (see below). |
|
|
88
|
+
| `--warn-unmapped` | Log each tool that is called but not mapped, once per tool name. |
|
|
89
|
+
|
|
90
|
+
Environment: `COMMITLAYER_API_KEY` (required to record; without it the recorder warns once at startup and
|
|
91
|
+
runs as a pure passthrough), `COMMITLAYER_API_URL`, `COMMITLAYER_RECORDER_MAP`. The key is never logged. All
|
|
92
|
+
diagnostics go to stderr, one line each, prefixed `commitlayer-recorder:`. The downstream server's own stderr
|
|
93
|
+
is relayed as is.
|
|
94
|
+
|
|
95
|
+
The proxy exits when the client closes stdin or the downstream server exits, with the server's exit code.
|
|
96
|
+
|
|
97
|
+
## Mapping file
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"agent_id": "support-bot",
|
|
102
|
+
"workflow_id": "refunds",
|
|
103
|
+
"tools": {
|
|
104
|
+
"refund_payment": {
|
|
105
|
+
"contract_id": "refund_workflow",
|
|
106
|
+
"action_type": "refund_order",
|
|
107
|
+
"subject": "arguments.order_id",
|
|
108
|
+
"subject_kind": "order",
|
|
109
|
+
"params": {"amount": "arguments.amount", "currency": "arguments.currency"},
|
|
110
|
+
"action_id": "refund:{arguments.order_id}",
|
|
111
|
+
"compensates": "arguments.original_action_id"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- Keys under `tools` are MCP tool names. An exact match wins; a key with a server prefix
|
|
118
|
+
(`stripe.refund_payment`) also matches the bare name (`refund_payment`).
|
|
119
|
+
- `contract_id` and `subject` are required per tool. Unknown keys are allowed and ignored.
|
|
120
|
+
- `subject`, each `params` value and `compensates` are dotted paths into the `tools/call` params object:
|
|
121
|
+
`arguments.order_id`, `name`, `_meta.progressToken`, `arguments.items.0`.
|
|
122
|
+
- `action_id` is a template with `{path}` placeholders. Without it the id is `mcp:{tool}:{request id}`, so a
|
|
123
|
+
retried request updates one record instead of creating two.
|
|
124
|
+
- `contract_version` (integer), `action_type` and `subject_kind` are passed through.
|
|
125
|
+
- A `params` path that is absent from the call is skipped; an absent `compensates` path is omitted. An absent
|
|
126
|
+
`subject` (or `action_id` placeholder) means the call is forwarded **but not recorded**, with a warning.
|
|
127
|
+
|
|
128
|
+
The mapping is validated on load; an invalid file stops the proxy with exit code 2 and a message naming the
|
|
129
|
+
tool and key.
|
|
130
|
+
|
|
131
|
+
### Example: the Coinbase MCP server
|
|
132
|
+
|
|
133
|
+
[`examples/coinbase.json`](examples/coinbase.json) maps `orders_create` (the order tool of the Coinbase MCP
|
|
134
|
+
server behind Coinbase for Agents) to the `coinbase_agent_trade` library contract: the subject is the
|
|
135
|
+
product id the agent passes, `amount` is its quote size, `side` and `client_order_id` ride along, and the
|
|
136
|
+
action id is derived from the client order id so a retried call updates one record. The paths follow the
|
|
137
|
+
create-order request; run the recorder once with `--warn-unmapped` against the server's own `tools/list`
|
|
138
|
+
and adjust them if the argument names differ. The Coinbase server is remote (HTTP), and the recorder is a
|
|
139
|
+
stdio proxy: put it in front of whatever stdio bridge the client uses to reach the remote server, so the
|
|
140
|
+
bridge is the "downstream command". Verification then reads the order and its fills from Coinbase through
|
|
141
|
+
the Coinbase connector; the tool's own answer is never evidence.
|
|
142
|
+
|
|
143
|
+
## What is recorded
|
|
144
|
+
|
|
145
|
+
For each client → server `tools/call` whose `params.name` is mapped, the recorder posts one action to
|
|
146
|
+
`POST {api_url}/v1/actions` (`x-api-key`, `content-type: application/json`) and then forwards the original
|
|
147
|
+
line unchanged:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"action_id": "refund:order_10231",
|
|
152
|
+
"contract_id": "refund_workflow",
|
|
153
|
+
"subject_id": "order_10231",
|
|
154
|
+
"subject_kind": "order",
|
|
155
|
+
"action_type": "refund_order",
|
|
156
|
+
"agent_id": "support-bot",
|
|
157
|
+
"workflow_id": "refunds",
|
|
158
|
+
"requested_at": "2026-10-15T12:00:00Z",
|
|
159
|
+
"params": {"amount": 180.0, "currency": "USD", "mcp_tool": "refund_payment", "mcp_request_id": "7"},
|
|
160
|
+
"channel": "mcp"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`params` always carries `mcp_tool` and `mcp_request_id`. `requested_at` is taken when the call is seen, so
|
|
165
|
+
it precedes every effect. Unmapped tools, notifications, `initialize`, `tools/list` and everything else pass
|
|
166
|
+
straight through.
|
|
167
|
+
|
|
168
|
+
When the downstream answer for a recorded request comes back, the recorder logs one line:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
commitlayer-recorder: recorded refund:order_10231 → tool result isError=false
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
That is all it does with the result. **The tool's own "success" is never evidence.** It is not sent to the
|
|
175
|
+
API and it does not change the verdict; CommitLayer settles the action from the facts the systems of record
|
|
176
|
+
report, under the governing contract. Read the verdict with the SDK or `GET /v1/actions/{action_id}`.
|
|
177
|
+
|
|
178
|
+
## Fail-open, and the alternative
|
|
179
|
+
|
|
180
|
+
By default recording is fail-open: if the API answers 4xx/5xx, times out or cannot be reached, a warning is
|
|
181
|
+
written to stderr and the call is forwarded anyway. A verifier that is down must not stop the business
|
|
182
|
+
action; the gap shows up as an action that was never recorded rather than one that never happened.
|
|
183
|
+
|
|
184
|
+
With `--no-fail-open` a call whose recording fails is **not** forwarded. The client receives a JSON-RPC
|
|
185
|
+
error for that request id (`code -32000`, message `CommitLayer recording failed: …`) and the downstream
|
|
186
|
+
server never sees it. Other traffic is unaffected.
|
|
187
|
+
|
|
188
|
+
## Library use
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from commitlayer_mcp_recorder import Recorder, build_action, load_mapping
|
|
192
|
+
|
|
193
|
+
mapping = load_mapping("mapping.json")
|
|
194
|
+
recorder = Recorder(mapping, api_key="spk_...", agent_id="support-bot")
|
|
195
|
+
decision = recorder.before_forward(message) # a parsed client message; .forward, .action_id, .error
|
|
196
|
+
recorder.after_response(message) # a parsed server message; logs the isError flag
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`build_action(tool_name, call_params, tool_mapping, request_id=...)` returns the body shown above.
|
|
200
|
+
|
|
201
|
+
## Limits
|
|
202
|
+
|
|
203
|
+
- Stdio transport only, newline-delimited JSON-RPC. Lines that are not JSON are forwarded untouched.
|
|
204
|
+
- JSON-RPC batch arrays are forwarded untouched and not inspected.
|
|
205
|
+
- A `tools/call` without a JSON-RPC id is forwarded without recording (there is nothing to match the answer
|
|
206
|
+
to).
|
|
207
|
+
|
|
208
|
+
## Tests
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
python -m pytest recorder/python/tests -q
|
|
212
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# CommitLayer MCP Recorder (Python)
|
|
2
|
+
|
|
3
|
+
A small stdio proxy that sits between an MCP client (Claude Desktop, Claude Code, Cursor, an agent framework)
|
|
4
|
+
and any downstream MCP server (a Stripe or Zendesk MCP server, your own). It forwards every JSON-RPC message
|
|
5
|
+
unchanged in both directions, and when the client calls a mapped tool it records the intent as a CommitLayer
|
|
6
|
+
action **before** forwarding the call. Standard library only; Python 3.10+.
|
|
7
|
+
|
|
8
|
+
> The agent may declare its intent. It may read the verdict. It must never control the independent facts, the governing contract, or the final evidence.
|
|
9
|
+
|
|
10
|
+
The recorder only does the first of those three. It never blocks, reorders or alters the agent's traffic, and
|
|
11
|
+
the downstream tool's result is never sent to CommitLayer: a tool answering "refund issued" is a claim, not
|
|
12
|
+
evidence. Verification reads the systems of record.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install ./recorder/python # from this repository; `pip install pyyaml` for .yaml mappings
|
|
16
|
+
export COMMITLAYER_API_KEY=spk_... # a workspace API key; without it the proxy is a passthrough
|
|
17
|
+
commitlayer-mcp-recorder --map mapping.json -- npx -y @stripe/mcp --tools=all
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Wiring it into a client
|
|
21
|
+
|
|
22
|
+
Wherever the client is told how to start the server, put the recorder in front and move the real command
|
|
23
|
+
after `--`.
|
|
24
|
+
|
|
25
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"stripe": {
|
|
31
|
+
"command": "commitlayer-mcp-recorder",
|
|
32
|
+
"args": ["--map", "/etc/commitlayer/stripe.json", "--agent-id", "support-bot", "--", "npx", "-y", "@stripe/mcp", "--tools=all"],
|
|
33
|
+
"env": {"COMMITLAYER_API_KEY": "spk_..."}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Claude Code:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
claude mcp add stripe -e COMMITLAYER_API_KEY=spk_... -- commitlayer-mcp-recorder --map stripe.json -- npx -y @stripe/mcp --tools=all
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Cursor and other clients use the same `command` / `args` / `env` shape. The downstream server does not know
|
|
46
|
+
the recorder is there; it sees the same bytes it would have seen.
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
commitlayer-mcp-recorder --map mapping.json [--agent-id ID] [--workflow-id ID] [--api-url URL]
|
|
52
|
+
[--timeout 5] [--no-fail-open] [--warn-unmapped] -- <downstream command and args>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Flag | Meaning |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `--map` | Mapping file, JSON or YAML (YAML needs PyYAML). Default `$COMMITLAYER_RECORDER_MAP`. |
|
|
58
|
+
| `--agent-id`, `--workflow-id` | Override `agent_id` / `workflow_id` from the mapping file. |
|
|
59
|
+
| `--api-url` | Default `$COMMITLAYER_API_URL`, then `https://api.commitlayer.ai`. |
|
|
60
|
+
| `--timeout` | Seconds to wait for the API per call (default 5). |
|
|
61
|
+
| `--no-fail-open` | When recording fails, refuse the call instead of forwarding it (see below). |
|
|
62
|
+
| `--warn-unmapped` | Log each tool that is called but not mapped, once per tool name. |
|
|
63
|
+
|
|
64
|
+
Environment: `COMMITLAYER_API_KEY` (required to record; without it the recorder warns once at startup and
|
|
65
|
+
runs as a pure passthrough), `COMMITLAYER_API_URL`, `COMMITLAYER_RECORDER_MAP`. The key is never logged. All
|
|
66
|
+
diagnostics go to stderr, one line each, prefixed `commitlayer-recorder:`. The downstream server's own stderr
|
|
67
|
+
is relayed as is.
|
|
68
|
+
|
|
69
|
+
The proxy exits when the client closes stdin or the downstream server exits, with the server's exit code.
|
|
70
|
+
|
|
71
|
+
## Mapping file
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"agent_id": "support-bot",
|
|
76
|
+
"workflow_id": "refunds",
|
|
77
|
+
"tools": {
|
|
78
|
+
"refund_payment": {
|
|
79
|
+
"contract_id": "refund_workflow",
|
|
80
|
+
"action_type": "refund_order",
|
|
81
|
+
"subject": "arguments.order_id",
|
|
82
|
+
"subject_kind": "order",
|
|
83
|
+
"params": {"amount": "arguments.amount", "currency": "arguments.currency"},
|
|
84
|
+
"action_id": "refund:{arguments.order_id}",
|
|
85
|
+
"compensates": "arguments.original_action_id"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- Keys under `tools` are MCP tool names. An exact match wins; a key with a server prefix
|
|
92
|
+
(`stripe.refund_payment`) also matches the bare name (`refund_payment`).
|
|
93
|
+
- `contract_id` and `subject` are required per tool. Unknown keys are allowed and ignored.
|
|
94
|
+
- `subject`, each `params` value and `compensates` are dotted paths into the `tools/call` params object:
|
|
95
|
+
`arguments.order_id`, `name`, `_meta.progressToken`, `arguments.items.0`.
|
|
96
|
+
- `action_id` is a template with `{path}` placeholders. Without it the id is `mcp:{tool}:{request id}`, so a
|
|
97
|
+
retried request updates one record instead of creating two.
|
|
98
|
+
- `contract_version` (integer), `action_type` and `subject_kind` are passed through.
|
|
99
|
+
- A `params` path that is absent from the call is skipped; an absent `compensates` path is omitted. An absent
|
|
100
|
+
`subject` (or `action_id` placeholder) means the call is forwarded **but not recorded**, with a warning.
|
|
101
|
+
|
|
102
|
+
The mapping is validated on load; an invalid file stops the proxy with exit code 2 and a message naming the
|
|
103
|
+
tool and key.
|
|
104
|
+
|
|
105
|
+
### Example: the Coinbase MCP server
|
|
106
|
+
|
|
107
|
+
[`examples/coinbase.json`](examples/coinbase.json) maps `orders_create` (the order tool of the Coinbase MCP
|
|
108
|
+
server behind Coinbase for Agents) to the `coinbase_agent_trade` library contract: the subject is the
|
|
109
|
+
product id the agent passes, `amount` is its quote size, `side` and `client_order_id` ride along, and the
|
|
110
|
+
action id is derived from the client order id so a retried call updates one record. The paths follow the
|
|
111
|
+
create-order request; run the recorder once with `--warn-unmapped` against the server's own `tools/list`
|
|
112
|
+
and adjust them if the argument names differ. The Coinbase server is remote (HTTP), and the recorder is a
|
|
113
|
+
stdio proxy: put it in front of whatever stdio bridge the client uses to reach the remote server, so the
|
|
114
|
+
bridge is the "downstream command". Verification then reads the order and its fills from Coinbase through
|
|
115
|
+
the Coinbase connector; the tool's own answer is never evidence.
|
|
116
|
+
|
|
117
|
+
## What is recorded
|
|
118
|
+
|
|
119
|
+
For each client → server `tools/call` whose `params.name` is mapped, the recorder posts one action to
|
|
120
|
+
`POST {api_url}/v1/actions` (`x-api-key`, `content-type: application/json`) and then forwards the original
|
|
121
|
+
line unchanged:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"action_id": "refund:order_10231",
|
|
126
|
+
"contract_id": "refund_workflow",
|
|
127
|
+
"subject_id": "order_10231",
|
|
128
|
+
"subject_kind": "order",
|
|
129
|
+
"action_type": "refund_order",
|
|
130
|
+
"agent_id": "support-bot",
|
|
131
|
+
"workflow_id": "refunds",
|
|
132
|
+
"requested_at": "2026-10-15T12:00:00Z",
|
|
133
|
+
"params": {"amount": 180.0, "currency": "USD", "mcp_tool": "refund_payment", "mcp_request_id": "7"},
|
|
134
|
+
"channel": "mcp"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`params` always carries `mcp_tool` and `mcp_request_id`. `requested_at` is taken when the call is seen, so
|
|
139
|
+
it precedes every effect. Unmapped tools, notifications, `initialize`, `tools/list` and everything else pass
|
|
140
|
+
straight through.
|
|
141
|
+
|
|
142
|
+
When the downstream answer for a recorded request comes back, the recorder logs one line:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
commitlayer-recorder: recorded refund:order_10231 → tool result isError=false
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
That is all it does with the result. **The tool's own "success" is never evidence.** It is not sent to the
|
|
149
|
+
API and it does not change the verdict; CommitLayer settles the action from the facts the systems of record
|
|
150
|
+
report, under the governing contract. Read the verdict with the SDK or `GET /v1/actions/{action_id}`.
|
|
151
|
+
|
|
152
|
+
## Fail-open, and the alternative
|
|
153
|
+
|
|
154
|
+
By default recording is fail-open: if the API answers 4xx/5xx, times out or cannot be reached, a warning is
|
|
155
|
+
written to stderr and the call is forwarded anyway. A verifier that is down must not stop the business
|
|
156
|
+
action; the gap shows up as an action that was never recorded rather than one that never happened.
|
|
157
|
+
|
|
158
|
+
With `--no-fail-open` a call whose recording fails is **not** forwarded. The client receives a JSON-RPC
|
|
159
|
+
error for that request id (`code -32000`, message `CommitLayer recording failed: …`) and the downstream
|
|
160
|
+
server never sees it. Other traffic is unaffected.
|
|
161
|
+
|
|
162
|
+
## Library use
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from commitlayer_mcp_recorder import Recorder, build_action, load_mapping
|
|
166
|
+
|
|
167
|
+
mapping = load_mapping("mapping.json")
|
|
168
|
+
recorder = Recorder(mapping, api_key="spk_...", agent_id="support-bot")
|
|
169
|
+
decision = recorder.before_forward(message) # a parsed client message; .forward, .action_id, .error
|
|
170
|
+
recorder.after_response(message) # a parsed server message; logs the isError flag
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`build_action(tool_name, call_params, tool_mapping, request_id=...)` returns the body shown above.
|
|
174
|
+
|
|
175
|
+
## Limits
|
|
176
|
+
|
|
177
|
+
- Stdio transport only, newline-delimited JSON-RPC. Lines that are not JSON are forwarded untouched.
|
|
178
|
+
- JSON-RPC batch arrays are forwarded untouched and not inspected.
|
|
179
|
+
- A `tools/call` without a JSON-RPC id is forwarded without recording (there is nothing to match the answer
|
|
180
|
+
to).
|
|
181
|
+
|
|
182
|
+
## Tests
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python -m pytest recorder/python/tests -q
|
|
186
|
+
```
|