@peac/mcp-server 0.10.13 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,20 +1,533 @@
1
1
  #!/usr/bin/env node
2
+ import { randomUUID } from 'crypto';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ import { createServer } from 'http';
2
5
  import { stat, realpath, readFile, mkdir, writeFile, rm, mkdtemp, rename, lstat } from 'fs/promises';
3
6
  import { Command } from 'commander';
4
7
  import { z } from 'zod';
5
8
  import { sha256Hex, validateKeypair, base64urlDecode, derivePublicKey, base64urlEncode, decode, sign } from '@peac/crypto';
6
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { ERRORS } from '@peac/kernel';
7
11
  import { verifyLocal, issue, IssueError } from '@peac/protocol';
8
12
  import { parseReceiptClaims } from '@peac/schema';
9
13
  import * as nodePath from 'path';
10
14
  import { join } from 'path';
11
- import { randomUUID } from 'crypto';
12
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+
16
+ var __defProp = Object.defineProperty;
17
+ var __getOwnPropNames = Object.getOwnPropertyNames;
18
+ var __esm = (fn, res) => function __init() {
19
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
20
+ };
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
24
+ };
13
25
 
14
26
  // src/infra/constants.ts
15
- var SERVER_NAME = "peac-mcp-server";
16
- var SERVER_VERSION = "0.10.12";
17
- var MCP_PROTOCOL_VERSION = "2025-11-25";
27
+ var SERVER_NAME, SERVER_VERSION, MCP_PROTOCOL_VERSION;
28
+ var init_constants = __esm({
29
+ "src/infra/constants.ts"() {
30
+ SERVER_NAME = "peac-mcp-server";
31
+ SERVER_VERSION = "0.11.2";
32
+ MCP_PROTOCOL_VERSION = "2025-11-25";
33
+ }
34
+ });
35
+ var SessionManager;
36
+ var init_session_manager = __esm({
37
+ "src/session-manager.ts"() {
38
+ SessionManager = class {
39
+ constructor(options = {}) {
40
+ this.options = options;
41
+ this.ttlMs = options.ttlMs ?? 30 * 60 * 1e3;
42
+ this.maxSessions = options.maxSessions ?? 100;
43
+ this.maxSessionsPerIp = options.maxSessionsPerIp ?? 10;
44
+ }
45
+ sessions = /* @__PURE__ */ new Map();
46
+ ipSessionCount = /* @__PURE__ */ new Map();
47
+ ttlMs;
48
+ maxSessions;
49
+ maxSessionsPerIp;
50
+ sweepTimer;
51
+ /** Start periodic eviction sweep */
52
+ startSweep() {
53
+ const intervalMs = this.options.sweepIntervalMs ?? 6e4;
54
+ this.sweepTimer = setInterval(() => this.evictStale(), intervalMs);
55
+ this.sweepTimer.unref();
56
+ }
57
+ /** Create a new isolated session with its own McpServer + transport */
58
+ async createSession(serverFactory, clientIp = "unknown") {
59
+ this.evictStale();
60
+ if (this.sessions.size >= this.maxSessions) {
61
+ throw new Error(`Session limit reached (${this.maxSessions}). Try again later.`);
62
+ }
63
+ const ipCount = this.ipSessionCount.get(clientIp) ?? 0;
64
+ if (ipCount >= this.maxSessionsPerIp) {
65
+ throw new Error(
66
+ `Per-IP session limit reached (${this.maxSessionsPerIp} for ${clientIp}). Try again later.`
67
+ );
68
+ }
69
+ const sessionId = randomUUID();
70
+ const server = serverFactory();
71
+ const transport = new StreamableHTTPServerTransport({
72
+ sessionIdGenerator: () => sessionId
73
+ });
74
+ await server.connect(transport);
75
+ const now = Date.now();
76
+ const entry = {
77
+ sessionId,
78
+ server,
79
+ transport,
80
+ clientIp,
81
+ createdAt: now,
82
+ lastSeen: now
83
+ };
84
+ this.sessions.set(sessionId, entry);
85
+ this.ipSessionCount.set(clientIp, ipCount + 1);
86
+ return entry;
87
+ }
88
+ /** Look up an existing session by ID */
89
+ getSession(sessionId) {
90
+ const entry = this.sessions.get(sessionId);
91
+ if (entry) {
92
+ entry.lastSeen = Date.now();
93
+ }
94
+ return entry;
95
+ }
96
+ /** Terminate and clean up a specific session */
97
+ async terminateSession(sessionId) {
98
+ const entry = this.sessions.get(sessionId);
99
+ if (!entry) return false;
100
+ this.sessions.delete(sessionId);
101
+ this.decrementIpCount(entry.clientIp);
102
+ try {
103
+ await entry.transport.close();
104
+ await entry.server.close();
105
+ } catch {
106
+ }
107
+ return true;
108
+ }
109
+ /** Terminate all sessions and stop sweep */
110
+ async cleanup() {
111
+ if (this.sweepTimer) {
112
+ clearInterval(this.sweepTimer);
113
+ this.sweepTimer = void 0;
114
+ }
115
+ const entries = [...this.sessions.values()];
116
+ this.sessions.clear();
117
+ this.ipSessionCount.clear();
118
+ await Promise.allSettled(
119
+ entries.map(async (entry) => {
120
+ try {
121
+ await entry.transport.close();
122
+ await entry.server.close();
123
+ } catch {
124
+ }
125
+ })
126
+ );
127
+ }
128
+ /** Get current session count */
129
+ get size() {
130
+ return this.sessions.size;
131
+ }
132
+ /** Evict sessions past TTL */
133
+ evictStale() {
134
+ const now = Date.now();
135
+ for (const [id, entry] of this.sessions) {
136
+ if (now - entry.lastSeen > this.ttlMs) {
137
+ this.sessions.delete(id);
138
+ this.decrementIpCount(entry.clientIp);
139
+ void entry.transport.close().catch(() => {
140
+ });
141
+ void entry.server.close().catch(() => {
142
+ });
143
+ }
144
+ }
145
+ }
146
+ /** Decrement per-IP session count, removing the key when it reaches 0 */
147
+ decrementIpCount(ip) {
148
+ const count = this.ipSessionCount.get(ip) ?? 0;
149
+ if (count <= 1) {
150
+ this.ipSessionCount.delete(ip);
151
+ } else {
152
+ this.ipSessionCount.set(ip, count - 1);
153
+ }
154
+ }
155
+ };
156
+ }
157
+ });
158
+
159
+ // src/http-transport.ts
160
+ var http_transport_exports = {};
161
+ __export(http_transport_exports, {
162
+ createHttpTransport: () => createHttpTransport
163
+ });
164
+ function isValidSessionId(id) {
165
+ if (id.length === 0 || id.length > MAX_SESSION_ID_LENGTH) return false;
166
+ for (let i = 0; i < id.length; i++) {
167
+ const code = id.charCodeAt(i);
168
+ if (code < 33 || code > 126) return false;
169
+ }
170
+ return true;
171
+ }
172
+ function isLoopbackAddr(addr) {
173
+ return addr.startsWith("127.") || addr === "::1" || addr === "::ffff:127.0.0.1";
174
+ }
175
+ function isLinkLocalAddr(addr) {
176
+ if (isLoopbackAddr(addr)) return true;
177
+ return addr.startsWith("169.254.") || addr.startsWith("fe80");
178
+ }
179
+ function isPrivateAddr(addr) {
180
+ if (isLinkLocalAddr(addr)) return true;
181
+ return addr.startsWith("10.") || addr.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(addr) || addr.startsWith("fc") || addr.startsWith("fd") || addr.startsWith("::ffff:10.") || addr.startsWith("::ffff:192.168.") || /^::ffff:172\.(1[6-9]|2\d|3[01])\./.test(addr);
182
+ }
183
+ function isTrustedProxy(remoteAddr, trustProxy) {
184
+ if (trustProxy === "off") return false;
185
+ if (trustProxy === "all") return true;
186
+ if (trustProxy === "loopback") return isLoopbackAddr(remoteAddr);
187
+ if (trustProxy === "linklocal") return isLinkLocalAddr(remoteAddr);
188
+ if (trustProxy === "private") return isPrivateAddr(remoteAddr);
189
+ const trusted = trustProxy.split(",").map((s) => s.trim());
190
+ return trusted.includes(remoteAddr);
191
+ }
192
+ function validatePublicUrl(rawUrl) {
193
+ let u;
194
+ try {
195
+ u = new URL(rawUrl);
196
+ } catch {
197
+ return { error: `Invalid URL: ${rawUrl}` };
198
+ }
199
+ if (u.hash) {
200
+ return { error: "Public URL must not contain fragments (#)" };
201
+ }
202
+ const isLoopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
203
+ if (u.protocol !== "https:" && !isLoopback) {
204
+ return { error: "Public URL must use https (except for localhost/loopback dev usage)" };
205
+ }
206
+ const normalized = u.href.replace(/\/+$/, "") || u.origin;
207
+ return { url: u, normalized };
208
+ }
209
+ async function createHttpTransport(options) {
210
+ const port = options.port ?? 3e3;
211
+ const host = options.host ?? "127.0.0.1";
212
+ const corsOrigins = new Set(options.corsOrigins ?? []);
213
+ const maxRequestBytes = options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES;
214
+ const rateLimitRpm = options.rateLimitRpm ?? DEFAULT_RATE_LIMIT_RPM;
215
+ const sessionManager = new SessionManager({
216
+ ttlMs: options.sessionTtlMs,
217
+ maxSessions: options.maxSessions,
218
+ maxSessionsPerIp: options.maxSessionsPerIp
219
+ });
220
+ sessionManager.startSweep();
221
+ const rateLimits = /* @__PURE__ */ new Map();
222
+ function checkRateLimit(key) {
223
+ const now = Date.now();
224
+ let entry = rateLimits.get(key);
225
+ if (!entry || now - entry.windowStart > ONE_MINUTE_MS) {
226
+ entry = { count: 0, windowStart: now };
227
+ rateLimits.set(key, entry);
228
+ }
229
+ entry.count++;
230
+ return entry.count <= rateLimitRpm;
231
+ }
232
+ const trustProxyValue = options.trustProxy ?? "off";
233
+ function getClientIp(req) {
234
+ const peerIp = req.socket.remoteAddress ?? "unknown";
235
+ if (trustProxyValue !== "off" && isTrustedProxy(peerIp, trustProxyValue)) {
236
+ const xff = req.headers["x-forwarded-for"];
237
+ if (typeof xff === "string") {
238
+ const first = xff.split(",")[0]?.trim();
239
+ if (first) return first;
240
+ }
241
+ }
242
+ return peerIp;
243
+ }
244
+ const allowedHosts = /* @__PURE__ */ new Set();
245
+ allowedHosts.add(`${host}:${port}`);
246
+ allowedHosts.add(host === "0.0.0.0" ? `localhost:${port}` : `${host}:${port}`);
247
+ if (host === "127.0.0.1" || host === "0.0.0.0") {
248
+ allowedHosts.add(`localhost:${port}`);
249
+ allowedHosts.add(`127.0.0.1:${port}`);
250
+ }
251
+ if (options.publicUrl) {
252
+ try {
253
+ const u = new URL(options.publicUrl);
254
+ allowedHosts.add(u.host);
255
+ } catch {
256
+ }
257
+ }
258
+ const allowedOrigins = new Set(corsOrigins);
259
+ function validateOrigin(req, res) {
260
+ const origin = req.headers.origin;
261
+ if (!origin) return true;
262
+ if (allowedOrigins.size === 0) {
263
+ res.writeHead(403, { "Content-Type": "application/json" });
264
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
265
+ return false;
266
+ }
267
+ if (!allowedOrigins.has(origin)) {
268
+ res.writeHead(403, { "Content-Type": "application/json" });
269
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
270
+ return false;
271
+ }
272
+ return true;
273
+ }
274
+ function setCorsHeaders(req, res) {
275
+ const origin = req.headers.origin;
276
+ if (origin && allowedOrigins.has(origin)) {
277
+ res.setHeader("Access-Control-Allow-Origin", origin);
278
+ res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, GET, OPTIONS");
279
+ res.setHeader(
280
+ "Access-Control-Allow-Headers",
281
+ "Content-Type, Mcp-Session-Id, Accept, MCP-Protocol-Version"
282
+ );
283
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
284
+ }
285
+ }
286
+ function sendJson(res, status, body) {
287
+ const json = JSON.stringify(body);
288
+ res.writeHead(status, { "Content-Type": "application/json" });
289
+ res.end(json);
290
+ }
291
+ async function readBody(req) {
292
+ const chunks = [];
293
+ let totalBytes = 0;
294
+ for await (const chunk of req) {
295
+ totalBytes += chunk.length;
296
+ if (totalBytes > maxRequestBytes) {
297
+ throw new Error(`Request body exceeds ${maxRequestBytes} bytes`);
298
+ }
299
+ chunks.push(chunk);
300
+ }
301
+ return Buffer.concat(chunks);
302
+ }
303
+ const authServers = (options.authorizationServers ?? []).filter((s) => s.trim().length > 0);
304
+ if (options.authorizationServers?.length && authServers.length === 0) {
305
+ process.stderr.write(
306
+ `[${SERVER_NAME}] WARNING: --authorization-servers contains only empty entries, PRM disabled
307
+ `
308
+ );
309
+ }
310
+ if (authServers.length > 0 && !options.publicUrl) {
311
+ process.stderr.write(
312
+ `[${SERVER_NAME}] WARNING: --authorization-servers set without --public-url, PRM disabled
313
+ `
314
+ );
315
+ }
316
+ if (options.publicUrl && authServers.length === 0) {
317
+ process.stderr.write(
318
+ `[${SERVER_NAME}] WARNING: --public-url set without --authorization-servers, PRM disabled
319
+ `
320
+ );
321
+ }
322
+ const prmEnabled = !!(authServers.length > 0 && options.publicUrl);
323
+ let prmPath = "/.well-known/oauth-protected-resource";
324
+ let prmDocument;
325
+ if (prmEnabled && options.publicUrl) {
326
+ const validation = validatePublicUrl(options.publicUrl);
327
+ if ("error" in validation) {
328
+ process.stderr.write(
329
+ `[${SERVER_NAME}] WARNING: --public-url invalid, PRM disabled: ${validation.error}
330
+ `
331
+ );
332
+ } else {
333
+ const { url: u, normalized } = validation;
334
+ const resourcePath = u.pathname.replace(/\/+$/, "");
335
+ if (resourcePath && resourcePath !== "/") {
336
+ prmPath = `/.well-known/oauth-protected-resource${resourcePath}`;
337
+ }
338
+ prmDocument = {
339
+ resource: normalized,
340
+ authorization_servers: authServers
341
+ };
342
+ }
343
+ }
344
+ const server = createServer(async (req, res) => {
345
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
346
+ const pathname = url.pathname;
347
+ res.setTimeout(3e4);
348
+ if (req.method === "OPTIONS") {
349
+ setCorsHeaders(req, res);
350
+ res.writeHead(204);
351
+ res.end();
352
+ return;
353
+ }
354
+ setCorsHeaders(req, res);
355
+ const hostHeader = req.headers.host;
356
+ if (hostHeader && !allowedHosts.has(hostHeader)) {
357
+ sendJson(res, 400, { error: "Invalid Host header" });
358
+ return;
359
+ }
360
+ if (!validateOrigin(req, res)) return;
361
+ if (pathname === "/health" && req.method === "GET") {
362
+ sendJson(res, 200, {
363
+ ok: true,
364
+ version: SERVER_VERSION,
365
+ protocolVersion: MCP_PROTOCOL_VERSION
366
+ });
367
+ return;
368
+ }
369
+ if (pathname === prmPath && req.method === "GET") {
370
+ if (prmEnabled && prmDocument) {
371
+ sendJson(res, 200, prmDocument);
372
+ } else {
373
+ sendJson(res, 404, { error: "Not found" });
374
+ }
375
+ return;
376
+ }
377
+ if (pathname === "/mcp") {
378
+ if (req.method === "GET") {
379
+ res.writeHead(405, { Allow: "POST, DELETE", "Content-Type": "application/json" });
380
+ res.end(JSON.stringify({ error: "Method Not Allowed. Use POST for JSON-RPC requests." }));
381
+ return;
382
+ }
383
+ if (req.method === "DELETE") {
384
+ const sessionId = req.headers["mcp-session-id"];
385
+ if (!sessionId) {
386
+ sendJson(res, 400, { error: "Missing Mcp-Session-Id header" });
387
+ return;
388
+ }
389
+ if (!isValidSessionId(sessionId)) {
390
+ sendJson(res, 400, {
391
+ error: "Invalid Mcp-Session-Id: must be visible ASCII (0x21-0x7E), max 128 chars"
392
+ });
393
+ return;
394
+ }
395
+ const terminated = await sessionManager.terminateSession(sessionId);
396
+ if (terminated) {
397
+ sendJson(res, 200, { ok: true });
398
+ } else {
399
+ sendJson(res, 404, { error: "Session not found or already terminated" });
400
+ }
401
+ return;
402
+ }
403
+ if (req.method === "POST") {
404
+ const sessionId = req.headers["mcp-session-id"];
405
+ const rateLimitKey = sessionId ?? `ip:${getClientIp(req)}`;
406
+ if (!checkRateLimit(rateLimitKey)) {
407
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": "60" });
408
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
409
+ return;
410
+ }
411
+ let body;
412
+ try {
413
+ const raw = await readBody(req);
414
+ body = JSON.parse(raw.toString("utf-8"));
415
+ } catch (err) {
416
+ const msg = err instanceof Error ? err.message : "Invalid request body";
417
+ sendJson(res, 400, { error: msg });
418
+ return;
419
+ }
420
+ const isInit = typeof body === "object" && body !== null && "method" in body && body.method === "initialize";
421
+ if (isInit) {
422
+ const protocolVersion = req.headers["mcp-protocol-version"];
423
+ if (protocolVersion && protocolVersion !== MCP_PROTOCOL_VERSION && protocolVersion !== "2025-03-26") {
424
+ sendJson(res, 400, { error: `Unsupported MCP protocol version: ${protocolVersion}` });
425
+ return;
426
+ }
427
+ const accept = req.headers.accept;
428
+ if (!accept) {
429
+ process.stderr.write(
430
+ `[${SERVER_NAME}] Warning: missing Accept header on init request
431
+ `
432
+ );
433
+ }
434
+ let entry2;
435
+ try {
436
+ entry2 = await sessionManager.createSession(options.serverFactory, getClientIp(req));
437
+ } catch (err) {
438
+ const msg = err instanceof Error ? err.message : "Failed to create session";
439
+ sendJson(res, 503, { error: msg });
440
+ return;
441
+ }
442
+ await entry2.transport.handleRequest(req, res, body);
443
+ return;
444
+ }
445
+ if (!sessionId) {
446
+ sendJson(res, 400, { error: "Missing Mcp-Session-Id header" });
447
+ return;
448
+ }
449
+ if (!isValidSessionId(sessionId)) {
450
+ sendJson(res, 400, {
451
+ error: "Invalid Mcp-Session-Id: must be visible ASCII (0x21-0x7E), max 128 chars"
452
+ });
453
+ return;
454
+ }
455
+ const entry = sessionManager.getSession(sessionId);
456
+ if (!entry) {
457
+ sendJson(res, 404, { error: "Session not found or terminated" });
458
+ return;
459
+ }
460
+ await entry.transport.handleRequest(req, res, body);
461
+ return;
462
+ }
463
+ res.writeHead(405, { Allow: "POST, DELETE", "Content-Type": "application/json" });
464
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
465
+ return;
466
+ }
467
+ sendJson(res, 404, { error: "Not found" });
468
+ });
469
+ server.headersTimeout = 1e4;
470
+ server.requestTimeout = 3e4;
471
+ server.keepAliveTimeout = 5e3;
472
+ return new Promise((resolve2, reject) => {
473
+ server.on("error", reject);
474
+ server.listen(port, host, () => {
475
+ process.stderr.write(`[${SERVER_NAME}] HTTP transport listening on http://${host}:${port}
476
+ `);
477
+ process.stderr.write(` Transport: Streamable HTTP (MCP 2025-06-18)
478
+ `);
479
+ process.stderr.write(
480
+ ` Auth: unprotected mode (no token validation); PRM discovery per RFC 9728
481
+ `
482
+ );
483
+ process.stderr.write(` Endpoints: POST /mcp, DELETE /mcp, GET /health
484
+ `);
485
+ if (prmEnabled) {
486
+ process.stderr.write(` PRM: GET ${prmPath} (implemented, enabled by config)
487
+ `);
488
+ } else {
489
+ process.stderr.write(
490
+ ` PRM: disabled (set --authorization-servers + --public-url to enable)
491
+ `
492
+ );
493
+ }
494
+ process.stderr.write(
495
+ ` Sessions: max ${options.maxSessions ?? 100} (${options.maxSessionsPerIp ?? 10}/IP), TTL ${(options.sessionTtlMs ?? 18e5) / 1e3}s
496
+ `
497
+ );
498
+ process.stderr.write(` Rate limit: ${rateLimitRpm} req/min per session
499
+ `);
500
+ process.stderr.write(
501
+ ` CORS: ${corsOrigins.size > 0 ? [...corsOrigins].join(", ") : "deny all"}
502
+ `
503
+ );
504
+ process.stderr.write(
505
+ ` Mode: unprotected (token validation deferred to v0.11.x+; deployer auth via reverse proxy)
506
+ `
507
+ );
508
+ resolve2({
509
+ cleanup: async () => {
510
+ await sessionManager.cleanup();
511
+ await new Promise((res) => server.close(() => res()));
512
+ }
513
+ });
514
+ });
515
+ });
516
+ }
517
+ var ONE_MINUTE_MS, DEFAULT_MAX_REQUEST_BYTES, DEFAULT_RATE_LIMIT_RPM, MAX_SESSION_ID_LENGTH;
518
+ var init_http_transport = __esm({
519
+ "src/http-transport.ts"() {
520
+ init_session_manager();
521
+ init_constants();
522
+ ONE_MINUTE_MS = 6e4;
523
+ DEFAULT_MAX_REQUEST_BYTES = 1048576;
524
+ DEFAULT_RATE_LIMIT_RPM = 100;
525
+ MAX_SESSION_ID_LENGTH = 128;
526
+ }
527
+ });
528
+
529
+ // src/cli.ts
530
+ init_constants();
18
531
 
