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/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<hr>
|
|
27
27
|
|
|
28
28
|
> [!IMPORTANT]
|
|
29
|
-
> **Maintenance mode.** Portkey was acquired by **Palo Alto Networks** (completed 2026‑05‑29) and is being folded into the Prisma AIRS platform. The Portkey Admin API this server targets is **live and unchanged as of June 2026**, and this project still works end‑to‑end — but it is now in **maintenance mode**: security and dependency patches only, no new features, pending Palo Alto's post‑acquisition API roadmap. If the hosted Admin API is ever deprecated, point `PORTKEY_BASE_URL` at a self‑hosted [Portkey gateway](https://github.com/Portkey-AI/gateway). See [docs/audit-2026-06.md](./docs/audit-2026-06.md) for the full assessment.
|
|
29
|
+
> **Maintenance mode.** Portkey was acquired by **Palo Alto Networks** (completed 2026‑05‑29) and is being folded into the Prisma AIRS platform. The Portkey Admin API this server targets is **live and unchanged as of June 2026**, and this project still works end‑to‑end — but it is now in **maintenance mode**: security and dependency patches only, no new features, pending Palo Alto's post‑acquisition API roadmap. If the hosted Admin API is ever deprecated, point `PORTKEY_BASE_URL` at a self‑hosted [Portkey gateway](https://github.com/Portkey-AI/gateway). See [docs/audit-2026-06.md](./docs/audit-2026-06.md) for the full assessment and [security_best_practices_report.md](./security_best_practices_report.md) for the 0.6.0 security-hardening review.
|
|
30
30
|
|
|
31
31
|
<h2 align="center">📑 Contents</h2>
|
|
32
32
|
|
|
@@ -193,24 +193,39 @@ For local-only HTTP use, leave `MCP_HOST` at its default `127.0.0.1`. Set `MCP_H
|
|
|
193
193
|
| Variable | Default | Description |
|
|
194
194
|
|----------|---------|-------------|
|
|
195
195
|
| `PORTKEY_API_KEY` | (required) | Your Portkey API key |
|
|
196
|
-
| `PORTKEY_BASE_URL` | `https://api.portkey.ai/v1` | Portkey Admin API base URL.
|
|
197
|
-
| `PORTKEY_ALLOW_PRIVATE_BASE_URL` | — | Set to `true` to allow a
|
|
198
|
-
| `
|
|
196
|
+
| `PORTKEY_BASE_URL` | `https://api.portkey.ai/v1` | Portkey Admin API base URL. Credentialed requests never auto-follow redirects |
|
|
197
|
+
| `PORTKEY_ALLOW_PRIVATE_BASE_URL` | — | Set to `true` to allow a literal loopback/private `PORTKEY_BASE_URL` |
|
|
198
|
+
| `PORTKEY_ALLOW_INSECURE_HTTP` | — | Separately set to `true` only when a trusted self-hosted gateway cannot use HTTPS |
|
|
199
|
+
| `PORTKEY_TOOL_DOMAINS` | — | Server-side tool-domain allowlist. HTTP `?tools=` may narrow this set but cannot expand it |
|
|
199
200
|
| `MCP_HOST` | `127.0.0.1` | Bind address |
|
|
200
201
|
| `MCP_PORT` | `3000` | Port |
|
|
201
202
|
| `MCP_PUBLIC_BASE_URL` | — | Public absolute base URL to advertise from `/auth/info` and the status page; recommended for hosted deployments |
|
|
202
203
|
| `MCP_AUTH_MODE` | `none` | `none`, `bearer`, or `clerk` (`none` is blocked for HTTP unless explicitly overridden) |
|
|
203
204
|
| `MCP_AUTH_TOKEN` | — | Secret for bearer auth |
|
|
205
|
+
| `CLERK_ISSUER` / `CLERK_AUDIENCE` | — | Required issuer and audience when `MCP_AUTH_MODE=clerk` |
|
|
206
|
+
| `CLERK_ALLOWED_SUBJECTS` | — | Optional CSV subject allowlist for Clerk; at least one Clerk authorization policy is required |
|
|
207
|
+
| `CLERK_ALLOWED_ORGANIZATION_IDS` / `CLERK_ALLOWED_ROLES` | — | Optional CSV organization and role constraints; every configured constraint must match |
|
|
208
|
+
| `CLERK_REQUIRED_PERMISSIONS` | — | Optional CSV permissions that must all be present in the verified Clerk JWT |
|
|
204
209
|
| `MCP_ALLOW_UNAUTHENTICATED_HTTP` | — | Set to `true` only for intentional local unauthenticated HTTP debugging |
|
|
205
210
|
| `MCP_SESSION_MODE` | `stateful` | `stateful` or `stateless` |
|
|
206
211
|
| `MCP_MAX_SESSIONS` | `100` | Maximum concurrent stateful sessions or active stateless request handlers |
|
|
207
212
|
| `MCP_EVENT_STORE` | `off` | `off`, `memory`, or `redis`; stateless `GET /mcp` replay requires `memory` or `redis` |
|
|
208
|
-
| `
|
|
213
|
+
| `MCP_EVENT_TTL_SECONDS` | `300` | Replay retention in seconds |
|
|
214
|
+
| `MCP_REDIS_URL` | — | Redis URL for shared event store; production requires `rediss://` and ACL-scoped credentials |
|
|
215
|
+
| `MCP_EVENT_ENCRYPTION_KEY` | — | Required 32-byte base64 AES key for Redis replay payloads; generate with `openssl rand -base64 32` |
|
|
216
|
+
| `MCP_REDIS_KEY_PREFIX` | `mcp:event-store` | Dedicated Redis namespace for replay data |
|
|
209
217
|
| `MCP_TLS_KEY_PATH` | — | TLS key for native HTTPS |
|
|
210
218
|
| `MCP_TLS_CERT_PATH` | — | TLS cert for native HTTPS |
|
|
211
219
|
| `ALLOWED_ORIGINS` | — | CORS allow-list; also used to validate the `Host` header (DNS-rebinding protection) when `MCP_AUTH_MODE=none` |
|
|
212
220
|
| `MCP_TRUST_PROXY` | `false` | Trust proxy headers (for reverse proxies) |
|
|
213
|
-
| `
|
|
221
|
+
| `RATE_LIMIT_STORE` | `memory` | `redis` for multi-instance/serverless deployments; production memory mode requires `RATE_LIMIT_SINGLE_PROCESS=true` |
|
|
222
|
+
| `RATE_LIMIT_REDIS_URL` | — | Shared limiter Redis URL, falling back to `MCP_REDIS_URL`/`REDIS_URL`; production requires `rediss://` |
|
|
223
|
+
| `RATE_LIMIT_REDIS_KEY_PREFIX` | `mcp:rate-limit` | Redis namespace for atomic pre-authentication IP and principal-plus-IP token buckets |
|
|
224
|
+
| `RATE_LIMIT_MAX_BUCKETS` | `10000` | Maximum local buckets in explicit memory mode before new clients share overflow capacity |
|
|
225
|
+
|
|
226
|
+
Production containers must choose their rate-limit topology explicitly: set
|
|
227
|
+
`RATE_LIMIT_STORE=redis` for multi-instance deployments, or set
|
|
228
|
+
`RATE_LIMIT_SINGLE_PROCESS=true` only for a single long-lived process.
|
|
214
229
|
|
|
215
230
|
<details>
|
|
216
231
|
<summary><strong>Vercel deployment</strong></summary>
|
|
@@ -218,7 +233,7 @@ For local-only HTTP use, leave `MCP_HOST` at its default `127.0.0.1`. Set `MCP_H
|
|
|
218
233
|
Vercel support is kept as a reference proof of concept — we do not run a hosted deployment. See [docs/VERCEL_DEPLOYMENT.md](./docs/VERCEL_DEPLOYMENT.md) if you want to self-deploy.
|
|
219
234
|
|
|
220
235
|
Key points:
|
|
221
|
-
- Uses stateless request handling with
|
|
236
|
+
- Uses stateless request handling with encrypted, principal-bound Redis replay and a shared atomic Redis rate limiter
|
|
222
237
|
- Requires Clerk or bearer auth
|
|
223
238
|
- Leave `MCP_TLS_*` unset (Vercel terminates HTTPS)
|
|
224
239
|
- Set `MCP_PUBLIC_BASE_URL` to your deployment URL so advertised MCP endpoints never depend on request headers
|
package/build/index.js
CHANGED
|
@@ -120,6 +120,7 @@ var Logger = {
|
|
|
120
120
|
// src/services/base.service.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.portkey.ai/v1";
|
|
122
122
|
var PRIVATE_BASE_URL_OVERRIDE_HINT = "Set PORTKEY_ALLOW_PRIVATE_BASE_URL=true to allow self-hosted gateways on loopback or private networks.";
|
|
123
|
+
var INSECURE_HTTP_OVERRIDE_HINT = "Set PORTKEY_ALLOW_INSECURE_HTTP=true only for a trusted self-hosted gateway when TLS is unavailable.";
|
|
123
124
|
function isPrivateOrLocalHost(hostname) {
|
|
124
125
|
const host = hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
125
126
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
@@ -186,6 +187,14 @@ function validateUrl(url) {
|
|
|
186
187
|
`Refusing to use a loopback or private-network PORTKEY_BASE_URL host: ${parsed.hostname}. ${PRIVATE_BASE_URL_OVERRIDE_HINT}`
|
|
187
188
|
);
|
|
188
189
|
}
|
|
190
|
+
const allowInsecureHttp = /^(1|true|yes)$/i.test(
|
|
191
|
+
process.env.PORTKEY_ALLOW_INSECURE_HTTP?.trim() ?? ""
|
|
192
|
+
);
|
|
193
|
+
if (parsed.protocol === "http:" && !allowInsecureHttp) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Refusing insecure HTTP PORTKEY_BASE_URL: ${parsed.origin}. ${INSECURE_HTTP_OVERRIDE_HINT}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
189
198
|
}
|
|
190
199
|
var BaseService = class {
|
|
191
200
|
apiKey;
|
|
@@ -197,13 +206,9 @@ var BaseService = class {
|
|
|
197
206
|
throw new Error("PORTKEY_API_KEY environment variable is not set");
|
|
198
207
|
}
|
|
199
208
|
this.apiKey = apiKey;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const baseUrl = process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
|
|
204
|
-
validateUrl(baseUrl);
|
|
205
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
206
|
-
}
|
|
209
|
+
const baseUrl = baseUrlOverride ?? process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
|
|
210
|
+
validateUrl(baseUrl);
|
|
211
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
207
212
|
}
|
|
208
213
|
encodePathSegment(value) {
|
|
209
214
|
return encodeURIComponent(value);
|
|
@@ -239,6 +244,7 @@ var BaseService = class {
|
|
|
239
244
|
try {
|
|
240
245
|
const response = await fetchWithTimeout(url, {
|
|
241
246
|
method,
|
|
247
|
+
redirect: "manual",
|
|
242
248
|
headers: this.buildHeaders(method),
|
|
243
249
|
body: this.serializeBody(options.body),
|
|
244
250
|
timeout: this.timeout
|
|
@@ -8532,6 +8538,25 @@ function parseConfiguredToolDomains(rawValue = process.env.PORTKEY_TOOL_DOMAINS?
|
|
|
8532
8538
|
}
|
|
8533
8539
|
return normalizeToolDomains(requestedDomains);
|
|
8534
8540
|
}
|
|
8541
|
+
function resolveToolDomains(requestedDomains) {
|
|
8542
|
+
const configuredDomains = parseConfiguredToolDomains();
|
|
8543
|
+
if (!configuredDomains) {
|
|
8544
|
+
return requestedDomains ? normalizeToolDomains(requestedDomains) : void 0;
|
|
8545
|
+
}
|
|
8546
|
+
if (!requestedDomains) {
|
|
8547
|
+
return configuredDomains;
|
|
8548
|
+
}
|
|
8549
|
+
const configuredDomainSet = new Set(configuredDomains);
|
|
8550
|
+
const disallowedDomains = normalizeToolDomains(requestedDomains).filter(
|
|
8551
|
+
(domain) => !configuredDomainSet.has(domain)
|
|
8552
|
+
);
|
|
8553
|
+
if (disallowedDomains.length > 0) {
|
|
8554
|
+
throw new Error(
|
|
8555
|
+
`Requested tool domains are not allowed by PORTKEY_TOOL_DOMAINS: ${disallowedDomains.join(", ")}`
|
|
8556
|
+
);
|
|
8557
|
+
}
|
|
8558
|
+
return normalizeToolDomains(requestedDomains);
|
|
8559
|
+
}
|
|
8535
8560
|
function createMcpServer(options = {}) {
|
|
8536
8561
|
const service = getSharedPortkeyService();
|
|
8537
8562
|
const server = new McpServer(
|
|
@@ -8550,7 +8575,7 @@ function createMcpServer(options = {}) {
|
|
|
8550
8575
|
);
|
|
8551
8576
|
registerServerPromptsAndResources(server);
|
|
8552
8577
|
registerAllTools(server, service, {
|
|
8553
|
-
domains: options.toolDomains
|
|
8578
|
+
domains: resolveToolDomains(options.toolDomains)
|
|
8554
8579
|
});
|
|
8555
8580
|
return {
|
|
8556
8581
|
server,
|