@google-cloud/cloud-run-mcp 1.3.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.
Binary file
@@ -3,7 +3,7 @@ name: Run Local Tests
3
3
  on:
4
4
  push:
5
5
  pull_request:
6
- branches: [ main ]
6
+ branches: [main]
7
7
 
8
8
  jobs:
9
9
  test:
package/CONTRIBUTING.md CHANGED
@@ -25,6 +25,12 @@ This project follows
25
25
 
26
26
  ## Contribution process
27
27
 
28
+ ### Start with an issue
29
+
30
+ Before sending a pull request, please open an issue describing the bug or feature
31
+ you would like to address. This will allow maintainers of this project to guide
32
+ you in your design and implementation.
33
+
28
34
  ### Code reviews
29
35
 
30
36
  All submissions, including submissions by project members, require review. We
package/README.md CHANGED
@@ -11,13 +11,17 @@ Enable MCP-compatible AI agents to deploy apps to Cloud Run.
11
11
  }
12
12
  ```
13
13
 
14
+ Deploy from Gemini CLI and other AI-powered CLI agents:
15
+
16
+ <img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploycli.gif" width="800">
17
+
14
18
  Deploy from AI-powered IDEs:
15
19
 
16
- <img src="https://github.com/user-attachments/assets/9fdcec30-2b38-4362-9eb1-54cab09e99d4" width="800">
20
+ <img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_ide.gif" width="800">
17
21
 
18
22
  Deploy from AI assistant apps:
19
23
 
20
- <img src="https://github.com/user-attachments/assets/b10f0335-b332-4640-af38-ea015b46b57c" width="800">
24
+ <img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_apps.gif" width="800">
21
25
 
22
26
  Deploy from agent SDKs, like the [Google Gen AI SDK](https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#use_model_context_protocol_mcp) or [Agent Development Kit](https://google.github.io/adk-docs/tools/mcp-tools/).
23
27
 
@@ -30,7 +34,7 @@ Deploy from agent SDKs, like the [Google Gen AI SDK](https://ai.google.dev/gemin
30
34
  - `list-services`: Lists Cloud Run services in a given project and region.
31
35
  - `get-service`: Gets details for a specific Cloud Run service.
32
36
  - `get-service-log`: Gets Logs and Error Messages for a specific Cloud Run service.
33
- - `deploy-local-files`\*: Deploys files from the local file system to a Google Cloud Run service.
37
+
34
38
  - `deploy-local-folder`\*: Deploys a local folder to a Google Cloud Run service.
35
39
  - `list-projects`\*: Lists available GCP projects.
36
40
  - `create-project`\*: Creates a new GCP project and attach it to the first available billing account. A project ID can be optionally specified.
@@ -44,6 +48,18 @@ Prompts are natural language commands that can be used to perform common tasks.
44
48
  - `deploy`: Deploys the current working directory to Cloud Run. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.
45
49
  - `logs`: Gets the logs for a Cloud Run service. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.
46
50
 
51
+
52
+ ## Use as a Gemini CLI extension
53
+
54
+ To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
55
+
56
+ ```bash
57
+ mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
58
+ curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
59
+ curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/GEMINI.md > ~/.gemini/extensions/cloud-run/gemini-extension/GEMINI.md
60
+ ```
61
+
62
+
47
63
  ## Use as local MCP server
48
64
 
49
65
  Run the Cloud Run MCP server on your local machine using local Google Cloud credentials. This is best if you are using an AI-assisted IDE (e.g. Cursor) or a desktop AI application (e.g. Claude).
@@ -118,19 +134,8 @@ See Docker's [MCP catalog](https://hub.docker.com/mcp/server/cloud-run-mcp/overv
118
134
  }
119
135
  ```
120
136
 
121
- ## Use as a Gemini CLI extension
122
-
123
- To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
124
-
125
- ```bash
126
- mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
127
- curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
128
- curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/Gemini.md > ~/.gemini/extensions/cloud-run/gemini-extension/Gemini.md
129
- ```
130
-
131
137
  ## Use as remote MCP server
132
138
 
133
-
134
139
  > [!WARNING]
135
140
  > Do not use the remote MCP server without authentication. In the following instructions, we will use IAM authentication to secure the connection to the MCP server from your local machine. This is important to prevent unauthorized access to your Google Cloud resources.
136
141
 