19
532
  // src/infra/errors.ts
20
533
  var McpServerError = class extends Error {
@@ -83,9 +596,9 @@ var JwksConfigSchema = z.object({
83
596
  var PolicySchema = z.object({
84
597
  version: z.literal("1"),
85
598
  allow_network: z.boolean().default(false),
86
- redaction: RedactionSchema.default({}),
87
- tools: z.record(z.string(), ToolPolicySchema).default({}),
88
- limits: LimitsSchema.default({}),
599
+ redaction: RedactionSchema.prefault({}),
600
+ tools: z.record(z.string(), ToolPolicySchema).prefault({}),
601
+ limits: LimitsSchema.prefault({}),
89
602
  jwks: JwksConfigSchema.optional()
90
603
  });
91
604
  var DEFAULT_POLICY = {
@@ -361,6 +874,9 @@ function installStdoutFence() {
361
874
  };
362
875
  }
363
876
 
877
+ // src/server.ts
878
+ init_constants();
879
+
364
880
  // src/handlers/guards.ts
365
881
  function checkJwsSize(jws, policy) {
366
882
  const byteLength = new TextEncoder().encode(jws).length;
@@ -966,10 +1482,40 @@ async function handleIssue(params) {
966
1482
  };
967
1483
  }
968
1484
  }
1485
+ var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
1486
+ "con",
1487
+ "prn",
1488
+ "aux",
1489
+ "nul",
1490
+ "com1",
1491
+ "com2",
1492
+ "com3",
1493
+ "com4",
1494
+ "com5",
1495
+ "com6",
1496
+ "com7",
1497
+ "com8",
1498
+ "com9",
1499
+ "lpt1",
1500
+ "lpt2",
1501
+ "lpt3",
1502
+ "lpt4",
1503
+ "lpt5",
1504
+ "lpt6",
1505
+ "lpt7",
1506
+ "lpt8",
1507
+ "lpt9"
1508
+ ]);
969
1509
  function assertRelativePath(p) {
970
1510
  if (!p || p.trim().length === 0) {
971
1511
  throw new PathTraversalError("Path must not be empty");
972
1512
  }
1513
+ if (p.includes("\0")) {
1514
+ throw new PathTraversalError("Null bytes are not allowed in paths");
1515
+ }
1516
+ if (p.includes(":")) {
1517
+ throw new PathTraversalError("Colons are not allowed in paths (drive letters, NTFS ADS)");
1518
+ }
973
1519
  if (nodePath.isAbsolute(p)) {
974
1520
  throw new PathTraversalError("Absolute paths are not allowed");
975
1521
  }
@@ -986,6 +1532,14 @@ function assertRelativePath(p) {
986
1532
  if (p.startsWith(".")) {
987
1533
  throw new PathTraversalError("Hidden paths (starting with .) are not allowed");
988
1534
  }
1535
+ if (p.endsWith(".") || p.endsWith(" ")) {
1536
+ throw new PathTraversalError("Trailing dots and spaces are not allowed");
1537
+ }
1538
+ const dotIndex = p.indexOf(".");
1539
+ const baseName = (dotIndex >= 0 ? p.slice(0, dotIndex) : p).toLowerCase();
1540
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
1541
+ throw new PathTraversalError(`Windows reserved device name: ${baseName.toUpperCase()}`);
1542
+ }
989
1543
  }
