@google/gemini-cli-core 0.21.0-nightly.20251203.533a3fb31 → 0.21.0-nightly.20251205.f4f2bcbd9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/google-gemini-cli-core-0.21.0-nightly.20251202.2d935b379.tgz +0 -0
  2. package/dist/src/code_assist/oauth2.d.ts +2 -0
  3. package/dist/src/code_assist/oauth2.js +28 -12
  4. package/dist/src/code_assist/oauth2.js.map +1 -1
  5. package/dist/src/code_assist/oauth2.test.js +40 -2
  6. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  7. package/dist/src/commands/restore.d.ts +19 -0
  8. package/dist/src/commands/restore.js +45 -0
  9. package/dist/src/commands/restore.js.map +1 -0
  10. package/dist/src/commands/restore.test.d.ts +6 -0
  11. package/dist/src/commands/restore.test.js +136 -0
  12. package/dist/src/commands/restore.test.js.map +1 -0
  13. package/dist/src/commands/types.d.ts +41 -0
  14. package/dist/src/commands/types.js +7 -0
  15. package/dist/src/commands/types.js.map +1 -0
  16. package/dist/src/config/config.d.ts +17 -2
  17. package/dist/src/config/config.js +28 -0
  18. package/dist/src/config/config.js.map +1 -1
  19. package/dist/src/core/client.js +2 -1
  20. package/dist/src/core/client.js.map +1 -1
  21. package/dist/src/core/client.test.js +23 -0
  22. package/dist/src/core/client.test.js.map +1 -1
  23. package/dist/src/core/geminiChat.test.js +4 -0
  24. package/dist/src/core/geminiChat.test.js.map +1 -1
  25. package/dist/src/core/sessionHookTriggers.d.ts +28 -0
  26. package/dist/src/core/sessionHookTriggers.js +68 -0
  27. package/dist/src/core/sessionHookTriggers.js.map +1 -0
  28. package/dist/src/generated/git-commit.d.ts +2 -2
  29. package/dist/src/generated/git-commit.js +2 -2
  30. package/dist/src/hooks/hookEventHandler.js +51 -0
  31. package/dist/src/hooks/hookEventHandler.js.map +1 -1
  32. package/dist/src/hooks/hookRegistry.js +8 -1
  33. package/dist/src/hooks/hookRegistry.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.test.js +1 -0
  35. package/dist/src/hooks/hookRegistry.test.js.map +1 -1
  36. package/dist/src/hooks/hookRunner.js +12 -2
  37. package/dist/src/hooks/hookRunner.js.map +1 -1
  38. package/dist/src/hooks/hookSystem.test.js +124 -0
  39. package/dist/src/hooks/hookSystem.test.js.map +1 -1
  40. package/dist/src/hooks/index.d.ts +3 -1
  41. package/dist/src/hooks/index.js +3 -0
  42. package/dist/src/hooks/index.js.map +1 -1
  43. package/dist/src/hooks/types.d.ts +1 -2
  44. package/dist/src/hooks/types.js +0 -1
  45. package/dist/src/hooks/types.js.map +1 -1
  46. package/dist/src/index.d.ts +2 -0
  47. package/dist/src/index.js +2 -0
  48. package/dist/src/index.js.map +1 -1
  49. package/dist/src/output/json-formatter.d.ts +2 -2
  50. package/dist/src/output/json-formatter.js +6 -3
  51. package/dist/src/output/json-formatter.js.map +1 -1
  52. package/dist/src/output/json-formatter.test.js +35 -9
  53. package/dist/src/output/json-formatter.test.js.map +1 -1
  54. package/dist/src/output/types.d.ts +1 -0
  55. package/dist/src/output/types.js.map +1 -1
  56. package/dist/src/services/chatCompressionService.js +12 -0
  57. package/dist/src/services/chatCompressionService.js.map +1 -1
  58. package/dist/src/services/chatCompressionService.test.js +2 -0
  59. package/dist/src/services/chatCompressionService.test.js.map +1 -1
  60. package/dist/src/services/shellExecutionService.js +5 -18
  61. package/dist/src/services/shellExecutionService.js.map +1 -1
  62. package/dist/src/services/shellExecutionService.test.js +29 -2
  63. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  64. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +1 -0
  65. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +20 -0
  66. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  67. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +49 -0
  68. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  69. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +1 -0
  70. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +3 -1
  71. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  72. package/dist/src/telemetry/config.js +2 -0
  73. package/dist/src/telemetry/config.js.map +1 -1
  74. package/dist/src/telemetry/config.test.js +25 -0
  75. package/dist/src/telemetry/config.test.js.map +1 -1
  76. package/dist/src/telemetry/gcp-exporters.d.ts +4 -3
  77. package/dist/src/telemetry/gcp-exporters.js +7 -4
  78. package/dist/src/telemetry/gcp-exporters.js.map +1 -1
  79. package/dist/src/telemetry/index.d.ts +1 -1
  80. package/dist/src/telemetry/index.js +1 -1
  81. package/dist/src/telemetry/index.js.map +1 -1
  82. package/dist/src/telemetry/loggers.js +335 -335
  83. package/dist/src/telemetry/loggers.js.map +1 -1
  84. package/dist/src/telemetry/loggers.test.js +15 -0
  85. package/dist/src/telemetry/loggers.test.js.map +1 -1
  86. package/dist/src/telemetry/sdk.d.ts +9 -2
  87. package/dist/src/telemetry/sdk.js +126 -16
  88. package/dist/src/telemetry/sdk.js.map +1 -1
  89. package/dist/src/telemetry/sdk.test.js +111 -28
  90. package/dist/src/telemetry/sdk.test.js.map +1 -1
  91. package/dist/src/telemetry/startupProfiler.test.js +4 -0
  92. package/dist/src/telemetry/startupProfiler.test.js.map +1 -1
  93. package/dist/src/telemetry/telemetry.test.js +10 -3
  94. package/dist/src/telemetry/telemetry.test.js.map +1 -1
  95. package/dist/src/tools/mcp-client-manager.js +15 -4
  96. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  97. package/dist/src/tools/mcp-client.d.ts +17 -2
  98. package/dist/src/tools/mcp-client.js +319 -166
  99. package/dist/src/tools/mcp-client.js.map +1 -1
  100. package/dist/src/tools/mcp-client.test.js +466 -26
  101. package/dist/src/tools/mcp-client.test.js.map +1 -1
  102. package/dist/src/tools/modifiable-tool.test.js +22 -13
  103. package/dist/src/tools/modifiable-tool.test.js.map +1 -1
  104. package/dist/src/utils/debugLogger.d.ts +3 -0
  105. package/dist/src/utils/debugLogger.js +27 -0
  106. package/dist/src/utils/debugLogger.js.map +1 -1
  107. package/dist/src/utils/editCorrector.test.js +4 -0
  108. package/dist/src/utils/editCorrector.test.js.map +1 -1
  109. package/dist/src/utils/editor.d.ts +9 -1
  110. package/dist/src/utils/editor.js +23 -14
  111. package/dist/src/utils/editor.js.map +1 -1
  112. package/dist/src/utils/errors.d.ts +8 -0
  113. package/dist/src/utils/errors.js +32 -0
  114. package/dist/src/utils/errors.js.map +1 -1
  115. package/dist/src/utils/errors.test.d.ts +6 -0
  116. package/dist/src/utils/errors.test.js +36 -0
  117. package/dist/src/utils/errors.test.js.map +1 -0
  118. package/dist/src/utils/nextSpeakerChecker.test.js +4 -0
  119. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  120. package/dist/src/utils/retry.js +38 -5
  121. package/dist/src/utils/retry.js.map +1 -1
  122. package/dist/src/utils/retry.test.js +35 -4
  123. package/dist/src/utils/retry.test.js.map +1 -1
  124. package/dist/src/utils/terminalSerializer.test.js +17 -0
  125. package/dist/src/utils/terminalSerializer.test.js.map +1 -1
  126. package/dist/tsconfig.tsbuildinfo +1 -1
  127. package/package.json +1 -1
