@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/README.md +109 -15
- package/dist/cli.cjs +634 -27
- 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 +634 -27
- 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 +53 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +53 -6
- 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/package.json +10 -6
- package/server.json +23 -0
- package/smithery.yaml +36 -0
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
|
|
37
|
-
var
|
|
38
|
-
|
|
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.
|
|
108
|
-
tools: zod.z.record(zod.z.string(), ToolPolicySchema).
|
|
109
|
-
limits: LimitsSchema.
|
|
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: {
|
|
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
|
}
|
|
@@ -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").
|
|
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
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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(
|