portkey-admin-mcp 0.5.0 → 0.6.1

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 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. Point at a self-hosted Portkey gateway if needed. Loopback/private-network hosts are rejected unless `PORTKEY_ALLOW_PRIVATE_BASE_URL=true` |
197
- | `PORTKEY_ALLOW_PRIVATE_BASE_URL` | — | Set to `true` to allow a `PORTKEY_BASE_URL` on loopback or a private network (e.g. a self-hosted gateway at `http://localhost:8787`) |
198
- | `PORTKEY_TOOL_DOMAINS` | — | Optional comma-separated stdio/HTTP default tool subset, e.g. `prompts,analytics` |
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
- | `MCP_REDIS_URL` | — | Redis URL for shared event store |
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
- | `RATE_LIMIT_MAX_BUCKETS` | `10000` | Maximum distinct in-memory rate-limit buckets before new clients share an overflow bucket |
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 a shared Redis event store so `GET /mcp` can replay an interrupted SSE stream from `Last-Event-ID` without a session ID
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
- if (baseUrlOverride !== void 0) {
201
- this.baseUrl = baseUrlOverride.replace(/\/+$/, "");
202
- } else {
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 ?? parseConfiguredToolDomains()
8578
+ domains: resolveToolDomains(options.toolDomains)
8554
8579
  });
8555
8580
  return {
8556
8581
  server,