@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.
@@ -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.1.2",
3
+ "version": "0.19.1-dev.0",
4
4
  "description": "Atbash safety judge exposed as a standalone MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "README.md",
18
- "LICENSE"
18
+ "LICENSE",
19
+ "NOTICE"
19
20
  ],
20
21
  "engines": {
21
22
  "node": ">=18.0.0"
@@ -32,7 +33,7 @@
32
33
  "judge",
33
34
  "policy"
34
35
  ],
35
- "license": "SEE LICENSE IN LICENSE",
36
+ "license": "Apache-2.0",
36
37
  "repository": {
37
38
  "type": "git",
38
39
  "url": "https://github.com/Atbash-Ai/atbash-mcp-plugin"
@@ -47,13 +48,10 @@
47
48
  "release": "node scripts/release.mjs --channel latest",
48
49
  "release:dev": "node scripts/release.mjs --channel dev"
49
50
  },
50
- "peerDependencies": {
51
- "@atbash/sdk": ">=0.9.2 <1.0.0"
52
- },
53
51
  "dependencies": {
54
- "@atbash/sdk": ">=0.9.2 <1.0.0",
55
52
  "@modelcontextprotocol/sdk": "^1.12.3",
56
- "zod": "^3.25.76"
53
+ "zod": "^3.25.76",
54
+ "@atbash/sdk": "0.19.1-dev.0"
57
55
  },
58
56
  "devDependencies": {
59
57
  "@types/node": "^25.7.0",
@@ -61,5 +59,8 @@
61
59
  "tsup": "^8.0.0",
62
60
  "typescript": "^5.0.0",
63
61
  "vitest": "^4.1.6"
62
+ },
63
+ "peerDependencies": {
64
+ "@atbash/sdk": "0.19.1-dev.0"
64
65
  }
65
66
  }