Skip to content

Commit 86ec96f

Browse files
authored
telemetry(mcp): adding mcp telemetry metrics (#1528)
* fix: two telemetry events are emitted for mcp except init case * fix: emitting amazonq_mcpConfig metric from init function of mcpManager. * fix: adding amazonq_mcpConfig to enable and disable config cases * fix: refactoring code
1 parent 0e68803 commit 86ec96f

File tree

8 files changed

+251
-15
lines changed

8 files changed

+251
-15
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class AgenticChatController implements ChatHandlers {
199199
this.#features.workspace,
200200
this.#features.lsp
201201
)
202-
this.#mcpEventHandler = new McpEventHandler(features)
202+
this.#mcpEventHandler = new McpEventHandler(features, telemetryService)
203203
}
204204

205205
async onButtonClick(params: ButtonClickParams): Promise<ButtonClickResult> {

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import * as sinon from 'sinon'
88
import { McpEventHandler } from './mcpEventHandler'
99
import { McpManager } from './mcpManager'
1010
import * as mcpUtils from './mcpUtils'
11+
import { TelemetryService } from '../../../../shared/telemetry/telemetryService'
1112

1213
describe('McpEventHandler error handling', () => {
1314
let eventHandler: McpEventHandler
1415
let features: any
16+
let telemetryService: TelemetryService
1517
let loadStub: sinon.SinonStub
1618

1719
beforeEach(() => {
@@ -44,8 +46,16 @@ describe('McpEventHandler error handling', () => {
4446
lsp: {},
4547
}
4648

49+
// Create mock telemetry service
50+
telemetryService = {
51+
emitUserTriggerDecision: sinon.stub(),
52+
emitChatInteractWithMessage: sinon.stub(),
53+
emitUserModificationEvent: sinon.stub(),
54+
emitCodeCoverageEvent: sinon.stub(),
55+
} as unknown as TelemetryService
56+
4757
// Create the event handler
48-
eventHandler = new McpEventHandler(features)
58+
eventHandler = new McpEventHandler(features, telemetryService)
4959

5060
// Stub loadPersonaPermissions
5161
sinon

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

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Features } from '../../../types'
22
import { MCP_SERVER_STATUS_CHANGED, McpManager } from './mcpManager'
3+
import { ChatTelemetryController } from '../../../chat/telemetry/chatTelemetryController'
34
import {
45
DetailedListGroup,
56
DetailedListItem,
@@ -22,6 +23,7 @@ import {
2223
McpServerRuntimeState,
2324
McpServerStatus,
2425
} from './mcpTypes'
26+
import { TelemetryService } from '../../../../shared/telemetry/telemetryService'
2527

2628
interface PermissionOption {
2729
label: string
@@ -33,12 +35,14 @@ export class McpEventHandler {
3335
#eventListenerRegistered: boolean
3436
#currentEditingServerName: string | undefined
3537
#shouldDisplayListMCPServers: boolean
38+
#telemetryController: ChatTelemetryController
3639

37-
constructor(features: Features) {
40+
constructor(features: Features, telemetryService: TelemetryService) {
3841
this.#features = features
3942
this.#eventListenerRegistered = false
4043
this.#currentEditingServerName = undefined
4144
this.#shouldDisplayListMCPServers = true
45+
this.#telemetryController = new ChatTelemetryController(features, telemetryService)
4246
}
4347

4448
/**
@@ -641,13 +645,26 @@ export class McpEventHandler {
641645
}
642646

643647
this.#currentEditingServerName = undefined
648+
644649
// need to check server state now, as there is possibility of error during server initialization
645650
const serverStatusError = this.#getServerStatusError(serverName)
651+
652+
// Emit telemetry event regardless of success/failure
653+
this.#telemetryController?.emitMCPServerInitializeEvent({
654+
source: isEditMode ? 'updateServer' : 'addServer',
655+
command: config.command,
656+
enabled: true,
657+
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
658+
scope: params.optionsValues['scope'] === 'global' ? 'global' : 'workspace',
659+
transportType: 'stdio',
660+
languageServerVersion: this.#features.runtime.serverInfo.version,
661+
})
662+
646663
if (serverStatusError) {
647-
// error case: remove config from config file but persist in memory
664+
// Error case: remove config from config file but persist in memory
648665
await McpManager.instance.removeServerFromConfigFile(serverName)
649666

650-
// stays on add/edit page and show error to user
667+
// Stay on add/edit page and show error to user
651668
if (isEditMode) {
652669
params.id = 'edit-mcp'
653670
params.title = originalServerName!
@@ -657,7 +674,7 @@ export class McpEventHandler {
657674
return this.#handleAddNewMcp(params)
658675
}
659676
} else {
660-
// success case: goes to tools permissions page
677+
// Success case: go to tools permissions page
661678
return this.#handleOpenMcpServer({ id: 'open-mcp-server', title: serverName })
662679
}
663680
}
@@ -755,6 +772,7 @@ export class McpEventHandler {
755772

756773
try {
757774
await McpManager.instance.updateServerPermission(serverName, perm)
775+
this.#emitMCPConfigEvent()
758776
} catch (error) {
759777
this.#features.logging.error(`Failed to enable MCP server: ${error}`)
760778
}
@@ -781,6 +799,7 @@ export class McpEventHandler {
781799

782800
try {
783801
await McpManager.instance.updateServerPermission(serverName, perm)
802+
this.#emitMCPConfigEvent()
784803
} catch (error) {
785804
this.#features.logging.error(`Failed to disable MCP server: ${error}`)
786805
}
@@ -973,20 +992,100 @@ export class McpEventHandler {
973992
const mcpServerPermission = await this.#processPermissionUpdates(updatedPermissionConfig)
974993

975994
await McpManager.instance.updateServerPermission(serverName, mcpServerPermission)
995+
this.#emitMCPConfigEvent()
996+
997+
// Get server config to emit telemetry
998+
const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName)
999+
if (serverConfig) {
1000+
// Emit server initialize event after permission change
1001+
this.#telemetryController?.emitMCPServerInitializeEvent({
1002+
source: 'updatePermission',
1003+
command: serverConfig.command,
1004+
enabled: true,
1005+
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
1006+
scope:
1007+
serverConfig?.__configPath__ ===
1008+
getGlobalMcpConfigPath(this.#features.workspace.fs.getUserHomeDir())
1009+
? 'global'
1010+
: 'workspace',
1011+
transportType: 'stdio',
1012+
languageServerVersion: this.#features.runtime.serverInfo.version,
1013+
})
1014+
}
9761015
return { id: params.id }
9771016
} catch (error) {
9781017
this.#features.logging.error(`Failed to update MCP permissions: ${error}`)
9791018
return { id: params.id }
9801019
}
9811020
}
9821021

1022+
#emitMCPConfigEvent() {
1023+
// Emit MCP config event after reinitialization
1024+
const mcpManager = McpManager.instance
1025+
const serverConfigs = mcpManager.getAllServerConfigs()
1026+
const activeServers = Array.from(serverConfigs.entries()).filter(
1027+
([name, _]) => !mcpManager.isServerDisabled(name)
1028+
)
1029+
1030+
// Count global vs project servers
1031+
const globalServers = Array.from(serverConfigs.entries()).filter(
1032+
([_, config]) =>
1033+
config?.__configPath__ === getGlobalMcpConfigPath(this.#features.workspace.fs.getUserHomeDir())
1034+
).length
1035+
const projectServers = serverConfigs.size - globalServers
1036+
1037+
// Count tools by permission
1038+
let toolsAlwaysAllowed = 0
1039+
let toolsDenied = 0
1040+
1041+
for (const [serverName, _] of activeServers) {
1042+
const toolsWithPermissions = mcpManager.getAllToolsWithPermissions(serverName)
1043+
toolsWithPermissions.forEach(item => {
1044+
if (item.permission === McpPermissionType.alwaysAllow) {
1045+
toolsAlwaysAllowed++
1046+
} else if (item.permission === McpPermissionType.deny) {
1047+
toolsDenied++
1048+
}
1049+
})
1050+
}
1051+
1052+
this.#telemetryController?.emitMCPConfigEvent({
1053+
numActiveServers: activeServers.length,
1054+
numGlobalServers: globalServers,
1055+
numProjectServers: projectServers,
1056+
numToolsAlwaysAllowed: toolsAlwaysAllowed,
1057+
numToolsDenied: toolsDenied,
1058+
languageServerVersion: this.#features.runtime.serverInfo.version,
1059+
})
1060+
1061+
// Emit server initialize events for all active servers
1062+
for (const [serverName, config] of serverConfigs.entries()) {
1063+
const enabled = !mcpManager.isServerDisabled(serverName)
1064+
if (enabled) {
1065+
this.#telemetryController?.emitMCPServerInitializeEvent({
1066+
source: 'reload',
1067+
command: config.command,
1068+
enabled,
1069+
numTools: mcpManager.getAllToolsWithPermissions(serverName).length,
1070+
scope:
1071+
config?.__configPath__ === getGlobalMcpConfigPath(this.#features.workspace.fs.getUserHomeDir())
1072+
? 'global'
1073+
: 'workspace',
1074+
transportType: 'stdio',
1075+
languageServerVersion: this.#features.runtime.serverInfo.version,
1076+
})
1077+
}
1078+
}
1079+
}
1080+
9831081
/**
9841082
* Handled refresh MCP list events
9851083
*/
9861084
async #handleRefreshMCPList(params: McpServerClickParams) {
9871085
this.#shouldDisplayListMCPServers = true
9881086
try {
9891087
await McpManager.instance.reinitializeMcpServers()
1088+
this.#emitMCPConfigEvent()
9901089
return {
9911090
id: params.id,
9921091
}

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

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55

66
import type { Features } from '@aws/language-server-runtimes/server-interface/server'
7+
import { ChatTelemetryEventName } from '../../../../shared/telemetry/types'
8+
import { getGlobalMcpConfigPath } from './mcpUtils'
79
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
810
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
911
import {
@@ -45,7 +47,10 @@ export class McpManager {
4547
private constructor(
4648
private configPaths: string[],
4749
private personaPaths: string[],
48-
private features: Pick<Features, 'logging' | 'workspace' | 'lsp'>
50+
private features: Pick<
51+
Features,
52+
'logging' | 'workspace' | 'lsp' | 'telemetry' | 'credentialsProvider' | 'runtime'
53+
>
4954
) {
5055
this.mcpTools = []
5156
this.clients = new Map<string, Client>()
@@ -58,19 +63,58 @@ export class McpManager {
5863
this.toolNameMapping = new Map<string, { serverName: string; toolName: string }>()
5964
}
6065

61-
/**
62-
* Initialize or return existing manager, then discover all servers.
63-
*/
6466
public static async init(
6567
configPaths: string[],
6668
personaPaths: string[],
67-
features: Pick<Features, 'logging' | 'workspace' | 'lsp'>
69+
features: Pick<Features, 'logging' | 'workspace' | 'lsp' | 'telemetry' | 'credentialsProvider' | 'runtime'>
6870
): Promise<McpManager> {
6971
if (!McpManager.#instance) {
7072
const mgr = new McpManager(configPaths, personaPaths, features)
7173
McpManager.#instance = mgr
7274
await mgr.discoverAllServers()
7375
features.logging.info(`MCP: discovered ${mgr.mcpTools.length} tools across all servers`)
76+
77+
// Emit MCP configuration metrics
78+
const serverConfigs = mgr.getAllServerConfigs()
79+
const activeServers = Array.from(serverConfigs.entries()).filter(([name, _]) => !mgr.isServerDisabled(name))
80+
81+
// Count global vs project servers
82+
const globalServers = Array.from(serverConfigs.entries()).filter(
83+
([_, config]) =>
84+
config?.__configPath__ === getGlobalMcpConfigPath(features.workspace.fs.getUserHomeDir())
85+
).length
86+
const projectServers = serverConfigs.size - globalServers
87+
88+
// Count tools by permission
89+
let toolsAlwaysAllowed = 0
90+
let toolsDenied = 0
91+
92+
for (const [serverName, _] of activeServers) {
93+
const toolsWithPermissions = mgr.getAllToolsWithPermissions(serverName)
94+
toolsWithPermissions.forEach(item => {
95+
if (item.permission === McpPermissionType.alwaysAllow) {
96+
toolsAlwaysAllowed++
97+
} else if (item.permission === McpPermissionType.deny) {
98+
toolsDenied++
99+
}
100+
})
101+
}
102+
103+
// Emit MCP configuration metrics
104+
if (features.telemetry) {
105+
features.telemetry.emitMetric({
106+
name: ChatTelemetryEventName.MCPConfig,
107+
data: {
108+
credentialStartUrl: features.credentialsProvider?.getConnectionMetadata()?.sso?.startUrl,
109+
languageServerVersion: features.runtime?.serverInfo.version,
110+
numActiveServers: activeServers.length,
111+
numGlobalServers: globalServers,
112+
numProjectServers: projectServers,
113+
numToolsAlwaysAllowed: toolsAlwaysAllowed,
114+
numToolsDenied: toolsDenied,
115+
},
116+
})
117+
}
74118
}
75119
return McpManager.#instance
76120
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ describe('McpTool', () => {
2020
},
2121
},
2222
lsp: {},
23+
credentialsProvider: {},
24+
telemetry: { record: () => {} },
2325
} as unknown as Pick<
2426
import('@aws/language-server-runtimes/server-interface/server').Features,
25-
'logging' | 'workspace' | 'lsp'
27+
'logging' | 'workspace' | 'lsp' | 'credentialsProvider' | 'telemetry' | 'runtime'
2628
>
2729

2830
const definition: McpToolDefinition = {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const LspToolsServer: Server = ({ workspace, logging, lsp, agent }) => {
8282
return () => {}
8383
}
8484

85-
export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging, lsp, agent }) => {
85+
export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging, lsp, agent, telemetry, runtime }) => {
8686
const registered: Record<string, string[]> = {}
8787

8888
const allNamespacedTools = new Set<string>()
@@ -141,7 +141,14 @@ export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging
141141
const globalPersonaPath = getGlobalPersonaConfigPath(workspace.fs.getUserHomeDir())
142142
const allPersonaPaths = [...wsPersonaPaths, globalPersonaPath]
143143

144-
const mgr = await McpManager.init(allConfigPaths, allPersonaPaths, { logging, workspace, lsp })
144+
const mgr = await McpManager.init(allConfigPaths, allPersonaPaths, {
145+
logging,
146+
workspace,
147+
lsp,
148+
telemetry,
149+
credentialsProvider,
150+
runtime,
151+
})
145152

146153
// Clear tool name mapping before registering all tools to avoid conflicts from previous registrations
147154
McpManager.instance.clearToolNameMapping()

0 commit comments

Comments
 (0)