@atbash/atbash-mcp 0.0.12 → 0.0.13-dev.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.
Files changed (3) hide show
  1. package/README.md +60 -9
  2. package/dist/index.js +128 -23
  3. 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, tool_call_id }`
84
+ Returns: `{ verdict, allow, reason, toolCallId }`
84
85
 
85
86
  #### `atbash_log`
86
87
 
87
- Log a tool call on-chain without requesting a verdict.
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` | Logged for async review | Execution is allowed; the action is flagged for operator review in the background — keep `tool_call_id` to poll status later |
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
- inputSchema: z.object({
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 decision = await client2.auditToolCall({
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 toErrorContent(error);
214
+ return toJudgeFailureContent(error);
107
215
  }
108
216
  }
109
217
  );
110
218
  server2.registerTool(
111
219
  "atbash_log",
112
220
  {
113
- description: "Log a tool call on-chain without requesting a verdict.",
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 result = await client2.auditToolCall({
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
- if (result.verdict === "HOLD") {
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: ${result.reason ?? ""}`
254
+ `Reason: ${decision.reason ?? ""}`
140
255
  ];
141
- if (result.toolCallId) holdParts.push(`Tool Call ID: ${result.toolCallId}`);
142
- return toJsonContent({ ...result, allow: false, reason: holdParts.join("\n") });
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(result);
259
+ return toJsonContent(decision);
145
260
  } catch (error) {
146
- return toErrorContent(error);
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 privkey = process.env.ATBASH_AGENT_PRIVKEY;
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.12",
3
+ "version": "0.0.13-dev.0",
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.7.1",
51
+ "@atbash/sdk": "0.15.0-dev.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
  }