portkey-admin-mcp 0.8.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 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 + test + build + e2e + verify)
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;
@@ -2684,6 +2689,14 @@ function buildConfigPayload(params) {
2684
2689
  function hasConfigSettings(config) {
2685
2690
  return config.cache !== void 0 || config.retry !== void 0 || config.strategy !== void 0 || config.targets !== void 0;
2686
2691
  }
2692
+ var createConfigSchema = z5.object(CONFIGS_TOOL_SCHEMAS.createConfig).superRefine((value, ctx) => {
2693
+ if (!hasConfigSettings(buildConfigPayload(value))) {
2694
+ ctx.addIssue({
2695
+ code: z5.ZodIssueCode.custom,
2696
+ message: "At least one config setting (cache_mode, retry_attempts, strategy_mode, or targets) must be provided"
2697
+ });
2698
+ }
2699
+ });
2687
2700
  function registerConfigsTools(server, service) {
2688
2701
  server.tool(
2689
2702
  "list_configs",
@@ -2749,25 +2762,15 @@ function registerConfigsTools(server, service) {
2749
2762
  "Create a config that defines routing, cache, retry, and targets for requests; use update_config to modify an existing one and list_config_versions for history. At least one setting is required, new configs become active immediately once referenced by a key or prompt, and the call returns the new id and version_id.",
2750
2763
  CONFIGS_TOOL_SCHEMAS.createConfig,
2751
2764
  async (params) => {
2752
- const config = buildConfigPayload(params);
2753
- if (!hasConfigSettings(config)) {
2754
- return {
2755
- content: [
2756
- {
2757
- type: "text",
2758
- text: "Error creating configuration: At least one config setting (cache_mode, retry_attempts, strategy_mode, or targets) must be provided"
2759
- }
2760
- ],
2761
- isError: true
2762
- };
2763
- }
2765
+ const validated = createConfigSchema.parse(params);
2766
+ const config = buildConfigPayload(validated);
2764
2767
  const result = await service.configs.createConfig({
2765
- name: params.name,
2768
+ name: validated.name,
2766
2769
  config,
2767
- workspace_id: params.workspace_id
2770
+ workspace_id: validated.workspace_id
2768
2771
  });
2769
2772
  return jsonResult({
2770
- message: `Successfully created configuration "${params.name}"`,
2773
+ message: `Successfully created configuration "${validated.name}"`,
2771
2774
  id: result.id,
2772
2775
  version_id: result.version_id
2773
2776
  });
@@ -3396,6 +3399,16 @@ var INTEGRATIONS_TOOL_SCHEMAS = {
3396
3399
  default_provider_slug: z8.string().optional().describe("Default slug for providers auto-created in workspaces")
3397
3400
  }
3398
3401
  };
3402
+ var updateIntegrationWorkspacesSchema = z8.object(INTEGRATIONS_TOOL_SCHEMAS.updateIntegrationWorkspaces).superRefine((value, ctx) => {
3403
+ const hasGlobalLimits = value.global_credit_limit !== void 0 || value.global_alert_threshold !== void 0 || value.global_rate_limit_rpm !== void 0;
3404
+ if (hasGlobalLimits && value.global_workspace_access_enabled === void 0) {
3405
+ ctx.addIssue({
3406
+ code: z8.ZodIssueCode.custom,
3407
+ path: ["global_workspace_access_enabled"],
3408
+ message: "global_workspace_access_enabled is required when setting global limits."
3409
+ });
3410
+ }
3411
+ });
3399
3412
  function buildIntegrationConfigurations(params) {
3400
3413
  const configurations = {};
3401
3414
  if (params.api_version !== void 0)
@@ -3673,19 +3686,8 @@ function registerIntegrationsTools(server, service) {
3673
3686
  idempotentHint: true,
3674
3687
  openWorldHint: true
3675
3688
  },
3676
- async (params) => {
3677
- const hasGlobalLimits = params.global_credit_limit !== void 0 || params.global_alert_threshold !== void 0 || params.global_rate_limit_rpm !== void 0;
3678
- if (hasGlobalLimits && params.global_workspace_access_enabled === void 0) {
3679
- return {
3680
- content: [
3681
- {
3682
- type: "text",
3683
- text: "global_workspace_access_enabled is required when setting global limits."
3684
- }
3685
- ],
3686
- isError: true
3687
- };
3688
- }
3689
+ async (rawParams) => {
3690
+ const params = updateIntegrationWorkspacesSchema.parse(rawParams);
3689
3691
  const globalUsageLimits = params.global_credit_limit !== void 0 || params.global_alert_threshold !== void 0 ? [
3690
3692
  {
3691
3693
  type: "cost",
@@ -4163,23 +4165,21 @@ var LABELS_TOOL_SCHEMAS = {
4163
4165
  label_id: z10.string().describe("Label ID to delete")
4164
4166
  }
4165
4167
  };
4168
+ var createPromptLabelSchema = z10.object(LABELS_TOOL_SCHEMAS.createPromptLabel).superRefine((value, ctx) => {
4169
+ if (!value.organisation_id && !value.workspace_id) {
4170
+ ctx.addIssue({
4171
+ code: z10.ZodIssueCode.custom,
4172
+ message: "Either organisation_id or workspace_id is required"
4173
+ });
4174
+ }
4175
+ });
4166
4176
  function registerLabelsTools(server, service) {
4167
4177
  server.tool(
4168
4178
  "create_prompt_label",
4169
4179
  "Create a prompt label for tagging prompt versions such as production, staging, or experiment. Requires either organisation_id or workspace_id to set scope, returns the new label id, and does not assign it to any versions yet.",
4170
4180
  LABELS_TOOL_SCHEMAS.createPromptLabel,
4171
- async (params) => {
4172
- if (!params.organisation_id && !params.workspace_id) {
4173
- return {
4174
- content: [
4175
- {
4176
- type: "text",
4177
- text: "Error: Either organisation_id or workspace_id is required"
4178
- }
4179
- ],
4180
- isError: true
4181
- };
4182
- }
4181
+ async (rawParams) => {
4182
+ const params = createPromptLabelSchema.parse(rawParams);
4183
4183
  const result = await service.labels.createLabel({
4184
4184
  name: params.name,
4185
4185
  organisation_id: params.organisation_id,
@@ -4589,12 +4589,13 @@ var logExportFieldSchema = z12.enum([
4589
4589
  "prompt_slug",
4590
4590
  "metadata"
4591
4591
  ]);
4592
- var getLogInputSchema = z12.object({
4592
+ var getLogShape = {
4593
4593
  log_id: z12.string().describe("Gateway log ID to retrieve"),
4594
4594
  path_format: z12.enum(["v1", "v2"]).optional().describe("Log storage path format; defaults to v1"),
4595
4595
  created_at: z12.iso.datetime({ offset: true }).optional().describe("Log creation timestamp in ISO 8601 format; required for v2"),
4596
4596
  type: z12.enum(["hooks"]).optional().describe("Set to hooks when retrieving a hook execution log")
4597
- }).superRefine((input, context) => {
4597
+ };
4598
+ var getLogSchema = z12.object(getLogShape).superRefine((input, context) => {
4598
4599
  if (input.path_format === "v2" && input.created_at === void 0) {
4599
4600
  context.addIssue({
4600
4601
  code: "custom",
@@ -4675,7 +4676,7 @@ function registerLoggingTools(server, service) {
4675
4676
  "get_log",
4676
4677
  {
4677
4678
  description: "Get one gateway request log by ID, including its request, response, usage, cost, and metadata payload when available. For path_format v2, also provide the log's created_at timestamp.",
4678
- inputSchema: getLogInputSchema,
4679
+ inputSchema: getLogShape,
4679
4680
  annotations: {
4680
4681
  title: "Get Gateway Log",
4681
4682
  readOnlyHint: true,
@@ -4684,7 +4685,8 @@ function registerLoggingTools(server, service) {
4684
4685
  openWorldHint: true
4685
4686
  }
4686
4687
  },
4687
- async (params) => {
4688
+ async (rawParams) => {
4689
+ const params = getLogSchema.parse(rawParams);
4688
4690
  const result = await service.logging.getLog(params.log_id, {
4689
4691
  path_format: params.path_format,
4690
4692
  created_at: params.created_at,
@@ -4976,6 +4978,17 @@ var MCP_INTEGRATIONS_TOOL_SCHEMAS = {
4976
4978
  ).min(1).describe("Array of workspace access updates")
4977
4979
  }
4978
4980
  };
4981
+ var createMcpIntegrationSchema = z13.object(MCP_INTEGRATIONS_TOOL_SCHEMAS.createMcpIntegration).superRefine((value, ctx) => {
4982
+ if (value.auth_type === "headers" && (!value.custom_headers || Object.keys(value.custom_headers).length === 0) && !value.secret_mappings?.some(
4983
+ (mapping) => mapping.target_field === "configurations.custom_headers"
4984
+ )) {
4985
+ ctx.addIssue({
4986
+ code: z13.ZodIssueCode.custom,
4987
+ path: ["custom_headers"],
4988
+ message: "custom_headers must be provided when auth_type is 'headers'"
4989
+ });
4990
+ }
4991
+ });
4979
4992
  function isRecord2(value) {
4980
4993
  return typeof value === "object" && value !== null;
4981
4994
  }
@@ -5054,20 +5067,8 @@ function registerMcpIntegrationsTools(server, service) {
5054
5067
  idempotentHint: false,
5055
5068
  openWorldHint: true
5056
5069
  },
5057
- async (params) => {
5058
- if (params.auth_type === "headers" && (!params.custom_headers || Object.keys(params.custom_headers).length === 0) && !params.secret_mappings?.some(
5059
- (mapping) => mapping.target_field === "configurations.custom_headers"
5060
- )) {
5061
- return {
5062
- content: [
5063
- {
5064
- type: "text",
5065
- text: "Error: custom_headers must be provided when auth_type is 'headers'"
5066
- }
5067
- ],
5068
- isError: true
5069
- };
5070
- }
5070
+ async (rawParams) => {
5071
+ const params = createMcpIntegrationSchema.parse(rawParams);
5071
5072
  const { custom_headers, ...rest } = params;
5072
5073
  const result = await service.mcpIntegrations.createMcpIntegration({
5073
5074
  ...rest,
@@ -5904,6 +5905,28 @@ var PROMPTS_TOOL_SCHEMAS = {
5904
5905
  )
5905
5906
  }
5906
5907
  };
5908
+ var createPromptSchema = z17.object(PROMPTS_TOOL_SCHEMAS.createPrompt).superRefine((value, ctx) => {
5909
+ if (!value.model && !value.ai_model_id && !value.finetune_id) {
5910
+ ctx.addIssue({
5911
+ code: z17.ZodIssueCode.custom,
5912
+ message: "At least one of model, ai_model_id, or finetune_id must be provided"
5913
+ });
5914
+ }
5915
+ if (value.string === void 0 && value.messages === void 0) {
5916
+ ctx.addIssue({
5917
+ code: z17.ZodIssueCode.custom,
5918
+ message: "Provide either string or messages"
5919
+ });
5920
+ }
5921
+ });
5922
+ var migratePromptSchema = z17.object(PROMPTS_TOOL_SCHEMAS.migratePrompt).superRefine((value, ctx) => {
5923
+ if (value.string === void 0 && value.messages === void 0) {
5924
+ ctx.addIssue({
5925
+ code: z17.ZodIssueCode.custom,
5926
+ message: "Provide either string or messages"
5927
+ });
5928
+ }
5929
+ });
5907
5930
  function normalizePromptTemplateString(params) {
5908
5931
  if (params.string !== void 0) {
5909
5932
  return params.string;
@@ -5978,30 +6001,9 @@ function registerPromptsTools(server, service) {
5978
6001
  "create_prompt",
5979
6002
  "Create a new prompt template and initial version. Use this for first-time setup; use migrate_prompt for idempotent CI/CD flows. Accepts plain text or structured chat messages, creates a new version immediately, and returns the prompt id, slug, and version id. For multi-message chat prompts pass messages (preferred) or a JSON-encoded array as string.",
5980
6003
  PROMPTS_TOOL_SCHEMAS.createPrompt,
5981
- async (params) => {
5982
- if (!params.model && !params.ai_model_id && !params.finetune_id) {
5983
- return {
5984
- content: [
5985
- {
5986
- type: "text",
5987
- text: "Error creating prompt: At least one of model, ai_model_id, or finetune_id must be provided"
5988
- }
5989
- ],
5990
- isError: true
5991
- };
5992
- }
6004
+ async (rawParams) => {
6005
+ const params = createPromptSchema.parse(rawParams);
5993
6006
  const templateString = normalizePromptTemplateString(params);
5994
- if (templateString === void 0) {
5995
- return {
5996
- content: [
5997
- {
5998
- type: "text",
5999
- text: "Error creating prompt: Provide either string or messages"
6000
- }
6001
- ],
6002
- isError: true
6003
- };
6004
- }
6005
6007
  if (params.dry_run) {
6006
6008
  return jsonResult({
6007
6009
  dry_run: true,
@@ -6111,14 +6113,18 @@ function registerPromptsTools(server, service) {
6111
6113
  });
6112
6114
  if (dry_run) {
6113
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
+ }
6114
6122
  return jsonResult({
6115
6123
  dry_run: true,
6116
6124
  action: "update",
6117
6125
  message: `Would update prompt "${current.name}"`,
6118
6126
  current_version: current.current_version?.version_number ?? null,
6119
- changes: Object.keys(updateData).filter(
6120
- (k) => updateData[k] !== void 0
6121
- )
6127
+ changes
6122
6128
  });
6123
6129
  }
6124
6130
  const result = await service.prompts.updatePrompt(prompt_id, {
@@ -6256,19 +6262,9 @@ function registerPromptsTools(server, service) {
6256
6262
  idempotentHint: true,
6257
6263
  openWorldHint: false
6258
6264
  },
6259
- async (params) => {
6265
+ async (rawParams) => {
6266
+ const params = migratePromptSchema.parse(rawParams);
6260
6267
  const templateString = normalizePromptTemplateString(params);
6261
- if (templateString === void 0) {
6262
- return {
6263
- content: [
6264
- {
6265
- type: "text",
6266
- text: "Error migrating prompt: Provide either string or messages"
6267
- }
6268
- ],
6269
- isError: true
6270
- };
6271
- }
6272
6268
  const result = await service.prompts.migratePrompt({
6273
6269
  name: params.name,
6274
6270
  app: params.app,
@@ -7234,7 +7230,7 @@ var scimWorkspaceMappingBaseShape = {
7234
7230
  workspace_id: z22.string().describe("Portkey workspace ID that the SCIM group should access"),
7235
7231
  role: z22.enum(["admin", "member", "manager"]).describe("Workspace role automatically granted to group members")
7236
7232
  };
7237
- var createScimWorkspaceMappingInputSchema = z22.object({
7233
+ var createScimWorkspaceMappingShape = {
7238
7234
  ...scimWorkspaceMappingBaseShape,
7239
7235
  scim_group_id: z22.string().min(1).optional().describe(
7240
7236
  "Existing identity-provider SCIM group ID; provide this or scim_group_name, but not both"
@@ -7242,7 +7238,8 @@ var createScimWorkspaceMappingInputSchema = z22.object({
7242
7238
  scim_group_name: z22.string().min(1).optional().describe(
7243
7239
  "Existing SCIM group display name; provide this or scim_group_id, but not both"
7244
7240
  )
7245
- }).superRefine((input, context) => {
7241
+ };
7242
+ var createScimWorkspaceMappingSchema = z22.object(createScimWorkspaceMappingShape).superRefine((input, context) => {
7246
7243
  if (Boolean(input.scim_group_id) === Boolean(input.scim_group_name)) {
7247
7244
  context.addIssue({
7248
7245
  code: "custom",
@@ -7397,7 +7394,7 @@ function registerWorkspacesTools(server, service) {
7397
7394
  "create_scim_workspace_mapping",
7398
7395
  {
7399
7396
  description: "Map one identity-provider SCIM group to a Portkey workspace role so current and future group members receive access automatically. Provide exactly one of scim_group_id or scim_group_name; a name can pre-create the Portkey SCIM group before the IdP provisions it. Use list_scim_groups to discover existing groups and list_workspaces for the workspace ID. This changes access provisioning and is distinct from add_workspace_member, which grants one user directly.",
7400
- inputSchema: createScimWorkspaceMappingInputSchema,
7397
+ inputSchema: createScimWorkspaceMappingShape,
7401
7398
  annotations: {
7402
7399
  title: "Create SCIM Workspace Mapping",
7403
7400
  readOnlyHint: false,
@@ -7406,7 +7403,8 @@ function registerWorkspacesTools(server, service) {
7406
7403
  openWorldHint: true
7407
7404
  }
7408
7405
  },
7409
- async (params) => {
7406
+ async (rawParams) => {
7407
+ const params = createScimWorkspaceMappingSchema.parse(rawParams);
7410
7408
  let request;
7411
7409
  if (params.scim_group_id !== void 0) {
7412
7410
  request = {
@@ -7767,7 +7765,25 @@ function augmentToolDescription(toolName, description) {
7767
7765
  }
7768
7766
  return `${description} ${ENTERPRISE_GATED_DESCRIPTION_NOTE}`;
7769
7767
  }
7768
+ function formatZodIssues(error) {
7769
+ return error.issues.map((issue) => {
7770
+ const path = issue.path.join(".");
7771
+ if (!path) {
7772
+ return issue.message;
7773
+ }
7774
+ const namesPath = new RegExp(`\\b${escapeRegExp(path)}\\b`).test(
7775
+ issue.message
7776
+ );
7777
+ return namesPath ? issue.message : `${path}: ${issue.message}`;
7778
+ }).join("; ");
7779
+ }
7780
+ function escapeRegExp(value) {
7781
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7782
+ }
7770
7783
  function getToolErrorMessage(error) {
7784
+ if (error instanceof z23.ZodError) {
7785
+ return formatZodIssues(error);
7786
+ }
7771
7787
  if (error instanceof Error && error.message) {
7772
7788
  return error.message;
7773
7789
  }
@@ -7914,14 +7930,14 @@ function wrapToolCallback(toolName, callback) {
7914
7930
  return normalizeToolResult(await callback(...args));
7915
7931
  } catch (error) {
7916
7932
  const message = getToolErrorMessage(error);
7933
+ const isValidationError = error instanceof z23.ZodError;
7934
+ const text = isValidationError ? `Invalid arguments for "${toolName}": ${message}` : `Tool "${toolName}" failed: ${message}`;
7917
7935
  Logger.error("Tool callback failed", {
7918
7936
  error: message,
7919
7937
  metadata: { toolName }
7920
7938
  });
7921
7939
  return normalizeToolResult({
7922
- content: [
7923
- { type: "text", text: `Tool "${toolName}" failed: ${message}` }
7924
- ],
7940
+ content: [{ type: "text", text }],
7925
7941
  isError: true
7926
7942
  });
7927
7943
  }
@@ -8233,6 +8249,11 @@ function createMcpServer(options = {}) {
8233
8249
  // src/index.ts
8234
8250
  async function main() {
8235
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
+ }
8236
8257
  const { server } = createMcpServer();
8237
8258
  const transport = new StdioServerTransport();
8238
8259
  await server.connect(transport);