@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/.mcp.json +9 -0
- package/README.md +116 -15
- package/dist/cli.cjs +639 -32
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.mjs +639 -32
- package/dist/cli.mjs.map +1 -1
- package/dist/http-transport.d.ts +65 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/index.cjs +58 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +58 -11
- package/dist/index.mjs.map +1 -1
- package/dist/infra/constants.d.ts +7 -1
- package/dist/infra/constants.d.ts.map +1 -1
- package/dist/infra/path-safety.d.ts +3 -1
- package/dist/infra/path-safety.d.ts.map +1 -1
- package/dist/infra/policy.d.ts +9 -95
- package/dist/infra/policy.d.ts.map +1 -1
- package/dist/schemas/bundle.d.ts +4 -48
- package/dist/schemas/bundle.d.ts.map +1 -1
- package/dist/schemas/decode.d.ts +3 -33
- package/dist/schemas/decode.d.ts.map +1 -1
- package/dist/schemas/inspect.d.ts +4 -68
- package/dist/schemas/inspect.d.ts.map +1 -1
- package/dist/schemas/issue.d.ts +8 -89
- package/dist/schemas/issue.d.ts.map +1 -1
- package/dist/schemas/verify.d.ts +4 -66
- package/dist/schemas/verify.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/session-manager.d.ts +59 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/manifest.json +84 -0
- package/package.json +13 -7
- package/server.json +23 -0
- package/smithery.yaml +36 -0
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
|
-
|
|
12
|
-
|
|
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
|
|
16
|
-
var
|
|
17
|
-
|
|
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.
|
|
87
|
-
tools: z.record(z.string(), ToolPolicySchema).
|
|
88
|
-
limits: LimitsSchema.
|
|
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: {
|
|
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
|
}
|
|
@@ -1500,7 +2063,7 @@ function createPeacMcpServer(options) {
|
|
|
1500
2063
|
"peac_verify",
|
|
1501
2064
|
{
|
|
1502
2065
|
title: "Verify PEAC Receipt",
|
|
1503
|
-
description: "Verify a PEAC receipt
|
|
2066
|
+
description: "Verify a PEAC receipt: check Ed25519 signature, validate claims, and return structured check results. Read-only, no side effects.",
|
|
1504
2067
|
inputSchema: VerifyInputSchema,
|
|
1505
2068
|
outputSchema: VerifyOutputSchema,
|
|
1506
2069
|
annotations: PURE_TOOL_ANNOTATIONS
|
|
@@ -1516,7 +2079,7 @@ function createPeacMcpServer(options) {
|
|
|
1516
2079
|
"peac_inspect",
|
|
1517
2080
|
{
|
|
1518
2081
|
title: "Inspect PEAC Receipt",
|
|
1519
|
-
description: "
|
|
2082
|
+
description: "Inspect a PEAC receipt without verifying the signature. Returns decoded header, payload metadata, and timestamps. Read-only.",
|
|
1520
2083
|
inputSchema: InspectInputSchema,
|
|
1521
2084
|
outputSchema: InspectOutputSchema,
|
|
1522
2085
|
annotations: PURE_TOOL_ANNOTATIONS
|
|
@@ -1532,7 +2095,7 @@ function createPeacMcpServer(options) {
|
|
|
1532
2095
|
"peac_decode",
|
|
1533
2096
|
{
|
|
1534
2097
|
title: "Decode PEAC Receipt",
|
|
1535
|
-
description: "Raw
|
|
2098
|
+
description: "Raw-decode a PEAC receipt JWS into header and payload objects. No signature check. Use peac_verify for trusted results.",
|
|
1536
2099
|
inputSchema: DecodeInputSchema,
|
|
1537
2100
|
outputSchema: DecodeOutputSchema,
|
|
1538
2101
|
annotations: PURE_TOOL_ANNOTATIONS
|
|
@@ -1549,7 +2112,7 @@ function createPeacMcpServer(options) {
|
|
|
1549
2112
|
"peac_issue",
|
|
1550
2113
|
{
|
|
1551
2114
|
title: "Issue PEAC Receipt",
|
|
1552
|
-
description: "Sign and return a PEAC receipt JWS. Requires server
|
|
2115
|
+
description: "Sign and return a new PEAC receipt JWS. Requires server configured with an Ed25519 issuer key (--issuer-key and --issuer-id).",
|
|
1553
2116
|
inputSchema: IssueInputSchema,
|
|
1554
2117
|
outputSchema: IssueOutputSchema,
|
|
1555
2118
|
annotations: ISSUE_TOOL_ANNOTATIONS
|
|
@@ -1566,7 +2129,7 @@ function createPeacMcpServer(options) {
|
|
|
1566
2129
|
"peac_create_bundle",
|
|
1567
2130
|
{
|
|
1568
2131
|
title: "Create Evidence Bundle",
|
|
1569
|
-
description: "Create a signed evidence bundle directory from receipt JWS strings. Requires --issuer-key, --issuer-id, and --bundle-dir.",
|
|
2132
|
+
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.",
|
|
1570
2133
|
inputSchema: BundleInputSchema,
|
|
1571
2134
|
outputSchema: BundleOutputSchema,
|
|
1572
2135
|
annotations: BUNDLE_TOOL_ANNOTATIONS
|
|
@@ -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").
|
|
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
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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(
|