Skip to content

Commit c9933c1

Browse files
authored
feat: add update server permission method without re-init server (#1363)
1 parent 17b7fe8 commit c9933c1

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

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

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import { expect } from 'chai'
77
import * as sinon from 'sinon'
8-
import { McpManager } from './mcpManager'
8+
import { AGENT_TOOLS_CHANGED, MCP_SERVER_STATUS_CHANGED, McpManager } from './mcpManager'
99
import * as mcpUtils from './mcpUtils'
10-
import type { MCPServerConfig } from './mcpTypes'
10+
import type { MCPServerConfig, MCPServerPermissionUpdate } from './mcpTypes'
1111
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1212

1313
// Shared fakes
@@ -484,6 +484,97 @@ describe('getEnabledTools()', () => {
484484
expect(mgr.getAllTools()).to.have.length(1)
485485
expect(mgr.getEnabledTools()).to.be.empty
486486
})
487+
488+
it('server-level permission change (enabled->disabled)', async () => {
489+
const cfg = {
490+
command: 'c',
491+
args: [],
492+
env: {},
493+
disabled: false,
494+
autoApprove: false,
495+
toolOverrides: {},
496+
__configPath__: 'srv.json',
497+
} as MCPServerConfig
498+
loadStub.resolves(new Map([['srv', cfg]]))
499+
const mgr = await McpManager.init(['srv.json'], features)
500+
const client = new Client({ name: 'x', version: 'v' })
501+
;(mgr as any).clients.set('srv', client)
502+
;(mgr as any).mcpTools.push({ serverName: 'srv', toolName: 'tool1', description: '', inputSchema: {} })
503+
504+
const statusEvents: any[] = []
505+
mgr.events.on(MCP_SERVER_STATUS_CHANGED, (_, s) => statusEvents.push(s))
506+
const toolsEvents: any[] = []
507+
mgr.events.on(AGENT_TOOLS_CHANGED, (_, t) => toolsEvents.push(t))
508+
509+
const closeSpy = sinon.spy(client, 'close')
510+
await mgr.updateServerPermission('srv', { disabled: true } as MCPServerPermissionUpdate)
511+
512+
expect(closeSpy.calledOnce).to.be.true
513+
expect(statusEvents).to.deep.equal([{ status: 'DISABLED', toolsCount: 0, lastError: undefined }])
514+
expect(toolsEvents).to.deep.equal([[]])
515+
})
516+
517+
it('server-level permission change (disabled->enabled)', async () => {
518+
const cfg = {
519+
command: 'c',
520+
args: [],
521+
env: {},
522+
disabled: true,
523+
autoApprove: false,
524+
toolOverrides: {},
525+
__configPath__: 'srv2.json',
526+
} as MCPServerConfig
527+
loadStub.resolves(new Map([['srv2', cfg]]))
528+
const mgr = await McpManager.init([], features)
529+
;(mgr as any).mcpServers.set('srv2', cfg)
530+
531+
await mgr.updateServerPermission('srv2', { disabled: false } as MCPServerPermissionUpdate)
532+
expect(initOneStub.calledOnceWith('srv2')).to.be.true
533+
})
534+
535+
it('disables individual tool-level permission and filters tool list', async () => {
536+
const cfg = {
537+
command: 'c',
538+
args: [],
539+
env: {},
540+
disabled: false,
541+
autoApprove: false,
542+
toolOverrides: {},
543+
__configPath__: 'srv3.json',
544+
} as MCPServerConfig
545+
loadStub.resolves(new Map([['srv3', cfg]]))
546+
const mgr = await McpManager.init(['srv3.json'], features)
547+
;(mgr as any).mcpTools = [{ serverName: 'srv3', toolName: 'toolA', description: '', inputSchema: {} }]
548+
549+
const toolsEvents: any[] = []
550+
mgr.events.on(AGENT_TOOLS_CHANGED, (_, t) => toolsEvents.push(t))
551+
await mgr.updateServerPermission('srv3', { toolOverrides: { toolA: { disabled: true } } })
552+
553+
expect(toolsEvents[0]).to.deep.equal([])
554+
})
555+
556+
it('re-enables individual tool-level permission and restores tool', async () => {
557+
const cfg = {
558+
command: 'c',
559+
args: [],
560+
env: {},
561+
disabled: false,
562+
autoApprove: false,
563+
toolOverrides: { toolB: { disabled: true } },
564+
__configPath__: 'srv4.json',
565+
} as MCPServerConfig
566+
loadStub.resolves(new Map([['srv4', cfg]]))
567+
const mgr = await McpManager.init(['srv4.json'], features)
568+
;(mgr as any).mcpTools = [{ serverName: 'srv4', toolName: 'toolB', description: '', inputSchema: {} }]
569+
570+
const toolsEvents: any[] = []
571+
mgr.events.on(AGENT_TOOLS_CHANGED, (_, t) => toolsEvents.push(t))
572+
await mgr.updateServerPermission('srv4', { toolOverrides: { toolB: { disabled: false } } })
573+
574+
expect(toolsEvents[0]).to.deep.equal([
575+
{ serverName: 'srv4', toolName: 'toolB', description: '', inputSchema: {} },
576+
])
577+
})
487578
})
488579

