portkey-admin-mcp 0.9.0 โ 0.10.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 +20 -1
- package/build/index.js +71 -57
- package/build/server.js +251 -154
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
16
16
|
<br>
|
|
17
17
|
<a href="https://github.com/CodesWhat/portkey-admin-mcp/actions/workflows/ci.yml"><img src="https://github.com/CodesWhat/portkey-admin-mcp/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
18
|
+
<a href="https://securityscorecards.dev/viewer/?uri=github.com/CodesWhat/portkey-admin-mcp"><img src="https://img.shields.io/ossf-scorecard/github.com/CodesWhat/portkey-admin-mcp?label=openssf+scorecard&style=flat" alt="OpenSSF Scorecard"></a>
|
|
19
|
+
<a href="https://www.bestpractices.dev/projects/14031"><img src="https://www.bestpractices.dev/projects/14031/badge" alt="OpenSSF Best Practices"></a>
|
|
18
20
|
<a href="https://github.com/punkpeye/awesome-mcp-servers"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome MCP Servers"></a>
|
|
19
21
|
<a href="https://lobehub.com/mcp/codeswhat-portkey-admin-mcp"><img src="https://lobehub.com/badge/mcp/codeswhat-portkey-admin-mcp" alt="LobeHub MCP"></a>
|
|
20
22
|
</p>
|
|
@@ -34,8 +36,13 @@
|
|
|
34
36
|
- [๐งฐ What You Can Do](#what-you-can-do)
|
|
35
37
|
- [๐ API Key Scopes](#api-key-scopes)
|
|
36
38
|
- [๐ HTTP Server (Experimental)](#http-server)
|
|
39
|
+
- [๐๏ธ Architecture](./docs/ARCHITECTURE.md)
|
|
37
40
|
- [๐ Prisma AIRS interoperability](./docs/PRISMA_AIRS_INTEROPERABILITY.md)
|
|
41
|
+
- [๐ Verify a release](./docs/VERIFY_RELEASE.md)
|
|
38
42
|
- [๐ ๏ธ Development](#development)
|
|
43
|
+
- [๐ค Contributing](./CONTRIBUTING.md)
|
|
44
|
+
- [๐๏ธ Governance](./GOVERNANCE.md)
|
|
45
|
+
- [๐ก๏ธ Security assurance](./SECURITY-ASSURANCE.md)
|
|
39
46
|
- [๐งพ Full tool list โ ENDPOINTS.md](./ENDPOINTS.md)
|
|
40
47
|
|
|
41
48
|
<hr>
|
|
@@ -219,6 +226,7 @@ For local-only HTTP use, leave `MCP_HOST` at its default `127.0.0.1`. Set `MCP_H
|
|
|
219
226
|
| `MCP_MAX_SESSIONS` | `100` | Maximum concurrent stateful sessions or active stateless request handlers |
|
|
220
227
|
| `MCP_EVENT_STORE` | `off` | `off`, `memory`, or `redis`; stateless `GET /mcp` replay requires `memory` or `redis` |
|
|
221
228
|
| `MCP_EVENT_TTL_SECONDS` | `300` | Replay retention in seconds |
|
|
229
|
+
| `MCP_EVENT_STORE_COMMAND_TIMEOUT_MS` | `5000` | Redis command timeout for the event store, in milliseconds; `0` disables the timeout (restores unbounded pre-v6 behavior) |
|
|
222
230
|
| `MCP_REDIS_URL` | โ | Redis URL for shared event store; production requires `rediss://` and ACL-scoped credentials |
|
|
223
231
|
| `MCP_EVENT_ENCRYPTION_KEY` | โ | Required 32-byte base64 AES key for Redis replay payloads; generate with `openssl rand -base64 32` |
|
|
224
232
|
| `MCP_REDIS_KEY_PREFIX` | `mcp:event-store` | Dedicated Redis namespace for replay data |
|
|
@@ -283,13 +291,24 @@ docker run \
|
|
|
283
291
|
npm run dev # stdio with hot reload
|
|
284
292
|
npm run dev:http # HTTP with hot reload
|
|
285
293
|
npm test # unit + contract tests
|
|
294
|
+
npm run test:coverage # unit + contract tests with the enforced 80% line floor
|
|
286
295
|
npm run test:e2e # MCP protocol tests
|
|
287
296
|
npm run test:http # HTTP endpoint smoke test
|
|
288
|
-
npm run ci # full pipeline (lint + typecheck +
|
|
297
|
+
npm run ci # full pipeline (lint + typecheck + coverage + build + e2e + verify)
|
|
289
298
|
```
|
|
290
299
|
|
|
300
|
+
The required CI and release gates measure every TypeScript source file and fail
|
|
301
|
+
below 80% line coverage. The current full report is 99.36% lines, 92.61%
|
|
302
|
+
branches, and 99.01% functions.
|
|
303
|
+
|
|
291
304
|
`npm run dev:http` now requires `MCP_AUTH_MODE=bearer` or `MCP_AUTH_MODE=clerk` by default. For deliberate local-only unauthenticated testing, set `MCP_ALLOW_UNAUTHENTICATED_HTTP=true`.
|
|
292
305
|
|
|
306
|
+
Contributions use pull requests and the checks documented in
|
|
307
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md). Project decisions and maintainer roles are
|
|
308
|
+
documented in [`GOVERNANCE.md`](GOVERNANCE.md). Report vulnerabilities through
|
|
309
|
+
[`SECURITY.md`](SECURITY.md), and see [`SECURITY-ASSURANCE.md`](SECURITY-ASSURANCE.md)
|
|
310
|
+
for the public threat model and assurance case.
|
|
311
|
+
|
|
293
312
|
---
|
|
294
313
|
|
|
295
314
|
<div align="center">
|
package/build/index.js
CHANGED
|
@@ -3,6 +3,59 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
+
// src/lib/logger.ts
|
|
7
|
+
var LOG_LEVELS = {
|
|
8
|
+
debug: 0,
|
|
9
|
+
info: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
error: 3
|
|
12
|
+
};
|
|
13
|
+
function getConfiguredLogLevel() {
|
|
14
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
15
|
+
if (envLevel && envLevel in LOG_LEVELS) {
|
|
16
|
+
return envLevel;
|
|
17
|
+
}
|
|
18
|
+
return "info";
|
|
19
|
+
}
|
|
20
|
+
var configuredLogLevel = getConfiguredLogLevel();
|
|
21
|
+
function shouldLog(level) {
|
|
22
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[configuredLogLevel];
|
|
23
|
+
}
|
|
24
|
+
function formatEntry(level, message, extra) {
|
|
25
|
+
return {
|
|
26
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27
|
+
level,
|
|
28
|
+
message,
|
|
29
|
+
...extra
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function writeLog(entry) {
|
|
33
|
+
process.stderr.write(`${JSON.stringify(entry)}
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
var Logger = {
|
|
37
|
+
debug(message, extra) {
|
|
38
|
+
if (shouldLog("debug")) {
|
|
39
|
+
writeLog(formatEntry("debug", message, extra));
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
info(message, extra) {
|
|
43
|
+
if (shouldLog("info")) {
|
|
44
|
+
writeLog(formatEntry("info", message, extra));
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
warn(message, extra) {
|
|
48
|
+
if (shouldLog("warn")) {
|
|
49
|
+
writeLog(formatEntry("warn", message, extra));
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
error(message, extra) {
|
|
53
|
+
if (shouldLog("error")) {
|
|
54
|
+
writeLog(formatEntry("error", message, extra));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
6
59
|
// src/lib/mcp-server.ts
|
|
7
60
|
import { readFileSync } from "node:fs";
|
|
8
61
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -64,62 +117,10 @@ async function parseErrorResponse(response) {
|
|
|
64
117
|
}
|
|
65
118
|
}
|
|
66
119
|
|
|
67
|
-
// src/lib/logger.ts
|
|
68
|
-
var LOG_LEVELS = {
|
|
69
|
-
debug: 0,
|
|
70
|
-
info: 1,
|
|
71
|
-
warn: 2,
|
|
72
|
-
error: 3
|
|
73
|
-
};
|
|
74
|
-
function getConfiguredLogLevel() {
|
|
75
|
-
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
76
|
-
if (envLevel && envLevel in LOG_LEVELS) {
|
|
77
|
-
return envLevel;
|
|
78
|
-
}
|
|
79
|
-
return "info";
|
|
80
|
-
}
|
|
81
|
-
var configuredLogLevel = getConfiguredLogLevel();
|
|
82
|
-
function shouldLog(level) {
|
|
83
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[configuredLogLevel];
|
|
84
|
-
}
|
|
85
|
-
function formatEntry(level, message, extra) {
|
|
86
|
-
return {
|
|
87
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
-
level,
|
|
89
|
-
message,
|
|
90
|
-
...extra
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function writeLog(entry) {
|
|
94
|
-
process.stderr.write(`${JSON.stringify(entry)}
|
|
95
|
-
`);
|
|
96
|
-
}
|
|
97
|
-
var Logger = {
|
|
98
|
-
debug(message, extra) {
|
|
99
|
-
if (shouldLog("debug")) {
|
|
100
|
-
writeLog(formatEntry("debug", message, extra));
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
info(message, extra) {
|
|
104
|
-
if (shouldLog("info")) {
|
|
105
|
-
writeLog(formatEntry("info", message, extra));
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
warn(message, extra) {
|
|
109
|
-
if (shouldLog("warn")) {
|
|
110
|
-
writeLog(formatEntry("warn", message, extra));
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
error(message, extra) {
|
|
114
|
-
if (shouldLog("error")) {
|
|
115
|
-
writeLog(formatEntry("error", message, extra));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
120
|
// src/services/base.service.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.portkey.ai/v1";
|
|
122
122
|
var DEFAULT_PUBLIC_BASE_URL = "https://api.portkey.ai";
|
|
123
|
+
var MISSING_API_KEY_PLACEHOLDER = "__PORTKEY_API_KEY_NOT_CONFIGURED__";
|
|
123
124
|
var PRIVATE_BASE_URL_OVERRIDE_HINT = "Set PORTKEY_ALLOW_PRIVATE_BASE_URL=true to allow self-hosted gateways on loopback or private networks.";
|
|
124
125
|
var INSECURE_HTTP_OVERRIDE_HINT = "Set PORTKEY_ALLOW_INSECURE_HTTP=true only for a trusted self-hosted gateway when TLS is unavailable.";
|
|
125
126
|
function isPrivateOrLocalHost(hostname) {
|
|
@@ -233,6 +234,11 @@ var BaseService = class {
|
|
|
233
234
|
return body ? JSON.stringify(body) : void 0;
|
|
234
235
|
}
|
|
235
236
|
async executeRequest(method, path, options = {}) {
|
|
237
|
+
if ((options.authenticate ?? true) && this.apiKey === MISSING_API_KEY_PLACEHOLDER) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"PORTKEY_API_KEY is not configured. Set the PORTKEY_API_KEY environment variable to a valid Portkey Admin API key."
|
|
240
|
+
);
|
|
241
|
+
}
|
|
236
242
|
const requestId = crypto.randomUUID();
|
|
237
243
|
const url = this.buildUrl(path, options.params, options.baseUrl);
|
|
238
244
|
const startTime = Date.now();
|
|
@@ -1795,7 +1801,6 @@ var WorkspacesService = class extends BaseService {
|
|
|
1795
1801
|
|
|
1796
1802
|
// src/services/index.ts
|
|
1797
1803
|
import { createHmac, randomBytes } from "node:crypto";
|
|
1798
|
-
var MISSING_API_KEY_PLACEHOLDER = "__PORTKEY_API_KEY_NOT_CONFIGURED__";
|
|
1799
1804
|
var SERVICE_CACHE_HMAC_KEY = randomBytes(32);
|
|
1800
1805
|
function resolvePortkeyApiKey(apiKey) {
|
|
1801
1806
|
const resolvedApiKey = apiKey ?? process.env.PORTKEY_API_KEY;
|
|
@@ -6108,14 +6113,18 @@ function registerPromptsTools(server, service) {
|
|
|
6108
6113
|
});
|
|
6109
6114
|
if (dry_run) {
|
|
6110
6115
|
const current = await service.prompts.getPrompt(prompt_id);
|
|
6116
|
+
const changes = Object.keys(updateData).filter(
|
|
6117
|
+
(k) => updateData[k] !== void 0
|
|
6118
|
+
);
|
|
6119
|
+
if (messages !== void 0 && !changes.includes("string")) {
|
|
6120
|
+
changes.push("string");
|
|
6121
|
+
}
|
|
6111
6122
|
return jsonResult({
|
|
6112
6123
|
dry_run: true,
|
|
6113
6124
|
action: "update",
|
|
6114
6125
|
message: `Would update prompt "${current.name}"`,
|
|
6115
6126
|
current_version: current.current_version?.version_number ?? null,
|
|
6116
|
-
changes
|
|
6117
|
-
(k) => updateData[k] !== void 0
|
|
6118
|
-
)
|
|
6127
|
+
changes
|
|
6119
6128
|
});
|
|
6120
6129
|
}
|
|
6121
6130
|
const result = await service.prompts.updatePrompt(prompt_id, {
|
|
@@ -8240,6 +8249,11 @@ function createMcpServer(options = {}) {
|
|
|
8240
8249
|
// src/index.ts
|
|
8241
8250
|
async function main() {
|
|
8242
8251
|
try {
|
|
8252
|
+
if (!process.env.PORTKEY_API_KEY) {
|
|
8253
|
+
Logger.warn(
|
|
8254
|
+
"PORTKEY_API_KEY is not set; tool calls will fail until it is configured."
|
|
8255
|
+
);
|
|
8256
|
+
}
|
|
8243
8257
|
const { server } = createMcpServer();
|
|
8244
8258
|
const transport = new StdioServerTransport();
|
|
8245
8259
|
await server.connect(transport);
|
package/build/server.js
CHANGED
|
@@ -137,6 +137,7 @@ var Logger = {
|
|
|
137
137
|
// src/services/base.service.ts
|
|
138
138
|
var DEFAULT_BASE_URL = "https://api.portkey.ai/v1";
|
|
139
139
|
var DEFAULT_PUBLIC_BASE_URL = "https://api.portkey.ai";
|
|
140
|
+
var MISSING_API_KEY_PLACEHOLDER = "__PORTKEY_API_KEY_NOT_CONFIGURED__";
|
|
140
141
|
var PRIVATE_BASE_URL_OVERRIDE_HINT = "Set PORTKEY_ALLOW_PRIVATE_BASE_URL=true to allow self-hosted gateways on loopback or private networks.";
|
|
141
142
|
var INSECURE_HTTP_OVERRIDE_HINT = "Set PORTKEY_ALLOW_INSECURE_HTTP=true only for a trusted self-hosted gateway when TLS is unavailable.";
|
|
142
143
|
function isPrivateOrLocalHost(hostname) {
|
|
@@ -250,6 +251,11 @@ var BaseService = class {
|
|
|
250
251
|
return body ? JSON.stringify(body) : void 0;
|
|
251
252
|
}
|
|
252
253
|
async executeRequest(method, path, options = {}) {
|
|
254
|
+
if ((options.authenticate ?? true) && this.apiKey === MISSING_API_KEY_PLACEHOLDER) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"PORTKEY_API_KEY is not configured. Set the PORTKEY_API_KEY environment variable to a valid Portkey Admin API key."
|
|
257
|
+
);
|
|
258
|
+
}
|
|
253
259
|
const requestId = crypto.randomUUID();
|
|
254
260
|
const url = this.buildUrl(path, options.params, options.baseUrl);
|
|
255
261
|
const startTime = Date.now();
|
|
@@ -1812,7 +1818,6 @@ var WorkspacesService = class extends BaseService {
|
|
|
1812
1818
|
|
|
1813
1819
|
// src/services/index.ts
|
|
1814
1820
|
import { createHmac, randomBytes } from "node:crypto";
|
|
1815
|
-
var MISSING_API_KEY_PLACEHOLDER = "__PORTKEY_API_KEY_NOT_CONFIGURED__";
|
|
1816
1821
|
var SERVICE_CACHE_HMAC_KEY = randomBytes(32);
|
|
1817
1822
|
function resolvePortkeyApiKey(apiKey) {
|
|
1818
1823
|
const resolvedApiKey = apiKey ?? process.env.PORTKEY_API_KEY;
|
|
@@ -6125,14 +6130,18 @@ function registerPromptsTools(server, service) {
|
|
|
6125
6130
|
});
|
|
6126
6131
|
if (dry_run) {
|
|
6127
6132
|
const current = await service.prompts.getPrompt(prompt_id);
|
|
6133
|
+
const changes = Object.keys(updateData).filter(
|
|
6134
|
+
(k) => updateData[k] !== void 0
|
|
6135
|
+
);
|
|
6136
|
+
if (messages !== void 0 && !changes.includes("string")) {
|
|
6137
|
+
changes.push("string");
|
|
6138
|
+
}
|
|
6128
6139
|
return jsonResult({
|
|
6129
6140
|
dry_run: true,
|
|
6130
6141
|
action: "update",
|
|
6131
6142
|
message: `Would update prompt "${current.name}"`,
|
|
6132
6143
|
current_version: current.current_version?.version_number ?? null,
|
|
6133
|
-
changes
|
|
6134
|
-
(k) => updateData[k] !== void 0
|
|
6135
|
-
)
|
|
6144
|
+
changes
|
|
6136
6145
|
});
|
|
6137
6146
|
}
|
|
6138
6147
|
const result = await service.prompts.updatePrompt(prompt_id, {
|
|
@@ -8086,6 +8095,21 @@ function registerAllTools(server, service, options = {}) {
|
|
|
8086
8095
|
// src/lib/auth.ts
|
|
8087
8096
|
import crypto2 from "node:crypto";
|
|
8088
8097
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
8098
|
+
|
|
8099
|
+
// src/lib/request-path.ts
|
|
8100
|
+
function canonicalizeRequestPath(path) {
|
|
8101
|
+
const normalized = path.toLowerCase();
|
|
8102
|
+
return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
8103
|
+
}
|
|
8104
|
+
function isMcpRequestPath(path) {
|
|
8105
|
+
return canonicalizeRequestPath(path) === "/mcp";
|
|
8106
|
+
}
|
|
8107
|
+
function isHealthOrReadyPath(path) {
|
|
8108
|
+
const canonical = canonicalizeRequestPath(path);
|
|
8109
|
+
return canonical === "/health" || canonical === "/ready";
|
|
8110
|
+
}
|
|
8111
|
+
|
|
8112
|
+
// src/lib/auth.ts
|
|
8089
8113
|
function getPrincipalOwnerKey(principal) {
|
|
8090
8114
|
return crypto2.createHash("sha256").update(principal.id, "utf8").digest("hex");
|
|
8091
8115
|
}
|
|
@@ -8306,7 +8330,7 @@ async function verifyClerkToken(token, config) {
|
|
|
8306
8330
|
return authorizeClerkClaims(payload, config);
|
|
8307
8331
|
}
|
|
8308
8332
|
async function mcpAuthMiddleware(req, res, next) {
|
|
8309
|
-
if (!req.path
|
|
8333
|
+
if (!isMcpRequestPath(req.path)) {
|
|
8310
8334
|
next();
|
|
8311
8335
|
return;
|
|
8312
8336
|
}
|
|
@@ -8435,6 +8459,15 @@ function getServerConfig() {
|
|
|
8435
8459
|
`Invalid MCP_EVENT_TTL_SECONDS value: ${eventStoreTtlStr}. Must be a positive integer`
|
|
8436
8460
|
);
|
|
8437
8461
|
}
|
|
8462
|
+
const eventStoreCommandTimeoutStr = (process.env.MCP_EVENT_STORE_COMMAND_TIMEOUT_MS || "5000").trim();
|
|
8463
|
+
const eventStoreCommandTimeoutMs = parseStrictInteger(
|
|
8464
|
+
eventStoreCommandTimeoutStr
|
|
8465
|
+
);
|
|
8466
|
+
if (Number.isNaN(eventStoreCommandTimeoutMs) || eventStoreCommandTimeoutMs < 0) {
|
|
8467
|
+
throw new Error(
|
|
8468
|
+
`Invalid MCP_EVENT_STORE_COMMAND_TIMEOUT_MS value: ${eventStoreCommandTimeoutStr}. Must be a non-negative integer (0 disables the timeout)`
|
|
8469
|
+
);
|
|
8470
|
+
}
|
|
8438
8471
|
const redisUrl = process.env.MCP_REDIS_URL?.trim() || process.env.REDIS_URL?.trim();
|
|
8439
8472
|
if (eventStoreMode === "redis" && !redisUrl) {
|
|
8440
8473
|
throw new Error(
|
|
@@ -8494,7 +8527,8 @@ function getServerConfig() {
|
|
|
8494
8527
|
ttlSeconds: eventStoreTtlSeconds,
|
|
8495
8528
|
redisUrl,
|
|
8496
8529
|
redisKeyPrefix,
|
|
8497
|
-
encryptionKey: eventEncryptionKey
|
|
8530
|
+
encryptionKey: eventEncryptionKey,
|
|
8531
|
+
commandTimeoutMs: eventStoreCommandTimeoutMs
|
|
8498
8532
|
},
|
|
8499
8533
|
protocol,
|
|
8500
8534
|
port,
|
|
@@ -8726,13 +8760,15 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8726
8760
|
keyPrefix;
|
|
8727
8761
|
ownerKey;
|
|
8728
8762
|
encryptionKey;
|
|
8763
|
+
commandTimeoutMs;
|
|
8729
8764
|
connection;
|
|
8730
|
-
constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, connection = {}) {
|
|
8765
|
+
constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, commandTimeoutMs, connection = {}) {
|
|
8731
8766
|
this.redisUrl = redisUrl;
|
|
8732
8767
|
this.ttlSeconds = ttlSeconds;
|
|
8733
8768
|
this.keyPrefix = keyPrefix;
|
|
8734
8769
|
this.ownerKey = ownerKey;
|
|
8735
8770
|
this.encryptionKey = encryptionKey;
|
|
8771
|
+
this.commandTimeoutMs = commandTimeoutMs;
|
|
8736
8772
|
this.connection = connection;
|
|
8737
8773
|
}
|
|
8738
8774
|
get client() {
|
|
@@ -8748,6 +8784,7 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8748
8784
|
this.ttlSeconds,
|
|
8749
8785
|
ownerKey,
|
|
8750
8786
|
this.encryptionKey,
|
|
8787
|
+
this.commandTimeoutMs,
|
|
8751
8788
|
this.connection
|
|
8752
8789
|
);
|
|
8753
8790
|
}
|
|
@@ -8769,11 +8806,19 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8769
8806
|
this.connection.client = createClient({
|
|
8770
8807
|
url: this.redisUrl,
|
|
8771
8808
|
// node-redis v6 defaults to RESP3, a 5-second command timeout, and a
|
|
8772
|
-
// 30-second keepalive delay.
|
|
8773
|
-
//
|
|
8809
|
+
// 30-second keepalive delay. storeEvent/replayEventsAfter sit on the
|
|
8810
|
+
// MCP request-critical path (SSE sends, GET replay), so an unbounded
|
|
8811
|
+
// command wait here would hang requests indefinitely if Redis accepts
|
|
8812
|
+
// TCP but stalls on replies. Keep RESP2 for v5-compatible replies, but
|
|
8813
|
+
// use a bounded, configurable command timeout (default 5s, matching
|
|
8814
|
+
// node-redis's own default) instead of disabling it. Set
|
|
8815
|
+
// MCP_EVENT_STORE_COMMAND_TIMEOUT_MS=0 to restore the old unbounded
|
|
8816
|
+
// (v5-compatible) behavior.
|
|
8774
8817
|
RESP: 2,
|
|
8775
8818
|
socket: { keepAliveInitialDelay: 5e3 },
|
|
8776
|
-
commandOptions: {
|
|
8819
|
+
commandOptions: {
|
|
8820
|
+
timeout: this.commandTimeoutMs === 0 ? void 0 : this.commandTimeoutMs
|
|
8821
|
+
}
|
|
8777
8822
|
});
|
|
8778
8823
|
this.connection.client.on("error", (error) => {
|
|
8779
8824
|
Logger.error("Redis event store error", {
|
|
@@ -8953,7 +8998,8 @@ function createManagedEventStore(config) {
|
|
|
8953
8998
|
config.eventStore.redisKeyPrefix,
|
|
8954
8999
|
config.eventStore.ttlSeconds,
|
|
8955
9000
|
"internal",
|
|
8956
|
-
encryptionKey
|
|
9001
|
+
encryptionKey,
|
|
9002
|
+
config.eventStore.commandTimeoutMs
|
|
8957
9003
|
);
|
|
8958
9004
|
return {
|
|
8959
9005
|
mode: "redis",
|
|
@@ -9229,7 +9275,7 @@ function isAllowedHost(host) {
|
|
|
9229
9275
|
});
|
|
9230
9276
|
}
|
|
9231
9277
|
function originValidationMiddleware(req, res, next) {
|
|
9232
|
-
if (req.path
|
|
9278
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9233
9279
|
next();
|
|
9234
9280
|
return;
|
|
9235
9281
|
}
|
|
@@ -9246,7 +9292,7 @@ function originValidationMiddleware(req, res, next) {
|
|
|
9246
9292
|
next();
|
|
9247
9293
|
}
|
|
9248
9294
|
function hostValidationMiddleware(req, res, next) {
|
|
9249
|
-
if (req.path
|
|
9295
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9250
9296
|
next();
|
|
9251
9297
|
return;
|
|
9252
9298
|
}
|
|
@@ -9378,23 +9424,26 @@ function resolveRateLimitRedisUrl(store) {
|
|
|
9378
9424
|
}
|
|
9379
9425
|
return redisUrl;
|
|
9380
9426
|
}
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9427
|
+
function resolveRateLimitConfig() {
|
|
9428
|
+
const enabled = process.env.RATE_LIMIT_ENABLED?.trim().toLowerCase() !== "false";
|
|
9429
|
+
const store = resolveRateLimitStore();
|
|
9430
|
+
if (enabled && store === "memory" && process.env.NODE_ENV?.trim().toLowerCase() === "production" && !isExplicitlyEnabled2(process.env.RATE_LIMIT_SINGLE_PROCESS)) {
|
|
9431
|
+
throw new Error(
|
|
9432
|
+
"Production in-memory rate limiting requires RATE_LIMIT_SINGLE_PROCESS=true. Use RATE_LIMIT_STORE=redis for multi-instance deployments."
|
|
9433
|
+
);
|
|
9434
|
+
}
|
|
9435
|
+
return {
|
|
9436
|
+
enabled,
|
|
9437
|
+
store,
|
|
9438
|
+
maxTokens: parsePositiveIntegerEnv("RATE_LIMIT_MAX", 60),
|
|
9439
|
+
windowMs: parsePositiveIntegerEnv("RATE_LIMIT_WINDOW_MS", 6e4),
|
|
9440
|
+
refillRate: parsePositiveIntegerEnv("RATE_LIMIT_REFILL", 60),
|
|
9441
|
+
maxBuckets: parsePositiveIntegerEnv("RATE_LIMIT_MAX_BUCKETS", 1e4),
|
|
9442
|
+
redisUrl: resolveRateLimitRedisUrl(store),
|
|
9443
|
+
redisKeyPrefix: process.env.RATE_LIMIT_REDIS_KEY_PREFIX?.trim() || "mcp:rate-limit"
|
|
9444
|
+
};
|
|
9445
|
+
}
|
|
9446
|
+
var RATE_LIMIT_CONFIG = resolveRateLimitConfig();
|
|
9398
9447
|
function getRateLimitConfig() {
|
|
9399
9448
|
return RATE_LIMIT_CONFIG;
|
|
9400
9449
|
}
|
|
@@ -9527,11 +9576,11 @@ function applyRateLimit(scope, req, res, next) {
|
|
|
9527
9576
|
next();
|
|
9528
9577
|
return;
|
|
9529
9578
|
}
|
|
9530
|
-
if (req.path
|
|
9579
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9531
9580
|
next();
|
|
9532
9581
|
return;
|
|
9533
9582
|
}
|
|
9534
|
-
if (scope === "authentication" && req.path
|
|
9583
|
+
if (scope === "authentication" && !isMcpRequestPath(req.path)) {
|
|
9535
9584
|
next();
|
|
9536
9585
|
return;
|
|
9537
9586
|
}
|
|
@@ -10385,14 +10434,16 @@ function createMcpPostHandler(deps) {
|
|
|
10385
10434
|
try {
|
|
10386
10435
|
await transport.handleRequest(req, res, req.body);
|
|
10387
10436
|
} catch (error) {
|
|
10388
|
-
if (hasReservedSessionSlot) {
|
|
10389
|
-
sessionStore.releaseReservation();
|
|
10390
|
-
}
|
|
10391
10437
|
respondJsonRpcInternalError(
|
|
10392
10438
|
res,
|
|
10393
10439
|
sessionId ? "POST /mcp stateful session" : "POST /mcp stateful initialize",
|
|
10394
10440
|
error
|
|
10395
10441
|
);
|
|
10442
|
+
} finally {
|
|
10443
|
+
if (hasReservedSessionSlot) {
|
|
10444
|
+
sessionStore.releaseReservation();
|
|
10445
|
+
hasReservedSessionSlot = false;
|
|
10446
|
+
}
|
|
10396
10447
|
}
|
|
10397
10448
|
};
|
|
10398
10449
|
}
|
|
@@ -10428,143 +10479,150 @@ function createMcpGetHandler(deps) {
|
|
|
10428
10479
|
);
|
|
10429
10480
|
return;
|
|
10430
10481
|
}
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
405,
|
|
10437
|
-
"Stateless GET /mcp replay requires MCP_EVENT_STORE=memory or redis",
|
|
10438
|
-
-32e3
|
|
10439
|
-
);
|
|
10440
|
-
return;
|
|
10441
|
-
}
|
|
10442
|
-
const lastEventId = req.headers["last-event-id"];
|
|
10443
|
-
if (typeof lastEventId !== "string" || lastEventId.length === 0) {
|
|
10444
|
-
res.setHeader("Allow", "POST");
|
|
10445
|
-
respondJsonRpcClientError(
|
|
10446
|
-
res,
|
|
10447
|
-
405,
|
|
10448
|
-
"Stateless GET /mcp only supports replay with Last-Event-ID",
|
|
10449
|
-
-32e3
|
|
10450
|
-
);
|
|
10451
|
-
return;
|
|
10452
|
-
}
|
|
10453
|
-
if (!req.headers.accept?.includes("text/event-stream")) {
|
|
10454
|
-
respondJsonRpcClientError(
|
|
10455
|
-
res,
|
|
10456
|
-
406,
|
|
10457
|
-
"Not Acceptable: Client must accept text/event-stream",
|
|
10458
|
-
-32e3
|
|
10459
|
-
);
|
|
10460
|
-
return;
|
|
10461
|
-
}
|
|
10462
|
-
const protocolError = validateRequiredProtocolVersion(
|
|
10463
|
-
getMcpProtocolVersion(req)
|
|
10464
|
-
);
|
|
10465
|
-
if (protocolError) {
|
|
10466
|
-
respondJsonRpcClientError(res, 400, protocolError, -32e3);
|
|
10467
|
-
return;
|
|
10468
|
-
}
|
|
10482
|
+
let releaseReplaySlot;
|
|
10483
|
+
const replaySlot = new Promise((resolve) => {
|
|
10484
|
+
releaseReplaySlot = resolve;
|
|
10485
|
+
});
|
|
10486
|
+
activeReplayOperations.add(replaySlot);
|
|
10469
10487
|
try {
|
|
10470
|
-
const
|
|
10471
|
-
|
|
10472
|
-
|
|
10488
|
+
const replayEventStore = managedEventStore.eventStoreForOwner(ownerKey);
|
|
10489
|
+
if (!replayEventStore || !managedEventStore.acquireReplayLease) {
|
|
10490
|
+
res.setHeader("Allow", "POST");
|
|
10491
|
+
respondJsonRpcClientError(
|
|
10492
|
+
res,
|
|
10493
|
+
405,
|
|
10494
|
+
"Stateless GET /mcp replay requires MCP_EVENT_STORE=memory or redis",
|
|
10495
|
+
-32e3
|
|
10496
|
+
);
|
|
10497
|
+
return;
|
|
10498
|
+
}
|
|
10499
|
+
const lastEventId = req.headers["last-event-id"];
|
|
10500
|
+
if (typeof lastEventId !== "string" || lastEventId.length === 0) {
|
|
10501
|
+
res.setHeader("Allow", "POST");
|
|
10502
|
+
respondJsonRpcClientError(
|
|
10503
|
+
res,
|
|
10504
|
+
405,
|
|
10505
|
+
"Stateless GET /mcp only supports replay with Last-Event-ID",
|
|
10506
|
+
-32e3
|
|
10507
|
+
);
|
|
10508
|
+
return;
|
|
10509
|
+
}
|
|
10510
|
+
if (!req.headers.accept?.includes("text/event-stream")) {
|
|
10511
|
+
respondJsonRpcClientError(
|
|
10512
|
+
res,
|
|
10513
|
+
406,
|
|
10514
|
+
"Not Acceptable: Client must accept text/event-stream",
|
|
10515
|
+
-32e3
|
|
10516
|
+
);
|
|
10517
|
+
return;
|
|
10518
|
+
}
|
|
10519
|
+
const protocolError = validateRequiredProtocolVersion(
|
|
10520
|
+
getMcpProtocolVersion(req)
|
|
10473
10521
|
);
|
|
10474
|
-
if (
|
|
10475
|
-
|
|
10522
|
+
if (protocolError) {
|
|
10523
|
+
respondJsonRpcClientError(res, 400, protocolError, -32e3);
|
|
10524
|
+
return;
|
|
10525
|
+
}
|
|
10526
|
+
try {
|
|
10527
|
+
const lease = await managedEventStore.acquireReplayLease(
|
|
10528
|
+
lastEventId,
|
|
10529
|
+
ownerKey
|
|
10530
|
+
);
|
|
10531
|
+
if (lease.status !== "acquired") {
|
|
10532
|
+
if (lease.status === "missing") {
|
|
10533
|
+
respondJsonRpcClientError(
|
|
10534
|
+
res,
|
|
10535
|
+
400,
|
|
10536
|
+
"Invalid event ID format",
|
|
10537
|
+
-32e3
|
|
10538
|
+
);
|
|
10539
|
+
return;
|
|
10540
|
+
}
|
|
10476
10541
|
respondJsonRpcClientError(
|
|
10477
10542
|
res,
|
|
10478
|
-
|
|
10479
|
-
"
|
|
10543
|
+
409,
|
|
10544
|
+
"Conflict: Stream already has an active replay connection",
|
|
10480
10545
|
-32e3
|
|
10481
10546
|
);
|
|
10482
10547
|
return;
|
|
10483
10548
|
}
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
const deadline = Date.now() + 25e3;
|
|
10501
|
-
while (getIsReady() && !res.destroyed && !finalResponseSeen) {
|
|
10502
|
-
await replayEventStore.replayEventsAfter(cursor, {
|
|
10503
|
-
send: async (eventId, message) => {
|
|
10504
|
-
cursor = eventId;
|
|
10505
|
-
finalResponseSeen = "id" in message && ("result" in message || "error" in message);
|
|
10506
|
-
if (!res.write(
|
|
10507
|
-
`event: message
|
|
10549
|
+
const replayOperation = (async () => {
|
|
10550
|
+
try {
|
|
10551
|
+
res.status(200);
|
|
10552
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
10553
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
10554
|
+
res.flushHeaders();
|
|
10555
|
+
let cursor = lastEventId;
|
|
10556
|
+
let finalResponseSeen = false;
|
|
10557
|
+
const deadline = Date.now() + 25e3;
|
|
10558
|
+
while (getIsReady() && !res.destroyed && !finalResponseSeen) {
|
|
10559
|
+
await replayEventStore.replayEventsAfter(cursor, {
|
|
10560
|
+
send: async (eventId, message) => {
|
|
10561
|
+
cursor = eventId;
|
|
10562
|
+
finalResponseSeen = "id" in message && ("result" in message || "error" in message);
|
|
10563
|
+
if (!res.write(
|
|
10564
|
+
`event: message
|
|
10508
10565
|
id: ${eventId}
|
|
10509
10566
|
data: ${JSON.stringify(message)}
|
|
10510
10567
|
|
|
10511
10568
|
`
|
|
10512
|
-
|
|
10513
|
-
|
|
10569
|
+
)) {
|
|
10570
|
+
await waitForResponseWritable(res);
|
|
10571
|
+
}
|
|
10514
10572
|
}
|
|
10573
|
+
});
|
|
10574
|
+
if (finalResponseSeen || res.destroyed) {
|
|
10575
|
+
break;
|
|
10515
10576
|
}
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
break;
|
|
10519
|
-
}
|
|
10520
|
-
if (Date.now() >= deadline) {
|
|
10521
|
-
res.write(`id: ${cursor}
|
|
10577
|
+
if (Date.now() >= deadline) {
|
|
10578
|
+
res.write(`id: ${cursor}
|
|
10522
10579
|
data:
|
|
10523
10580
|
|
|
10524
10581
|
`);
|
|
10525
|
-
|
|
10526
|
-
}
|
|
10527
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
10528
|
-
}
|
|
10529
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
10530
|
-
await new Promise((resolveResponse) => {
|
|
10531
|
-
const done = () => resolveResponse();
|
|
10532
|
-
res.once("finish", done);
|
|
10533
|
-
res.once("close", done);
|
|
10534
|
-
res.end();
|
|
10535
|
-
});
|
|
10536
|
-
}
|
|
10537
|
-
} finally {
|
|
10538
|
-
try {
|
|
10539
|
-
await lease.release();
|
|
10540
|
-
} catch (error) {
|
|
10541
|
-
Logger.error("Failed to release stateless replay lease", {
|
|
10542
|
-
metadata: {
|
|
10543
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10582
|
+
break;
|
|
10544
10583
|
}
|
|
10545
|
-
|
|
10584
|
+
await new Promise(
|
|
10585
|
+
(resolvePoll) => setTimeout(resolvePoll, 100)
|
|
10586
|
+
);
|
|
10587
|
+
}
|
|
10588
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
10589
|
+
await new Promise((resolveResponse) => {
|
|
10590
|
+
const done = () => resolveResponse();
|
|
10591
|
+
res.once("finish", done);
|
|
10592
|
+
res.once("close", done);
|
|
10593
|
+
res.end();
|
|
10594
|
+
});
|
|
10595
|
+
}
|
|
10596
|
+
} finally {
|
|
10597
|
+
try {
|
|
10598
|
+
await lease.release();
|
|
10599
|
+
} catch (error) {
|
|
10600
|
+
Logger.error("Failed to release stateless replay lease", {
|
|
10601
|
+
metadata: {
|
|
10602
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10603
|
+
}
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10546
10606
|
}
|
|
10547
|
-
}
|
|
10548
|
-
})();
|
|
10549
|
-
activeReplayOperations.add(replayOperation);
|
|
10550
|
-
try {
|
|
10607
|
+
})();
|
|
10551
10608
|
await replayOperation;
|
|
10552
|
-
}
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10609
|
+
} catch (error) {
|
|
10610
|
+
if (res.headersSent) {
|
|
10611
|
+
Logger.error("GET /mcp stateless replay failed", {
|
|
10612
|
+
metadata: {
|
|
10613
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10614
|
+
}
|
|
10615
|
+
});
|
|
10616
|
+
if (!res.writableEnded) {
|
|
10617
|
+
res.end();
|
|
10560
10618
|
}
|
|
10561
|
-
}
|
|
10562
|
-
|
|
10563
|
-
res.end();
|
|
10619
|
+
} else {
|
|
10620
|
+
respondJsonRpcInternalError(res, "GET /mcp stateless", error);
|
|
10564
10621
|
}
|
|
10565
|
-
} else {
|
|
10566
|
-
respondJsonRpcInternalError(res, "GET /mcp stateless", error);
|
|
10567
10622
|
}
|
|
10623
|
+
} finally {
|
|
10624
|
+
activeReplayOperations.delete(replaySlot);
|
|
10625
|
+
releaseReplaySlot();
|
|
10568
10626
|
}
|
|
10569
10627
|
return;
|
|
10570
10628
|
}
|
|
@@ -10601,6 +10659,45 @@ data:
|
|
|
10601
10659
|
return;
|
|
10602
10660
|
}
|
|
10603
10661
|
sessionStore.touch(sessionId);
|
|
10662
|
+
const lastEventId = req.headers["last-event-id"];
|
|
10663
|
+
if (typeof lastEventId === "string" && lastEventId.length > 0 && managedEventStore.acquireReplayLease) {
|
|
10664
|
+
const lease = await managedEventStore.acquireReplayLease(
|
|
10665
|
+
lastEventId,
|
|
10666
|
+
ownerKey
|
|
10667
|
+
);
|
|
10668
|
+
if (lease.status !== "acquired") {
|
|
10669
|
+
if (lease.status === "missing") {
|
|
10670
|
+
respondJsonRpcClientError(
|
|
10671
|
+
res,
|
|
10672
|
+
400,
|
|
10673
|
+
"Invalid event ID format",
|
|
10674
|
+
-32e3
|
|
10675
|
+
);
|
|
10676
|
+
return;
|
|
10677
|
+
}
|
|
10678
|
+
respondJsonRpcClientError(
|
|
10679
|
+
res,
|
|
10680
|
+
409,
|
|
10681
|
+
"Conflict: Stream already has an active replay connection",
|
|
10682
|
+
-32e3
|
|
10683
|
+
);
|
|
10684
|
+
return;
|
|
10685
|
+
}
|
|
10686
|
+
try {
|
|
10687
|
+
await transport.handleRequest(req, res);
|
|
10688
|
+
} finally {
|
|
10689
|
+
try {
|
|
10690
|
+
await lease.release();
|
|
10691
|
+
} catch (error) {
|
|
10692
|
+
Logger.error("Failed to release stateful replay lease", {
|
|
10693
|
+
metadata: {
|
|
10694
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10695
|
+
}
|
|
10696
|
+
});
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
return;
|
|
10700
|
+
}
|
|
10604
10701
|
await transport.handleRequest(req, res);
|
|
10605
10702
|
} else {
|
|
10606
10703
|
res.status(404).json({
|
|
@@ -10674,9 +10771,9 @@ function createHttpAppRuntime() {
|
|
|
10674
10771
|
const readyCheckMode = getReadyCheckMode();
|
|
10675
10772
|
const requestBodyLimit = process.env.MCP_MAX_REQUEST_SIZE?.trim() || "1mb";
|
|
10676
10773
|
const allowedOrigins = getAllowedOrigins();
|
|
10677
|
-
const corsOriginConfig =
|
|
10678
|
-
|
|
10679
|
-
|
|
10774
|
+
const corsOriginConfig = (origin, callback) => {
|
|
10775
|
+
callback(null, validateOrigin(origin));
|
|
10776
|
+
};
|
|
10680
10777
|
if (allowedOrigins.includes("*") && authConfig.mode === "none") {
|
|
10681
10778
|
Logger.warn(
|
|
10682
10779
|
"CORS wildcard (ALLOWED_ORIGINS=*) combined with MCP_AUTH_MODE=none: all origins are accepted with no authentication. Do not expose this server publicly."
|
|
@@ -10715,7 +10812,6 @@ function createHttpAppRuntime() {
|
|
|
10715
10812
|
} : false
|
|
10716
10813
|
})
|
|
10717
10814
|
);
|
|
10718
|
-
app.use(express.json({ limit: requestBodyLimit }));
|
|
10719
10815
|
if (authConfig.mode === "none") {
|
|
10720
10816
|
app.use(hostValidationMiddleware);
|
|
10721
10817
|
}
|
|
@@ -10728,13 +10824,14 @@ function createHttpAppRuntime() {
|
|
|
10728
10824
|
standardHeaders: false,
|
|
10729
10825
|
legacyHeaders: false,
|
|
10730
10826
|
validate: false,
|
|
10731
|
-
skip: (req) => !rateLimitConfig.enabled || req.path
|
|
10827
|
+
skip: (req) => !rateLimitConfig.enabled || !isMcpRequestPath(req.path),
|
|
10732
10828
|
message: { error: "Too Many Requests" }
|
|
10733
10829
|
})
|
|
10734
10830
|
);
|
|
10735
10831
|
app.use(rateLimitMiddleware);
|
|
10736
10832
|
app.use(mcpAuthMiddleware);
|
|
10737
10833
|
app.use(principalRateLimitMiddleware);
|
|
10834
|
+
app.use(express.json({ limit: requestBodyLimit }));
|
|
10738
10835
|
app.use(
|
|
10739
10836
|
(err, _req, res, next) => {
|
|
10740
10837
|
const errorType = err && typeof err === "object" && "type" in err ? err.type : void 0;
|
package/package.json
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"lint": "biome lint .",
|
|
12
12
|
"format": "biome format --write .",
|
|
13
13
|
"check": "biome check --write .",
|
|
14
|
-
"test": "tsx --test tests/config.test.ts tests/security.test.ts tests/contract.test.ts tests/contract-extended.test.ts tests/unit.test.ts tests/http-server.test.ts tests/tools-observability.test.ts tests/tools-catalog.test.ts tests/tools-platform.test.ts tests/tools-keys.test.ts tests/tools-users.test.ts tests/tools-analytics.test.ts tests/current-api.test.ts tests/release-readiness.test.ts tests/auth-clerk.test.ts",
|
|
14
|
+
"test": "tsx --test tests/config.test.ts tests/security-worker-layout.test.ts tests/security-edge-runtime.test.ts tests/contract.test.ts tests/contract-extended.test.ts tests/unit.test.ts tests/services.test.ts tests/base-services.test.ts tests/http-server.test.ts tests/http-edge-runtime.test.ts tests/http-edge-runtime-final.test.ts tests/tools-observability.test.ts tests/tools-catalog.test.ts tests/tools-prompts-configs.test.ts tests/tools-remaining.test.ts tests/tools-edge-workflows.test.ts tests/tools-platform.test.ts tests/tools-keys.test.ts tests/tools-users.test.ts tests/tools-analytics.test.ts tests/current-api.test.ts tests/release-readiness.test.ts tests/auth-clerk.test.ts tests/lib-event-store.test.ts tests/lib-session-mcp.test.ts",
|
|
15
|
+
"test:coverage": "tsx --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-lines=80 $TEST_COVERAGE_REPORTERS tests/config.test.ts tests/security-worker-layout.test.ts tests/security-edge-runtime.test.ts tests/contract.test.ts tests/contract-extended.test.ts tests/unit.test.ts tests/services.test.ts tests/base-services.test.ts tests/http-server.test.ts tests/http-edge-runtime.test.ts tests/http-edge-runtime-final.test.ts tests/tools-observability.test.ts tests/tools-catalog.test.ts tests/tools-prompts-configs.test.ts tests/tools-remaining.test.ts tests/tools-edge-workflows.test.ts tests/tools-platform.test.ts tests/tools-keys.test.ts tests/tools-users.test.ts tests/tools-analytics.test.ts tests/current-api.test.ts tests/release-readiness.test.ts tests/auth-clerk.test.ts tests/lib-event-store.test.ts tests/lib-session-mcp.test.ts",
|
|
16
|
+
"test:coverage:qlty": "mkdir -p coverage && TEST_COVERAGE_REPORTERS=\"--test-reporter=lcov --test-reporter-destination=$PWD/coverage/lcov.info --test-reporter=spec --test-reporter-destination=stdout\" npm run test:coverage",
|
|
15
17
|
"dev": "tsx --watch src/index.ts",
|
|
16
18
|
"dev:http": "tsx --watch src/server.ts",
|
|
17
19
|
"smoke": "tsx tests/smoke.ts",
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"verify:tool-quality": "node scripts/check-tool-definition-quality.mjs",
|
|
24
26
|
"verify:readme-tools": "node scripts/verify-readme-tools.mjs",
|
|
25
27
|
"knip": "knip",
|
|
26
|
-
"ci": "npm run lint && npm run knip && npm run typecheck && npm run typecheck:tests && npm run test && npm run build && npm run verify:tool-quality && npm run test:e2e && npm run verify:readme-tools",
|
|
28
|
+
"ci": "npm run lint && npm run knip && npm run typecheck && npm run typecheck:tests && npm run test:coverage && npm run build && npm run verify:tool-quality && npm run test:e2e && npm run verify:readme-tools",
|
|
27
29
|
"start:http": "node build/server.js",
|
|
28
30
|
"generate:lobehub-tools": "npm run build && node scripts/generate-lobehub-tools.mjs && biome format --write lhm.plugin.json",
|
|
29
31
|
"update:lobehub": "npm run generate:lobehub-tools && npx -y @lobehub/market-cli plugin update --dir \"$PWD\"",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
},
|
|
54
56
|
"name": "portkey-admin-mcp",
|
|
55
57
|
"mcpName": "io.github.CodesWhat/portkey-admin-mcp",
|
|
56
|
-
"version": "0.
|
|
58
|
+
"version": "0.10.0",
|
|
57
59
|
"main": "build/index.js",
|
|
58
60
|
"keywords": [
|
|
59
61
|
"mcp",
|