@atbash/atbash-mcp 0.1.2 → 0.19.1-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.
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
+ }