@atbash/atbash-mcp 0.0.12 → 0.0.13
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 +60 -9
- package/dist/index.js +128 -23
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `@atbash/mcp`
|
|
1
|
+
# `@atbash/atbash-mcp`
|
|
2
2
|
|
|
3
3
|
Expose Atbash as a standalone MCP server.
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ This package starts an MCP server process that loads one Atbash agent identity a
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @atbash/mcp
|
|
10
|
+
npm install @atbash/atbash-mcp@0.0.12-dev.3
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## When To Use It
|
|
@@ -31,7 +31,7 @@ Add to your `claude_desktop_config.json`:
|
|
|
31
31
|
"mcpServers": {
|
|
32
32
|
"atbash": {
|
|
33
33
|
"command": "npx",
|
|
34
|
-
"args": ["-y", "@atbash/mcp"],
|
|
34
|
+
"args": ["-y", "@atbash/atbash-mcp@0.0.12-dev.3"],
|
|
35
35
|
"env": {
|
|
36
36
|
"ATBASH_AGENT_PRIVKEY": "your_agent_private_key_here",
|
|
37
37
|
"ATBASH_ORG_NAME": "your_org_name"
|
|
@@ -48,7 +48,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
48
48
|
|
|
49
49
|
const transport = new StdioClientTransport({
|
|
50
50
|
command: "npx",
|
|
51
|
-
args: ["-y", "@atbash/mcp"],
|
|
51
|
+
args: ["-y", "@atbash/atbash-mcp@0.0.12-dev.3"],
|
|
52
52
|
env: {
|
|
53
53
|
...process.env,
|
|
54
54
|
ATBASH_AGENT_PRIVKEY: process.env.ATBASH_AGENT_PRIVKEY,
|
|
@@ -64,6 +64,7 @@ const transport = new StdioClientTransport({
|
|
|
64
64
|
| `ATBASH_AGENT_PRIVKEY` | Yes | Your Atbash agent private key |
|
|
65
65
|
| `ATBASH_ORG_NAME` | Recommended | Your organization name. The SDK resolves the correct chain from it; without it, calls fall back to the default public chain. |
|
|
66
66
|
| `ATBASH_ENDPOINT` | No | Override the default Atbash endpoint (`https://atbash.ai`) |
|
|
67
|
+
| `ATBASH_FAIL_CLOSED` | — | Has no effect. The MCP server is unconditionally fail-closed — judge errors always block, regardless of this variable. |
|
|
67
68
|
|
|
68
69
|
## Tools
|
|
69
70
|
|
|
@@ -80,11 +81,18 @@ Submit an action for safety judgment before executing it.
|
|
|
80
81
|
| `tool_name` | string | No | Name of the tool being called |
|
|
81
82
|
| `tool_args_json` | string | No | JSON string of tool arguments |
|
|
82
83
|
|
|
83
|
-
Returns: `{ verdict, allow, reason,
|
|
84
|
+
Returns: `{ verdict, allow, reason, toolCallId }`
|
|
84
85
|
|
|
85
86
|
#### `atbash_log`
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
Submit a tool call through Atbash's redacting safety-judgment path. The judge
|
|
89
|
+
logs/broadcasts the call and returns the same fail-closed decision contract as
|
|
90
|
+
`atbash_judge`: `{ verdict, allow, reason, toolCallId }`.
|
|
91
|
+
|
|
92
|
+
Despite its legacy name, this is not a distinct non-judging log operation. The
|
|
93
|
+
pinned SDK has no server-broadcasting audit-only API; its `logToolCall()` method
|
|
94
|
+
only creates a local signed transaction, so this MCP tool deliberately does not
|
|
95
|
+
use or expose that local-only result as proof of persistence.
|
|
88
96
|
|
|
89
97
|
| Input | Type | Required |
|
|
90
98
|
|---|---|---|
|
|
@@ -130,10 +138,53 @@ Poll the status of a previously submitted judgment.
|
|
|
130
138
|
|
|
131
139
|
| Verdict | Meaning | What To Do |
|
|
132
140
|
|---|---|---|
|
|
133
|
-
| `ALLOW` | Safe to proceed | Execute the real tool |
|
|
134
|
-
| `HOLD` |
|
|
141
|
+
| `ALLOW` | Safe to proceed | Execute the real tool only when `allow` is also exactly `true` |
|
|
142
|
+
| `HOLD` | Pending operator review | Do not proceed; keep `tool_call_id` to poll status later |
|
|
135
143
|
| `BLOCK` | Policy violation | Stop; show `reason` to operator |
|
|
136
|
-
| `ERROR` | Judge unreachable | Fail closed |
|
|
144
|
+
| `ERROR` | Judge unreachable | Fail closed — do not proceed |
|
|
145
|
+
|
|
146
|
+
Unknown or missing verdicts, and `ALLOW` responses carrying `allow: false`, are
|
|
147
|
+
also returned with `allow: false`.
|
|
148
|
+
|
|
149
|
+
### Enforcement is advisory in MCP
|
|
150
|
+
|
|
151
|
+
An MCP server exposes tools; it cannot intercept the *other* tools your client
|
|
152
|
+
calls. So this package can tell you an action is unsafe, but it cannot stop the
|
|
153
|
+
client from running it — the model has to honour the verdict. If a verdict is
|
|
154
|
+
unreachable, both `atbash_judge` and `atbash_log` return
|
|
155
|
+
`{"verdict": "ERROR", "allow": false}` with an explicit instruction not to
|
|
156
|
+
proceed, rather than a bare transport error that is easy to mistake for a
|
|
157
|
+
broken tool.
|
|
158
|
+
|
|
159
|
+
For enforcement that a compromised or prompt-injected agent cannot talk its way
|
|
160
|
+
past, use a plugin that wraps the tool-call boundary itself — for example
|
|
161
|
+
`@atbash/atbash-langchain`, `@atbash/atbash-langgraph`, the OpenClaw plugin, or
|
|
162
|
+
the Codex/ChatGPT hook, all of which refuse the call before the tool executes.
|
|
163
|
+
|
|
164
|
+
An `ALLOW` also has to have a verdict behind it. Prior SDK versions (e.g. 0.5.3-dev.0) treated a judge response body with no `verdict` field as an audit-tier pass — this plugin guards against that at any SDK version — synthesizing `{"allow": true, "verdict": "ALLOW"}` from it — including for a body
|
|
165
|
+
that goes on to say `"action_type": "block"`. The shipped configuration points
|
|
166
|
+
at an allowlisted endpoint, for which the SDK sets no `verifyPubKey` and so
|
|
167
|
+
skips response-signature verification entirely, which leaves schema drift, a
|
|
168
|
+
truncated response and a rewriting proxy all arriving as permission. This
|
|
169
|
+
package therefore re-checks every allow against the `JudgeResult` the SDK parsed
|
|
170
|
+
that body into, and refuses it with `{"verdict": "ERROR", "allow": false}`
|
|
171
|
+
unless the judge sent a verdict and an `action_type` of exactly `allow`.
|
|
172
|
+
|
|
173
|
+
Be precise about what that inspects, because it is one layer above the HTTP
|
|
174
|
+
body: `action_type` is the response body's value verbatim, whereas the verdict
|
|
175
|
+
has already been case-folded and aliased by the SDK. So `"verdict": "green"`
|
|
176
|
+
is honoured as an `ALLOW` — `GREEN` is the judge's own alias, not drift — while
|
|
177
|
+
an absent verdict is refused. An organization on a logging-only tier therefore
|
|
178
|
+
sees denials rather than passes; that is deliberate, since absence of a verdict
|
|
179
|
+
is not a verdict.
|
|
180
|
+
|
|
181
|
+
The judge endpoint is resolved through `Atbash.fromConfig`, which validates
|
|
182
|
+
`ATBASH_ENDPOINT` against the SDK's trusted allowlist / self-hosted policy. The
|
|
183
|
+
plain `new Atbash(privkey, { endpoint })` constructor performs no such check,
|
|
184
|
+
so an injected endpoint would be trusted outright. Startup also requires the
|
|
185
|
+
endpoint-validation and fail-closed audit capabilities of the pinned
|
|
186
|
+
`@atbash/sdk`; the process refuses to start if they are absent, rather than
|
|
187
|
+
running with an inert safety gate.
|
|
137
188
|
|
|
138
189
|
## Sending Better Inputs
|
|
139
190
|
|
package/dist/index.js
CHANGED
|
@@ -16,10 +16,10 @@ function registerSafetyPrompt(server2) {
|
|
|
16
16
|
"atbash_safety_check",
|
|
17
17
|
{
|
|
18
18
|
description: "Prepare a consistent pre-execution Atbash safety review request.",
|
|
19
|
-
|
|
19
|
+
argsSchema: {
|
|
20
20
|
action: z.string().describe("Action to evaluate"),
|
|
21
21
|
context: z.string().describe("Why the action is being taken")
|
|
22
|
-
}
|
|
22
|
+
}
|
|
23
23
|
},
|
|
24
24
|
async ({ action, context }) => ({
|
|
25
25
|
messages: [
|
|
@@ -46,6 +46,86 @@ function registerPolicyResource(server2, client2) {
|
|
|
46
46
|
void client2;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// src/startup.ts
|
|
50
|
+
function assertMcpSdkCapabilities(sdk) {
|
|
51
|
+
const missing = [
|
|
52
|
+
typeof sdk.fromConfig === "function" ? null : "fromConfig",
|
|
53
|
+
typeof sdk.prototype?.auditToolCall === "function" ? null : "auditToolCall"
|
|
54
|
+
].filter((name) => name !== null);
|
|
55
|
+
if (missing.length > 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Incompatible @atbash/sdk: missing required MCP capabilities: ${missing.join(", ")}. Refusing to start without an active safety gate.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var DECISION_VERDICTS = [
|
|
62
|
+
"ALLOW",
|
|
63
|
+
"HOLD",
|
|
64
|
+
"BLOCK",
|
|
65
|
+
"ERROR"
|
|
66
|
+
];
|
|
67
|
+
function isDecisionShaped(value) {
|
|
68
|
+
const decision = value;
|
|
69
|
+
return typeof decision === "object" && decision !== null && typeof decision.allow === "boolean" && DECISION_VERDICTS.includes(decision.verdict);
|
|
70
|
+
}
|
|
71
|
+
function hardenJudgeClient(client2) {
|
|
72
|
+
const sdkAudit = client2.auditToolCall;
|
|
73
|
+
const sdkJudge = client2.judgeAction;
|
|
74
|
+
async function guardedAuditToolCall(input) {
|
|
75
|
+
let wire;
|
|
76
|
+
const shadow = Object.create(client2);
|
|
77
|
+
Object.defineProperty(shadow, "judgeAction", {
|
|
78
|
+
value: async (...args) => {
|
|
79
|
+
const result = await sdkJudge.apply(client2, args);
|
|
80
|
+
wire = result;
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const decision = await sdkAudit.call(shadow, input);
|
|
85
|
+
if (!isDecisionShaped(decision)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"judge decision is not verdict-shaped and cannot be trusted"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (decision.verdict !== "ALLOW" || decision.allow !== true) {
|
|
91
|
+
return { ...decision, allow: false };
|
|
92
|
+
}
|
|
93
|
+
if (wire === void 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"judge returned an allow without a judge response to back it"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (wire.verdict !== "ALLOW" || wire.actionType !== "allow") {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`judge response body does not carry an allow (verdict ${JSON.stringify(wire.verdict)}, action_type ${JSON.stringify(wire.actionType)})`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return decision;
|
|
104
|
+
}
|
|
105
|
+
Object.defineProperty(client2, "auditToolCall", {
|
|
106
|
+
value: guardedAuditToolCall,
|
|
107
|
+
writable: true,
|
|
108
|
+
configurable: true
|
|
109
|
+
});
|
|
110
|
+
return client2;
|
|
111
|
+
}
|
|
112
|
+
function createAtbashClient(env, sdk) {
|
|
113
|
+
assertMcpSdkCapabilities(sdk);
|
|
114
|
+
const agentKey = env.ATBASH_AGENT_PRIVKEY;
|
|
115
|
+
if (!agentKey) {
|
|
116
|
+
throw new Error("ATBASH_AGENT_PRIVKEY env var is required");
|
|
117
|
+
}
|
|
118
|
+
const endpoint = env.ATBASH_ENDPOINT?.trim();
|
|
119
|
+
const orgName = env.ATBASH_ORG_NAME?.trim();
|
|
120
|
+
return hardenJudgeClient(
|
|
121
|
+
sdk.fromConfig({
|
|
122
|
+
agentKey,
|
|
123
|
+
...endpoint ? { judge: { endpoint } } : {},
|
|
124
|
+
...orgName ? { orgName } : {}
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
49
129
|
// src/tools/judge.ts
|
|
50
130
|
import { z as z2 } from "zod";
|
|
51
131
|
|
|
@@ -66,6 +146,28 @@ function toErrorContent(error) {
|
|
|
66
146
|
}
|
|
67
147
|
|
|
68
148
|
// src/tools/judge.ts
|
|
149
|
+
function toJudgeFailureContent(error, toolName = "atbash_judge") {
|
|
150
|
+
return {
|
|
151
|
+
content: [
|
|
152
|
+
{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
verdict: "ERROR",
|
|
157
|
+
allow: false,
|
|
158
|
+
reason: `Atbash judge unavailable \u2014 treat this as BLOCK and do NOT proceed with the action (fail-closed). Retry ${toolName} once the service is reachable. Cause: ${safeErrorMessage(error)}`
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function denyUnlessExplicitlyAllowed(decision) {
|
|
169
|
+
return decision.verdict === "ALLOW" && decision.allow === true ? decision : { ...decision, allow: false };
|
|
170
|
+
}
|
|
69
171
|
function registerJudgeTools(server2, client2) {
|
|
70
172
|
server2.registerTool(
|
|
71
173
|
"atbash_judge",
|
|
@@ -88,11 +190,17 @@ function registerJudgeTools(server2, client2) {
|
|
|
88
190
|
parsedArgs = tool_args_json;
|
|
89
191
|
}
|
|
90
192
|
}
|
|
91
|
-
const
|
|
193
|
+
const rawDecision = await client2.auditToolCall({
|
|
92
194
|
toolName: tool_name ?? "mcp_judge",
|
|
93
195
|
args: parsedArgs ?? { action },
|
|
94
196
|
context: `${action} \u2014 ${context}`
|
|
95
197
|
});
|
|
198
|
+
const decision = denyUnlessExplicitlyAllowed(rawDecision);
|
|
199
|
+
if (decision.verdict === "ERROR") {
|
|
200
|
+
return toJudgeFailureContent(
|
|
201
|
+
new Error(decision.reason ?? "judge returned an error verdict")
|
|
202
|
+
);
|
|
203
|
+
}
|
|
96
204
|
if (decision.verdict === "HOLD") {
|
|
97
205
|
const holdParts = [
|
|
98
206
|
"Action held for operator review. The agent will not be jailed \u2014 please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",
|
|
@@ -103,14 +211,14 @@ function registerJudgeTools(server2, client2) {
|
|
|
103
211
|
}
|
|
104
212
|
return toJsonContent(decision);
|
|
105
213
|
} catch (error) {
|
|
106
|
-
return
|
|
214
|
+
return toJudgeFailureContent(error);
|
|
107
215
|
}
|
|
108
216
|
}
|
|
109
217
|
);
|
|
110
218
|
server2.registerTool(
|
|
111
219
|
"atbash_log",
|
|
112
220
|
{
|
|
113
|
-
description: "
|
|
221
|
+
description: "Submit a tool call for Atbash safety judgment and logging. Returns ALLOW, HOLD, BLOCK, or ERROR.",
|
|
114
222
|
inputSchema: z2.object({
|
|
115
223
|
action: z2.string().describe("Action description"),
|
|
116
224
|
context: z2.string().describe("Action context"),
|
|
@@ -128,22 +236,29 @@ function registerJudgeTools(server2, client2) {
|
|
|
128
236
|
parsedArgs = tool_args_json;
|
|
129
237
|
}
|
|
130
238
|
}
|
|
131
|
-
const
|
|
239
|
+
const rawDecision = await client2.auditToolCall({
|
|
132
240
|
toolName: tool_name ?? "mcp_log",
|
|
133
241
|
args: parsedArgs ?? { action },
|
|
134
242
|
context: `${action} \u2014 ${context}`
|
|
135
243
|
});
|
|
136
|
-
|
|
244
|
+
const decision = denyUnlessExplicitlyAllowed(rawDecision);
|
|
245
|
+
if (decision.verdict === "ERROR") {
|
|
246
|
+
return toJudgeFailureContent(
|
|
247
|
+
new Error(decision.reason ?? "judge returned an error verdict"),
|
|
248
|
+
"atbash_log"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (decision.verdict === "HOLD") {
|
|
137
252
|
const holdParts = [
|
|
138
253
|
"Action held for operator review. The agent will not be jailed \u2014 please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",
|
|
139
|
-
`Reason: ${
|
|
254
|
+
`Reason: ${decision.reason ?? ""}`
|
|
140
255
|
];
|
|
141
|
-
if (
|
|
142
|
-
return toJsonContent({ ...
|
|
256
|
+
if (decision.toolCallId) holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
|
|
257
|
+
return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
|
|
143
258
|
}
|
|
144
|
-
return toJsonContent(
|
|
259
|
+
return toJsonContent(decision);
|
|
145
260
|
} catch (error) {
|
|
146
|
-
return
|
|
261
|
+
return toJudgeFailureContent(error, "atbash_log");
|
|
147
262
|
}
|
|
148
263
|
}
|
|
149
264
|
);
|
|
@@ -379,21 +494,11 @@ function registerStatusTools(server2, client2) {
|
|
|
379
494
|
}
|
|
380
495
|
|
|
381
496
|
// src/index.ts
|
|
382
|
-
var
|
|
383
|
-
if (!privkey) {
|
|
384
|
-
console.error("ATBASH_AGENT_PRIVKEY env var is required");
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
var endpoint = process.env.ATBASH_ENDPOINT;
|
|
388
|
-
var orgName = process.env.ATBASH_ORG_NAME;
|
|
497
|
+
var client = createAtbashClient(process.env, Atbash);
|
|
389
498
|
setupTelemetry({ enabled: true, source: "plugin:mcp" });
|
|
390
499
|
process.once("beforeExit", () => shutdownTelemetry());
|
|
391
500
|
process.once("SIGINT", () => shutdownTelemetry().finally(() => process.exit(0)));
|
|
392
501
|
process.once("SIGTERM", () => shutdownTelemetry().finally(() => process.exit(0)));
|
|
393
|
-
var clientOpts = {};
|
|
394
|
-
if (endpoint) clientOpts.endpoint = endpoint;
|
|
395
|
-
if (orgName) clientOpts.orgName = orgName;
|
|
396
|
-
var client = new Atbash(privkey, clientOpts);
|
|
397
502
|
var server = new McpServer({
|
|
398
503
|
name: "atbash-safety",
|
|
399
504
|
version: "1.0.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atbash/atbash-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Atbash safety judge exposed as a standalone MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,13 +39,16 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "node scripts/clean.mjs && tsup src/index.ts --format esm --dts --clean",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"test:compile": "tsc -p tsconfig.json --outDir dist --declaration false --sourceMap false",
|
|
44
|
+
"test": "vitest run && npm run test:compile && node --test tests/*.test.mjs",
|
|
42
45
|
"example": "npm run build && npm install --prefix examples/mcp-runtime-agent && node examples/mcp-runtime-agent/client.mjs",
|
|
43
46
|
"verify:package": "node scripts/release.mjs --verify-current",
|
|
44
47
|
"release": "node scripts/release.mjs --channel latest",
|
|
45
48
|
"release:dev": "node scripts/release.mjs --channel dev"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@atbash/sdk": "0.
|
|
51
|
+
"@atbash/sdk": "0.8.0",
|
|
49
52
|
"@modelcontextprotocol/sdk": "^1.12.3",
|
|
50
53
|
"zod": "^3.25.76"
|
|
51
54
|
},
|
|
@@ -53,6 +56,7 @@
|
|
|
53
56
|
"@types/node": "^25.7.0",
|
|
54
57
|
"semver": "7.8.5",
|
|
55
58
|
"tsup": "^8.0.0",
|
|
56
|
-
"typescript": "^5.0.0"
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"vitest": "^4.1.6"
|
|
57
61
|
}
|
|
58
62
|
}
|