@atbash/atbash-mcp 0.0.12-dev.0 → 0.0.12-dev.2

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/dist/index.js CHANGED
@@ -1,512 +1,26 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/index.ts
4
2
  import { Atbash, setupTelemetry, shutdownTelemetry } from "@atbash/sdk";
5
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
-
8
- // src/prompts/safety.ts
9
- import { z } from "zod";
10
- function registerSafetyPrompt(server2) {
11
- const promptServer = server2;
12
- if (!promptServer.registerPrompt) {
13
- return;
14
- }
15
- promptServer.registerPrompt(
16
- "atbash_safety_check",
17
- {
18
- description: "Prepare a consistent pre-execution Atbash safety review request.",
19
- argsSchema: {
20
- action: z.string().describe("Action to evaluate"),
21
- context: z.string().describe("Why the action is being taken")
22
- }
23
- },
24
- async ({ action, context }) => ({
25
- messages: [
26
- {
27
- role: "user",
28
- content: {
29
- type: "text",
30
- text: [
31
- "Use the Atbash tools to evaluate the following action before execution.",
32
- `Action: ${action}`,
33
- `Context: ${context}`,
34
- "If the result is HOLD, explain that operator review is required before proceeding."
35
- ].join("\n")
36
- }
37
- }
38
- ]
39
- })
40
- );
41
- }
42
-
43
- // src/resources/policy.ts
44
- function registerPolicyResource(server2, client2) {
45
- void server2;
46
- void client2;
47
- }
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
-
129
- // src/tools/judge.ts
130
- import { z as z2 } from "zod";
131
-
132
- // src/utils.ts
133
- function safeErrorMessage(error) {
134
- return error instanceof Error ? error.message : String(error);
135
- }
136
- function toJsonContent(value) {
137
- return {
138
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
139
- };
140
- }
141
- function toErrorContent(error) {
142
- return {
143
- content: [{ type: "text", text: `Error: ${safeErrorMessage(error)}` }],
144
- isError: true
145
- };
146
- }
147
-
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
- }
171
- function registerJudgeTools(server2, client2) {
172
- server2.registerTool(
173
- "atbash_judge",
174
- {
175
- description: "Submit an action for safety judgment before executing it. Returns ALLOW, HOLD, BLOCK, or ERROR.",
176
- inputSchema: z2.object({
177
- action: z2.string().describe("Plain text description of the action to judge"),
178
- context: z2.string().describe("Why this action is being taken"),
179
- tool_name: z2.string().optional().describe("Name of the tool being called"),
180
- tool_args_json: z2.string().optional().describe("JSON string of tool arguments")
181
- })
182
- },
183
- async ({ action, context, tool_name, tool_args_json }) => {
184
- try {
185
- let parsedArgs = void 0;
186
- if (tool_args_json) {
187
- try {
188
- parsedArgs = JSON.parse(tool_args_json);
189
- } catch {
190
- parsedArgs = tool_args_json;
191
- }
192
- }
193
- const rawDecision = await client2.auditToolCall({
194
- toolName: tool_name ?? "mcp_judge",
195
- args: parsedArgs ?? { action },
196
- context: `${action} \u2014 ${context}`
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
- }
204
- if (decision.verdict === "HOLD") {
205
- const holdParts = [
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.",
207
- `Reason: ${decision.reason ?? ""}`
208
- ];
209
- if (decision.toolCallId) holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
210
- return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
211
- }
212
- return toJsonContent(decision);
213
- } catch (error) {
214
- return toJudgeFailureContent(error);
215
- }
216
- }
217
- );
218
- server2.registerTool(
219
- "atbash_log",
220
- {
221
- description: "Submit a tool call for Atbash safety judgment and logging. Returns ALLOW, HOLD, BLOCK, or ERROR.",
222
- inputSchema: z2.object({
223
- action: z2.string().describe("Action description"),
224
- context: z2.string().describe("Action context"),
225
- tool_name: z2.string().optional().describe("Tool name"),
226
- tool_args_json: z2.string().optional().describe("Tool arguments JSON")
227
- })
228
- },
229
- async ({ action, context, tool_name, tool_args_json }) => {
230
- try {
231
- let parsedArgs = void 0;
232
- if (tool_args_json) {
233
- try {
234
- parsedArgs = JSON.parse(tool_args_json);
235
- } catch {
236
- parsedArgs = tool_args_json;
237
- }
238
- }
239
- const rawDecision = await client2.auditToolCall({
240
- toolName: tool_name ?? "mcp_log",
241
- args: parsedArgs ?? { action },
242
- context: `${action} \u2014 ${context}`
243
- });
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") {
252
- const holdParts = [
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.",
254
- `Reason: ${decision.reason ?? ""}`
255
- ];
256
- if (decision.toolCallId) holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
257
- return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
258
- }
259
- return toJsonContent(decision);
260
- } catch (error) {
261
- return toJudgeFailureContent(error, "atbash_log");
262
- }
263
- }
264
- );
265
- server2.registerTool(
266
- "atbash_check_agent",
267
- {
268
- description: "Check whether an agent is registered on the Atbash platform.",
269
- inputSchema: z2.object({
270
- pubkey: z2.string().optional().describe("Agent public key, defaults to this server agent")
271
- })
272
- },
273
- async ({ pubkey }) => {
274
- try {
275
- const selectedPubkey = pubkey ?? client2.pubkey;
276
- const registered = await client2.checkAgentExists(selectedPubkey);
277
- return toJsonContent({ registered, pubkey: selectedPubkey });
278
- } catch (error) {
279
- return toErrorContent(error);
280
- }
281
- }
282
- );
283
- }
284
-
285
- // src/tools/queries.ts
286
- import { z as z3 } from "zod";
287
- function defaultMaxCount(value) {
288
- return value ?? 20;
289
- }
290
- function registerQueryTools(server2, client2) {
291
- server2.registerTool(
292
- "atbash_get_policy",
293
- {
294
- description: "Get an agent policy configuration and jail status.",
295
- inputSchema: z3.object({
296
- pubkey: z3.string().optional().describe("Agent public key, defaults to the server agent")
297
- })
298
- },
299
- async ({ pubkey }) => {
300
- try {
301
- return toJsonContent(await client2.getAgentPolicy(pubkey ?? client2.pubkey));
302
- } catch (error) {
303
- return toErrorContent(error);
304
- }
305
- }
306
- );
307
- server2.registerTool(
308
- "atbash_get_agent_detail",
309
- {
310
- description: "Get agent metadata for a public key.",
311
- inputSchema: z3.object({
312
- pubkey: z3.string().optional().describe("Agent public key, defaults to the server agent")
313
- })
314
- },
315
- async ({ pubkey }) => {
316
- try {
317
- return toJsonContent(await client2.getAgentDetail(pubkey ?? client2.pubkey));
318
- } catch (error) {
319
- return toErrorContent(error);
320
- }
321
- }
322
- );
323
- server2.registerTool(
324
- "atbash_get_tool_calls",
325
- {
326
- description: "List recent tool calls across all agents.",
327
- inputSchema: z3.object({
328
- max_count: z3.number().int().positive().optional().describe("Maximum number of records")
329
- })
330
- },
331
- async ({ max_count }) => {
332
- try {
333
- return toJsonContent(await client2.getToolCalls(defaultMaxCount(max_count)));
334
- } catch (error) {
335
- return toErrorContent(error);
336
- }
337
- }
338
- );
339
- server2.registerTool(
340
- "atbash_get_agent_tool_calls",
341
- {
342
- description: "List recent tool calls for one agent.",
343
- inputSchema: z3.object({
344
- pubkey: z3.string().optional().describe("Agent public key, defaults to the server agent"),
345
- max_count: z3.number().int().positive().optional().describe("Maximum number of records")
346
- })
347
- },
348
- async ({ pubkey, max_count }) => {
349
- try {
350
- return toJsonContent(
351
- await client2.getAgentToolCalls(pubkey ?? client2.pubkey, defaultMaxCount(max_count))
352
- );
353
- } catch (error) {
354
- return toErrorContent(error);
355
- }
356
- }
357
- );
358
- server2.registerTool(
359
- "atbash_get_org_tool_calls",
360
- {
361
- description: "List recent tool calls for an organization.",
362
- inputSchema: z3.object({
363
- org_name: z3.string().describe("Organization name"),
364
- max_count: z3.number().int().positive().optional().describe("Maximum number of records")
365
- })
366
- },
367
- async ({ org_name, max_count }) => {
368
- try {
369
- return toJsonContent(await client2.getOrgToolCalls(org_name, defaultMaxCount(max_count)));
370
- } catch (error) {
371
- return toErrorContent(error);
372
- }
373
- }
374
- );
375
- server2.registerTool(
376
- "atbash_get_tool_call_full",
377
- {
378
- description: "Get the full detail for a single tool call.",
379
- inputSchema: z3.object({
380
- tool_call_id: z3.string().describe("Tool call identifier")
381
- })
382
- },
383
- async ({ tool_call_id }) => {
384
- try {
385
- return toJsonContent(await client2.getToolCallFull(tool_call_id));
386
- } catch (error) {
387
- return toErrorContent(error);
388
- }
389
- }
390
- );
391
- server2.registerTool(
392
- "atbash_get_tool_call_count",
393
- {
394
- description: "Get the total number of tool calls on-chain.",
395
- inputSchema: z3.object({})
396
- },
397
- async () => {
398
- try {
399
- return toJsonContent({ count: await client2.getToolCallCount() });
400
- } catch (error) {
401
- return toErrorContent(error);
402
- }
403
- }
404
- );
405
- server2.registerTool(
406
- "atbash_get_tier_info",
407
- {
408
- description: "Get an organization's tier and enforcement information.",
409
- inputSchema: z3.object({
410
- org_name: z3.string().describe("Organization name")
411
- })
412
- },
413
- async ({ org_name }) => {
414
- try {
415
- return toJsonContent(await client2.getOrgTierInfo(org_name));
416
- } catch (error) {
417
- return toErrorContent(error);
418
- }
419
- }
420
- );
421
- server2.registerTool(
422
- "atbash_get_held_actions",
423
- {
424
- description: "List actions currently waiting for operator review.",
425
- inputSchema: z3.object({
426
- org_name: z3.string().describe("Organization name"),
427
- max_count: z3.number().int().positive().optional().describe("Maximum number of records")
428
- })
429
- },
430
- async ({ org_name, max_count }) => {
431
- try {
432
- return toJsonContent(
433
- await client2.getPendingHeldActions(org_name, defaultMaxCount(max_count))
434
- );
435
- } catch (error) {
436
- return toErrorContent(error);
437
- }
438
- }
439
- );
440
- server2.registerTool(
441
- "atbash_get_reviews",
442
- {
443
- description: "List completed operator reviews for held actions.",
444
- inputSchema: z3.object({
445
- org_name: z3.string().describe("Organization name"),
446
- max_count: z3.number().int().positive().optional().describe("Maximum number of records")
447
- })
448
- },
449
- async ({ org_name, max_count }) => {
450
- try {
451
- return toJsonContent(await client2.getHeldActionReviews(org_name, defaultMaxCount(max_count)));
452
- } catch (error) {
453
- return toErrorContent(error);
454
- }
455
- }
456
- );
457
- server2.registerTool(
458
- "atbash_get_safety_stats",
459
- {
460
- description: "Get high-level chain-wide safety statistics.",
461
- inputSchema: z3.object({})
462
- },
463
- async () => {
464
- try {
465
- return toJsonContent(await client2.getSafetyStats());
466
- } catch (error) {
467
- return toErrorContent(error);
468
- }
469
- }
470
- );
471
- }
472
-
473
- // src/tools/status.ts
474
- import { z as z4 } from "zod";
475
- function registerStatusTools(server2, client2) {
476
- server2.registerTool(
477
- "atbash_judgment_status",
478
- {
479
- description: "Poll the status of a previously submitted judgment.",
480
- inputSchema: z4.object({
481
- tool_call_id: z4.string().describe("The tool_call_id returned from atbash_judge"),
482
- agent_pubkey: z4.string().optional().describe("Agent public key, defaults to the server agent")
483
- })
484
- },
485
- async ({ tool_call_id, agent_pubkey }) => {
486
- try {
487
- const status = await client2.getJudgmentStatus(tool_call_id, agent_pubkey);
488
- return toJsonContent(status);
489
- } catch (error) {
490
- return toErrorContent(error);
491
- }
492
- }
493
- );
494
- }
495
-
496
- // src/index.ts
497
- var client = createAtbashClient(process.env, Atbash);
5
+ import { registerSafetyPrompt } from "./prompts/safety.js";
6
+ import { registerPolicyResource } from "./resources/policy.js";
7
+ import { createAtbashClient } from "./startup.js";
8
+ import { registerJudgeTools } from "./tools/judge.js";
9
+ import { registerQueryTools } from "./tools/queries.js";
10
+ import { registerStatusTools } from "./tools/status.js";
11
+ const client = createAtbashClient(process.env, Atbash);
498
12
  setupTelemetry({ enabled: true, source: "plugin:mcp" });
