diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b595147735..e68a5df0ca 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -126,8 +126,6 @@ jobs: if: "runner.os == 'macOS'" run: | npm cache clean --force - npm install --no-save @rollup/rollup-darwin-arm64 || true - - name: 'Run E2E tests (non-Windows)' if: "runner.os != 'Windows'" env: @@ -135,7 +133,7 @@ jobs: KEEP_OUTPUT: 'true' SANDBOX: 'sandbox:none' VERBOSE: 'true' - run: 'npx vitest run --root ./integration-tests' + run: 'npm run test:integration:sandbox:none' e2e_windows: name: 'Slow E2E - Win' @@ -204,7 +202,7 @@ jobs: UV_THREADPOOL_SIZE: '32' NODE_ENV: 'test' shell: 'pwsh' - run: 'npx vitest run --root ./integration-tests' + run: 'npm run test:integration:sandbox:none' e2e: name: 'E2E' diff --git a/integration-tests/mcp_server_cyclic_schema.test.ts b/integration-tests/mcp_server_cyclic_schema.test.ts index e85d2877d4..c1ed12ce3e 100644 --- a/integration-tests/mcp_server_cyclic_schema.test.ts +++ b/integration-tests/mcp_server_cyclic_schema.test.ts @@ -5,14 +5,26 @@ */ /** - * This test verifies we can match maximum schema depth errors from Gemini - * and then detect and warn about the potential tools that caused the error. + * This test verifies we can provide MCP tools with recursive input schemas + * (in JSON, using the $ref keyword) and both the GenAI SDK and the Gemini + * API calls succeed. Note that prior to + * https://github.com/googleapis/js-genai/commit/36f6350705ecafc47eaea3f3eecbcc69512edab7#diff-fdde9372aec859322b7c5a5efe467e0ad25a57210c7229724586ee90ea4f5a30 + * the Gemini API call would fail for such tools because the schema was + * passed not as a JSON string but using the Gemini API's tool parameter + * schema object which has stricter typing and recursion restrictions. + * If this test fails, it's likely because either the GenAI SDK or Gemini API + * has become more restrictive about the type of tool parameter schemas that + * are accepted. If this occurs: Gemini CLI previously attempted to detect + * such tools and proactively remove them from the set of tools provided in + * the Gemini API call (as FunctionDeclaration objects). It may be appropriate + * to resurrect that behavior but note that it's difficult to keep the + * GCLI filters in sync with the Gemini API restrictions and behavior. */ -import { describe, it, beforeAll, expect } from 'vitest'; -import { TestRig } from './test-helper.js'; -import { join } from 'node:path'; import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { TestRig } from './test-helper.js'; // Create a minimal MCP server that doesn't require external dependencies // This implements the MCP protocol directly using Node.js built-ins @@ -180,15 +192,14 @@ describe('mcp server with cyclic tool schema is detected', () => { } }); - it('should error and suggest disabling the cyclic tool', async () => { - // Just run any command to trigger the schema depth error. - // If this test starts failing, check `isSchemaDepthError` from - // geminiChat.ts to see if it needs to be updated. - // Or, possibly it could mean that gemini has fixed the issue. - const output = await rig.run('hello'); + it('mcp tool list should include tool with cyclic tool schema', async () => { + const tool_list_output = await rig.run('/mcp list'); + expect(tool_list_output).toContain('tool_with_cyclic_schema'); + }); - expect(output).toMatch( - /Skipping tool 'tool_with_cyclic_schema' from MCP server 'cyclic-schema-server' because it has missing types in its parameter schema/, - ); + it('gemini api call should be successful with cyclic mcp tool schema', async () => { + // Run any command and verify that we get a non-error response from + // the Gemini API. + await rig.run('hello'); }); }); diff --git a/integration-tests/telemetry.test.ts b/integration-tests/telemetry.test.ts new file mode 100644 index 0000000000..111f24c866 --- /dev/null +++ b/integration-tests/telemetry.test.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { TestRig } from './test-helper.js'; + +describe('telemetry', () => { + it('should emit a metric and a log event', async () => { + const rig = new TestRig(); + rig.setup('should emit a metric and a log event'); + + // Run a simple command that should trigger telemetry + await rig.run('just saying hi'); + + // Verify that a user_prompt event was logged + const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt'); + expect(hasUserPromptEvent).toBe(true); + + // Verify that a cli_command_count metric was emitted + const cliCommandCountMetric = rig.readMetric('session.count'); + expect(cliCommandCountMetric).not.toBeNull(); + }); +}); diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index a894be28b1..817323f648 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -112,6 +112,23 @@ export function validateModelOutput( return true; } +interface ParsedLog { + attributes?: { + 'event.name'?: string; + function_name?: string; + function_args?: string; + success?: boolean; + duration_ms?: number; + }; + scopeMetrics?: { + metrics: { + descriptor: { + name: string; + }; + }[]; + }[]; +} + export class TestRig { bundlePath: string; testDir: string | null; @@ -418,37 +435,12 @@ export class TestRig { return this.poll( () => { - const logFilePath = join(this.testDir!, 'telemetry.log'); - - if (!logFilePath || !fs.existsSync(logFilePath)) { - return false; - } - - const content = readFileSync(logFilePath, 'utf-8'); - const jsonObjects = content - .split(/}\n{/) - .map((obj, index, array) => { - // Add back the braces we removed during split - if (index > 0) obj = '{' + obj; - if (index < array.length - 1) obj = obj + '}'; - return obj.trim(); - }) - .filter((obj) => obj); - - for (const jsonStr of jsonObjects) { - try { - const logData = JSON.parse(jsonStr); - if ( - logData.attributes && - logData.attributes['event.name'] === `gemini_cli.${eventName}` - ) { - return true; - } - } catch { - // ignore - } - } - return false; + const logs = this._readAndParseTelemetryLog(); + return logs.some( + (logData) => + logData.attributes && + logData.attributes['event.name'] === `gemini_cli.${eventName}`, + ); }, timeout, 100, @@ -645,6 +637,45 @@ export class TestRig { return logs; } + private _readAndParseTelemetryLog(): ParsedLog[] { + // Telemetry is always written to the test directory + const logFilePath = join(this.testDir!, 'telemetry.log'); + + if (!logFilePath || !fs.existsSync(logFilePath)) { + return []; + } + + const content = readFileSync(logFilePath, 'utf-8'); + + // Split the content into individual JSON objects + // They are separated by "}\n{" + const jsonObjects = content + .split(/}\n{/) + .map((obj, index, array) => { + // Add back the braces we removed during split + if (index > 0) obj = '{' + obj; + if (index < array.length - 1) obj = obj + '}'; + return obj.trim(); + }) + .filter((obj) => obj); + + const logs: ParsedLog[] = []; + + for (const jsonStr of jsonObjects) { + try { + const logData = JSON.parse(jsonStr); + logs.push(logData); + } catch (e) { + // Skip objects that aren't valid JSON + if (env.VERBOSE === 'true') { + console.error('Failed to parse telemetry object:', e); + } + } + } + + return logs; + } + readToolLogs() { // For Podman, first check if telemetry file exists and has content // If not, fall back to parsing from stdout @@ -674,33 +705,7 @@ export class TestRig { } } - // Telemetry is always written to the test directory - const logFilePath = join(this.testDir!, 'telemetry.log'); - - if (!logFilePath) { - console.warn(`TELEMETRY_LOG_FILE environment variable not set`); - return []; - } - - // Check if file exists, if not return empty array (file might not be created yet) - if (!fs.existsSync(logFilePath)) { - return []; - } - - const content = readFileSync(logFilePath, 'utf-8'); - - // Split the content into individual JSON objects - // They are separated by "}\n{" - const jsonObjects = content - .split(/}\n{/) - .map((obj, index, array) => { - // Add back the braces we removed during split - if (index > 0) obj = '{' + obj; - if (index < array.length - 1) obj = obj + '}'; - return obj.trim(); - }) - .filter((obj) => obj); - + const parsedLogs = this._readAndParseTelemetryLog(); const logs: { toolRequest: { name: string; @@ -710,29 +715,21 @@ export class TestRig { }; }[] = []; - for (const jsonStr of jsonObjects) { - try { - const logData = JSON.parse(jsonStr); - // Look for tool call logs - if ( - logData.attributes && - logData.attributes['event.name'] === 'gemini_cli.tool_call' - ) { - const toolName = logData.attributes.function_name; - logs.push({ - toolRequest: { - name: toolName, - args: logData.attributes.function_args, - success: logData.attributes.success, - duration_ms: logData.attributes.duration_ms, - }, - }); - } - } catch (e) { - // Skip objects that aren't valid JSON - if (env.VERBOSE === 'true') { - console.error('Failed to parse telemetry object:', e); - } + for (const logData of parsedLogs) { + // Look for tool call logs + if ( + logData.attributes && + logData.attributes['event.name'] === 'gemini_cli.tool_call' + ) { + const toolName = logData.attributes.function_name; + logs.push({ + toolRequest: { + name: toolName, + args: logData.attributes.function_args, + success: logData.attributes.success, + duration_ms: logData.attributes.duration_ms, + }, + }); } } @@ -740,39 +737,29 @@ export class TestRig { } readLastApiRequest(): Record | null { - // Telemetry is always written to the test directory - const logFilePath = join(this.testDir!, 'telemetry.log'); + const logs = this._readAndParseTelemetryLog(); + const apiRequests = logs.filter( + (logData) => + logData.attributes && + logData.attributes['event.name'] === 'gemini_cli.api_request', + ); + return apiRequests.pop() || null; + } - if (!logFilePath || !fs.existsSync(logFilePath)) { - return null; - } - - const content = readFileSync(logFilePath, 'utf-8'); - const jsonObjects = content - .split(/}\n{/) - .map((obj, index, array) => { - if (index > 0) obj = '{' + obj; - if (index < array.length - 1) obj = obj + '}'; - return obj.trim(); - }) - .filter((obj) => obj); - - let lastApiRequest = null; - - for (const jsonStr of jsonObjects) { - try { - const logData = JSON.parse(jsonStr); - if ( - logData.attributes && - logData.attributes['event.name'] === 'gemini_cli.api_request' - ) { - lastApiRequest = logData; + readMetric(metricName: string): Record | null { + const logs = this._readAndParseTelemetryLog(); + for (const logData of logs) { + if (logData.scopeMetrics) { + for (const scopeMetric of logData.scopeMetrics) { + for (const metric of scopeMetric.metrics) { + if (metric.descriptor.name === `gemini_cli.${metricName}`) { + return metric; + } + } } - } catch { - // ignore } } - return lastApiRequest; + return null; } runInteractive(...args: string[]): { diff --git a/package-lock.json b/package-lock.json index cfdd04544d..eaca5b57e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "packages/*" ], "dependencies": { + "@testing-library/dom": "^10.4.1", "simple-git": "^3.28.0" }, "bin": { @@ -419,7 +420,6 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3630,6 +3630,66 @@ "node": ">=14.16" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/@textlint/ast-node-types": { "version": "15.2.2", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", @@ -3763,9 +3823,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -5097,7 +5155,6 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -5106,22 +5163,11 @@ "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/accepts/node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5314,6 +5360,15 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -5683,7 +5738,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -5708,7 +5762,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -5718,7 +5771,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -5730,15 +5782,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/body-parser/node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6415,12 +6465,6 @@ "color-name": "1.1.3" } }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -6577,8 +6621,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cookiejar": { "version": "2.1.4", @@ -6961,7 +7004,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -7028,9 +7070,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -8130,13 +8170,6 @@ "ms": "2.0.0" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true - }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8503,16 +8536,6 @@ "node": ">= 18" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/form-data/node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -8564,7 +8587,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -11296,9 +11318,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11392,7 +11412,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -11433,7 +11452,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -11769,7 +11787,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -12135,16 +12152,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/npm-run-all2/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, "node_modules/npm-run-all2/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -14002,7 +14009,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -14027,7 +14033,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -14036,15 +14041,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -14054,7 +14057,6 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", - "peer": true, "bin": { "mime": "cli.js" }, @@ -14067,7 +14069,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -14077,7 +14078,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -14772,6 +14772,15 @@ "node": ">=8" } }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -15598,7 +15607,6 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", - "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -15607,22 +15615,11 @@ "node": ">= 0.6" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/type-is/node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -17214,43 +17211,6 @@ "node": ">=20" } }, - "packages/cli/node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "packages/cli/node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "packages/cli/node_modules/@testing-library/react": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", @@ -17280,53 +17240,14 @@ } }, "packages/cli/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=8" } }, - "packages/cli/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "packages/cli/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "packages/cli/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "license": "MIT" - }, - "packages/cli/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, "packages/cli/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -17456,12 +17377,6 @@ "node": ">= 4" } }, - "packages/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "packages/core/node_modules/mime": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", @@ -17535,39 +17450,6 @@ "dev": true, "license": "MIT" }, - "packages/vscode-ide-companion/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "packages/vscode-ide-companion/node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "packages/vscode-ide-companion/node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -17580,15 +17462,6 @@ "node": ">= 0.6" } }, - "packages/vscode-ide-companion/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "packages/vscode-ide-companion/node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -17648,45 +17521,6 @@ "node": ">= 0.8" } }, - "packages/vscode-ide-companion/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "packages/vscode-ide-companion/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "packages/vscode-ide-companion/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/vscode-ide-companion/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "packages/vscode-ide-companion/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -17724,21 +17558,6 @@ "node": ">= 18" } }, - "packages/vscode-ide-companion/node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "packages/vscode-ide-companion/node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index db76d3e673..ceb63ea411 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,13 @@ "clean": "node scripts/clean.js", "pre-commit": "node scripts/pre-commit.js" }, + "overrides": { + "wrap-ansi": "9.0.2", + "ansi-regex": "5.0.1", + "cliui": { + "wrap-ansi": "7.0.0" + } + }, "bin": { "gemini": "bundle/gemini.js" }, @@ -98,6 +105,7 @@ "yargs": "^17.7.2" }, "dependencies": { + "@testing-library/dom": "^10.4.1", "simple-git": "^3.28.0" }, "optionalDependencies": { @@ -109,12 +117,6 @@ "@lydell/node-pty-win32-x64": "1.1.0", "node-pty": "^1.0.0" }, - "overrides": { - "wrap-ansi": "9.0.2", - "cliui": { - "wrap-ansi": "7.0.0" - } - }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ "prettier --write", diff --git a/packages/cli/package.json b/packages/cli/package.json index a64c35138a..38d0ffd4ce 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,7 +18,7 @@ "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", "test": "vitest run", - "test:ci": "vitest run --coverage", + "test:ci": "vitest run", "typecheck": "tsc --noEmit" }, "files": [ diff --git a/packages/core/package.json b/packages/core/package.json index b3b4783bc3..b52ff32bf8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,7 +13,7 @@ "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", "test": "vitest run", - "test:ci": "vitest run --coverage", + "test:ci": "vitest run", "typecheck": "tsc --noEmit" }, "files": [ diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index d2c87bb8e4..a8a94484cf 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -4,25 +4,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import { - populateMcpServerCommand, - createTransport, - isEnabled, - hasValidTypes, - McpClient, - hasNetworkTransport, -} from './mcp-client.js'; +import * as GenAiLib from '@google/genai'; +import * as ClientLib from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js'; -import * as ClientLib from '@modelcontextprotocol/sdk/client/index.js'; -import * as GenAiLib from '@google/genai'; -import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { AuthProviderType, type Config } from '../config/config.js'; +import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js'; import type { PromptRegistry } from '../prompts/prompt-registry.js'; -import type { ToolRegistry } from './tool-registry.js'; import type { WorkspaceContext } from '../utils/workspaceContext.js'; +import { + createTransport, + hasNetworkTransport, + isEnabled, + McpClient, + populateMcpServerCommand, +} from './mcp-client.js'; +import type { ToolRegistry } from './tool-registry.js'; vi.mock('@modelcontextprotocol/sdk/client/stdio.js'); vi.mock('@modelcontextprotocol/sdk/client/index.js'); @@ -78,7 +77,7 @@ describe('mcp-client', () => { expect(mockedMcpToTool).toHaveBeenCalledOnce(); }); - it('should skip tools if a parameter is missing a type', async () => { + it('should not skip tools even if a parameter is missing a type', async () => { const consoleWarnSpy = vi .spyOn(console, 'warn') .mockImplementation(() => {}); @@ -137,12 +136,8 @@ describe('mcp-client', () => { ); await client.connect(); await client.discover({} as Config); - expect(mockedToolRegistry.registerTool).toHaveBeenCalledOnce(); - expect(consoleWarnSpy).toHaveBeenCalledOnce(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Skipping tool 'invalidTool' from MCP server 'test-server' because it has ` + - `missing types in its parameter schema. Please file an issue with the owner of the MCP server.`, - ); + expect(mockedToolRegistry.registerTool).toHaveBeenCalledTimes(2); + expect(consoleWarnSpy).not.toHaveBeenCalled(); consoleWarnSpy.mockRestore(); }); @@ -409,165 +404,6 @@ describe('mcp-client', () => { }); }); - describe('hasValidTypes', () => { - it('should return true for a valid schema with anyOf', () => { - const schema = { - anyOf: [{ type: 'string' }, { type: 'number' }], - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false for an invalid schema with anyOf', () => { - const schema = { - anyOf: [{ type: 'string' }, { description: 'no type' }], - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a valid schema with allOf', () => { - const schema = { - allOf: [ - { type: 'string' }, - { type: 'object', properties: { foo: { type: 'string' } } }, - ], - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false for an invalid schema with allOf', () => { - const schema = { - allOf: [{ type: 'string' }, { description: 'no type' }], - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a valid schema with oneOf', () => { - const schema = { - oneOf: [{ type: 'string' }, { type: 'number' }], - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false for an invalid schema with oneOf', () => { - const schema = { - oneOf: [{ type: 'string' }, { description: 'no type' }], - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a valid schema with nested subschemas', () => { - const schema = { - anyOf: [ - { type: 'string' }, - { - allOf: [ - { type: 'object', properties: { a: { type: 'string' } } }, - { type: 'object', properties: { b: { type: 'number' } } }, - ], - }, - ], - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false for an invalid schema with nested subschemas', () => { - const schema = { - anyOf: [ - { type: 'string' }, - { - allOf: [ - { type: 'object', properties: { a: { type: 'string' } } }, - { description: 'no type' }, - ], - }, - ], - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a schema with a type and subschemas', () => { - const schema = { - type: 'string', - anyOf: [{ minLength: 1 }, { maxLength: 5 }], - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false for a schema with no type and no subschemas', () => { - const schema = { - description: 'a schema with no type', - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a valid schema', () => { - const schema = { - type: 'object', - properties: { - param1: { type: 'string' }, - }, - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return false if a parameter is missing a type', () => { - const schema = { - type: 'object', - properties: { - param1: { description: 'a param with no type' }, - }, - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return false if a nested parameter is missing a type', () => { - const schema = { - type: 'object', - properties: { - param1: { - type: 'object', - properties: { - nestedParam: { - description: 'a nested param with no type', - }, - }, - }, - }, - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return false if an array item is missing a type', () => { - const schema = { - type: 'object', - properties: { - param1: { - type: 'array', - items: { - description: 'an array item with no type', - }, - }, - }, - }; - expect(hasValidTypes(schema)).toBe(false); - }); - - it('should return true for a schema with no properties', () => { - const schema = { - type: 'object', - }; - expect(hasValidTypes(schema)).toBe(true); - }); - - it('should return true for a schema with an empty properties object', () => { - const schema = { - type: 'object', - properties: {}, - }; - expect(hasValidTypes(schema)).toBe(true); - }); - }); - describe('hasNetworkTransport', () => { it('should return true if only url is provided', () => { const config = { url: 'http://example.com' }; diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 468d43139f..1879948a99 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -5,19 +5,19 @@ */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { - Prompt, GetPromptResult, + Prompt, } from '@modelcontextprotocol/sdk/types.js'; import { - ListPromptsResultSchema, GetPromptResultSchema, + ListPromptsResultSchema, ListRootsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { parse } from 'shell-quote'; @@ -28,18 +28,18 @@ import { DiscoveredMCPTool } from './mcp-tool.js'; import type { FunctionDeclaration } from '@google/genai'; import { mcpToTool } from '@google/genai'; -import type { ToolRegistry } from './tool-registry.js'; -import type { PromptRegistry } from '../prompts/prompt-registry.js'; -import { MCPOAuthProvider } from '../mcp/oauth-provider.js'; -import { OAuthUtils } from '../mcp/oauth-utils.js'; -import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js'; -import { getErrorMessage } from '../utils/errors.js'; import { basename } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { MCPOAuthProvider } from '../mcp/oauth-provider.js'; +import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js'; +import { OAuthUtils } from '../mcp/oauth-utils.js'; +import type { PromptRegistry } from '../prompts/prompt-registry.js'; +import { getErrorMessage } from '../utils/errors.js'; import type { Unsubscribe, WorkspaceContext, } from '../utils/workspaceContext.js'; +import type { ToolRegistry } from './tool-registry.js'; export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes @@ -564,65 +564,6 @@ export async function connectAndDiscover( } } -/** - * Recursively validates that a JSON schema and all its nested properties and - * items have a `type` defined. - * - * @param schema The JSON schema to validate. - * @returns `true` if the schema is valid, `false` otherwise. - * - * @visiblefortesting - */ -export function hasValidTypes(schema: unknown): boolean { - if (typeof schema !== 'object' || schema === null) { - // Not a schema object we can validate, or not a schema at all. - // Treat as valid as it has no properties to be invalid. - return true; - } - - const s = schema as Record; - - if (!s['type']) { - // These keywords contain an array of schemas that should be validated. - // - // If no top level type was given, then they must each have a type. - let hasSubSchema = false; - const schemaArrayKeywords = ['anyOf', 'allOf', 'oneOf']; - for (const keyword of schemaArrayKeywords) { - const subSchemas = s[keyword]; - if (Array.isArray(subSchemas)) { - hasSubSchema = true; - for (const subSchema of subSchemas) { - if (!hasValidTypes(subSchema)) { - return false; - } - } - } - } - - // If the node itself is missing a type and had no subschemas, then it isn't valid. - if (!hasSubSchema) return false; - } - - if (s['type'] === 'object' && s['properties']) { - if (typeof s['properties'] === 'object' && s['properties'] !== null) { - for (const prop of Object.values(s['properties'])) { - if (!hasValidTypes(prop)) { - return false; - } - } - } - } - - if (s['type'] === 'array' && s['items']) { - if (!hasValidTypes(s['items'])) { - return false; - } - } - - return true; -} - /** * Discovers and sanitizes tools from a connected MCP client. * It retrieves function declarations from the client, filters out disabled tools, @@ -658,15 +599,6 @@ export async function discoverTools( continue; } - if (!hasValidTypes(funcDecl.parametersJsonSchema)) { - console.warn( - `Skipping tool '${funcDecl.name}' from MCP server '${mcpServerName}' ` + - `because it has missing types in its parameter schema. Please file an ` + - `issue with the owner of the MCP server.`, - ); - continue; - } - discoveredTools.push( new DiscoveredMCPTool( mcpCallableTool,