Skip to content

Commit dc795ae

Browse files
committed
test(mcp): serialize the proto-name suite on the shared mutation lock
The suite swaps NODE_ENV and the process-wide global and project MCP configurations without holding sharedMutationLock, so bun can run it alongside another state-mutating file: that file observes the injected realserver/locallyreal fixtures, or its own updates are overwritten when this teardown restores its snapshots. Acquire the lock in setup and release it from a finally in teardown, the same shape officialRegistry.test.ts uses. Also cover the reserved __proto__ entry arriving from parsed file config.
1 parent 7f2a1fb commit dc795ae

1 file changed

Lines changed: 50 additions & 10 deletions

File tree

src/services/mcp/config.protoName.test.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { afterEach, beforeEach, expect, test } from 'bun:test'
22

3+
import {
4+
acquireSharedMutationLock,
5+
releaseSharedMutationLock,
6+
} from '../../test/sharedMutationLock.js'
37
import {
48
getCurrentProjectConfig,
59
getGlobalConfig,
@@ -9,6 +13,7 @@ import {
913
import {
1014
addMcpConfig,
1115
getMcpConfigByName,
16+
parseMcpConfig,
1217
removeMcpConfig,
1318
} from './config.js'
1419

@@ -30,7 +35,12 @@ let savedProjectMcp: ReturnType<typeof getCurrentProjectConfig>['mcpServers']
3035

3136
let savedNodeEnv: string | undefined
3237

33-
beforeEach(() => {
38+
beforeEach(async () => {
39+
// This suite swaps NODE_ENV and the process-wide global/project MCP configs,
40+
// so it has to be serialized against the other state-mutating suites bun may
41+
// run alongside it -- otherwise they observe the injected fixtures, or their
42+
// updates are clobbered when this teardown restores a stale snapshot.
43+
await acquireSharedMutationLock('services/mcp/config.protoName.test.ts')
3444
savedNodeEnv = process.env.NODE_ENV
3545
process.env.NODE_ENV = 'test'
3646
savedGlobalMcp = getGlobalConfig().mcpServers
@@ -46,15 +56,19 @@ beforeEach(() => {
4656
})
4757

4858
afterEach(() => {
49-
saveGlobalConfig(config => ({ ...config, mcpServers: savedGlobalMcp }))
50-
saveCurrentProjectConfig(config => ({
51-
...config,
52-
mcpServers: savedProjectMcp,
53-
}))
54-
if (savedNodeEnv === undefined) {
55-
delete process.env.NODE_ENV
56-
} else {
57-
process.env.NODE_ENV = savedNodeEnv
59+
try {
60+
saveGlobalConfig(config => ({ ...config, mcpServers: savedGlobalMcp }))
61+
saveCurrentProjectConfig(config => ({
62+
...config,
63+
mcpServers: savedProjectMcp,
64+
}))
65+
if (savedNodeEnv === undefined) {
66+
delete process.env.NODE_ENV
67+
} else {
68+
process.env.NODE_ENV = savedNodeEnv
69+
}
70+
} finally {
71+
releaseSharedMutationLock()
5872
}
5973
})
6074

@@ -97,6 +111,32 @@ test('reports proto-name removal as not found instead of succeeding', async () =
97111
}
98112
})
99113

114+
test('surfaces a __proto__ entry in file config instead of dropping it', () => {
115+
// A hand-authored .mcp.json can carry this name: the schema accepts it, but
116+
// copying it into a plain object hits the prototype setter, so the entry
117+
// vanished with no diagnostic and the user could not tell why their server
118+
// did not exist. It must be reported, not silently discarded.
119+
// Parsed from text, exactly as the real file is: JSON.parse creates a true
120+
// own "__proto__" key, which an object literal would not.
121+
const { config, errors } = parseMcpConfig({
122+
configObject: JSON.parse(
123+
'{"mcpServers":{"__proto__":{"command":"echo","args":[]},' +
124+
'"realone":{"command":"echo","args":[]}}}',
125+
),
126+
expandVars: false,
127+
scope: 'project',
128+
filePath: '/tmp/.mcp.json',
129+
})
130+
131+
const protoError = errors.find(e => e.path === 'mcpServers.__proto__')
132+
expect(protoError).toBeDefined()
133+
expect(protoError?.message).toContain('reserved')
134+
expect(protoError?.mcpErrorMetadata?.severity).toBe('fatal')
135+
// The rest of the file still parses, and the bad name is not an own key.
136+
expect(Object.hasOwn(config?.mcpServers ?? {}, 'realone')).toBe(true)
137+
expect(Object.hasOwn(config?.mcpServers ?? {}, '__proto__')).toBe(false)
138+
})
139+
100140
test('still allows adding and removing a real server name', async () => {
101141
await addMcpConfig('addedserver', { command: 'echo', args: [] }, 'user')
102142
expect(getMcpConfigByName('addedserver')).not.toBeNull()

0 commit comments

Comments
 (0)