499
13
  process.once("beforeExit", () => shutdownTelemetry());
500
14
  process.once("SIGINT", () => shutdownTelemetry().finally(() => process.exit(0)));
501
15
  process.once("SIGTERM", () => shutdownTelemetry().finally(() => process.exit(0)));
502
- var server = new McpServer({
503
- name: "atbash-safety",
504
- version: "1.0.0"
16
+ const server = new McpServer({
17
+ name: "atbash-safety",
18
+ version: "1.0.0",
505
19
  });
506
20
  registerJudgeTools(server, client);
507
21
  registerStatusTools(server, client);
508
22
  registerQueryTools(server, client);
509
23
  registerPolicyResource(server, client);
510
24
  registerSafetyPrompt(server);
511
- var transport = new StdioServerTransport();
25
+ const transport = new StdioServerTransport();
512
26
  await server.connect(transport);
@@ -0,0 +1,103 @@
1
+ // CRITICAL-2 regression sentinel (see SECURITY-AUDIT-corebreak.md and
2
+ // SECURITY-AUDIT-atbash-ai-migration.md): this package registers
3
+ // atbash_judge/atbash_log as ordinary, optional MCP tools alongside whatever
4
+ // real tools exist elsewhere on the host. It does not wrap, proxy, or gate
5
+ // execution of any other tool — there is no executor in this package at
6
+ // all. A tool call that arrives already-formed (prompt injection, a
7
+ // compromised model, a host replaying a stored function call, or a model
8
+ // that just skips the advisory step) reaches whatever real MCP server
9
+ // executes it with zero interaction with Atbash.
10
+ //
11
+ // This test currently PASSES because that gap still exists. If a future
12
+ // version adds real tool-execution gating, the assertions below should
13
+ // start failing — that's the signal to re-audit CRITICAL-2, not to widen
14
+ // the allowlist to make the test pass again.
15
+ import { describe, it, expect, vi } from "vitest";
16
+ import { readFileSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { registerJudgeTools } from "./tools/judge.js";
19
+ import { registerQueryTools } from "./tools/queries.js";
20
+ import { registerStatusTools } from "./tools/status.js";
21
+ import { registerPolicyResource } from "./resources/policy.js";
22
+ const KNOWN_SAFE_TOOL_NAMES = [
23
+ "atbash_judge",
24
+ "atbash_log",
25
+ "atbash_check_agent",
26
+ "atbash_get_policy",
27
+ "atbash_get_agent_detail",
28
+ "atbash_get_tool_calls",
29
+ "atbash_get_agent_tool_calls",
30
+ "atbash_get_org_tool_calls",
31
+ "atbash_get_tool_call_full",
32
+ "atbash_get_tool_call_count",
33
+ "atbash_get_tier_info",
34
+ "atbash_get_held_actions",
35
+ "atbash_get_reviews",
36
+ "atbash_get_safety_stats",
37
+ "atbash_judgment_status",
38
+ ].sort();
39
+ // A server double that only implements registerTool — any other method call
40
+ // (setRequestHandler, a generic dispatch/proxy API, etc.) throws instead of
41
+ // silently succeeding, so a future executor-style registration can't hide
42
+ // behind an untested server method.
43
+ function makeStrictServer() {
44
+ const registered = [];
45
+ return new Proxy({ registerTool: (name) => registered.push(name) }, {
46
+ get(target, prop, receiver) {
47
+ if (prop === "registered")
48
+ return registered;
49
+ if (prop in target)
50
+ return Reflect.get(target, prop, receiver);
51
+ throw new Error(`unexpected MCP server method called: ${String(prop)}`);
52
+ },
53
+ });
54
+ }
55
+ const fakeClient = {
56
+ auditToolCall: vi.fn(),
57
+ checkAgentExists: vi.fn(),
58
+ pubkey: "x",
59
+ };
60
+ describe("CRITICAL-2 sentinel: atbash-mcp registers no execution gate", () => {
61
+ it("the registered tool set is exactly the known judge/read-only set — nothing else", () => {
62
+ const server = makeStrictServer();
63
+ registerJudgeTools(server, fakeClient);
64
+ registerQueryTools(server, fakeClient);
65
+ registerStatusTools(server, fakeClient);
66
+ registerPolicyResource(server, fakeClient);
67
+ expect([...server.registered].sort()).toEqual(KNOWN_SAFE_TOOL_NAMES);
68
+ });
69
+ it("registerPolicyResource is a documented no-op", () => {
70
+ const server = makeStrictServer();
71
+ // Would throw via the Proxy above if this function called any server
72
+ // method other than nothing at all.
73
+ expect(() => registerPolicyResource(server, fakeClient)).not.toThrow();
74
+ expect(server.registered).toEqual([]);
75
+ });
76
+ it("no source file in this package contains an execution/proxy primitive", () => {
77
+ const srcDir = fileURLToPath(new URL(".", import.meta.url));
78
+ const files = [
79
+ "index.ts",
80
+ "tools/judge.ts",
81
+ "tools/queries.ts",
82
+ "tools/status.ts",
83
+ "resources/policy.ts",
84
+ "prompts/safety.ts",
85
+ ];
86
+ const executionPatterns = [
87
+ /child_process/,
88
+ /\bexec\(/,
89
+ /\bspawn\(/,
90
+ /new Function\(/,
91
+ /\beval\(/,
92
+ /forwardRequest/,
93
+ /proxyTool/,
94
+ /dispatchTool/,
95
+ ];
96
+ for (const file of files) {
97
+ const source = readFileSync(srcDir + file, "utf8");
98
+ for (const pattern of executionPatterns) {
99
+ expect(source, `${file} matched ${pattern}`).not.toMatch(pattern);
100
+ }
101
+ }
102
+ });
103
+ });
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ export function registerSafetyPrompt(server) {
3
+ const promptServer = server;
4
+ if (!promptServer.registerPrompt) {
5
+ return;
6
+ }
7
+ promptServer.registerPrompt("atbash_safety_check", {
8
+ description: "Prepare a consistent pre-execution Atbash safety review request.",
9
+ argsSchema: {
10
+ action: z.string().describe("Action to evaluate"),
11
+ context: z.string().describe("Why the action is being taken"),
12
+ },
13
+ }, async ({ action, context }) => ({
14
+ messages: [
15
+ {
16
+ role: "user",
17
+ content: {
18
+ type: "text",
19
+ text: [
20
+ "Use the Atbash tools to evaluate the following action before execution.",
21
+ `Action: ${action}`,
22
+ `Context: ${context}`,
23
+ "If the result is HOLD, explain that operator review is required before proceeding.",
24
+ ].join("\n"),
25
+ },
26
+ },
27
+ ],
28
+ }));
29
+ }
@@ -0,0 +1,6 @@
1
+ export function registerPolicyResource(server, client) {
2
+ void server;
3
+ void client;
4
+ // MCP SDK resource registration shape is unstable across versions.
5
+ // Tool-based policy access remains available through `atbash_get_policy`.
6
+ }
@@ -0,0 +1,124 @@
1
+ export function assertMcpSdkCapabilities(sdk) {
2
+ const missing = [
3
+ typeof sdk.fromConfig === "function" ? null : "fromConfig",
4
+ typeof sdk.prototype?.auditToolCall === "function" ? null : "auditToolCall",
5
+ ].filter((name) => name !== null);
6
+ if (missing.length > 0) {
7
+ throw new Error(`Incompatible @atbash/sdk: missing required MCP capabilities: ${missing.join(", ")}. ` +
8
+ "Refusing to start without an active safety gate.");
9
+ }
10
+ }
11
+ const DECISION_VERDICTS = [
12
+ "ALLOW",
13
+ "HOLD",
14
+ "BLOCK",
15
+ "ERROR",
16
+ ];
17
+ function isDecisionShaped(value) {
18
+ const decision = value;
19
+ return (typeof decision === "object" &&
20
+ decision !== null &&
21
+ typeof decision.allow === "boolean" &&
22
+ DECISION_VERDICTS.includes(decision.verdict));
23
+ }
24
+ /**
25
+ * `auditToolCall` collapses the judge's response body into a Decision, and one
26
+ * of its branches is affirmative on absence: a 200 body carrying no `verdict`
27
+ * key becomes `{allow: true, verdict: "ALLOW"}` even when that same body says
28
+ * `action_type: "block"` (@atbash/sdk 0.10.5-dev.0, dist/index.mjs:3589+). The
29
+ * body's own reason is forwarded verbatim, so nothing downstream can tell that
30
+ * synthesized allow apart from a real one — and the shipped MCP configuration
31
+ * talks to an allowlisted endpoint, for which the SDK sets no `verifyPubKey`
32
+ * and therefore skips response-signature verification entirely. Schema drift, a
33
+ * truncated body and a rewriting proxy all land in that branch and read as
34
+ * permission.
35
+ *
36
+ * So bind the allow to the `JudgeResult` the SDK collapsed from: an allow is an
37
+ * allow only when the judge positively said so.
38
+ *
39
+ * Precisely what that inspects, since it is one layer above the HTTP body:
40
+ * `JudgeResult` is built directly from the parsed response
41
+ * (@atbash/sdk 0.10.5-dev.0, dist/index.mjs:3455+), so `actionType` is
42
+ * `String(body.action_type ?? "")` — verbatim, and the check below demands
43
+ * exactly lowercase `"allow"`. `verdict`, however, has been through
44
+ * `normalizeVerdict` (dist/index.mjs:3289+), which upper-cases and aliases:
45
+ * `GREEN`→`ALLOW`, `YELLOW`→`HOLD`, `RED`→`BLOCK`, any other string→`HOLD`,
46
+ * and — the case this guard exists for — `null`/absent→`"No verdict"`. So a
47
+ * judge body of `{"verdict": "green", "action_type": "allow"}` is honoured as
48
+ * an allow, deliberately: `GREEN` is the judge's own alias, not drift. What
49
+ * cannot pass is an allow with no verdict behind it, or one whose
50
+ * `action_type` is anything but `allow`.
51
+ *
52
+ * A non-allow decision is returned with `allow` forced to `false` rather than
53
+ * passed through: the SDK's `fail()` emits `{verdict: "ERROR", allow: true}`
54
+ * when `failClosed` is false (dist/index.mjs:3654+), and this function is
55
+ * exported as a defence-in-depth layer that should not depend on a caller
56
+ * re-checking it. Everything else throws, which the judge tools turn into the
57
+ * explicit verdict-shaped refusal in `toJudgeFailureContent` rather than a
58
+ * silent pass.
59
+ */
60
+ export function hardenJudgeClient(client) {
61
+ const sdkAudit = client.auditToolCall;
62
+ const sdkJudge = client.judgeAction;
63
+ async function guardedAuditToolCall(input) {
64
+ let wire;
65
+ // `auditToolCall` reads its redaction and org config off `this` and calls
66
+ // `this.judgeAction` exactly once. A per-call prototype shadow captures that
67
+ // raw result without mutating the shared client, so two MCP tool calls in
68
+ // flight at the same time cannot observe each other's verdict.
69
+ const shadow = Object.create(client);
70
+ Object.defineProperty(shadow, "judgeAction", {
71
+ value: async (...args) => {
72
+ const result = await sdkJudge.apply(client, args);
73
+ wire = result;
74
+ return result;
75
+ },
76
+ });
77
+ const decision = await sdkAudit.call(shadow, input);
78
+ if (!isDecisionShaped(decision)) {
79
+ throw new Error("judge decision is not verdict-shaped and cannot be trusted");
80
+ }
81
+ if (decision.verdict !== "ALLOW" || decision.allow !== true) {
82
+ return { ...decision, allow: false };
83
+ }
84
+ if (wire === undefined) {
85
+ throw new Error("judge returned an allow without a judge response to back it");
86
+ }
87
+ if (wire.verdict !== "ALLOW" || wire.actionType !== "allow") {
88
+ throw new Error("judge response body does not carry an allow " +
89
+ `(verdict ${JSON.stringify(wire.verdict)}, action_type ${JSON.stringify(wire.actionType)})`);
90
+ }
91
+ return decision;
92
+ }
93
+ Object.defineProperty(client, "auditToolCall", {
94
+ value: guardedAuditToolCall,
95
+ writable: true,
96
+ configurable: true,
97
+ });
98
+ return client;
99
+ }
100
+ /**
101
+ * Build the MCP server's Atbash client.
102
+ *
103
+ * `Atbash.fromConfig` is used rather than `new Atbash(privkey, {endpoint})`
104
+ * because only `fromConfig` runs `validateJudgeEndpoint` — the direct
105
+ * constructor accepts any `ATBASH_ENDPOINT` unchecked and with no response
106
+ * signature verification, so an operator-supplied (or environment-injected)
107
+ * judge URL would be trusted outright. The endpoint is passed under `judge`,
108
+ * which is the key `fromConfig` actually reads; passing it at the top level
109
+ * silently drops it and falls back to the default endpoint.
110
+ */
111
+ export function createAtbashClient(env, sdk) {
112
+ assertMcpSdkCapabilities(sdk);
113
+ const agentKey = env.ATBASH_AGENT_PRIVKEY;
114
+ if (!agentKey) {
115
+ throw new Error("ATBASH_AGENT_PRIVKEY env var is required");
116
+ }
117
+ const endpoint = env.ATBASH_ENDPOINT?.trim();
118
+ const orgName = env.ATBASH_ORG_NAME?.trim();
119
+ return hardenJudgeClient(sdk.fromConfig({
120
+ agentKey,
121
+ ...(endpoint ? { judge: { endpoint } } : {}),
122
+ ...(orgName ? { orgName } : {}),
123
+ }));
124
+ }
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ import { toErrorContent, toJsonContent, safeErrorMessage } from "../utils.js";
3
+ // MCP is judge-as-a-tool — the host cannot intercept OTHER tools, so
4
+ // enforcement here is advisory and the model itself acts on this payload. A
5
+ // judge outage therefore must not come back as a generic "Error: fetch
6
+ // failed" (easy for the model to shrug off as a broken tool); it comes back
7
+ // as an explicit verdict-shaped refusal that instructs the model not to
8
+ // proceed. Only the safety-gate tools use this — diagnostics keep the
9
+ // generic toErrorContent.
10
+ function toJudgeFailureContent(error, toolName = "atbash_judge") {
11
+ return {
12
+ content: [
13
+ {
14
+ type: "text",
15
+ text: JSON.stringify({
16
+ verdict: "ERROR",
17
+ allow: false,
18
+ reason: `Atbash judge unavailable — treat this as BLOCK and do NOT proceed with the action (fail-closed). ` +
19
+ `Retry ${toolName} once the service is reachable. Cause: ${safeErrorMessage(error)}`,
20
+ }, null, 2),
21
+ },
22
+ ],
23
+ isError: true,
24
+ };
25
+ }
26
+ // Permission requires a positive match: verdict exactly "ALLOW" AND the SDK's
27
+ // boolean allow exactly true. Anything else — HOLD, BLOCK, ERROR, an
28
+ // unrecognised verdict, a missing verdict, or an "ALLOW" that carries
29
+ // allow:false — is handed to the model with allow:false.
30
+ function denyUnlessExplicitlyAllowed(decision) {
31
+ return decision.verdict === "ALLOW" && decision.allow === true
32
+ ? decision
33
+ : { ...decision, allow: false };
34
+ }
35
+ export function registerJudgeTools(server, client) {
36
+ server.registerTool("atbash_judge", {
37
+ description: "Submit an action for safety judgment before executing it. Returns ALLOW, HOLD, BLOCK, or ERROR.",
38
+ inputSchema: z.object({
39
+ action: z.string().describe("Plain text description of the action to judge"),
40
+ context: z.string().describe("Why this action is being taken"),
41
+ tool_name: z.string().optional().describe("Name of the tool being called"),
42
+ tool_args_json: z.string().optional().describe("JSON string of tool arguments"),
43
+ }),
44
+ }, async ({ action, context, tool_name, tool_args_json }) => {
45
+ try {
46
+ let parsedArgs = undefined;
47
+ if (tool_args_json) {
48
+ try {
49
+ parsedArgs = JSON.parse(tool_args_json);
50
+ }
51
+ catch {
52
+ parsedArgs = tool_args_json;
53
+ }
54
+ }
55
+ const rawDecision = await client.auditToolCall({
56
+ toolName: tool_name ?? "mcp_judge",
57
+ args: parsedArgs ?? { action },
58
+ context: `${action} — ${context}`,
59
+ });
60
+ const decision = denyUnlessExplicitlyAllowed(rawDecision);
61
+ if (decision.verdict === "ERROR") {
62
+ return toJudgeFailureContent(new Error(decision.reason ?? "judge returned an error verdict"));
63
+ }
64
+ if (decision.verdict === "HOLD") {
65
+ const holdParts = [
66
+ "Action held for operator review. The agent will not be jailed — please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",
67
+ `Reason: ${decision.reason ?? ""}`,
68
+ ];
69
+ if (decision.toolCallId)
70
+ holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
71
+ return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
72
+ }
73
+ return toJsonContent(decision);
74
+ }
75
+ catch (error) {
76
+ return toJudgeFailureContent(error);
77
+ }
78
+ });
79
+ server.registerTool("atbash_log", {
80
+ description: "Submit a tool call for Atbash safety judgment and logging. Returns ALLOW, HOLD, BLOCK, or ERROR.",
81
+ inputSchema: z.object({
82
+ action: z.string().describe("Action description"),
83
+ context: z.string().describe("Action context"),
84
+ tool_name: z.string().optional().describe("Tool name"),
85
+ tool_args_json: z.string().optional().describe("Tool arguments JSON"),
86
+ }),
87
+ }, async ({ action, context, tool_name, tool_args_json }) => {
88
+ try {
89
+ let parsedArgs = undefined;
90
+ if (tool_args_json) {
91
+ try {
92
+ parsedArgs = JSON.parse(tool_args_json);
93
+ }
94
+ catch {
95
+ parsedArgs = tool_args_json;
96
+ }
97
+ }
98
+ const rawDecision = await client.auditToolCall({
99
+ toolName: tool_name ?? "mcp_log",
100
+ args: parsedArgs ?? { action },
101
+ context: `${action} — ${context}`,
102
+ });
103
+ const decision = denyUnlessExplicitlyAllowed(rawDecision);
104
+ if (decision.verdict === "ERROR") {
105
+ return toJudgeFailureContent(new Error(decision.reason ?? "judge returned an error verdict"), "atbash_log");
106
+ }
107
+ if (decision.verdict === "HOLD") {
108
+ const holdParts = [
109
+ "Action held for operator review. The agent will not be jailed — please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",
110
+ `Reason: ${decision.reason ?? ""}`,
111
+ ];
112
+ if (decision.toolCallId)
113
+ holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
114
+ return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
115
+ }
116
+ return toJsonContent(decision);
117
+ }
118
+ catch (error) {
119
+ return toJudgeFailureContent(error, "atbash_log");
120
+ }
121
+ });
122
+ server.registerTool("atbash_check_agent", {
123
+ description: "Check whether an agent is registered on the Atbash platform.",
124
+ inputSchema: z.object({
125
+ pubkey: z.string().optional().describe("Agent public key, defaults to this server agent"),
126
+ }),
127
+ }, async ({ pubkey }) => {
128
+ try {
129
+ const selectedPubkey = pubkey ?? client.pubkey;
130
+ const registered = await client.checkAgentExists(selectedPubkey);
131
+ return toJsonContent({ registered, pubkey: selectedPubkey });
132
+ }
133
+ catch (error) {
134
+ return toErrorContent(error);
135
+ }
136
+ });
137
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { registerJudgeTools } from "./judge.js";
3
+ function makeFakeServer() {
4
+ const handlers = new Map();
5
+ return {
6
+ handlers,
7
+ registerTool: vi.fn((name, _config, handler) => {
8
+ handlers.set(name, handler);
9
+ }),
10
+ };
11
+ }
12
+ function makeClient(decision) {
13
+ return {
14
+ auditToolCall: vi.fn().mockResolvedValue(decision),
15
+ checkAgentExists: vi.fn(),
16
+ pubkey: "server-agent-pubkey",
17
+ };
18
+ }
19
+ describe("registerJudgeTools", () => {
20
+ it("registers exactly atbash_judge, atbash_log, atbash_check_agent", () => {
21
+ const server = makeFakeServer();
22
+ registerJudgeTools(server, makeClient({ verdict: "ALLOW" }));
23
+ expect([...server.handlers.keys()].sort()).toEqual([
24
+ "atbash_check_agent",
25
+ "atbash_judge",
26
+ "atbash_log",
27
+ ]);
28
+ });
29
+ describe("atbash_judge", () => {
30
+ it("passes ALLOW through unchanged", async () => {
31
+ const decision = { verdict: "ALLOW", allow: true, reason: "fine", toolCallId: "tc-1" };
32
+ const client = makeClient(decision);
33
+ const server = makeFakeServer();
34
+ registerJudgeTools(server, client);
35
+ const result = await server.handlers.get("atbash_judge")({
36
+ action: "wire $5000",
37
+ context: "customer request",
38
+ tool_name: "wire_transfer",
39
+ tool_args_json: JSON.stringify({ to: "0xabc" }),
40
+ });
41
+ const parsed = JSON.parse(result.content[0].text);
42
+ expect(parsed).toEqual(decision);
43
+ expect(client.auditToolCall).toHaveBeenCalledWith({
44
+ toolName: "wire_transfer",
45
+ args: { to: "0xabc" },
46
+ context: "wire $5000 — customer request",
47
+ });
48
+ });
49
+ it("reformats HOLD with allow:false and the tool_call_id reference", async () => {
50
+ const decision = {
51
+ verdict: "HOLD",
52
+ reason: "needs operator review",
53
+ toolCallId: "tc-2",
54
+ };
55
+ const server = makeFakeServer();
56
+ registerJudgeTools(server, makeClient(decision));
57
+ const result = await server.handlers.get("atbash_judge")({
58
+ action: "wire $999999",
59
+ context: "customer request",
60
+ });
61
+ const parsed = JSON.parse(result.content[0].text);
62
+ expect(parsed.verdict).toBe("HOLD");
63
+ expect(parsed.allow).toBe(false);
64
+ expect(parsed.reason).toContain("held for operator review");
65
+ expect(parsed.reason).toContain("Tool Call ID: tc-2");
66
+ });
67
+ it("falls back to a bare {action} payload when tool_args_json is absent", async () => {
68
+ const client = makeClient({ verdict: "ALLOW" });
69
+ const server = makeFakeServer();
70
+ registerJudgeTools(server, client);
71
+ await server.handlers.get("atbash_judge")({ action: "list files", context: "audit" });
72
+ expect(client.auditToolCall).toHaveBeenCalledWith({
73
+ toolName: "mcp_judge",
74
+ args: { action: "list files" },
75
+ context: "list files — audit",
76
+ });
77
+ });
78
+ it("returns an error content block instead of throwing when the judge is unavailable", async () => {
79
+ const client = {
80
+ auditToolCall: vi.fn().mockRejectedValue(new Error("judge unreachable")),
81
+ };
82
+ const server = makeFakeServer();
83
+ registerJudgeTools(server, client);
84
+ const result = await server.handlers.get("atbash_judge")({
85
+ action: "x",
86
+ context: "y",
87
+ });
88
+ expect(result.isError).toBe(true);
89
+ expect(result.content[0].text).toContain("judge unreachable");
90
+ });
91
+ });
92
+ describe("atbash_check_agent", () => {
93
+ it("defaults to the server agent's own pubkey when none is given", async () => {
94
+ const client = makeClient({ verdict: "ALLOW" });
95
+ client.checkAgentExists.mockResolvedValue(true);
96
+ const server = makeFakeServer();
97
+ registerJudgeTools(server, client);
98
+ const result = await server.handlers.get("atbash_check_agent")({});
99
+ expect(client.checkAgentExists).toHaveBeenCalledWith("server-agent-pubkey");
100
+ const parsed = JSON.parse(result.content[0].text);
101
+ expect(parsed).toEqual({ registered: true, pubkey: "server-agent-pubkey" });
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+ import { toErrorContent, toJsonContent } from "../utils.js";
3
+ function defaultMaxCount(value) {
4
+ return value ?? 20;
5
+ }
6
+ export function registerQueryTools(server, client) {
7
+ server.registerTool("atbash_get_policy", {
8
+ description: "Get an agent policy configuration and jail status.",
9
+ inputSchema: z.object({
10
+ pubkey: z.string().optional().describe("Agent public key, defaults to the server agent"),
11
+ }),
12
+ }, async ({ pubkey }) => {
13
+ try {
14
+ return toJsonContent(await client.getAgentPolicy(pubkey ?? client.pubkey));
15
+ }
16
+ catch (error) {
17
+ return toErrorContent(error);
18
+ }
19
+ });
20
+ server.registerTool("atbash_get_agent_detail", {
21
+ description: "Get agent metadata for a public key.",
22
+ inputSchema: z.object({
23
+ pubkey: z.string().optional().describe("Agent public key, defaults to the server agent"),
24
+ }),
25
+ }, async ({ pubkey }) => {
26
+ try {
27
+ return toJsonContent(await client.getAgentDetail(pubkey ?? client.pubkey));
28
+ }
29
+ catch (error) {
30
+ return toErrorContent(error);
31
+ }
32
+ });
33
+ server.registerTool("atbash_get_tool_calls", {
34
+ description: "List recent tool calls across all agents.",
35
+ inputSchema: z.object({
36
+ max_count: z.number().int().positive().optional().describe("Maximum number of records"),
37
+ }),
38
+ }, async ({ max_count }) => {
39
+ try {
40
+ return toJsonContent(await client.getToolCalls(defaultMaxCount(max_count)));
41
+ }
42
+ catch (error) {
43
+ return toErrorContent(error);
44
+ }
45
+ });
46
+ server.registerTool("atbash_get_agent_tool_calls", {
47
+ description: "List recent tool calls for one agent.",
48
+ inputSchema: z.object({
49
+ pubkey: z.string().optional().describe("Agent public key, defaults to the server agent"),
50
+ max_count: z.number().int().positive().optional().describe("Maximum number of records"),
51
+ }),
52
+ }, async ({ pubkey, max_count }) => {
53
+ try {
54
+ return toJsonContent(await client.getAgentToolCalls(pubkey ?? client.pubkey, defaultMaxCount(max_count)));
55
+ }
56
+ catch (error) {
57
+ return toErrorContent(error);
58
+ }
59
+ });
60
+ server.registerTool("atbash_get_org_tool_calls", {
61
+ description: "List recent tool calls for an organization.",
62
+ inputSchema: z.object({
63
+ org_name: z.string().describe("Organization name"),
64
+ max_count: z.number().int().positive().optional().describe("Maximum number of records"),
65
+ }),
66
+ }, async ({ org_name, max_count }) => {
67
+ try {
68
+ return toJsonContent(await client.getOrgToolCalls(org_name, defaultMaxCount(max_count)));
69
+ }
70
+ catch (error) {
71
+ return toErrorContent(error);
72
+ }
73
+ });
74
+ server.registerTool("atbash_get_tool_call_full", {
75
+ description: "Get the full detail for a single tool call.",
76
+ inputSchema: z.object({
77
+ tool_call_id: z.string().describe("Tool call identifier"),
78
+ }),
79
+ }, async ({ tool_call_id }) => {
80
+ try {
81
+ return toJsonContent(await client.getToolCallFull(tool_call_id));
82
+ }
83
+ catch (error) {
84
+ return toErrorContent(error);
85
+ }
86
+ });
87
+ server.registerTool("atbash_get_tool_call_count", {
88
+ description: "Get the total number of tool calls on-chain.",
89
+ inputSchema: z.object({}),
90
+ }, async () => {
91
+ try {
92
+ return toJsonContent({ count: await client.getToolCallCount() });
93
+ }
94
+ catch (error) {
95
+ return toErrorContent(error);
96
+ }
97
+ });
98
+ server.registerTool("atbash_get_tier_info", {
99
+ description: "Get an organization's tier and enforcement information.",
100
+ inputSchema: z.object({
101
+ org_name: z.string().describe("Organization name"),
102
+ }),
103
+ }, async ({ org_name }) => {
104
+ try {
105
+ return toJsonContent(await client.getOrgTierInfo(org_name));
106
+ }
107
+ catch (error) {
108
+ return toErrorContent(error);
109
+ }
110
+ });
111
+ server.registerTool("atbash_get_held_actions", {
112
+ description: "List actions currently waiting for operator review.",
113
+ inputSchema: z.object({
114
+ org_name: z.string().describe("Organization name"),
115
+ max_count: z.number().int().positive().optional().describe("Maximum number of records"),
116
+ }),
117
+ }, async ({ org_name, max_count }) => {
118
+ try {
119
+ return toJsonContent(await client.getPendingHeldActions(org_name, defaultMaxCount(max_count)));
120
+ }
121
+ catch (error) {
122
+ return toErrorContent(error);
123
+ }
124
+ });
125
+ server.registerTool("atbash_get_reviews", {
126
+ description: "List completed operator reviews for held actions.",
127
+ inputSchema: z.object({
128
+ org_name: z.string().describe("Organization name"),
129
+ max_count: z.number().int().positive().optional().describe("Maximum number of records"),
130
+ }),
131
+ }, async ({ org_name, max_count }) => {
132
+ try {
133
+ return toJsonContent(await client.getHeldActionReviews(org_name, defaultMaxCount(max_count)));
134
+ }
135
+ catch (error) {
136
+ return toErrorContent(error);
137
+ }
138
+ });
139
+ server.registerTool("atbash_get_safety_stats", {
140
+ description: "Get high-level chain-wide safety statistics.",
141
+ inputSchema: z.object({}),
142
+ }, async () => {
143
+ try {
144
+ return toJsonContent(await client.getSafetyStats());
145
+ }
146
+ catch (error) {
147
+ return toErrorContent(error);
148
+ }
149
+ });
150
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { toErrorContent, toJsonContent } from "../utils.js";
3
+ export function registerStatusTools(server, client) {
4
+ server.registerTool("atbash_judgment_status", {
5
+ description: "Poll the status of a previously submitted judgment.",
6
+ inputSchema: z.object({
7
+ tool_call_id: z.string().describe("The tool_call_id returned from atbash_judge"),
8
+ agent_pubkey: z
9
+ .string()
10
+ .optional()
11
+ .describe("Agent public key, defaults to the server agent"),
12
+ }),
13
+ }, async ({ tool_call_id, agent_pubkey }) => {
14
+ try {
15
+ const status = await client.getJudgmentStatus(tool_call_id, agent_pubkey);
16
+ return toJsonContent(status);
17
+ }
18
+ catch (error) {
19
+ return toErrorContent(error);
20
+ }
21
+ });
22
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,17 @@
1
+ export function safeErrorMessage(error) {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
4
+ export function toJsonContent(value) {
5
+ return {
6
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
7
+ };
8
+ }
9
+ export function toErrorContent(error) {
10
+ return {
11
+ content: [{ type: "text", text: `Error: ${safeErrorMessage(error)}` }],
12
+ isError: true,
13
+ };
14
+ }
15
+ export function truncateText(value, max = 500) {
16
+ return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/atbash-mcp",
3
- "version": "0.0.12-dev.0",
3
+ "version": "0.0.12-dev.2",
4
4
  "description": "Atbash safety judge exposed as a standalone MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,7 +48,7 @@
48
48
  "release:dev": "node scripts/release.mjs --channel dev"
49
49
  },
50
50
  "dependencies": {
51
- "@atbash/sdk": "0.10.11-dev.0",
51
+ "@atbash/sdk": "0.13.3-dev.0",
52
52
  "@modelcontextprotocol/sdk": "^1.12.3",
53
53
  "zod": "^3.25.76"
54
54
  },