Skip to content

Commit 386be02

Browse files
committed
chore: add conditional subset and lower than cases
1 parent 24e8e92 commit 386be02

File tree

7 files changed

+282
-22
lines changed

7 files changed

+282
-22
lines changed

src/common/config/configOverrides.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export const CONFIG_QUERY_PREFIX = "mongodbMcp";
88

99
/**
1010
* Applies config overrides from request context (headers and query parameters).
11-
* Query parameters take precedence over headers.
11+
* Query parameters take precedence over headers. Can be used within the createSessionConfig
12+
* hook to manually apply the overrides. Requires `allowRequestOverrides` to be enabled.
1213
*
1314
* @param baseConfig - The base user configuration
1415
* @param request - The request context containing headers and query parameters

src/common/config/configUtils.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,38 @@ export function oneWayOverride<T>(allowedValue: T): CustomOverrideLogic {
138138
throw new Error(`Can only set to ${String(allowedValue)}`);
139139
};
140140
}
141+
142+
/** Allow overriding only to a value lower than the specified value */
143+
export function onlyLowerThanBaseValueOverride(): CustomOverrideLogic {
144+
return (oldValue, newValue) => {
145+
if (typeof oldValue !== "number") {
146+
throw new Error(`Unsupported type for base value for override: ${typeof oldValue}`);
147+
}
148+
if (typeof newValue !== "number") {
149+
throw new Error(`Unsupported type for new value for override: ${typeof newValue}`);
150+
}
151+
if (newValue >= oldValue) {
152+
throw new Error(`Can only set to a value lower than the base value`);
153+
}
154+
return newValue;
155+
};
156+
}
157+
158+
/** Allow overriding only to a subset of an array but not a superset */
159+
export function onlySubsetOfBaseValueOverride(): CustomOverrideLogic {
160+
return (oldValue, newValue) => {
161+
if (!Array.isArray(oldValue)) {
162+
throw new Error(`Unsupported type for base value for override: ${typeof oldValue}`);
163+
}
164+
if (!Array.isArray(newValue)) {
165+
throw new Error(`Unsupported type for new value for override: ${typeof newValue}`);
166+
}
167+
if (newValue.length > oldValue.length) {
168+
throw new Error(`Can only override to a subset of the base value`);
169+
}
170+
if (!newValue.every((value) => oldValue.includes(value))) {
171+
throw new Error(`Can only override to a subset of the base value`);
172+
}
173+
return newValue as unknown;
174+
};
175+
}