489580
// getAllToolsWithStates()

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ListToolsResponse,
1313
McpServerRuntimeState,
1414
McpServerStatus,
15+
MCPServerPermissionUpdate,
1516
} from './mcpTypes'
1617
import { loadMcpServerConfigs } from './mcpUtils'
1718
import { AgenticChatError } from '../../errors'
@@ -434,7 +435,56 @@ export class McpManager {
434435
}
435436

436437
/**
437-
* Read, mutate, and write the JSON config at the given path atomically
438+
* Update permission for given server: if only tool permission changes, does not teardown and re-init.
439+
*/
440+
public async updateServerPermission(serverName: string, perm: MCPServerPermissionUpdate): Promise<void> {
441+
const oldCfg = this.mcpServers.get(serverName)
442+
if (!oldCfg || !oldCfg.__configPath__) {
443+
throw new Error(`MCP: server '${serverName}' not found`)
444+
}
445+
446+
await this.mutateConfigFile(oldCfg.__configPath__, json => {
447+
json.mcpServers ||= {}
448+
json.mcpServers[serverName] = {
449+
...json.mcpServers[serverName],
450+
...perm,
451+
}
452+
})
453+
454+
const newCfg: MCPServerConfig = {
455+
...oldCfg,
456+
...perm,
457+
__configPath__: oldCfg.__configPath__,
458+
}
459+
this.mcpServers.set(serverName, newCfg)
460+
461+
const oldDisabled = !!oldCfg.disabled // undefined -> false
462+
const newDisabled = perm.disabled ?? oldDisabled // no value -> old value
463+
464+
if (perm.disabled !== undefined && newDisabled !== oldDisabled) {
465+
// Server level ENABLED → DISABLED
466+
if (newDisabled) {
467+
const client = this.clients.get(serverName)
468+
if (client) {
469+
await client.close()
470+
this.clients.delete(serverName)
471+
}
472+
this.setState(serverName, 'DISABLED', 0)
473+
} else {
474+
// Server level DISABLED → ENABLED
475+
await this.initOneServer(serverName, newCfg)
476+
return
477+
}
478+
} else {
479+
const count = this.mcpTools.filter(t => t.serverName === serverName).length
480+
this.setState(serverName, newDisabled ? 'DISABLED' : 'ENABLED', count)
481+
}
482+
483+
this.emitToolsChanged(serverName)
484+
}
485+
486+
/**
487+
* Read, mutate, and write the JSON config at the given path.
438488
* @private
439489
*/
440490
private async mutateConfigFile(configPath: string, mutator: (json: any) => void): Promise<void> {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface MCPServerConfig {
3030
__configPath__?: string
3131
}
3232

33+
export interface MCPServerPermissionUpdate {
34+
disabled?: boolean
35+
autoApprove?: boolean
36+
toolOverrides?: Record<string, { autoApprove?: boolean; disabled?: boolean }>
37+
}
38+
3339
export interface ListToolsResponse {
3440
tools: {
3541
name?: string

0 commit comments

Comments
 (0)