Skip to content

Conversation

@gagik
Copy link
Collaborator

@gagik gagik commented Nov 24, 2025

This adds header and query param-based overrides.

@gagik gagik requested a review from a team as a code owner November 24, 2025 14:15
Copilot AI review requested due to automatic review settings November 24, 2025 14:15
protected async setupServer(): Promise<Server> {
protected async setupServer(request?: RequestContext): Promise<Server> {
// Apply config overrides from request context (headers and query parameters)
let userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request });
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make this config override opt-in?
Can it be problematic that people who update the MCP server don't release that others can override some other settings now?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the override should be opt-in using the --allowRequestOverrides flag. Just realised that I did not write this info in the ticket but its further explained in Goal#3 in the scope.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the ability to override configuration parameters via HTTP headers and query parameters when using the HTTP transport. Overrides are applied automatically from request context before the createSessionConfig hook runs, with query parameters taking precedence over headers. Each config field defines its override behavior (not-allowed, override, merge, or custom logic).

Key changes:

  • New config override system with three behaviors: "not-allowed" (security-sensitive fields), "override" (simple replacement), and "merge" (arrays)
  • Conditional overrides for security flags (readOnly, indexCheck, disableEmbeddingsValidation) using custom logic
  • Request context (headers/query) passed through transport layer to config application

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/common/config/configOverrides.ts New module implementing config override extraction and application logic
src/common/config/configUtils.ts Added override behavior types, parseBoolean preprocessor, and oneWayOverride helper
src/common/config/userConfig.ts Registered override behaviors for all config fields and added boolean preprocessors
src/transports/base.ts Modified setupServer to accept RequestContext and apply overrides before createSessionConfig
src/transports/streamableHttp.ts Extracts headers/query into RequestContext and passes to setupServer
tests/unit/common/config/configOverrides.test.ts Comprehensive unit tests for override behaviors and edge cases
tests/integration/transports/configOverrides.test.ts Integration tests verifying overrides work end-to-end via HTTP
tests/integration/transports/createSessionConfig.test.ts Refactored to use helpers and verify createSessionConfig receives request context
tests/integration/server.test.ts Updated test to check for insert-many instead of insert-one
package.json Added --run flag to test script

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@coveralls
Copy link
Collaborator

coveralls commented Nov 24, 2025

Pull Request Test Coverage Report for Build 19679690652

Details

  • 240 of 249 (96.39%) changed or added relevant lines in 7 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.6%) to 80.466%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/lib.ts 0 1 0.0%
src/common/config/configUtils.ts 48 50 96.0%
src/common/config/configOverrides.ts 112 118 94.92%
Totals Coverage Status
Change from base Build 19677845049: 0.6%
Covered Lines: 6589
Relevant Lines: 8100

💛 - Coveralls

protected async setupServer(): Promise<Server> {
protected async setupServer(request?: RequestContext): Promise<Server> {
// Apply config overrides from request context (headers and query parameters)
let userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the override should be opt-in using the --allowRequestOverrides flag. Just realised that I did not write this info in the ticket but its further explained in Goal#3 in the scope.

Copy link
Collaborator

@nirinchev nirinchev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't looked at the tests yet, but I figured it's better to leave the comments I have so far so you don't wait for the full review.

return behavior(baseValue, overrideValue);
} catch (error) {
throw new Error(
`Cannot apply override for ${key} from ${JSON.stringify(baseValue)} to ${JSON.stringify(overrideValue)}: ${error instanceof Error ? error.message : String(error)}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the potential to leak the base value to the client - do we want that?

if (Array.isArray(baseValue) && Array.isArray(overrideValue)) {
return [...(baseValue as unknown[]), ...(overrideValue as unknown[])];
}
throw new Error("Cannot merge non-array values, did you mean to use the 'override' behavior?");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include the key here? Otherwise it's kind of hard to know what you did wrong.

* Preprocessor for boolean values that handles string "true"/"false" correctly.
* Zod's coerce.boolean() treats any non-empty string as true, which is not what we want.
*/
export function parseBoolean(val: unknown): unknown {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears to treat any non-empty string as true too (except false and 0) - is this intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this was a good balance between making sure old behaviors will work similar way (so if someone has --readonly=yes before, it'd still be true and not suddenly become false. while also making sure intentional setting of the value works too.
This helps us have the same logic for CLI and request parsing/validation. Though open to adjusting if there's concerns here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just error out if the string value is different from true/false/1/0? Because based on the same logic, if we have --readonly=no, that would now make the server readonly.

Copy link
Collaborator Author

@gagik gagik Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to this but it does mean this is a breaking change.
in latest v1.2.0 --readOnly no would make the server readOnly
--readOnly false would make the server write-able

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the question is if this is intentional - I would say this is more of a bugfix than a breaking change. Additionally, if we throw, it won't be a silent breaking change - we'll give users feedback about what to fix, which I think is a reasonable compromise.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay apparently this preprocessing only really applies to these headers.
everything else is configured by yargs-parser... so it's about changing settings there

.default(300_000)
.describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."),
.describe("Time in milliseconds after which an export is considered expired and eligible for cleanup.")
.register(configRegistry, { overrideBehavior: "override" }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds sketchy - can I setup my exports to timeout after a year, then flood the server storage with exports?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. But what if the client wanted a smaller timeout of their export? We can cap the overridable max timeout to the server configured timeout? But yea if it becomes too complex, I am fine leaving it as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting this to onlyLowerThanBaseValueOverride

.describe("When set to true, disables validation of embeddings dimensions."),
.describe("When set to true, disables validation of embeddings dimensions.")
.register(configRegistry, {
overrideBehavior: oneWayOverride(false),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder why this is oneWayOverride?

Copy link
Collaborator Author

@gagik gagik Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if validation is enabled (i.e. disableValidation is false) then we would not want a client to disable validation and let the MCP server insert arbitrary data in a vector search field (which apparently breaks vector search)

By the way, I find the double negative to be confusing so it'd be great to rename this in general

.default([])
.describe("An array of preview features that are enabled."),
.describe("An array of preview features that are enabled.")
.register(configRegistry, { overrideBehavior: "merge" }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we merging these arrays?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just following the scope document. Agree some can be adjusted cc: @himanshusinghs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is based on understanding that individual clients might want to enable specific preview features for their session.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should either be override (so you can remove features if needed) or not-allowed (so no experimental features can be enabled)

@gagik gagik force-pushed the gagik/query-header-override branch from baf9bbe to d2731d8 Compare November 25, 2025 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants