@google-cloud/cloud-run-mcp 1.7.0 → 1.8.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,13 +15,14 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import protofiles from 'google-proto-files';
18
+ import {
19
+ getRunClient,
20
+ getServiceUsageClient,
21
+ getLoggingClient,
22
+ } from '../clients.js';
18
23
  import { callWithRetry, ensureApisEnabled } from './helpers.js';
19
24
  import { logAndProgress } from '../util/helpers.js';
20
25
 
21
- let runClient;
22
- let serviceUsageClient;
23
- let loggingClient;
24
-
25
26
  async function listCloudRunLocations(projectId) {
26
27
  const listLocationsRequest = {
27
28
  name: `projects/${projectId}`,
@@ -29,6 +30,7 @@ async function listCloudRunLocations(projectId) {
29
30
 
30
31
  const availableLocations = [];
31
32
  try {
33
+ const runClient = await getRunClient(projectId);
32
34
  console.log('Listing Cloud Run supported locations:');
33
35
  const iterable = runClient.listLocationsAsync(listLocationsRequest);
34
36
  for await (const location of iterable) {
@@ -50,18 +52,9 @@ async function listCloudRunLocations(projectId) {
50
52
  * @returns {Promise<object>} - A promise that resolves to an object mapping region to list of service objects in that region.
51
53
  */
52
54
  export async function listServices(projectId) {
53
- if (!runClient) {
54
- const { v2 } = await import('@google-cloud/run');
55
- const { ServicesClient } = v2;
56
- const { ServiceUsageClient } = await import('@google-cloud/service-usage');
57
- runClient = new ServicesClient({ projectId });
58
- serviceUsageClient = new ServiceUsageClient({ projectId });
59
- }
60
- const context = {
61
- runClient: runClient,
62
- serviceUsageClient: serviceUsageClient,
63
- };
64
- await ensureApisEnabled(context, projectId, ['run.googleapis.com']);
55
+ const runClient = await getRunClient(projectId);
56
+
57
+ await ensureApisEnabled(projectId, ['run.googleapis.com']);
65
58
  const locations = await listCloudRunLocations(projectId);
66
59
 
67
60
  const allServices = {};
@@ -93,11 +86,7 @@ export async function listServices(projectId) {
93
86
  * @returns {Promise<object>} - A promise that resolves to the service object.
94
87
  */
95
88
  export async function getService(projectId, location, serviceId) {
96
- if (!runClient) {
97
- const { v2 } = await import('@google-cloud/run');
98
- const { ServicesClient } = v2;
99
- runClient = new ServicesClient({ projectId });
100
- }
89
+ const runClient = await getRunClient(projectId);
101
90
 
102
91
  const servicePath = runClient.servicePath(projectId, location, serviceId);
103
92
 
@@ -138,10 +127,7 @@ export async function getServiceLogs(
138
127
  serviceId,
139
128
  requestOptions
140
129
  ) {
141
- if (!loggingClient) {
142
- const { Logging } = await import('@google-cloud/logging');
143
- loggingClient = new Logging({ projectId });
144
- }
130
+ const loggingClient = await getLoggingClient(projectId);
145
131
 
146
132
  try {
147
133
  const LOG_SEVERITY = 'DEFAULT'; // e.g., 'DEFAULT', 'INFO', 'WARNING', 'ERROR'
@@ -237,21 +223,16 @@ function formatLogEntry(entry) {
237
223
  * @throws {Error} If there's an error checking the service (other than not found).
238
224
  */
239
225
  export async function checkCloudRunServiceExists(
240
- context,
241
226
  projectId,
242
227
  location,
243
228
  serviceId,
244
229
  progressCallback
245
230
  ) {
246
- const parent = context.runClient.locationPath(projectId, location);
247
- const servicePath = context.runClient.servicePath(
248
- projectId,
249
- location,
250
- serviceId
251
- );
231
+ const runClient = await getRunClient(projectId);
232
+ const servicePath = runClient.servicePath(projectId, location, serviceId);
252
233
  try {
253
234
  await callWithRetry(
254
- () => context.runClient.getService({ name: servicePath }),
235
+ () => runClient.getService({ name: servicePath }),
255
236
  `getService ${serviceId}`
256
237
  );
257
238
  await logAndProgress(
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { getStorageClient } from '../clients.js';
17
18
  import { callWithRetry } from './helpers.js';
18
19
  import { logAndProgress } from '../util/helpers.js';
19
20
 
@@ -22,7 +23,7 @@ import { logAndProgress } from '../util/helpers.js';
22
23
  * If the bucket does not exist, it attempts to create it in the specified location.
23
24
  *
24
25
  * @async
25
- * @param {object} context - The context object containing clients and other parameters.
26
+ * @param {string} projectId - The Google Cloud project ID.
26
27
  * @param {string} bucketName - The name of the storage bucket.
27
28
  * @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
28
29
  * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
@@ -30,12 +31,13 @@ import { logAndProgress } from '../util/helpers.js';
30
31
  * @throws {Error} If there's an error checking or creating the bucket.
31
32
  */
32
33
  export async function ensureStorageBucketExists(
33
- context,
34
+ projectId,
34
35
  bucketName,
35
36
  location = 'us',
36
37
  progressCallback
37
38
  ) {
38
- const bucket = context.storage.bucket(bucketName);
39
+ const storage = await getStorageClient(projectId);
40
+ const bucket = storage.bucket(bucketName);
39
41
  try {
40
42
  const [exists] = await callWithRetry(
41
43
  () => bucket.exists(),
@@ -54,8 +56,7 @@ export async function ensureStorageBucketExists(
54
56
  );
55
57
  try {
56
58
  const [createdBucket] = await callWithRetry(
57
- () =>
58
- context.storage.createBucket(bucketName, { location: location }),
59
+ () => storage.createBucket(bucketName, { location: location }),
59
60
  `storage.createBucket ${bucketName}`
60
61
  );
61
62
  await logAndProgress(
@@ -85,7 +86,6 @@ export async function ensureStorageBucketExists(
85
86
  * Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
86
87
  *
87
88
  * @async
88
- * @param {object} context - The context object containing clients and other parameters.
89
89
  * @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
90
90
  * @param {Buffer} buffer - The buffer containing the data to upload.
91
91
  * @param {string} destinationBlobName - The name for the blob in the bucket.
@@ -94,7 +94,6 @@ export async function ensureStorageBucketExists(
94
94
  * @throws {Error} If the upload fails.
95
95
  */
96
96
  export async function uploadToStorageBucket(
97
- context,
98
97
  bucket,
99
98
  buffer,
100
99
  destinationBlobName,
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { getRunClient } from '../clients.js';
17
18
  import { callWithRetry, ensureApisEnabled } from '../cloud-api/helpers.js';
18
19
  import { zipFiles } from '../util/archive.js';
19
20
  import {
@@ -58,7 +59,6 @@ import {
58
59
  * @throws {Error} If the deployment or update process fails.
59
60
  */
60
61
  async function deployToCloudRun(
61
- context,
62
62
  projectId,
63
63
  location,
64
64
  serviceId,
@@ -69,12 +69,9 @@ async function deployToCloudRun(
69
69
  zippedSourceContainer,
70
70
  runtime
71
71
  ) {
72
- const parent = context.runClient.locationPath(projectId, location);
73
- const servicePath = context.runClient.servicePath(
74
- projectId,
75
- location,
76
- serviceId
77
- );
72
+ const runClient = await getRunClient(projectId);
73
+ const parent = runClient.locationPath(projectId, location);
74
+ const servicePath = runClient.servicePath(projectId, location, serviceId);
78
75
  const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
79
76
 
80
77
  const containers =
@@ -101,7 +98,6 @@ async function deployToCloudRun(
101
98
 
102
99
  try {
103
100
  const exists = await checkCloudRunServiceExists(
104
- context,
105
101
  projectId,
106
102
  location,
107
103
  serviceId,
@@ -121,7 +117,7 @@ async function deployToCloudRun(
121
117
  dryRunServiceConfig.name = servicePath;
122
118
  await callWithRetry(
123
119
  () =>
124
- context.runClient.updateService({
120
+ runClient.updateService({
125
121
  service: dryRunServiceConfig,
126
122
  validateOnly: true,
127
123
  }),
@@ -130,7 +126,7 @@ async function deployToCloudRun(
130
126
  } else {
131
127
  await callWithRetry(
132
128
  () =>
133
- context.runClient.createService({
129
+ runClient.createService({
134
130
  parent: parent,
135
131
  service: dryRunServiceConfig,
136
132
  serviceId: serviceId,
@@ -181,7 +177,7 @@ async function deployToCloudRun(
181
177
  );
182
178
  service.name = servicePath;
183
179
  [operation] = await callWithRetry(
184
- () => context.runClient.updateService({ service }),
180
+ () => runClient.updateService({ service }),
185
181
  `updateService ${serviceId}`
186
182
  );
187
183
  } else {
@@ -191,7 +187,7 @@ async function deployToCloudRun(
191
187
  );
192
188
  [operation] = await callWithRetry(
193
189
  () =>
194
- context.runClient.createService({
190
+ runClient.createService({
195
191
  parent: parent,
196
192
  service: service, // 'service' object might have invokerIamDisabled removed
197
193
  serviceId: serviceId,
@@ -242,7 +238,6 @@ async function deployToCloudRun(
242
238
  * @throws {Error} If any part of the deployment process fails.
243
239
  */
244
240
  async function deployWithZip(
245
- context,
246
241
  projectId,
247
242
  region,
248
243
  serviceName,
@@ -278,14 +273,13 @@ async function deployWithZip(
278
273
  }
279
274
 
280
275
  const bucket = await ensureStorageBucketExists(
281
- context,
276
+ projectId,
282
277
  bucketName,
283
278
  region,
284
279
  progressCallback
285
280
  );
286
281
 
287
282
  await uploadToStorageBucket(
288
- context,
289
283
  bucket,
290
284
  archiveBuffer,
291
285
  archiveName,
@@ -300,7 +294,6 @@ async function deployWithZip(
300
294
  });
301
295
 
302
296
  const service = await deployToCloudRun(
303
- context,
304
297
  projectId,
305
298
  region,
306
299
  serviceName,
@@ -337,7 +330,6 @@ async function deployWithZip(
337
330
  * @throws {Error} If any part of the build or deployment process fails.
338
331
  */
339
332
  async function deployWithBuild(
340
- context,
341
333
  projectId,
342
334
  region,
343
335
  serviceName,
@@ -350,7 +342,7 @@ async function deployWithBuild(
350
342
  const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
351
343
 
352
344
  const bucket = await ensureStorageBucketExists(
353
- context,
345
+ projectId,
354
346
  bucketName,
355
347
  region,
356
348
  progressCallback
@@ -360,7 +352,6 @@ async function deployWithBuild(
360
352
  const archiveName = DEPLOYMENT_CONFIG.ZIP_FILE_NAME;
361
353
 
362
354
  await uploadToStorageBucket(
363
- context,
364
355
  bucket,
365
356
  archiveBuffer,
366
357
  archiveName,
@@ -369,7 +360,6 @@ async function deployWithBuild(
369
360
  await logAndProgress('Source code uploaded successfully', progressCallback);
370
361
 
371
362
  await ensureArtifactRegistryRepoExists(
372
- context,
373
363
  projectId,
374
364
  region,
375
365
  DEPLOYMENT_CONFIG.REPO_NAME,
@@ -380,7 +370,6 @@ async function deployWithBuild(
380
370
  await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
381
371
 
382
372
  const buildResult = await triggerCloudBuild(
383
- context,
384
373
  projectId,
385
374
  region,
386
375
  bucketName,
@@ -394,7 +383,6 @@ async function deployWithBuild(
394
383
  const builtImageUrl = buildResult.results.images[0].name;
395
384
 
396
385
  const service = await deployToCloudRun(
397
- context,
398
386
  projectId,
399
387
  region,
400
388
  serviceName,
@@ -452,28 +440,8 @@ export async function deploy({
452
440
  }
453
441
  }
454
442
 
455
- const { Storage } = await import('@google-cloud/storage');
456
- const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
457
- const { ArtifactRegistryClient } = await import(
458
- '@google-cloud/artifact-registry'
459
- );
460
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
461
- const { ServicesClient } = CloudRunV2Module;
462
- const { ServiceUsageClient } = await import('@google-cloud/service-usage');
463
- const { Logging } = await import('@google-cloud/logging');
464
-
465
443
  try {
466
- const context = {
467
- storage: new Storage({ projectId }),
468
- cloudBuildClient: new CloudBuildClient({ projectId }),
469
- artifactRegistryClient: new ArtifactRegistryClient({ projectId }),
470
- runClient: new ServicesClient({ projectId }),
471
- serviceUsageClient: new ServiceUsageClient({ projectId }),
472
- loggingClient: new Logging({ projectId }),
473
- };
474
-
475
444
  await ensureApisEnabled(
476
- context,
477
445
  projectId,
478
446
  REQUIRED_APIS.SOURCE_DEPLOY,
479
447
  progressCallback
@@ -495,7 +463,6 @@ export async function deploy({
495
463
  if (canDeployWithoutBuild({ hasDockerfile, deploymentAttrs })) {
496
464
  try {
497
465
  return await deployWithZip(
498
- context,
499
466
  projectId,
500
467
  region,
501
468
  serviceName,
@@ -515,7 +482,6 @@ export async function deploy({
515
482
  }
516
483
 
517
484
  return await deployWithBuild(
518
- context,
519
485
  projectId,
520
486
  region,
521
487
  serviceName,
@@ -577,18 +543,8 @@ export async function deployImage({
577
543
  }
578
544
  }
579
545
 
580
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
581
- const { ServicesClient } = CloudRunV2Module;
582
- const { ServiceUsageClient } = await import('@google-cloud/service-usage');
583
-
584
546
  try {
585
- const context = {
586
- runClient: new ServicesClient({ projectId }),
587
- serviceUsageClient: new ServiceUsageClient({ projectId }),
588
- };
589
-
590
547
  await ensureApisEnabled(
591
- context,
592
548
  projectId,
593
549
  REQUIRED_APIS.IMAGE_DEPLOY,
594
550
  progressCallback
@@ -600,7 +556,6 @@ export async function deployImage({
600
556
  await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
601
557
 
602
558
  const service = await deployToCloudRun(
603
- context,
604
559
  projectId,
605
560
  region,
606
561
  serviceName,
@@ -170,7 +170,7 @@ export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
170
170
  * Creates the container spec for a direct source deployment (no Cloud Build).
171
171
  * @param {object} params - The parameters for creating the container spec.
172
172
  * @param {string} params.bucketName - The GCS bucket name where the source is uploaded.
173
- * @param {string} params.fileName - The GCS object name (e.g., source.tar.gz).
173
+ * @param {string} params.fileName - The GCS object name (eg., source.tar.gz).
174
174
  * @param {{cmd: string[], args: string[], baseImage: string}} params.deploymentAttrs - Deployment attributes including command, args, and the base image to use.
175
175
  * @returns {object} The container specification object for the Cloud Run service.
176
176
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/cloud-run-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "Cloud Run MCP deployment tool",
6
6
  "main": "mcp-server.js",
@@ -58,6 +58,6 @@
58
58
  "devDependencies": {
59
59
  "c8": "^10.1.3",
60
60
  "esmock": "^2.7.1",
61
- "prettier": "3.6.2"
61
+ "prettier": "^3.8.0"
62
62
  }
63
63
  }
@@ -0,0 +1,146 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { getClient } from '../../lib/clients.js';
4
+
5
+ describe('getClient Helper', () => {
6
+ class MockClient {
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ }
11
+
12
+ test('creates new client instance with authClient when access token provided', async () => {
13
+ const projectId = 'test-project-1';
14
+ const accessToken = 'fake-token-1';
15
+ const service = 'run'; // Must match keys in lib/clients.js clients object
16
+ const key = projectId + accessToken;
17
+
18
+ const client = await getClient(
19
+ service,
20
+ key,
21
+ async () => MockClient,
22
+ { projectId },
23
+ accessToken
24
+ );
25
+
26
+ assert.ok(client instanceof MockClient);
27
+ assert.strictEqual(client.options.projectId, projectId);
28
+ assert.ok(client.options.authClient);
29
+
30
+ const headers = client.options.authClient.getRequestHeaders();
31
+ assert.strictEqual(headers.get('Authorization'), 'Bearer fake-token-1');
32
+ });
33
+
34
+ test('creates new client instance WITHOUT authClient when NO access token provided', async () => {
35
+ const projectId = 'test-project-2';
36
+ const service = 'run';
37
+ const key = projectId;
38
+
39
+ const client = await getClient(
40
+ service,
41
+ key,
42
+ async () => MockClient,
43
+ { projectId },
44
+ null
45
+ );
46
+
47
+ assert.ok(client instanceof MockClient);
48
+ assert.strictEqual(client.options.projectId, projectId);
49
+ assert.strictEqual(client.options.authClient, undefined);
50
+ });
51
+
52
+ test('caches client instances by key', async () => {
53
+ const projectId = 'test-project-3';
54
+ const accessToken = 'token-A';
55
+ const service = 'run';
56
+ const key = projectId + accessToken;
57
+
58
+ const client1 = await getClient(
59
+ service,
60
+ key,
61
+ async () => MockClient,
62
+ { projectId },
63
+ accessToken
64
+ );
65
+
66
+ const client2 = await getClient(
67
+ service,
68
+ key,
69
+ async () => MockClient,
70
+ { projectId },
71
+ accessToken
72
+ );
73
+
74
+ assert.strictEqual(client1, client2);
75
+ });
76
+
77
+ test('creates DIFFERENT client instances for different keys', async () => {
78
+ const projectId = 'test-project-4';
79
+ const service = 'run';
80
+
81
+ const client1 = await getClient(
82
+ service,
83
+ projectId + 'token-A',
84
+ async () => MockClient,
85
+ { projectId },
86
+ 'token-A'
87
+ );
88
+
89
+ const client2 = await getClient(
90
+ service,
91
+ projectId + 'token-B',
92
+ async () => MockClient,
93
+ { projectId },
94
+ 'token-B'
95
+ );
96
+
97
+ assert.notStrictEqual(client1, client2);
98
+
99
+ const h1 = client1.options.authClient.getRequestHeaders();
100
+ assert.strictEqual(h1.get('Authorization'), 'Bearer token-A');
101
+
102
+ const h2 = client2.options.authClient.getRequestHeaders();
103
+ assert.strictEqual(h2.get('Authorization'), 'Bearer token-B');
104
+ });
105
+
106
+ test('supports different services', async () => {
107
+ const projectId = 'test-project-5';
108
+ const accessToken = 'token-C';
109
+
110
+ const runClient = await getClient(
111
+ 'run',
112
+ projectId + accessToken,
113
+ async () => MockClient,
114
+ { projectId },
115
+ accessToken
116
+ );
117
+
118
+ const storageClient = await getClient(
119
+ 'storage',
120
+ projectId + accessToken,
121
+ async () => MockClient,
122
+ { projectId },
123
+ accessToken
124
+ );
125
+
126
+ assert.notStrictEqual(runClient, storageClient); // Different maps
127
+ assert.ok(runClient.options.authClient);
128
+ assert.ok(storageClient.options.authClient);
129
+ });
130
+
131
+ test('passes additional options correctly', async () => {
132
+ const projectId = 'test-project-6';
133
+ const service = 'run';
134
+ const extraOpt = 'foo';
135
+
136
+ const client = await getClient(
137
+ service,
138
+ projectId,
139
+ async () => MockClient,
140
+ { projectId, extraOpt },
141
+ null
142
+ );
143
+
144
+ assert.strictEqual(client.options.extraOpt, extraOpt);
145
+ });
146
+ });