@@ -47,11 +47,11 @@ server {
47
47
  The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.
48
48
 
49
49
  If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
50
- The user mush follow these steps:
50
+ The user must follow these steps:
51
51
 
52
52
  1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.
53
53
 
54
54
  2. Set up application credentials using the command:
55
55
  ```bash
56
56
  gcloud auth application-default login
57
- ```
57
+ ```
@@ -48,7 +48,7 @@ let runClient;
48
48
  * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
49
49
  * @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
50
50
  */
51
- function logAndProgress(message, progressCallback, severity = 'info') {
51
+ async function logAndProgress(message, progressCallback, severity = 'info') {
52
52
  switch (severity) {
53
53
  case 'error':
54
54
  console.error(message);
@@ -89,14 +89,14 @@ async function checkCloudRunServiceExists(
89
89
  () => runClient.getService({ name: servicePath }),
90
90
  `getService ${serviceId}`
91
91
  );
92
- logAndProgress(
92
+ await logAndProgress(
93
93
  `Cloud Run service ${serviceId} already exists.`,
94
94
  progressCallback
95
95
  );
96
96
  return true;
97
97
  } catch (error) {
98
98
  if (error.code === 5) {
99
- logAndProgress(
99
+ await logAndProgress(
100
100
  `Cloud Run service ${serviceId} does not exist.`,
101
101
  progressCallback
102
102
  );
@@ -104,7 +104,7 @@ async function checkCloudRunServiceExists(
104
104
  }
105
105
  const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
106
106
  console.error(`Error checking Cloud Run service ${serviceId}:`, error);
107
- logAndProgress(errorMessage, progressCallback, 'error');
107
+ await logAndProgress(errorMessage, progressCallback, 'error');
108
108
  throw error;
109
109
  }
110
110
  }
@@ -160,7 +160,7 @@ async function deployToCloudRun(
160
160
 
161
161
  // Always perform a dry run first for general validation
162
162
  try {
163
- logAndProgress(
163
+ await logAndProgress(
164
164
  `Performing dry run for service ${serviceId}...`,
165
165
  progressCallback,
166
166
  'debug'
@@ -189,13 +189,13 @@ async function deployToCloudRun(
189
189
  `createService (dry run) ${serviceId}`
190
190
  );
191
191
  }
192
- logAndProgress(
192
+ await logAndProgress(
193
193
  `Dry run successful for ${serviceId} with current configuration.`,
194
194
  progressCallback,
195
195
  'debug'
196
196
  );
197
197
  } catch (dryRunError) {
198
- logAndProgress(
198
+ await logAndProgress(
199
199
  `Dry run for ${serviceId} failed: ${dryRunError.message}`,
200
200
  progressCallback,
201
201
  'warn'
@@ -209,7 +209,7 @@ async function deployToCloudRun(
209
209
  dryRunError.message.toLowerCase().includes('iam policy violation') ||
210
210
  dryRunError.code === 3) /* INVALID_ARGUMENT */
211
211
  ) {
212
- logAndProgress(
212
+ await logAndProgress(
213
213
  `Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
214
214
  progressCallback,
215
215
  'warn'
@@ -218,14 +218,14 @@ async function deployToCloudRun(
218
218
  } else {
219
219
  // For any other validation errors, rethrow to stop the deployment
220
220
  const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
221
- logAndProgress(errorMessage, progressCallback, 'error');
221
+ await logAndProgress(errorMessage, progressCallback, 'error');
222
222
  throw new Error(errorMessage);
223
223
  }
224
224
  }
225
225
 
226
226
  let operation;
