@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
@@ -1,6 +1,15 @@
1
- import { test, describe } from 'node:test';
1
+ import { test, describe, mock, beforeEach } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { getClient } from '../../lib/clients.js';
3
+ import { google } from 'googleapis';
4
+ import {
5
+ clients,
6
+ getClient,
7
+ getRunClient,
8
+ getRunV1Client,
9
+ getCloudRunRegions,
10
+ resetCachedRegions,
11
+ } from '../../lib/clients.js';
12
+ import { GCLOUD_AUTH } from '../../constants.js';
4
13
 
5
14
  describe('getClient Helper', () => {
6
15
  class MockClient {
@@ -173,4 +182,300 @@ describe('getClient Helper', () => {
173
182
 
174
183
  assert.strictEqual(client.options.extraOpt, extraOpt);
175
184
  });
185
+
186
+ test('throws error when OAUTH_ENABLED=true and accessToken is missing or GCLOUD_AUTH', async () => {
187
+ const originalOAuth = process.env.OAUTH_ENABLED;
188
+ process.env.OAUTH_ENABLED = 'true';
189
+ try {
190
+ const projectId = 'test-project-oauth-err';
191
+ const service = 'run';
192
+
193
+ await assert.rejects(
194
+ async () =>
195
+ getClient(
196
+ service,
197
+ projectId,
198
+ async () => MockClient,
199
+ { projectId },
200
+ null
201
+ ),
202
+ /Access token is required when OAuth is enabled/
203
+ );
204
+
205
+ await assert.rejects(
206
+ async () =>
207
+ getClient(
208
+ service,
209
+ projectId,
210
+ async () => MockClient,
211
+ { projectId },
212
+ undefined
213
+ ),
214
+ /Access token is required when OAuth is enabled/
215
+ );
216
+
217
+ await assert.rejects(
218
+ async () =>
219
+ getClient(
220
+ service,
221
+ projectId,
222
+ async () => MockClient,
223
+ { projectId },
224
+ GCLOUD_AUTH
225
+ ),
226
+ /Access token is required when OAuth is enabled/
227
+ );
228
+
229
+ await assert.rejects(
230
+ async () => getRunClient(projectId),
231
+ /Access token is required when OAuth is enabled/
232
+ );
233
+ } finally {
234
+ if (originalOAuth === undefined) {
235
+ delete process.env.OAUTH_ENABLED;
236
+ } else {
237
+ process.env.OAUTH_ENABLED = originalOAuth;
238
+ }
239
+ }
240
+ });
241
+ });
242
+
243
+ describe('getRunV1Client', () => {
244
+ test('creates new client instance with Application Default Credentials for GCLOUD_AUTH', async () => {
245
+ // Mock the GoogleAuth class to avoid looking up ADC
246
+ const originalGoogleAuth = google.auth.GoogleAuth;
247
+ const authInstances = [];
248
+ google.auth.GoogleAuth = class MockGoogleAuth {
249
+ constructor(options) {
250
+ this.options = options;
251
+ authInstances.push(this);
252
+ }
253
+ };
254
+
255
+ const runMock = mock.method(google, 'run', (options) => {
256
+ return { options };
257
+ });
258
+
259
+ try {
260
+ const projectId = 'test-project';
261
+ const client = await getRunV1Client(projectId, GCLOUD_AUTH);
262
+
263
+ assert.ok(client);
264
+ assert.strictEqual(authInstances.length, 1);
265
+ assert.deepEqual(authInstances[0].options.scopes, [
266
+ 'https://www.googleapis.com/auth/cloud-platform',
267
+ ]);
268
+ assert.strictEqual(runMock.mock.calls.length, 1);
269
+ assert.strictEqual(
270
+ runMock.mock.calls[0].arguments[0].auth,
271
+ authInstances[0]
272
+ );
273
+ } finally {
274
+ google.auth.GoogleAuth = originalGoogleAuth;
275
+ runMock.mock.restore();
276
+ }
277
+ });
278
+
279
+ test('creates new client instance with OAuth client when access token provided', async () => {
280
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
281
+ try {
282
+ const projectId = 'test-project-oauth';
283
+ const accessToken = 'fake-access-token';
284
+ const client = await getRunV1Client(projectId, accessToken);
285
+
286
+ assert.ok(client);
287
+ assert.strictEqual(runMock.mock.calls.length, 1);
288
+ const lastCall = runMock.mock.calls[0];
289
+ assert.ok(lastCall.arguments[0].auth);
290
+ assert.strictEqual(
291
+ lastCall.arguments[0].auth.credentials.access_token,
292
+ accessToken
293
+ );
294
+ } finally {
295
+ runMock.mock.restore();
296
+ }
297
+ });
298
+
299
+ test('sets rootUrl if region is provided', async () => {
300
+ const originalGoogleAuth = google.auth.GoogleAuth;
301
+ google.auth.GoogleAuth = class MockGoogleAuth {
302
+ constructor(options) {
303
+ this.options = options;
304
+ }
305
+ };
306
+
307
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
308
+
309
+ const projectId = 'test-project-region';
310
+ const region = 'us-central1';
311
+ const key = projectId; // If GCLOUD_AUTH, key is just projectId
312
+
313
+ // Mock getCloudRunRegions call by injecting into clients.run
314
+ clients.run.set(key, {
315
+ async *listLocationsAsync() {
316
+ yield { locationId: 'us-central1' };
317
+ },
318
+ });
319
+
320
+ try {
321
+ await getRunV1Client(projectId, GCLOUD_AUTH, region);
322
+
323
+ const lastCall = runMock.mock.calls[0];
324
+ assert.strictEqual(
325
+ lastCall.arguments[0].rootUrl,
326
+ 'https://us-central1-run.googleapis.com/'
327
+ );
328
+ } finally {
329
+ google.auth.GoogleAuth = originalGoogleAuth;
330
+ runMock.mock.restore();
331
+ clients.run.delete(key);
332
+ }
333
+ });
334
+
335
+ test('caches client instances by key', async () => {
336
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
337
+ try {
338
+ const projectId = 'test-project-cache';
339
+ const client1 = await getRunV1Client(projectId, GCLOUD_AUTH);
340
+ const client2 = await getRunV1Client(projectId, GCLOUD_AUTH);
341
+
342
+ assert.strictEqual(client1, client2);
343
+ assert.strictEqual(runMock.mock.calls.length, 1);
344
+ } finally {
345
+ runMock.mock.restore();
346
+ }
347
+ });
348
+
349
+ test('caches client instances separately for different regions', async () => {
350
+ resetCachedRegions();
351
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
352
+ try {
353
+ const projectId = 'test-project-region-cache';
354
+ const region1 = 'us-central1';
355
+ const region2 = 'europe-west1';
356
+
357
+ // Mock getCloudRunRegions for both regions
358
+ const key = projectId; // GCLOUD_AUTH key for getRunClient/getCloudRunRegions
359
+ clients.run.set(key, {
360
+ async *listLocationsAsync() {
361
+ yield { locationId: region1 };
362
+ yield { locationId: region2 };
363
+ },
364
+ });
365
+
366
+ const client1 = await getRunV1Client(projectId, GCLOUD_AUTH, region1);
367
+ const client2 = await getRunV1Client(projectId, GCLOUD_AUTH, region2);
368
+
369
+ assert.notStrictEqual(client1, client2);
370
+ assert.strictEqual(runMock.mock.calls.length, 2);
371
+ assert.strictEqual(
372
+ runMock.mock.calls[0].arguments[0].rootUrl,
373
+ `https://${region1}-run.googleapis.com/`
374
+ );
375
+ assert.strictEqual(
376
+ runMock.mock.calls[1].arguments[0].rootUrl,
377
+ `https://${region2}-run.googleapis.com/`
378
+ );
379
+ } finally {
380
+ runMock.mock.restore();
381
+ clients.run.clear();
382
+ }
383
+ });
384
+
385
+ test('throws error when OAUTH_ENABLED=true and accessToken is missing or GCLOUD_AUTH', async () => {
386
+ const originalOAuth = process.env.OAUTH_ENABLED;
387
+ process.env.OAUTH_ENABLED = 'true';
388
+ try {
389
+ const projectId = 'test-project-v1-oauth-err';
390
+ await assert.rejects(
391
+ async () => getRunV1Client(projectId),
392
+ /Access token is required when OAuth is enabled/
393
+ );
394
+ await assert.rejects(
395
+ async () => getRunV1Client(projectId, undefined),
396
+ /Access token is required when OAuth is enabled/
397
+ );
398
+ await assert.rejects(
399
+ async () => getRunV1Client(projectId, GCLOUD_AUTH),
400
+ /Access token is required when OAuth is enabled/
401
+ );
402
+ } finally {
403
+ if (originalOAuth === undefined) {
404
+ delete process.env.OAUTH_ENABLED;
405
+ } else {
406
+ process.env.OAUTH_ENABLED = originalOAuth;
407
+ }
408
+ }
409
+ });
410
+ });
411
+
412
+ describe('getCloudRunRegions', () => {
413
+ beforeEach(() => {
414
+ resetCachedRegions();
415
+ // Clear the run map to ensure isolation between tests
416
+ clients.run.clear();
417
+ });
418
+
419
+ test('returns list of regions using a mock injected into the clients cache', async () => {
420
+ const projectId = 'test-project-1';
421
+ const accessToken = 'token-1';
422
+ // Match the key generation logic
423
+ const key = projectId + accessToken;
424
+
425
+ const mockRunClient = {
426
+ async *listLocationsAsync() {
427
+ yield { locationId: 'us-central1' };
428
+ yield { locationId: 'europe-west1' };
429
+ },
430
+ };
431
+
432
+ clients.run.set(key, mockRunClient);
433
+
434
+ const regions = await getCloudRunRegions(projectId, accessToken);
435
+ assert.deepStrictEqual(regions, ['us-central1', 'europe-west1']);
436
+ });
437
+
438
+ test('caches the regions after first call', async () => {
439
+ const projectId = 'test-project-2';
440
+ const accessToken = 'token-2';
441
+ const key = projectId + accessToken;
442
+
443
+ let callCount = 0;
444
+ const mockRunClient = {
445
+ async *listLocationsAsync() {
446
+ callCount++;
447
+ yield { locationId: 'us-central1' };
448
+ },
449
+ };
450
+
451
+ clients.run.set(key, mockRunClient);
452
+
453
+ await getCloudRunRegions(projectId, accessToken);
454
+ const regions = await getCloudRunRegions(projectId, accessToken);
455
+
456
+ assert.strictEqual(callCount, 1);
457
+ assert.deepStrictEqual(regions, ['us-central1']);
458
+ });
459
+
460
+ test('using different accessToken allows isolated client mocks', async () => {
461
+ const projectId = 'test-project-3';
462
+
463
+ clients.run.set(projectId + 'token-a', {
464
+ async *listLocationsAsync() {
465
+ yield { locationId: 'region-a' };
466
+ },
467
+ });
468
+ clients.run.set(projectId + 'token-b', {
469
+ async *listLocationsAsync() {
470
+ yield { locationId: 'region-b' };
471
+ },
472
+ });
473
+
474
+ const resA = await getCloudRunRegions(projectId, 'token-a');
475
+ resetCachedRegions();
476
+ const resB = await getCloudRunRegions(projectId, 'token-b');
477
+
478
+ assert.deepStrictEqual(resA, ['region-a']);
479
+ assert.deepStrictEqual(resB, ['region-b']);
480
+ });
176
481
  });