portkey-admin-mcp 0.5.0 → 0.6.0
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 +22 -7
- package/build/index.js +33 -8
- package/build/server.js +629 -113
- package/package.json +12 -11
package/build/server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
6
|
// src/lib/http-app.ts
|
|
7
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8
8
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
9
9
|
import {
|
|
10
10
|
createServer as createHttpServer
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
23
|
import cors from "cors";
|
|
24
24
|
import express from "express";
|
|
25
|
+
import { rateLimit as expressRateLimit } from "express-rate-limit";
|
|
25
26
|
import helmet from "helmet";
|
|
26
27
|
|
|
27
28
|
// src/services/base.service.ts
|
|
@@ -136,6 +137,7 @@ var Logger = {
|
|
|
136
137
|
// src/services/base.service.ts
|
|
137
138
|
var DEFAULT_BASE_URL = "https://api.portkey.ai/v1";
|
|
138
139
|
var PRIVATE_BASE_URL_OVERRIDE_HINT = "Set PORTKEY_ALLOW_PRIVATE_BASE_URL=true to allow self-hosted gateways on loopback or private networks.";
|
|
140
|
+
var INSECURE_HTTP_OVERRIDE_HINT = "Set PORTKEY_ALLOW_INSECURE_HTTP=true only for a trusted self-hosted gateway when TLS is unavailable.";
|
|
139
141
|
function isPrivateOrLocalHost(hostname) {
|
|
140
142
|
const host = hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
141
143
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
@@ -202,6 +204,14 @@ function validateUrl(url) {
|
|
|
202
204
|
`Refusing to use a loopback or private-network PORTKEY_BASE_URL host: ${parsed.hostname}. ${PRIVATE_BASE_URL_OVERRIDE_HINT}`
|
|
203
205
|
);
|
|
204
206
|
}
|
|
207
|
+
const allowInsecureHttp = /^(1|true|yes)$/i.test(
|
|
208
|
+
process.env.PORTKEY_ALLOW_INSECURE_HTTP?.trim() ?? ""
|
|
209
|
+
);
|
|
210
|
+
if (parsed.protocol === "http:" && !allowInsecureHttp) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Refusing insecure HTTP PORTKEY_BASE_URL: ${parsed.origin}. ${INSECURE_HTTP_OVERRIDE_HINT}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
205
215
|
}
|
|
206
216
|
var BaseService = class {
|
|
207
217
|
apiKey;
|
|
@@ -213,13 +223,9 @@ var BaseService = class {
|
|
|
213
223
|
throw new Error("PORTKEY_API_KEY environment variable is not set");
|
|
214
224
|
}
|
|
215
225
|
this.apiKey = apiKey;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const baseUrl = process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
|
|
220
|
-
validateUrl(baseUrl);
|
|
221
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
222
|
-
}
|
|
226
|
+
const baseUrl = baseUrlOverride ?? process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
|
|
227
|
+
validateUrl(baseUrl);
|
|
228
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
223
229
|
}
|
|
224
230
|
encodePathSegment(value) {
|
|
225
231
|
return encodeURIComponent(value);
|
|
@@ -255,6 +261,7 @@ var BaseService = class {
|
|
|
255
261
|
try {
|
|
256
262
|
const response = await fetchWithTimeout(url, {
|
|
257
263
|
method,
|
|
264
|
+
redirect: "manual",
|
|
258
265
|
headers: this.buildHeaders(method),
|
|
259
266
|
body: this.serializeBody(options.body),
|
|
260
267
|
timeout: this.timeout
|
|
@@ -8425,6 +8432,11 @@ function registerAllTools(server, service, options = {}) {
|
|
|
8425
8432
|
// src/lib/auth.ts
|
|
8426
8433
|
import crypto2 from "node:crypto";
|
|
8427
8434
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
8435
|
+
function getPrincipalOwnerKey(principal) {
|
|
8436
|
+
return crypto2.createHash("sha256").update(principal.id, "utf8").digest("hex");
|
|
8437
|
+
}
|
|
8438
|
+
var ClerkAuthorizationError = class extends Error {
|
|
8439
|
+
};
|
|
8428
8440
|
var AUTH_SCHEMES = {
|
|
8429
8441
|
bearer: "Bearer"
|
|
8430
8442
|
};
|
|
@@ -8471,6 +8483,12 @@ function getHttpAuthConfigFromEnv() {
|
|
|
8471
8483
|
process.env.CLERK_JWKS_URL?.trim(),
|
|
8472
8484
|
issuer
|
|
8473
8485
|
);
|
|
8486
|
+
const allowedSubjects = parseCsv(process.env.CLERK_ALLOWED_SUBJECTS);
|
|
8487
|
+
const allowedOrganizationIds = parseCsv(
|
|
8488
|
+
process.env.CLERK_ALLOWED_ORGANIZATION_IDS
|
|
8489
|
+
);
|
|
8490
|
+
const allowedRoles = parseCsv(process.env.CLERK_ALLOWED_ROLES);
|
|
8491
|
+
const requiredPermissions = parseCsv(process.env.CLERK_REQUIRED_PERMISSIONS);
|
|
8474
8492
|
if (mode === "bearer" && !bearerToken) {
|
|
8475
8493
|
throw new Error("MCP_AUTH_MODE=bearer requires MCP_AUTH_TOKEN to be set");
|
|
8476
8494
|
}
|
|
@@ -8502,13 +8520,22 @@ function getHttpAuthConfigFromEnv() {
|
|
|
8502
8520
|
`MCP_AUTH_MODE=clerk configuration error (${issues.join("; ")})`
|
|
8503
8521
|
);
|
|
8504
8522
|
}
|
|
8523
|
+
if (!allowedSubjects && !allowedOrganizationIds && !allowedRoles && !requiredPermissions) {
|
|
8524
|
+
throw new Error(
|
|
8525
|
+
"MCP_AUTH_MODE=clerk requires an explicit authorization policy. Set at least one of CLERK_ALLOWED_SUBJECTS, CLERK_ALLOWED_ORGANIZATION_IDS, CLERK_ALLOWED_ROLES, or CLERK_REQUIRED_PERMISSIONS."
|
|
8526
|
+
);
|
|
8527
|
+
}
|
|
8505
8528
|
}
|
|
8506
8529
|
return {
|
|
8507
8530
|
mode,
|
|
8508
8531
|
bearerToken,
|
|
8509
8532
|
jwksUrl,
|
|
8510
8533
|
issuer,
|
|
8511
|
-
audience
|
|
8534
|
+
audience,
|
|
8535
|
+
allowedSubjects,
|
|
8536
|
+
allowedOrganizationIds,
|
|
8537
|
+
allowedRoles,
|
|
8538
|
+
requiredPermissions
|
|
8512
8539
|
};
|
|
8513
8540
|
}
|
|
8514
8541
|
var HTTP_AUTH_CONFIG = getHttpAuthConfigFromEnv();
|
|
@@ -8543,6 +8570,65 @@ function timingSafeEqual(a, b) {
|
|
|
8543
8570
|
return crypto2.timingSafeEqual(left, right);
|
|
8544
8571
|
}
|
|
8545
8572
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
8573
|
+
function asString(value) {
|
|
8574
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8575
|
+
}
|
|
8576
|
+
function asStringArray(value) {
|
|
8577
|
+
if (!Array.isArray(value)) {
|
|
8578
|
+
return [];
|
|
8579
|
+
}
|
|
8580
|
+
return value.flatMap((item) => {
|
|
8581
|
+
const normalized = asString(item);
|
|
8582
|
+
return normalized ? [normalized] : [];
|
|
8583
|
+
});
|
|
8584
|
+
}
|
|
8585
|
+
function unique(values) {
|
|
8586
|
+
return [...new Set(values)];
|
|
8587
|
+
}
|
|
8588
|
+
function authorizeClerkClaims(payload, config) {
|
|
8589
|
+
const subject = asString(payload.sub);
|
|
8590
|
+
if (!subject) {
|
|
8591
|
+
throw new ClerkAuthorizationError("Clerk token is missing a subject");
|
|
8592
|
+
}
|
|
8593
|
+
const organization = typeof payload.o === "object" && payload.o !== null ? payload.o : void 0;
|
|
8594
|
+
const organizationId = asString(payload.org_id) ?? asString(organization?.id);
|
|
8595
|
+
const roles = unique(
|
|
8596
|
+
[
|
|
8597
|
+
asString(payload.org_role),
|
|
8598
|
+
asString(organization?.rol),
|
|
8599
|
+
...asStringArray(payload.roles)
|
|
8600
|
+
].filter((role) => Boolean(role))
|
|
8601
|
+
);
|
|
8602
|
+
const permissions = unique([
|
|
8603
|
+
...asStringArray(payload.org_permissions),
|
|
8604
|
+
...asStringArray(organization?.per),
|
|
8605
|
+
...asStringArray(payload.permissions)
|
|
8606
|
+
]);
|
|
8607
|
+
if (config.allowedSubjects && !config.allowedSubjects.includes(subject)) {
|
|
8608
|
+
throw new ClerkAuthorizationError("Clerk subject is not authorized");
|
|
8609
|
+
}
|
|
8610
|
+
if (config.allowedOrganizationIds && (!organizationId || !config.allowedOrganizationIds.includes(organizationId))) {
|
|
8611
|
+
throw new ClerkAuthorizationError("Clerk organization is not authorized");
|
|
8612
|
+
}
|
|
8613
|
+
if (config.allowedRoles && !roles.some((role) => config.allowedRoles?.includes(role))) {
|
|
8614
|
+
throw new ClerkAuthorizationError("Clerk role is not authorized");
|
|
8615
|
+
}
|
|
8616
|
+
if (config.requiredPermissions && !config.requiredPermissions.every(
|
|
8617
|
+
(permission) => permissions.includes(permission)
|
|
8618
|
+
)) {
|
|
8619
|
+
throw new ClerkAuthorizationError(
|
|
8620
|
+
"Clerk token is missing a required permission"
|
|
8621
|
+
);
|
|
8622
|
+
}
|
|
8623
|
+
return {
|
|
8624
|
+
id: `clerk:${config.issuer}:${subject}`,
|
|
8625
|
+
mode: "clerk",
|
|
8626
|
+
subject,
|
|
8627
|
+
organizationId,
|
|
8628
|
+
roles,
|
|
8629
|
+
permissions
|
|
8630
|
+
};
|
|
8631
|
+
}
|
|
8546
8632
|
async function verifyClerkToken(token, config) {
|
|
8547
8633
|
if (!config.jwksUrl) {
|
|
8548
8634
|
throw new Error("Missing Clerk JWKS URL configuration");
|
|
@@ -8558,11 +8644,12 @@ async function verifyClerkToken(token, config) {
|
|
|
8558
8644
|
jwks = createRemoteJWKSet(new URL(config.jwksUrl));
|
|
8559
8645
|
jwksCache.set(config.jwksUrl, jwks);
|
|
8560
8646
|
}
|
|
8561
|
-
await jwtVerify(token, jwks, {
|
|
8647
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
8562
8648
|
issuer: config.issuer,
|
|
8563
8649
|
audience: config.audience,
|
|
8564
8650
|
clockTolerance: "5s"
|
|
8565
8651
|
});
|
|
8652
|
+
return authorizeClerkClaims(payload, config);
|
|
8566
8653
|
}
|
|
8567
8654
|
async function mcpAuthMiddleware(req, res, next) {
|
|
8568
8655
|
if (!req.path.startsWith("/mcp")) {
|
|
@@ -8571,6 +8658,12 @@ async function mcpAuthMiddleware(req, res, next) {
|
|
|
8571
8658
|
}
|
|
8572
8659
|
const config = getHttpAuthConfig();
|
|
8573
8660
|
if (config.mode === "none") {
|
|
8661
|
+
res.locals.authPrincipal = {
|
|
8662
|
+
id: "anonymous",
|
|
8663
|
+
mode: "none",
|
|
8664
|
+
roles: [],
|
|
8665
|
+
permissions: []
|
|
8666
|
+
};
|
|
8574
8667
|
next();
|
|
8575
8668
|
return;
|
|
8576
8669
|
}
|
|
@@ -8582,13 +8675,21 @@ async function mcpAuthMiddleware(req, res, next) {
|
|
|
8582
8675
|
return;
|
|
8583
8676
|
}
|
|
8584
8677
|
try {
|
|
8678
|
+
let principal;
|
|
8585
8679
|
if (config.mode === "bearer") {
|
|
8586
8680
|
if (!config.bearerToken || !timingSafeEqual(token, config.bearerToken)) {
|
|
8587
8681
|
throw new Error("Bearer token mismatch");
|
|
8588
8682
|
}
|
|
8683
|
+
principal = {
|
|
8684
|
+
id: `bearer:${crypto2.createHash("sha256").update(token, "utf8").digest("hex")}`,
|
|
8685
|
+
mode: "bearer",
|
|
8686
|
+
roles: [],
|
|
8687
|
+
permissions: []
|
|
8688
|
+
};
|
|
8589
8689
|
} else {
|
|
8590
|
-
await verifyClerkToken(token, config);
|
|
8690
|
+
principal = await verifyClerkToken(token, config);
|
|
8591
8691
|
}
|
|
8692
|
+
res.locals.authPrincipal = principal;
|
|
8592
8693
|
next();
|
|
8593
8694
|
} catch (error) {
|
|
8594
8695
|
Logger.warn("MCP auth failed", {
|
|
@@ -8599,6 +8700,10 @@ async function mcpAuthMiddleware(req, res, next) {
|
|
|
8599
8700
|
reason: error instanceof Error ? error.message : "Unknown error"
|
|
8600
8701
|
}
|
|
8601
8702
|
});
|
|
8703
|
+
if (error instanceof ClerkAuthorizationError) {
|
|
8704
|
+
res.status(403).json({ error: "Forbidden: Principal is not authorized" });
|
|
8705
|
+
return;
|
|
8706
|
+
}
|
|
8602
8707
|
res.status(401).json({ error: "Unauthorized: Token validation failed" });
|
|
8603
8708
|
}
|
|
8604
8709
|
}
|
|
@@ -8669,7 +8774,7 @@ function getServerConfig() {
|
|
|
8669
8774
|
`Invalid MCP_MAX_SESSIONS value: ${maxSessionsStr}. Must be a positive integer`
|
|
8670
8775
|
);
|
|
8671
8776
|
}
|
|
8672
|
-
const eventStoreTtlStr = (process.env.MCP_EVENT_TTL_SECONDS || "
|
|
8777
|
+
const eventStoreTtlStr = (process.env.MCP_EVENT_TTL_SECONDS || "300").trim();
|
|
8673
8778
|
const eventStoreTtlSeconds = parseStrictInteger(eventStoreTtlStr);
|
|
8674
8779
|
if (Number.isNaN(eventStoreTtlSeconds) || eventStoreTtlSeconds <= 0) {
|
|
8675
8780
|
throw new Error(
|
|
@@ -8682,6 +8787,38 @@ function getServerConfig() {
|
|
|
8682
8787
|
"MCP_EVENT_STORE=redis requires MCP_REDIS_URL (or REDIS_URL) to be set"
|
|
8683
8788
|
);
|
|
8684
8789
|
}
|
|
8790
|
+
if (eventStoreMode === "redis" && redisUrl) {
|
|
8791
|
+
let parsedRedisUrl;
|
|
8792
|
+
try {
|
|
8793
|
+
parsedRedisUrl = new URL(redisUrl);
|
|
8794
|
+
} catch {
|
|
8795
|
+
throw new Error(
|
|
8796
|
+
"MCP_REDIS_URL must be a valid redis:// or rediss:// URL"
|
|
8797
|
+
);
|
|
8798
|
+
}
|
|
8799
|
+
if (!["redis:", "rediss:"].includes(parsedRedisUrl.protocol)) {
|
|
8800
|
+
throw new Error("MCP_REDIS_URL must use redis:// or rediss://");
|
|
8801
|
+
}
|
|
8802
|
+
if (process.env.NODE_ENV?.trim().toLowerCase() === "production" && parsedRedisUrl.protocol !== "rediss:") {
|
|
8803
|
+
throw new Error("MCP_REDIS_URL must use rediss:// in production");
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
const rawEventEncryptionKey = process.env.MCP_EVENT_ENCRYPTION_KEY?.trim();
|
|
8807
|
+
let eventEncryptionKey;
|
|
8808
|
+
if (eventStoreMode === "redis") {
|
|
8809
|
+
if (!rawEventEncryptionKey) {
|
|
8810
|
+
throw new Error(
|
|
8811
|
+
"MCP_EVENT_ENCRYPTION_KEY must be base64-encoded 32-byte key when MCP_EVENT_STORE=redis"
|
|
8812
|
+
);
|
|
8813
|
+
}
|
|
8814
|
+
const decodedKey = Buffer.from(rawEventEncryptionKey, "base64");
|
|
8815
|
+
if (decodedKey.length !== 32 || decodedKey.toString("base64") !== rawEventEncryptionKey) {
|
|
8816
|
+
throw new Error(
|
|
8817
|
+
"MCP_EVENT_ENCRYPTION_KEY must be base64-encoded 32-byte key when MCP_EVENT_STORE=redis"
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
eventEncryptionKey = decodedKey;
|
|
8821
|
+
}
|
|
8685
8822
|
const redisKeyPrefix = process.env.MCP_REDIS_KEY_PREFIX?.trim() || "mcp:event-store";
|
|
8686
8823
|
const keyPath = process.env.MCP_TLS_KEY_PATH?.trim() || void 0;
|
|
8687
8824
|
const certPath = process.env.MCP_TLS_CERT_PATH?.trim() || void 0;
|
|
@@ -8702,7 +8839,8 @@ function getServerConfig() {
|
|
|
8702
8839
|
mode: eventStoreMode,
|
|
8703
8840
|
ttlSeconds: eventStoreTtlSeconds,
|
|
8704
8841
|
redisUrl,
|
|
8705
|
-
redisKeyPrefix
|
|
8842
|
+
redisKeyPrefix,
|
|
8843
|
+
encryptionKey: eventEncryptionKey
|
|
8706
8844
|
},
|
|
8707
8845
|
protocol,
|
|
8708
8846
|
port,
|
|
@@ -8720,8 +8858,69 @@ function getServerConfig() {
|
|
|
8720
8858
|
}
|
|
8721
8859
|
|
|
8722
8860
|
// src/lib/event-store.ts
|
|
8723
|
-
import {
|
|
8861
|
+
import {
|
|
8862
|
+
createCipheriv,
|
|
8863
|
+
createDecipheriv,
|
|
8864
|
+
randomBytes as randomBytes2,
|
|
8865
|
+
randomUUID
|
|
8866
|
+
} from "node:crypto";
|
|
8724
8867
|
var EVENT_STORE_CLEANUP_INTERVAL_MS = 3e4;
|
|
8868
|
+
var EVENT_PAYLOAD_ENCRYPTION_VERSION = "v1";
|
|
8869
|
+
var EVENT_PAYLOAD_AAD = Buffer.from(
|
|
8870
|
+
"portkey-admin-mcp:event-payload:v1",
|
|
8871
|
+
"utf8"
|
|
8872
|
+
);
|
|
8873
|
+
function assertEncryptionKey(key) {
|
|
8874
|
+
if (key.length !== 32) {
|
|
8875
|
+
throw new Error("Event payload encryption requires a 32-byte key");
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
function encryptEventPayload(message, key) {
|
|
8879
|
+
assertEncryptionKey(key);
|
|
8880
|
+
const iv = randomBytes2(12);
|
|
8881
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv, {
|
|
8882
|
+
authTagLength: 16
|
|
8883
|
+
});
|
|
8884
|
+
cipher.setAAD(EVENT_PAYLOAD_AAD);
|
|
8885
|
+
const ciphertext = Buffer.concat([
|
|
8886
|
+
cipher.update(JSON.stringify(message), "utf8"),
|
|
8887
|
+
cipher.final()
|
|
8888
|
+
]);
|
|
8889
|
+
const authenticationTag = cipher.getAuthTag();
|
|
8890
|
+
return [
|
|
8891
|
+
EVENT_PAYLOAD_ENCRYPTION_VERSION,
|
|
8892
|
+
iv.toString("base64url"),
|
|
8893
|
+
authenticationTag.toString("base64url"),
|
|
8894
|
+
ciphertext.toString("base64url")
|
|
8895
|
+
].join(".");
|
|
8896
|
+
}
|
|
8897
|
+
function decryptEventPayload(encoded, key) {
|
|
8898
|
+
assertEncryptionKey(key);
|
|
8899
|
+
const [version, encodedIv, encodedTag, encodedCiphertext, ...rest] = encoded.split(".");
|
|
8900
|
+
if (version !== EVENT_PAYLOAD_ENCRYPTION_VERSION || !encodedIv || !encodedTag || !encodedCiphertext || rest.length > 0) {
|
|
8901
|
+
throw new Error("Invalid encrypted event payload envelope");
|
|
8902
|
+
}
|
|
8903
|
+
const iv = Buffer.from(encodedIv, "base64url");
|
|
8904
|
+
const authenticationTag = Buffer.from(encodedTag, "base64url");
|
|
8905
|
+
if (iv.length !== 12) {
|
|
8906
|
+
throw new Error("Encrypted event payload must use a 12-byte nonce");
|
|
8907
|
+
}
|
|
8908
|
+
if (authenticationTag.length !== 16) {
|
|
8909
|
+
throw new Error(
|
|
8910
|
+
"Encrypted event payload must use a 16-byte authentication tag"
|
|
8911
|
+
);
|
|
8912
|
+
}
|
|
8913
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv, {
|
|
8914
|
+
authTagLength: 16
|
|
8915
|
+
});
|
|
8916
|
+
decipher.setAAD(EVENT_PAYLOAD_AAD);
|
|
8917
|
+
decipher.setAuthTag(authenticationTag);
|
|
8918
|
+
const plaintext = Buffer.concat([
|
|
8919
|
+
decipher.update(Buffer.from(encodedCiphertext, "base64url")),
|
|
8920
|
+
decipher.final()
|
|
8921
|
+
]);
|
|
8922
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
8923
|
+
}
|
|
8725
8924
|
var SAFE_EVENT_ID_PATTERN = /^[\w-]{1,128}$/;
|
|
8726
8925
|
function assertSafeRedisId(id, kind) {
|
|
8727
8926
|
if (!SAFE_EVENT_ID_PATTERN.test(id)) {
|
|
@@ -8731,36 +8930,47 @@ function assertSafeRedisId(id, kind) {
|
|
|
8731
8930
|
}
|
|
8732
8931
|
return id;
|
|
8733
8932
|
}
|
|
8734
|
-
var InMemoryEventStore = class {
|
|
8933
|
+
var InMemoryEventStore = class _InMemoryEventStore {
|
|
8735
8934
|
ttlMs;
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8935
|
+
ownerKey;
|
|
8936
|
+
state;
|
|
8937
|
+
constructor(ttlSeconds, ownerKey, state = {
|
|
8938
|
+
events: /* @__PURE__ */ new Map(),
|
|
8939
|
+
streamEvents: /* @__PURE__ */ new Map(),
|
|
8940
|
+
activeReplayStreams: /* @__PURE__ */ new Set(),
|
|
8941
|
+
lastCleanupAt: 0
|
|
8942
|
+
}) {
|
|
8741
8943
|
this.ttlMs = ttlSeconds * 1e3;
|
|
8944
|
+
this.ownerKey = ownerKey;
|
|
8945
|
+
this.state = state;
|
|
8946
|
+
}
|
|
8947
|
+
forOwner(ownerKey) {
|
|
8948
|
+
return new _InMemoryEventStore(this.ttlMs / 1e3, ownerKey, this.state);
|
|
8742
8949
|
}
|
|
8743
|
-
removeEvent(eventId, event = this.events.get(eventId)) {
|
|
8950
|
+
removeEvent(eventId, event = this.state.events.get(eventId)) {
|
|
8744
8951
|
if (!event) {
|
|
8745
8952
|
return;
|
|
8746
8953
|
}
|
|
8747
|
-
this.events.delete(eventId);
|
|
8748
|
-
const eventIds = this.streamEvents.get(event.streamId);
|
|
8954
|
+
this.state.events.delete(eventId);
|
|
8955
|
+
const eventIds = this.state.streamEvents.get(event.streamId);
|
|
8749
8956
|
if (!eventIds) {
|
|
8750
8957
|
return;
|
|
8751
8958
|
}
|
|
8752
8959
|
const filtered = eventIds.filter((candidate) => candidate !== eventId);
|
|
8753
8960
|
if (filtered.length === 0) {
|
|
8754
|
-
this.streamEvents.delete(event.streamId);
|
|
8961
|
+
this.state.streamEvents.delete(event.streamId);
|
|
8755
8962
|
} else if (filtered.length !== eventIds.length) {
|
|
8756
|
-
this.streamEvents.set(event.streamId, filtered);
|
|
8963
|
+
this.state.streamEvents.set(event.streamId, filtered);
|
|
8757
8964
|
}
|
|
8758
8965
|
}
|
|
8759
8966
|
getEventIfUnexpired(eventId, now = Date.now()) {
|
|
8760
|
-
const event = this.events.get(eventId);
|
|
8967
|
+
const event = this.state.events.get(eventId);
|
|
8761
8968
|
if (!event) {
|
|
8762
8969
|
return void 0;
|
|
8763
8970
|
}
|
|
8971
|
+
if (event.ownerKey !== this.ownerKey) {
|
|
8972
|
+
return void 0;
|
|
8973
|
+
}
|
|
8764
8974
|
if (event.expiresAt > now) {
|
|
8765
8975
|
return event;
|
|
8766
8976
|
}
|
|
@@ -8768,38 +8978,41 @@ var InMemoryEventStore = class {
|
|
|
8768
8978
|
return void 0;
|
|
8769
8979
|
}
|
|
8770
8980
|
cleanupExpired(now = Date.now()) {
|
|
8771
|
-
for (const [eventId, event] of this.events.entries()) {
|
|
8981
|
+
for (const [eventId, event] of this.state.events.entries()) {
|
|
8772
8982
|
if (event.expiresAt <= now) {
|
|
8773
|
-
this.events.delete(eventId);
|
|
8983
|
+
this.state.events.delete(eventId);
|
|
8774
8984
|
}
|
|
8775
8985
|
}
|
|
8776
|
-
for (const [streamId, eventIds] of this.streamEvents.entries()) {
|
|
8777
|
-
const filtered = eventIds.filter(
|
|
8986
|
+
for (const [streamId, eventIds] of this.state.streamEvents.entries()) {
|
|
8987
|
+
const filtered = eventIds.filter(
|
|
8988
|
+
(eventId) => this.state.events.has(eventId)
|
|
8989
|
+
);
|
|
8778
8990
|
if (filtered.length === 0) {
|
|
8779
|
-
this.streamEvents.delete(streamId);
|
|
8991
|
+
this.state.streamEvents.delete(streamId);
|
|
8780
8992
|
} else if (filtered.length !== eventIds.length) {
|
|
8781
|
-
this.streamEvents.set(streamId, filtered);
|
|
8993
|
+
this.state.streamEvents.set(streamId, filtered);
|
|
8782
8994
|
}
|
|
8783
8995
|
}
|
|
8784
8996
|
}
|
|
8785
8997
|
maybeCleanupExpired(now = Date.now()) {
|
|
8786
|
-
if (now - this.lastCleanupAt < EVENT_STORE_CLEANUP_INTERVAL_MS) {
|
|
8998
|
+
if (now - this.state.lastCleanupAt < EVENT_STORE_CLEANUP_INTERVAL_MS) {
|
|
8787
8999
|
return;
|
|
8788
9000
|
}
|
|
8789
9001
|
this.cleanupExpired(now);
|
|
8790
|
-
this.lastCleanupAt = now;
|
|
9002
|
+
this.state.lastCleanupAt = now;
|
|
8791
9003
|
}
|
|
8792
9004
|
async storeEvent(streamId, message) {
|
|
8793
9005
|
this.maybeCleanupExpired();
|
|
8794
9006
|
const eventId = randomUUID();
|
|
8795
|
-
this.events.set(eventId, {
|
|
9007
|
+
this.state.events.set(eventId, {
|
|
8796
9008
|
streamId,
|
|
9009
|
+
ownerKey: this.ownerKey,
|
|
8797
9010
|
message,
|
|
8798
9011
|
expiresAt: Date.now() + this.ttlMs
|
|
8799
9012
|
});
|
|
8800
|
-
const streamIds = this.streamEvents.get(streamId) || [];
|
|
9013
|
+
const streamIds = this.state.streamEvents.get(streamId) || [];
|
|
8801
9014
|
streamIds.push(eventId);
|
|
8802
|
-
this.streamEvents.set(streamId, streamIds);
|
|
9015
|
+
this.state.streamEvents.set(streamId, streamIds);
|
|
8803
9016
|
return eventId;
|
|
8804
9017
|
}
|
|
8805
9018
|
async getStreamIdForEventId(eventId) {
|
|
@@ -8812,17 +9025,17 @@ var InMemoryEventStore = class {
|
|
|
8812
9025
|
if (!streamId) {
|
|
8813
9026
|
return { status: "missing" };
|
|
8814
9027
|
}
|
|
8815
|
-
if (this.activeReplayStreams.has(streamId)) {
|
|
9028
|
+
if (this.state.activeReplayStreams.has(streamId)) {
|
|
8816
9029
|
return { status: "conflict" };
|
|
8817
9030
|
}
|
|
8818
|
-
this.activeReplayStreams.add(streamId);
|
|
9031
|
+
this.state.activeReplayStreams.add(streamId);
|
|
8819
9032
|
let released = false;
|
|
8820
9033
|
return {
|
|
8821
9034
|
status: "acquired",
|
|
8822
9035
|
release: async () => {
|
|
8823
9036
|
if (!released) {
|
|
8824
9037
|
released = true;
|
|
8825
|
-
this.activeReplayStreams.delete(streamId);
|
|
9038
|
+
this.state.activeReplayStreams.delete(streamId);
|
|
8826
9039
|
}
|
|
8827
9040
|
}
|
|
8828
9041
|
};
|
|
@@ -8836,7 +9049,7 @@ var InMemoryEventStore = class {
|
|
|
8836
9049
|
if (!lastEvent) {
|
|
8837
9050
|
throw new Error(`Event not found for replay: ${lastEventId}`);
|
|
8838
9051
|
}
|
|
8839
|
-
const eventIds = this.streamEvents.get(lastEvent.streamId) || [];
|
|
9052
|
+
const eventIds = this.state.streamEvents.get(lastEvent.streamId) || [];
|
|
8840
9053
|
const index = eventIds.indexOf(lastEventId);
|
|
8841
9054
|
if (index < 0) {
|
|
8842
9055
|
throw new Error(
|
|
@@ -8853,16 +9066,36 @@ var InMemoryEventStore = class {
|
|
|
8853
9066
|
return lastEvent.streamId;
|
|
8854
9067
|
}
|
|
8855
9068
|
};
|
|
8856
|
-
var RedisEventStore = class {
|
|
8857
|
-
client;
|
|
9069
|
+
var RedisEventStore = class _RedisEventStore {
|
|
8858
9070
|
redisUrl;
|
|
8859
9071
|
ttlSeconds;
|
|
8860
9072
|
keyPrefix;
|
|
8861
|
-
|
|
8862
|
-
|
|
9073
|
+
ownerKey;
|
|
9074
|
+
encryptionKey;
|
|
9075
|
+
connection;
|
|
9076
|
+
constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, connection = {}) {
|
|
8863
9077
|
this.redisUrl = redisUrl;
|
|
8864
9078
|
this.ttlSeconds = ttlSeconds;
|
|
8865
9079
|
this.keyPrefix = keyPrefix;
|
|
9080
|
+
this.ownerKey = ownerKey;
|
|
9081
|
+
this.encryptionKey = encryptionKey;
|
|
9082
|
+
this.connection = connection;
|
|
9083
|
+
}
|
|
9084
|
+
get client() {
|
|
9085
|
+
return this.connection.client;
|
|
9086
|
+
}
|
|
9087
|
+
set client(client) {
|
|
9088
|
+
this.connection.client = client;
|
|
9089
|
+
}
|
|
9090
|
+
forOwner(ownerKey) {
|
|
9091
|
+
return new _RedisEventStore(
|
|
9092
|
+
this.redisUrl,
|
|
9093
|
+
this.keyPrefix,
|
|
9094
|
+
this.ttlSeconds,
|
|
9095
|
+
ownerKey,
|
|
9096
|
+
this.encryptionKey,
|
|
9097
|
+
this.connection
|
|
9098
|
+
);
|
|
8866
9099
|
}
|
|
8867
9100
|
counterKey() {
|
|
8868
9101
|
return `${this.keyPrefix}:counter`;
|
|
@@ -8877,9 +9110,9 @@ var RedisEventStore = class {
|
|
|
8877
9110
|
return `${this.keyPrefix}:stream:${assertSafeRedisId(streamId, "streamId")}:replay-lease`;
|
|
8878
9111
|
}
|
|
8879
9112
|
async ensureConnected() {
|
|
8880
|
-
if (!this.client) {
|
|
9113
|
+
if (!this.connection.client) {
|
|
8881
9114
|
const { createClient } = await import("redis");
|
|
8882
|
-
this.client = createClient({
|
|
9115
|
+
this.connection.client = createClient({
|
|
8883
9116
|
url: this.redisUrl,
|
|
8884
9117
|
// node-redis v6 defaults to RESP3, a 5-second command timeout, and a
|
|
8885
9118
|
// 30-second keepalive delay. Keep the event store's v5 behavior stable
|
|
@@ -8888,7 +9121,7 @@ var RedisEventStore = class {
|
|
|
8888
9121
|
socket: { keepAliveInitialDelay: 5e3 },
|
|
8889
9122
|
commandOptions: { timeout: void 0 }
|
|
8890
9123
|
});
|
|
8891
|
-
this.client.on("error", (error) => {
|
|
9124
|
+
this.connection.client.on("error", (error) => {
|
|
8892
9125
|
Logger.error("Redis event store error", {
|
|
8893
9126
|
metadata: {
|
|
8894
9127
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -8896,22 +9129,22 @@ var RedisEventStore = class {
|
|
|
8896
9129
|
});
|
|
8897
9130
|
});
|
|
8898
9131
|
}
|
|
8899
|
-
if (this.client.isOpen) {
|
|
9132
|
+
if (this.connection.client.isOpen) {
|
|
8900
9133
|
return;
|
|
8901
9134
|
}
|
|
8902
|
-
if (!this.connectPromise) {
|
|
8903
|
-
this.connectPromise = this.client.connect().catch((error) => {
|
|
8904
|
-
this.connectPromise = void 0;
|
|
9135
|
+
if (!this.connection.connectPromise) {
|
|
9136
|
+
this.connection.connectPromise = this.connection.client.connect().catch((error) => {
|
|
9137
|
+
this.connection.connectPromise = void 0;
|
|
8905
9138
|
throw error;
|
|
8906
9139
|
});
|
|
8907
9140
|
}
|
|
8908
|
-
await this.connectPromise;
|
|
9141
|
+
await this.connection.connectPromise;
|
|
8909
9142
|
}
|
|
8910
9143
|
getConnectedClient() {
|
|
8911
|
-
if (!this.client) {
|
|
9144
|
+
if (!this.connection.client) {
|
|
8912
9145
|
throw new Error("Redis client not initialized");
|
|
8913
9146
|
}
|
|
8914
|
-
return this.client;
|
|
9147
|
+
return this.connection.client;
|
|
8915
9148
|
}
|
|
8916
9149
|
async storeEvent(streamId, message) {
|
|
8917
9150
|
await this.ensureConnected();
|
|
@@ -8923,8 +9156,9 @@ var RedisEventStore = class {
|
|
|
8923
9156
|
const tx = client.multi();
|
|
8924
9157
|
tx.hSet(eventKey, {
|
|
8925
9158
|
streamId,
|
|
9159
|
+
ownerKey: this.ownerKey,
|
|
8926
9160
|
sequence: String(sequence),
|
|
8927
|
-
message:
|
|
9161
|
+
message: encryptEventPayload(message, this.encryptionKey)
|
|
8928
9162
|
});
|
|
8929
9163
|
tx.expire(eventKey, this.ttlSeconds);
|
|
8930
9164
|
tx.zAdd(streamKey, {
|
|
@@ -8940,11 +9174,10 @@ var RedisEventStore = class {
|
|
|
8940
9174
|
return void 0;
|
|
8941
9175
|
}
|
|
8942
9176
|
await this.ensureConnected();
|
|
8943
|
-
const
|
|
8944
|
-
this.eventKey(eventId)
|
|
8945
|
-
"streamId"
|
|
9177
|
+
const event = await this.getConnectedClient().hGetAll(
|
|
9178
|
+
this.eventKey(eventId)
|
|
8946
9179
|
);
|
|
8947
|
-
return streamId || void 0;
|
|
9180
|
+
return event.ownerKey === this.ownerKey ? event.streamId || void 0 : void 0;
|
|
8948
9181
|
}
|
|
8949
9182
|
async acquireReplayLease(eventId) {
|
|
8950
9183
|
const streamId = await this.getStreamIdForEventId(eventId);
|
|
@@ -8983,7 +9216,7 @@ var RedisEventStore = class {
|
|
|
8983
9216
|
const client = this.getConnectedClient();
|
|
8984
9217
|
const baseEvent = await client.hGetAll(this.eventKey(lastEventId));
|
|
8985
9218
|
const streamId = baseEvent.streamId;
|
|
8986
|
-
if (!streamId) {
|
|
9219
|
+
if (!streamId || baseEvent.ownerKey !== this.ownerKey) {
|
|
8987
9220
|
throw new Error(`Event not found for replay: ${lastEventId}`);
|
|
8988
9221
|
}
|
|
8989
9222
|
const baseScore = Number(baseEvent.sequence);
|
|
@@ -9000,17 +9233,17 @@ var RedisEventStore = class {
|
|
|
9000
9233
|
}
|
|
9001
9234
|
const tx = client.multi();
|
|
9002
9235
|
for (const eventId of eventIds) {
|
|
9003
|
-
tx.
|
|
9236
|
+
tx.hGetAll(this.eventKey(eventId));
|
|
9004
9237
|
}
|
|
9005
|
-
const
|
|
9238
|
+
const encodedEvents = await tx.exec();
|
|
9006
9239
|
for (const [index, eventId] of eventIds.entries()) {
|
|
9007
|
-
const
|
|
9008
|
-
if (typeof
|
|
9240
|
+
const event = encodedEvents[index];
|
|
9241
|
+
if (typeof event !== "object" || event === null || !("message" in event) || !("ownerKey" in event) || event.ownerKey !== this.ownerKey || typeof event.message !== "string") {
|
|
9009
9242
|
continue;
|
|
9010
9243
|
}
|
|
9011
9244
|
let message;
|
|
9012
9245
|
try {
|
|
9013
|
-
message =
|
|
9246
|
+
message = decryptEventPayload(event.message, this.encryptionKey);
|
|
9014
9247
|
} catch {
|
|
9015
9248
|
continue;
|
|
9016
9249
|
}
|
|
@@ -9019,17 +9252,17 @@ var RedisEventStore = class {
|
|
|
9019
9252
|
return streamId;
|
|
9020
9253
|
}
|
|
9021
9254
|
async close() {
|
|
9022
|
-
if (!this.client) {
|
|
9255
|
+
if (!this.connection.client) {
|
|
9023
9256
|
return;
|
|
9024
9257
|
}
|
|
9025
|
-
if (!this.client.isOpen && this.connectPromise) {
|
|
9258
|
+
if (!this.connection.client.isOpen && this.connection.connectPromise) {
|
|
9026
9259
|
try {
|
|
9027
|
-
await this.connectPromise;
|
|
9260
|
+
await this.connection.connectPromise;
|
|
9028
9261
|
} catch {
|
|
9029
9262
|
}
|
|
9030
9263
|
}
|
|
9031
|
-
if (this.client.isOpen) {
|
|
9032
|
-
await this.client.close();
|
|
9264
|
+
if (this.connection.client.isOpen) {
|
|
9265
|
+
await this.connection.client.close();
|
|
9033
9266
|
}
|
|
9034
9267
|
}
|
|
9035
9268
|
};
|
|
@@ -9037,29 +9270,41 @@ function createManagedEventStore(config) {
|
|
|
9037
9270
|
if (config.eventStore.mode === "off") {
|
|
9038
9271
|
return {
|
|
9039
9272
|
mode: "off",
|
|
9273
|
+
eventStoreForOwner: () => void 0,
|
|
9040
9274
|
close: async () => {
|
|
9041
9275
|
}
|
|
9042
9276
|
};
|
|
9043
9277
|
}
|
|
9044
9278
|
if (config.eventStore.mode === "memory") {
|
|
9045
|
-
const memoryStore = new InMemoryEventStore(
|
|
9279
|
+
const memoryStore = new InMemoryEventStore(
|
|
9280
|
+
config.eventStore.ttlSeconds,
|
|
9281
|
+
"internal"
|
|
9282
|
+
);
|
|
9046
9283
|
return {
|
|
9047
9284
|
mode: "memory",
|
|
9048
|
-
|
|
9049
|
-
acquireReplayLease: (eventId) => memoryStore.acquireReplayLease(eventId),
|
|
9285
|
+
eventStoreForOwner: (ownerKey) => memoryStore.forOwner(ownerKey),
|
|
9286
|
+
acquireReplayLease: (eventId, ownerKey) => memoryStore.forOwner(ownerKey).acquireReplayLease(eventId),
|
|
9050
9287
|
close: async () => {
|
|
9051
9288
|
}
|
|
9052
9289
|
};
|
|
9053
9290
|
}
|
|
9291
|
+
const encryptionKey = config.eventStore.encryptionKey;
|
|
9292
|
+
if (!encryptionKey) {
|
|
9293
|
+
throw new Error(
|
|
9294
|
+
"Redis event store requires MCP_EVENT_ENCRYPTION_KEY configuration"
|
|
9295
|
+
);
|
|
9296
|
+
}
|
|
9054
9297
|
const redisStore = new RedisEventStore(
|
|
9055
9298
|
config.eventStore.redisUrl,
|
|
9056
9299
|
config.eventStore.redisKeyPrefix,
|
|
9057
|
-
config.eventStore.ttlSeconds
|
|
9300
|
+
config.eventStore.ttlSeconds,
|
|
9301
|
+
"internal",
|
|
9302
|
+
encryptionKey
|
|
9058
9303
|
);
|
|
9059
9304
|
return {
|
|
9060
9305
|
mode: "redis",
|
|
9061
|
-
|
|
9062
|
-
acquireReplayLease: (eventId) => redisStore.acquireReplayLease(eventId),
|
|
9306
|
+
eventStoreForOwner: (ownerKey) => redisStore.forOwner(ownerKey),
|
|
9307
|
+
acquireReplayLease: (eventId, ownerKey) => redisStore.forOwner(ownerKey).acquireReplayLease(eventId),
|
|
9063
9308
|
close: async () => {
|
|
9064
9309
|
await redisStore.close();
|
|
9065
9310
|
}
|
|
@@ -9195,6 +9440,25 @@ function parseConfiguredToolDomains(rawValue = process.env.PORTKEY_TOOL_DOMAINS?
|
|
|
9195
9440
|
}
|
|
9196
9441
|
return normalizeToolDomains(requestedDomains);
|
|
9197
9442
|
}
|
|
9443
|
+
function resolveToolDomains(requestedDomains) {
|
|
9444
|
+
const configuredDomains = parseConfiguredToolDomains();
|
|
9445
|
+
if (!configuredDomains) {
|
|
9446
|
+
return requestedDomains ? normalizeToolDomains(requestedDomains) : void 0;
|
|
9447
|
+
}
|
|
9448
|
+
if (!requestedDomains) {
|
|
9449
|
+
return configuredDomains;
|
|
9450
|
+
}
|
|
9451
|
+
const configuredDomainSet = new Set(configuredDomains);
|
|
9452
|
+
const disallowedDomains = normalizeToolDomains(requestedDomains).filter(
|
|
9453
|
+
(domain) => !configuredDomainSet.has(domain)
|
|
9454
|
+
);
|
|
9455
|
+
if (disallowedDomains.length > 0) {
|
|
9456
|
+
throw new Error(
|
|
9457
|
+
`Requested tool domains are not allowed by PORTKEY_TOOL_DOMAINS: ${disallowedDomains.join(", ")}`
|
|
9458
|
+
);
|
|
9459
|
+
}
|
|
9460
|
+
return normalizeToolDomains(requestedDomains);
|
|
9461
|
+
}
|
|
9198
9462
|
function createMcpServer(options = {}) {
|
|
9199
9463
|
const service = getSharedPortkeyService();
|
|
9200
9464
|
const server = new McpServer(
|
|
@@ -9213,7 +9477,7 @@ function createMcpServer(options = {}) {
|
|
|
9213
9477
|
);
|
|
9214
9478
|
registerServerPromptsAndResources(server);
|
|
9215
9479
|
registerAllTools(server, service, {
|
|
9216
|
-
domains: options.toolDomains
|
|
9480
|
+
domains: resolveToolDomains(options.toolDomains)
|
|
9217
9481
|
});
|
|
9218
9482
|
return {
|
|
9219
9483
|
server,
|
|
@@ -9222,6 +9486,8 @@ function createMcpServer(options = {}) {
|
|
|
9222
9486
|
}
|
|
9223
9487
|
|
|
9224
9488
|
// src/lib/security.ts
|
|
9489
|
+
import { createHash } from "node:crypto";
|
|
9490
|
+
import { ipKeyGenerator } from "express-rate-limit";
|
|
9225
9491
|
function parseOrigins(raw) {
|
|
9226
9492
|
return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
9227
9493
|
}
|
|
@@ -9342,6 +9608,65 @@ function hostValidationMiddleware(req, res, next) {
|
|
|
9342
9608
|
}
|
|
9343
9609
|
next();
|
|
9344
9610
|
}
|
|
9611
|
+
var RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS = 5e3;
|
|
9612
|
+
var REDIS_TOKEN_BUCKET_SCRIPT = `
|
|
9613
|
+
local values = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
|
|
9614
|
+
local tokens = tonumber(values[1])
|
|
9615
|
+
local lastRefill = tonumber(values[2])
|
|
9616
|
+
local maxTokens = tonumber(ARGV[1])
|
|
9617
|
+
local windowMs = tonumber(ARGV[2])
|
|
9618
|
+
local refillRate = tonumber(ARGV[3])
|
|
9619
|
+
local now = tonumber(ARGV[4])
|
|
9620
|
+
|
|
9621
|
+
if not tokens or not lastRefill then
|
|
9622
|
+
tokens = maxTokens
|
|
9623
|
+
lastRefill = now
|
|
9624
|
+
end
|
|
9625
|
+
|
|
9626
|
+
local elapsedMs = math.max(0, now - lastRefill)
|
|
9627
|
+
local tokensToAdd = math.floor((elapsedMs / windowMs) * refillRate)
|
|
9628
|
+
if tokensToAdd > 0 then
|
|
9629
|
+
tokens = math.min(maxTokens, tokens + tokensToAdd)
|
|
9630
|
+
lastRefill = now
|
|
9631
|
+
end
|
|
9632
|
+
|
|
9633
|
+
local allowed = 0
|
|
9634
|
+
if tokens > 0 then
|
|
9635
|
+
tokens = tokens - 1
|
|
9636
|
+
allowed = 1
|
|
9637
|
+
end
|
|
9638
|
+
|
|
9639
|
+
redis.call('HSET', KEYS[1], 'tokens', tokens, 'lastRefill', lastRefill)
|
|
9640
|
+
redis.call('PEXPIRE', KEYS[1], math.max(windowMs * 2, 1000))
|
|
9641
|
+
return { allowed, tokens }
|
|
9642
|
+
`;
|
|
9643
|
+
async function consumeRedisRateLimitToken(client, options) {
|
|
9644
|
+
let timeout;
|
|
9645
|
+
const result = await Promise.race([
|
|
9646
|
+
client.eval(REDIS_TOKEN_BUCKET_SCRIPT, {
|
|
9647
|
+
keys: [options.key],
|
|
9648
|
+
arguments: [
|
|
9649
|
+
String(options.maxTokens),
|
|
9650
|
+
String(options.windowMs),
|
|
9651
|
+
String(options.refillRate),
|
|
9652
|
+
String(options.now)
|
|
9653
|
+
]
|
|
9654
|
+
}),
|
|
9655
|
+
new Promise((_resolve, reject) => {
|
|
9656
|
+
timeout = setTimeout(() => {
|
|
9657
|
+
reject(new Error("Redis rate-limit command timed out"));
|
|
9658
|
+
}, options.timeoutMs ?? RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS);
|
|
9659
|
+
})
|
|
9660
|
+
]).finally(() => {
|
|
9661
|
+
if (timeout) {
|
|
9662
|
+
clearTimeout(timeout);
|
|
9663
|
+
}
|
|
9664
|
+
});
|
|
9665
|
+
if (!Array.isArray(result) || result.length < 1) {
|
|
9666
|
+
throw new Error("Invalid response from Redis rate-limit script");
|
|
9667
|
+
}
|
|
9668
|
+
return { allowed: Number(result[0]) === 1 };
|
|
9669
|
+
}
|
|
9345
9670
|
function parsePositiveIntegerEnv(name, fallback) {
|
|
9346
9671
|
const raw = process.env[name];
|
|
9347
9672
|
if (raw === void 0) {
|
|
@@ -9361,20 +9686,71 @@ function parsePositiveIntegerEnv(name, fallback) {
|
|
|
9361
9686
|
});
|
|
9362
9687
|
return fallback;
|
|
9363
9688
|
}
|
|
9689
|
+
function isExplicitlyEnabled2(value) {
|
|
9690
|
+
return /^(1|true|yes)$/i.test(value?.trim() ?? "");
|
|
9691
|
+
}
|
|
9692
|
+
function resolveRateLimitStore() {
|
|
9693
|
+
const store = process.env.RATE_LIMIT_STORE?.trim().toLowerCase() || "memory";
|
|
9694
|
+
if (store !== "memory" && store !== "redis") {
|
|
9695
|
+
throw new Error(
|
|
9696
|
+
`Invalid RATE_LIMIT_STORE value: ${store}. Must be 'memory' or 'redis'`
|
|
9697
|
+
);
|
|
9698
|
+
}
|
|
9699
|
+
return store;
|
|
9700
|
+
}
|
|
9701
|
+
function resolveRateLimitRedisUrl(store) {
|
|
9702
|
+
if (store !== "redis") {
|
|
9703
|
+
return void 0;
|
|
9704
|
+
}
|
|
9705
|
+
const redisUrl = process.env.RATE_LIMIT_REDIS_URL?.trim() || process.env.MCP_REDIS_URL?.trim() || process.env.REDIS_URL?.trim();
|
|
9706
|
+
if (!redisUrl) {
|
|
9707
|
+
throw new Error(
|
|
9708
|
+
"RATE_LIMIT_STORE=redis requires RATE_LIMIT_REDIS_URL, MCP_REDIS_URL, or REDIS_URL"
|
|
9709
|
+
);
|
|
9710
|
+
}
|
|
9711
|
+
let parsedUrl;
|
|
9712
|
+
try {
|
|
9713
|
+
parsedUrl = new URL(redisUrl);
|
|
9714
|
+
} catch {
|
|
9715
|
+
throw new Error(
|
|
9716
|
+
"Rate-limit Redis URL must be a valid redis:// or rediss:// URL"
|
|
9717
|
+
);
|
|
9718
|
+
}
|
|
9719
|
+
if (!["redis:", "rediss:"].includes(parsedUrl.protocol)) {
|
|
9720
|
+
throw new Error("Rate-limit Redis URL must use redis:// or rediss://");
|
|
9721
|
+
}
|
|
9722
|
+
if (process.env.NODE_ENV?.trim().toLowerCase() === "production" && parsedUrl.protocol !== "rediss:") {
|
|
9723
|
+
throw new Error("Rate-limit Redis URL must use rediss:// in production");
|
|
9724
|
+
}
|
|
9725
|
+
return redisUrl;
|
|
9726
|
+
}
|
|
9727
|
+
var rateLimitEnabled = process.env.RATE_LIMIT_ENABLED?.trim().toLowerCase() !== "false";
|
|
9728
|
+
var rateLimitStore = resolveRateLimitStore();
|
|
9729
|
+
if (rateLimitEnabled && rateLimitStore === "memory" && process.env.NODE_ENV?.trim().toLowerCase() === "production" && !isExplicitlyEnabled2(process.env.RATE_LIMIT_SINGLE_PROCESS)) {
|
|
9730
|
+
throw new Error(
|
|
9731
|
+
"Production in-memory rate limiting requires RATE_LIMIT_SINGLE_PROCESS=true. Use RATE_LIMIT_STORE=redis for multi-instance deployments."
|
|
9732
|
+
);
|
|
9733
|
+
}
|
|
9364
9734
|
var RATE_LIMIT_CONFIG = {
|
|
9365
|
-
enabled:
|
|
9735
|
+
enabled: rateLimitEnabled,
|
|
9736
|
+
store: rateLimitStore,
|
|
9366
9737
|
maxTokens: parsePositiveIntegerEnv("RATE_LIMIT_MAX", 60),
|
|
9367
9738
|
windowMs: parsePositiveIntegerEnv("RATE_LIMIT_WINDOW_MS", 6e4),
|
|
9368
9739
|
refillRate: parsePositiveIntegerEnv("RATE_LIMIT_REFILL", 60),
|
|
9369
|
-
maxBuckets: parsePositiveIntegerEnv("RATE_LIMIT_MAX_BUCKETS", 1e4)
|
|
9740
|
+
maxBuckets: parsePositiveIntegerEnv("RATE_LIMIT_MAX_BUCKETS", 1e4),
|
|
9741
|
+
redisUrl: resolveRateLimitRedisUrl(rateLimitStore),
|
|
9742
|
+
redisKeyPrefix: process.env.RATE_LIMIT_REDIS_KEY_PREFIX?.trim() || "mcp:rate-limit"
|
|
9370
9743
|
};
|
|
9371
9744
|
function getRateLimitConfig() {
|
|
9372
9745
|
return RATE_LIMIT_CONFIG;
|
|
9373
9746
|
}
|
|
9374
9747
|
var buckets = /* @__PURE__ */ new Map();
|
|
9375
9748
|
var overflowBucket;
|
|
9376
|
-
function getClientIdentifier(req) {
|
|
9377
|
-
|
|
9749
|
+
function getClientIdentifier(req, res, scope) {
|
|
9750
|
+
const principal = res.locals?.authPrincipal;
|
|
9751
|
+
const principalKey = scope === "principal" && principal ? getPrincipalOwnerKey(principal) : scope;
|
|
9752
|
+
const trustedIp = ipKeyGenerator(req.ip || "unknown");
|
|
9753
|
+
return createHash("sha256").update(scope, "utf8").update("\0", "utf8").update(principalKey, "utf8").update("\0", "utf8").update(trustedIp, "utf8").digest("hex");
|
|
9378
9754
|
}
|
|
9379
9755
|
function refillBucket(bucket, config) {
|
|
9380
9756
|
const now = Date.now();
|
|
@@ -9435,7 +9811,63 @@ function consumeToken(clientId, config) {
|
|
|
9435
9811
|
}
|
|
9436
9812
|
return { allowed: false };
|
|
9437
9813
|
}
|
|
9438
|
-
|
|
9814
|
+
var rateLimitRedisClient;
|
|
9815
|
+
var rateLimitRedisCreatePromise;
|
|
9816
|
+
var rateLimitRedisConnectPromise;
|
|
9817
|
+
async function createRateLimitRedisClient(config) {
|
|
9818
|
+
const { createClient } = await import("redis");
|
|
9819
|
+
const client = createClient({
|
|
9820
|
+
url: config.redisUrl,
|
|
9821
|
+
RESP: 2,
|
|
9822
|
+
socket: { keepAliveInitialDelay: 5e3 },
|
|
9823
|
+
commandOptions: { timeout: RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS }
|
|
9824
|
+
});
|
|
9825
|
+
client.on("error", (error) => {
|
|
9826
|
+
Logger.error("Redis rate-limit store error", {
|
|
9827
|
+
metadata: {
|
|
9828
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9829
|
+
}
|
|
9830
|
+
});
|
|
9831
|
+
});
|
|
9832
|
+
return client;
|
|
9833
|
+
}
|
|
9834
|
+
async function getRateLimitRedisClient(config) {
|
|
9835
|
+
if (!config.redisUrl) {
|
|
9836
|
+
throw new Error("Rate-limit Redis URL is not configured");
|
|
9837
|
+
}
|
|
9838
|
+
if (!rateLimitRedisClient) {
|
|
9839
|
+
rateLimitRedisCreatePromise ??= createRateLimitRedisClient(config).catch(
|
|
9840
|
+
(error) => {
|
|
9841
|
+
rateLimitRedisCreatePromise = void 0;
|
|
9842
|
+
throw error;
|
|
9843
|
+
}
|
|
9844
|
+
);
|
|
9845
|
+
rateLimitRedisClient = await rateLimitRedisCreatePromise;
|
|
9846
|
+
}
|
|
9847
|
+
if (!rateLimitRedisClient.isOpen) {
|
|
9848
|
+
rateLimitRedisConnectPromise ??= rateLimitRedisClient.connect().catch((error) => {
|
|
9849
|
+
rateLimitRedisConnectPromise = void 0;
|
|
9850
|
+
throw error;
|
|
9851
|
+
});
|
|
9852
|
+
await rateLimitRedisConnectPromise;
|
|
9853
|
+
}
|
|
9854
|
+
return rateLimitRedisClient;
|
|
9855
|
+
}
|
|
9856
|
+
function applyRateLimitDecision(allowed, config, req, res, next, clientId) {
|
|
9857
|
+
if (allowed) {
|
|
9858
|
+
next();
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
9861
|
+
const retryAfterMs = Math.ceil(config.windowMs / config.refillRate);
|
|
9862
|
+
Logger.warn("Rate limit exceeded", {
|
|
9863
|
+
path: req.path,
|
|
9864
|
+
method: req.method,
|
|
9865
|
+
metadata: { clientFingerprint: clientId.slice(0, 16), retryAfterMs }
|
|
9866
|
+
});
|
|
9867
|
+
res.setHeader("Retry-After", Math.ceil(retryAfterMs / 1e3).toString());
|
|
9868
|
+
res.status(429).json({ error: "Too Many Requests" });
|
|
9869
|
+
}
|
|
9870
|
+
function applyRateLimit(scope, req, res, next) {
|
|
9439
9871
|
const config = getRateLimitConfig();
|
|
9440
9872
|
if (!config.enabled) {
|
|
9441
9873
|
next();
|
|
@@ -9445,20 +9877,69 @@ function rateLimitMiddleware(req, res, next) {
|
|
|
9445
9877
|
next();
|
|
9446
9878
|
return;
|
|
9447
9879
|
}
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
if (!allowed) {
|
|
9451
|
-
const retryAfterMs = Math.ceil(config.windowMs / config.refillRate);
|
|
9452
|
-
Logger.warn("Rate limit exceeded", {
|
|
9453
|
-
path: req.path,
|
|
9454
|
-
method: req.method,
|
|
9455
|
-
metadata: { clientId, retryAfterMs }
|
|
9456
|
-
});
|
|
9457
|
-
res.setHeader("Retry-After", Math.ceil(retryAfterMs / 1e3).toString());
|
|
9458
|
-
res.status(429).json({ error: "Too Many Requests" });
|
|
9880
|
+
if (scope === "authentication" && req.path !== "/mcp") {
|
|
9881
|
+
next();
|
|
9459
9882
|
return;
|
|
9460
9883
|
}
|
|
9461
|
-
|
|
9884
|
+
const clientId = getClientIdentifier(req, res, scope);
|
|
9885
|
+
if (config.store === "memory") {
|
|
9886
|
+
const { allowed } = consumeToken(clientId, config);
|
|
9887
|
+
applyRateLimitDecision(allowed, config, req, res, next, clientId);
|
|
9888
|
+
return;
|
|
9889
|
+
}
|
|
9890
|
+
return (async () => {
|
|
9891
|
+
try {
|
|
9892
|
+
const client = await getRateLimitRedisClient(config);
|
|
9893
|
+
const { allowed } = await consumeRedisRateLimitToken(client, {
|
|
9894
|
+
key: `${config.redisKeyPrefix}:${clientId}`,
|
|
9895
|
+
maxTokens: config.maxTokens,
|
|
9896
|
+
windowMs: config.windowMs,
|
|
9897
|
+
refillRate: config.refillRate,
|
|
9898
|
+
now: Date.now()
|
|
9899
|
+
});
|
|
9900
|
+
applyRateLimitDecision(allowed, config, req, res, next, clientId);
|
|
9901
|
+
} catch (error) {
|
|
9902
|
+
Logger.error("Rate-limit store unavailable", {
|
|
9903
|
+
path: req.path,
|
|
9904
|
+
method: req.method,
|
|
9905
|
+
metadata: {
|
|
9906
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9907
|
+
}
|
|
9908
|
+
});
|
|
9909
|
+
res.status(503).json({ error: "Rate limit service unavailable" });
|
|
9910
|
+
}
|
|
9911
|
+
})();
|
|
9912
|
+
}
|
|
9913
|
+
function rateLimitMiddleware(req, res, next) {
|
|
9914
|
+
return applyRateLimit("authentication", req, res, next);
|
|
9915
|
+
}
|
|
9916
|
+
function principalRateLimitMiddleware(req, res, next) {
|
|
9917
|
+
return applyRateLimit("principal", req, res, next);
|
|
9918
|
+
}
|
|
9919
|
+
async function closeRateLimitStore() {
|
|
9920
|
+
if (!rateLimitRedisClient && rateLimitRedisCreatePromise) {
|
|
9921
|
+
try {
|
|
9922
|
+
rateLimitRedisClient = await rateLimitRedisCreatePromise;
|
|
9923
|
+
} catch {
|
|
9924
|
+
rateLimitRedisCreatePromise = void 0;
|
|
9925
|
+
return;
|
|
9926
|
+
}
|
|
9927
|
+
}
|
|
9928
|
+
if (!rateLimitRedisClient) {
|
|
9929
|
+
return;
|
|
9930
|
+
}
|
|
9931
|
+
if (!rateLimitRedisClient.isOpen && rateLimitRedisConnectPromise) {
|
|
9932
|
+
try {
|
|
9933
|
+
await rateLimitRedisConnectPromise;
|
|
9934
|
+
} catch {
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
if (rateLimitRedisClient.isOpen) {
|
|
9938
|
+
await rateLimitRedisClient.close();
|
|
9939
|
+
}
|
|
9940
|
+
rateLimitRedisClient = void 0;
|
|
9941
|
+
rateLimitRedisCreatePromise = void 0;
|
|
9942
|
+
rateLimitRedisConnectPromise = void 0;
|
|
9462
9943
|
}
|
|
9463
9944
|
var cleanupTimer = setInterval(
|
|
9464
9945
|
() => {
|
|
@@ -9495,6 +9976,11 @@ var SessionStore = class {
|
|
|
9495
9976
|
get(sessionId) {
|
|
9496
9977
|
return this.sessions.get(sessionId);
|
|
9497
9978
|
}
|
|
9979
|
+
/** Return a session only when it belongs to the supplied principal. */
|
|
9980
|
+
getOwned(sessionId, ownerKey) {
|
|
9981
|
+
const entry = this.sessions.get(sessionId);
|
|
9982
|
+
return entry?.ownerKey === ownerKey ? entry : void 0;
|
|
9983
|
+
}
|
|
9498
9984
|
/**
|
|
9499
9985
|
* Get a transport by session ID
|
|
9500
9986
|
* @param sessionId - The session identifier
|
|
@@ -9746,6 +10232,16 @@ function respondJsonRpcClientError(res, statusCode, message, code = -32602) {
|
|
|
9746
10232
|
id: null
|
|
9747
10233
|
});
|
|
9748
10234
|
}
|
|
10235
|
+
function getRequestOwnerKey(res) {
|
|
10236
|
+
const principal = res.locals.authPrincipal;
|
|
10237
|
+
if (!principal) {
|
|
10238
|
+
throw new Error("Authenticated MCP request is missing its principal");
|
|
10239
|
+
}
|
|
10240
|
+
return getPrincipalOwnerKey(principal);
|
|
10241
|
+
}
|
|
10242
|
+
function fingerprintCapabilityId(id) {
|
|
10243
|
+
return createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 16);
|
|
10244
|
+
}
|
|
9749
10245
|
function parseRequestedToolDomains(rawQuery) {
|
|
9750
10246
|
if (rawQuery === void 0) {
|
|
9751
10247
|
return void 0;
|
|
@@ -9773,7 +10269,7 @@ function parseRequestedToolDomains(rawQuery) {
|
|
|
9773
10269
|
`Unknown tool domains: ${invalidDomains.join(", ")}. Valid domains: ${TOOL_DOMAIN_NAMES.join(", ")}`
|
|
9774
10270
|
);
|
|
9775
10271
|
}
|
|
9776
|
-
return
|
|
10272
|
+
return resolveToolDomains(normalizedDomains);
|
|
9777
10273
|
}
|
|
9778
10274
|
function areToolDomainsEqual(left, right) {
|
|
9779
10275
|
const normalizedLeft = left ? normalizeToolDomains(left) : [];
|
|
@@ -9845,11 +10341,11 @@ function createHttpAppRuntime() {
|
|
|
9845
10341
|
let closeRuntimeResourcesPromise;
|
|
9846
10342
|
let sigintHandler;
|
|
9847
10343
|
let sigtermHandler;
|
|
9848
|
-
async function handleStatelessRequest(req, res, context, parsedBody, toolDomains) {
|
|
10344
|
+
async function handleStatelessRequest(req, res, context, ownerKey, parsedBody, toolDomains) {
|
|
9849
10345
|
let replayStreamId;
|
|
9850
10346
|
let responseDisconnected = false;
|
|
9851
10347
|
const storedMessages = /* @__PURE__ */ new WeakSet();
|
|
9852
|
-
const sharedEventStore = managedEventStore.
|
|
10348
|
+
const sharedEventStore = managedEventStore.eventStoreForOwner(ownerKey);
|
|
9853
10349
|
const requestEventStore = sharedEventStore ? {
|
|
9854
10350
|
storeEvent: async (streamId, message) => {
|
|
9855
10351
|
replayStreamId ??= streamId;
|
|
@@ -9960,8 +10456,21 @@ function createHttpAppRuntime() {
|
|
|
9960
10456
|
app.use(hostValidationMiddleware);
|
|
9961
10457
|
}
|
|
9962
10458
|
app.use(originValidationMiddleware);
|
|
10459
|
+
const rateLimitConfig = getRateLimitConfig();
|
|
10460
|
+
app.use(
|
|
10461
|
+
expressRateLimit({
|
|
10462
|
+
windowMs: rateLimitConfig.windowMs,
|
|
10463
|
+
limit: rateLimitConfig.maxTokens,
|
|
10464
|
+
standardHeaders: false,
|
|
10465
|
+
legacyHeaders: false,
|
|
10466
|
+
validate: false,
|
|
10467
|
+
skip: (req) => !rateLimitConfig.enabled || req.path !== "/mcp",
|
|
10468
|
+
message: { error: "Too Many Requests" }
|
|
10469
|
+
})
|
|
10470
|
+
);
|
|
9963
10471
|
app.use(rateLimitMiddleware);
|
|
9964
10472
|
app.use(mcpAuthMiddleware);
|
|
10473
|
+
app.use(principalRateLimitMiddleware);
|
|
9965
10474
|
app.use(
|
|
9966
10475
|
(err, _req, res, next) => {
|
|
9967
10476
|
const errorType = err && typeof err === "object" && "type" in err ? err.type : void 0;
|
|
@@ -10118,6 +10627,7 @@ function createHttpAppRuntime() {
|
|
|
10118
10627
|
});
|
|
10119
10628
|
});
|
|
10120
10629
|
app.post("/mcp", async (req, res) => {
|
|
10630
|
+
const ownerKey = getRequestOwnerKey(res);
|
|
10121
10631
|
let requestedToolDomains;
|
|
10122
10632
|
const protocolVersionHeader = getMcpProtocolVersion(req);
|
|
10123
10633
|
try {
|
|
@@ -10154,6 +10664,7 @@ function createHttpAppRuntime() {
|
|
|
10154
10664
|
req,
|
|
10155
10665
|
res,
|
|
10156
10666
|
"POST /mcp stateless",
|
|
10667
|
+
ownerKey,
|
|
10157
10668
|
req.body,
|
|
10158
10669
|
requestedToolDomains
|
|
10159
10670
|
);
|
|
@@ -10165,7 +10676,7 @@ function createHttpAppRuntime() {
|
|
|
10165
10676
|
const sessionId = req.headers["mcp-session-id"];
|
|
10166
10677
|
let transport;
|
|
10167
10678
|
let hasReservedSessionSlot = false;
|
|
10168
|
-
const sessionEntry = sessionId ? sessionStore.
|
|
10679
|
+
const sessionEntry = sessionId ? sessionStore.getOwned(sessionId, ownerKey) : void 0;
|
|
10169
10680
|
if (sessionId && sessionEntry) {
|
|
10170
10681
|
transport = sessionEntry.transport;
|
|
10171
10682
|
if (transport) {
|
|
@@ -10209,10 +10720,11 @@ function createHttpAppRuntime() {
|
|
|
10209
10720
|
hasReservedSessionSlot = true;
|
|
10210
10721
|
const newTransport = new StreamableHTTPServerTransport({
|
|
10211
10722
|
sessionIdGenerator: () => randomUUID2(),
|
|
10212
|
-
eventStore: managedEventStore.
|
|
10723
|
+
eventStore: managedEventStore.eventStoreForOwner(ownerKey),
|
|
10213
10724
|
onsessioninitialized: (id) => {
|
|
10214
10725
|
sessionStore.set(id, {
|
|
10215
10726
|
transport: newTransport,
|
|
10727
|
+
ownerKey,
|
|
10216
10728
|
toolDomains: requestedToolDomains,
|
|
10217
10729
|
protocolVersion: getNegotiatedProtocolVersion(req.body),
|
|
10218
10730
|
createdAt: Date.now(),
|
|
@@ -10220,13 +10732,13 @@ function createHttpAppRuntime() {
|
|
|
10220
10732
|
});
|
|
10221
10733
|
hasReservedSessionSlot = false;
|
|
10222
10734
|
Logger.info("MCP session initialized", {
|
|
10223
|
-
metadata: {
|
|
10735
|
+
metadata: { sessionFingerprint: fingerprintCapabilityId(id) }
|
|
10224
10736
|
});
|
|
10225
10737
|
},
|
|
10226
10738
|
onsessionclosed: (id) => {
|
|
10227
10739
|
sessionStore.delete(id);
|
|
10228
10740
|
Logger.info("MCP session closed", {
|
|
10229
|
-
metadata: {
|
|
10741
|
+
metadata: { sessionFingerprint: fingerprintCapabilityId(id) }
|
|
10230
10742
|
});
|
|
10231
10743
|
}
|
|
10232
10744
|
});
|
|
@@ -10278,6 +10790,7 @@ function createHttpAppRuntime() {
|
|
|
10278
10790
|
}
|
|
10279
10791
|
});
|
|
10280
10792
|
app.get("/mcp", async (req, res) => {
|
|
10793
|
+
const ownerKey = getRequestOwnerKey(res);
|
|
10281
10794
|
let requestedToolDomains;
|
|
10282
10795
|
try {
|
|
10283
10796
|
requestedToolDomains = parseRequestedToolDomains(req.query.tools);
|
|
@@ -10299,7 +10812,8 @@ function createHttpAppRuntime() {
|
|
|
10299
10812
|
);
|
|
10300
10813
|
return;
|
|
10301
10814
|
}
|
|
10302
|
-
|
|
10815
|
+
const replayEventStore = managedEventStore.eventStoreForOwner(ownerKey);
|
|
10816
|
+
if (!replayEventStore || !managedEventStore.acquireReplayLease) {
|
|
10303
10817
|
res.setHeader("Allow", "POST");
|
|
10304
10818
|
respondJsonRpcClientError(
|
|
10305
10819
|
res,
|
|
@@ -10336,12 +10850,11 @@ function createHttpAppRuntime() {
|
|
|
10336
10850
|
respondJsonRpcClientError(res, 400, protocolError, -32e3);
|
|
10337
10851
|
return;
|
|
10338
10852
|
}
|
|
10339
|
-
const replayEventStore = managedEventStore.eventStore;
|
|
10340
|
-
if (!replayEventStore) {
|
|
10341
|
-
return;
|
|
10342
|
-
}
|
|
10343
10853
|
try {
|
|
10344
|
-
const lease = await managedEventStore.acquireReplayLease(
|
|
10854
|
+
const lease = await managedEventStore.acquireReplayLease(
|
|
10855
|
+
lastEventId,
|
|
10856
|
+
ownerKey
|
|
10857
|
+
);
|
|
10345
10858
|
if (lease.status !== "acquired") {
|
|
10346
10859
|
if (lease.status === "missing") {
|
|
10347
10860
|
respondJsonRpcClientError(
|
|
@@ -10452,7 +10965,7 @@ data:
|
|
|
10452
10965
|
});
|
|
10453
10966
|
return;
|
|
10454
10967
|
}
|
|
10455
|
-
const sessionEntry = sessionStore.
|
|
10968
|
+
const sessionEntry = sessionStore.getOwned(sessionId, ownerKey);
|
|
10456
10969
|
const transport = sessionEntry?.transport;
|
|
10457
10970
|
if (transport) {
|
|
10458
10971
|
const protocolError = validateRequiredProtocolVersion(
|
|
@@ -10485,6 +10998,7 @@ data:
|
|
|
10485
10998
|
}
|
|
10486
10999
|
});
|
|
10487
11000
|
app.delete("/mcp", async (req, res) => {
|
|
11001
|
+
const ownerKey = getRequestOwnerKey(res);
|
|
10488
11002
|
if (!isStatefulSessionMode) {
|
|
10489
11003
|
res.status(405).json({
|
|
10490
11004
|
jsonrpc: "2.0",
|
|
@@ -10509,7 +11023,7 @@ data:
|
|
|
10509
11023
|
});
|
|
10510
11024
|
return;
|
|
10511
11025
|
}
|
|
10512
|
-
const sessionEntry = sessionStore.
|
|
11026
|
+
const sessionEntry = sessionStore.getOwned(sessionId, ownerKey);
|
|
10513
11027
|
const transport = sessionEntry?.transport;
|
|
10514
11028
|
if (transport) {
|
|
10515
11029
|
const protocolError = validateRequiredProtocolVersion(
|
|
@@ -10537,7 +11051,9 @@ data:
|
|
|
10537
11051
|
const expiredIds = await sessionStore.cleanup(config.sessionTimeout);
|
|
10538
11052
|
for (const id of expiredIds) {
|
|
10539
11053
|
Logger.info("MCP session expired and cleaned up", {
|
|
10540
|
-
metadata: {
|
|
11054
|
+
metadata: {
|
|
11055
|
+
sessionFingerprint: fingerprintCapabilityId(id)
|
|
11056
|
+
}
|
|
10541
11057
|
});
|
|
10542
11058
|
}
|
|
10543
11059
|
} catch (error) {
|
|
@@ -10584,7 +11100,7 @@ data:
|
|
|
10584
11100
|
);
|
|
10585
11101
|
await Promise.allSettled(activeReplayOperations);
|
|
10586
11102
|
} finally {
|
|
10587
|
-
await managedEventStore.close();
|
|
11103
|
+
await Promise.all([managedEventStore.close(), closeRateLimitStore()]);
|
|
10588
11104
|
}
|
|
10589
11105
|
})();
|
|
10590
11106
|
}
|
|
@@ -10631,7 +11147,7 @@ data:
|
|
|
10631
11147
|
console.log(" GET /ready - Readiness check");
|
|
10632
11148
|
console.log(" POST /mcp - MCP requests");
|
|
10633
11149
|
console.log(
|
|
10634
|
-
` GET /mcp - ${isStatefulSessionMode ? "SSE notifications and replay" : managedEventStore.
|
|
11150
|
+
` GET /mcp - ${isStatefulSessionMode ? "SSE notifications and replay" : managedEventStore.mode !== "off" ? "SSE replay (requires Last-Event-ID)" : "Unavailable (event store off)"}`
|
|
10635
11151
|
);
|
|
10636
11152
|
console.log(
|
|
10637
11153
|
` DELETE /mcp - ${isStatefulSessionMode ? "Close session" : "Not used in stateless mode"}`
|