@google-cloud/cloud-run-mcp 1.10.0 → 1.12.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.
Files changed (52) hide show
  1. package/.github/workflows/npm-publish.yml +10 -6
  2. package/README.md +26 -0
  3. package/SECURITY.md +7 -0
  4. package/cloudbuild.yaml +37 -0
  5. package/example-sources-to-deploy/java/pom.xml +41 -0
  6. package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
  7. package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
  8. package/example-sources-to-deploy/nodejs/index.js +15 -0
  9. package/example-sources-to-deploy/nodejs/package.json +12 -0
  10. package/example-sources-to-deploy/python/pip-project/main.py +11 -0
  11. package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
  12. package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
  13. package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
  14. package/lib/clients.js +142 -12
  15. package/lib/cloud-api/build.js +192 -28
  16. package/lib/cloud-api/run.js +62 -0
  17. package/lib/cloud-api/secrets.js +196 -0
  18. package/lib/cloud-api/storage.js +164 -16
  19. package/lib/deployment/compose.js +517 -0
  20. package/lib/deployment/constants.js +8 -0
  21. package/lib/deployment/deployer.js +283 -28
  22. package/lib/deployment/helpers.js +58 -1
  23. package/lib/deployment/universal-maker.js +21 -148
  24. package/lib/middleware/oauth.js +26 -15
  25. package/lib/util/archive.js +3 -3
  26. package/lib/util/artifacts.js +189 -0
  27. package/lib/util/helpers.js +109 -0
  28. package/mcp-server.js +83 -20
  29. package/package.json +7 -4
  30. package/skills/cloud-run/SKILL.md +109 -0
  31. package/test/local/archive.test.js +88 -0
  32. package/test/local/clients.test.js +307 -2
  33. package/test/local/cloud-api/build.test.js +330 -25
  34. package/test/local/cloud-api/run.test.js +129 -0
  35. package/test/local/cloud-api/secrets.test.js +291 -0
  36. package/test/local/cloud-api/storage.test.js +405 -0
  37. package/test/local/compose.test.js +618 -0
  38. package/test/local/deploy.test.js +408 -0
  39. package/test/local/deployment-helpers.test.js +100 -1
  40. package/test/local/mcp-server-streamable-http.test.js +132 -12
  41. package/test/local/middleware/oauth.test.js +106 -5
  42. package/test/local/test-utils.js +2 -18
  43. package/test/local/tools.test.js +43 -1
  44. package/test/local/universal-maker.test.js +23 -114
  45. package/test/local/util.test.js +361 -0
  46. package/test/need-gcp/deployer.test.js +1 -1
  47. package/test/need-gcp/test-helpers.js +5 -1
  48. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  49. package/tools/register-tools.js +98 -78
  50. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  51. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  52. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -4,19 +4,23 @@ on:
4
4
  release:
5
5
  types: [published]
6
6
 
7
+ permissions:
8
+ contents: write
9
+
7
10
  jobs:
8
11
  publish-npm:
9
12
  runs-on: ubuntu-latest
10
13
  steps:
11
- - uses: actions/checkout@v4
12
- - uses: actions/setup-node@v4
14
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
15
+ with:
16
+ persist-credentials: false
17
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
13
18
  with:
14
- node-version: 20
19
+ node-version: 24
15
20
  - run: npm ci
16
- # Now configure with the publish service for install.
17
- - uses: actions/setup-node@v4
21
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
18
22
  with:
19
- node-version: 20
23
+ node-version: 24
20
24
  registry-url: https://wombat-dressing-room.appspot.com/
21
25
  - run: npm publish
22
26
  env:
package/README.md CHANGED
@@ -60,6 +60,8 @@ The Cloud Run MCP server can be configured using the following environment varia
60
60
  | `SKIP_IAM_CHECK` | Controls whether to check for IAM permissions for a Cloud Run service. Set to `false` to enable checks. This is `true` by default which is a recommended way to make the service public. |