990
1544
  async function assertNotSymlink(componentPath) {
991
1545
  try {
@@ -1017,6 +1571,7 @@ async function createTempDir(basePath) {
1017
1571
  }
1018
1572
 
1019
1573
  // src/handlers/bundle.ts
1574
+ init_constants();
1020
1575
  function stableJsonStringify(value) {
1021
1576
  return JSON.stringify(
1022
1577
  value,
@@ -1338,7 +1893,7 @@ var IssueOutputSchema = z.object({
1338
1893
  });
1339
1894
  var BundleInputSchema = z.object({
1340
1895
  receipts: z.array(z.string().max(16384)).min(1).max(256).describe("Receipt JWS strings to bundle (1-256)"),
1341
- metadata: z.record(z.unknown()).optional().describe("Optional metadata to include in the manifest"),
1896
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Optional metadata to include in the manifest"),
1342
1897
  output_path: z.string().max(255).optional().describe("Output directory name (single segment within --bundle-dir)")
1343
1898
  });
1344
1899
  var MetaSchema5 = z.object({
@@ -1394,9 +1949,17 @@ var BUNDLE_TOOL_ANNOTATIONS = {
1394
1949
  };
1395
1950
  var MAX_INPUT_DEPTH = 10;
1396
1951
  function errorCallToolResult(meta, code, message) {
1952
+ const errorDef = ERRORS[code];
1397
1953
  return {
1398
1954
  content: [{ type: "text", text: message }],
1399
- structuredContent: { _meta: meta, ok: false, code, message },
1955
+ structuredContent: {
1956
+ _meta: meta,
1957
+ ok: false,
1958
+ code,
1959
+ message,
1960
+ retryable: errorDef?.retryable ?? false,
1961
+ next_action: errorDef?.next_action ?? "none"
1962
+ },
1400
1963
  isError: true
1401
1964
  };
1402
1965
  }
@@ -1582,8 +2145,25 @@ function createPeacMcpServer(options) {
1582
2145
  }
1583
2146
  return server;
1584
2147
  }
2148
+
2149
+ // src/cli.ts
1585
2150
  var program = new Command();
1586
- 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) => {
2151
+ 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").option("--transport <type>", "Transport: stdio (default) or http", "stdio").option("--port <number>", "HTTP port (default: 3000, http only)", "3000").option("--host <address>", "HTTP bind address (default: 127.0.0.1)", "127.0.0.1").option("--cors-origins <list>", "Allowed CORS origins (comma-separated)").option(
2152
+ "--authorization-servers <list>",
2153
+ "OAuth authorization server URIs (enables PRM with --public-url)"
2154
+ ).option("--public-url <url>", "Canonical public URL of this server (required for PRM)").option(
2155
+ "--trust-proxy <value>",
2156
+ "Trust X-Forwarded-For: off (default), loopback, linklocal, private, all (discouraged), or comma-separated IPs",
2157
+ "off"
2158
+ ).action(async (opts) => {
2159
+ const transportType = opts.transport;
2160
+ if (transportType !== "stdio" && transportType !== "http") {
2161
+ process.stderr.write(
2162
+ `ERROR: --transport must be "stdio" or "http", got "${transportType}"
2163
+ `
2164
+ );
2165
+ process.exit(1);
2166
+ }
1587
2167
  if (opts.issuerKey && !opts.issuerId) {
1588
2168
  process.stderr.write("ERROR: --issuer-key requires --issuer-id\n");
1589
2169
  process.exit(1);
@@ -1637,13 +2217,6 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1637
2217
  issuerId: opts.issuerId,
1638
2218
  bundleDir
1639
2219
  };
1640
- const teardownFence = installStdoutFence();
1641
- const server = createPeacMcpServer({
1642
- version: SERVER_VERSION,
1643
- policy,
1644
- context
1645
- });
1646
- const transport = new StdioServerTransport();
1647
2220
  const tools = ["peac_verify", "peac_inspect", "peac_decode"];
1648
2221
  if (issuerKey && opts.issuerId) {
1649
2222
  tools.push("peac_issue");
@@ -1651,10 +2224,17 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1651
2224
  tools.push("peac_create_bundle");
1652
2225
  }
1653
2226
  }
2227
+ const serverOptions = {
2228
+ version: SERVER_VERSION,
2229
+ policy,
2230
+ context
2231
+ };
1654
2232
  process.stderr.write(
1655
2233
  `[${SERVER_NAME}] v${SERVER_VERSION} | protocol ${MCP_PROTOCOL_VERSION}
1656
2234
  `
1657
2235
  );
2236
+ process.stderr.write(` Transport: ${transportType}
2237
+ `);
1658
2238
  process.stderr.write(` Tools: ${tools.join(", ")}
1659
2239
  `);
1660
2240
  process.stderr.write(` Key: ${issuerKey ? "loaded" : "none"}
@@ -1665,16 +2245,43 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1665
2245
  `);
1666
2246
  process.stderr.write(` Policy hash: ${policyHash.slice(0, 16)}...
1667
2247
  `);
1668
- const shutdown = async () => {
1669
- process.stderr.write(`[${SERVER_NAME}] Shutting down...
2248
+ if (transportType === "http") {
2249
+ const { createHttpTransport: createHttpTransport2 } = await Promise.resolve().then(() => (init_http_transport(), http_transport_exports));
2250
+ const corsOrigins = opts.corsOrigins ? opts.corsOrigins.split(",").map((s) => s.trim()) : void 0;
2251
+ const authorizationServers = opts.authorizationServers ? opts.authorizationServers.split(",").map((s) => s.trim()) : void 0;
2252
+ const { cleanup } = await createHttpTransport2({
2253
+ port: parseInt(opts.port, 10),
2254
+ host: opts.host,
2255
+ corsOrigins,
2256
+ authorizationServers,
2257
+ publicUrl: opts.publicUrl,
2258
+ trustProxy: opts.trustProxy || "off",
2259
+ serverFactory: () => createPeacMcpServer(serverOptions)
2260
+ });
2261
+ const shutdown = async () => {
2262
+ process.stderr.write(`[${SERVER_NAME}] Shutting down...
1670
2263
  `);
1671
- await server.close();
1672
- teardownFence();
1673
- process.exit(0);
1674
- };
1675
- process.on("SIGINT", shutdown);
1676
- process.on("SIGTERM", shutdown);
1677
- await server.connect(transport);
2264
+ await cleanup();
2265
+ process.exit(0);
2266
+ };
2267
+ process.on("SIGINT", shutdown);
2268
+ process.on("SIGTERM", shutdown);
2269
+ } else {
2270
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
2271
+ const teardownFence = installStdoutFence();
2272
+ const server = createPeacMcpServer(serverOptions);
2273
+ const transport = new StdioServerTransport();
2274
+ const shutdown = async () => {
2275
+ process.stderr.write(`[${SERVER_NAME}] Shutting down...
2276
+ `);
2277
+ await server.close();
2278
+ teardownFence();
2279
+ process.exit(0);
2280
+ };
2281
+ process.on("SIGINT", shutdown);
2282
+ process.on("SIGTERM", shutdown);
2283
+ await server.connect(transport);
2284
+ }
1678
2285
  });
1679
2286
  void program.parseAsync(process.argv).catch((err) => {
1680
2287
  process.stderr.write(