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 +20 -1
- package/build/index.js +178 -157
- package/build/server.js +358 -254
- package/package.json +5 -3
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;
|
|
@@ -2701,6 +2706,14 @@ function buildConfigPayload(params) {
|
|
|
2701
2706
|
function hasConfigSettings(config) {
|
|
2702
2707
|
return config.cache !== void 0 || config.retry !== void 0 || config.strategy !== void 0 || config.targets !== void 0;
|
|
2703
2708
|
}
|
|
2709
|
+
var createConfigSchema = z5.object(CONFIGS_TOOL_SCHEMAS.createConfig).superRefine((value, ctx) => {
|
|
2710
|
+
if (!hasConfigSettings(buildConfigPayload(value))) {
|
|
2711
|
+
ctx.addIssue({
|
|
2712
|
+
code: z5.ZodIssueCode.custom,
|
|
2713
|
+
message: "At least one config setting (cache_mode, retry_attempts, strategy_mode, or targets) must be provided"
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2704
2717
|
function registerConfigsTools(server, service) {
|
|
2705
2718
|
server.tool(
|
|
2706
2719
|
"list_configs",
|
|
@@ -2766,25 +2779,15 @@ function registerConfigsTools(server, service) {
|
|
|
2766
2779
|
"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.",
|
|
2767
2780
|
CONFIGS_TOOL_SCHEMAS.createConfig,
|
|
2768
2781
|
async (params) => {
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
return {
|
|
2772
|
-
content: [
|
|
2773
|
-
{
|
|
2774
|
-
type: "text",
|
|
2775
|
-
text: "Error creating configuration: At least one config setting (cache_mode, retry_attempts, strategy_mode, or targets) must be provided"
|
|
2776
|
-
}
|
|
2777
|
-
],
|
|
2778
|
-
isError: true
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2782
|
+
const validated = createConfigSchema.parse(params);
|
|
2783
|
+
const config = buildConfigPayload(validated);
|
|
2781
2784
|
const result = await service.configs.createConfig({
|
|
2782
|
-
name:
|
|
2785
|
+
name: validated.name,
|
|
2783
2786
|
config,
|
|
2784
|
-
workspace_id:
|
|
2787
|
+
workspace_id: validated.workspace_id
|
|
2785
2788
|
});
|
|
2786
2789
|
return jsonResult({
|
|
2787
|
-
message: `Successfully created configuration "${
|
|
2790
|
+
message: `Successfully created configuration "${validated.name}"`,
|
|
2788
2791
|
id: result.id,
|
|
2789
2792
|
version_id: result.version_id
|
|
2790
2793
|
});
|
|
@@ -3413,6 +3416,16 @@ var INTEGRATIONS_TOOL_SCHEMAS = {
|
|
|
3413
3416
|
default_provider_slug: z8.string().optional().describe("Default slug for providers auto-created in workspaces")
|
|
3414
3417
|
}
|
|
3415
3418
|
};
|
|
3419
|
+
var updateIntegrationWorkspacesSchema = z8.object(INTEGRATIONS_TOOL_SCHEMAS.updateIntegrationWorkspaces).superRefine((value, ctx) => {
|
|
3420
|
+
const hasGlobalLimits = value.global_credit_limit !== void 0 || value.global_alert_threshold !== void 0 || value.global_rate_limit_rpm !== void 0;
|
|
3421
|
+
if (hasGlobalLimits && value.global_workspace_access_enabled === void 0) {
|
|
3422
|
+
ctx.addIssue({
|
|
3423
|
+
code: z8.ZodIssueCode.custom,
|
|
3424
|
+
path: ["global_workspace_access_enabled"],
|
|
3425
|
+
message: "global_workspace_access_enabled is required when setting global limits."
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
});
|
|
3416
3429
|
function buildIntegrationConfigurations(params) {
|
|
3417
3430
|
const configurations = {};
|
|
3418
3431
|
if (params.api_version !== void 0)
|
|
@@ -3690,19 +3703,8 @@ function registerIntegrationsTools(server, service) {
|
|
|
3690
3703
|
idempotentHint: true,
|
|
3691
3704
|
openWorldHint: true
|
|
3692
3705
|
},
|
|
3693
|
-
async (
|
|
3694
|
-
const
|
|
3695
|
-
if (hasGlobalLimits && params.global_workspace_access_enabled === void 0) {
|
|
3696
|
-
return {
|
|
3697
|
-
content: [
|
|
3698
|
-
{
|
|
3699
|
-
type: "text",
|
|
3700
|
-
text: "global_workspace_access_enabled is required when setting global limits."
|
|
3701
|
-
}
|
|
3702
|
-
],
|
|
3703
|
-
isError: true
|
|
3704
|
-
};
|
|
3705
|
-
}
|
|
3706
|
+
async (rawParams) => {
|
|
3707
|
+
const params = updateIntegrationWorkspacesSchema.parse(rawParams);
|
|
3706
3708
|
const globalUsageLimits = params.global_credit_limit !== void 0 || params.global_alert_threshold !== void 0 ? [
|
|
3707
3709
|
{
|
|
3708
3710
|
type: "cost",
|
|
@@ -4180,23 +4182,21 @@ var LABELS_TOOL_SCHEMAS = {
|
|
|
4180
4182
|
label_id: z10.string().describe("Label ID to delete")
|
|
4181
4183
|
}
|
|
4182
4184
|
};
|
|
4185
|
+
var createPromptLabelSchema = z10.object(LABELS_TOOL_SCHEMAS.createPromptLabel).superRefine((value, ctx) => {
|
|
4186
|
+
if (!value.organisation_id && !value.workspace_id) {
|
|
4187
|
+
ctx.addIssue({
|
|
4188
|
+
code: z10.ZodIssueCode.custom,
|
|
4189
|
+
message: "Either organisation_id or workspace_id is required"
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4183
4193
|
function registerLabelsTools(server, service) {
|
|
4184
4194
|
server.tool(
|
|
4185
4195
|
"create_prompt_label",
|
|
4186
4196
|
"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.",
|
|
4187
4197
|
LABELS_TOOL_SCHEMAS.createPromptLabel,
|
|
4188
|
-
async (
|
|
4189
|
-
|
|
4190
|
-
return {
|
|
4191
|
-
content: [
|
|
4192
|
-
{
|
|
4193
|
-
type: "text",
|
|
4194
|
-
text: "Error: Either organisation_id or workspace_id is required"
|
|
4195
|
-
}
|
|
4196
|
-
],
|
|
4197
|
-
isError: true
|
|
4198
|
-
};
|
|
4199
|
-
}
|
|
4198
|
+
async (rawParams) => {
|
|
4199
|
+
const params = createPromptLabelSchema.parse(rawParams);
|
|
4200
4200
|
const result = await service.labels.createLabel({
|
|
4201
4201
|
name: params.name,
|
|
4202
4202
|
organisation_id: params.organisation_id,
|
|
@@ -4606,12 +4606,13 @@ var logExportFieldSchema = z12.enum([
|
|
|
4606
4606
|
"prompt_slug",
|
|
4607
4607
|
"metadata"
|
|
4608
4608
|
]);
|
|
4609
|
-
var
|
|
4609
|
+
var getLogShape = {
|
|
4610
4610
|
log_id: z12.string().describe("Gateway log ID to retrieve"),
|
|
4611
4611
|
path_format: z12.enum(["v1", "v2"]).optional().describe("Log storage path format; defaults to v1"),
|
|
4612
4612
|
created_at: z12.iso.datetime({ offset: true }).optional().describe("Log creation timestamp in ISO 8601 format; required for v2"),
|
|
4613
4613
|
type: z12.enum(["hooks"]).optional().describe("Set to hooks when retrieving a hook execution log")
|
|
4614
|
-
}
|
|
4614
|
+
};
|
|
4615
|
+
var getLogSchema = z12.object(getLogShape).superRefine((input, context) => {
|
|
4615
4616
|
if (input.path_format === "v2" && input.created_at === void 0) {
|
|
4616
4617
|
context.addIssue({
|
|
4617
4618
|
code: "custom",
|
|
@@ -4692,7 +4693,7 @@ function registerLoggingTools(server, service) {
|
|
|
4692
4693
|
"get_log",
|
|
4693
4694
|
{
|
|
4694
4695
|
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.",
|
|
4695
|
-
inputSchema:
|
|
4696
|
+
inputSchema: getLogShape,
|
|
4696
4697
|
annotations: {
|
|
4697
4698
|
title: "Get Gateway Log",
|
|
4698
4699
|
readOnlyHint: true,
|
|
@@ -4701,7 +4702,8 @@ function registerLoggingTools(server, service) {
|
|
|
4701
4702
|
openWorldHint: true
|
|
4702
4703
|
}
|
|
4703
4704
|
},
|
|
4704
|
-
async (
|
|
4705
|
+
async (rawParams) => {
|
|
4706
|
+
const params = getLogSchema.parse(rawParams);
|
|
4705
4707
|
const result = await service.logging.getLog(params.log_id, {
|
|
4706
4708
|
path_format: params.path_format,
|
|
4707
4709
|
created_at: params.created_at,
|
|
@@ -4993,6 +4995,17 @@ var MCP_INTEGRATIONS_TOOL_SCHEMAS = {
|
|
|
4993
4995
|
).min(1).describe("Array of workspace access updates")
|
|
4994
4996
|
}
|
|
4995
4997
|
};
|
|
4998
|
+
var createMcpIntegrationSchema = z13.object(MCP_INTEGRATIONS_TOOL_SCHEMAS.createMcpIntegration).superRefine((value, ctx) => {
|
|
4999
|
+
if (value.auth_type === "headers" && (!value.custom_headers || Object.keys(value.custom_headers).length === 0) && !value.secret_mappings?.some(
|
|
5000
|
+
(mapping) => mapping.target_field === "configurations.custom_headers"
|
|
5001
|
+
)) {
|
|
5002
|
+
ctx.addIssue({
|
|
5003
|
+
code: z13.ZodIssueCode.custom,
|
|
5004
|
+
path: ["custom_headers"],
|
|
5005
|
+
message: "custom_headers must be provided when auth_type is 'headers'"
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
});
|
|
4996
5009
|
function isRecord2(value) {
|
|
4997
5010
|
return typeof value === "object" && value !== null;
|
|
4998
5011
|
}
|
|
@@ -5071,20 +5084,8 @@ function registerMcpIntegrationsTools(server, service) {
|
|
|
5071
5084
|
idempotentHint: false,
|
|
5072
5085
|
openWorldHint: true
|
|
5073
5086
|
},
|
|
5074
|
-
async (
|
|
5075
|
-
|
|
5076
|
-
(mapping) => mapping.target_field === "configurations.custom_headers"
|
|
5077
|
-
)) {
|
|
5078
|
-
return {
|
|
5079
|
-
content: [
|
|
5080
|
-
{
|
|
5081
|
-
type: "text",
|
|
5082
|
-
text: "Error: custom_headers must be provided when auth_type is 'headers'"
|
|
5083
|
-
}
|
|
5084
|
-
],
|
|
5085
|
-
isError: true
|
|
5086
|
-
};
|
|
5087
|
-
}
|
|
5087
|
+
async (rawParams) => {
|
|
5088
|
+
const params = createMcpIntegrationSchema.parse(rawParams);
|
|
5088
5089
|
const { custom_headers, ...rest } = params;
|
|
5089
5090
|
const result = await service.mcpIntegrations.createMcpIntegration({
|
|
5090
5091
|
...rest,
|
|
@@ -5921,6 +5922,28 @@ var PROMPTS_TOOL_SCHEMAS = {
|
|
|
5921
5922
|
)
|
|
5922
5923
|
}
|
|
5923
5924
|
};
|
|
5925
|
+
var createPromptSchema = z17.object(PROMPTS_TOOL_SCHEMAS.createPrompt).superRefine((value, ctx) => {
|
|
5926
|
+
if (!value.model && !value.ai_model_id && !value.finetune_id) {
|
|
5927
|
+
ctx.addIssue({
|
|
5928
|
+
code: z17.ZodIssueCode.custom,
|
|
5929
|
+
message: "At least one of model, ai_model_id, or finetune_id must be provided"
|
|
5930
|
+
});
|
|
5931
|
+
}
|
|
5932
|
+
if (value.string === void 0 && value.messages === void 0) {
|
|
5933
|
+
ctx.addIssue({
|
|
5934
|
+
code: z17.ZodIssueCode.custom,
|
|
5935
|
+
message: "Provide either string or messages"
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
});
|
|
5939
|
+
var migratePromptSchema = z17.object(PROMPTS_TOOL_SCHEMAS.migratePrompt).superRefine((value, ctx) => {
|
|
5940
|
+
if (value.string === void 0 && value.messages === void 0) {
|
|
5941
|
+
ctx.addIssue({
|
|
5942
|
+
code: z17.ZodIssueCode.custom,
|
|
5943
|
+
message: "Provide either string or messages"
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
});
|
|
5924
5947
|
function normalizePromptTemplateString(params) {
|
|
5925
5948
|
if (params.string !== void 0) {
|
|
5926
5949
|
return params.string;
|
|
@@ -5995,30 +6018,9 @@ function registerPromptsTools(server, service) {
|
|
|
5995
6018
|
"create_prompt",
|
|
5996
6019
|
"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.",
|
|
5997
6020
|
PROMPTS_TOOL_SCHEMAS.createPrompt,
|
|
5998
|
-
async (
|
|
5999
|
-
|
|
6000
|
-
return {
|
|
6001
|
-
content: [
|
|
6002
|
-
{
|
|
6003
|
-
type: "text",
|
|
6004
|
-
text: "Error creating prompt: At least one of model, ai_model_id, or finetune_id must be provided"
|
|
6005
|
-
}
|
|
6006
|
-
],
|
|
6007
|
-
isError: true
|
|
6008
|
-
};
|
|
6009
|
-
}
|
|
6021
|
+
async (rawParams) => {
|
|
6022
|
+
const params = createPromptSchema.parse(rawParams);
|
|
6010
6023
|
const templateString = normalizePromptTemplateString(params);
|
|
6011
|
-
if (templateString === void 0) {
|
|
6012
|
-
return {
|
|
6013
|
-
content: [
|
|
6014
|
-
{
|
|
6015
|
-
type: "text",
|
|
6016
|
-
text: "Error creating prompt: Provide either string or messages"
|
|
6017
|
-
}
|
|
6018
|
-
],
|
|
6019
|
-
isError: true
|
|
6020
|
-
};
|
|
6021
|
-
}
|
|
6022
6024
|
if (params.dry_run) {
|
|
6023
6025
|
return jsonResult({
|
|
6024
6026
|
dry_run: true,
|
|
@@ -6128,14 +6130,18 @@ function registerPromptsTools(server, service) {
|
|
|
6128
6130
|
});
|
|
6129
6131
|
if (dry_run) {
|
|
6130
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
|
+
}
|
|
6131
6139
|
return jsonResult({
|
|
6132
6140
|
dry_run: true,
|
|
6133
6141
|
action: "update",
|
|
6134
6142
|
message: `Would update prompt "${current.name}"`,
|
|
6135
6143
|
current_version: current.current_version?.version_number ?? null,
|
|
6136
|
-
changes
|
|
6137
|
-
(k) => updateData[k] !== void 0
|
|
6138
|
-
)
|
|
6144
|
+
changes
|
|
6139
6145
|
});
|
|
6140
6146
|
}
|
|
6141
6147
|
const result = await service.prompts.updatePrompt(prompt_id, {
|
|
@@ -6273,19 +6279,9 @@ function registerPromptsTools(server, service) {
|
|
|
6273
6279
|
idempotentHint: true,
|
|
6274
6280
|
openWorldHint: false
|
|
6275
6281
|
},
|
|
6276
|
-
async (
|
|
6282
|
+
async (rawParams) => {
|
|
6283
|
+
const params = migratePromptSchema.parse(rawParams);
|
|
6277
6284
|
const templateString = normalizePromptTemplateString(params);
|
|
6278
|
-
if (templateString === void 0) {
|
|
6279
|
-
return {
|
|
6280
|
-
content: [
|
|
6281
|
-
{
|
|
6282
|
-
type: "text",
|
|
6283
|
-
text: "Error migrating prompt: Provide either string or messages"
|
|
6284
|
-
}
|
|
6285
|
-
],
|
|
6286
|
-
isError: true
|
|
6287
|
-
};
|
|
6288
|
-
}
|
|
6289
6285
|
const result = await service.prompts.migratePrompt({
|
|
6290
6286
|
name: params.name,
|
|
6291
6287
|
app: params.app,
|
|
@@ -7251,7 +7247,7 @@ var scimWorkspaceMappingBaseShape = {
|
|
|
7251
7247
|
workspace_id: z22.string().describe("Portkey workspace ID that the SCIM group should access"),
|
|
7252
7248
|
role: z22.enum(["admin", "member", "manager"]).describe("Workspace role automatically granted to group members")
|
|
7253
7249
|
};
|
|
7254
|
-
var
|
|
7250
|
+
var createScimWorkspaceMappingShape = {
|
|
7255
7251
|
...scimWorkspaceMappingBaseShape,
|
|
7256
7252
|
scim_group_id: z22.string().min(1).optional().describe(
|
|
7257
7253
|
"Existing identity-provider SCIM group ID; provide this or scim_group_name, but not both"
|
|
@@ -7259,7 +7255,8 @@ var createScimWorkspaceMappingInputSchema = z22.object({
|
|
|
7259
7255
|
scim_group_name: z22.string().min(1).optional().describe(
|
|
7260
7256
|
"Existing SCIM group display name; provide this or scim_group_id, but not both"
|
|
7261
7257
|
)
|
|
7262
|
-
}
|
|
7258
|
+
};
|
|
7259
|
+
var createScimWorkspaceMappingSchema = z22.object(createScimWorkspaceMappingShape).superRefine((input, context) => {
|
|
7263
7260
|
if (Boolean(input.scim_group_id) === Boolean(input.scim_group_name)) {
|
|
7264
7261
|
context.addIssue({
|
|
7265
7262
|
code: "custom",
|
|
@@ -7414,7 +7411,7 @@ function registerWorkspacesTools(server, service) {
|
|
|
7414
7411
|
"create_scim_workspace_mapping",
|
|
7415
7412
|
{
|
|
7416
7413
|
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.",
|
|
7417
|
-
inputSchema:
|
|
7414
|
+
inputSchema: createScimWorkspaceMappingShape,
|
|
7418
7415
|
annotations: {
|
|
7419
7416
|
title: "Create SCIM Workspace Mapping",
|
|
7420
7417
|
readOnlyHint: false,
|
|
@@ -7423,7 +7420,8 @@ function registerWorkspacesTools(server, service) {
|
|
|
7423
7420
|
openWorldHint: true
|
|
7424
7421
|
}
|
|
7425
7422
|
},
|
|
7426
|
-
async (
|
|
7423
|
+
async (rawParams) => {
|
|
7424
|
+
const params = createScimWorkspaceMappingSchema.parse(rawParams);
|
|
7427
7425
|
let request;
|
|
7428
7426
|
if (params.scim_group_id !== void 0) {
|
|
7429
7427
|
request = {
|
|
@@ -7784,7 +7782,25 @@ function augmentToolDescription(toolName, description) {
|
|
|
7784
7782
|
}
|
|
7785
7783
|
return `${description} ${ENTERPRISE_GATED_DESCRIPTION_NOTE}`;
|
|
7786
7784
|
}
|
|
7785
|
+
function formatZodIssues(error) {
|
|
7786
|
+
return error.issues.map((issue) => {
|
|
7787
|
+
const path = issue.path.join(".");
|
|
7788
|
+
if (!path) {
|
|
7789
|
+
return issue.message;
|
|
7790
|
+
}
|
|
7791
|
+
const namesPath = new RegExp(`\\b${escapeRegExp(path)}\\b`).test(
|
|
7792
|
+
issue.message
|
|
7793
|
+
);
|
|
7794
|
+
return namesPath ? issue.message : `${path}: ${issue.message}`;
|
|
7795
|
+
}).join("; ");
|
|
7796
|
+
}
|
|
7797
|
+
function escapeRegExp(value) {
|
|
7798
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7799
|
+
}
|
|
7787
7800
|
function getToolErrorMessage(error) {
|
|
7801
|
+
if (error instanceof z23.ZodError) {
|
|
7802
|
+
return formatZodIssues(error);
|
|
7803
|
+
}
|
|
7788
7804
|
if (error instanceof Error && error.message) {
|
|
7789
7805
|
return error.message;
|
|
7790
7806
|
}
|
|
@@ -7931,14 +7947,14 @@ function wrapToolCallback(toolName, callback) {
|
|
|
7931
7947
|
return normalizeToolResult(await callback(...args));
|
|
7932
7948
|
} catch (error) {
|
|
7933
7949
|
const message = getToolErrorMessage(error);
|
|
7950
|
+
const isValidationError = error instanceof z23.ZodError;
|
|
7951
|
+
const text = isValidationError ? `Invalid arguments for "${toolName}": ${message}` : `Tool "${toolName}" failed: ${message}`;
|
|
7934
7952
|
Logger.error("Tool callback failed", {
|
|
7935
7953
|
error: message,
|
|
7936
7954
|
metadata: { toolName }
|
|
7937
7955
|
});
|
|
7938
7956
|
return normalizeToolResult({
|
|
7939
|
-
content: [
|
|
7940
|
-
{ type: "text", text: `Tool "${toolName}" failed: ${message}` }
|
|
7941
|
-
],
|
|
7957
|
+
content: [{ type: "text", text }],
|
|
7942
7958
|
isError: true
|
|
7943
7959
|
});
|
|
7944
7960
|
}
|
|
@@ -8079,6 +8095,21 @@ function registerAllTools(server, service, options = {}) {
|
|
|
8079
8095
|
// src/lib/auth.ts
|
|
8080
8096
|
import crypto2 from "node:crypto";
|
|
8081
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
|
|
8082
8113
|
function getPrincipalOwnerKey(principal) {
|
|
8083
8114
|
return crypto2.createHash("sha256").update(principal.id, "utf8").digest("hex");
|
|
8084
8115
|
}
|
|
@@ -8299,7 +8330,7 @@ async function verifyClerkToken(token, config) {
|
|
|
8299
8330
|
return authorizeClerkClaims(payload, config);
|
|
8300
8331
|
}
|
|
8301
8332
|
async function mcpAuthMiddleware(req, res, next) {
|
|
8302
|
-
if (!req.path
|
|
8333
|
+
if (!isMcpRequestPath(req.path)) {
|
|
8303
8334
|
next();
|
|
8304
8335
|
return;
|
|
8305
8336
|
}
|
|
@@ -8428,6 +8459,15 @@ function getServerConfig() {
|
|
|
8428
8459
|
`Invalid MCP_EVENT_TTL_SECONDS value: ${eventStoreTtlStr}. Must be a positive integer`
|
|
8429
8460
|
);
|
|
8430
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
|
+
}
|
|
8431
8471
|
const redisUrl = process.env.MCP_REDIS_URL?.trim() || process.env.REDIS_URL?.trim();
|
|
8432
8472
|
if (eventStoreMode === "redis" && !redisUrl) {
|
|
8433
8473
|
throw new Error(
|
|
@@ -8487,7 +8527,8 @@ function getServerConfig() {
|
|
|
8487
8527
|
ttlSeconds: eventStoreTtlSeconds,
|
|
8488
8528
|
redisUrl,
|
|
8489
8529
|
redisKeyPrefix,
|
|
8490
|
-
encryptionKey: eventEncryptionKey
|
|
8530
|
+
encryptionKey: eventEncryptionKey,
|
|
8531
|
+
commandTimeoutMs: eventStoreCommandTimeoutMs
|
|
8491
8532
|
},
|
|
8492
8533
|
protocol,
|
|
8493
8534
|
port,
|
|
@@ -8719,13 +8760,15 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8719
8760
|
keyPrefix;
|
|
8720
8761
|
ownerKey;
|
|
8721
8762
|
encryptionKey;
|
|
8763
|
+
commandTimeoutMs;
|
|
8722
8764
|
connection;
|
|
8723
|
-
constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, connection = {}) {
|
|
8765
|
+
constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, commandTimeoutMs, connection = {}) {
|
|
8724
8766
|
this.redisUrl = redisUrl;
|
|
8725
8767
|
this.ttlSeconds = ttlSeconds;
|
|
8726
8768
|
this.keyPrefix = keyPrefix;
|
|
8727
8769
|
this.ownerKey = ownerKey;
|
|
8728
8770
|
this.encryptionKey = encryptionKey;
|
|
8771
|
+
this.commandTimeoutMs = commandTimeoutMs;
|
|
8729
8772
|
this.connection = connection;
|
|
8730
8773
|
}
|
|
8731
8774
|
get client() {
|
|
@@ -8741,6 +8784,7 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8741
8784
|
this.ttlSeconds,
|
|
8742
8785
|
ownerKey,
|
|
8743
8786
|
this.encryptionKey,
|
|
8787
|
+
this.commandTimeoutMs,
|
|
8744
8788
|
this.connection
|
|
8745
8789
|
);
|
|
8746
8790
|
}
|
|
@@ -8762,11 +8806,19 @@ var RedisEventStore = class _RedisEventStore {
|
|
|
8762
8806
|
this.connection.client = createClient({
|
|
8763
8807
|
url: this.redisUrl,
|
|
8764
8808
|
// node-redis v6 defaults to RESP3, a 5-second command timeout, and a
|
|
8765
|
-
// 30-second keepalive delay.
|
|
8766
|
-
//
|
|
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.
|
|
8767
8817
|
RESP: 2,
|
|
8768
8818
|
socket: { keepAliveInitialDelay: 5e3 },
|
|
8769
|
-
commandOptions: {
|
|
8819
|
+
commandOptions: {
|
|
8820
|
+
timeout: this.commandTimeoutMs === 0 ? void 0 : this.commandTimeoutMs
|
|
8821
|
+
}
|
|
8770
8822
|
});
|
|
8771
8823
|
this.connection.client.on("error", (error) => {
|
|
8772
8824
|
Logger.error("Redis event store error", {
|
|
@@ -8946,7 +8998,8 @@ function createManagedEventStore(config) {
|
|
|
8946
8998
|
config.eventStore.redisKeyPrefix,
|
|
8947
8999
|
config.eventStore.ttlSeconds,
|
|
8948
9000
|
"internal",
|
|
8949
|
-
encryptionKey
|
|
9001
|
+
encryptionKey,
|
|
9002
|
+
config.eventStore.commandTimeoutMs
|
|
8950
9003
|
);
|
|
8951
9004
|
return {
|
|
8952
9005
|
mode: "redis",
|
|
@@ -9222,7 +9275,7 @@ function isAllowedHost(host) {
|
|
|
9222
9275
|
});
|
|
9223
9276
|
}
|
|
9224
9277
|
function originValidationMiddleware(req, res, next) {
|
|
9225
|
-
if (req.path
|
|
9278
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9226
9279
|
next();
|
|
9227
9280
|
return;
|
|
9228
9281
|
}
|
|
@@ -9239,7 +9292,7 @@ function originValidationMiddleware(req, res, next) {
|
|
|
9239
9292
|
next();
|
|
9240
9293
|
}
|
|
9241
9294
|
function hostValidationMiddleware(req, res, next) {
|
|
9242
|
-
if (req.path
|
|
9295
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9243
9296
|
next();
|
|
9244
9297
|
return;
|
|
9245
9298
|
}
|
|
@@ -9371,23 +9424,26 @@ function resolveRateLimitRedisUrl(store) {
|
|
|
9371
9424
|
}
|
|
9372
9425
|
return redisUrl;
|
|
9373
9426
|
}
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
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();
|
|
9391
9447
|
function getRateLimitConfig() {
|
|
9392
9448
|
return RATE_LIMIT_CONFIG;
|
|
9393
9449
|
}
|
|
@@ -9520,11 +9576,11 @@ function applyRateLimit(scope, req, res, next) {
|
|
|
9520
9576
|
next();
|
|
9521
9577
|
return;
|
|
9522
9578
|
}
|
|
9523
|
-
if (req.path
|
|
9579
|
+
if (isHealthOrReadyPath(req.path)) {
|
|
9524
9580
|
next();
|
|
9525
9581
|
return;
|
|
9526
9582
|
}
|
|
9527
|
-
if (scope === "authentication" && req.path
|
|
9583
|
+
if (scope === "authentication" && !isMcpRequestPath(req.path)) {
|
|
9528
9584
|
next();
|
|
9529
9585
|
return;
|
|
9530
9586
|
}
|
|
@@ -10378,14 +10434,16 @@ function createMcpPostHandler(deps) {
|
|
|
10378
10434
|
try {
|
|
10379
10435
|
await transport.handleRequest(req, res, req.body);
|
|
10380
10436
|
} catch (error) {
|
|
10381
|
-
if (hasReservedSessionSlot) {
|
|
10382
|
-
sessionStore.releaseReservation();
|
|
10383
|
-
}
|
|
10384
10437
|
respondJsonRpcInternalError(
|
|
10385
10438
|
res,
|
|
10386
10439
|
sessionId ? "POST /mcp stateful session" : "POST /mcp stateful initialize",
|
|
10387
10440
|
error
|
|
10388
10441
|
);
|
|
10442
|
+
} finally {
|
|
10443
|
+
if (hasReservedSessionSlot) {
|
|
10444
|
+
sessionStore.releaseReservation();
|
|
10445
|
+
hasReservedSessionSlot = false;
|
|
10446
|
+
}
|
|
10389
10447
|
}
|
|
10390
10448
|
};
|
|
10391
10449
|
}
|
|
@@ -10421,143 +10479,150 @@ function createMcpGetHandler(deps) {
|
|
|
10421
10479
|
);
|
|
10422
10480
|
return;
|
|
10423
10481
|
}
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
405,
|
|
10430
|
-
"Stateless GET /mcp replay requires MCP_EVENT_STORE=memory or redis",
|
|
10431
|
-
-32e3
|
|
10432
|
-
);
|
|
10433
|
-
return;
|
|
10434
|
-
}
|
|
10435
|
-
const lastEventId = req.headers["last-event-id"];
|
|
10436
|
-
if (typeof lastEventId !== "string" || lastEventId.length === 0) {
|
|
10437
|
-
res.setHeader("Allow", "POST");
|
|
10438
|
-
respondJsonRpcClientError(
|
|
10439
|
-
res,
|
|
10440
|
-
405,
|
|
10441
|
-
"Stateless GET /mcp only supports replay with Last-Event-ID",
|
|
10442
|
-
-32e3
|
|
10443
|
-
);
|
|
10444
|
-
return;
|
|
10445
|
-
}
|
|
10446
|
-
if (!req.headers.accept?.includes("text/event-stream")) {
|
|
10447
|
-
respondJsonRpcClientError(
|
|
10448
|
-
res,
|
|
10449
|
-
406,
|
|
10450
|
-
"Not Acceptable: Client must accept text/event-stream",
|
|
10451
|
-
-32e3
|
|
10452
|
-
);
|
|
10453
|
-
return;
|
|
10454
|
-
}
|
|
10455
|
-
const protocolError = validateRequiredProtocolVersion(
|
|
10456
|
-
getMcpProtocolVersion(req)
|
|
10457
|
-
);
|
|
10458
|
-
if (protocolError) {
|
|
10459
|
-
respondJsonRpcClientError(res, 400, protocolError, -32e3);
|
|
10460
|
-
return;
|
|
10461
|
-
}
|
|
10482
|
+
let releaseReplaySlot;
|
|
10483
|
+
const replaySlot = new Promise((resolve) => {
|
|
10484
|
+
releaseReplaySlot = resolve;
|
|
10485
|
+
});
|
|
10486
|
+
activeReplayOperations.add(replaySlot);
|
|
10462
10487
|
try {
|
|
10463
|
-
const
|
|
10464
|
-
|
|
10465
|
-
|
|
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)
|
|
10466
10521
|
);
|
|
10467
|
-
if (
|
|
10468
|
-
|
|
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
|
+
}
|
|
10469
10541
|
respondJsonRpcClientError(
|
|
10470
10542
|
res,
|
|
10471
|
-
|
|
10472
|
-
"
|
|
10543
|
+
409,
|
|
10544
|
+
"Conflict: Stream already has an active replay connection",
|
|
10473
10545
|
-32e3
|
|
10474
10546
|
);
|
|
10475
10547
|
return;
|
|
10476
10548
|
}
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
const deadline = Date.now() + 25e3;
|
|
10494
|
-
while (getIsReady() && !res.destroyed && !finalResponseSeen) {
|
|
10495
|
-
await replayEventStore.replayEventsAfter(cursor, {
|
|
10496
|
-
send: async (eventId, message) => {
|
|
10497
|
-
cursor = eventId;
|
|
10498
|
-
finalResponseSeen = "id" in message && ("result" in message || "error" in message);
|
|
10499
|
-
if (!res.write(
|
|
10500
|
-
`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
|
|
10501
10565
|
id: ${eventId}
|
|
10502
10566
|
data: ${JSON.stringify(message)}
|
|
10503
10567
|
|
|
10504
10568
|
`
|
|
10505
|
-
|
|
10506
|
-
|
|
10569
|
+
)) {
|
|
10570
|
+
await waitForResponseWritable(res);
|
|
10571
|
+
}
|
|
10507
10572
|
}
|
|
10573
|
+
});
|
|
10574
|
+
if (finalResponseSeen || res.destroyed) {
|
|
10575
|
+
break;
|
|
10508
10576
|
}
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
break;
|
|
10512
|
-
}
|
|
10513
|
-
if (Date.now() >= deadline) {
|
|
10514
|
-
res.write(`id: ${cursor}
|
|
10577
|
+
if (Date.now() >= deadline) {
|
|
10578
|
+
res.write(`id: ${cursor}
|
|
10515
10579
|
data:
|
|
10516
10580
|
|
|
10517
10581
|
`);
|
|
10518
|
-
|
|
10519
|
-
}
|
|
10520
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
10521
|
-
}
|
|
10522
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
10523
|
-
await new Promise((resolveResponse) => {
|
|
10524
|
-
const done = () => resolveResponse();
|
|
10525
|
-
res.once("finish", done);
|
|
10526
|
-
res.once("close", done);
|
|
10527
|
-
res.end();
|
|
10528
|
-
});
|
|
10529
|
-
}
|
|
10530
|
-
} finally {
|
|
10531
|
-
try {
|
|
10532
|
-
await lease.release();
|
|
10533
|
-
} catch (error) {
|
|
10534
|
-
Logger.error("Failed to release stateless replay lease", {
|
|
10535
|
-
metadata: {
|
|
10536
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10582
|
+
break;
|
|
10537
10583
|
}
|
|
10538
|
-
|
|
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
|
+
}
|
|
10539
10606
|
}
|
|
10540
|
-
}
|
|
10541
|
-
})();
|
|
10542
|
-
activeReplayOperations.add(replayOperation);
|
|
10543
|
-
try {
|
|
10607
|
+
})();
|
|
10544
10608
|
await replayOperation;
|
|
10545
|
-
}
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
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();
|
|
10553
10618
|
}
|
|
10554
|
-
}
|
|
10555
|
-
|
|
10556
|
-
res.end();
|
|
10619
|
+
} else {
|
|
10620
|
+
respondJsonRpcInternalError(res, "GET /mcp stateless", error);
|
|
10557
10621
|
}
|
|
10558
|
-
} else {
|
|
10559
|
-
respondJsonRpcInternalError(res, "GET /mcp stateless", error);
|
|
10560
10622
|
}
|
|
10623
|
+
} finally {
|
|
10624
|
+
activeReplayOperations.delete(replaySlot);
|
|
10625
|
+
releaseReplaySlot();
|
|
10561
10626
|
}
|
|
10562
10627
|
return;
|
|
10563
10628
|
}
|
|
@@ -10594,6 +10659,45 @@ data:
|
|
|
10594
10659
|
return;
|
|
10595
10660
|
}
|
|
10596
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
|
+
}
|
|
10597
10701
|
await transport.handleRequest(req, res);
|
|
10598
10702
|
} else {
|
|
10599
10703
|
res.status(404).json({
|
|
@@ -10667,9 +10771,9 @@ function createHttpAppRuntime() {
|
|
|
10667
10771
|
const readyCheckMode = getReadyCheckMode();
|
|
10668
10772
|
const requestBodyLimit = process.env.MCP_MAX_REQUEST_SIZE?.trim() || "1mb";
|
|
10669
10773
|
const allowedOrigins = getAllowedOrigins();
|
|
10670
|
-
const corsOriginConfig =
|
|
10671
|
-
|
|
10672
|
-
|
|
10774
|
+
const corsOriginConfig = (origin, callback) => {
|
|
10775
|
+
callback(null, validateOrigin(origin));
|
|
10776
|
+
};
|
|
10673
10777
|
if (allowedOrigins.includes("*") && authConfig.mode === "none") {
|
|
10674
10778
|
Logger.warn(
|
|
10675
10779
|
"CORS wildcard (ALLOWED_ORIGINS=*) combined with MCP_AUTH_MODE=none: all origins are accepted with no authentication. Do not expose this server publicly."
|
|
@@ -10708,7 +10812,6 @@ function createHttpAppRuntime() {
|
|
|
10708
10812
|
} : false
|
|
10709
10813
|
})
|
|
10710
10814
|
);
|
|
10711
|
-
app.use(express.json({ limit: requestBodyLimit }));
|
|
10712
10815
|
if (authConfig.mode === "none") {
|
|
10713
10816
|
app.use(hostValidationMiddleware);
|
|
10714
10817
|
}
|
|
@@ -10721,13 +10824,14 @@ function createHttpAppRuntime() {
|
|
|
10721
10824
|
standardHeaders: false,
|
|
10722
10825
|
legacyHeaders: false,
|
|
10723
10826
|
validate: false,
|
|
10724
|
-
skip: (req) => !rateLimitConfig.enabled || req.path
|
|
10827
|
+
skip: (req) => !rateLimitConfig.enabled || !isMcpRequestPath(req.path),
|
|
10725
10828
|
message: { error: "Too Many Requests" }
|
|
10726
10829
|
})
|
|
10727
10830
|
);
|
|
10728
10831
|
app.use(rateLimitMiddleware);
|
|
10729
10832
|
app.use(mcpAuthMiddleware);
|
|
10730
10833
|
app.use(principalRateLimitMiddleware);
|
|
10834
|
+
app.use(express.json({ limit: requestBodyLimit }));
|
|
10731
10835
|
app.use(
|
|
10732
10836
|
(err, _req, res, next) => {
|
|
10733
10837
|
const errorType = err && typeof err === "object" && "type" in err ? err.type : void 0;
|