227
227
  if (exists) {
228
- logAndProgress(
228
+ await logAndProgress(
229
229
  `Updating existing service ${serviceId}...`,
230
230
  progressCallback
231
231
  );
@@ -235,7 +235,7 @@ async function deployToCloudRun(
235
235
  `updateService ${serviceId}`
236
236
  );
237
237
  } else {
238
- logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
238
+ await logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
239
239
  [operation] = await callWithRetry(
240
240
  () =>
241
241
  runClient.createService({
@@ -247,10 +247,13 @@ async function deployToCloudRun(
247
247
  );
248
248
  }
249
249
 
250
- logAndProgress(`Deploying ${serviceId} to Cloud Run...`, progressCallback);
250
+ await logAndProgress(
251
+ `Deploying ${serviceId} to Cloud Run...`,
252
+ progressCallback
253
+ );
251
254
  const [response] = await operation.promise();
252
255
 
253
- logAndProgress(
256
+ await logAndProgress(
254
257
  `Service deployed/updated successfully: ${response.uri}`,
255
258
  progressCallback
256
259
  );
@@ -258,7 +261,7 @@ async function deployToCloudRun(
258
261
  } catch (error) {
259
262
  const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
260
263
  console.error(`Error deploying/updating service ${serviceId}:`, error);
261
- logAndProgress(errorMessage, progressCallback, 'error');
264
+ await logAndProgress(errorMessage, progressCallback, 'error');
262
265
  throw error;
263
266
  }
264
267
  }
@@ -286,10 +289,13 @@ async function ensureStorageBucketExists(
286
289
  `storage.bucket.exists ${bucketName}`
287
290
  );
288
291
  if (exists) {
289
- logAndProgress(`Bucket ${bucketName} already exists.`, progressCallback);
292
+ await logAndProgress(
293
+ `Bucket ${bucketName} already exists.`,
294
+ progressCallback
295
+ );
290
296
  return bucket;
291
297
  } else {
292
- logAndProgress(
298
+ await logAndProgress(
293
299
  `Bucket ${bucketName} does not exist. Creating in location ${location}...`,
294
300
  progressCallback
295
301
  );
@@ -298,7 +304,7 @@ async function ensureStorageBucketExists(
298
304
  () => storage.createBucket(bucketName, { location: location }),
299
305
  `storage.createBucket ${bucketName}`
300
306
  );
301
- logAndProgress(
307
+ await logAndProgress(
302
308
  `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
303
309
  progressCallback
304
310
  );
@@ -309,14 +315,14 @@ async function ensureStorageBucketExists(
309
315
  `Failed to create storage bucket ${bucketName}. Error details:`,
310
316
  createError
311
317
  );
312
- logAndProgress(errorMessage, progressCallback, 'error');
318
+ await logAndProgress(errorMessage, progressCallback, 'error');
313
319
  throw createError;
314
320
  }
315
321
  }
316
322
  } catch (error) {
317
323
  const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
318
324
  console.error(`Error checking/creating bucket ${bucketName}:`, error);
319
- logAndProgress(errorMessage, progressCallback, 'error');
325
+ await logAndProgress(errorMessage, progressCallback, 'error');
320
326
  throw error;
321
327
  }
322
328
  }
@@ -417,7 +423,7 @@ async function uploadToStorageBucket(
417
423
  progressCallback
418
424
  ) {
419
425
  try {
420
- logAndProgress(
426
+ await logAndProgress(
421
427
  `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
422
428
  progressCallback
423
429
  );
@@ -425,7 +431,7 @@ async function uploadToStorageBucket(
425
431
  () => bucket.file(destinationBlobName).save(buffer),
426
432
  `storage.bucket.file.save ${destinationBlobName}`
427
433
  );
428
- logAndProgress(
434
+ await logAndProgress(
429
435
  `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
430
436
  progressCallback
431
437
  );
@@ -433,7 +439,7 @@ async function uploadToStorageBucket(
433
439
  } catch (error) {
434
440
  const errorMessage = `Error uploading buffer: ${error.message}`;
435
441
  console.error(`Error uploading buffer:`, error);
436
- logAndProgress(errorMessage, progressCallback, 'error');
442
+ await logAndProgress(errorMessage, progressCallback, 'error');
437
443
  throw error;
438
444
  }
439
445
  }
@@ -470,14 +476,14 @@ async function ensureArtifactRegistryRepoExists(
470
476
  () => artifactRegistryClient.getRepository({ name: repoPath }),
471
477
  `artifactRegistry.getRepository ${repositoryId}`
472
478
  );
473
- logAndProgress(
479
+ await logAndProgress(
474
480
  `Repository ${repositoryId} already exists in ${location}.`,
475
481
  progressCallback
476
482
  );
477
483
  return repository;
478
484
  } catch (error) {
479
485
  if (error.code === 5) {
480
- logAndProgress(
486
+ await logAndProgress(
481
487
  `Repository ${repositoryId} does not exist in ${location}. Creating...`,
482
488
  progressCallback
483
489
  );
@@ -494,12 +500,12 @@ async function ensureArtifactRegistryRepoExists(
494
500
  }),
495
501
  `artifactRegistry.createRepository ${repositoryId}`
496
502
  );
497
- logAndProgress(
503
+ await logAndProgress(
498
504
  `Creating Artifact Registry repository ${repositoryId}...`,
499
505
  progressCallback
500
506
  );
501
507
  const [result] = await operation.promise();
502
- logAndProgress(
508
+ await logAndProgress(
503
509
  `Artifact Registry repository ${result.name} created successfully.`,
504
510
  progressCallback
505
511
  );
@@ -510,7 +516,7 @@ async function ensureArtifactRegistryRepoExists(
510
516
  `Error checking/creating repository ${repositoryId}:`,
511
517
  error
512
518
  );
513
- logAndProgress(errorMessage, progressCallback, 'error');
519
+ await logAndProgress(errorMessage, progressCallback, 'error');
514
520
  throw error;
515
521
  }
516
522
  }
@@ -580,7 +586,7 @@ async function triggerCloudBuild(
580
586
  images: [targetImageUrl],
581
587
  };
582
588
 
583
- logAndProgress(
589
+ await logAndProgress(
584
590
  `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
585
591
  progressCallback
586
592
  );
@@ -589,7 +595,7 @@ async function triggerCloudBuild(
589
595
  'cloudBuild.createBuild'
590
596
  );
591
597
 
592
- logAndProgress(`Cloud Build job started...`, progressCallback);
598
+ await logAndProgress(`Cloud Build job started...`, progressCallback);
593
599
  const buildId = operation.metadata.build.id;
594
600
  let completedBuild;
595
601
  while (true) {
@@ -605,7 +611,7 @@ async function triggerCloudBuild(
605
611
  completedBuild = getBuildOperation;
606
612
  break;
607
613
  }
608
- logAndProgress(
614
+ await logAndProgress(
609
615
  `Build status: ${getBuildOperation.status}. Waiting...`,
610
616
  progressCallback,
611
617
  'debug'
@@ -614,20 +620,20 @@ async function triggerCloudBuild(
614
620
  }
615
621
 
616
622
  if (completedBuild.status === 'SUCCESS') {
617
- logAndProgress(
623
+ await logAndProgress(
618
624
  `Cloud Build job ${buildId} completed successfully.`,
619
625
  progressCallback
620
626
  );
621
- logAndProgress(
627
+ await logAndProgress(
622
628
  `Image built: ${completedBuild.results.images[0].name}`,
623
629
  progressCallback
624
630
  );
625
631
  return completedBuild;
626
632
  } else {
627
633
  const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
628
- logAndProgress(failureMessage, progressCallback, 'error');
634
+ await logAndProgress(failureMessage, progressCallback, 'error');
629
635
  const logsMessage = `Build logs: ${completedBuild.logUrl}`;
630
- logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
636
+ await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
631
637
 
632
638
  let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
633
639
  try {
@@ -635,7 +641,7 @@ async function triggerCloudBuild(
635
641
  const loggingClient = new Logging({ projectId });
636
642
 
637
643
  const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
638
- logAndProgress(
644
+ await logAndProgress(
639
645
  `Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
640
646
  progressCallback,
641
647
  'debug'
@@ -662,14 +668,14 @@ async function triggerCloudBuild(
662
668
  const logLines = entries.reverse().map((entry) => entry.data || '');
663
669
  if (logLines.length > 0) {
664
670
  buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
665
- logAndProgress(
671
+ await logAndProgress(
666
672
  `Successfully fetched snippet of build logs for ${buildId}.`,
667
673
  progressCallback,
668
674
  'info'
669
675
  );
670
676
  }
671
677
  } else {
672
- logAndProgress(
678
+ await logAndProgress(
673
679
  `No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
674
680
  progressCallback,
675
681
  'warn'
@@ -677,7 +683,7 @@ async function triggerCloudBuild(
677
683
  }
678
684
  } catch (logError) {
679
685
  console.error(`Error fetching build logs for ${buildId}:`, logError);
680
- logAndProgress(
686
+ await logAndProgress(
681
687
  `Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
682
688
  progressCallback,
683
689
  'warn'
@@ -710,14 +716,21 @@ export async function deploy({
710
716
  if (!projectId) {
711
717
  const errorMsg =
712
718
  'Error: projectId is required in the configuration object.';
713
- logAndProgress(errorMsg, progressCallback, 'error');
714
- process.exit(1);
719
+ await logAndProgress(errorMsg, progressCallback, 'error');
720
+ throw new Error(errorMsg);
721
+ }
722
+
723
+ if (!serviceName) {
724
+ const errorMsg =
725
+ 'Error: serviceName is required in the configuration object.';
726
+ await logAndProgress(errorMsg, progressCallback, 'error');
727
+ throw new Error(errorMsg);
715
728
  }
716
729
 
717
730
  if (!files || !Array.isArray(files) || files.length === 0) {
718
731
  const errorMsg =
719
732
  'Error: files array is required in the configuration object.';
720
- logAndProgress(errorMsg, progressCallback, 'error');
733
+ await logAndProgress(errorMsg, progressCallback, 'error');
721
734
  if (typeof process !== 'undefined' && process.exit) {
722
735
  process.exit(1);
723
736
  } else {
@@ -750,10 +763,10 @@ export async function deploy({
750
763
  const bucketName = `${projectId}-source-bucket`;
751
764
  const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
752
765
 
753
- logAndProgress(`Project: ${projectId}`, progressCallback);
754
- logAndProgress(`Region: ${region}`, progressCallback);
755
- logAndProgress(`Service Name: ${serviceName}`, progressCallback);
756
- logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
766
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
767
+ await logAndProgress(`Region: ${region}`, progressCallback);
768
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
769
+ await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
757
770
 
758
771
  let hasDockerfile = false;
759
772
  if (
@@ -786,7 +799,7 @@ export async function deploy({
786
799
  }
787
800
  }
788
801
  }
789
- logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
802
+ await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
790
803
 
791
804
  const bucket = await ensureStorageBucketExists(
792
805
  bucketName,
@@ -801,7 +814,7 @@ export async function deploy({
801
814
  ZIP_FILE_NAME,
802
815
  progressCallback
803
816
  );
804
- logAndProgress('Source code uploaded successfully', progressCallback);
817
+ await logAndProgress('Source code uploaded successfully', progressCallback);
805
818
 
806
819
  await ensureArtifactRegistryRepoExists(
807
820
  projectId,
@@ -833,12 +846,12 @@ export async function deploy({
833
846
  skipIamCheck
834
847
  );
835
848
 
836
- logAndProgress(`Deployment Completed Successfully`, progressCallback);
849
+ await logAndProgress(`Deployment Completed Successfully`, progressCallback);
837
850
  return service;
838
851
  } catch (error) {
839
852
  const deployFailedMessage = `Deployment Failed: ${error.message}`;
840
853
  console.error(`Deployment Failed`, error);
841
- logAndProgress(deployFailedMessage, progressCallback, 'error');
854
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
842
855
  throw error;
843
856
  }
844
857
  }
@@ -866,13 +879,20 @@ export async function deployImage({
866
879
  if (!projectId) {
867
880
  const errorMsg =
868
881
  'Error: projectId is required in the configuration object.';
869
- logAndProgress(errorMsg, progressCallback, 'error');
870
- process.exit(1);
882
+ await logAndProgress(errorMsg, progressCallback, 'error');
883
+ throw new Error(errorMsg);
884
+ }
885
+
886
+ if (!serviceName) {
887
+ const errorMsg =
888
+ 'Error: serviceName is required in the configuration object.';
889
+ await logAndProgress(errorMsg, progressCallback, 'error');
890
+ throw new Error(errorMsg);
871
891
  }
872
892
 
873
893
  if (!imageUrl) {
874
894
  const errorMsg = 'Error: imageUrl is required in the configuration object.';
875
- logAndProgress(errorMsg, progressCallback, 'error');
895
+ await logAndProgress(errorMsg, progressCallback, 'error');
876
896
  if (typeof process !== 'undefined' && process.exit) {
877
897
  process.exit(1);
878
898
  } else {
@@ -892,10 +912,10 @@ export async function deployImage({
892
912
 
893
913
  runClient = new ServicesClient({ projectId });
894
914
 
895
- logAndProgress(`Project: ${projectId}`, progressCallback);
896
- logAndProgress(`Region: ${region}`, progressCallback);
897
- logAndProgress(`Service Name: ${serviceName}`, progressCallback);
898
- logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
915
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
916
+ await logAndProgress(`Region: ${region}`, progressCallback);
917
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
918
+ await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
899
919
 
900
920
  const service = await deployToCloudRun(
901
921
  projectId,
@@ -906,12 +926,12 @@ export async function deployImage({
906
926
  skipIamCheck
907
927
  );
908
928
 
909
- logAndProgress(`Deployment Completed Successfully`, progressCallback);
929
+ await logAndProgress(`Deployment Completed Successfully`, progressCallback);
910
930
  return service;
911
931
  } catch (error) {
912
932
  const deployFailedMessage = `Deployment Failed: ${error.message}`;
913
933
  console.error(`Deployment Failed`, error);
914
- logAndProgress(deployFailedMessage, progressCallback, 'error');
934
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
915
935
  throw error;
916
936
  }
917
937
  }
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.3.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",
@@ -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
+ });