@peac/mcp-server 0.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +229 -0
  3. package/dist/cli.cjs +1708 -0
  4. package/dist/cli.cjs.map +1 -0
  5. package/dist/cli.d.ts +9 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.mjs +1687 -0
  8. package/dist/cli.mjs.map +1 -0
  9. package/dist/handlers/bundle.d.ts +10 -0
  10. package/dist/handlers/bundle.d.ts.map +1 -0
  11. package/dist/handlers/decode.d.ts +10 -0
  12. package/dist/handlers/decode.d.ts.map +1 -0
  13. package/dist/handlers/guards.d.ts +60 -0
  14. package/dist/handlers/guards.d.ts.map +1 -0
  15. package/dist/handlers/index.d.ts +8 -0
  16. package/dist/handlers/index.d.ts.map +1 -0
  17. package/dist/handlers/inspect.d.ts +10 -0
  18. package/dist/handlers/inspect.d.ts.map +1 -0
  19. package/dist/handlers/issue.d.ts +10 -0
  20. package/dist/handlers/issue.d.ts.map +1 -0
  21. package/dist/handlers/types.d.ts +31 -0
  22. package/dist/handlers/types.d.ts.map +1 -0
  23. package/dist/handlers/verify.d.ts +12 -0
  24. package/dist/handlers/verify.d.ts.map +1 -0
  25. package/dist/index.cjs +1685 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.ts +37 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.mjs +1623 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/infra/constants.d.ts +14 -0
  32. package/dist/infra/constants.d.ts.map +1 -0
  33. package/dist/infra/errors.d.ts +36 -0
  34. package/dist/infra/errors.d.ts.map +1 -0
  35. package/dist/infra/index.d.ts +7 -0
  36. package/dist/infra/index.d.ts.map +1 -0
  37. package/dist/infra/jwks-loader.d.ts +13 -0
  38. package/dist/infra/jwks-loader.d.ts.map +1 -0
  39. package/dist/infra/key-loader.d.ts +14 -0
  40. package/dist/infra/key-loader.d.ts.map +1 -0
  41. package/dist/infra/path-safety.d.ts +35 -0
  42. package/dist/infra/path-safety.d.ts.map +1 -0
  43. package/dist/infra/policy.d.ts +130 -0
  44. package/dist/infra/policy.d.ts.map +1 -0
  45. package/dist/schemas/bundle.d.ts +71 -0
  46. package/dist/schemas/bundle.d.ts.map +1 -0
  47. package/dist/schemas/decode.d.ts +50 -0
  48. package/dist/schemas/decode.d.ts.map +1 -0
  49. package/dist/schemas/index.d.ts +6 -0
  50. package/dist/schemas/index.d.ts.map +1 -0
  51. package/dist/schemas/inspect.d.ts +94 -0
  52. package/dist/schemas/inspect.d.ts.map +1 -0
  53. package/dist/schemas/issue.d.ts +123 -0
  54. package/dist/schemas/issue.d.ts.map +1 -0
  55. package/dist/schemas/verify.d.ts +94 -0
  56. package/dist/schemas/verify.d.ts.map +1 -0
  57. package/dist/server.d.ts +19 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/stdout-fence.d.ts +11 -0
  60. package/dist/stdout-fence.d.ts.map +1 -0
  61. package/package.json +66 -0
