@peac/mcp-server 0.10.13 → 0.11.3

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.cjs CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var crypto$1 = require('crypto');
5
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
6
+ var http = require('http');
4
7
  var promises = require('fs/promises');
5
8
  var commander = require('commander');
6
9
  var zod = require('zod');
7
10
  var crypto = require('@peac/crypto');
8
11
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
12
+ var kernel = require('@peac/kernel');
9
13
  var protocol = require('@peac/protocol');
10
14
  var schema = require('@peac/schema');
11
15
  var nodePath = require('path');
12
- var crypto$1 = require('crypto');
13
- var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
14
16
 
15
17
  function _interopNamespace(e) {
16
18
  if (e && e.__esModule) return e;
@@ -32,10 +34,521 @@ function _interopNamespace(e) {
32
34
 
33
35
  var nodePath__namespace = /*#__PURE__*/_interopNamespace(nodePath);
34
36
 
37
+ var __defProp = Object.defineProperty;
38
+ var __getOwnPropNames = Object.getOwnPropertyNames;
39
+ var __esm = (fn, res) => function __init() {
40
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
41
+ };
42
+ var __export = (target, all) => {
43
+ for (var name in all)
44
+ __defProp(target, name, { get: all[name], enumerable: true });
45
+ };
46
+
35
47
  // 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";
48
+ var SERVER_NAME, SERVER_VERSION, MCP_PROTOCOL_VERSION;
49
+ var init_constants = __esm({
50
+ "src/infra/constants.ts"() {
51
+ SERVER_NAME = "peac-mcp-server";
52
+ SERVER_VERSION = "0.11.2";
53
+ MCP_PROTOCOL_VERSION = "2025-11-25";
54
+ }
55
+ });
56
+ var SessionManager;
57
+ var init_session_manager = __esm({
58
+ "src/session-manager.ts"() {
59
+ SessionManager = class {
60
+ constructor(options = {}) {
61
+ this.options = options;
62
+ this.ttlMs = options.ttlMs ?? 30 * 60 * 1e3;
63
+ this.maxSessions = options.maxSessions ?? 100;
64
+ this.maxSessionsPerIp = options.maxSessionsPerIp ?? 10;
65
+ }
66
+ sessions = /* @__PURE__ */ new Map();
67
+ ipSessionCount = /* @__PURE__ */ new Map();
68
+ ttlMs;
69
+ maxSessions;
70
+ maxSessionsPerIp;
71
+ sweepTimer;
72
+ /** Start periodic eviction sweep */
73
+ startSweep() {
74
+ const intervalMs = this.options.sweepIntervalMs ?? 6e4;
75
+ this.sweepTimer = setInterval(() => this.evictStale(), intervalMs);
76
+ this.sweepTimer.unref();
77
+ }
78
+ /** Create a new isolated session with its own McpServer + transport */
79
+ async createSession(serverFactory, clientIp = "unknown") {
80
+ this.evictStale();
81
+ if (this.sessions.size >= this.maxSessions) {
82
+ throw new Error(`Session limit reached (${this.maxSessions}). Try again later.`);
83
+ }
84
+ const ipCount = this.ipSessionCount.get(clientIp) ?? 0;
85
+ if (ipCount >= this.maxSessionsPerIp) {
86
+ throw new Error(
87
+ `Per-IP session limit reached (${this.maxSessionsPerIp} for ${clientIp}). Try again later.`
88
+ );
89
+ }
90
+ const sessionId = crypto$1.randomUUID();
91
+ const server = serverFactory();
92
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
93
+ sessionIdGenerator: () => sessionId
94
+ });
95
+ await server.connect(transport);
96
+ const now = Date.now();
97
+ const entry = {
98
+ sessionId,
99
+ server,
100
+ transport,
101
+ clientIp,
102
+ createdAt: now,
103
+ lastSeen: now
104
+ };
105
+ this.sessions.set(sessionId, entry);
106
+ this.ipSessionCount.set(clientIp, ipCount + 1);
107
+ return entry;
108
+ }
109
+ /** Look up an existing session by ID */
110
+ getSession(sessionId) {
111
+ const entry = this.sessions.get(sessionId);
112
+ if (entry) {
113
+ entry.lastSeen = Date.now();
114
+ }
115
+ return entry;
116
+ }
117
+ /** Terminate and clean up a specific session */
118
+ async terminateSession(sessionId) {
119
+ const entry = this.sessions.get(sessionId);
120
+ if (!entry) return false;
121
+ this.sessions.delete(sessionId);
122
+ this.decrementIpCount(entry.clientIp);
123
+ try {
124
+ await entry.transport.close();
125
+ await entry.server.close();
126
+ } catch {
127
+ }
128
+ return true;
129
+ }
130
+ /** Terminate all sessions and stop sweep */
131
+ async cleanup() {
132
+ if (this.sweepTimer) {
133
+ clearInterval(this.sweepTimer);
134
+ this.sweepTimer = void 0;
135
+ }
136
+ const entries = [...this.sessions.values()];
137
+ this.sessions.clear();
138
+ this.ipSessionCount.clear();
139
+ await Promise.allSettled(
140
+ entries.map(async (entry) => {
141
+ try {
142
+ await entry.transport.close();
143
+ await entry.server.close();
144
+ } catch {
145
+ }
146
+ })
147
+ );
148
+ }
149
+ /** Get current session count */
150
+ get size() {
151
+ return this.sessions.size;
152
+ }
153
+ /** Evict sessions past TTL */
154
+ evictStale() {
155
+ const now = Date.now();
156
+ for (const [id, entry] of this.sessions) {
157
+ if (now - entry.lastSeen > this.ttlMs) {
158
+ this.sessions.delete(id);
159
+ this.decrementIpCount(entry.clientIp);
160
+ void entry.transport.close().catch(() => {
161
+ });
162
+ void entry.server.close().catch(() => {
163
+ });
164
+ }
165
+ }
166
+ }
167
+ /** Decrement per-IP session count, removing the key when it reaches 0 */
168
+ decrementIpCount(ip) {
169
+ const count = this.ipSessionCount.get(ip) ?? 0;
170
+ if (count <= 1) {
171
+ this.ipSessionCount.delete(ip);
172
+ } else {
173
+ this.ipSessionCount.set(ip, count - 1);
174
+ }
175
+ }
176
+ };
177
+ }
178
+ });
179
+
180
+ // src/http-transport.ts
181
+ var http_transport_exports = {};
182
+ __export(http_transport_exports, {
183
+ createHttpTransport: () => createHttpTransport
184
+ });
185
+ function isValidSessionId(id) {
186
+ if (id.length === 0 || id.length > MAX_SESSION_ID_LENGTH) return false;
187
+ for (let i = 0; i < id.length; i++) {
188
+ const code = id.charCodeAt(i);
189
+ if (code < 33 || code > 126) return false;
190
+ }
191
+ return true;
192
+ }
193
+ function isLoopbackAddr(addr) {
194
+ return addr.startsWith("127.") || addr === "::1" || addr === "::ffff:127.0.0.1";
195
+ }
196
+ function isLinkLocalAddr(addr) {
197
+ if (isLoopbackAddr(addr)) return true;
198
+ return addr.startsWith("169.254.") || addr.startsWith("fe80");
199
+ }
200
+ function isPrivateAddr(addr) {
201
+ if (isLinkLocalAddr(addr)) return true;
202
+ 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);
203
+ }
204
+ function isTrustedProxy(remoteAddr, trustProxy) {
205
+ if (trustProxy === "off") return false;
206
+ if (trustProxy === "all") return true;
207
+ if (trustProxy === "loopback") return isLoopbackAddr(remoteAddr);
208
+ if (trustProxy === "linklocal") return isLinkLocalAddr(remoteAddr);
209
+ if (trustProxy === "private") return isPrivateAddr(remoteAddr);
210
+ const trusted = trustProxy.split(",").map((s) => s.trim());
211
+ return trusted.includes(remoteAddr);
212
+ }
213
+ function validatePublicUrl(rawUrl) {
214
+ let u;
215
+ try {
216
+ u = new URL(rawUrl);
217
+ } catch {
218
+ return { error: `Invalid URL: ${rawUrl}` };
219
+ }
220
+ if (u.hash) {
221
+ return { error: "Public URL must not contain fragments (#)" };
222
+ }
223
+ const isLoopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
224
+ if (u.protocol !== "https:" && !isLoopback) {
225
+ return { error: "Public URL must use https (except for localhost/loopback dev usage)" };
226
+ }
227
+ const normalized = u.href.replace(/\/+$/, "") || u.origin;
228
+ return { url: u, normalized };
229
+ }
230
+ async function createHttpTransport(options) {
231
+ const port = options.port ?? 3e3;
232
+ const host = options.host ?? "127.0.0.1";
233
+ const corsOrigins = new Set(options.corsOrigins ?? []);
234
+ const maxRequestBytes = options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES;
235
+ const rateLimitRpm = options.rateLimitRpm ?? DEFAULT_RATE_LIMIT_RPM;
236
+ const sessionManager = new SessionManager({
237
+ ttlMs: options.sessionTtlMs,
238
+ maxSessions: options.maxSessions,
239
+ maxSessionsPerIp: options.maxSessionsPerIp
240
+ });
241
+ sessionManager.startSweep();
242
+ const rateLimits = /* @__PURE__ */ new Map();
243
+ function checkRateLimit(key) {
244
+ const now = Date.now();
245
+ let entry = rateLimits.get(key);
246
+ if (!entry || now - entry.windowStart > ONE_MINUTE_MS) {
247
+ entry = { count: 0, windowStart: now };
248
+ rateLimits.set(key, entry);
249
+ }
250
+ entry.count++;
251
+ return entry.count <= rateLimitRpm;
252
+ }
253
+ const trustProxyValue = options.trustProxy ?? "off";
254
+ function getClientIp(req) {
255
+ const peerIp = req.socket.remoteAddress ?? "unknown";
256
+ if (trustProxyValue !== "off" && isTrustedProxy(peerIp, trustProxyValue)) {
257
+ const xff = req.headers["x-forwarded-for"];
258
+ if (typeof xff === "string") {
259
+ const first = xff.split(",")[0]?.trim();
260
+ if (first) return first;
261
+ }
262
+ }
263
+ return peerIp;
264
+ }
265
+ const allowedHosts = /* @__PURE__ */ new Set();
266
+ allowedHosts.add(`${host}:${port}`);
267
+ allowedHosts.add(host === "0.0.0.0" ? `localhost:${port}` : `${host}:${port}`);
268
+ if (host === "127.0.0.1" || host === "0.0.0.0") {
269
+ allowedHosts.add(`localhost:${port}`);
270
+ allowedHosts.add(`127.0.0.1:${port}`);
271
+ }
272
+ if (options.publicUrl) {
273
+ try {
274
+ const u = new URL(options.publicUrl);
275
+ allowedHosts.add(u.host);
276
+ } catch {
277
+ }
278
+ }
279
+ const allowedOrigins = new Set(corsOrigins);
280
+ function validateOrigin(req, res) {
281
+ const origin = req.headers.origin;
282
+ if (!origin) return true;
283
+ if (allowedOrigins.size === 0) {
284
+ res.writeHead(403, { "Content-Type": "application/json" });
285
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
286
+ return false;
287
+ }
288
+ if (!allowedOrigins.has(origin)) {
289
+ res.writeHead(403, { "Content-Type": "application/json" });
290
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
291
+ return false;
292
+ }
293
+ return true;
294
+ }
295
+ function setCorsHeaders(req, res) {
296
+ const origin = req.headers.origin;
297
+ if (origin && allowedOrigins.has(origin)) {
298
+ res.setHeader("Access-Control-Allow-Origin", origin);
299
+ res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, GET, OPTIONS");
300
+ res.setHeader(
301
+ "Access-Control-Allow-Headers",
302
+ "Content-Type, Mcp-Session-Id, Accept, MCP-Protocol-Version"
303
+ );
304
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
305
+ }
306
+ }
307
+ function sendJson(res, status, body) {
308
+ const json = JSON.stringify(body);
309
+ res.writeHead(status, { "Content-Type": "application/json" });
310
+ res.end(json);
311
+ }
312
+ async function readBody(req) {
313
+ const chunks = [];
314
+ let totalBytes = 0;
315
+ for await (const chunk of req) {
316
+ totalBytes += chunk.length;
317
+ if (totalBytes > maxRequestBytes) {
318
+ throw new Error(`Request body exceeds ${maxRequestBytes} bytes`);
319
+ }
320
+ chunks.push(chunk);
321
+ }
322
+ return Buffer.concat(chunks);
323
+ }
324
+ const authServers = (options.authorizationServers ?? []).filter((s) => s.trim().length > 0);
325
+ if (options.authorizationServers?.length && authServers.length === 0) {
326
+ process.stderr.write(
327
+ `[${SERVER_NAME}] WARNING: --authorization-servers contains only empty entries, PRM disabled
328
+ `
329
+ );
330
+ }
331
+ if (authServers.length > 0 && !options.publicUrl) {
332
+ process.stderr.write(
333
+ `[${SERVER_NAME}] WARNING: --authorization-servers set without --public-url, PRM disabled
334
+ `
335
+ );
336
+ }
337
+ if (options.publicUrl && authServers.length === 0) {
338
+ process.stderr.write(
339
+ `[${SERVER_NAME}] WARNING: --public-url set without --authorization-servers, PRM disabled
340
+ `
341
+ );
342
+ }
343
+ const prmEnabled = !!(authServers.length > 0 && options.publicUrl);
344
+ let prmPath = "/.well-known/oauth-protected-resource";
345
+ let prmDocument;
346
+ if (prmEnabled && options.publicUrl) {
347
+ const validation = validatePublicUrl(options.publicUrl);
348
+ if ("error" in validation) {
349
+ process.stderr.write(
350
+ `[${SERVER_NAME}] WARNING: --public-url invalid, PRM disabled: ${validation.error}
351
+ `
352
+ );
353
+ } else {
354
+ const { url: u, normalized } = validation;
355
+ const resourcePath = u.pathname.replace(/\/+$/, "");
356
+ if (resourcePath && resourcePath !== "/") {
357
+ prmPath = `/.well-known/oauth-protected-resource${resourcePath}`;
358
+ }
359
+ prmDocument = {
360
+ resource: normalized,
361
+ authorization_servers: authServers
362
+ };
363
+ }
364
+ }
365
+ const server = http.createServer(async (req, res) => {
366
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
367
+ const pathname = url.pathname;
368
+ res.setTimeout(3e4);
369
+ if (req.method === "OPTIONS") {
370
+ setCorsHeaders(req, res);
371
+ res.writeHead(204);
372
+ res.end();
373
+ return;
374
+ }
375
+ setCorsHeaders(req, res);
376
+ const hostHeader = req.headers.host;
377
+ if (hostHeader && !allowedHosts.has(hostHeader)) {
378
+ sendJson(res, 400, { error: "Invalid Host header" });
379
+ return;
380
+ }
381
+ if (!validateOrigin(req, res)) return;
382
+ if (pathname === "/health" && req.method === "GET") {
383
+ sendJson(res, 200, {
384
+ ok: true,
385
+ version: SERVER_VERSION,
386
+ protocolVersion: MCP_PROTOCOL_VERSION
387
+ });
388
+ return;
389
+ }
390
+ if (pathname === prmPath && req.method === "GET") {
391
+ if (prmEnabled && prmDocument) {
392
+ sendJson(res, 200, prmDocument);
393
+ } else {
394
+ sendJson(res, 404, { error: "Not found" });
395
+ }
396
+ return;
397
+ }
398
+ if (pathname === "/mcp") {
399
+ if (req.method === "GET") {
400
+ res.writeHead(405, { Allow: "POST, DELETE", "Content-Type": "application/json" });
401
+ res.end(JSON.stringify({ error: "Method Not Allowed. Use POST for JSON-RPC requests." }));
402
+ return;
403
+ }
404
+ if (req.method === "DELETE") {
405
+ const sessionId = req.headers["mcp-session-id"];
406
+ if (!sessionId) {
407
+ sendJson(res, 400, { error: "Missing Mcp-Session-Id header" });
408
+ return;
409
+ }
410
+ if (!isValidSessionId(sessionId)) {
411
+ sendJson(res, 400, {
412
+ error: "Invalid Mcp-Session-Id: must be visible ASCII (0x21-0x7E), max 128 chars"
413
+ });
414
+ return;
415
+ }
416
+ const terminated = await sessionManager.terminateSession(sessionId);
417
+ if (terminated) {
418
+ sendJson(res, 200, { ok: true });
419
+ } else {
420
+ sendJson(res, 404, { error: "Session not found or already terminated" });
421
+ }
422
+ return;
423
+ }
424
+ if (req.method === "POST") {
425
+ const sessionId = req.headers["mcp-session-id"];
426
+ const rateLimitKey = sessionId ?? `ip:${getClientIp(req)}`;
427
+ if (!checkRateLimit(rateLimitKey)) {
428
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": "60" });
429
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
430
+ return;
431
+ }
432
+ let body;
433
+ try {
434
+ const raw = await readBody(req);
435
+ body = JSON.parse(raw.toString("utf-8"));
436
+ } catch (err) {
437
+ const msg = err instanceof Error ? err.message : "Invalid request body";
438
+ sendJson(res, 400, { error: msg });
439
+ return;
440
+ }
441
+ const isInit = typeof body === "object" && body !== null && "method" in body && body.method === "initialize";
442
+ if (isInit) {
443
+ const protocolVersion = req.headers["mcp-protocol-version"];
444
+ if (protocolVersion && protocolVersion !== MCP_PROTOCOL_VERSION && protocolVersion !== "2025-03-26") {
445
+ sendJson(res, 400, { error: `Unsupported MCP protocol version: ${protocolVersion}` });
446
+ return;
447
+ }
448
+ const accept = req.headers.accept;
449
+ if (!accept) {
450
+ process.stderr.write(
451
+ `[${SERVER_NAME}] Warning: missing Accept header on init request
452
+ `
453
+ );
454
+ }
455
+ let entry2;
456
+ try {
457
+ entry2 = await sessionManager.createSession(options.serverFactory, getClientIp(req));
458
+ } catch (err) {
459
+ const msg = err instanceof Error ? err.message : "Failed to create session";
460
+ sendJson(res, 503, { error: msg });
461
+ return;
462
+ }
463
+ await entry2.transport.handleRequest(req, res, body);
464
+ return;
465
+ }
466
+ if (!sessionId) {
467
+ sendJson(res, 400, { error: "Missing Mcp-Session-Id header" });
468
+ return;
469
+ }
470
+ if (!isValidSessionId(sessionId)) {
471
+ sendJson(res, 400, {
472
+ error: "Invalid Mcp-Session-Id: must be visible ASCII (0x21-0x7E), max 128 chars"
473
+ });
474
+ return;
475
+ }
476
+ const entry = sessionManager.getSession(sessionId);
477
+ if (!entry) {
478
+ sendJson(res, 404, { error: "Session not found or terminated" });
479
+ return;
480
+ }
481
+ await entry.transport.handleRequest(req, res, body);
482
+ return;
483
+ }
484
+ res.writeHead(405, { Allow: "POST, DELETE", "Content-Type": "application/json" });
485
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
486
+ return;
487
+ }
488
+ sendJson(res, 404, { error: "Not found" });
489
+ });
490
+ server.headersTimeout = 1e4;
491
+ server.requestTimeout = 3e4;
492
+ server.keepAliveTimeout = 5e3;
493
+ return new Promise((resolve2, reject) => {
494
+ server.on("error", reject);
495
+ server.listen(port, host, () => {
496
+ process.stderr.write(`[${SERVER_NAME}] HTTP transport listening on http://${host}:${port}
497
+ `);
498
+ process.stderr.write(` Transport: Streamable HTTP (MCP 2025-06-18)
499
+ `);
500
+ process.stderr.write(
501
+ ` Auth: unprotected mode (no token validation); PRM discovery per RFC 9728
502
+ `
503
+ );
504
+ process.stderr.write(` Endpoints: POST /mcp, DELETE /mcp, GET /health
505
+ `);
506
+ if (prmEnabled) {
507
+ process.stderr.write(` PRM: GET ${prmPath} (implemented, enabled by config)
508
+ `);
509
+ } else {
510
+ process.stderr.write(
511
+ ` PRM: disabled (set --authorization-servers + --public-url to enable)
512
+ `
513
+ );
514
+ }
515
+ process.stderr.write(
516
+ ` Sessions: max ${options.maxSessions ?? 100} (${options.maxSessionsPerIp ?? 10}/IP), TTL ${(options.sessionTtlMs ?? 18e5) / 1e3}s
517
+ `
518
+ );
519
+ process.stderr.write(` Rate limit: ${rateLimitRpm} req/min per session
520
+ `);
521
+ process.stderr.write(
522
+ ` CORS: ${corsOrigins.size > 0 ? [...corsOrigins].join(", ") : "deny all"}
523
+ `
524
+ );
525
+ process.stderr.write(
526
+ ` Mode: unprotected (token validation deferred to v0.11.x+; deployer auth via reverse proxy)
527
+ `
528
+ );
529
+ resolve2({
530
+ cleanup: async () => {
531
+ await sessionManager.cleanup();
532
+ await new Promise((res) => server.close(() => res()));
533
+ }
534
+ });
535
+ });
536
+ });
537
+ }
538
+ var ONE_MINUTE_MS, DEFAULT_MAX_REQUEST_BYTES, DEFAULT_RATE_LIMIT_RPM, MAX_SESSION_ID_LENGTH;
539
+ var init_http_transport = __esm({
540
+ "src/http-transport.ts"() {
541
+ init_session_manager();
542
+ init_constants();
543
+ ONE_MINUTE_MS = 6e4;
544
+ DEFAULT_MAX_REQUEST_BYTES = 1048576;
545
+ DEFAULT_RATE_LIMIT_RPM = 100;
546
+ MAX_SESSION_ID_LENGTH = 128;
547
+ }
548
+ });
549
+
550
+ // src/cli.ts
551
+ init_constants();
39
552
 
