@google-cloud/cloud-run-mcp 1.2.0 → 1.3.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.
package/CONTRIBUTING.md CHANGED
@@ -112,8 +112,10 @@ To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run
112
112
 
113
113
  Run the following:
114
114
 
115
+ * `git checkout -b new-release`
115
116
  * `npm test`
116
117
  * `npm version minor`
117
118
  * `git push --follow-tags`
119
+ * On GitHub, [create a new release](https://github.com/GoogleCloudPlatform/cloud-run-mcp/releases/new) for this tag.
118
120
 
119
121
  Then a [GitHub Action]https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/.github/workflows/npm-publish.yml) automatically publishes to npm when new releases are pushes.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Enable MCP-compatible AI agents to deploy apps to Cloud Run.
6
6
  "mcpServers":{
7
7
  "cloud-run": {
8
8
  "command": "npx",
9
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
9
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
10
10
  }
11
11
  }
12
12
  ```
@@ -72,7 +72,7 @@ Then configure the MCP server using either Node.js or Docker:
72
72
  ```json
73
73
  "cloud-run": {
74
74
  "command": "npx",
75
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
75
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
76
76
  }
77
77
  ```
78
78
 
@@ -81,7 +81,7 @@ Then configure the MCP server using either Node.js or Docker:
81
81
  ```json
