ratchet-mcp 0.1.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/LICENSE +17 -0
- package/README.md +71 -0
- package/bin/ratchet-mcp.mjs +104 -0
- package/package.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
Full text: https://www.apache.org/licenses/LICENSE-2.0.txt
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ratchet-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Ratchet](https://ratchetgate.com) — an effect gate for AI agents.
|
|
4
|
+
|
|
5
|
+
Agents retry. LLM control flow is non-deterministic, network calls fail ambiguously, and processes
|
|
6
|
+
die mid-action, so the same logical action gets attempted zero, one, or several times and nothing
|
|
7
|
+
knows which. Ratchet is a gate you ask **before** acting, and it answers durably.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Get a key at <https://ratchetgate.com/console> (free, no card), then:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"ratchet": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "ratchet-mcp"],
|
|
19
|
+
"env": { "RATCHET_API_KEY": "rk_live_..." }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Put the key in `env`, never in `args` — arguments are visible in process listings.
|
|
26
|
+
|
|
27
|
+
Works with Claude Desktop, Claude Code, Cursor, and any MCP client that spawns a stdio server.
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Tool | What it does |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `ratchet_begin_effect` | Ask permission before a side effect. Returns `execute`, `duplicate`, `in_flight`, `blocked`, `approval_required`, or `denied` |
|
|
34
|
+
| `ratchet_report_effect` | Report the outcome after acting |
|
|
35
|
+
| `ratchet_check_effect` | Ask "did I already do this?" without reserving anything |
|
|
36
|
+
| `ratchet_resolve_effect` | Settle an effect whose outcome was unknown, after verifying |
|
|
37
|
+
| `ratchet_list_effects` | Review recent effects; filter by `indeterminate` to find unresolved work |
|
|
38
|
+
| `ratchet_get_policy` | Read the retry and budget policy for an effect type |
|
|
39
|
+
| `ratchet_usage` | Plan, allowance, credit balance, and today's spend |
|
|
40
|
+
|
|
41
|
+
Only `execute` authorises the model to act. Every other decision returns a `next_step` beginning
|
|
42
|
+
with `STOP`.
|
|
43
|
+
|
|
44
|
+
## The part that matters
|
|
45
|
+
|
|
46
|
+
If your agent dies between "go" and "done", Ratchet does **not** quietly let the next caller retry.
|
|
47
|
+
The lease expires, the effect becomes `indeterminate` — a known unknown — and your configured
|
|
48
|
+
policy for that effect type decides what happens: block (the default), retry, or verify first.
|
|
49
|
+
|
|
50
|
+
Exactly-once delivery is not achievable and is not claimed. What is guaranteed is at-most-once
|
|
51
|
+
initiation, a recorded outcome that later callers replay, and an explicit state for the case most
|
|
52
|
+
systems bury.
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Variable | Default | Purpose |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `RATCHET_API_KEY` | *(required)* | Your scoped API key |
|
|
59
|
+
| `RATCHET_BASE_URL` | `https://ratchetgate.com` | Point at your own instance |
|
|
60
|
+
| `RATCHET_TIMEOUT_MS` | `15000` | Request timeout |
|
|
61
|
+
|
|
62
|
+
This package holds no database connection and no server secret — only your key, which is scoped
|
|
63
|
+
and revocable. It is a thin bridge to the HTTP API, with zero dependencies.
|
|
64
|
+
|
|
65
|
+
If the gate is unreachable it returns a JSON-RPC error rather than crashing, so your agent can
|
|
66
|
+
apply its fail-open or fail-closed policy. Decide which **before** integrating:
|
|
67
|
+
<https://ratchetgate.com/docs>
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Apache-2.0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ratchet MCP server (stdio transport).
|
|
4
|
+
*
|
|
5
|
+
* A thin bridge: it reads line-delimited JSON-RPC on stdin, forwards each
|
|
6
|
+
* message to a Ratchet instance's /mcp endpoint over HTTPS, and writes the
|
|
7
|
+
* reply to stdout.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately holds no database connection and no server secret. The only
|
|
10
|
+
* credential is the caller's own API key, which is scoped and revocable. An
|
|
11
|
+
* MCP server that required its users to hold the service's database
|
|
12
|
+
* credentials would be a much larger thing to trust than the service itself.
|
|
13
|
+
*
|
|
14
|
+
* Zero dependencies — Node built-ins only.
|
|
15
|
+
*
|
|
16
|
+
* RATCHET_API_KEY=rk_live_... npx ratchet-mcp
|
|
17
|
+
*/
|
|
18
|
+
import { createInterface } from 'node:readline';
|
|
19
|
+
|
|
20
|
+
const BASE = (process.env.RATCHET_BASE_URL ?? 'https://ratchetgate.com').replace(/\/+$/, '');
|
|
21
|
+
const KEY = process.env.RATCHET_API_KEY;
|
|
22
|
+
const TIMEOUT_MS = Number.parseInt(process.env.RATCHET_TIMEOUT_MS ?? '15000', 10);
|
|
23
|
+
|
|
24
|
+
// stdout carries the protocol and nothing else; a stray log line there
|
|
25
|
+
// corrupts the stream and the client fails in confusing ways.
|
|
26
|
+
const log = (m) => process.stderr.write(`[ratchet-mcp] ${m}\n`);
|
|
27
|
+
const send = (o) => process.stdout.write(`${JSON.stringify(o)}\n`);
|
|
28
|
+
|
|
29
|
+
if (!KEY) {
|
|
30
|
+
log('RATCHET_API_KEY is not set.');
|
|
31
|
+
log('Add it to the "env" block of your MCP client config — not "args", which');
|
|
32
|
+
log('is visible in process listings. Get a key at ' + BASE + '/console');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rpcError = (id, code, message, data) => ({
|
|
37
|
+
jsonrpc: '2.0', id: id ?? null,
|
|
38
|
+
error: { code, message, ...(data ? { data } : {}) },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function forward(msg) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(`${BASE}/mcp`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${KEY}`,
|
|
49
|
+
'content-type': 'application/json',
|
|
50
|
+
accept: 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(msg),
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// A notification is answered with 202 and no body, by design.
|
|
57
|
+
if (res.status === 202) return null;
|
|
58
|
+
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
if (!text) return null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
return rpcError(msg.id, -32603, `Ratchet returned a non-JSON response (HTTP ${res.status})`);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Surface transport failures as JSON-RPC errors rather than dying: the
|
|
69
|
+
// client can then tell its model the gate is unreachable, which is a
|
|
70
|
+
// decision it needs to make rather than a crash it cannot see.
|
|
71
|
+
const aborted = err?.name === 'AbortError';
|
|
72
|
+
return rpcError(msg.id, -32001,
|
|
73
|
+
aborted ? `Ratchet did not respond within ${TIMEOUT_MS}ms` : `Cannot reach Ratchet at ${BASE}`,
|
|
74
|
+
{ hint: 'If your agent cannot reach the gate, apply your configured fail-open or '
|
|
75
|
+
+ 'fail-closed policy. See ' + BASE + '/docs' });
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
log(`bridging stdio → ${BASE}/mcp`);
|
|
82
|
+
|
|
83
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
84
|
+
|
|
85
|
+
for await (const line of rl) {
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (!trimmed) continue;
|
|
88
|
+
|
|
89
|
+
let msg;
|
|
90
|
+
try {
|
|
91
|
+
msg = JSON.parse(trimmed);
|
|
92
|
+
} catch {
|
|
93
|
+
send(rpcError(null, -32700, 'Parse error'));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const reply = await forward(msg);
|
|
99
|
+
if (reply) send(reply);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log(`unexpected: ${err?.message ?? err}`);
|
|
102
|
+
send(rpcError(msg?.id, -32603, 'Internal error in the stdio bridge'));
|
|
103
|
+
}
|
|
104
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ratchet-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Ratchet — the effect gate for AI agents. Ask before you act, so the same side effect happens at most once.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "ratchet-mcp": "bin/ratchet-mcp.mjs" },
|
|
7
|
+
"files": ["bin", "README.md", "LICENSE"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp", "model-context-protocol", "ai-agents", "idempotency",
|
|
12
|
+
"at-most-once", "side-effects", "agent-infrastructure", "reliability"
|
|
13
|
+
],
|
|
14
|
+
"repository": { "type": "git", "url": "git+https://github.com/thearchitect0x-glitch/ratchet.git", "directory": "packages/ratchet-mcp" },
|
|
15
|
+
"homepage": "https://ratchetgate.com",
|
|
16
|
+
"publishConfig": { "access": "public" }
|
|
17
|
+
}
|