src/common/config/userConfig.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
getExportsPath,
77
getLogPath,
88
oneWayOverride,
9+
onlyLowerThanBaseValueOverride,
10+
onlySubsetOfBaseValueOverride,
911
parseBoolean,
1012
} from "./configUtils.js";
1113
import { previewFeatureValues, similarityValues } from "../schemas.js";
@@ -38,7 +40,7 @@ export const UserConfigSchema = z4.object({
3840
.describe(
3941
"MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data."
4042
)
41-
.register(configRegistry, { isSecret: true, overrideBehavior: "override" }),
43+
.register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }),
4244
loggers: z4
4345
.preprocess(
4446
(val: string | string[] | undefined) => commaSeparatedToArray(val),
@@ -54,7 +56,7 @@ export const UserConfigSchema = z4.object({
5456
.describe("An array of logger types.")
5557
.register(configRegistry, {
5658
defaultValueDescription: '`"disk,mcp"` see below*',
57-
overrideBehavior: "merge",
59+
overrideBehavior: "not-allowed",
5860
}),
5961
logPath: z4
6062
.string()
@@ -133,12 +135,12 @@ export const UserConfigSchema = z4.object({
133135
.number()
134136
.default(600_000)
135137
.describe("Idle timeout for a client to disconnect (only applies to http transport).")
136-
.register(configRegistry, { overrideBehavior: "override" }),
138+
.register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }),
137139
notificationTimeoutMs: z4.coerce
138140
.number()
139141
.default(540_000)
140142
.describe("Notification timeout for a client to be aware of disconnect (only applies to http transport).")
141-
.register(configRegistry, { overrideBehavior: "override" }),
143+
.register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }),
142144
maxBytesPerQuery: z4.coerce
143145
.number()
144146
.default(16_777_216)
@@ -167,21 +169,21 @@ export const UserConfigSchema = z4.object({
167169
.number()
168170
.default(120_000)
169171
.describe("Time in milliseconds between export cleanup cycles that remove expired export files.")
170-
.register(configRegistry, { overrideBehavior: "override" }),
172+
.register(configRegistry, { overrideBehavior: "not-allowed" }),
171173
atlasTemporaryDatabaseUserLifetimeMs: z4.coerce
172174
.number()
173175
.default(14_400_000)
174176
.describe(
175177
"Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted."
176178
)
177-
.register(configRegistry, { overrideBehavior: "override" }),
179+
.register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }),
178180
voyageApiKey: z4
179181
.string()
180182
.default("")
181183
.describe(
182184
"API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)."
183185
)
184-
.register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }),
186+
.register(configRegistry, { isSecret: true, overrideBehavior: "override" }),
185187
disableEmbeddingsValidation: z4
186188
.preprocess(parseBoolean, z4.boolean())
187189
.default(false)
@@ -206,7 +208,7 @@ export const UserConfigSchema = z4.object({
206208
)
207209
.default([])
208210
.describe("An array of preview features that are enabled.")
209-
.register(configRegistry, { overrideBehavior: "merge" }),
211+
.register(configRegistry, { overrideBehavior: onlySubsetOfBaseValueOverride() }),
210212
allowRequestOverrides: z4
211213
.preprocess(parseBoolean, z4.boolean())
212214
.default(false)

src/lib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export { Telemetry } from "./telemetry/telemetry.js";
2424
export { Keychain, registerGlobalSecretToRedact } from "./common/keychain.js";
2525
export type { Secret } from "./common/keychain.js";
2626
export { Elicitation } from "./elicitation.js";
27+
export { applyConfigOverrides } from "./common/config/configOverrides.js";

src/transports/base.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,12 @@ export abstract class TransportRunnerBase {
100100
}
101101

102102
protected async setupServer(request?: RequestContext): Promise<Server> {
103-
// Apply config overrides from request context (headers and query parameters)
104-
let userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request });
103+
let userConfig: UserConfig = this.userConfig;
105104

106-
// Call the config provider hook if provided, allowing consumers to
107-
// fetch or modify configuration after applying request context overrides
108105
if (this.createSessionConfig) {
109106
userConfig = await this.createSessionConfig({ userConfig, request });
107+
} else {
108+
userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request });
110109
}
111110

