Skip to content

fix(memory-plugin): make MCP proxy extra-header precedence case-insensitive - #6164

Open
akushonkamen wants to merge 2 commits into
volcengine:mainfrom
akushonkamen:akushonkamen/fix/6158-case-insensitive-header-precedence
Open

akushonkamen wants to merge 2 commits into
volcengine:mainfrom
akushonkamen:akushonkamen/fix/6158-case-insensitive-header-precedence

Conversation

@akushonkamen

Copy link
Copy Markdown
Contributor

Description

The MCP proxy's "proxy headers win" precedence over operator-supplied OPENVIKING_EXTRA_HEADERS was exact-case only. Because HTTP header names are case-insensitive, an extra spelled x-openviking-account survived next to the proxy's own X-OpenViking-Account, and Node fetch sent both. The upstream joined the duplicate into one value, so the wire identity became obsolete, account-a instead of the configured account (issue reproduction).

Root cause. headersForRequest (examples/memory-plugin-shared/lib/mcp-proxy-core.mjs:268) merged operator extras with the proxy's self-set headers via an exact-case object spread ({ ...opExtras, ...built }), while buildOvHeaders (examples/memory-plugin-shared/lib/ov-http.mjs:31-34) emits canonical-case keys (X-OpenViking-Account, X-OpenViking-User, X-OpenViking-Actor-Peer). An extra key spelled in a different case therefore survived as a second key and reached the wire as a duplicate header, which the upstream joins with a comma.

Fix. Before merging, drop extras whose lowercase name collides with a header the proxy already set, so the existing "proxy headers win" precedence holds regardless of casing. Unrelated extras still pass through, the exact-case spelling keeps the proxy's value exactly as before, and the reserved-name parser and credential/identity configuration are unchanged.

Reproduction status. Reproduced through the real production entrypoint: the pre-fix run failed the new regression tests with the actual wire values stale, zeus and stale, openviking-memory-test/9.9.9, matching the issue's comma-joined values. Not inferred from code alone.

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

Fixes #6158

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • examples/memory-plugin-shared/lib/mcp-proxy-core.mjs: headersForRequest now drops operator extras whose lowercase name matches a proxy-set header before the merge, making the existing precedence case-insensitive (17 lines changed).
  • The same shared-lib source is synced to the three committed copies used by distributed plugins (agent-plugins/servers/shared/mcp-proxy-core.mjs, examples/dsh-memory-plugin/scripts/shared/mcp-proxy-core.mjs, examples/pi-coding-agent-extension/scripts/shared/mcp-proxy-core.mjs).
  • examples/memory-plugin-shared/mcp-proxy-core.test.mjs: 6 regression tests covering the case-collision identity headers (account, user, actor-peer, user-agent) plus unrelated-extras and exact-case controls, asserting wire values on both initialize and tools/list against a loopback upstream.
  • Distributed plugin versions bumped in one dedicated commit (chore(memory-plugin)) so the shared-runtime change ships with fresh versions, per the release check.

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

Validation commands and results (macOS, arm64):

  1. Red first: before the fix, node --test examples/memory-plugin-shared/mcp-proxy-core.test.mjs failed 5 of the 6 new regression tests with the actual wire values stale, zeus / stale, openviking-memory-test/9.9.9 (the issue's comma-join reproduced). After the fix: 23 tests / 23 pass / 0 fail. Re-run green after rebasing onto the latest main (base advanced 5 commits; none touched plugin code paths — only web-studio and 4 markdown files under examples/).
  2. Full plugin CI set, pr.yml-equivalent collection (103 test files, node examples/memory-plugin-shared/sync.mjs first, then npm ci in the dsh and pi lanes): 1318 tests / 1307 pass / 3 fail / 8 skipped. The 3 failures are all install-tui.test.mjs TRAE cases and reproduce byte-identically on an unmodified main worktree (git worktree add + sync.mjs), so they are a pre-existing environment issue, not introduced by this change.
  3. Installer tests run separately at --test-concurrency=1 (node --test --test-concurrency=1 install-agent-hooks + release-marketplace): 22/22 pass.
  4. Gate: node examples/memory-plugin-shared/sync.mjs re-run after the rebase — working tree stays clean (all synced copies are committed).

Skipped checks and why: web-studio eslint was not run — this PR does not touch web-studio/.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

(Documentation checkbox left unchecked intentionally: no user-facing documented behavior changes — the fix restores the precedence the docs already promise.)

Screenshots (if applicable)

Not applicable.

Additional Notes

…sitive

headersForRequest merged operator extras (OPENVIKING_EXTRA_HEADERS) under
the proxy's own headers with an exact-case object spread, but HTTP header
names are case-insensitive: an extra spelled x-openviking-account survived
next to the proxy's X-OpenViking-Account and Node fetch sent both, which
the upstream joins into 'obsolete, account-a' instead of the configured
identity value.

Drop extras whose lowercase name collides with a header the proxy already
set before merging, so the existing 'proxy headers win' precedence holds
regardless of casing. Unrelated extras still pass through, the exact-case
spelling keeps the proxy's value as before, and the reserved-name parser
and credential/identity configuration are unchanged.

Regression tests cover the four case-collision identity headers (account,
user, actor-peer, user-agent) plus the unrelated-extras and exact-case
controls, asserting the wire values on both initialize and tools/list
against a loopback upstream.

Fixes volcengine#6158
…hared-lib change

The shared runtime in examples/memory-plugin-shared/lib reaches all six
distributed plugins, and hosts install by the version string in each
manifest, so check-plugin-version-bumps.sh requires each of them to move
(required by the plugin-versions check; bash .github/scripts/
check-plugin-version-bumps.sh origin/main exits 1 without this). Bump the
manifests, the claude-code package.json that mirrors its plugin.json, the
agent-hook host manifests that pair with plugin.json, the dsh wire version
reported by config.mjs, and the two lockfiles that npm ci validates.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

[Bug]: MCP proxy combines identity header values when extra header names use different casing

1 participant