@google-cloud/cloud-run-mcp 1.2.0 → 1.5.0

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.
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import protofiles from 'google-proto-files';
18
+ import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
18
19
 
19
20
  let runClient;
20
21
  let loggingClient;
@@ -32,13 +33,19 @@ export async function listServices(projectId, location) {
32
33
  const { ServicesClient } = v2;
33
34
  runClient = new ServicesClient({ projectId });
34
35
  }
36
+
37
+ await ensureApisEnabled(projectId, ['run.googleapis.com']);
38
+
35
39
  const parent = runClient.locationPath(projectId, location);
36
40
 
37
41
  try {
38
42
  console.log(
39
43
  `Listing Cloud Run services in project ${projectId}, location ${location}...`
40
44
  );
41
- const [services] = await runClient.listServices({ parent });
45
+ const [services] = await callWithRetry(
46
+ () => runClient.listServices({ parent }),
47
+ 'listServices'
48
+ );
42
49
  return services;
43
50
  } catch (error) {
44
51
  console.error(`Error listing Cloud Run services:`, error);
@@ -66,7 +73,10 @@ export async function getService(projectId, location, serviceId) {
66
73
  console.log(
67
74
  `Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
68
75
  );
69
- const [service] = await runClient.getService({ name: servicePath });
76
+ const [service] = await callWithRetry(
77
+ () => runClient.getService({ name: servicePath }),
78
+ 'getService'
79
+ );
70
80
  return service;
71
81
  } catch (error) {
72
82
  console.error(
@@ -123,8 +133,10 @@ export async function getServiceLogs(
123
133
  console.log(`Request options: ${JSON.stringify(options)}`);
124
134
 
125
135
  // getEntries returns the entries and the full API response
126
- const [entries, nextRequestOptions, apiResponse] =
127
- await loggingClient.getEntries(options);
136
+ const [entries, nextRequestOptions, apiResponse] = await callWithRetry(
137
+ () => loggingClient.getEntries(options),
138
+ 'getEntries'
139
+ );
128
140
 
129
141
  const formattedLogLines = entries
130
142
  .map((entry) => formatLogEntry(entry))
@@ -0,0 +1,143 @@
1
+ /*
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * Calls a function with retry logic for GCP API calls.
19
+ * Retries on gRPC error code 7 (PERMISSION_DENIED).
20
+ * @param {Function} fn The function to call.
21
+ * @param {string} description A description of the function being called, for logging.
22
+ * @returns {Promise<any>} The result of the function.
23
+ */
24
+ export async function callWithRetry(fn, description) {
25
+ const maxRetries = 7;
26
+ const initialBackoff = 1000; // 1 second
27
+ let retries = 0;
28
+
29
+ while (true) {
30
+ try {
31
+ return await fn();
32
+ } catch (error) {
33
+ if (error.code === 7 && retries < maxRetries) {
34
+ retries++;
35
+ let backoff;
36
+ if (retries === 1) {
37
+ backoff = 15000; // 15 seconds for the first retry
38
+ } else {
39
+ backoff = initialBackoff * Math.pow(2, retries - 2);
40
+ }
41
+ console.warn(
42
+ `API call "${description}" failed with PERMISSION_DENIED. Retrying in ${
43
+ backoff / 1000
44
+ }s... (attempt ${retries}/${maxRetries})`
45
+ );
46
+ await new Promise((resolve) => setTimeout(resolve, backoff));
47
+ } else {
48
+ throw error;
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Checks if a single Google Cloud API is enabled and enables it if not.
56
+ *
57
+ * @param {object} serviceUsageClient - The Service Usage client.
58
+ * @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
59
+ * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
60
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
61
+ * @returns {Promise<void>} A promise that resolves when the API is enabled.
62
+ * @throws {Error} If the API fails to enable or if there's an issue checking its status.
63
+ */
64
+ async function checkAndEnableApi(
65
+ serviceUsageClient,
66
+ serviceName,
67
+ api,
68
+ progressCallback
69
+ ) {
70
+ const [service] = await callWithRetry(
71
+ () => serviceUsageClient.getService({ name: serviceName }),
72
+ `getService ${api}`
73
+ );
74
+ if (service.state !== 'ENABLED') {
75
+ const message = `API [${api}] is not enabled. Enabling...`;
76
+ console.log(message);
77
+ if (progressCallback) progressCallback({ level: 'info', data: message });
78
+
79
+ const [operation] = await callWithRetry(
80
+ () => serviceUsageClient.enableService({ name: serviceName }),
81
+ `enableService ${api}`
82
+ );
83
+ await operation.promise();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Ensures that the specified Google Cloud APIs are enabled for the given project.
89
+ * If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
90
+ * Throws an error if an API cannot be enabled.
91
+ *
92
+ * @async
93
+ * @param {string} projectId - The Google Cloud project ID.
94
+ * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
95
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
96
+ * The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
97
+ * @throws {Error} If an API fails to enable or if there's an issue checking its status.
98
+ * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
99
+ */
100
+ export async function ensureApisEnabled(projectId, apis, progressCallback) {
101
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
102
+ const serviceUsageClient = new ServiceUsageClient({ projectId });
103
+ const message = 'Checking and enabling required APIs...';
104
+ console.log(message);
105
+ if (progressCallback) progressCallback({ level: 'info', data: message });
106
+
107
+
108
+ for (const api of apis) {
109
+ const serviceName = `projects/${projectId}/services/${api}`;
110
+ try {
111
+ await checkAndEnableApi(
112
+ serviceUsageClient,
113
+ serviceName,
114
+ api,
115
+ progressCallback
116
+ );
117
+ } catch (error) {
118
+ // First attempt failed, log a warning and retry once after a delay.
119
+ const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
120
+ console.warn(warnMsg);
121
+ if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
122
+
123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
124
+ try {
125
+ await checkAndEnableApi(
126
+ serviceUsageClient,
127
+ serviceName,
128
+ api,
129
+ progressCallback
130
+ );
131
+ } catch (retryError) {
132
+ // If the retry also fails, throw an error.
133
+ const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
134
+ console.error(errorMessage, retryError);
135
+ if (progressCallback) progressCallback({ level: 'error', data: errorMessage });
136
+ throw new Error(errorMessage);
137
+ }
138
+ }
139
+ }
140
+ const successMsg = 'All required APIs are enabled.';
141
+ console.log(successMsg);
142
+ if (progressCallback) progressCallback({ level: 'info', data: successMsg });
143
+ }
@@ -72,7 +72,7 @@ export async function createProject(projectId) {
72
72
  }
73
73
 
74
74
  try {
75
- const projectPayload = { projectId: projectIdToUse, name: projectIdToUse };
75
+ const projectPayload = { projectId: projectIdToUse };
76
76
 
77
77
  console.log(`Attempting to create project with ID: ${projectIdToUse}`);
78
78
 
package/mcp-server.js CHANGED
@@ -23,7 +23,8 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
23
23
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
24
24
  // Support stdio, as it is easier to use locally
25
25
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
26
- import { registerTools, registerToolsRemote } from './tools.js';
26
+ import { registerTools, registerToolsRemote } from './tools/tools.js';
27
+ import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
27
28
  import { registerPrompts } from './prompts.js';
28
29
  import { checkGCP } from './lib/gcp-metadata.js';
29
30
  import { ensureGCPCredentials } from './lib/gcp-auth-check.js';
@@ -71,6 +72,12 @@ async function getServer() {
71
72
  { capabilities: { logging: {} } }
72
73
  );
73
74
 
75
+ // this is no-op handler is required for mcp-inspector to function due to a mismatch between the SDK mcp-inspector
76
+ server.server.setRequestHandler(SetLevelRequestSchema, (request) => {
77
+ console.log(`Log Level: ${request.params.level}`);
78
+ return {};
79
+ });
80
+
74
81
  // Get GCP metadata info once
75
82
  const gcpInfo = await checkGCP();
76
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/cloud-run-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Cloud Run MCP deployment tool",
6
6
  "main": "mcp-server.js",
@@ -10,8 +10,8 @@
10
10
  "scripts": {
11
11
  "deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
12
12
  "test:deploy": "node test/need-gcp/cloud-run-deploy.test.js",
13
- "test:create-project": "node test/need-gcp/gcp-projects.test.js",
14
- "test:service-logs": "node test/need-gcp/cloud-run-services.test.js",
13
+ "test:projects": "node test/need-gcp/gcp-projects.test.js",
14
+ "test:services": "node test/need-gcp/cloud-run-services.test.js",
15
15
  "test:gcp-auth": "node test/need-gcp/gcp-auth-check.test.js",
16
16
  "test:prompts": "node --test test/local/prompts.test.js",
17
17
  "test:mcp": "node --test test/local/mcp-server.test.js",
@@ -0,0 +1,35 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const rootDir = path.resolve(__dirname, '..', '..');
10
+ const geminiExtensionJsonPath = path.resolve(rootDir, 'gemini-extension.json');
11
+
12
+ describe('Gemini CLI extension', () => {
13
+ test('gemini-extension.json should be at the root', () => {
14
+ assert.ok(
15
+ fs.existsSync(geminiExtensionJsonPath),
16
+ 'gemini-extension.json not found at the project root'
17
+ );
18
+ });
19
+
20
+ test('contextFileName from gemini-extension.json should exist', () => {
21
+ const geminiExtensionJson = JSON.parse(
22
+ fs.readFileSync(geminiExtensionJsonPath, 'utf-8')
23
+ );
24
+ const contextFileName = geminiExtensionJson.contextFileName;
25
+ assert.ok(
26
+ contextFileName,
27
+ 'contextFileName not found in gemini-extension.json'
28
+ );
29
+ const contextFilePath = path.resolve(rootDir, contextFileName);
30
+ assert.ok(
31
+ fs.existsSync(contextFilePath),
32
+ `context file name '${contextFileName}' not found`
33
+ );
34
+ });
35
+ });
@@ -35,7 +35,6 @@ describe('MCP Server in stdio mode', () => {
35
35
  'create_project',
36
36
  'deploy_container_image',
37
37
  'deploy_file_contents',
38
- 'deploy_local_files',
39
38
  'deploy_local_folder',
40
39
  'get_service',
41
40
  'get_service_log',
@@ -0,0 +1,45 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it, mock } from 'node:test';
3
+ import esmock from 'esmock';
4
+
5
+ describe('Tool Notifications', () => {
6
+ it('should send notifications during deployment', async () => {
7
+ const server = {
8
+ registerTool: mock.fn(),
9
+ };
10
+
11
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
12
+ '../../lib/cloud-run-deploy.js': {
13
+ deploy: () => Promise.resolve({ uri: 'my-uri' }),
14
+ },
15
+ },
16
+ );
17
+
18
+ registerTools(server, { gcpCredentialsAvailable: true });
19
+
20
+ const handler = server.registerTool.mock.calls.find(
21
+ (call) => call.arguments[0] === 'deploy_local_folder'
22
+ ).arguments[2];
23
+
24
+ const sendNotification = mock.fn();
25
+
26
+ await handler(
27
+ {
28
+ project: 'my-project',
29
+ region: 'my-region',
30
+ service: 'my-service',
31
+ folderPath: '/my/folder',
32
+ },
33
+ { sendNotification }
34
+ );
35
+
36
+ assert.strictEqual(sendNotification.mock.callCount(), 1);
37
+ assert.deepStrictEqual(sendNotification.mock.calls[0].arguments[0], {
38
+ method: 'notifications/message',
39
+ params: {
40
+ level: 'info',
41
+ data: 'Starting deployment of local folder for service my-service in project my-project...',
42
+ },
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,36 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
10
+ const mcpServerPath = path.resolve(__dirname, '..', '..', 'mcp-server.js');
11
+
12
+ describe('The repository is properly structured to be executed using npx', () => {
13
+ test('package.json should be at the root', () => {
14
+ assert.ok(
15
+ fs.existsSync(packageJsonPath),
16
+ 'package.json not found at the project root'
17
+ );
18
+ });
19
+
20
+ test('package.json should have a bin attribute for cloud-run-mcp', () => {
21
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
22
+ assert.ok(packageJson.bin, 'bin attribute not found in package.json');
23
+ assert.ok(
24
+ packageJson.bin['cloud-run-mcp'],
25
+ 'cloud-run-mcp not found in bin attribute'
26
+ );
27
+ });
28
+
29
+ test('mcp-server.js should start with #!/usr/bin/env node', () => {
30
+ const mcpServerContent = fs.readFileSync(mcpServerPath, 'utf-8');
31
+ assert.ok(
32
+ mcpServerContent.startsWith('#!/usr/bin/env node'),
33
+ 'mcp-server.js does not start with #!/usr/bin/env node'
34
+ );
35
+ });
36
+ });
@@ -8,11 +8,11 @@ describe('registerTools', () => {
8
8
  registerTool: mock.fn(),
9
9
  };
10
10
 
11
- const { registerTools } = await esmock('../../tools.js', {});
11
+ const { registerTools } = await esmock('../../tools/tools.js', {});
12
12
 
13
13
  registerTools(server);
14
14
 
15
- assert.strictEqual(server.registerTool.mock.callCount(), 9);
15
+ assert.strictEqual(server.registerTool.mock.callCount(), 8);
16
16
  const toolNames = server.registerTool.mock.calls.map(
17
17
  (call) => call.arguments[0]
18
18
  );
@@ -22,7 +22,6 @@ describe('registerTools', () => {
22
22
  'create_project',
23
23
  'deploy_container_image',
24
24
  'deploy_file_contents',
25
- 'deploy_local_files',
26
25
  'deploy_local_folder',
27
26
  'get_service',
28
27
  'get_service_log',
@@ -38,7 +37,7 @@ describe('registerTools', () => {
38
37
  registerTool: mock.fn(),
39
38
  };
40
39
 
41
- const { registerTools } = await esmock('../../tools.js', {
40
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
42
41
  '../../lib/gcp-projects.js': {
43
42
  listProjects: () =>
44
43
  Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
@@ -69,7 +68,7 @@ describe('registerTools', () => {
69
68
  registerTool: mock.fn(),
70
69
  };
71
70
 
72
- const { registerTools } = await esmock('../../tools.js', {
71
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
73
72
  '../../lib/gcp-projects.js': {
74
73
  createProjectAndAttachBilling: (projectId) =>
75
74
  Promise.resolve({
@@ -101,7 +100,7 @@ describe('registerTools', () => {
101
100
  registerTool: mock.fn(),
102
101
  };
103
102
 
104
- const { registerTools } = await esmock('../../tools.js', {
103
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
105
104
  '../../lib/gcp-projects.js': {
106
105
  createProjectAndAttachBilling: () =>
107
106
  Promise.resolve({
@@ -135,7 +134,7 @@ describe('registerTools', () => {
135
134
  registerTool: mock.fn(),
136
135
  };
137
136
 
138
- const { registerTools } = await esmock('../../tools.js', {
137
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
139
138
  '../../lib/cloud-run-services.js': {
140
139
  listServices: () =>
141
140
  Promise.resolve([
@@ -172,7 +171,7 @@ describe('registerTools', () => {
172
171
  registerTool: mock.fn(),
173
172
  };
174
173
 
175
- const { registerTools } = await esmock('../../tools.js', {
174
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
176
175
  '../../lib/cloud-run-services.js': {
177
176
  getService: () =>
178
177
  Promise.resolve({
@@ -223,7 +222,7 @@ describe('registerTools', () => {
223
222
  return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
224
223
  };
225
224
 
226
- const { registerTools } = await esmock('../../tools.js', {
225
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
227
226
  '../../lib/cloud-run-services.js': {
228
227
  getServiceLogs: getServiceLogs,
229
228
  },
@@ -251,40 +250,7 @@ describe('registerTools', () => {
251
250
  });
252
251
  });
253
252
 
254
- describe('deploy_local_files', () => {
255
- it('should deploy local files', async () => {
256
- const server = {
257
- registerTool: mock.fn(),
258
- };
259
-
260
- const { registerTools } = await esmock('../../tools.js', {
261
- '../../lib/cloud-run-deploy.js': {
262
- deploy: () => Promise.resolve({ uri: 'my-uri' }),
263
- },
264
- });
265
-
266
- registerTools(server, { gcpCredentialsAvailable: true });
267
-
268
- const handler = server.registerTool.mock.calls.find(
269
- (call) => call.arguments[0] === 'deploy_local_files'
270
- ).arguments[2];
271
- const result = await handler({
272
- project: 'my-project',
273
- region: 'my-region',
274
- service: 'my-service',
275
- files: ['file1', 'file2'],
276
- });
277
-
278
- assert.deepStrictEqual(result, {
279
- content: [
280
- {
281
- type: 'text',
282
- text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
283
- },
284
- ],
285
- });
286
- });
287
- });
253
+
288
254
 
289
255
  describe('deploy_local_folder', () => {
290
256
  it('should deploy local folder', async () => {
@@ -292,7 +258,7 @@ describe('registerTools', () => {
292
258
  registerTool: mock.fn(),
293
259
  };
294
260
 
295
- const { registerTools } = await esmock('../../tools.js', {
261
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
296
262
  '../../lib/cloud-run-deploy.js': {
297
263
  deploy: () => Promise.resolve({ uri: 'my-uri' }),
298
264
  },
@@ -303,12 +269,15 @@ describe('registerTools', () => {
303
269
  const handler = server.registerTool.mock.calls.find(
304
270
  (call) => call.arguments[0] === 'deploy_local_folder'
305
271
  ).arguments[2];
306
- const result = await handler({
307
- project: 'my-project',
308
- region: 'my-region',
309
- service: 'my-service',
310
- folderPath: '/my/folder',
311
- });
272
+ const result = await handler(
273
+ {
274
+ project: 'my-project',
275
+ region: 'my-region',
276
+ service: 'my-service',
277
+ folderPath: '/my/folder',
278
+ },
279
+ { sendNotification: mock.fn() }
280
+ );
312
281
 
313
282
  assert.deepStrictEqual(result, {
314
283
  content: [
@@ -327,7 +296,7 @@ describe('registerTools', () => {
327
296
  registerTool: mock.fn(),
328
297
  };
329
298
 
330
- const { registerTools } = await esmock('../../tools.js', {
299
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
331
300
  '../../lib/cloud-run-deploy.js': {
332
301
  deploy: () => Promise.resolve({ uri: 'my-uri' }),
333
302
  },
@@ -338,12 +307,15 @@ describe('registerTools', () => {
338
307
  const handler = server.registerTool.mock.calls.find(
339
308
  (call) => call.arguments[0] === 'deploy_file_contents'
340
309
  ).arguments[2];
341
- const result = await handler({
342
- project: 'my-project',
343
- region: 'my-region',
344
- service: 'my-service',
345
- files: [{ filename: 'file1', content: 'content1' }],
346
- });
310
+ const result = await handler(
311
+ {
312
+ project: 'my-project',
313
+ region: 'my-region',
314
+ service: 'my-service',
315
+ files: [{ filename: 'file1', content: 'content1' }],
316
+ },
317
+ { sendNotification: mock.fn() }
318
+ );
347
319
 
348
320
  assert.deepStrictEqual(result, {
349
321
  content: [
@@ -362,7 +334,7 @@ describe('registerTools', () => {
362
334
  registerTool: mock.fn(),
363
335
  };
364
336
 
365
- const { registerTools } = await esmock('../../tools.js', {
337
+ const { registerTools } = await esmock('../../tools/tools.js', {}, {
366
338
  '../../lib/cloud-run-deploy.js': {
367
339
  deployImage: () => Promise.resolve({ uri: 'my-uri' }),
368
340
  },
@@ -373,12 +345,15 @@ describe('registerTools', () => {
373
345
  const handler = server.registerTool.mock.calls.find(
374
346
  (call) => call.arguments[0] === 'deploy_container_image'
375
347
  ).arguments[2];
376
- const result = await handler({
377
- project: 'my-project',
378
- region: 'my-region',
379
- service: 'my-service',
380
- imageUrl: 'gcr.io/my-project/my-image',
381
- });
348
+ const result = await handler(
349
+ {
350
+ project: 'my-project',
351
+ region: 'my-region',
352
+ service: 'my-service',
353
+ imageUrl: 'gcr.io/my-project/my-image',
354
+ },
355
+ { sendNotification: mock.fn() }
356
+ );
382
357
 
383
358
  assert.deepStrictEqual(result, {
384
359
  content: [
@@ -397,7 +372,7 @@ describe('registerTools', () => {
397
372
  registerTool: mock.fn(),
398
373
  };
399
374
 
400
- const { registerTools } = await esmock('../../tools.js', {});
375
+ const { registerTools } = await esmock('../../tools/tools.js', {});
401
376
 
402
377
  registerTools(server, { gcpCredentialsAvailable: false });
403
378