61
61
  | `ENABLE_HOST_VALIDATION` | Prevents [DNS Rebinding](https://en.wikipedia.org/wiki/DNS_rebinding) attacks by validating the Host header. This is disabled by default. |
62
62
  | `ALLOWED_HOSTS` | Comma-separated list of allowed Host headers (if host validation is enabled). The default value is `localhost,127.0.0.1,::1`. |
63
+ | `OAUTH_ENABLED` | Enables OAuth 2.0 Bearer token verification for incoming HTTP/SSE requests when set to `true`. Disabled by default. |
64
+ | `GOOGLE_OAUTH_AUDIENCE` | Expected OAuth 2.0 token audience (typically your OAuth Client ID). Required when `OAUTH_ENABLED=true`. |
63
65
 
64
66
  ## Use as a Gemini CLI extension
65
67
 
@@ -274,3 +276,27 @@ Post the configuration changes as shown above, start the Gemini CLI. You should
274
276
  This will take you to the authentication page on your browser, wherein you need to sign in using the appropriate gmail id, and accept the terms and conditions. Once the authentication is succcessful, you can come back to the Gemini CLI, and the Cloud Run MCP server will be ready to use.
275
277
 
276
278
  The Google Cloud Platform Terms of Service (available at https://cloud.google.com/terms/) and the Data Processing and Security Terms (available at https://cloud.google.com/terms/data-processing-terms) do not apply to any component of the Cloud Run MCP Server software.
279
+
280
+ # Cloud Run Skills
281
+
282
+ We introduce Cloud Run skills to enable AI agents to perform actions on Cloud Run. You can use these skills with Gemini CLI and other agentic AI tools. The skills are available at [Cloud Run Skills](https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/skills/cloud-run/SKILL.md).
283
+
284
+ The Cloud Run skills are based on top of gcloud cli for Cloud Run empowering agents to perform all the actions on the Cloud Run using gcloud, as can be performed by the GCP user using gcloud cli.
285
+
286
+ In order to use Cloud Run skills:
287
+
288
+ 1. Ensure you have the `gcloud` CLI installed and authenticated with `gcloud auth login` and `gcloud auth application-default login`.
289
+ 2. Set your project with `gcloud config set project [PROJECT_ID]`.
290
+ 3. Enable the skills on your agentic AI tool. For example, you can enable the skills for Gemini CLI using the following command on your terminal:
291
+
292
+ ```bash
293
+ gemini skills install https://github.com/GoogleCloudPlatform/cloud-run-mcp.git --path skills/cloud-run
294
+ ```
295
+
296
+ 4. Once the skills are enabled, you can use them to perform actions on Cloud Run. Here are some of the prompts for you to get started:
297
+
298
+ - List the Cloud Run services in the project test-gcp-project in the region us-west1.
299
+
300
+ - Deploy the folder /home/username/workspace/hello-world as Cloud Run service hello-world to the project test-gcp-project in the region us-west1.
301
+
302
+ - Describe the Cloud Run job hello-job in the project test-gcp-project in the region europe-west1.
package/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
4
+
5
+ The Google Security Team will respond within 5 working days of your report on [g.co/vulnz](https://g.co/vulnz).
6
+
7
+ We use [g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
@@ -0,0 +1,37 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Default cloudbuild.yaml for Cloud Shell custom images.
16
+ # This file is used by the Cloud Build trigger that builds this image.
17
+
18
+ substitutions:
19
+ _IMAGE_TAG: '${COMMIT_SHA}'
20
+
21
+ steps:
22
+ - name: 'gcr.io/cloud-builders/docker'
23
+ args:
24
+ [
25
+ 'build',
26
+ '-t',
27
+ 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:latest',
28
+ '-t',
29
+ 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:public-image-${_IMAGE_TAG}',
30
+ '.',
31
+ ]
32
+ images:
33
+ - 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:latest'
34
+ - 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:public-image-${_IMAGE_TAG}'
35
+
36
+ options:
37
+ dynamicSubstitutions: true
@@ -0,0 +1,41 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4
+ <modelVersion>4.0.0</modelVersion>
5
+ <parent>
6
+ <groupId>org.springframework.boot</groupId>
7
+ <artifactId>spring-boot-starter-parent</artifactId>
8
+ <version>3.2.2</version>
9
+ <relativePath/> <!-- lookup parent from repository -->
10
+ </parent>
11
+ <groupId>com.example</groupId>
12
+ <artifactId>helloworld</artifactId>
13
+ <version>0.0.1-SNAPSHOT</version>
14
+ <name>helloworld</name>
15
+ <description>Demo project for Spring Boot</description>
16
+ <properties>
17
+ <java.version>17</java.version>
18
+ </properties>
19
+ <dependencies>
20
+ <dependency>
21
+ <groupId>org.springframework.boot</groupId>
22
+ <artifactId>spring-boot-starter-web</artifactId>
23
+ </dependency>
24
+
25
+ <dependency>
26
+ <groupId>org.springframework.boot</groupId>
27
+ <artifactId>spring-boot-starter-test</artifactId>
28
+ <scope>test</scope>
29
+ </dependency>
30
+ </dependencies>
31
+
32
+ <build>
33
+ <plugins>
34
+ <plugin>
35
+ <groupId>org.springframework.boot</groupId>
36
+ <artifactId>spring-boot-maven-plugin</artifactId>
37
+ </plugin>
38
+ </plugins>
39
+ </build>
40
+
41
+ </project>
@@ -0,0 +1,21 @@
1
+ package com.example.helloworld;
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+ import org.springframework.web.bind.annotation.GetMapping;
6
+ import org.springframework.web.bind.annotation.RestController;
7
+
8
+ @SpringBootApplication
9
+ @RestController
10
+ public class HelloworldApplication {
11
+
12
+ public static void main(String[] args) {
13
+ SpringApplication.run(HelloworldApplication.class, args);
14
+ }
15
+
16
+ @GetMapping("/")
17
+ public String hello() {
18
+ return "Hello from Spring Boot on Cloud Run!";
19
+ }
20
+
21
+ }
@@ -0,0 +1,15 @@
1
+ const express = require('express');
2
+ const app = express();
3
+ const port = process.env.PORT || 8080;
4
+
5
+ app.get('/', (req, res) => {
6
+ res.send('Hello from Node.js on Cloud Run!');
7
+ });
8
+
9
+ app.get('/health', (req, res) => {
10
+ res.status(200).send('OK');
11
+ });
12
+
13
+ app.listen(port, () => {
14
+ console.log(`Nodejs sample app listening at http://localhost:${port}`);
15
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nodejs-sample-app",
3
+ "version": "1.0.0",
4
+ "description": "A simple Node.js sample application for Cloud Run",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.18.2"
11
+ }
12
+ }
@@ -0,0 +1,11 @@
1
+ from flask import Flask, request
2
+
3
+ app = Flask(__name__)
4
+
5
+ @app.route('/')
6
+ def hello():
7
+ user = request.args.get('user', 'World')
8
+ return f'Hello {user}'
9
+
10
+ if __name__ == '__main__':
11
+ app.run(host='0.0.0.0', port=8080)
@@ -0,0 +1,11 @@
1
+ from flask import Flask, request
2
+
3
+ app = Flask(__name__)
4
+
5
+ @app.route('/')
6
+ def hello_world():
7
+ user = request.args.get('user', 'World')
8
+ return f'Hello {user}'
9
+
10
+ if __name__ == '__main__':
11
+ app.run(host='0.0.0.0', port=8080)
@@ -0,0 +1,10 @@
1
+ [project]
2
+ name = "pyproject-project"
3
+ version = "0.1.0"
4
+ dependencies = [
5
+ "Flask",
6
+ "gunicorn",
7
+ ]
8
+ [build-system]
9
+ requires = ["setuptools>=61.0"]
10
+ build-backend = "setuptools.build_meta"
package/lib/clients.js CHANGED
@@ -22,12 +22,30 @@ const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
22
22
  const REDIRECT_URI = process.env.GOOGLE_OAUTH_REDIRECT_URI;
23
23
  const AUTHORIZATION_HEADER = 'Authorization';
24
24
  const BEARER_PREFIX = 'Bearer';
25
+ const CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
25
26
 
26
- function keyGenerator(projectId, accessToken) {
27
- return accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
27
+ export function getDefaultAccessToken() {
28
+ return process.env.OAUTH_ENABLED === 'true' ? undefined : GCLOUD_AUTH;
28
29
  }
29
30
 
30
- const clients = {
31
+ export function validateAccessToken(accessToken) {
32
+ if (
33
+ process.env.OAUTH_ENABLED === 'true' &&
34
+ (!accessToken || accessToken === GCLOUD_AUTH)
35
+ ) {
36
+ throw new Error('Access token is required when OAuth is enabled.');
37
+ }
38
+ }
39
+
40
+ function keyGenerator(projectId, accessToken, region = '') {
41
+ const baseKey =
42
+ accessToken && accessToken !== GCLOUD_AUTH
43
+ ? projectId + accessToken
44
+ : projectId;
45
+ return baseKey + (region || '');
46
+ }
47
+
48
+ export const clients = {
31
49
  run: new Map(),
32
50
  builds: new Map(),
33
51
  serviceUsage: new Map(),
@@ -37,7 +55,9 @@ const clients = {
37
55
  logging: new Map(),
38
56
  billing: new Map(),
39
57
  projects: new Map(),
58
+ runV1: new Map(),
40
59
  oauth: new Map(),
60
+ secretManager: new Map(),
41
61
  };
42
62
 
43
63
  function getAuthClient(accessToken) {
@@ -98,6 +118,7 @@ export async function getClient(
98
118
  options = {},
99
119
  accessToken = null
100
120
  ) {
121
+ validateAccessToken(accessToken);
101
122
  if (!clients[service].has(key)) {
102
123
  const ClientClass = await loadClient();
103
124
  const finalOptions = { ...options };
@@ -121,7 +142,10 @@ export async function getClient(
121
142
  * @param {string} projectId - The Google Cloud project ID.
122
143
  * @returns {Promise<import('@google-cloud/run').v2.ServicesClient>}
123
144
  */
124
- export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
145
+ export async function getRunClient(
146
+ projectId,
147
+ accessToken = getDefaultAccessToken()
148
+ ) {
125
149
  const key = keyGenerator(projectId, accessToken);
126
150
  return getClient(
127
151
  'run',
@@ -137,7 +161,10 @@ export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
137
161
  * @param {string} projectId - The Google Cloud project ID.
138
162
  * @returns {Promise<import('@google-cloud/run').v2.BuildsClient>}
139
163
  */
140
- export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
164
+ export async function getBuildsClient(
165
+ projectId,
166
+ accessToken = getDefaultAccessToken()
167
+ ) {
141
168
  const key = keyGenerator(projectId, accessToken);
142
169
  return getClient(
143
170
  'builds',
@@ -155,7 +182,7 @@ export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
155
182
  */
156
183
  export async function getServiceUsageClient(
157
184
  projectId,
158
- accessToken = GCLOUD_AUTH
185
+ accessToken = getDefaultAccessToken()
159
186
  ) {
160
187
  const key = keyGenerator(projectId, accessToken);
161
188
  return getClient(
@@ -173,7 +200,10 @@ export async function getServiceUsageClient(
173
200
  * @param {string} projectId - The Google Cloud project ID.
174
201
  * @returns {Promise<import('@google-cloud/storage').Storage>}
175
202
  */
176
- export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
203
+ export async function getStorageClient(
204
+ projectId,
205
+ accessToken = getDefaultAccessToken()
206
+ ) {
177
207
  const key = keyGenerator(projectId, accessToken);
178
208
  return getClient(
179
209
  'storage',
@@ -191,7 +221,7 @@ export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
191
221
  */
192
222
  export async function getCloudBuildClient(
193
223
  projectId,
194
- accessToken = GCLOUD_AUTH
224
+ accessToken = getDefaultAccessToken()
195
225
  ) {
196
226
  const key = keyGenerator(projectId, accessToken);
197
227
  return getClient(
@@ -210,7 +240,7 @@ export async function getCloudBuildClient(
210
240
  */
211
241
  export async function getArtifactRegistryClient(
212
242
  projectId,
213
- accessToken = GCLOUD_AUTH
243
+ accessToken = getDefaultAccessToken()
214
244
  ) {
215
245
  const key = keyGenerator(projectId, accessToken);
216
246
  return getClient(
@@ -228,7 +258,10 @@ export async function getArtifactRegistryClient(
228
258
  * @param {string} projectId - The Google Cloud project ID.
229
259
  * @returns {Promise<import('@google-cloud/logging').Logging>}
230
260
  */
231
- export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
261
+ export async function getLoggingClient(
262
+ projectId,
263
+ accessToken = getDefaultAccessToken()
264
+ ) {
232
265
  const key = keyGenerator(projectId, accessToken);
233
266
  return getClient(
234
267
  'logging',
@@ -248,7 +281,7 @@ export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
248
281
  */
249
282
  export async function getBillingClient(
250
283
  projectId = 'global',
251
- accessToken = GCLOUD_AUTH
284
+ accessToken = getDefaultAccessToken()
252
285
  ) {
253
286
  const key = keyGenerator(projectId, accessToken);
254
287
  return getClient(
@@ -264,7 +297,7 @@ export async function getBillingClient(
264
297
  * Gets a Projects Client (Resource Manager).
265
298
  * @returns {Promise<import('@google-cloud/resource-manager').ProjectsClient>}
266
299
  */
267
- export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
300
+ export async function getProjectsClient(accessToken = getDefaultAccessToken()) {
268
301
  const key = keyGenerator('global', accessToken);
269
302
  return getClient(
270
303
  'projects',
@@ -275,6 +308,26 @@ export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
275
308
  );
276
309
  }
277
310
 
311
+ /**
312
+ * Gets a Secret Manager Client for the specified project.
313
+ * @param {string} projectId - The Google Cloud project ID.
314
+ * @returns {Promise<import('@google-cloud/secret-manager').SecretManagerServiceClient>}
315
+ */
316
+ export async function getSecretManagerClient(
317
+ projectId,
318
+ accessToken = getDefaultAccessToken()
319
+ ) {
320
+ const key = keyGenerator(projectId, accessToken);
321
+ return getClient(
322
+ 'secretManager',
323
+ key,
324
+ async () =>
325
+ (await import('@google-cloud/secret-manager')).SecretManagerServiceClient,
326
+ { projectId },
327
+ accessToken
328
+ );
329
+ }
330
+
278
331
  /**
279
332
  * Gets an OAuth2 Client for the specified access token.
280
333
  * @param {string} accessToken - The access token.
@@ -290,3 +343,80 @@ export async function getOAuthClient(accessToken) {
290
343
 
291
344
  return clients.oauth.get(accessToken);
292
345
  }
346
+
347
+ /**
348
+ * Gets a Cloud Run v1 Client (from googleapis).
349
+ * @param {string} projectId - The Google Cloud project ID.
350
+ * @param {string} accessToken - Access token for authentication.
351
+ * @param {string} [region] - The Google Cloud region (optional).
352
+ * @returns {Promise<import('googleapis').run_v1.Run>}
353
+ */
354
+ export async function getRunV1Client(
355
+ projectId,
356
+ accessToken = getDefaultAccessToken(),
357
+ region = null
358
+ ) {
359
+ validateAccessToken(accessToken);
360
+ const key = keyGenerator(projectId, accessToken, region);
361
+ if (!clients.runV1.has(key)) {
362
+ const { google } = await import('googleapis');
363
+ const options = { version: 'v1' };
364
+
365
+ if (accessToken && accessToken !== GCLOUD_AUTH) {
366
+ options.auth = await getOAuthClient(accessToken);
367
+ } else {
368
+ // Use Application Default Credentials for GCLOUD_AUTH
369
+ const auth = new google.auth.GoogleAuth({
370
+ scopes: [CLOUD_PLATFORM_SCOPE],
371
+ projectId: projectId,
372
+ });
373
+ options.auth = auth;
374
+ }
375
+
376
+ if (region) {
377
+ const availableRegions = await getCloudRunRegions(projectId, accessToken);
378
+ if (!availableRegions.includes(region)) {
379
+ throw new Error(`Invalid region: ${region}.`);
380
+ }
381
+ options.rootUrl = `https://${region}-run.googleapis.com/`;
382
+ }
383
+ clients.runV1.set(key, google.run(options));
384
+ }
385
+ return clients.runV1.get(key);
386
+ }
387
+
388
+ let cachedRegions = null;
389
+
390
+ /**
391
+ * Resets the cached regions. Used for testing.
392
+ */
393
+ export function resetCachedRegions() {
394
+ cachedRegions = null;
395
+ }
396
+
397
+ /**
398
+ * Returns a list of supported Cloud Run regions by fetching them from Compute Engine API.
399
+ * This is the SDK equivalent of `gcloud compute regions list`.
400
+ * Note: This is not moved to utils as it is required by getRunV1Client. Moving it to utils
401
+ * would lead to circular dependency.
402
+ * @param {string} projectId - The project ID to fetch regions for.
403
+ * @param {string} accessToken - The access token for authentication.
404
+ * @returns {Promise<string[]>}
405
+ */
406
+ export async function getCloudRunRegions(projectId, accessToken) {
407
+ if (cachedRegions) {
408
+ return cachedRegions;
409
+ }
410
+ const runClient = await getRunClient(projectId, accessToken);
411
+ const iterable = runClient.listLocationsAsync({
412
+ name: `projects/${projectId}`,
413
+ });
414
+
415
+ const regions = [];
416
+ for await (const region of iterable) {
417
+ regions.push(region.locationId);
418
+ }
419
+
420
+ cachedRegions = regions;
421
+ return cachedRegions;
422
+ }