Skip to content

Commit ad71312

Browse files
authored
fix: fix for mcp delete to remove it from mcp config file (aws#1956)
* fix: fix for mcp delete to remove it from mcp config file * fix: fix for unit test failure
1 parent 81e19b9 commit ad71312

File tree

3 files changed

+191
-4
lines changed

3 files changed

+191
-4
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class McpEventHandler {
242242
*/
243243

244244
async onMcpServerClick(params: McpServerClickParams) {
245-
this.#features.logging.log(`[VSCode Server] onMcpServerClick event with params: ${JSON.stringify(params)}`)
245+
this.#features.logging.log(`onMcpServerClick event with params: ${JSON.stringify(params)}`)
246246

247247
// Use a map of handlers for different action types
248248
const handlers: Record<string, () => Promise<any>> = {
@@ -1143,7 +1143,6 @@ export class McpEventHandler {
11431143
this.#pendingPermissionConfig = undefined
11441144

11451145
this.#features.logging.info(`Applied permission changes for server: ${serverName}`)
1146-
this.#isProgrammaticChange = false
11471146
return { id: params.id }
11481147
} catch (error) {
11491148
this.#features.logging.error(`Failed to save MCP permissions: ${error}`)

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const fakeWorkspace = {
2626
mkdir: (_: string, __: any) => Promise.resolve(),
2727
},
2828
getUserHomeDir: () => '',
29+
getAllWorkspaceFolders: () => [{ uri: '/fake/workspace' }],
2930
}
3031
const features = { logging: fakeLogging, workspace: fakeWorkspace, lsp: {} } as any
3132

@@ -234,10 +235,26 @@ describe('addServer()', () => {
234235
describe('removeServer()', () => {
235236
let loadStub: sinon.SinonStub
236237
let saveAgentConfigStub: sinon.SinonStub
238+
let existsStub: sinon.SinonStub
239+
let readFileStub: sinon.SinonStub
240+
let writeFileStub: sinon.SinonStub
241+
let mkdirStub: sinon.SinonStub
242+
let getWorkspaceMcpConfigPathsStub: sinon.SinonStub
243+
let getGlobalMcpConfigPathStub: sinon.SinonStub
237244

238245
beforeEach(() => {
239246
loadStub = stubAgentConfig()
240247
saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves()
248+
existsStub = sinon.stub(fakeWorkspace.fs, 'exists').resolves(true)
249+
readFileStub = sinon
250+
.stub(fakeWorkspace.fs, 'readFile')
251+
.resolves(Buffer.from(JSON.stringify({ mcpServers: { x: {} } })))
252+
writeFileStub = sinon.stub(fakeWorkspace.fs, 'writeFile').resolves()
253+
mkdirStub = sinon.stub(fakeWorkspace.fs, 'mkdir').resolves()
254+
getWorkspaceMcpConfigPathsStub = sinon
255+
.stub(mcpUtils, 'getWorkspaceMcpConfigPaths')
256+
.returns(['ws1/config.json', 'ws2/config.json'])
257+
getGlobalMcpConfigPathStub = sinon.stub(mcpUtils, 'getGlobalMcpConfigPath').returns('global/config.json')
241258
})
242259

243260
afterEach(async () => {
@@ -275,6 +292,106 @@ describe('removeServer()', () => {
275292
expect(saveAgentConfigStub.calledOnce).to.be.true
276293
expect((mgr as any).clients.has('x')).to.be.false
277294
})
295+
296+
it('removes server from all config files', async () => {
297+
const mgr = await McpManager.init([], features)
298+
const dummy = new Client({ name: 'c', version: 'v' })
299+
;(mgr as any).clients.set('x', dummy)
300+
;(mgr as any).mcpServers.set('x', {
301+
command: '',
302+
args: [],
303+
env: {},
304+
timeout: 0,
305+
__configPath__: 'c.json',
306+
} as MCPServerConfig)
307+
;(mgr as any).serverNameMapping.set('x', 'x')
308+
;(mgr as any).agentConfig = {
309+
name: 'test-agent',
310+
version: '1.0.0',
311+
description: 'Test agent',
312+
mcpServers: { x: {} },
313+
tools: ['@x'],
314+
allowedTools: [],
315+
toolsSettings: {},
316+
includedFiles: [],
317+
resources: [],
318+
}
319+
320+
await mgr.removeServer('x')
321+
322+
// Verify that writeFile was called for each config path (2 workspace + 1 global)
323+
expect(writeFileStub.callCount).to.equal(3)
324+
325+
// Verify the content of the writes (should have removed the server)
326+
writeFileStub.getCalls().forEach(call => {
327+
const content = JSON.parse(call.args[1])
328+
expect(content.mcpServers).to.not.have.property('x')
329+
})
330+
})
331+
})
332+
333+
describe('mutateConfigFile()', () => {
334+
let existsStub: sinon.SinonStub
335+
let readFileStub: sinon.SinonStub
336+
let writeFileStub: sinon.SinonStub
337+
let mkdirStub: sinon.SinonStub
338+
let mgr: McpManager
339+
340+
beforeEach(async () => {
341+
sinon.restore()
342+
stubAgentConfig()
343+
existsStub = sinon.stub(fakeWorkspace.fs, 'exists').resolves(true)
344+
readFileStub = sinon
345+
.stub(fakeWorkspace.fs, 'readFile')
346+
.resolves(Buffer.from(JSON.stringify({ mcpServers: { test: {} } })))
347+
writeFileStub = sinon.stub(fakeWorkspace.fs, 'writeFile').resolves()
348+
mkdirStub = sinon.stub(fakeWorkspace.fs, 'mkdir').resolves()
349+
mgr = await McpManager.init([], features)
350+
})
351+
352+
afterEach(async () => {
353+
sinon.restore()
354+
try {
355+
await McpManager.instance.close()
356+
} catch {}
357+
})
358+
359+
it('reads, mutates, and writes config file', async () => {
360+
// Access the private method using type assertion
361+
const mutateConfigFile = (mgr as any).mutateConfigFile.bind(mgr)
362+
363+
await mutateConfigFile('test/path.json', (json: any) => {
364+
json.mcpServers.newServer = { command: 'test' }
365+
delete json.mcpServers.test
366+
})
367+
368+
expect(readFileStub.calledOnce).to.be.true
369+
expect(writeFileStub.calledOnce).to.be.true
370+
371+
// Verify the content was modified correctly
372+
const writtenContent = JSON.parse(writeFileStub.firstCall.args[1])
373+
expect(writtenContent.mcpServers).to.have.property('newServer')
374+
expect(writtenContent.mcpServers).to.not.have.property('test')
375+
})
376+
377+
it('creates new config file if it does not exist', async () => {
378+
existsStub.resolves(false)
379+
readFileStub.rejects({ code: 'ENOENT' })
380+
381+
// Access the private method using type assertion
382+
const mutateConfigFile = (mgr as any).mutateConfigFile.bind(mgr)
383+
384+
await mutateConfigFile('test/path.json', (json: any) => {
385+
json.mcpServers.newServer = { command: 'test' }
386+
})
387+
388+
expect(mkdirStub.calledOnce).to.be.true
389+
expect(writeFileStub.calledOnce).to.be.true
390+
391+
// Verify the content was created correctly
392+
const writtenContent = JSON.parse(writeFileStub.firstCall.args[1])
393+
expect(writtenContent.mcpServers).to.have.property('newServer')
394+
})
278395
})
279396

280397
describe('updateServer()', () => {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ import {
1414
McpServerRuntimeState,
1515
McpServerStatus,
1616
McpPermissionType,
17-
PersonaModel,
1817
MCPServerPermission,
1918
AgentConfig,
2019
} from './mcpTypes'
21-
import { isEmptyEnv, loadAgentConfig, saveAgentConfig, sanitizeName, getGlobalAgentConfigPath } from './mcpUtils'
20+
import {
21+
isEmptyEnv,
22+
loadAgentConfig,
23+
saveAgentConfig,
24+
sanitizeName,
25+
getGlobalAgentConfigPath,
26+
getWorkspaceMcpConfigPaths,
27+
getGlobalMcpConfigPath,
28+
} from './mcpUtils'
2229
import { AgenticChatError } from '../../errors'
2330
import { EventEmitter } from 'events'
2431
import { Mutex } from 'async-mutex'
@@ -658,6 +665,30 @@ export class McpManager {
658665

659666
// Save agent config
660667
await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, cfg.__configPath__)
668+
669+
// Get all config paths and delete the server from each one
670+
const wsUris = this.features.workspace.getAllWorkspaceFolders()?.map(f => f.uri) ?? []
671+
const wsConfigPaths = getWorkspaceMcpConfigPaths(wsUris)
672+
const globalConfigPath = getGlobalMcpConfigPath(this.features.workspace.fs.getUserHomeDir())
673+
const allConfigPaths = [...wsConfigPaths, globalConfigPath]
674+
675+
// Delete the server from all config files
676+
for (const configPath of allConfigPaths) {
677+
try {
678+
await this.mutateConfigFile(configPath, json => {
679+
if (json.mcpServers && json.mcpServers[unsanitizedName]) {
680+
delete json.mcpServers[unsanitizedName]
681+
this.features.logging.info(
682+
`Deleted server '${unsanitizedName}' from config file: ${configPath}`
683+
)
684+
}
685+
})
686+
} catch (err) {
687+
this.features.logging.warn(
688+
`Failed to delete server '${unsanitizedName}' from config file ${configPath}: ${err}`
689+
)
690+
}
691+
}
661692
}
662693

663694
this.mcpServers.delete(serverName)
@@ -1047,6 +1078,46 @@ export class McpManager {
10471078
}
10481079
}
10491080

1081+
/**
1082+
* Read, mutate, and write the MCP JSON config at the given path.
1083+
* @private
1084+
*/
1085+
private async mutateConfigFile(configPath: string, mutator: (json: any) => void): Promise<void> {
1086+
return McpManager.configMutex
1087+
.runExclusive(async () => {
1088+
let json: any = { mcpServers: {} }
1089+
try {
1090+
const raw = await this.features.workspace.fs.readFile(configPath)
1091+
this.features.logging.info(`Updating MCP config file: ${configPath}`)
1092+
const existing = JSON.parse(raw.toString())
1093+
json = { mcpServers: {}, ...existing }
1094+
} catch (err: any) {
1095+
// ignore fire not exist error
1096+
if (err?.code !== 'ENOENT') throw err
1097+
}
1098+
mutator(json)
1099+
1100+
let fsPath: string
1101+
try {
1102+
const uri = URI.parse(configPath)
1103+
fsPath = uri.scheme === 'file' ? uri.fsPath : configPath
1104+
} catch {
1105+
fsPath = configPath
1106+
}
1107+
fsPath = path.normalize(fsPath)
1108+
1109+
const dir = path.dirname(fsPath)
1110+
await this.features.workspace.fs.mkdir(dir, { recursive: true })
1111+
1112+
await this.features.workspace.fs.writeFile(fsPath, JSON.stringify(json, null, 2))
1113+
this.features.logging.debug(`MCP config file write complete: ${configPath}`)
1114+
})
1115+
.catch((e: any) => {
1116+
this.features.logging.error(`MCP: failed to update config at ${configPath}: ${e.message}`)
1117+
throw e
1118+
})
1119+
}
1120+
10501121
public getOriginalToolNames(namespacedName: string): { serverName: string; toolName: string } | undefined {
10511122
return this.toolNameMapping.get(namespacedName)
10521123
}

0 commit comments

Comments
 (0)