82
82
  "cloud-run": {
83
83
  "command": "npx",
84
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"],
84
+ "args": ["-y", "@google-cloud/cloud-run-mcp"],
85
85
  "env": {
86
86
  "GOOGLE_CLOUD_PROJECT": "PROJECT_NAME",
87
87
  "GOOGLE_CLOUD_REGION": "PROJECT_REGION",
@@ -4,7 +4,7 @@
4
4
  "mcpServers": {
5
5
  "cloud-run": {
6
6
  "command": "npx",
7
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
7
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
8
8
  }
9
9
  },
10
10
  "contextFileName": "gemini-extension/GEMINI.md"
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
18
+
17
19
  // Configuration
18
20
  const REPO_NAME = 'mcp-cloud-run-deployments';
19
21
  const ZIP_FILE_NAME = 'source.zip';
@@ -63,91 +65,6 @@ function logAndProgress(message, progressCallback, severity = 'info') {
63
65
  }
64
66
  }
65
67
 
66
- /**
67
- * Checks if a single Google Cloud API is enabled and enables it if not.
68
- *
69
- * @param {object} serviceUsageClient - The Service Usage client.
70
- * @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
71
- * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
72
- * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
73
- * @returns {Promise<void>} A promise that resolves when the API is enabled.
74
- * @throws {Error} If the API fails to enable or if there's an issue checking its status.
75
- */
76
- async function checkAndEnableApi(
77
- serviceUsageClient,
78
- serviceName,
79
- api,
80
- progressCallback
81
- ) {
82
- const [service] = await serviceUsageClient.getService({
83
- name: serviceName,
84
- });
85
- if (service.state !== 'ENABLED') {
86
- logAndProgress(
87
- `API [${api}] is not enabled. Enabling...`,
88
- progressCallback
89
- );
90
- const [operation] = await serviceUsageClient.enableService({
91
- name: serviceName,
92
- });
93
- await operation.promise();
94
- }
95
- }
96
-
97
- /**
98
- * Ensures that the specified Google Cloud APIs are enabled for the given project.
99
- * If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
100
- * Throws an error if an API cannot be enabled.
101
- *
102
- * @async
103
- * @param {string} projectId - The Google Cloud project ID.
104
- * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
105
- * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
106
- * The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
107
- * @throws {Error} If an API fails to enable or if there's an issue checking its status.
108
- * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
109
- */
110
- async function ensureApisEnabled(projectId, apis, progressCallback) {
111
- const { ServiceUsageClient } = await import('@google-cloud/service-usage');
112
- const serviceUsageClient = new ServiceUsageClient({ projectId });
113
- logAndProgress('Checking and enabling required APIs...', progressCallback);
114
-
115
- for (const api of apis) {
116
- const serviceName = `projects/${projectId}/services/${api}`;
117
- try {
118
- await checkAndEnableApi(
119
- serviceUsageClient,
120
- serviceName,
121
- api,
122
- progressCallback
123
- );
124
- } catch (error) {
125
- // First attempt failed, log a warning and retry once after a delay.
126
- logAndProgress(
127
- `Failed to check/enable ${api}, retrying in 1s...`,
128
- progressCallback,
129
- 'warn'
130
- );
131
- await new Promise((resolve) => setTimeout(resolve, 1000));
132
- try {
133
- await checkAndEnableApi(
134
- serviceUsageClient,
135
- serviceName,
136
- api,
137
- progressCallback
138
- );
139
- } catch (retryError) {
140
- // If the retry also fails, throw an error.
141
- const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
142
- console.error(errorMessage, retryError);
143
- logAndProgress(errorMessage, progressCallback, 'error');
144
- throw new Error(errorMessage);
145
- }
146
- }
147
- }
148
- logAndProgress('All required APIs are enabled.', progressCallback);
149
- }
150
-
151
68
  /**
152
69
  * Checks if a Cloud Run service already exists.
153
70
  *
@@ -168,7 +85,10 @@ async function checkCloudRunServiceExists(
168
85
  const parent = runClient.locationPath(projectId, location);
169
86
  const servicePath = runClient.servicePath(projectId, location, serviceId);
170
87
  try {
171
- await runClient.getService({ name: servicePath });
88
+ await callWithRetry(
89
+ () => runClient.getService({ name: servicePath }),
90
+ `getService ${serviceId}`
91
+ );
172
92
  logAndProgress(
173
93
  `Cloud Run service ${serviceId} already exists.`,
174
94
  progressCallback
@@ -249,17 +169,25 @@ async function deployToCloudRun(
249
169
 
250
170
  if (exists) {
251
171
  dryRunServiceConfig.name = servicePath;
252
- await runClient.updateService({
253
- service: dryRunServiceConfig,
254
- validateOnly: true,
255
- });
172
+ await callWithRetry(
173
+ () =>
174
+ runClient.updateService({
175
+ service: dryRunServiceConfig,
176
+ validateOnly: true,
177
+ }),
178
+ `updateService (dry run) ${serviceId}`
179
+ );
256
180
  } else {
257
- await runClient.createService({
258
- parent: parent,
259
- service: dryRunServiceConfig,
260
- serviceId: serviceId,
261
- validateOnly: true,
262
- });
181
+ await callWithRetry(
182
+ () =>
183
+ runClient.createService({
184
+ parent: parent,
185
+ service: dryRunServiceConfig,
186
+ serviceId: serviceId,
187
+ validateOnly: true,
188
+ }),
189
+ `createService (dry run) ${serviceId}`
190
+ );
263
191
  }
264
192
  logAndProgress(
265
193
  `Dry run successful for ${serviceId} with current configuration.`,
@@ -302,14 +230,21 @@ async function deployToCloudRun(
302
230
  progressCallback
303
231
  );
304
232
  service.name = servicePath;
305
- [operation] = await runClient.updateService({ service });
233
+ [operation] = await callWithRetry(
234
+ () => runClient.updateService({ service }),
235
+ `updateService ${serviceId}`
236
+ );
306
237
  } else {
307
238
  logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
308
- [operation] = await runClient.createService({
309
- parent: parent,
310
- service: service, // 'service' object might have invokerIamDisabled removed
311
- serviceId: serviceId,
312
- });
239
+ [operation] = await callWithRetry(
240
+ () =>
241
+ runClient.createService({
242
+ parent: parent,
243
+ service: service, // 'service' object might have invokerIamDisabled removed
244
+ serviceId: serviceId,
245
+ }),
246
+ `createService ${serviceId}`
247
+ );
313
248
  }
314
249
 
315
250
  logAndProgress(`Deploying ${serviceId} to Cloud Run...`, progressCallback);
@@ -346,7 +281,10 @@ async function ensureStorageBucketExists(
346
281
  ) {
347
282
  const bucket = storage.bucket(bucketName);
348
283
  try {
349
- const [exists] = await bucket.exists();
284
+ const [exists] = await callWithRetry(
285
+ () => bucket.exists(),
286
+ `storage.bucket.exists ${bucketName}`
287
+ );
350
288
  if (exists) {
351
289
  logAndProgress(`Bucket ${bucketName} already exists.`, progressCallback);
352
290
  return bucket;
@@ -356,9 +294,10 @@ async function ensureStorageBucketExists(
356
294
  progressCallback
357
295
  );
358
296
  try {
359
- const [createdBucket] = await storage.createBucket(bucketName, {
360
- location: location,
361
- });
297
+ const [createdBucket] = await callWithRetry(
298
+ () => storage.createBucket(bucketName, { location: location }),
299
+ `storage.createBucket ${bucketName}`
300
+ );
362
301
  logAndProgress(
363
302
  `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
364
303
  progressCallback
@@ -482,7 +421,10 @@ async function uploadToStorageBucket(
482
421
  `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
483
422
  progressCallback
484
423
  );
485
- await bucket.file(destinationBlobName).save(buffer);
424
+ await callWithRetry(
425
+ () => bucket.file(destinationBlobName).save(buffer),
426
+ `storage.bucket.file.save ${destinationBlobName}`
427
+ );
486
428
  logAndProgress(
487
429
  `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
488
430
  progressCallback
@@ -524,9 +466,10 @@ async function ensureArtifactRegistryRepoExists(
524
466
  );
525
467
 
526
468
  try {
527
- const [repository] = await artifactRegistryClient.getRepository({
528
- name: repoPath,
529
- });
469
+ const [repository] = await callWithRetry(
470
+ () => artifactRegistryClient.getRepository({ name: repoPath }),
471
+ `artifactRegistry.getRepository ${repositoryId}`
472
+ );
530
473
  logAndProgress(
531
474
  `Repository ${repositoryId} already exists in ${location}.`,
532
475
  progressCallback
@@ -541,46 +484,26 @@ async function ensureArtifactRegistryRepoExists(
541
484
  const repositoryToCreate = {
542
485
  format: format,
543
486
  };
544
- const maxRetries = 3;
545
- const retryDelay = 10000; // 10s
546
487
 
547
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
548
- try {
549
- const [operation] = await artifactRegistryClient.createRepository({
488
+ const [operation] = await callWithRetry(
489
+ () =>
490
+ artifactRegistryClient.createRepository({
550
491
  parent: parent,
551
492
  repository: repositoryToCreate,
552
493
  repositoryId: repositoryId,
553
- });
554
- logAndProgress(
555
- `Creating Artifact Registry repository ${repositoryId}...`,
556
- progressCallback
557
- );
558
- const [result] = await operation.promise();
559
- logAndProgress(
560
- `Artifact Registry repository ${result.name} created successfully.`,
561
- progressCallback
562
- );
563
- return result;
564
- } catch (createError) {
565
- // PERMISSION_DENIED is code 7 for gRPC. Retry on this error.
566
- if (createError.code === 7 && attempt < maxRetries) {
567
- logAndProgress(
568
- `Attempt ${attempt} to create repository failed. This can happen on new projects. Retrying in 10s...`,
569
- progressCallback,
570
- 'warn'
571
- );
572
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
573
- } else {
574
- const errorMessage = `Failed to create Artifact Registry repository ${repositoryId}. Error details: ${createError.message}`;
575
- console.error(
576
- `Failed to create Artifact Registry repository ${repositoryId}. Error details:`,
577
- createError
578
- );
579
- logAndProgress(errorMessage, progressCallback, 'error');
580
- throw createError;
581
- }
582
- }
583
- }
494
+ }),
495
+ `artifactRegistry.createRepository ${repositoryId}`
496
+ );
497
+ logAndProgress(
498
+ `Creating Artifact Registry repository ${repositoryId}...`,
499
+ progressCallback
500
+ );
501
+ const [result] = await operation.promise();
502
+ logAndProgress(
503
+ `Artifact Registry repository ${result.name} created successfully.`,
504
+ progressCallback
505
+ );
506
+ return result;
584
507
  } else {
585
508
  const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
586
509
  console.error(
@@ -661,19 +584,19 @@ async function triggerCloudBuild(
661
584
  `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
662
585
  progressCallback
663
586
  );
664
- const [operation] = await cloudBuildClient.createBuild({
665
- projectId: projectId,
666
- build: build,
667
- });
587
+ const [operation] = await callWithRetry(
588
+ () => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
589
+ 'cloudBuild.createBuild'
590
+ );
668
591
 
669
592
  logAndProgress(`Cloud Build job started...`, progressCallback);
670
593
  const buildId = operation.metadata.build.id;
671
594
  let completedBuild;
672
595
  while (true) {
673
- const [getBuildOperation] = await cloudBuildClient.getBuild({
674
- projectId: projectId,
675
- id: buildId,
676
- });
596
+ const [getBuildOperation] = await callWithRetry(
597
+ () => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
598
+ `cloudBuild.getBuild ${buildId}`
599
+ );
677
600
  if (
678
601
  ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
679
602
  getBuildOperation.status
@@ -724,11 +647,15 @@ async function triggerCloudBuild(
724
647
  );
725
648
 
726
649
  // Fetch the most recent N log entries
727
- const [entries] = await loggingClient.getEntries({
728
- filter: logFilter,
729
- orderBy: 'timestamp desc', // Get latest logs first
730
- pageSize: BUILD_LOGS_LINES_TO_FETCH,
731
- });
650
+ const [entries] = await callWithRetry(
651
+ () =>
652
+ loggingClient.getEntries({
653
+ filter: logFilter,
654
+ orderBy: 'timestamp desc', // Get latest logs first
655
+ pageSize: BUILD_LOGS_LINES_TO_FETCH,
656
+ }),
657
+ `logging.getEntries for build ${buildId}`
658
+ );
732
659
 
733
660
  if (entries && entries.length > 0) {
734
661
  // Entries are newest first, reverse for chronological order of the snippet
@@ -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/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.3.0",
4
4
  "type": "module",
5
5
  "description": "Cloud Run MCP deployment tool",
6
6
  "main": "mcp-server.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
+ });
@@ -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
+ });
File without changes