@@ -8,7 +8,7 @@ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv
8
8
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
9
9
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
10
10
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
11
- import { ListRootsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
+ import { ListRootsRequestSchema, ToolListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
12
12
  import { parse } from 'shell-quote';
13
13
  import { AuthProviderType } from '../config/config.js';
14
14
  import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
@@ -19,7 +19,7 @@ import { pathToFileURL } from 'node:url';
19
19
  import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
20
20
  import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
21
21
  import { OAuthUtils } from '../mcp/oauth-utils.js';
22
- import { getErrorMessage } from '../utils/errors.js';
22
+ import { getErrorMessage, isAuthenticationError, UnauthorizedError, } from '../utils/errors.js';
23
23
  import { debugLogger } from '../utils/debugLogger.js';
24
24
  import { coreEvents } from '../utils/events.js';
25
25
  export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
@@ -61,17 +61,23 @@ export class McpClient {
61
61
  toolRegistry;
62
62
  promptRegistry;
63
63
  workspaceContext;
64
+ cliConfig;
64
65
  debugMode;
66
+ onToolsUpdated;
65
67
  client;
66
68
  transport;
67
69
  status = MCPServerStatus.DISCONNECTED;
68
- constructor(serverName, serverConfig, toolRegistry, promptRegistry, workspaceContext, debugMode) {
70
+ isRefreshing = false;
71
+ pendingRefresh = false;
72
+ constructor(serverName, serverConfig, toolRegistry, promptRegistry, workspaceContext, cliConfig, debugMode, onToolsUpdated) {
69
73
  this.serverName = serverName;
70
74
  this.serverConfig = serverConfig;
71
75
  this.toolRegistry = toolRegistry;
72
76
  this.promptRegistry = promptRegistry;
73
77
  this.workspaceContext = workspaceContext;
78
+ this.cliConfig = cliConfig;
74
79
  this.debugMode = debugMode;
80
+ this.onToolsUpdated = onToolsUpdated;
75
81
  }
76
82
  /**
77
83
  * Connects to the MCP server.
@@ -83,6 +89,15 @@ export class McpClient {
83
89
  this.updateStatus(MCPServerStatus.CONNECTING);
84
90
  try {
85
91
  this.client = await connectToMcpServer(this.serverName, this.serverConfig, this.debugMode, this.workspaceContext);
92
+ // setup dynamic tool listener
93
+ const capabilities = this.client.getServerCapabilities();
94
+ if (capabilities?.tools?.listChanged) {
95
+ debugLogger.log(`Server '${this.serverName}' supports tool updates. Listening for changes...`);
96
+ this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
97
+ debugLogger.log(`🔔 Received tool update notification from '${this.serverName}'`);
98
+ await this.refreshTools();
99
+ });
100
+ }
86
101
  const originalOnError = this.client.onerror;
87
102
  this.client.onerror = (error) => {
88
103
  if (this.status !== MCPServerStatus.CONNECTED) {
@@ -150,9 +165,11 @@ export class McpClient {
150
165
  throw new Error(`Client is not connected, must connect before interacting with the server. Current state is ${this.status}`);
151
166
  }
152
167
  }
153
- async discoverTools(cliConfig) {
168
+ async discoverTools(cliConfig, options) {
154
169
  this.assertConnected();
155
- return discoverTools(this.serverName, this.serverConfig, this.client, cliConfig, this.toolRegistry.getMessageBus());
170
+ return discoverTools(this.serverName, this.serverConfig, this.client, cliConfig, this.toolRegistry.getMessageBus(), options ?? {
171
+ timeout: this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
172
+ });
156
173
  }
157
174
  async discoverPrompts() {
158
175
  this.assertConnected();
@@ -164,6 +181,59 @@ export class McpClient {
164
181
  getInstructions() {
165
182
  return this.client?.getInstructions();
166
183
  }
184
+ /**
185
+ * Refreshes the tools for this server by re-querying the MCP `tools/list` endpoint.
186
+ *
187
+ * This method implements a **Coalescing Pattern** to handle rapid bursts of notifications
188
+ * (e.g., during server startup or bulk updates) without overwhelming the server or
189
+ * creating race conditions in the global ToolRegistry.
190
+ */
191
+ async refreshTools() {
192
+ if (this.isRefreshing) {
193
+ debugLogger.log(`Tool refresh for '${this.serverName}' is already in progress. Pending update.`);
194
+ this.pendingRefresh = true;
195
+ return;
196
+ }
197
+ this.isRefreshing = true;
198
+ try {
199
+ do {
200
+ this.pendingRefresh = false;
201
+ if (this.status !== MCPServerStatus.CONNECTED || !this.client)
202
+ break;
203
+ const timeoutMs = this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC;
204
+ const abortController = new AbortController();
205
+ const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
206
+ let newTools;
207
+ try {
208
+ newTools = await this.discoverTools(this.cliConfig, {
209
+ signal: abortController.signal,
210
+ });
211
+ }
212
+ catch (err) {
213
+ debugLogger.error(`Discovery failed during refresh: ${getErrorMessage(err)}`);
214
+ clearTimeout(timeoutId);
215
+ break;
216
+ }
217
+ this.toolRegistry.removeMcpToolsByServer(this.serverName);
218
+ for (const tool of newTools) {
219
+ this.toolRegistry.registerTool(tool);
220
+ }
221
+ this.toolRegistry.sortTools();
222
+ if (this.onToolsUpdated) {
223
+ await this.onToolsUpdated(abortController.signal);
224
+ }
225
+ clearTimeout(timeoutId);
226
+ coreEvents.emitFeedback('info', `Tools updated for server: ${this.serverName}`);
227
+ } while (this.pendingRefresh);
228
+ }
229
+ catch (error) {
230
+ debugLogger.error(`Critical error in refresh loop for ${this.serverName}: ${getErrorMessage(error)}`);
231
+ }
232
+ finally {
233
+ this.isRefreshing = false;
234
+ this.pendingRefresh = false;
235
+ }
236
+ }
167
237
  }
168
238
  /**
169
239
  * Map to track the status of each MCP server within the core package
@@ -322,21 +392,6 @@ function createAuthProvider(mcpServerConfig) {
322
392
  }
323
393
  return undefined;
324
394
  }
325
- /**
326
- * Create a transport for URL based servers (remote servers).
327
- *
328
- * @param mcpServerConfig The MCP server configuration
329
- * @param transportOptions The transport options
330
- */
331
- function createUrlTransport(mcpServerConfig, transportOptions) {
332
- if (mcpServerConfig.httpUrl) {
333
- return new StreamableHTTPClientTransport(new URL(mcpServerConfig.httpUrl), transportOptions);
334
- }
335
- if (mcpServerConfig.url) {
336
- return new SSEClientTransport(new URL(mcpServerConfig.url), transportOptions);
337
- }
338
- throw new Error('No URL configured for MCP Server');
339
- }
340
395
  /**
341
396
  * Create a transport with OAuth token for the given server configuration.
342
397
  *
@@ -353,7 +408,7 @@ async function createTransportWithOAuth(mcpServerName, mcpServerConfig, accessTo
353
408
  const transportOptions = {
354
409
  requestInit: createTransportRequestInit(mcpServerConfig, headers),
355
410
  };
356
- return createUrlTransport(mcpServerConfig, transportOptions);
411
+ return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions);
357
412
  }
358
413
  catch (error) {
359
414
  coreEvents.emitFeedback('error', `Failed to create OAuth transport for server '${mcpServerName}': ${getErrorMessage(error)}`, error);
@@ -445,7 +500,7 @@ export async function connectAndDiscover(mcpServerName, mcpServerConfig, toolReg
445
500
  };
446
501
  // Attempt to discover both prompts and tools
447
502
  const prompts = await discoverPrompts(mcpServerName, mcpClient, promptRegistry);
448
- const tools = await discoverTools(mcpServerName, mcpServerConfig, mcpClient, cliConfig, toolRegistry.getMessageBus());
503
+ const tools = await discoverTools(mcpServerName, mcpServerConfig, mcpClient, cliConfig, toolRegistry.getMessageBus(), { timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC });
449
504
  // If we have neither prompts nor tools, it's a failed discovery
450
505
  if (prompts.length === 0 && tools.length === 0) {
451
506
  throw new Error('No prompts or tools found on the server.');
@@ -479,12 +534,12 @@ export async function connectAndDiscover(mcpServerName, mcpServerConfig, toolReg
479
534
  * @returns A promise that resolves to an array of discovered and enabled tools.
480
535
  * @throws An error if no enabled tools are found or if the server provides invalid function declarations.
481
536
  */
482
- export async function discoverTools(mcpServerName, mcpServerConfig, mcpClient, cliConfig, messageBus) {
537
+ export async function discoverTools(mcpServerName, mcpServerConfig, mcpClient, cliConfig, messageBus, options) {
483
538
  try {
484
539
  // Only request tools if the server supports them.
485
540
  if (mcpClient.getServerCapabilities()?.tools == null)
486
541
  return [];
487
- const response = await mcpClient.listTools({});
542
+ const response = await mcpClient.listTools({}, options);
488
543
  const discoveredTools = [];
489
544
  for (const toolDef of response.tools) {
490
545
  try {
@@ -639,6 +694,116 @@ export async function invokeMcpPrompt(mcpServerName, mcpClient, promptName, prom
639
694
  export function hasNetworkTransport(config) {
640
695
  return !!(config.url || config.httpUrl);
641
696
  }
697
+ /**
698
+ * Helper function to retrieve a stored OAuth token for an MCP server.
699
+ * Handles token validation and refresh automatically.
700
+ *
701
+ * @param serverName The name of the MCP server
702
+ * @returns The valid access token, or null if no token is stored
703
+ */
704
+ async function getStoredOAuthToken(serverName) {
705
+ const tokenStorage = new MCPOAuthTokenStorage();
706
+ const credentials = await tokenStorage.getCredentials(serverName);
707
+ if (!credentials)
708
+ return null;
709
+ const authProvider = new MCPOAuthProvider(tokenStorage);
710
+ return authProvider.getValidToken(serverName, {
711
+ // Pass client ID if available
712
+ clientId: credentials.clientId,
713
+ });
714
+ }
715
+ /**
716
+ * Helper function to create an SSE transport with optional OAuth authentication.
717
+ *
718
+ * @param config The MCP server configuration
719
+ * @param accessToken Optional OAuth access token for authentication
720
+ * @returns A configured SSE transport ready for connection
721
+ */
722
+ function createSSETransportWithAuth(config, accessToken) {
723
+ const headers = {
724
+ ...config.headers,
725
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
726
+ };
727
+ const options = {};
728
+ if (Object.keys(headers).length > 0) {
729
+ options.requestInit = { headers };
730
+ }
731
+ return new SSEClientTransport(new URL(config.url), options);
732
+ }
733
+ /**
734
+ * Helper function to connect a client using SSE transport with optional OAuth.
735
+ *
736
+ * @param client The MCP client to connect
737
+ * @param config The MCP server configuration
738
+ * @param accessToken Optional OAuth access token for authentication
739
+ */
740
+ async function connectWithSSETransport(client, config, accessToken) {
741
+ const transport = createSSETransportWithAuth(config, accessToken);
742
+ await client.connect(transport, {
743
+ timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
744
+ });
745
+ }
746
+ /**
747
+ * Helper function to show authentication required message and throw error.
748
+ * Checks if there's a stored token that was rejected (requires re-auth).
749
+ *
750
+ * @param serverName The name of the MCP server
751
+ * @throws Always throws an error with authentication instructions
752
+ */
753
+ async function showAuthRequiredMessage(serverName) {
754
+ const hasRejectedToken = !!(await getStoredOAuthToken(serverName));
755
+ const message = hasRejectedToken
756
+ ? `MCP server '${serverName}' rejected stored OAuth token. Please re-authenticate using: /mcp auth ${serverName}`
757
+ : `MCP server '${serverName}' requires authentication using: /mcp auth ${serverName}`;
758
+ coreEvents.emitFeedback('info', message);
759
+ throw new UnauthorizedError(message);
760
+ }
761
+ /**
762
+ * Helper function to retry connection with OAuth token after authentication.
763
+ * Handles both HTTP and SSE transports based on what previously failed.
764
+ *
765
+ * @param client The MCP client to connect
766
+ * @param serverName The name of the MCP server
767
+ * @param config The MCP server configuration
768
+ * @param accessToken The OAuth access token to use
769
+ * @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
770
+ */
771
+ async function retryWithOAuth(client, serverName, config, accessToken, httpReturned404) {
772
+ if (httpReturned404) {
773
+ // HTTP returned 404, only try SSE
774
+ debugLogger.log(`Retrying SSE connection to '${serverName}' with OAuth token...`);
775
+ await connectWithSSETransport(client, config, accessToken);
776
+ debugLogger.log(`Successfully connected to '${serverName}' using SSE with OAuth.`);
777
+ return;
778
+ }
779
+ // HTTP returned 401, try HTTP with OAuth first
780
+ debugLogger.log(`Retrying connection to '${serverName}' with OAuth token...`);
781
+ const httpTransport = await createTransportWithOAuth(serverName, config, accessToken);
782
+ if (!httpTransport) {
783
+ throw new Error(`Failed to create OAuth transport for server '${serverName}'`);
784
+ }
785
+ try {
786
+ await client.connect(httpTransport, {
787
+ timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
788
+ });
789
+ debugLogger.log(`Successfully connected to '${serverName}' using HTTP with OAuth.`);
790
+ }
791
+ catch (httpError) {
792
+ await httpTransport.close();
793
+ // If HTTP+OAuth returns 404 and auto-detection enabled, try SSE+OAuth
794
+ if (String(httpError).includes('404') &&
795
+ config.url &&
796
+ !config.type &&
797
+ !config.httpUrl) {
798
+ debugLogger.log(`HTTP with OAuth returned 404, trying SSE with OAuth...`);
799
+ await connectWithSSETransport(client, config, accessToken);
800
+ debugLogger.log(`Successfully connected to '${serverName}' using SSE with OAuth.`);
801
+ }
802
+ else {
803
+ throw httpError;
804
+ }
805
+ }
806
+ }
642
807
  /**
643
808
  * Creates and connects an MCP client to a server based on the provided configuration.
644
809
  * It determines the appropriate transport (Stdio, SSE, or Streamable HTTP) and
@@ -698,6 +863,9 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
698
863
  unlistenDirectories?.();
699
864
  unlistenDirectories = undefined;
700
865
  };
866
+ let firstAttemptError = null;
867
+ let httpReturned404 = false; // Track if HTTP returned 404 to skip it in OAuth retry
868
+ let sseError = null; // Track SSE fallback error
701
869
  try {
702
870
  const transport = await createTransport(mcpServerName, mcpServerConfig, debugMode);
703
871
  try {
@@ -708,52 +876,92 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
708
876
  }
709
877
  catch (error) {
710
878
  await transport.close();
879
+ firstAttemptError = error;
711
880
  throw error;
712
881
  }
713
882
  }
714
- catch (error) {
883
+ catch (initialError) {
884
+ let error = initialError;
885
+ // Check if this is a 401 error FIRST (before attempting SSE fallback)
886
+ // This ensures OAuth flow happens before we try SSE
887
+ if (isAuthenticationError(error) && hasNetworkTransport(mcpServerConfig)) {
888
+ // Continue to OAuth handling below (after SSE fallback section)
889
+ }
890
+ else if (
891
+ // If not 401, and HTTP failed with url without explicit type, try SSE fallback
892
+ firstAttemptError &&
893
+ mcpServerConfig.url &&
894
+ !mcpServerConfig.type &&
895
+ !mcpServerConfig.httpUrl) {
896
+ // Check if HTTP returned 404 - if so, we know it's not an HTTP server
897
+ httpReturned404 = String(firstAttemptError).includes('404');
898
+ const logMessage = httpReturned404
899
+ ? `HTTP returned 404, trying SSE transport...`
900
+ : `HTTP connection failed, attempting SSE fallback...`;
901
+ debugLogger.log(`MCP server '${mcpServerName}': ${logMessage}`);
902
+ try {
903
+ // Try SSE with stored OAuth token if available
904
+ // This ensures that SSE fallback works for authenticated servers
905
+ await connectWithSSETransport(mcpClient, mcpServerConfig, await getStoredOAuthToken(mcpServerName));
906
+ debugLogger.log(`MCP server '${mcpServerName}': Successfully connected using SSE transport.`);
907
+ return mcpClient;
908
+ }
909
+ catch (sseFallbackError) {
910
+ sseError = sseFallbackError;
911
+ // If SSE also returned 401, handle OAuth below
912
+ if (isAuthenticationError(sseError)) {
913
+ debugLogger.log(`MCP server '${mcpServerName}': SSE returned 401, OAuth authentication required.`);
914
+ // Update error to be the SSE error for OAuth handling
915
+ error = sseError;
916
+ // Continue to OAuth handling below
917
+ }
918
+ else {
919
+ debugLogger.log(`MCP server '${mcpServerName}': SSE fallback also failed.`);
920
+ // Both failed without 401, throw the original error
921
+ throw firstAttemptError;
922
+ }
923
+ }
924
+ }
715
925
  // Check if this is a 401 error that might indicate OAuth is required
716
- const errorString = String(error);
717
- if (errorString.includes('401') && hasNetworkTransport(mcpServerConfig)) {
926
+ if (isAuthenticationError(error) && hasNetworkTransport(mcpServerConfig)) {
718
927
  mcpServerRequiresOAuth.set(mcpServerName, true);
719
- // Only trigger automatic OAuth discovery for HTTP servers or when OAuth is explicitly configured
720
- // For SSE servers, we should not trigger new OAuth flows automatically
721
- const shouldTriggerOAuth = mcpServerConfig.httpUrl || mcpServerConfig.oauth?.enabled;
928
+ // Only trigger automatic OAuth if explicitly enabled in config
929
+ // Otherwise, show error and tell user to run /mcp auth command
930
+ const shouldTriggerOAuth = mcpServerConfig.oauth?.enabled;
722
931
  if (!shouldTriggerOAuth) {
723
- // For SSE servers without explicit OAuth config, if a token was found but rejected, report it accurately.
724
- const tokenStorage = new MCPOAuthTokenStorage();
725
- const credentials = await tokenStorage.getCredentials(mcpServerName);
726
- if (credentials) {
727
- const authProvider = new MCPOAuthProvider(tokenStorage);
728
- const hasStoredTokens = await authProvider.getValidToken(mcpServerName, {
729
- // Pass client ID if available
730
- clientId: credentials.clientId,
731
- });
732
- if (hasStoredTokens) {
733
- coreEvents.emitFeedback('error', `Stored OAuth token for SSE server '${mcpServerName}' was rejected. ` +
734
- `Please re-authenticate using: /mcp auth ${mcpServerName}`);
735
- }
736
- else {
737
- coreEvents.emitFeedback('error', `401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
738
- `Please authenticate using: /mcp auth ${mcpServerName}`);
739
- }
740
- }
741
- throw new Error(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
742
- `Please authenticate using: /mcp auth ${mcpServerName}`);
932
+ await showAuthRequiredMessage(mcpServerName);
743
933
  }
744
934
  // Try to extract www-authenticate header from the error
935
+ const errorString = String(error);
745
936
  let wwwAuthenticate = extractWWWAuthenticateHeader(errorString);
746
937
  // If we didn't get the header from the error string, try to get it from the server
747
938
  if (!wwwAuthenticate && hasNetworkTransport(mcpServerConfig)) {
748
939
  debugLogger.log(`No www-authenticate header in error, trying to fetch it from server...`);
749
940
  try {
750
941
  const urlToFetch = mcpServerConfig.httpUrl || mcpServerConfig.url;
942
+ // Determine correct Accept header based on what transport failed
943
+ let acceptHeader;
944
+ if (mcpServerConfig.httpUrl) {
945
+ acceptHeader = 'application/json';
946
+ }
947
+ else if (mcpServerConfig.type === 'http') {
948
+ acceptHeader = 'application/json';
949
+ }
950
+ else if (mcpServerConfig.type === 'sse') {
951
+ acceptHeader = 'text/event-stream';
952
+ }
953
+ else if (httpReturned404) {
954
+ // HTTP failed with 404, SSE returned 401 - use SSE header
955
+ acceptHeader = 'text/event-stream';
956
+ }
957
+ else {
958
+ // HTTP returned 401 - use HTTP header
959
+ acceptHeader = 'application/json';
960
+ }
751
961
  const response = await fetch(urlToFetch, {
752
962
  method: 'HEAD',
753
963
  headers: {
754
- Accept: mcpServerConfig.httpUrl
755
- ? 'application/json'
756
- : 'text/event-stream',
964
+ Accept: acceptHeader,
757
965
  },
758
966
  signal: AbortSignal.timeout(5000),
759
967
  });
@@ -774,38 +982,12 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
774
982
  const oauthSuccess = await handleAutomaticOAuth(mcpServerName, mcpServerConfig, wwwAuthenticate);
775
983
  if (oauthSuccess) {
776
984
  // Retry connection with OAuth token
777
- debugLogger.log(`Retrying connection to '${mcpServerName}' with OAuth token...`);
778
- // Get the valid token - we need to create a proper OAuth config
779
- // The token should already be available from the authentication process
780
- const tokenStorage = new MCPOAuthTokenStorage();
781
- const credentials = await tokenStorage.getCredentials(mcpServerName);
782
- if (credentials) {
783
- const authProvider = new MCPOAuthProvider(tokenStorage);
784
- const accessToken = await authProvider.getValidToken(mcpServerName, {
785
- // Pass client ID if available
786
- clientId: credentials.clientId,
787
- });
788
- if (accessToken) {
789
- // Create transport with OAuth token
790
- const oauthTransport = await createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken);
791
- if (oauthTransport) {
792
- await mcpClient.connect(oauthTransport, {
793
- timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
794
- });
795
- // Connection successful with OAuth
796
- return mcpClient;
797
- }
798
- else {
799
- throw new Error(`Failed to create OAuth transport for server '${mcpServerName}'`);
800
- }
801
- }
802
- else {
803
- throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
804
- }
805
- }
806
- else {
807
- throw new Error(`Failed to get credentials for server '${mcpServerName}' after successful OAuth authentication`);
985
+ const accessToken = await getStoredOAuthToken(mcpServerName);
986
+ if (!accessToken) {
987
+ throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
808
988
  }
989
+ await retryWithOAuth(mcpClient, mcpServerName, mcpServerConfig, accessToken, httpReturned404);
990
+ return mcpClient;
809
991
  }
810
992
  else {
811
993
  throw new Error(`Failed to handle automatic OAuth for server '${mcpServerName}'`);
@@ -813,29 +995,10 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
813
995
  }
814
996
  else {
815
997
  // No www-authenticate header found, but we got a 401
816
- // Only try OAuth discovery for HTTP servers or when OAuth is explicitly configured
817
- // For SSE servers, we should not trigger new OAuth flows automatically
818
- const shouldTryDiscovery = mcpServerConfig.httpUrl || mcpServerConfig.oauth?.enabled;
998
+ // Only try OAuth discovery when OAuth is explicitly enabled in config
999
+ const shouldTryDiscovery = mcpServerConfig.oauth?.enabled;
819
1000
  if (!shouldTryDiscovery) {
820
- const tokenStorage = new MCPOAuthTokenStorage();
821
- const credentials = await tokenStorage.getCredentials(mcpServerName);
822
- if (credentials) {
823
- const authProvider = new MCPOAuthProvider(tokenStorage);
824
- const hasStoredTokens = await authProvider.getValidToken(mcpServerName, {
825
- // Pass client ID if available
826
- clientId: credentials.clientId,
827
- });
828
- if (hasStoredTokens) {
829
- coreEvents.emitFeedback('error', `Stored OAuth token for SSE server '${mcpServerName}' was rejected. ` +
830
- `Please re-authenticate using: /mcp auth ${mcpServerName}`);
831
- }
832
- else {
833
- coreEvents.emitFeedback('error', `401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
834
- `Please authenticate using: /mcp auth ${mcpServerName}`);
835
- }
836
- }
837
- throw new Error(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
838
- `Please authenticate using: /mcp auth ${mcpServerName}`);
1001
+ await showAuthRequiredMessage(mcpServerName);
839
1002
  }
840
1003
  // For SSE/HTTP servers, try to discover OAuth configuration from the base URL
841
1004
  debugLogger.log(`🔍 Attempting OAuth discovery for '${mcpServerName}'...`);
@@ -860,35 +1023,20 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
860
1023
  const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage());
861
1024
  await authProvider.authenticate(mcpServerName, oauthAuthConfig, authServerUrl);
862
1025
  // Retry connection with OAuth token
863
- const tokenStorage = new MCPOAuthTokenStorage();
864
- const credentials = await tokenStorage.getCredentials(mcpServerName);
865
- if (credentials) {
866
- const authProvider = new MCPOAuthProvider(tokenStorage);
867
- const accessToken = await authProvider.getValidToken(mcpServerName, {
868
- // Pass client ID if available
869
- clientId: credentials.clientId,
870
- });
871
- if (accessToken) {
872
- // Create transport with OAuth token
873
- const oauthTransport = await createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken);
874
- if (oauthTransport) {
875
- await mcpClient.connect(oauthTransport, {
876
- timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
877
- });
878
- // Connection successful with OAuth
879
- return mcpClient;
880
- }
881
- else {
882
- throw new Error(`Failed to create OAuth transport for server '${mcpServerName}'`);
883
- }
884
- }
885
- else {
886
- throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
887
- }
1026
+ const accessToken = await getStoredOAuthToken(mcpServerName);
1027
+ if (!accessToken) {
1028
+ throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
888
1029
  }
889
- else {
890
- throw new Error(`Failed to get stored credentials for server '${mcpServerName}'`);
1030
+ // Create transport with OAuth token
1031
+ const oauthTransport = await createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken);
1032
+ if (!oauthTransport) {
1033
+ throw new Error(`Failed to create OAuth transport for server '${mcpServerName}'`);
891
1034
  }
1035
+ await mcpClient.connect(oauthTransport, {
1036
+ timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
1037
+ });
1038
+ // Connection successful with OAuth
1039
+ return mcpClient;
892
1040
  }
893
1041
  else {
894
1042
  throw new Error(`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`);
@@ -901,23 +1049,38 @@ export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMo
901
1049
  }
902
1050
  else {
903
1051
  // Handle other connection errors
904
- // Create a concise error message
905
- const errorMessage = error.message || String(error);
906
- const isNetworkError = errorMessage.includes('ENOTFOUND') ||
907
- errorMessage.includes('ECONNREFUSED');
908
- let conciseError;
909
- if (isNetworkError) {
910
- conciseError = `Cannot connect to '${mcpServerName}' - server may be down or URL incorrect`;
911
- }
912
- else {
913
- conciseError = `Connection failed for '${mcpServerName}': ${errorMessage}`;
914
- }
915
- if (process.env['SANDBOX']) {
916
- conciseError += ` (check sandbox availability)`;
917
- }
918
- throw new Error(conciseError);
1052
+ // Re-throw the original error to preserve its structure
1053
+ throw error;
1054
+ }
1055
+ }
1056
+ }
1057
+ /**
1058
+ * Helper function to create the appropriate transport based on config
1059
+ * This handles the logic for httpUrl/url/type consistently
1060
+ */
1061
+ function createUrlTransport(mcpServerName, mcpServerConfig, transportOptions) {
1062
+ // Priority 1: httpUrl (deprecated)
1063
+ if (mcpServerConfig.httpUrl) {
1064
+ if (mcpServerConfig.url) {
1065
+ debugLogger.warn(`MCP server '${mcpServerName}': Both 'httpUrl' and 'url' are configured. ` +
1066
+ `Using deprecated 'httpUrl'. Please migrate to 'url' with 'type: "http"'.`);
1067
+ }
1068
+ return new StreamableHTTPClientTransport(new URL(mcpServerConfig.httpUrl), transportOptions);
1069
+ }
1070
+ // Priority 2 & 3: url with explicit type
1071
+ if (mcpServerConfig.url && mcpServerConfig.type) {
1072
+ if (mcpServerConfig.type === 'http') {
1073
+ return new StreamableHTTPClientTransport(new URL(mcpServerConfig.url), transportOptions);
1074
+ }
1075
+ else if (mcpServerConfig.type === 'sse') {
1076
+ return new SSEClientTransport(new URL(mcpServerConfig.url), transportOptions);
919
1077
  }
920
1078
  }
1079
+ // Priority 4: url without type (default to HTTP)
1080
+ if (mcpServerConfig.url) {
1081
+ return new StreamableHTTPClientTransport(new URL(mcpServerConfig.url), transportOptions);
1082
+ }
1083
+ throw new Error(`No URL configured for MCP server '${mcpServerName}'`);
921
1084
  }
922
1085
  /** Visible for Testing */
923
1086
  export async function createTransport(mcpServerName, mcpServerConfig, debugMode) {
@@ -937,33 +1100,23 @@ export async function createTransport(mcpServerName, mcpServerConfig, debugMode)
937
1100
  if (authProvider === undefined) {
938
1101
  // Check if we have OAuth configuration or stored tokens
939
1102
  let accessToken = null;
940
- let hasOAuthConfig = mcpServerConfig.oauth?.enabled;
941
- if (hasOAuthConfig && mcpServerConfig.oauth) {
1103
+ if (mcpServerConfig.oauth?.enabled && mcpServerConfig.oauth) {
942
1104
  const tokenStorage = new MCPOAuthTokenStorage();
943
1105
  const mcpAuthProvider = new MCPOAuthProvider(tokenStorage);
944
1106
  accessToken = await mcpAuthProvider.getValidToken(mcpServerName, mcpServerConfig.oauth);
945
1107
  if (!accessToken) {
946
- throw new Error(`MCP server '${mcpServerName}' requires OAuth authentication. ` +
947
- `Please authenticate using the /mcp auth command.`);
1108
+ // Emit info message (not error) since this is expected behavior
1109
+ coreEvents.emitFeedback('info', `MCP server '${mcpServerName}' requires authentication using: /mcp auth ${mcpServerName}`);
948
1110
  }
949
1111
  }
950
1112
  else {
951
1113
  // Check if we have stored OAuth tokens for this server (from previous authentication)
952
- const tokenStorage = new MCPOAuthTokenStorage();
953
- const credentials = await tokenStorage.getCredentials(mcpServerName);
954
- if (credentials) {
955
- const mcpAuthProvider = new MCPOAuthProvider(tokenStorage);
956
- accessToken = await mcpAuthProvider.getValidToken(mcpServerName, {
957
- // Pass client ID if available
958
- clientId: credentials.clientId,
959
- });
960
- if (accessToken) {
961
- hasOAuthConfig = true;
962
- debugLogger.log(`Found stored OAuth token for server '${mcpServerName}'`);
963
- }
1114
+ accessToken = await getStoredOAuthToken(mcpServerName);
1115
+ if (accessToken) {
1116
+ debugLogger.log(`Found stored OAuth token for server '${mcpServerName}'`);
964
1117
  }
965
1118
  }
966
- if (hasOAuthConfig && accessToken) {
1119
+ if (accessToken) {
967
1120
  headers['Authorization'] = `Bearer ${accessToken}`;
968
1121
  }
969
1122
  }
@@ -971,7 +1124,7 @@ export async function createTransport(mcpServerName, mcpServerConfig, debugMode)
971
1124
  requestInit: createTransportRequestInit(mcpServerConfig, headers),
972
1125
  authProvider,
973
1126
  };
974
- return createUrlTransport(mcpServerConfig, transportOptions);
1127
+ return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions);
975
1128
  }
976
1129
  if (mcpServerConfig.command) {
977
1130
  const transport = new StdioClientTransport({