Skip to content

Commit 6cfa06c

Browse files
committed
fix: multiple fixes for remote mcp error and timeout
1 parent 2c7d91c commit 6cfa06c

File tree

5 files changed

+125
-69
lines changed

5 files changed

+125
-69
lines changed

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

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { MCP_SERVER_STATUS_CHANGED, McpManager } from './mcpManager'
33
import { ChatTelemetryController } from '../../../chat/telemetry/chatTelemetryController'
44
import { ChokidarFileWatcher } from './chokidarFileWatcher'
55
// eslint-disable-next-line import/no-nodejs-modules
6-
import * as path from 'path'
76
import {
87
DetailedListGroup,
98
DetailedListItem,
@@ -13,14 +12,7 @@ import {
1312
Status,
1413
} from '@aws/language-server-runtimes/protocol'
1514

16-
import {
17-
getGlobalMcpConfigPath,
18-
getGlobalAgentConfigPath,
19-
getWorkspaceMcpConfigPaths,
20-
getWorkspaceAgentConfigPaths,
21-
sanitizeName,
22-
normalizePathFromUri,
23-
} from './mcpUtils'
15+
import { getGlobalAgentConfigPath, getWorkspaceAgentConfigPaths, sanitizeName, normalizePathFromUri } from './mcpUtils'
2416
import {
2517
McpPermissionType,
2618
MCPServerConfig,
@@ -29,7 +21,6 @@ import {
2921
McpServerStatus,
3022
} from './mcpTypes'
3123
import { TelemetryService } from '../../../../shared/telemetry/telemetryService'
32-
import { URI } from 'vscode-uri'
3324
import { ProfileStatusMonitor } from './profileStatusMonitor'
3425

3526
interface PermissionOption {
@@ -38,6 +29,11 @@ interface PermissionOption {
3829
description?: string
3930
}
4031

32+
enum TransportType {
33+
STDIO = 'stdio',
34+
HTTP = 'http',
35+
}
36+
4137
export class McpEventHandler {
4238
#features: Features
4339
#eventListenerRegistered: boolean
@@ -393,7 +389,7 @@ export class McpEventHandler {
393389
const serverStatusError = this.#getServerStatusError(existingValues.name) || {}
394390

395391
// Determine which transport is selected (default to stdio)
396-
const selectedTransport = existingValues.transport || 'stdio'
392+
const selectedTransport = existingValues.transport || TransportType.STDIO
397393

398394
return {
399395
id: params.id,
@@ -432,14 +428,14 @@ export class McpEventHandler {
432428
title: 'Transport',
433429
mandatory: true,
434430
options: [
435-
{ label: 'stdio', value: 'stdio' },
436-
{ label: 'http', value: 'http' },
431+
{ label: TransportType.STDIO, value: TransportType.STDIO },
432+
{ label: TransportType.HTTP, value: TransportType.HTTP },
437433
],
438434
value: selectedTransport,
439435
},
440436
]
441437

442-
if (selectedTransport === 'http') {
438+
if (selectedTransport === TransportType.HTTP) {
443439
return [
444440
...common,
445441
{
@@ -603,8 +599,13 @@ export class McpEventHandler {
603599
errors.push('Either command or url is required')
604600
} else if (command && url) {
605601
errors.push('Provide either command OR url, not both')
606-
} else if (transport && ((transport === 'stdio' && !command) || (transport !== 'stdio' && !url))) {
607-
errors.push(`${transport === 'stdio' ? 'Command' : 'URL'} is required for ${transport} transport`)
602+
} else if (
603+
transport &&
604+
((transport === TransportType.STDIO && !command) || (transport !== TransportType.STDIO && !url))
605+
) {
606+
errors.push(
607+
`${transport === TransportType.STDIO ? 'Command' : 'URL'} is required for ${transport} transport`
608+
)
608609
}
609610

610611
if (values.timeout && values.timeout.trim() !== '') {
@@ -692,7 +693,7 @@ export class McpEventHandler {
692693
// stdio‑specific parsing
693694
let args: string[] = []
694695
let env: Record<string, string> = {}
695-
if (selectedTransport === 'stdio') {
696+
if (selectedTransport === TransportType.STDIO) {
696697
try {
697698
args = (Array.isArray(params.optionsValues.args) ? params.optionsValues.args : [])
698699
.map((item: any) =>
@@ -719,7 +720,7 @@ export class McpEventHandler {
719720

720721
// http‑specific parsing
721722
let headers: Record<string, string> = {}
722-
if (selectedTransport === 'http') {
723+
if (selectedTransport === TransportType.HTTP) {
723724
try {
724725
const raw = Array.isArray(params.optionsValues.headers) ? params.optionsValues.headers : []
725726
headers = raw.reduce((acc: Record<string, string>, item: any) => {
@@ -743,7 +744,7 @@ export class McpEventHandler {
743744

744745
// build final config (no transport field persisted)
745746
let config: MCPServerConfig
746-
if (selectedTransport === 'http') {
747+
if (selectedTransport === TransportType.HTTP) {
747748
config = {
748749
url: params.optionsValues.url,
749750
headers,
@@ -786,14 +787,15 @@ export class McpEventHandler {
786787
}
787788

788789
this.#currentEditingServerName = undefined
790+
this.#serverNameBeforeUpdate = undefined
789791

790792
// need to check server state now, as there is possibility of error during server initialization
791793
const serverStatusError = this.#getServerStatusError(serverName)
792794

793795
this.#telemetryController?.emitMCPServerInitializeEvent({
794796
source: isEditMode ? 'updateServer' : 'addServer',
795-
command: selectedTransport === 'stdio' ? params.optionsValues.command : undefined,
796-
url: selectedTransport === 'http' ? params.optionsValues.url : undefined,
797+
command: selectedTransport === TransportType.STDIO ? params.optionsValues.command : undefined,
798+
url: selectedTransport === TransportType.HTTP ? params.optionsValues.url : undefined,
797799
enabled: true,
798800
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
799801
scope: params.optionsValues['scope'] === 'global' ? 'global' : 'workspace',
@@ -1014,7 +1016,7 @@ export class McpEventHandler {
10141016
}
10151017

10161018
// Respect a user flip first; otherwise fall back to what the stored configuration implies.
1017-
const transport = params.optionsValues?.transport ?? (config.url ? 'http' : 'stdio')
1019+
const transport = params.optionsValues?.transport ?? (config.url ? TransportType.HTTP : TransportType.STDIO)
10181020

10191021
// Convert stored structures to UI‑friendly lists
10201022
const argsList = (config.args ?? []).map(a => ({ arg_key: a })) // for stdio
@@ -1090,8 +1092,9 @@ export class McpEventHandler {
10901092

10911093
// Clean up transport-specific fields
10921094
if (optionsValues) {
1093-
const transport = optionsValues.transport ?? 'stdio' // Maintain default to 'stdio'
1094-
const fieldsToDelete = transport === 'http' ? ['command', 'args', 'env_variables'] : ['url', 'headers']
1095+
const transport = optionsValues.transport ?? TransportType.STDIO // Maintain default to 'stdio'
1096+
const fieldsToDelete =
1097+
transport === TransportType.HTTP ? ['command', 'args', 'env_variables'] : ['url', 'headers']
10951098

10961099
fieldsToDelete.forEach(field => delete optionsValues[field])
10971100
}
@@ -1238,11 +1241,11 @@ export class McpEventHandler {
12381241
const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName)
12391242
if (serverConfig) {
12401243
// Emit server initialize event after permission change
1241-
const transportType = serverConfig.command ? 'stdio' : 'http'
1244+
const transportType = serverConfig.command?.trim() ? TransportType.STDIO : TransportType.HTTP
12421245
this.#telemetryController?.emitMCPServerInitializeEvent({
12431246
source: 'updatePermission',
1244-
command: transportType === 'stdio' ? serverConfig.command : undefined,
1245-
url: transportType === 'http' ? serverConfig.url : undefined,
1247+
command: transportType === TransportType.STDIO ? serverConfig.command : undefined,
1248+
url: transportType === TransportType.HTTP ? serverConfig.url : undefined,
12461249
enabled: true,
12471250
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
12481251
scope:
@@ -1310,16 +1313,16 @@ export class McpEventHandler {
13101313

13111314
// Emit server initialize events for all active servers
13121315
for (const [serverName, config] of serverConfigs.entries()) {
1313-
const transportType = config.command ? 'stdio' : 'http'
1316+
const transportType = config.command ? TransportType.STDIO : TransportType.HTTP
13141317
const enabled = !mcpManager.isServerDisabled(serverName)
13151318
this.#telemetryController?.emitMCPServerInitializeEvent({
13161319
source: 'reload',
1317-
command: transportType === 'stdio' ? config.command : undefined,
1318-
url: transportType === 'http' ? config.url : undefined,
1320+
command: transportType === TransportType.STDIO ? config.command : undefined,
1321+
url: transportType === TransportType.HTTP ? config.url : undefined,
13191322
enabled: enabled,
13201323
numTools: mcpManager.getAllToolsWithPermissions(serverName).length,
13211324
scope: config.__configPath__ === globalAgentPath ? 'global' : 'workspace',
1322-
transportType: 'stdio',
1325+
transportType: transportType,
13231326
languageServerVersion: this.#features.runtime.serverInfo.version,
13241327
})
13251328
}

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export class McpManager {
300300
this.features.logging.debug(`MCP: initializing server [${serverName}]`)
301301

302302
const client = new Client({
303-
name: `mcp-client-${serverName}`,
303+
name: `q-chat-plugin`, // Do not use server name in the client name to avoid polluting builder-mcp metrics
304304
version: '1.0.0',
305305
})
306306

@@ -670,7 +670,7 @@ export class McpManager {
670670
disabled: cfg.disabled ?? false,
671671
}
672672
// Only add timeout to agent config if it's not 0
673-
if (cfg.timeout !== 0) {
673+
if (cfg.timeout !== undefined) {
674674
serverConfig.timeout = cfg.timeout
675675
}
676676
if (cfg.args && cfg.args.length > 0) {
@@ -1293,11 +1293,21 @@ export class McpManager {
12931293
private handleError(server: string | undefined, err: unknown) {
12941294
const msg = err instanceof Error ? err.message : String(err)
12951295

1296-
this.features.logging.error(`MCP ERROR${server ? ` [${server}]` : ''}: ${msg}`)
1296+
const isBenignSseDisconnect =
1297+
/SSE error:\s*TypeError:\s*terminated:\s*Body Timeout Error/i.test(msg) ||
1298+
/TypeError:\s*terminated:\s*Body Timeout Error/i.test(msg) ||
1299+
/TypeError:\s*terminated:\s*other side closed/i.test(msg) ||
1300+
/ECONNRESET|ENETRESET|EPIPE/i.test(msg)
12971301

1298-
if (server) {
1299-
this.setState(server, McpServerStatus.FAILED, 0, msg)
1300-
this.emitToolsChanged(server)
1302+
if (isBenignSseDisconnect) {
1303+
this.features.logging.debug(`MCP SSE idle timeout${server ? ` [${server}]` : ''}: ${msg}`)
1304+
} else {
1305+
// default path for real errors
1306+
this.features.logging.error(`MCP ERROR${server ? ` [${server}]` : ''}: ${msg}`)
1307+
if (server) {
1308+
this.setState(server, McpServerStatus.FAILED, 0, msg)
1309+
this.emitToolsChanged(server)
1310+
}
13011311
}
13021312
}
13031313

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { expect } from 'chai'
77
import * as sinon from 'sinon'
88
import * as crypto from 'crypto'
99
import * as http from 'http'
10+
import { EventEmitter } from 'events'
1011
import * as path from 'path'
1112
import { OAuthClient } from './mcpOauthClient'
1213

@@ -51,21 +52,22 @@ function stubFileSystem(tokenObj?: any, regObj?: any): void {
5152
}
5253

5354
function stubHttpServer(): void {
54-
sinon.stub(http, 'createServer').returns({
55-
listen: (...args: any[]) => {
56-
const cb = args.find(a => typeof a === 'function')
57-
if (cb) cb()
58-
return {} as any
59-
},
60-
close: (cb?: any) => {
61-
if (cb) cb()
62-
return {} as any
63-
},
64-
on: (..._args: any[]) => {
65-
return {} as any
66-
},
67-
address: () => ({ address: '127.0.0.1', port: 12345, family: 'IPv4' }),
68-
} as unknown as http.Server)
55+
sinon.stub(http, 'createServer').callsFake(() => {
56+
const srv = new EventEmitter() as unknown as http.Server & EventEmitter
57+
;(srv as any).address = () => ({ address: '127.0.0.1', port: 12345, family: 'IPv4' })
58+
;(srv as any).listen = (_port?: any, _host?: any, _backlog?: any, cb?: any) => {
59+
if (typeof cb === 'function') cb()
60+
// simulate async readiness like a real server
61+
process.nextTick(() => srv.emit('listening'))
62+
return srv
63+
}
64+
;(srv as any).close = (cb?: any) => {
65+
if (typeof cb === 'function') cb()
66+
srv.removeAllListeners()
67+
return srv
68+
}
69+
return srv
70+
})
6971
}
7072

7173
describe('OAuthClient helpers', () => {

0 commit comments

Comments
 (0)