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