112111
const mcpServer = new McpServer({

tests/integration/transports/configOverrides.test.ts

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,26 @@ describe("Config Overrides via HTTP", () => {
9595
expect(readTools.length).toBe(1);
9696
});
9797

98-
it("should override connectionString with header", async () => {
98+
it("should not be able tooverride connectionString with header", async () => {
9999
await startRunner({
100100
...defaultTestConfig,
101101
httpPort: 0,
102102
connectionString: undefined,
103103
allowRequestOverrides: true,
104104
});
105105

106-
await connectClient({
107-
["x-mongodb-mcp-connection-string"]: "mongodb://override:27017",
108-
});
109-
110-
const response = await client.listTools();
111-
112-
expect(response).toBeDefined();
106+
try {
107+
await connectClient({
108+
["x-mongodb-mcp-connection-string"]: "mongodb://override:27017",
109+
});
110+
expect.fail("Expected an error to be thrown");
111+
} catch (error) {
112+
if (!(error instanceof Error)) {
113+
throw new Error("Expected an error to be thrown");
114+
}
115+
expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)");
116+
expect(error.message).toContain(`Config key connectionString is not allowed to be overridden`);
117+
}
113118
});
114119
});
115120

@@ -415,4 +420,131 @@ describe("Config Overrides via HTTP", () => {
415420
expect(findTool).toBeDefined();
416421
});
417422
});
423+
424+
describe("onlyLowerThanBaseValueOverride behavior", () => {
425+
it("should allow override to a lower value", async () => {
426+
await startRunner({
427+
...defaultTestConfig,
428+
httpPort: 0,
429+
idleTimeoutMs: 600_000,
430+
allowRequestOverrides: true,
431+
});
432+
433+
await connectClient({
434+
["x-mongodb-mcp-idle-timeout-ms"]: "300000",
435+
});
436+
437+
const response = await client.listTools();
438+
expect(response).toBeDefined();
439+
});
440+
441+
it("should reject override to a higher value", async () => {
442+
await startRunner({
443+
...defaultTestConfig,
444+
httpPort: 0,
445+
idleTimeoutMs: 600_000,
446+
allowRequestOverrides: true,
447+
});
448+
449+
try {
450+
await connectClient({
451+
["x-mongodb-mcp-idle-timeout-ms"]: "900000",
452+
});
453+
expect.fail("Expected an error to be thrown");
454+
} catch (error) {
455+
if (!(error instanceof Error)) {
456+
throw new Error("Expected an error to be thrown");
457+
}
458+
expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)");
459+
expect(error.message).toContain(
460+
"Cannot apply override for idleTimeoutMs: Can only set to a value lower than the base value"
461+
);
462+
}
463+
});
464+
465+
it("should reject override to equal value", async () => {
466+
await startRunner({
467+
...defaultTestConfig,
468+
httpPort: 0,
469+
idleTimeoutMs: 600_000,
470+
allowRequestOverrides: true,
471+
});
472+
473+
try {
474+
await connectClient({
475+
["x-mongodb-mcp-idle-timeout-ms"]: "600000",
476+
});
477+
expect.fail("Expected an error to be thrown");
478+
} catch (error) {
479+
if (!(error instanceof Error)) {
480+
throw new Error("Expected an error to be thrown");
481+
}
482+
expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)");
483+
expect(error.message).toContain(
484+
"Cannot apply override for idleTimeoutMs: Can only set to a value lower than the base value"
485+
);
486+
}
487+
});
488+
});
489+
490+
describe("onlySubsetOfBaseValueOverride behavior", () => {
491+
describe("previewFeatures", () => {
492+
it("should allow override to same value", async () => {
493+
await startRunner({
494+
...defaultTestConfig,
495+
httpPort: 0,
496+
previewFeatures: ["vectorSearch"],
497+
allowRequestOverrides: true,
498+
});
499+
500+
await connectClient({
501+
["x-mongodb-mcp-preview-features"]: "vectorSearch",
502+
});
503+
504+
const response = await client.listTools();
505+
expect(response).toBeDefined();
506+
});
507+
508+
it("should allow override to an empty array (subset of any array)", async () => {
509+
await startRunner({
510+
...defaultTestConfig,
511+
httpPort: 0,
512+
previewFeatures: ["vectorSearch"],
513+
allowRequestOverrides: true,
514+
});
515+
516+
await connectClient({
517+
["x-mongodb-mcp-preview-features"]: "",
518+
});
519+
520+
const response = await client.listTools();
521+
expect(response).toBeDefined();
522+
});
523+
524+
it("should reject override when base is empty array and trying to add items", async () => {
525+
await startRunner({
526+
...defaultTestConfig,
527+
httpPort: 0,
528+
previewFeatures: [],
529+
allowRequestOverrides: true,
530+
});
531+
532+
// Empty array trying to override with non-empty should fail (superset)
533+
try {
534+
await connectClient({
535+
["x-mongodb-mcp-preview-features"]: "vectorSearch",
536+
});
537+
expect.fail("Expected an error to be thrown");
538+
} catch (error) {
539+
if (!(error instanceof Error)) {
540+
throw new Error("Expected an error to be thrown");
541+
}
542+
expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)");
543+
expect(error.message).toContain(
544+
"Cannot apply override for previewFeatures: Can only override to a subset of the base value"
545+
);
546+
}
547+
});
548+
});
549+
});
418550
});

0 commit comments

Comments
 (0)