40
553
  // src/infra/errors.ts
41
554
  var McpServerError = class extends Error {
@@ -104,9 +617,9 @@ var JwksConfigSchema = zod.z.object({
104
617
  var PolicySchema = zod.z.object({
105
618
  version: zod.z.literal("1"),
106
619
  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({}),
620
+ redaction: RedactionSchema.prefault({}),
621
+ tools: zod.z.record(zod.z.string(), ToolPolicySchema).prefault({}),
622
+ limits: LimitsSchema.prefault({}),
110
623
  jwks: JwksConfigSchema.optional()
111
624
  });
112
625
  var DEFAULT_POLICY = {
@@ -382,6 +895,9 @@ function installStdoutFence() {
382
895
  };
383
896
  }
384
897
 
898
+ // src/server.ts
899
+ init_constants();
900
+
385
901
  // src/handlers/guards.ts
386
902
  function checkJwsSize(jws, policy) {
387
903
  const byteLength = new TextEncoder().encode(jws).length;
@@ -987,10 +1503,40 @@ async function handleIssue(params) {
987
1503
  };
988
1504
  }
989
1505
  }
1506
+ var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
1507
+ "con",
1508
+ "prn",
1509
+ "aux",
1510
+ "nul",
1511
+ "com1",
1512
+ "com2",
1513
+ "com3",
1514
+ "com4",
1515
+ "com5",
1516
+ "com6",
1517
+ "com7",
1518
+ "com8",
1519
+ "com9",
1520
+ "lpt1",
1521
+ "lpt2",
1522
+ "lpt3",
1523
+ "lpt4",
1524
+ "lpt5",
1525
+ "lpt6",
1526
+ "lpt7",
1527
+ "lpt8",
1528
+ "lpt9"
1529
+ ]);
990
1530
  function assertRelativePath(p) {
991
1531
  if (!p || p.trim().length === 0) {
992
1532
  throw new PathTraversalError("Path must not be empty");
993
1533
  }
1534
+ if (p.includes("\0")) {
1535
+ throw new PathTraversalError("Null bytes are not allowed in paths");
1536
+ }
1537
+ if (p.includes(":")) {
1538
+ throw new PathTraversalError("Colons are not allowed in paths (drive letters, NTFS ADS)");
1539
+ }
994
1540
  if (nodePath__namespace.isAbsolute(p)) {
995
1541
  throw new PathTraversalError("Absolute paths are not allowed");
996
1542
  }
@@ -1007,6 +1553,14 @@ function assertRelativePath(p) {
1007
1553
  if (p.startsWith(".")) {
1008
1554
  throw new PathTraversalError("Hidden paths (starting with .) are not allowed");
1009
1555
  }
1556
+ if (p.endsWith(".") || p.endsWith(" ")) {
1557
+ throw new PathTraversalError("Trailing dots and spaces are not allowed");
1558
+ }
1559
+ const dotIndex = p.indexOf(".");
1560
+ const baseName = (dotIndex >= 0 ? p.slice(0, dotIndex) : p).toLowerCase();
1561
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
1562
+ throw new PathTraversalError(`Windows reserved device name: ${baseName.toUpperCase()}`);
1563
+ }
1010
1564
  }
