diff options
| author | Bryan Morgan <[email protected]> | 2025-06-07 15:06:18 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-07 15:06:18 -0400 |
| commit | 28ff62e7b1b2191c5f5193314523f848a0f3dea5 (patch) | |
| tree | 6285889f556c2840031fde0180c4cd5a2817d135 /packages/core/src | |
| parent | 6ea4479064a4275390ef96216f5d68bd27f65ae7 (diff) | |
Added /mcp command support and cleaned up broken tests (#817)
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/index.ts | 2 | ||||
| -rw-r--r-- | packages/core/src/tools/edit.ts | 2 | ||||
| -rw-r--r-- | packages/core/src/tools/glob.test.ts | 10 | ||||
| -rw-r--r-- | packages/core/src/tools/mcp-client.ts | 94 | ||||
| -rw-r--r-- | packages/core/src/utils/memoryDiscovery.test.ts | 4 | ||||
| -rw-r--r-- | packages/core/src/utils/memoryDiscovery.ts | 45 |
6 files changed, 140 insertions, 17 deletions
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a285df14..1f58d97c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,8 @@ export * from './tools/memoryTool.js'; export * from './tools/shell.js'; export * from './tools/web-search.js'; export * from './tools/read-many-files.js'; +export * from './tools/mcp-client.js'; +export * from './tools/mcp-tool.js'; // Export telemetry functions export * from './telemetry/index.js'; diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index bdaa805c..eb22d0ac 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -91,7 +91,7 @@ Expectation for required parameters: 2. \`old_string\` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.). 3. \`new_string\` MUST be the exact literal text to replace \`old_string\` with (also including all whitespace, indentation, newlines, and surrounding code etc.). Ensure the resulting code is correct and idiomatic. 4. NEVER escape \`old_string\` or \`new_string\`, that would break the exact literal text requirement. -**Important:** If ANY of the above are not satisfied, the tool will fail. CRITICAL for \`old_string\`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail., +**Important:** If ANY of the above are not satisfied, the tool will fail. CRITICAL for \`old_string\`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail. **Multiple replacements:** Set \`expected_replacements\` to the number of occurrences you want to replace. The tool will replace ALL occurrences that match \`old_string\` exactly. Ensure the number of replacements matches your expectation.`, { properties: { diff --git a/packages/core/src/tools/glob.test.ts b/packages/core/src/tools/glob.test.ts index b630a0d8..f975fc08 100644 --- a/packages/core/src/tools/glob.test.ts +++ b/packages/core/src/tools/glob.test.ts @@ -185,7 +185,9 @@ describe('GlobTool', () => { }); it('should return error if pattern is missing (schema validation)', () => { - const params = { path: '.' } as unknown as GlobToolParams; + // Need to correctly define this as an object without pattern + const params = { path: '.' }; + // @ts-expect-error - We're intentionally creating invalid params for testing expect(globTool.validateToolParams(params)).toContain( 'Parameters failed schema validation', ); @@ -209,7 +211,8 @@ describe('GlobTool', () => { const params = { pattern: '*.ts', path: 123, - } as unknown as GlobToolParams; + }; + // @ts-expect-error - We're intentionally creating invalid params for testing expect(globTool.validateToolParams(params)).toContain( 'Parameters failed schema validation', ); @@ -219,7 +222,8 @@ describe('GlobTool', () => { const params = { pattern: '*.ts', case_sensitive: 'true', - } as unknown as GlobToolParams; + }; + // @ts-expect-error - We're intentionally creating invalid params for testing expect(globTool.validateToolParams(params)).toContain( 'Parameters failed schema validation', ); diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 9025ac7b..9a02df0c 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -13,6 +13,83 @@ import { DiscoveredMCPTool } from './mcp-tool.js'; import { CallableTool, FunctionDeclaration, mcpToTool } from '@google/genai'; import { ToolRegistry } from './tool-registry.js'; +/** + * Enum representing the connection status of an MCP server + */ +export enum MCPServerStatus { + /** Server is disconnected or experiencing errors */ + DISCONNECTED = 'disconnected', + /** Server is in the process of connecting */ + CONNECTING = 'connecting', + /** Server is connected and ready to use */ + CONNECTED = 'connected', +} + +/** + * Map to track the status of each MCP server within the core package + */ +const mcpServerStatusesInternal: Map<string, MCPServerStatus> = new Map(); + +/** + * Event listeners for MCP server status changes + */ +type StatusChangeListener = ( + serverName: string, + status: MCPServerStatus, +) => void; +const statusChangeListeners: StatusChangeListener[] = []; + +/** + * Add a listener for MCP server status changes + */ +export function addMCPStatusChangeListener( + listener: StatusChangeListener, +): void { + statusChangeListeners.push(listener); +} + +/** + * Remove a listener for MCP server status changes + */ +export function removeMCPStatusChangeListener( + listener: StatusChangeListener, +): void { + const index = statusChangeListeners.indexOf(listener); + if (index !== -1) { + statusChangeListeners.splice(index, 1); + } +} + +/** + * Update the status of an MCP server + */ +function updateMCPServerStatus( + serverName: string, + status: MCPServerStatus, +): void { + mcpServerStatusesInternal.set(serverName, status); + // Notify all listeners + for (const listener of statusChangeListeners) { + listener(serverName, status); + } +} + +/** + * Get the current status of an MCP server + */ +export function getMCPServerStatus(serverName: string): MCPServerStatus { + return ( + mcpServerStatusesInternal.get(serverName) || MCPServerStatus.DISCONNECTED + ); +} + +/** + * Get all MCP server statuses + */ +export function getAllMCPServerStatuses(): Map<string, MCPServerStatus> { + return new Map(mcpServerStatusesInternal); +} + export async function discoverMcpTools( mcpServers: Record<string, MCPServerConfig>, mcpServerCommand: string | undefined, @@ -43,6 +120,9 @@ async function connectAndDiscover( mcpServerConfig: MCPServerConfig, toolRegistry: ToolRegistry, ): Promise<void> { + // Initialize the server status as connecting + updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING); + let transport; if (mcpServerConfig.url) { transport = new SSEClientTransport(new URL(mcpServerConfig.url)); @@ -61,6 +141,8 @@ async function connectAndDiscover( console.error( `MCP server '${mcpServerName}' has invalid configuration: missing both url (for SSE) and command (for stdio). Skipping.`, ); + // Update status to disconnected + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); return; } @@ -72,16 +154,22 @@ async function connectAndDiscover( try { await mcpClient.connect(transport); + // Connection successful + updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED); } catch (error) { console.error( `failed to start or connect to MCP server '${mcpServerName}' ` + `${JSON.stringify(mcpServerConfig)}; \n${error}`, ); + // Update status to disconnected + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); return; } mcpClient.onerror = (error) => { console.error(`MCP ERROR (${mcpServerName}):`, error.toString()); + // Update status to disconnected on error + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); }; if (transport instanceof StdioClientTransport && transport.stderr) { @@ -110,6 +198,8 @@ async function connectAndDiscover( } else if (transport instanceof SSEClientTransport) { await transport.close(); } + // Update status to disconnected + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); return; } @@ -168,6 +258,8 @@ async function connectAndDiscover( ) { await transport.close(); } + // Update status to disconnected + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); } // If no tools were registered from this MCP server, the following 'if' block @@ -184,6 +276,8 @@ async function connectAndDiscover( transport instanceof SSEClientTransport ) { await transport.close(); + // Update status to disconnected + updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); } } } diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index db0ffd1d..a9d34bf3 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -45,6 +45,10 @@ describe('loadServerHierarchicalMemory', () => { beforeEach(() => { vi.resetAllMocks(); + // Set environment variables to indicate test environment + process.env.NODE_ENV = 'test'; + process.env.VITEST = 'true'; + setGeminiMdFilename(DEFAULT_CONTEXT_FILENAME); // Use defined const mockOs.homedir.mockReturnValue(USER_HOME); diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index 2e6ce9fc..221bf2c6 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -56,17 +56,29 @@ async function findProjectRoot(startDir: string): Promise<string | null> { return currentDir; } } catch (error: unknown) { - if (typeof error === 'object' && error !== null && 'code' in error) { - const fsError = error as { code: string; message: string }; - if (fsError.code !== 'ENOENT') { + // Don't log ENOENT errors as they're expected when .git doesn't exist + // Also don't log errors in test environments, which often have mocked fs + const isENOENT = + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code: string }).code === 'ENOENT'; + + // Only log unexpected errors in non-test environments + // process.env.NODE_ENV === 'test' or VITEST are common test indicators + const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST; + + if (!isENOENT && !isTestEnv) { + if (typeof error === 'object' && error !== null && 'code' in error) { + const fsError = error as { code: string; message: string }; logger.warn( `Error checking for .git directory at ${gitPath}: ${fsError.message}`, ); + } else { + logger.warn( + `Non-standard error checking for .git directory at ${gitPath}: ${String(error)}`, + ); } - } else { - logger.warn( - `Non-standard error checking for .git directory at ${gitPath}: ${String(error)}`, - ); } } const parentDir = path.dirname(currentDir); @@ -136,8 +148,12 @@ async function collectDownwardGeminiFiles( } } } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.warn(`Error scanning directory ${directory}: ${message}`); + // Only log warnings in non-test environments + const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST; + if (!isTestEnv) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`Error scanning directory ${directory}: ${message}`); + } if (debugMode) logger.debug(`Failed to scan directory: ${directory}`); } return collectedPaths; @@ -283,10 +299,13 @@ async function readGeminiMdFiles( `Successfully read: ${filePath} (Length: ${content.length})`, ); } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - logger.warn( - `Warning: Could not read ${getCurrentGeminiMdFilename()} file at ${filePath}. Error: ${message}`, - ); + const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST; + if (!isTestEnv) { + const message = error instanceof Error ? error.message : String(error); + logger.warn( + `Warning: Could not read ${getCurrentGeminiMdFilename()} file at ${filePath}. Error: ${message}`, + ); + } results.push({ filePath, content: null }); // Still include it with null content if (debugMode) logger.debug(`Failed to read: ${filePath}`); } |