package/dist/cli.cjs ADDED
@@ -0,0 +1,1708 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var promises = require('fs/promises');
5
+ var commander = require('commander');
6
+ var zod = require('zod');
7
+ var crypto = require('@peac/crypto');
8
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
9
+ var protocol = require('@peac/protocol');
10
+ var schema = require('@peac/schema');
11
+ var nodePath = require('path');
12
+ var crypto$1 = require('crypto');
13
+ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
14
+
15
+ function _interopNamespace(e) {
16
+ if (e && e.__esModule) return e;
17
+ var n = Object.create(null);
18
+ if (e) {
19
+ Object.keys(e).forEach(function (k) {
20
+ if (k !== 'default') {
21
+ var d = Object.getOwnPropertyDescriptor(e, k);
22
+ Object.defineProperty(n, k, d.get ? d : {
23
+ enumerable: true,
24
+ get: function () { return e[k]; }
25
+ });
26
+ }
27
+ });
28
+ }
29
+ n.default = e;
30
+ return Object.freeze(n);
31
+ }
32
+
33
+ var nodePath__namespace = /*#__PURE__*/_interopNamespace(nodePath);
34
+
35
+ // src/infra/constants.ts
36
+ var SERVER_NAME = "peac-mcp-server";
37
+ var SERVER_VERSION = "0.10.12";
38
+ var MCP_PROTOCOL_VERSION = "2025-11-25";
39
+
40
+ // src/infra/errors.ts
41
+ var McpServerError = class extends Error {
42
+ code;
43
+ constructor(code, message) {
44
+ super(message);
45
+ this.name = "McpServerError";
46
+ this.code = code;
47
+ }
48
+ };
49
+ var KeyLoadError = class extends McpServerError {
50
+ constructor(message) {
51
+ super("E_MCP_KEY_LOAD", message);
52
+ this.name = "KeyLoadError";
53
+ }
54
+ };
55
+ var PolicyLoadError = class extends McpServerError {
56
+ constructor(message) {
57
+ super("E_MCP_POLICY_LOAD", message);
58
+ this.name = "PolicyLoadError";
59
+ }
60
+ };
61
+ var JwksLoadError = class extends McpServerError {
62
+ constructor(message) {
63
+ super("E_MCP_JWKS_LOAD", message);
64
+ this.name = "JwksLoadError";
65
+ }
66
+ };
67
+ var PathTraversalError = class extends McpServerError {
68
+ constructor(message) {
69
+ super("E_MCP_PATH_TRAVERSAL", message);
70
+ this.name = "PathTraversalError";
71
+ }
72
+ };
73
+ function sanitizeOutput(text, sensitivePatterns) {
74
+ let result = text;
75
+ for (const pattern of sensitivePatterns) {
76
+ result = result.replace(pattern, "[REDACTED]");
77
+ }
78
+ return result;
79
+ }
80
+
81
+ // src/infra/policy.ts
82
+ var ToolPolicySchema = zod.z.object({
83
+ enabled: zod.z.boolean().default(true),
84
+ allowed_kinds: zod.z.array(zod.z.string()).optional()
85
+ });
86
+ var RedactionSchema = zod.z.object({
87
+ strip_evidence: zod.z.boolean().default(false),
88
+ strip_payment: zod.z.boolean().default(false),
89
+ inspect_full_claims: zod.z.boolean().default(false)
90
+ });
91
+ var LimitsSchema = zod.z.object({
92
+ max_jws_bytes: zod.z.number().int().positive().default(16384),
93
+ max_response_bytes: zod.z.number().int().positive().default(65536),
94
+ tool_timeout_ms: zod.z.number().int().positive().default(3e4),
95
+ max_concurrency: zod.z.number().int().positive().default(10),
96
+ max_claims_bytes: zod.z.number().int().positive().default(262144),
97
+ max_bundle_receipts: zod.z.number().int().positive().default(256),
98
+ max_bundle_bytes: zod.z.number().int().positive().default(16777216),
99
+ max_ttl_seconds: zod.z.number().int().positive().default(86400)
100
+ });
101
+ var JwksConfigSchema = zod.z.object({
102
+ file: zod.z.string().optional()
103
+ });
104
+ var PolicySchema = zod.z.object({
105
+ version: zod.z.literal("1"),
106
+ allow_network: zod.z.boolean().default(false),
107
+ redaction: RedactionSchema.default({}),
108
+ tools: zod.z.record(zod.z.string(), ToolPolicySchema).default({}),
109
+ limits: LimitsSchema.default({}),
110
+ jwks: JwksConfigSchema.optional()
111
+ });
112
+ var DEFAULT_POLICY = {
113
+ version: "1",
114
+ allow_network: false,
115
+ redaction: { strip_evidence: false, strip_payment: false, inspect_full_claims: false },
116
+ tools: {},
117
+ limits: {
118
+ max_jws_bytes: 16384,
119
+ max_response_bytes: 65536,
120
+ tool_timeout_ms: 3e4,
121
+ max_concurrency: 10,
122
+ max_claims_bytes: 262144,
123
+ max_bundle_receipts: 256,
124
+ max_bundle_bytes: 16777216,
125
+ max_ttl_seconds: 86400
126
+ }
127
+ };
128
+ function getDefaultPolicy() {
129
+ return structuredClone(DEFAULT_POLICY);
130
+ }
131
+ function deepStableStringify(value) {
132
+ if (value === null || typeof value !== "object") {
133
+ return JSON.stringify(value);
134
+ }
135
+ if (Array.isArray(value)) {
136
+ return "[" + value.map(deepStableStringify).join(",") + "]";
137
+ }
138
+ const obj = value;
139
+ const keys = Object.keys(obj).sort();
140
+ const pairs = keys.map((k) => JSON.stringify(k) + ":" + deepStableStringify(obj[k]));
141
+ return "{" + pairs.join(",") + "}";
142
+ }
143
+ async function computePolicyHash(input) {
144
+ const canonical = typeof input === "string" ? input : deepStableStringify(input);
145
+ return crypto.sha256Hex(canonical);
146
+ }
147
+ async function loadPolicy(filePath) {
148
+ let raw;
149
+ try {
150
+ raw = await promises.readFile(filePath, "utf-8");
151
+ } catch (err) {
152
+ throw new PolicyLoadError(
153
+ `Failed to read policy file: ${filePath} -- ${err instanceof Error ? err.message : String(err)}`
154
+ );
155
+ }
156
+ try {
157
+ const s = await promises.stat(filePath);
158
+ if (s.mode & 2) {
159
+ process.stderr.write(
160
+ `[peac-mcp-server] WARNING: Policy file ${filePath} is world-writable
161
+ `
162
+ );
163
+ }
164
+ } catch {
165
+ }
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(raw);
169
+ } catch {
170
+ throw new PolicyLoadError(`Policy file is not valid JSON: ${filePath}`);
171
+ }
172
+ const result = PolicySchema.safeParse(parsed);
173
+ if (!result.success) {
174
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
175
+ throw new PolicyLoadError(`Policy validation failed: ${issues}`);
176
+ }
177
+ const hash = await computePolicyHash(result.data);
178
+ return { policy: result.data, hash };
179
+ }
180
+ async function loadIssuerKey(schemeUri) {
181
+ let raw;
182
+ if (schemeUri.startsWith("env:")) {
183
+ const varName = schemeUri.slice(4);
184
+ const envValue = process.env[varName];
185
+ if (!envValue) {
186
+ throw new KeyLoadError(`Environment variable ${varName} is not set or empty`);
187
+ }
188
+ raw = envValue;
189
+ } else if (schemeUri.startsWith("file:")) {
190
+ const filePath = schemeUri.slice(5);
191
+ try {
192
+ raw = await promises.readFile(filePath, "utf-8");
193
+ } catch (err) {
194
+ throw new KeyLoadError(
195
+ `Failed to read key file: ${filePath} -- ${err instanceof Error ? err.message : String(err)}`
196
+ );
197
+ }
198
+ } else {
199
+ throw new KeyLoadError(
200
+ `Unsupported key scheme: ${schemeUri} (expected env:VAR_NAME or file:/path)`
201
+ );
202
+ }
203
+ let jwk;
204
+ try {
205
+ jwk = JSON.parse(raw);
206
+ } catch {
207
+ throw new KeyLoadError("Key data is not valid JSON");
208
+ }
209
+ if (typeof jwk !== "object" || jwk === null || !("kty" in jwk) || !("crv" in jwk) || !("d" in jwk)) {
210
+ throw new KeyLoadError("JWK must contain kty, crv, and d fields");
211
+ }
212
+ const j = jwk;
213
+ if (j.kty !== "OKP" || j.crv !== "Ed25519") {
214
+ throw new KeyLoadError(
215
+ `JWK must be Ed25519 (kty: OKP, crv: Ed25519), got kty=${String(j.kty)} crv=${String(j.crv)}`
216
+ );
217
+ }
218
+ if (typeof j.d !== "string" || typeof j.x !== "string") {
219
+ throw new KeyLoadError("JWK d and x fields must be base64url strings");
220
+ }
221
+ const ed25519Jwk = {
222
+ kty: "OKP",
223
+ crv: "Ed25519",
224
+ x: j.x,
225
+ d: j.d
226
+ };
227
+ const valid = await crypto.validateKeypair(ed25519Jwk);
228
+ if (!valid) {
229
+ throw new KeyLoadError("JWK keypair validation failed: d does not derive to x");
230
+ }
231
+ const privateKey = crypto.base64urlDecode(ed25519Jwk.d);
232
+ const publicKey = await crypto.derivePublicKey(privateKey);
233
+ let kid;
234
+ if (typeof j.kid === "string") {
235
+ kid = j.kid;
236
+ } else {
237
+ const pubB64 = crypto.base64urlEncode(publicKey);
238
+ const hash = await crypto.sha256Hex(pubB64);
239
+ kid = hash.slice(0, 16);
240
+ }
241
+ return { privateKey, publicKey, kid };
242
+ }
243
+ async function loadJwksFile(filePath) {
244
+ let raw;
245
+ try {
246
+ raw = await promises.readFile(filePath, "utf-8");
247
+ } catch (err) {
248
+ throw new JwksLoadError(
249
+ `Failed to read JWKS file: ${filePath} -- ${err instanceof Error ? err.message : String(err)}`
250
+ );
251
+ }
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(raw);
255
+ } catch {
256
+ throw new JwksLoadError(`JWKS file is not valid JSON: ${filePath}`);
257
+ }
258
+ if (typeof parsed !== "object" || parsed === null || !("keys" in parsed)) {
259
+ throw new JwksLoadError('JWKS must contain a "keys" array');
260
+ }
261
+ const jwks = parsed;
262
+ if (!Array.isArray(jwks.keys)) {
263
+ throw new JwksLoadError('JWKS "keys" must be an array');
264
+ }
265
+ const entries = [];
266
+ for (const key of jwks.keys) {
267
+ if (typeof key !== "object" || key === null) continue;
268
+ const k = key;
269
+ if (k.kty !== "OKP" || k.crv !== "Ed25519") continue;
270
+ if (typeof k.x !== "string") continue;
271
+ if (k.alg !== void 0 && k.alg !== "EdDSA") continue;
272
+ const kid = typeof k.kid === "string" ? k.kid : `key-${entries.length}`;
273
+ try {
274
+ const publicKey = crypto.base64urlDecode(k.x);
275
+ if (publicKey.length === 32) {
276
+ entries.push({ kid, publicKey });
277
+ }
278
+ } catch {
279
+ }
280
+ }
281
+ return entries;
282
+ }
283
+ function resolveKeyByKid(keys, kid) {
284
+ const entry = keys.find((k) => k.kid === kid);
285
+ return entry?.publicKey;
286
+ }
287
+
288
+ // src/stdout-fence.ts
289
+ var MAX_LINE_BYTES = 4 * 1024 * 1024;
290
+ function installStdoutFence() {
291
+ const originalWrite = process.stdout.write.bind(process.stdout);
292
+ let buffer = "";
293
+ let bufferBytes = 0;
294
+ function validateJsonRpcLine(line) {
295
+ const lineBytes = new TextEncoder().encode(line).length;
296
+ if (lineBytes > MAX_LINE_BYTES) {
297
+ throw new McpServerError(
298
+ "E_MCP_STDOUT_FENCE_VIOLATION",
299
+ `Stdout fence: line is ${lineBytes} bytes, exceeding limit of ${MAX_LINE_BYTES}`
300
+ );
301
+ }
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(line);
305
+ } catch {
306
+ throw new McpServerError(
307
+ "E_MCP_STDOUT_FENCE_VIOLATION",
308
+ `Stdout fence: non-JSON line detected: ${line.slice(0, 100)}`
309
+ );
310
+ }
311
+ if (typeof parsed !== "object" || parsed === null) {
312
+ throw new McpServerError(
313
+ "E_MCP_STDOUT_FENCE_VIOLATION",
314
+ `Stdout fence: non-JSON-RPC 2.0 object on stdout: ${line.slice(0, 100)}`
315
+ );
316
+ }
317
+ const obj = parsed;
318
+ if (obj.jsonrpc !== "2.0") {
319
+ throw new McpServerError(
320
+ "E_MCP_STDOUT_FENCE_VIOLATION",
321
+ `Stdout fence: non-JSON-RPC 2.0 object on stdout: ${line.slice(0, 100)}`
322
+ );
323
+ }
324
+ const hasId = "id" in obj;
325
+ const hasResult = "result" in obj;
326
+ const hasError = "error" in obj;
327
+ const hasMethod = "method" in obj;
328
+ if (hasId && (hasResult || hasError)) {
329
+ return;
330
+ }
331
+ if (hasMethod && !hasId) {
332
+ return;
333
+ }
334
+ throw new McpServerError(
335
+ "E_MCP_STDOUT_FENCE_VIOLATION",
336
+ `Stdout fence: malformed JSON-RPC 2.0 message (expected response or notification): ${line.slice(0, 100)}`
337
+ );
338
+ }
339
+ function processBuffer() {
340
+ const lines = buffer.split("\n");
341
+ buffer = lines.pop();
342
+ bufferBytes = new TextEncoder().encode(buffer).length;
343
+ for (const line of lines) {
344
+ const trimmed = line.trim();
345
+ if (trimmed.length === 0) continue;
346
+ validateJsonRpcLine(trimmed);
347
+ }
348
+ }
349
+ const patchedWrite = function(chunk, encodingOrCallback, callback) {
350
+ let encoding;
351
+ let cb;
352
+ if (typeof encodingOrCallback === "function") {
353
+ cb = encodingOrCallback;
354
+ } else {
355
+ encoding = encodingOrCallback;
356
+ cb = callback;
357
+ }
358
+ const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
359
+ const chunkBytes = new TextEncoder().encode(str).length;
360
+ if (bufferBytes + chunkBytes > MAX_LINE_BYTES) {
361
+ throw new McpServerError(
362
+ "E_MCP_STDOUT_FENCE_VIOLATION",
363
+ `Stdout fence: buffer exceeded ${MAX_LINE_BYTES} bytes without a newline`
364
+ );
365
+ }
366
+ buffer += str;
367
+ bufferBytes += chunkBytes;
368
+ processBuffer();
369
+ if (encoding) {
370
+ return originalWrite(chunk, encoding, cb);
371
+ }
372
+ return originalWrite(chunk, cb);
373
+ };
374
+ process.stdout.write = patchedWrite;
375
+ return function teardown() {
376
+ if (buffer.trim().length > 0) {
377
+ validateJsonRpcLine(buffer.trim());
378
+ }
379
+ buffer = "";
380
+ bufferBytes = 0;
381
+ process.stdout.write = originalWrite;
382
+ };
383
+ }
384
+
385
+ // src/handlers/guards.ts
386
+ function checkJwsSize(jws, policy) {
387
+ const byteLength = new TextEncoder().encode(jws).length;
388
+ if (byteLength > policy.limits.max_jws_bytes) {
389
+ return {
390
+ text: `Input rejected: JWS is ${byteLength} bytes, exceeding limit of ${policy.limits.max_jws_bytes} bytes`,
391
+ structured: {
392
+ ok: false,
393
+ code: "E_MCP_INPUT_TOO_LARGE",
394
+ message: `JWS input is ${byteLength} bytes, limit is ${policy.limits.max_jws_bytes}`
395
+ },
396
+ isError: true
397
+ };
398
+ }
399
+ return void 0;
400
+ }
401
+ function checkToolEnabled(toolName, policy) {
402
+ const toolConfig = policy.tools[toolName];
403
+ if (toolConfig && toolConfig.enabled === false) {
404
+ return {
405
+ text: `Tool "${toolName}" is disabled by server policy`,
406
+ structured: {
407
+ ok: false,
408
+ code: "E_MCP_TOOL_DISABLED",
409
+ message: `Tool "${toolName}" is disabled by server policy`
410
+ },
411
+ isError: true
412
+ };
413
+ }
414
+ return void 0;
415
+ }
416
+ function sumStringBytes(value, seen) {
417
+ if (typeof value === "string") {
418
+ return new TextEncoder().encode(value).length;
419
+ }
420
+ if (typeof value !== "object" || value === null) return 0;
421
+ const visited = seen ?? /* @__PURE__ */ new WeakSet();
422
+ if (visited.has(value)) return 0;
423
+ visited.add(value);
424
+ if (Array.isArray(value)) {
425
+ let total2 = 0;
426
+ for (const item of value) {
427
+ total2 += sumStringBytes(item, visited);
428
+ }
429
+ return total2;
430
+ }
431
+ let total = 0;
432
+ for (const v of Object.values(value)) {
433
+ total += sumStringBytes(v, visited);
434
+ }
435
+ return total;
436
+ }
437
+ function estimateSerializedBytes(value, limit, seen) {
438
+ if (value === null) return 4;
439
+ if (value === void 0) return 0;
440
+ if (typeof value === "boolean") return value ? 4 : 5;
441
+ if (typeof value === "number") return String(value).length;
442
+ if (typeof value === "string") return Buffer.byteLength(JSON.stringify(value), "utf8");
443
+ if (typeof value !== "object") return 0;
444
+ const visited = seen ?? /* @__PURE__ */ new WeakSet();
445
+ if (visited.has(value)) return 0;
446
+ visited.add(value);
447
+ let total = 2;
448
+ if (Array.isArray(value)) {
449
+ for (let i = 0; i < value.length; i++) {
450
+ if (i > 0) total += 1;
451
+ total += estimateSerializedBytes(value[i], limit, visited);
452
+ if (total > limit) return Infinity;
453
+ }
454
+ return total;
455
+ }
456
+ const entries = Object.entries(value);
457
+ for (let i = 0; i < entries.length; i++) {
458
+ if (i > 0) total += 1;
459
+ const [k, v] = entries[i];
460
+ total += Buffer.byteLength(JSON.stringify(k), "utf8") + 1;
461
+ total += estimateSerializedBytes(v, limit, visited);
462
+ if (total > limit) return Infinity;
463
+ }
464
+ return total;
465
+ }
466
+ function checkInputSizes(input, policy) {
467
+ const limit = policy.limits.max_jws_bytes * 2;
468
+ const totalBytes = sumStringBytes(input);
469
+ if (totalBytes > limit) {
470
+ return {
471
+ text: `Input rejected: total input is ${totalBytes} bytes, exceeding limit of ${limit} bytes`,
472
+ structured: {
473
+ ok: false,
474
+ code: "E_MCP_INPUT_TOO_LARGE",
475
+ message: `Total input is ${totalBytes} bytes, limit is ${limit}`
476
+ },
477
+ isError: true
478
+ };
479
+ }
480
+ const estimatedBytes = estimateSerializedBytes(input, limit);
481
+ if (estimatedBytes > limit) {
482
+ return {
483
+ text: `Input rejected: estimated serialized input exceeds limit of ${limit} bytes`,
484
+ structured: {
485
+ ok: false,
486
+ code: "E_MCP_INPUT_TOO_LARGE",
487
+ message: `Estimated serialized input exceeds limit of ${limit} bytes`
488
+ },
489
+ isError: true
490
+ };
491
+ }
492
+ return void 0;
493
+ }
494
+ function checkObjectDepth(obj, maxDepth = 10, currentDepth = 0) {
495
+ if (currentDepth > maxDepth) return false;
496
+ if (typeof obj !== "object" || obj === null) return true;
497
+ if (Array.isArray(obj)) {
498
+ return obj.every((item) => checkObjectDepth(item, maxDepth, currentDepth + 1));
499
+ }
500
+ return Object.values(obj).every(
501
+ (value) => checkObjectDepth(value, maxDepth, currentDepth + 1)
502
+ );
503
+ }
504
+ var CONSERVATIVE_ID_PLACEHOLDER = "00000000-0000-0000-0000-000000000000";
505
+ function measureEnvelopeBytes(callToolResult, id = CONSERVATIVE_ID_PLACEHOLDER) {
506
+ const envelope = {
507
+ jsonrpc: "2.0",
508
+ id,
509
+ result: {
510
+ content: callToolResult.content,
511
+ structuredContent: callToolResult.structuredContent,
512
+ isError: callToolResult.isError
513
+ }
514
+ };
515
+ return Buffer.byteLength(JSON.stringify(envelope), "utf8") + 1;
516
+ }
517
+ var TRUNCATION_SUFFIX = "\n\n[TRUNCATED: response exceeded policy limit]";
518
+ var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
519
+ function truncateResponse(text, policy) {
520
+ const maxBytes = policy.limits.max_response_bytes;
521
+ const encoded = new TextEncoder().encode(text);
522
+ if (encoded.length <= maxBytes) {
523
+ return { text, truncated: false, originalBytes: encoded.length, returnedBytes: encoded.length };
524
+ }
525
+ const bodyBudget = Math.max(0, maxBytes - TRUNCATION_SUFFIX_BYTES);
526
+ const truncatedText = new TextDecoder().decode(encoded.slice(0, bodyBudget));
527
+ const finalText = truncatedText + TRUNCATION_SUFFIX;
528
+ const finalEncoded = new TextEncoder().encode(finalText);
529
+ return {
530
+ text: finalText,
531
+ truncated: true,
532
+ originalBytes: encoded.length,
533
+ returnedBytes: finalEncoded.length
534
+ };
535
+ }
536
+
537
+ // src/handlers/verify.ts
538
+ function resolvePublicKey(params) {
539
+ const { input, context } = params;
540
+ if (input.public_key_base64url) {
541
+ return {
542
+ publicKey: crypto.base64urlDecode(input.public_key_base64url),
543
+ keySource: "inline"
544
+ };
545
+ }
546
+ if (input.jwks) {
547
+ const jwks = JSON.parse(input.jwks);
548
+ if (!jwks.keys?.length) {
549
+ throw new Error("Provided JWKS contains no keys");
550
+ }
551
+ const { header } = crypto.decode(input.jws);
552
+ const kid = header.kid;
553
+ const ed25519Keys = jwks.keys.filter(
554
+ (k) => k.kty === "OKP" && k.crv === "Ed25519" && typeof k.x === "string" && (k.alg === void 0 || k.alg === "EdDSA")
555
+ );
556
+ if (ed25519Keys.length === 0) {
557
+ throw new Error("Provided JWKS contains no Ed25519 keys");
558
+ }
559
+ if (kid) {
560
+ const matched = ed25519Keys.find((k) => k.kid === kid);
561
+ if (matched) {
562
+ return {
563
+ publicKey: crypto.base64urlDecode(matched.x),
564
+ keySource: "inline-jwks"
565
+ };
566
+ }
567
+ throw Object.assign(
568
+ new Error(`No matching Ed25519 key found in provided JWKS for kid="${kid}"`),
569
+ { errorCode: "E_MCP_JWKS_NO_MATCH" }
570
+ );
571
+ }
572
+ if (ed25519Keys.length === 1) {
573
+ return {
574
+ publicKey: crypto.base64urlDecode(ed25519Keys[0].x),
575
+ keySource: "inline-jwks"
576
+ };
577
+ }
578
+ throw Object.assign(
579
+ new Error(
580
+ `JWS has no kid and JWKS contains ${ed25519Keys.length} Ed25519 keys -- provide kid to select`
581
+ ),
582
+ { errorCode: "E_MCP_KID_REQUIRED" }
583
+ );
584
+ }
585
+ if (context.jwksKeys?.length) {
586
+ const { header } = crypto.decode(input.jws);
587
+ const kid = header.kid;
588
+ if (kid) {
589
+ const key = resolveKeyByKid(context.jwksKeys, kid);
590
+ if (key) {
591
+ return { publicKey: key, keySource: "server-jwks" };
592
+ }
593
+ throw Object.assign(new Error(`No matching key in server JWKS for kid="${kid}"`), {
594
+ errorCode: "E_MCP_JWKS_NO_MATCH"
595
+ });
596
+ }
597
+ if (context.jwksKeys.length === 1) {
598
+ return { publicKey: context.jwksKeys[0].publicKey, keySource: "server-jwks" };
599
+ }
600
+ throw Object.assign(
601
+ new Error(
602
+ `JWS has no kid and server JWKS contains ${context.jwksKeys.length} keys -- provide kid to select`
603
+ ),
604
+ { errorCode: "E_MCP_KID_REQUIRED" }
605
+ );
606
+ }
607
+ throw Object.assign(
608
+ new Error(
609
+ "No public key available: provide public_key_base64url, jwks, or configure --jwks-file"
610
+ ),
611
+ { errorCode: "E_MCP_KEY_RESOLUTION" }
612
+ );
613
+ }
614
+ async function handleVerify(params) {
615
+ const { input, policy } = params;
616
+ const disabledResult = checkToolEnabled("peac_verify", policy);
617
+ if (disabledResult) return disabledResult;
618
+ const sizeResult = checkJwsSize(input.jws, policy);
619
+ if (sizeResult) return sizeResult;
620
+ let publicKey;
621
+ let keySource;
622
+ try {
623
+ const resolved = resolvePublicKey(params);
624
+ publicKey = resolved.publicKey;
625
+ keySource = resolved.keySource;
626
+ } catch (err) {
627
+ const code = err.errorCode ?? "E_MCP_KEY_RESOLUTION";
628
+ return {
629
+ text: `Verification failed: ${err instanceof Error ? err.message : String(err)}`,
630
+ structured: {
631
+ ok: false,
632
+ code,
633
+ message: err instanceof Error ? err.message : String(err),
634
+ checks: []
635
+ },
636
+ isError: true
637
+ };
638
+ }
639
+ const result = await protocol.verifyLocal(input.jws, publicKey, {
640
+ issuer: input.issuer,
641
+ audience: input.audience
642
+ });
643
+ if (result.valid) {
644
+ const checks2 = [
645
+ { name: "signature", passed: true },
646
+ { name: "schema", passed: true },
647
+ { name: "expiry", passed: true }
648
+ ];
649
+ if (input.issuer) {
650
+ checks2.push({ name: "issuer_binding", passed: true });
651
+ }
652
+ if (input.audience) {
653
+ checks2.push({ name: "audience_binding", passed: true });
654
+ }
655
+ const claims = result.claims;
656
+ const claimsSummary = {
657
+ iss: claims.iss,
658
+ aud: claims.aud,
659
+ rid: claims.rid,
660
+ variant: result.variant
661
+ };
662
+ if (result.variant === "commerce") {
663
+ claimsSummary.amt = claims.amt;
664
+ claimsSummary.cur = claims.cur;
665
+ }
666
+ const tr = truncateResponse(
667
+ `Verification PASSED (${result.variant} receipt, kid=${result.kid})`,
668
+ policy
669
+ );
670
+ return {
671
+ text: tr.text,
672
+ structured: {
673
+ ok: true,
674
+ variant: result.variant,
675
+ checks: checks2,
676
+ claimsSummary,
677
+ keySource,
678
+ ...tr.truncated ? {
679
+ _truncation: {
680
+ truncated: tr.truncated,
681
+ originalBytes: tr.originalBytes,
682
+ returnedBytes: tr.returnedBytes
683
+ }
684
+ } : {}
685
+ }
686
+ };
687
+ }
688
+ const checks = [];
689
+ if (result.code === "E_INVALID_SIGNATURE") {
690
+ checks.push({ name: "signature", passed: false, message: result.message });
691
+ } else if (result.code === "E_INVALID_FORMAT") {
692
+ checks.push({ name: "signature", passed: true });
693
+ checks.push({ name: "schema", passed: false, message: result.message });
694
+ } else if (result.code === "E_EXPIRED") {
695
+ checks.push({ name: "signature", passed: true });
696
+ checks.push({ name: "schema", passed: true });
697
+ checks.push({ name: "expiry", passed: false, message: result.message });
698
+ } else if (result.code === "E_INVALID_ISSUER") {
699
+ checks.push({ name: "signature", passed: true });
700
+ checks.push({ name: "schema", passed: true });
701
+ checks.push({ name: "issuer_binding", passed: false, message: result.message });
702
+ } else if (result.code === "E_INVALID_AUDIENCE") {
703
+ checks.push({ name: "signature", passed: true });
704
+ checks.push({ name: "schema", passed: true });
705
+ checks.push({ name: "audience_binding", passed: false, message: result.message });
706
+ } else {
707
+ checks.push({ name: result.code, passed: false, message: result.message });
708
+ }
709
+ const trFail = truncateResponse(
710
+ `Verification FAILED: ${result.code} -- ${result.message}`,
711
+ policy
712
+ );
713
+ return {
714
+ text: trFail.text,
715
+ structured: {
716
+ ok: false,
717
+ code: result.code,
718
+ message: result.message,
719
+ checks,
720
+ keySource,
721
+ ...trFail.truncated ? {
722
+ _truncation: {
723
+ truncated: trFail.truncated,
724
+ originalBytes: trFail.originalBytes,
725
+ returnedBytes: trFail.returnedBytes
726
+ }
727
+ } : {}
728
+ }
729
+ };
730
+ }
731
+ async function handleInspect(params) {
732
+ const { input, policy } = params;
733
+ const disabledResult = checkToolEnabled("peac_inspect", policy);
734
+ if (disabledResult) return disabledResult;
735
+ const sizeResult = checkJwsSize(input.jws, policy);
736
+ if (sizeResult) return sizeResult;
737
+ let header;
738
+ let payload;
739
+ try {
740
+ const decoded = crypto.decode(input.jws);
741
+ header = decoded.header;
742
+ payload = decoded.payload;
743
+ } catch (err) {
744
+ return {
745
+ text: `Inspect failed: ${err instanceof Error ? err.message : String(err)}`,
746
+ structured: {
747
+ error: err instanceof Error ? err.message : String(err),
748
+ code: "E_MCP_INVALID_INPUT"
749
+ },
750
+ isError: true
751
+ };
752
+ }
753
+ let variant = "unknown";
754
+ try {
755
+ const pr = schema.parseReceiptClaims(payload);
756
+ if (pr.ok) {
757
+ variant = pr.variant;
758
+ }
759
+ } catch {
760
+ }
761
+ const payloadMeta = {
762
+ variant,
763
+ issuer: payload.iss,
764
+ audience: payload.aud
765
+ };
766
+ if (typeof payload.iat === "number") {
767
+ payloadMeta.issuedAt = new Date(payload.iat * 1e3).toISOString();
768
+ }
769
+ if (typeof payload.exp === "number") {
770
+ payloadMeta.expiresAt = new Date(payload.exp * 1e3).toISOString();
771
+ }
772
+ if (typeof payload.rid === "string") {
773
+ payloadMeta.receiptId = payload.rid;
774
+ }
775
+ let redacted = false;
776
+ let fullPayload;
777
+ const effectiveFullClaims = input.full_claims && policy.redaction.inspect_full_claims;
778
+ if (effectiveFullClaims) {
779
+ fullPayload = { ...payload };
780
+ if (policy.redaction.strip_evidence && "evidence" in fullPayload) {
781
+ fullPayload.evidence = "[REDACTED by policy]";
782
+ redacted = true;
783
+ }
784
+ if (policy.redaction.strip_payment && "payment" in fullPayload) {
785
+ fullPayload.payment = "[REDACTED by policy]";
786
+ redacted = true;
787
+ }
788
+ }
789
+ const lines = [
790
+ "WARNING: Signature NOT verified (use peac_verify to verify)",
791
+ "",
792
+ `Variant: ${variant}`,
793
+ `Issuer: ${payload.iss ?? "N/A"}`,
794
+ `Audience: ${payload.aud ?? "N/A"}`
795
+ ];
796
+ if (payloadMeta.issuedAt) lines.push(`Issued At: ${payloadMeta.issuedAt}`);
797
+ if (payloadMeta.expiresAt) lines.push(`Expires At: ${payloadMeta.expiresAt}`);
798
+ if (payloadMeta.receiptId) lines.push(`Receipt ID: ${payloadMeta.receiptId}`);
799
+ if (redacted) lines.push("", "Note: Some fields redacted by policy");
800
+ const tr = truncateResponse(lines.join("\n"), policy);
801
+ return {
802
+ text: tr.text,
803
+ structured: {
804
+ header,
805
+ payloadMeta,
806
+ ...fullPayload !== void 0 ? { fullPayload } : {},
807
+ redacted,
808
+ verified: false,
809
+ ...tr.truncated ? {
810
+ _truncation: {
811
+ truncated: tr.truncated,
812
+ originalBytes: tr.originalBytes,
813
+ returnedBytes: tr.returnedBytes
814
+ }
815
+ } : {}
816
+ }
817
+ };
818
+ }
819
+ async function handleDecode(params) {
820
+ const { input, policy } = params;
821
+ const disabledResult = checkToolEnabled("peac_decode", policy);
822
+ if (disabledResult) return disabledResult;
823
+ const sizeResult = checkJwsSize(input.jws, policy);
824
+ if (sizeResult) return sizeResult;
825
+ try {
826
+ const decoded = crypto.decode(input.jws);
827
+ const header = decoded.header;
828
+ const payload = decoded.payload;
829
+ const tr = truncateResponse(
830
+ [
831
+ "WARNING: Signature NOT verified",
832
+ "",
833
+ `Header: ${JSON.stringify(header, null, 2)}`,
834
+ "",
835
+ `Payload: ${JSON.stringify(payload, null, 2)}`
836
+ ].join("\n"),
837
+ policy
838
+ );
839
+ return {
840
+ text: tr.text,
841
+ structured: {
842
+ header,
843
+ payload,
844
+ verified: false,
845
+ ...tr.truncated ? {
846
+ _truncation: {
847
+ truncated: tr.truncated,
848
+ originalBytes: tr.originalBytes,
849
+ returnedBytes: tr.returnedBytes
850
+ }
851
+ } : {}
852
+ }
853
+ };
854
+ } catch (err) {
855
+ return {
856
+ text: `Decode failed: ${err instanceof Error ? err.message : String(err)}`,
857
+ structured: {
858
+ error: err instanceof Error ? err.message : String(err),
859
+ code: "E_MCP_INVALID_INPUT"
860
+ },
861
+ isError: true
862
+ };
863
+ }
864
+ }
865
+ function buildKeyPatterns(privateKey, publicKey) {
866
+ const privB64 = crypto.base64urlEncode(privateKey);
867
+ const pubB64 = crypto.base64urlEncode(publicKey);
868
+ const escPriv = privB64.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
869
+ const escPub = pubB64.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
870
+ return [new RegExp(escPriv, "g"), new RegExp(escPub, "g")];
871
+ }
872
+ async function handleIssue(params) {
873
+ const { input, policy, context } = params;
874
+ const disabledResult = checkToolEnabled("peac_issue", policy);
875
+ if (disabledResult) return disabledResult;
876
+ if (!context.issuerKey || !context.issuerId) {
877
+ return {
878
+ text: "Issue failed: server not configured with issuer key and ID (--issuer-key + --issuer-id)",
879
+ structured: {
880
+ ok: false,
881
+ code: "E_MCP_KEY_REQUIRED",
882
+ message: "Server requires --issuer-key and --issuer-id for peac_issue"
883
+ },
884
+ isError: true
885
+ };
886
+ }
887
+ const inputJson = JSON.stringify(input);
888
+ const inputBytes = new TextEncoder().encode(inputJson).length;
889
+ if (inputBytes > policy.limits.max_claims_bytes) {
890
+ return {
891
+ text: `Input rejected: claims are ${inputBytes} bytes, exceeding limit of ${policy.limits.max_claims_bytes} bytes`,
892
+ structured: {
893
+ ok: false,
894
+ code: "E_MCP_INPUT_TOO_LARGE",
895
+ message: `Claims input is ${inputBytes} bytes, limit is ${policy.limits.max_claims_bytes}`
896
+ },
897
+ isError: true
898
+ };
899
+ }
900
+ if (input.ttl_seconds !== void 0 && input.ttl_seconds > policy.limits.max_ttl_seconds) {
901
+ return {
902
+ text: `Input rejected: ttl_seconds ${input.ttl_seconds} exceeds max_ttl_seconds ${policy.limits.max_ttl_seconds}`,
903
+ structured: {
904
+ ok: false,
905
+ code: "E_MCP_INVALID_INPUT",
906
+ message: `ttl_seconds ${input.ttl_seconds} exceeds policy max of ${policy.limits.max_ttl_seconds}`
907
+ },
908
+ isError: true
909
+ };
910
+ }
911
+ const keyPatterns = buildKeyPatterns(context.issuerKey.privateKey, context.issuerKey.publicKey);
912
+ const now = Math.floor(Date.now() / 1e3);
913
+ const exp = input.ttl_seconds !== void 0 ? now + input.ttl_seconds : void 0;
914
+ try {
915
+ const result = await protocol.issue({
916
+ iss: context.issuerId,
917
+ aud: input.aud,
918
+ amt: input.amt,
919
+ cur: input.cur,
920
+ rail: input.rail,
921
+ reference: input.reference,
922
+ asset: input.asset,
923
+ env: input.env,
924
+ network: input.network,
925
+ evidence: input.evidence,
926
+ subject: input.subject,
927
+ exp,
928
+ privateKey: context.issuerKey.privateKey,
929
+ kid: context.issuerKey.kid
930
+ });
931
+ const { payload } = crypto.decode(result.jws);
932
+ const claimsSummary = {
933
+ iss: payload.iss,
934
+ aud: payload.aud,
935
+ iat: payload.iat,
936
+ ...payload.exp !== void 0 ? { exp: payload.exp } : {},
937
+ rid: payload.rid,
938
+ amt: payload.amt,
939
+ cur: payload.cur
940
+ };
941
+ const tr = truncateResponse(
942
+ `Receipt issued: rid=${claimsSummary.rid}, ${claimsSummary.amt} ${claimsSummary.cur}`,
943
+ policy
944
+ );
945
+ const safeText = sanitizeOutput(tr.text, keyPatterns);
946
+ return {
947
+ text: safeText,
948
+ structured: {
949
+ ok: true,
950
+ jws: result.jws,
951
+ claimsSummary
952
+ }
953
+ };
954
+ } catch (err) {
955
+ const rawMessage = err instanceof Error ? err.message : String(err);
956
+ const safeMessage = sanitizeOutput(rawMessage, keyPatterns);
957
+ if (err instanceof protocol.IssueError) {
958
+ return {
959
+ text: `Issue failed: ${safeMessage}`,
960
+ structured: {
961
+ ok: false,
962
+ code: "E_MCP_ISSUE_FAILED",
963
+ message: safeMessage
964
+ },
965
+ isError: true
966
+ };
967
+ }
968
+ if (err instanceof McpServerError) {
969
+ return {
970
+ text: `Issue failed: ${safeMessage}`,
971
+ structured: {
972
+ ok: false,
973
+ code: err.code,
974
+ message: safeMessage
975
+ },
976
+ isError: true
977
+ };
978
+ }
979
+ return {
980
+ text: `Issue failed: ${safeMessage}`,
981
+ structured: {
982
+ ok: false,
983
+ code: "E_MCP_ISSUE_FAILED",
984
+ message: safeMessage
985
+ },
986
+ isError: true
987
+ };
988
+ }
989
+ }
990
+ function assertRelativePath(p) {
991
+ if (!p || p.trim().length === 0) {
992
+ throw new PathTraversalError("Path must not be empty");
993
+ }
994
+ if (nodePath__namespace.isAbsolute(p)) {
995
+ throw new PathTraversalError("Absolute paths are not allowed");
996
+ }
997
+ const normalized = nodePath__namespace.normalize(p);
998
+ if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
999
+ throw new PathTraversalError("Path traversal (..) is not allowed");
1000
+ }
1001
+ if (p.includes("/") || p.includes("\\")) {
1002
+ throw new PathTraversalError("Nested directories are not allowed (single segment only)");
1003
+ }
1004
+ if (p.length > 255) {
1005
+ throw new PathTraversalError(`Path segment exceeds 255 characters (got ${p.length})`);
1006
+ }
1007
+ if (p.startsWith(".")) {
1008
+ throw new PathTraversalError("Hidden paths (starting with .) are not allowed");
1009
+ }
1010
+ }
1011
+ async function assertNotSymlink(componentPath) {
1012
+ try {
1013
+ const stats = await promises.lstat(componentPath);
1014
+ if (stats.isSymbolicLink()) {
1015
+ throw new PathTraversalError(`Symlink detected at ${componentPath}`);
1016
+ }
1017
+ } catch (err) {
1018
+ if (err instanceof PathTraversalError) throw err;
1019
+ if (err.code === "ENOENT") return;
1020
+ throw err;
1021
+ }
1022
+ }
1023
+ async function atomicWriteDir(tempDir, finalPath) {
1024
+ try {
1025
+ await promises.rename(tempDir, finalPath);
1026
+ } catch (err) {
1027
+ try {
1028
+ await promises.rm(tempDir, { recursive: true, force: true });
1029
+ } catch {
1030
+ }
1031
+ throw err;
1032
+ }
1033
+ }
1034
+ async function createTempDir(basePath) {
1035
+ await assertNotSymlink(basePath);
1036
+ const prefix = nodePath__namespace.join(basePath, `.tmp-${crypto$1.randomUUID().slice(0, 8)}-`);
1037
+ return promises.mkdtemp(prefix);
1038
+ }
1039
+
1040
+ // src/handlers/bundle.ts
1041
+ function stableJsonStringify(value) {
1042
+ return JSON.stringify(
1043
+ value,
1044
+ (_key, val) => {
1045
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
1046
+ const sorted = {};
1047
+ for (const k of Object.keys(val).sort()) {
1048
+ sorted[k] = val[k];
1049
+ }
1050
+ return sorted;
1051
+ }
1052
+ return val;
1053
+ },
1054
+ 2
1055
+ ) + "\n";
1056
+ }
1057
+ function buildKeyPatterns2(privateKey, publicKey) {
1058
+ const privB64 = crypto.base64urlEncode(privateKey);
1059
+ const pubB64 = crypto.base64urlEncode(publicKey);
1060
+ const escPriv = privB64.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1061
+ const escPub = pubB64.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1062
+ return [new RegExp(escPriv, "g"), new RegExp(escPub, "g")];
1063
+ }
1064
+ function generateBundleName() {
1065
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1066
+ const rand = crypto$1.randomUUID().slice(0, 8);
1067
+ return `bundle-${ts}-${rand}`;
1068
+ }
1069
+ async function handleCreateBundle(params) {
1070
+ const { input, policy, context } = params;
1071
+ const disabledResult = checkToolEnabled("peac_create_bundle", policy);
1072
+ if (disabledResult) return disabledResult;
1073
+ if (!context.bundleDir) {
1074
+ return {
1075
+ text: "Bundle creation failed: server not configured with --bundle-dir",
1076
+ structured: {
1077
+ ok: false,
1078
+ code: "E_MCP_BUNDLE_DIR_REQUIRED",
1079
+ message: "Server requires --bundle-dir for peac_create_bundle"
1080
+ },
1081
+ isError: true
1082
+ };
1083
+ }
1084
+ if (!context.issuerKey || !context.issuerId) {
1085
+ return {
1086
+ text: "Bundle creation failed: server not configured with issuer key and ID",
1087
+ structured: {
1088
+ ok: false,
1089
+ code: "E_MCP_KEY_REQUIRED",
1090
+ message: "Server requires --issuer-key and --issuer-id for peac_create_bundle"
1091
+ },
1092
+ isError: true
1093
+ };
1094
+ }
1095
+ if (input.receipts.length > policy.limits.max_bundle_receipts) {
1096
+ return {
1097
+ text: `Input rejected: ${input.receipts.length} receipts exceeds limit of ${policy.limits.max_bundle_receipts}`,
1098
+ structured: {
1099
+ ok: false,
1100
+ code: "E_MCP_INPUT_TOO_LARGE",
1101
+ message: `${input.receipts.length} receipts exceeds limit of ${policy.limits.max_bundle_receipts}`
1102
+ },
1103
+ isError: true
1104
+ };
1105
+ }
1106
+ let totalReceiptBytes = 0;
1107
+ for (const r of input.receipts) {
1108
+ totalReceiptBytes += new TextEncoder().encode(r).length;
1109
+ }
1110
+ if (totalReceiptBytes > policy.limits.max_bundle_bytes) {
1111
+ return {
1112
+ text: `Input rejected: total receipt bytes ${totalReceiptBytes} exceeds limit of ${policy.limits.max_bundle_bytes}`,
1113
+ structured: {
1114
+ ok: false,
1115
+ code: "E_MCP_INPUT_TOO_LARGE",
1116
+ message: `Total receipt bytes ${totalReceiptBytes} exceeds limit of ${policy.limits.max_bundle_bytes}`
1117
+ },
1118
+ isError: true
1119
+ };
1120
+ }
1121
+ const outputName = input.output_path ?? generateBundleName();
1122
+ try {
1123
+ assertRelativePath(outputName);
1124
+ } catch (err) {
1125
+ return {
1126
+ text: `Bundle creation failed: ${err instanceof Error ? err.message : String(err)}`,
1127
+ structured: {
1128
+ ok: false,
1129
+ code: "E_MCP_PATH_TRAVERSAL",
1130
+ message: err instanceof Error ? err.message : String(err)
1131
+ },
1132
+ isError: true
1133
+ };
1134
+ }
1135
+ const keyPatterns = buildKeyPatterns2(context.issuerKey.privateKey, context.issuerKey.publicKey);
1136
+ let tempDir;
1137
+ try {
1138
+ tempDir = await createTempDir(context.bundleDir);
1139
+ const receiptsDir = nodePath.join(tempDir, "receipts");
1140
+ await promises.mkdir(receiptsDir, { mode: 448 });
1141
+ const entries = [];
1142
+ let totalBytes = 0;
1143
+ const seenHashes = /* @__PURE__ */ new Set();
1144
+ const receiptHashes = [];
1145
+ for (let i = 0; i < input.receipts.length; i++) {
1146
+ if (params.signal?.aborted) {
1147
+ throw new McpServerError("E_MCP_CANCELLED", "Request cancelled during bundle creation");
1148
+ }
1149
+ const jws = input.receipts[i];
1150
+ const data = new TextEncoder().encode(jws);
1151
+ const hash = await crypto.sha256Hex(data);
1152
+ if (seenHashes.has(hash)) continue;
1153
+ seenHashes.add(hash);
1154
+ receiptHashes.push(hash);
1155
+ const fileName = `${hash}.jws`;
1156
+ const filePath = nodePath.join(receiptsDir, fileName);
1157
+ await promises.writeFile(filePath, data, { mode: 384 });
1158
+ entries.push({
1159
+ index: i,
1160
+ file: `receipts/${fileName}`,
1161
+ sha256: hash,
1162
+ length: data.length
1163
+ });
1164
+ totalBytes += data.length;
1165
+ }
1166
+ entries.sort((a, b) => a.sha256.localeCompare(b.sha256));
1167
+ const sortedHashes = [...receiptHashes].sort();
1168
+ const bundleIdInput = `${context.policyHash}:${sortedHashes.join(":")}`;
1169
+ const bundleId = await crypto.sha256Hex(new TextEncoder().encode(bundleIdInput));
1170
+ const manifest = {
1171
+ bundle_id: bundleId,
1172
+ tool_version: SERVER_VERSION,
1173
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1174
+ policy_hash: context.policyHash,
1175
+ receipt_count: entries.length,
1176
+ receipts: entries,
1177
+ ...input.metadata ? { metadata: input.metadata } : {}
1178
+ };
1179
+ const manifestJson = stableJsonStringify(manifest);
1180
+ await promises.writeFile(nodePath.join(tempDir, "manifest.json"), manifestJson, { mode: 384 });
1181
+ totalBytes += new TextEncoder().encode(manifestJson).length;
1182
+ const manifestBytes = new TextEncoder().encode(manifestJson);
1183
+ const manifestSha256 = await crypto.sha256Hex(manifestBytes);
1184
+ const canonicalManifest = JSON.parse(manifestJson);
1185
+ const manifestJws = await crypto.sign(
1186
+ canonicalManifest,
1187
+ context.issuerKey.privateKey,
1188
+ context.issuerKey.kid
1189
+ );
1190
+ await promises.writeFile(nodePath.join(tempDir, "manifest.jws"), manifestJws, { mode: 384 });
1191
+ totalBytes += new TextEncoder().encode(manifestJws).length;
1192
+ const fileCount = entries.length + 2;
1193
+ const finalPath = nodePath.join(context.bundleDir, outputName);
1194
+ try {
1195
+ await promises.stat(finalPath);
1196
+ return {
1197
+ text: `Bundle creation failed: output path already exists: ${outputName}`,
1198
+ structured: {
1199
+ ok: false,
1200
+ code: "E_MCP_BUNDLE_FAILED",
1201
+ message: `Output path already exists: ${outputName}`
1202
+ },
1203
+ isError: true
1204
+ };
1205
+ } catch (err) {
1206
+ if (err.code !== "ENOENT") {
1207
+ throw err;
1208
+ }
1209
+ }
1210
+ await atomicWriteDir(tempDir, finalPath);
1211
+ tempDir = void 0;
1212
+ const tr = truncateResponse(
1213
+ `Bundle created: ${outputName} (${fileCount} files, ${totalBytes} bytes)`,
1214
+ policy
1215
+ );
1216
+ const safeText = sanitizeOutput(tr.text, keyPatterns);
1217
+ return {
1218
+ text: safeText,
1219
+ structured: {
1220
+ ok: true,
1221
+ bundleId,
1222
+ bundleName: outputName,
1223
+ receiptCount: entries.length,
1224
+ fileCount,
1225
+ totalBytes,
1226
+ createdAt: manifest.created_at,
1227
+ manifestSha256
1228
+ }
1229
+ };
1230
+ } catch (err) {
1231
+ if (tempDir) {
1232
+ try {
1233
+ await promises.rm(tempDir, { recursive: true, force: true });
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ const rawMessage = err instanceof Error ? err.message : String(err);
1238
+ const safeMessage = sanitizeOutput(rawMessage, keyPatterns);
1239
+ if (err instanceof McpServerError) {
1240
+ return {
1241
+ text: `Bundle creation failed: ${safeMessage}`,
1242
+ structured: {
1243
+ ok: false,
1244
+ code: err.code,
1245
+ message: safeMessage
1246
+ },
1247
+ isError: true
1248
+ };
1249
+ }
1250
+ return {
1251
+ text: `Bundle creation failed: ${safeMessage}`,
1252
+ structured: {
1253
+ ok: false,
1254
+ code: "E_MCP_BUNDLE_FAILED",
1255
+ message: safeMessage
1256
+ },
1257
+ isError: true
1258
+ };
1259
+ }
1260
+ }
1261
+ var VerifyInputSchema = zod.z.object({
1262
+ jws: zod.z.string().describe("JWS compact serialization (header.payload.signature)"),
1263
+ public_key_base64url: zod.z.string().optional().describe("Ed25519 public key in base64url encoding (32 bytes)"),
1264
+ jwks: zod.z.string().optional().describe("Inline JWKS JSON containing the verifying key"),
1265
+ issuer: zod.z.string().optional().describe("Expected issuer URI for binding check"),
1266
+ audience: zod.z.string().optional().describe("Expected audience URI for binding check")
1267
+ });
1268
+ var MetaSchema = zod.z.object({
1269
+ serverVersion: zod.z.string(),
1270
+ policyHash: zod.z.string(),
1271
+ protocolVersion: zod.z.string()
1272
+ });
1273
+ var CheckSchema = zod.z.object({
1274
+ name: zod.z.string(),
1275
+ passed: zod.z.boolean(),
1276
+ message: zod.z.string().optional()
1277
+ });
1278
+ var VerifyOutputSchema = zod.z.object({
1279
+ _meta: MetaSchema,
1280
+ ok: zod.z.boolean(),
1281
+ code: zod.z.string().optional(),
1282
+ message: zod.z.string().optional(),
1283
+ variant: zod.z.string().optional(),
1284
+ checks: zod.z.array(CheckSchema),
1285
+ claimsSummary: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1286
+ keySource: zod.z.string().optional()
1287
+ });
1288
+ var InspectInputSchema = zod.z.object({
1289
+ jws: zod.z.string().describe("JWS compact serialization to inspect"),
1290
+ full_claims: zod.z.boolean().optional().default(false).describe("Include full decoded claims (may be redacted by policy)")
1291
+ });
1292
+ var MetaSchema2 = zod.z.object({
1293
+ serverVersion: zod.z.string(),
1294
+ policyHash: zod.z.string(),
1295
+ protocolVersion: zod.z.string()
1296
+ });
1297
+ var InspectOutputSchema = zod.z.object({
1298
+ _meta: MetaSchema2,
1299
+ header: zod.z.record(zod.z.string(), zod.z.unknown()),
1300
+ payloadMeta: zod.z.object({
1301
+ variant: zod.z.string(),
1302
+ issuer: zod.z.string().optional(),
1303
+ audience: zod.z.string().optional(),
1304
+ issuedAt: zod.z.string().optional(),
1305
+ expiresAt: zod.z.string().optional(),
1306
+ receiptId: zod.z.string().optional()
1307
+ }),
1308
+ fullPayload: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1309
+ redacted: zod.z.boolean(),
1310
+ verified: zod.z.literal(false)
1311
+ });
1312
+ var DecodeInputSchema = zod.z.object({
1313
+ jws: zod.z.string().describe("JWS compact serialization to decode")
1314
+ });
1315
+ var MetaSchema3 = zod.z.object({
1316
+ serverVersion: zod.z.string(),
1317
+ policyHash: zod.z.string(),
1318
+ protocolVersion: zod.z.string()
1319
+ });
1320
+ var DecodeOutputSchema = zod.z.object({
1321
+ _meta: MetaSchema3,
1322
+ header: zod.z.record(zod.z.string(), zod.z.unknown()),
1323
+ payload: zod.z.record(zod.z.string(), zod.z.unknown()),
1324
+ verified: zod.z.literal(false)
1325
+ });
1326
+ var IssueInputSchema = zod.z.object({
1327
+ aud: zod.z.string().url().describe("Audience / resource URI (https://)"),
1328
+ amt: zod.z.number().int().nonnegative().describe("Amount in smallest currency unit"),
1329
+ cur: zod.z.string().regex(/^[A-Z]{3}$/).describe("ISO 4217 currency code (uppercase, e.g. USD)"),
1330
+ rail: zod.z.string().describe("Payment rail identifier"),
1331
+ reference: zod.z.string().describe("Unique payment reference"),
1332
+ asset: zod.z.string().optional().describe("Asset transferred (e.g. USDC) -- defaults to currency"),
1333
+ env: zod.z.enum(["live", "test"]).default("test").describe("Environment"),
1334
+ network: zod.z.string().optional().describe("Network/chain identifier"),
1335
+ evidence: zod.z.unknown().optional().describe("Rail-specific evidence (JSON-safe)"),
1336
+ subject: zod.z.string().url().optional().describe("Subject URI (https://)"),
1337
+ ttl_seconds: zod.z.number().int().positive().optional().describe("Time-to-live in seconds (computes exp = iat + ttl)"),
1338
+ kind: zod.z.string().optional().describe("Receipt kind for policy filtering")
1339
+ });
1340
+ var MetaSchema4 = zod.z.object({
1341
+ serverVersion: zod.z.string(),
1342
+ policyHash: zod.z.string(),
1343
+ protocolVersion: zod.z.string()
1344
+ });
1345
+ var ClaimsSummarySchema = zod.z.object({
1346
+ iss: zod.z.string(),
1347
+ aud: zod.z.string(),
1348
+ iat: zod.z.number(),
1349
+ exp: zod.z.number().optional(),
1350
+ rid: zod.z.string(),
1351
+ amt: zod.z.number(),
1352
+ cur: zod.z.string()
1353
+ });
1354
+ var IssueOutputSchema = zod.z.object({
1355
+ _meta: MetaSchema4,
1356
+ ok: zod.z.boolean(),
1357
+ jws: zod.z.string(),
1358
+ claimsSummary: ClaimsSummarySchema
1359
+ });
1360
+ var BundleInputSchema = zod.z.object({
1361
+ receipts: zod.z.array(zod.z.string().max(16384)).min(1).max(256).describe("Receipt JWS strings to bundle (1-256)"),
1362
+ metadata: zod.z.record(zod.z.unknown()).optional().describe("Optional metadata to include in the manifest"),
1363
+ output_path: zod.z.string().max(255).optional().describe("Output directory name (single segment within --bundle-dir)")
1364
+ });
1365
+ var MetaSchema5 = zod.z.object({
1366
+ serverVersion: zod.z.string(),
1367
+ policyHash: zod.z.string(),
1368
+ protocolVersion: zod.z.string()
1369
+ });
1370
+ var BundleOutputSchema = zod.z.object({
1371
+ _meta: MetaSchema5,
1372
+ ok: zod.z.boolean(),
1373
+ bundleId: zod.z.string(),
1374
+ bundleName: zod.z.string(),
1375
+ receiptCount: zod.z.number(),
1376
+ fileCount: zod.z.number(),
1377
+ totalBytes: zod.z.number(),
1378
+ createdAt: zod.z.string(),
1379
+ manifestSha256: zod.z.string()
1380
+ });
1381
+
1382
+ // src/server.ts
1383
+ function makeMeta(ctx, serverName) {
1384
+ return {
1385
+ serverName,
1386
+ serverVersion: ctx.version,
1387
+ policyHash: ctx.policyHash,
1388
+ protocolVersion: ctx.protocolVersion
1389
+ };
1390
+ }
1391
+ function handlerResultToCallToolResult(result, meta) {
1392
+ return {
1393
+ content: [{ type: "text", text: result.text }],
1394
+ structuredContent: { _meta: meta, ...result.structured },
1395
+ isError: result.isError
1396
+ };
1397
+ }
1398
+ var PURE_TOOL_ANNOTATIONS = {
1399
+ readOnlyHint: true,
1400
+ destructiveHint: false,
1401
+ idempotentHint: true,
1402
+ openWorldHint: false
1403
+ };
1404
+ var ISSUE_TOOL_ANNOTATIONS = {
1405
+ readOnlyHint: true,
1406
+ destructiveHint: false,
1407
+ idempotentHint: false,
1408
+ openWorldHint: false
1409
+ };
1410
+ var BUNDLE_TOOL_ANNOTATIONS = {
1411
+ readOnlyHint: false,
1412
+ destructiveHint: true,
1413
+ idempotentHint: false,
1414
+ openWorldHint: true
1415
+ };
1416
+ var MAX_INPUT_DEPTH = 10;
1417
+ function errorCallToolResult(meta, code, message) {
1418
+ return {
1419
+ content: [{ type: "text", text: message }],
1420
+ structuredContent: { _meta: meta, ok: false, code, message },
1421
+ isError: true
1422
+ };
1423
+ }
1424
+ function createPeacMcpServer(options) {
1425
+ const { version, policy, context } = options;
1426
+ const registeredTools = ["peac_verify", "peac_inspect", "peac_decode"];
1427
+ if (context.issuerKey && context.issuerId) {
1428
+ registeredTools.push("peac_issue");
1429
+ if (context.bundleDir) {
1430
+ registeredTools.push("peac_create_bundle");
1431
+ }
1432
+ }
1433
+ const meta = { ...makeMeta(context, SERVER_NAME), registeredTools };
1434
+ const maxConcurrency = policy.limits.max_concurrency;
1435
+ let activeHandlers = 0;
1436
+ async function wrapHandler(toolName, args, handlerFn, signal) {
1437
+ if (signal?.aborted) {
1438
+ return errorCallToolResult(meta, "E_MCP_CANCELLED", "Request cancelled");
1439
+ }
1440
+ if (activeHandlers >= maxConcurrency) {
1441
+ return errorCallToolResult(
1442
+ meta,
1443
+ "E_MCP_CONCURRENCY_LIMIT",
1444
+ `Server at capacity (${maxConcurrency} concurrent handlers)`
1445
+ );
1446
+ }
1447
+ if (!checkObjectDepth(args, MAX_INPUT_DEPTH)) {
1448
+ return errorCallToolResult(
1449
+ meta,
1450
+ "E_MCP_INPUT_TOO_DEEP",
1451
+ `Input exceeds maximum nesting depth of ${MAX_INPUT_DEPTH}`
1452
+ );
1453
+ }
1454
+ const inputSizeResult = checkInputSizes(args, policy);
1455
+ if (inputSizeResult) {
1456
+ return handlerResultToCallToolResult(inputSizeResult, meta);
1457
+ }
1458
+ activeHandlers++;
1459
+ const handlerPromise = handlerFn(signal);
1460
+ void handlerPromise.finally(() => {
1461
+ activeHandlers--;
1462
+ }).catch(() => {
1463
+ });
1464
+ let timedOut = false;
1465
+ let cancelled = false;
1466
+ let timer;
1467
+ const timeoutPromise = new Promise((_resolve, reject) => {
1468
+ timer = setTimeout(() => {
1469
+ timedOut = true;
1470
+ reject(new Error(`Tool "${toolName}" timed out after ${policy.limits.tool_timeout_ms}ms`));
1471
+ }, policy.limits.tool_timeout_ms);
1472
+ timer.unref?.();
1473
+ });
1474
+ const raceParticipants = [handlerPromise, timeoutPromise];
1475
+ let abortCleanup;
1476
+ if (signal && !signal.aborted) {
1477
+ const cancelPromise = new Promise((_resolve, reject) => {
1478
+ const onAbort = () => {
1479
+ cancelled = true;
1480
+ reject(new Error("Request cancelled"));
1481
+ };
1482
+ signal.addEventListener("abort", onAbort, { once: true });
1483
+ abortCleanup = () => signal.removeEventListener("abort", onAbort);
1484
+ });
1485
+ raceParticipants.push(cancelPromise);
1486
+ }
1487
+ try {
1488
+ const result = await Promise.race(raceParticipants);
1489
+ const callResult = handlerResultToCallToolResult(result, meta);
1490
+ const outputBytes = measureEnvelopeBytes(callResult);
1491
+ if (outputBytes > policy.limits.max_response_bytes) {
1492
+ return errorCallToolResult(
1493
+ meta,
1494
+ "E_MCP_OUTPUT_TOO_LARGE",
1495
+ `Response is ${outputBytes} bytes, exceeding limit of ${policy.limits.max_response_bytes}`
1496
+ );
1497
+ }
1498
+ return callResult;
1499
+ } catch (err) {
1500
+ const message = err instanceof Error ? err.message : String(err);
1501
+ if (cancelled) {
1502
+ return errorCallToolResult(meta, "E_MCP_CANCELLED", "Request cancelled");
1503
+ }
1504
+ if (timedOut) {
1505
+ return errorCallToolResult(meta, "E_MCP_TOOL_TIMEOUT", message);
1506
+ }
1507
+ return errorCallToolResult(meta, "E_MCP_INTERNAL", message);
1508
+ } finally {
1509
+ clearTimeout(timer);
1510
+ abortCleanup?.();
1511
+ }
1512
+ }
1513
+ const server = new mcp_js.McpServer(
1514
+ { name: SERVER_NAME, version },
1515
+ // listChanged: false -- tool set is static per server instance (determined
1516
+ // at startup from CLI flags). The server never emits notifications/tools/list_changed.
1517
+ { capabilities: { tools: { listChanged: false } } }
1518
+ );
1519
+ const register = server.registerTool.bind(server);
1520
+ register(
1521
+ "peac_verify",
1522
+ {
1523
+ title: "Verify PEAC Receipt",
1524
+ description: "Verify a PEAC receipt JWS signature and validate claims. Returns structured check results.",
1525
+ inputSchema: VerifyInputSchema,
1526
+ outputSchema: VerifyOutputSchema,
1527
+ annotations: PURE_TOOL_ANNOTATIONS
1528
+ },
1529
+ async (args, extra) => wrapHandler(
1530
+ "peac_verify",
1531
+ args,
1532
+ (signal) => handleVerify({ input: args, policy, context, signal }),
1533
+ extra.signal
1534
+ )
1535
+ );
1536
+ register(
1537
+ "peac_inspect",
1538
+ {
1539
+ title: "Inspect PEAC Receipt",
1540
+ description: "Decode and inspect a PEAC receipt without verifying the signature. Shows header, payload metadata, and optionally full claims.",
1541
+ inputSchema: InspectInputSchema,
1542
+ outputSchema: InspectOutputSchema,
1543
+ annotations: PURE_TOOL_ANNOTATIONS
1544
+ },
1545
+ async (args, extra) => wrapHandler(
1546
+ "peac_inspect",
1547
+ args,
1548
+ (signal) => handleInspect({ input: args, policy}),
1549
+ extra.signal
1550
+ )
1551
+ );
1552
+ register(
1553
+ "peac_decode",
1554
+ {
1555
+ title: "Decode PEAC Receipt",
1556
+ description: "Raw decode of a PEAC receipt JWS. Returns header and payload without signature verification.",
1557
+ inputSchema: DecodeInputSchema,
1558
+ outputSchema: DecodeOutputSchema,
1559
+ annotations: PURE_TOOL_ANNOTATIONS
1560
+ },
1561
+ async (args, extra) => wrapHandler(
1562
+ "peac_decode",
1563
+ args,
1564
+ (signal) => handleDecode({ input: args, policy}),
1565
+ extra.signal
1566
+ )
1567
+ );
1568
+ if (context.issuerKey && context.issuerId) {
1569
+ register(
1570
+ "peac_issue",
1571
+ {
1572
+ title: "Issue PEAC Receipt",
1573
+ description: "Sign and return a PEAC receipt JWS. Requires server to be configured with --issuer-key and --issuer-id.",
1574
+ inputSchema: IssueInputSchema,
1575
+ outputSchema: IssueOutputSchema,
1576
+ annotations: ISSUE_TOOL_ANNOTATIONS
1577
+ },
1578
+ async (args, extra) => wrapHandler(
1579
+ "peac_issue",
1580
+ args,
1581
+ (signal) => handleIssue({ input: args, policy, context}),
1582
+ extra.signal
1583
+ )
1584
+ );
1585
+ if (context.bundleDir) {
1586
+ register(
1587
+ "peac_create_bundle",
1588
+ {
1589
+ title: "Create Evidence Bundle",
1590
+ description: "Create a signed evidence bundle directory from receipt JWS strings. Requires --issuer-key, --issuer-id, and --bundle-dir.",
1591
+ inputSchema: BundleInputSchema,
1592
+ outputSchema: BundleOutputSchema,
1593
+ annotations: BUNDLE_TOOL_ANNOTATIONS
1594
+ },
1595
+ async (args, extra) => wrapHandler(
1596
+ "peac_create_bundle",
1597
+ args,
1598
+ (signal) => handleCreateBundle({ input: args, policy, context, signal }),
1599
+ extra.signal
1600
+ )
1601
+ );
1602
+ }
1603
+ }
1604
+ return server;
1605
+ }
1606
+ var program = new commander.Command();
1607
+ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt operations as MCP tools").option("--issuer-key <ref>", "Issuer key reference (env:VAR or file:/path)").option("--issuer-id <uri>", "Issuer identifier URI").option("--policy <path>", "Policy configuration file path").option("--jwks-file <path>", "JWKS file for verifier key resolution").option("--bundle-dir <path>", "Directory for evidence bundle output").action(async (opts) => {
1608
+ if (opts.issuerKey && !opts.issuerId) {
1609
+ process.stderr.write("ERROR: --issuer-key requires --issuer-id\n");
1610
+ process.exit(1);
1611
+ }
1612
+ if (opts.issuerId && !opts.issuerKey) {
1613
+ process.stderr.write("ERROR: --issuer-id requires --issuer-key\n");
1614
+ process.exit(1);
1615
+ }
1616
+ let policy;
1617
+ let policyHash;
1618
+ if (opts.policy) {
1619
+ const loaded = await loadPolicy(opts.policy);
1620
+ policy = loaded.policy;
1621
+ policyHash = loaded.hash;
1622
+ } else {
1623
+ policy = getDefaultPolicy();
1624
+ policyHash = await computePolicyHash(JSON.stringify(policy));
1625
+ }
1626
+ let issuerKey;
1627
+ if (opts.issuerKey) {
1628
+ issuerKey = await loadIssuerKey(opts.issuerKey);
1629
+ }
1630
+ let jwksKeys;
1631
+ if (opts.jwksFile) {
1632
+ jwksKeys = await loadJwksFile(opts.jwksFile);
1633
+ }
1634
+ let bundleDir;
1635
+ if (opts.bundleDir) {
1636
+ try {
1637
+ const s = await promises.stat(opts.bundleDir);
1638
+ if (!s.isDirectory()) {
1639
+ process.stderr.write(`ERROR: --bundle-dir is not a directory: ${opts.bundleDir}
1640
+ `);
1641
+ process.exit(1);
1642
+ }
1643
+ bundleDir = await promises.realpath(opts.bundleDir);
1644
+ } catch (err) {
1645
+ process.stderr.write(
1646
+ `ERROR: --bundle-dir not accessible: ${opts.bundleDir} -- ${err instanceof Error ? err.message : String(err)}
1647
+ `
1648
+ );
1649
+ process.exit(1);
1650
+ }
1651
+ }
1652
+ const context = {
1653
+ version: SERVER_VERSION,
1654
+ policyHash,
1655
+ protocolVersion: MCP_PROTOCOL_VERSION,
1656
+ issuerKey,
1657
+ jwksKeys,
1658
+ issuerId: opts.issuerId,
1659
+ bundleDir
1660
+ };
1661
+ const teardownFence = installStdoutFence();
1662
+ const server = createPeacMcpServer({
1663
+ version: SERVER_VERSION,
1664
+ policy,
1665
+ context
1666
+ });
1667
+ const transport = new stdio_js.StdioServerTransport();
1668
+ const tools = ["peac_verify", "peac_inspect", "peac_decode"];
1669
+ if (issuerKey && opts.issuerId) {
1670
+ tools.push("peac_issue");
1671
+ if (bundleDir) {
1672
+ tools.push("peac_create_bundle");
1673
+ }
1674
+ }
1675
+ process.stderr.write(
1676
+ `[${SERVER_NAME}] v${SERVER_VERSION} | protocol ${MCP_PROTOCOL_VERSION}
1677
+ `
1678
+ );
1679
+ process.stderr.write(` Tools: ${tools.join(", ")}
1680
+ `);
1681
+ process.stderr.write(` Key: ${issuerKey ? "loaded" : "none"}
1682
+ `);
1683
+ process.stderr.write(` JWKS: ${jwksKeys ? `${jwksKeys.length} key(s)` : "none"}
1684
+ `);
1685
+ process.stderr.write(` Bundle dir: ${bundleDir ?? "none"}
1686
+ `);
1687
+ process.stderr.write(` Policy hash: ${policyHash.slice(0, 16)}...
1688
+ `);
1689
+ const shutdown = async () => {
1690
+ process.stderr.write(`[${SERVER_NAME}] Shutting down...
1691
+ `);
1692
+ await server.close();
1693
+ teardownFence();
1694
+ process.exit(0);
1695
+ };
1696
+ process.on("SIGINT", shutdown);
1697
+ process.on("SIGTERM", shutdown);
1698
+ await server.connect(transport);
1699
+ });
1700
+ void program.parseAsync(process.argv).catch((err) => {
1701
+ process.stderr.write(
1702
+ `[${SERVER_NAME}] Fatal: ${err instanceof Error ? err.message : String(err)}
1703
+ `
1704
+ );
1705
+ process.exit(1);
1706
+ });
1707
+ //# sourceMappingURL=cli.cjs.map
1708
+ //# sourceMappingURL=cli.cjs.map