1011
1565
  async function assertNotSymlink(componentPath) {
1012
1566
  try {
@@ -1038,6 +1592,7 @@ async function createTempDir(basePath) {
1038
1592
  }
1039
1593
 
1040
1594
  // src/handlers/bundle.ts
1595
+ init_constants();
1041
1596
  function stableJsonStringify(value) {
1042
1597
  return JSON.stringify(
1043
1598
  value,
@@ -1359,7 +1914,7 @@ var IssueOutputSchema = zod.z.object({
1359
1914
  });
1360
1915
  var BundleInputSchema = zod.z.object({
1361
1916
  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"),
1917
+ metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe("Optional metadata to include in the manifest"),
1363
1918
  output_path: zod.z.string().max(255).optional().describe("Output directory name (single segment within --bundle-dir)")
1364
1919
  });
1365
1920
  var MetaSchema5 = zod.z.object({
@@ -1415,9 +1970,17 @@ var BUNDLE_TOOL_ANNOTATIONS = {
1415
1970
  };
1416
1971
  var MAX_INPUT_DEPTH = 10;
1417
1972
  function errorCallToolResult(meta, code, message) {
1973
+ const errorDef = kernel.ERRORS[code];
1418
1974
  return {
1419
1975
  content: [{ type: "text", text: message }],
1420
- structuredContent: { _meta: meta, ok: false, code, message },
1976
+ structuredContent: {
1977
+ _meta: meta,
1978
+ ok: false,
1979
+ code,
1980
+ message,
1981
+ retryable: errorDef?.retryable ?? false,
1982
+ next_action: errorDef?.next_action ?? "none"
1983
+ },
1421
1984
  isError: true
1422
1985
  };
1423
1986
  }
@@ -1521,7 +2084,7 @@ function createPeacMcpServer(options) {
1521
2084
  "peac_verify",
1522
2085
  {
1523
2086
  title: "Verify PEAC Receipt",
1524
- description: "Verify a PEAC receipt JWS signature and validate claims. Returns structured check results.",
2087
+ description: "Verify a PEAC receipt: check Ed25519 signature, validate claims, and return structured check results. Read-only, no side effects.",
1525
2088
  inputSchema: VerifyInputSchema,
1526
2089
  outputSchema: VerifyOutputSchema,
1527
2090
  annotations: PURE_TOOL_ANNOTATIONS
@@ -1537,7 +2100,7 @@ function createPeacMcpServer(options) {
1537
2100
  "peac_inspect",
1538
2101
  {
1539
2102
  title: "Inspect PEAC Receipt",
1540
- description: "Decode and inspect a PEAC receipt without verifying the signature. Shows header, payload metadata, and optionally full claims.",
2103
+ description: "Inspect a PEAC receipt without verifying the signature. Returns decoded header, payload metadata, and timestamps. Read-only.",
1541
2104
  inputSchema: InspectInputSchema,
1542
2105
  outputSchema: InspectOutputSchema,
1543
2106
  annotations: PURE_TOOL_ANNOTATIONS
@@ -1553,7 +2116,7 @@ function createPeacMcpServer(options) {
1553
2116
  "peac_decode",
1554
2117
  {
1555
2118
  title: "Decode PEAC Receipt",
1556
- description: "Raw decode of a PEAC receipt JWS. Returns header and payload without signature verification.",
2119
+ description: "Raw-decode a PEAC receipt JWS into header and payload objects. No signature check. Use peac_verify for trusted results.",
1557
2120
  inputSchema: DecodeInputSchema,
1558
2121
  outputSchema: DecodeOutputSchema,
1559
2122
  annotations: PURE_TOOL_ANNOTATIONS
@@ -1570,7 +2133,7 @@ function createPeacMcpServer(options) {
1570
2133
  "peac_issue",
1571
2134
  {
1572
2135
  title: "Issue PEAC Receipt",
1573
- description: "Sign and return a PEAC receipt JWS. Requires server to be configured with --issuer-key and --issuer-id.",
2136
+ description: "Sign and return a new PEAC receipt JWS. Requires server configured with an Ed25519 issuer key (--issuer-key and --issuer-id).",
1574
2137
  inputSchema: IssueInputSchema,
1575
2138
  outputSchema: IssueOutputSchema,
1576
2139
  annotations: ISSUE_TOOL_ANNOTATIONS
@@ -1587,7 +2150,7 @@ function createPeacMcpServer(options) {
1587
2150
  "peac_create_bundle",
1588
2151
  {
1589
2152
  title: "Create Evidence Bundle",
1590
- description: "Create a signed evidence bundle directory from receipt JWS strings. Requires --issuer-key, --issuer-id, and --bundle-dir.",
2153
+ description: "Create a signed evidence bundle directory from one or more receipt JWS strings. Writes files to disk. Requires --issuer-key, --issuer-id, and --bundle-dir.",
1591
2154
  inputSchema: BundleInputSchema,
1592
2155
  outputSchema: BundleOutputSchema,
1593
2156
  annotations: BUNDLE_TOOL_ANNOTATIONS
@@ -1603,8 +2166,25 @@ function createPeacMcpServer(options) {
1603
2166
  }
1604
2167
  return server;
1605
2168
  }
2169
+
2170
+ // src/cli.ts
1606
2171
  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) => {
2172
+ 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(
2173
+ "--authorization-servers <list>",
2174
+ "OAuth authorization server URIs (enables PRM with --public-url)"
2175
+ ).option("--public-url <url>", "Canonical public URL of this server (required for PRM)").option(
2176
+ "--trust-proxy <value>",
2177
+ "Trust X-Forwarded-For: off (default), loopback, linklocal, private, all (discouraged), or comma-separated IPs",
2178
+ "off"
2179
+ ).action(async (opts) => {
2180
+ const transportType = opts.transport;
2181
+ if (transportType !== "stdio" && transportType !== "http") {
2182
+ process.stderr.write(
2183
+ `ERROR: --transport must be "stdio" or "http", got "${transportType}"
2184
+ `
2185
+ );
2186
+ process.exit(1);
2187
+ }
1608
2188
  if (opts.issuerKey && !opts.issuerId) {
1609
2189
  process.stderr.write("ERROR: --issuer-key requires --issuer-id\n");
1610
2190
  process.exit(1);
@@ -1658,13 +2238,6 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1658
2238
  issuerId: opts.issuerId,
1659
2239
  bundleDir
1660
2240
  };
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
2241
  const tools = ["peac_verify", "peac_inspect", "peac_decode"];
1669
2242
  if (issuerKey && opts.issuerId) {
1670
2243
  tools.push("peac_issue");
@@ -1672,10 +2245,17 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1672
2245
  tools.push("peac_create_bundle");
1673
2246
  }
1674
2247
  }
2248
+ const serverOptions = {
2249
+ version: SERVER_VERSION,
2250
+ policy,
2251
+ context
2252
+ };
1675
2253
  process.stderr.write(
1676
2254
  `[${SERVER_NAME}] v${SERVER_VERSION} | protocol ${MCP_PROTOCOL_VERSION}
1677
2255
  `
1678
2256
  );
2257
+ process.stderr.write(` Transport: ${transportType}
2258
+ `);
1679
2259
  process.stderr.write(` Tools: ${tools.join(", ")}
1680
2260
  `);
1681
2261
  process.stderr.write(` Key: ${issuerKey ? "loaded" : "none"}
@@ -1686,16 +2266,43 @@ program.name(SERVER_NAME).version(SERVER_VERSION).description("PEAC receipt oper
1686
2266
  `);
1687
2267
  process.stderr.write(` Policy hash: ${policyHash.slice(0, 16)}...
1688
2268
  `);
1689
- const shutdown = async () => {
1690
- process.stderr.write(`[${SERVER_NAME}] Shutting down...
2269
+ if (transportType === "http") {
2270
+ const { createHttpTransport: createHttpTransport2 } = await Promise.resolve().then(() => (init_http_transport(), http_transport_exports));
2271
+ const corsOrigins = opts.corsOrigins ? opts.corsOrigins.split(",").map((s) => s.trim()) : void 0;
2272
+ const authorizationServers = opts.authorizationServers ? opts.authorizationServers.split(",").map((s) => s.trim()) : void 0;
2273
+ const { cleanup } = await createHttpTransport2({
2274
+ port: parseInt(opts.port, 10),
2275
+ host: opts.host,
2276
+ corsOrigins,
2277
+ authorizationServers,
2278
+ publicUrl: opts.publicUrl,
2279
+ trustProxy: opts.trustProxy || "off",
2280
+ serverFactory: () => createPeacMcpServer(serverOptions)
2281
+ });
2282
+ const shutdown = async () => {
2283
+ process.stderr.write(`[${SERVER_NAME}] Shutting down...
1691
2284
  `);
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);
2285
+ await cleanup();
2286
+ process.exit(0);
2287
+ };
2288
+ process.on("SIGINT", shutdown);
2289
+ process.on("SIGTERM", shutdown);
2290
+ } else {
2291
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
2292
+ const teardownFence = installStdoutFence();
2293
+ const server = createPeacMcpServer(serverOptions);
2294
+ const transport = new StdioServerTransport();
2295
+ const shutdown = async () => {
2296
+ process.stderr.write(`[${SERVER_NAME}] Shutting down...
2297
+ `);
2298
+ await server.close();
2299
+ teardownFence();
2300
+ process.exit(0);
2301
+ };
2302
+ process.on("SIGINT", shutdown);
2303
+ process.on("SIGTERM", shutdown);
2304
+ await server.connect(transport);
2305
+ }
1699
2306
  });
1700
2307
  void program.parseAsync(process.argv).catch((err) => {
1701
2308
  process.stderr.write(