@google-cloud/cloud-run-mcp 1.0.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/tools.js CHANGED
@@ -25,12 +25,32 @@ import {
25
25
  listProjects,
26
26
  createProjectAndAttachBilling,
27
27
  } from './lib/gcp-projects.js';
28
- import { checkGCP } from './lib/gcp-metadata.js';
28
+
29
29
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
30
30
 
31
+ function gcpTool(gcpCredentialsAvailable, fn) {
32
+ if (!gcpCredentialsAvailable) {
33
+ return () => ({
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: 'GCP credentials are not available. Please configure your environment.',
38
+ },
39
+ ],
40
+ });
41
+ }
42
+ return fn;
43
+ }
44
+
31
45
  export const registerTools = (
32
46
  server,
33
- { defaultProjectId, defaultRegion, defaultServiceName, skipIamCheck } = {}
47
+ {
48
+ defaultProjectId,
49
+ defaultRegion,
50
+ defaultServiceName,
51
+ skipIamCheck,
52
+ gcpCredentialsAvailable,
53
+ } = {}
34
54
  ) => {
35
55
  // Tool to list GCP projects
36
56
  server.registerTool(
@@ -39,7 +59,7 @@ export const registerTools = (
39
59
  description: 'Lists available GCP projects',
40
60
  inputSchema: {},
41
61
  },
42
- async () => {
62
+ gcpTool(gcpCredentialsAvailable, async () => {
43
63
  try {
44
64
  const projects = await listProjects();
45
65
  return {
@@ -60,7 +80,7 @@ export const registerTools = (
60
80
  ],
61
81
  };
62
82
  }
63
- }
83
+ })
64
84
  );
65
85
 
66
86
  // Tool to create a new GCP project
@@ -78,7 +98,7 @@ export const registerTools = (
78
98
  ),
79
99
  },
80
100
  },
81
- async ({ projectId }) => {
101
+ gcpTool(gcpCredentialsAvailable, async ({ projectId }) => {
82
102
  if (
83
103
  projectId !== undefined &&
84
104
  (typeof projectId !== 'string' || projectId.trim() === '')
@@ -112,7 +132,7 @@ export const registerTools = (
112
132
  ],
113
133
  };
114
134
  }
115
- }
135
+ })
116
136
  );
117
137
 
118
138
  // Listing Cloud Run services
@@ -131,7 +151,7 @@ export const registerTools = (
131
151
  .default(defaultRegion),
132
152
  },
133
153
  },
134
- async ({ project, region }) => {
154
+ gcpTool(gcpCredentialsAvailable, async ({ project, region }) => {
135
155
  if (typeof project !== 'string') {
136
156
  return {
137
157
  content: [
@@ -169,7 +189,7 @@ export const registerTools = (
169
189
  ],
170
190
  };
171
191
  }
172
- }
192
+ })
173
193
  );
174
194
 
175
195
  // Dynamic resource for getting a specific service
@@ -192,7 +212,7 @@ export const registerTools = (
192
212
  .default(defaultServiceName),
193
213
  },
194
214
  },
195
- async ({ project, region, service }) => {
215
+ gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
196
216
  if (typeof project !== 'string') {
197
217
  return {
198
218
  content: [
@@ -238,7 +258,7 @@ export const registerTools = (
238
258
  ],
239
259
  };
240
260
  }
241
- }
261
+ })
242
262
  );
243
263
 
244
264
  // Logs for a service
@@ -262,7 +282,7 @@ export const registerTools = (
262
282
  .default(defaultServiceName),
263
283
  },
264
284
  },
265
- async ({ project, region, service }) => {
285
+ gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
266
286
  let allLogs = [];
267
287
  let requestOptions;
268
288
  try {
@@ -301,7 +321,7 @@ export const registerTools = (
301
321
  ],
302
322
  };
303
323
  }
304
- }
324
+ })
305
325
  );
306
326
 
307
327
  server.registerTool(
@@ -333,48 +353,51 @@ export const registerTools = (
333
353
  ),
334
354
  },
335
355
  },
336
- async ({ project, region, service, files }) => {
337
- if (typeof project !== 'string') {
338
- throw new Error(
339
- 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
340
- );
341
- }
342
- if (typeof files !== 'object' || !Array.isArray(files)) {
343
- throw new Error('Files must specified');
344
- }
345
- if (files.length === 0) {
346
- throw new Error('No files specified for deployment');
347
- }
356
+ gcpTool(
357
+ gcpCredentialsAvailable,
358
+ async ({ project, region, service, files }) => {
359
+ if (typeof project !== 'string') {
360
+ throw new Error(
361
+ 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
362
+ );
363
+ }
364
+ if (typeof files !== 'object' || !Array.isArray(files)) {
365
+ throw new Error('Files must specified');
366
+ }
367
+ if (files.length === 0) {
368
+ throw new Error('No files specified for deployment');
369
+ }
348
370
 
349
- // Deploy to Cloud Run
350
- try {
351
- // TODO: Should we return intermediate progress messages? we'd need to use sendNotification for that, see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/server/jsonResponseStreamableHttp.ts#L46C24-L46C41
352
- const response = await deploy({
353
- projectId: project,
354
- serviceName: service,
355
- region: region,
356
- files: files,
357
- skipIamCheck: skipIamCheck, // Pass the new flag
358
- });
359
- return {
360
- content: [
361
- {
362
- type: 'text',
363
- text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
364
- },
365
- ],
366
- };
367
- } catch (error) {
368
- return {
369
- content: [
370
- {
371
- type: 'text',
372
- text: `Error deploying to Cloud Run: ${error.message || error}`,
373
- },
374
- ],
375
- };
371
+ // Deploy to Cloud Run
372
+ try {
373
+ // TODO: Should we return intermediate progress messages? we'd need to use sendNotification for that, see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/server/jsonResponseStreamableHttp.ts#L46C24-L46C41
374
+ const response = await deploy({
375
+ projectId: project,
376
+ serviceName: service,
377
+ region: region,
378
+ files: files,
379
+ skipIamCheck: skipIamCheck, // Pass the new flag
380
+ });
381
+ return {
382
+ content: [
383
+ {
384
+ type: 'text',
385
+ text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
386
+ },
387
+ ],
388
+ };
389
+ } catch (error) {
390
+ return {
391
+ content: [
392
+ {
393
+ type: 'text',
394
+ text: `Error deploying to Cloud Run: ${error.message || error}`,
395
+ },
396
+ ],
397
+ };
398
+ }
376
399
  }
377
- }
400
+ )
378
401
  );
379
402
 
380
403
  server.registerTool(
@@ -406,46 +429,49 @@ export const registerTools = (
406
429
  ),
407
430
  },
408
431
  },
409
- async ({ project, region, service, folderPath }) => {
410
- if (typeof project !== 'string') {
411
- throw new Error(
412
- 'Project must be specified, please prompt the user for a valid existing Google Cloud project ID.'
413
- );
414
- }
415
- if (typeof folderPath !== 'string' || folderPath.trim() === '') {
416
- throw new Error(
417
- 'Folder path must be specified and be a non-empty string.'
418
- );
419
- }
432
+ gcpTool(
433
+ gcpCredentialsAvailable,
434
+ async ({ project, region, service, folderPath }) => {
435
+ if (typeof project !== 'string') {
436
+ throw new Error(
437
+ 'Project must be specified, please prompt the user for a valid existing Google Cloud project ID.'
438
+ );
439
+ }
440
+ if (typeof folderPath !== 'string' || folderPath.trim() === '') {
441
+ throw new Error(
442
+ 'Folder path must be specified and be a non-empty string.'
443
+ );
444
+ }
420
445
 
421
- // Deploy to Cloud Run
422
- try {
423
- const response = await deploy({
424
- projectId: project,
425
- serviceName: service,
426
- region: region,
427
- files: [folderPath],
428
- skipIamCheck: skipIamCheck, // Pass the new flag
429
- });
430
- return {
431
- content: [
432
- {
433
- type: 'text',
434
- text: `Cloud Run service ${service} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
435
- },
436
- ],
437
- };
438
- } catch (error) {
439
- return {
440
- content: [
441
- {
442
- type: 'text',
443
- text: `Error deploying folder to Cloud Run: ${error.message || error}`,
444
- },
445
- ],
446
- };
446
+ // Deploy to Cloud Run
447
+ try {
448
+ const response = await deploy({
449
+ projectId: project,
450
+ serviceName: service,
451
+ region: region,
452
+ files: [folderPath],
453
+ skipIamCheck: skipIamCheck, // Pass the new flag
454
+ });
455
+ return {
456
+ content: [
457
+ {
458
+ type: 'text',
459
+ text: `Cloud Run service ${service} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
460
+ },
461
+ ],
462
+ };
463
+ } catch (error) {
464
+ return {
465
+ content: [
466
+ {
467
+ type: 'text',
468
+ text: `Error deploying folder to Cloud Run: ${error.message || error}`,
469
+ },
470
+ ],
471
+ };
472
+ }
447
473
  }
448
- }
474
+ )
449
475
  );
450
476
 
451
477
  server.registerTool(
@@ -487,54 +513,57 @@ export const registerTools = (
487
513
  .describe('Array of file objects containing filename and content'),
488
514
  },
489
515
  },
490
- async ({ project, region, service, files }) => {
491
- if (typeof project !== 'string') {
492
- throw new Error(
493
- 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
494
- );
495
- }
496
- if (typeof files !== 'object' || !Array.isArray(files)) {
497
- throw new Error('Files must be specified');
498
- }
499
- if (files.length === 0) {
500
- throw new Error('No files specified for deployment');
501
- }
516
+ gcpTool(
517
+ gcpCredentialsAvailable,
518
+ async ({ project, region, service, files }) => {
519
+ if (typeof project !== 'string') {
520
+ throw new Error(
521
+ 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
522
+ );
523
+ }
524
+ if (typeof files !== 'object' || !Array.isArray(files)) {
525
+ throw new Error('Files must be specified');
526
+ }
527
+ if (files.length === 0) {
528
+ throw new Error('No files specified for deployment');
529
+ }
502
530
 
503
- // Validate that each file has either content
504
- for (const file of files) {
505
- if (!file.content) {
506
- throw new Error(`File ${file.filename} must have content`);
531
+ // Validate that each file has either content
532
+ for (const file of files) {
533
+ if (!file.content) {
534
+ throw new Error(`File ${file.filename} must have content`);
535
+ }
507
536
  }
508
- }
509
537
 
510
- // Deploy to Cloud Run
511
- try {
512
- const response = await deploy({
513
- projectId: project,
514
- serviceName: service,
515
- region: region,
516
- files: files,
517
- skipIamCheck: skipIamCheck, // Pass the new flag
518
- });
519
- return {
520
- content: [
521
- {
522
- type: 'text',
523
- text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
524
- },
525
- ],
526
- };
527
- } catch (error) {
528
- return {
529
- content: [
530
- {
531
- type: 'text',
532
- text: `Error deploying to Cloud Run: ${error.message || error}`,
533
- },
534
- ],
535
- };
538
+ // Deploy to Cloud Run
539
+ try {
540
+ const response = await deploy({
541
+ projectId: project,
542
+ serviceName: service,
543
+ region: region,
544
+ files: files,
545
+ skipIamCheck: skipIamCheck, // Pass the new flag
546
+ });
547
+ return {
548
+ content: [
549
+ {
550
+ type: 'text',
551
+ text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
552
+ },
553
+ ],
554
+ };
555
+ } catch (error) {
556
+ return {
557
+ content: [
558
+ {
559
+ type: 'text',
560
+ text: `Error deploying to Cloud Run: ${error.message || error}`,
561
+ },
562
+ ],
563
+ };
564
+ }
536
565
  }
537
- }
566
+ )
538
567
  );
539
568
 
540
569
  server.registerTool(
@@ -566,46 +595,49 @@ export const registerTools = (
566
595
  ),
567
596
  },
568
597
  },
569
- async ({ project, region, service, imageUrl }) => {
570
- if (typeof project !== 'string') {
571
- throw new Error(
572
- 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
573
- );
574
- }
575
- if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
576
- throw new Error(
577
- 'Container image URL must be specified and be a non-empty string.'
578
- );
579
- }
598
+ gcpTool(
599
+ gcpCredentialsAvailable,
600
+ async ({ project, region, service, imageUrl }) => {
601
+ if (typeof project !== 'string') {
602
+ throw new Error(
603
+ 'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
604
+ );
605
+ }
606
+ if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
607
+ throw new Error(
608
+ 'Container image URL must be specified and be a non-empty string.'
609
+ );
610
+ }
580
611
 
581
- // Deploy to Cloud Run
582
- try {
583
- const response = await deployImage({
584
- projectId: project,
585
- serviceName: service,
586
- region: region,
587
- imageUrl: imageUrl,
588
- skipIamCheck: skipIamCheck,
589
- });
590
- return {
591
- content: [
592
- {
593
- type: 'text',
594
- text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
595
- },
596
- ],
597
- };
598
- } catch (error) {
599
- return {
600
- content: [
601
- {
602
- type: 'text',
603
- text: `Error deploying to Cloud Run: ${error.message || error}`,
604
- },
605
- ],
606
- };
612
+ // Deploy to Cloud Run
613
+ try {
614
+ const response = await deployImage({
615
+ projectId: project,
616
+ serviceName: service,
617
+ region: region,
618
+ imageUrl: imageUrl,
619
+ skipIamCheck: skipIamCheck,
620
+ });
621
+ return {
622
+ content: [
623
+ {
624
+ type: 'text',
625
+ text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
626
+ },
627
+ ],
628
+ };
629
+ } catch (error) {
630
+ return {
631
+ content: [
632
+ {
633
+ type: 'text',
634
+ text: `Error deploying to Cloud Run: ${error.message || error}`,
635
+ },
636
+ ],
637
+ };
638
+ }
607
639
  }
608
- }
640
+ )
609
641
  );
610
642
  };
611
643
 
@@ -616,6 +648,7 @@ export const registerToolsRemote = async (
616
648
  defaultRegion,
617
649
  defaultServiceName,
618
650
  skipIamCheck = false,
651
+ gcpCredentialsAvailable,
619
652
  } = {}
620
653
  ) => {
621
654
  // We no longer call checkGCP here; the effective defaults are passed in.
@@ -640,7 +673,7 @@ export const registerToolsRemote = async (
640
673
  .default(currentRegion),
641
674
  },
642
675
  },
643
- async ({ region }) => {
676
+ gcpTool(gcpCredentialsAvailable, async ({ region }) => {
644
677
  try {
645
678
  const services = await listServices(currentProject, region);
646
679
  const serviceList = services
@@ -667,7 +700,7 @@ export const registerToolsRemote = async (
667
700
  ],
668
701
  };
669
702
  }
670
- }
703
+ })
671
704
  );
672
705
 
673
706
  // Dynamic resource for getting a specific service (Remote)
@@ -686,7 +719,7 @@ export const registerToolsRemote = async (
686
719
  .default(defaultServiceName),
687
720
  },
688
721
  },
689
- async ({ region, service }) => {
722
+ gcpTool(gcpCredentialsAvailable, async ({ region, service }) => {
690
723
  if (typeof service !== 'string') {
691
724
  return {
692
725
  content: [
@@ -729,7 +762,7 @@ export const registerToolsRemote = async (
729
762
  ],
730
763
  };
731
764
  }
732
- }
765
+ })
733
766
  );
734
767
 
735
768
  // Logs for a service
@@ -753,7 +786,7 @@ export const registerToolsRemote = async (
753
786
  .default(defaultServiceName),
754
787
  },
755
788
  },
756
- async ({ project, region, service }) => {
789
+ gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
757
790
  let allLogs = [];
758
791
  let requestOptions;
759
792
  try {
@@ -792,7 +825,7 @@ export const registerToolsRemote = async (
792
825
  ],
793
826
  };
794
827
  }
795
- }
828
+ })
796
829
  );
797
830
 
798
831
  // Deploy file contents to Cloud Run (Remote)
@@ -825,7 +858,7 @@ export const registerToolsRemote = async (
825
858
  .describe('Array of file objects containing filename and content'),
826
859
  },
827
860
  },
828
- async ({ region, service, files }) => {
861
+ gcpTool(gcpCredentialsAvailable, async ({ region, service, files }) => {
829
862
  console.log(
830
863
  `New deploy request (remote): ${JSON.stringify({ project: currentProject, region, service, files })}`
831
864
  );
@@ -872,7 +905,7 @@ export const registerToolsRemote = async (
872
905
  ],
873
906
  };
874
907
  }
875
- }
908
+ })
876
909
  );
877
910
 
878
911
  server.registerTool(
@@ -897,7 +930,7 @@ export const registerToolsRemote = async (
897
930
  ),
898
931
  },
899
932
  },
900
- async ({ region, service, imageUrl }) => {
933
+ gcpTool(gcpCredentialsAvailable, async ({ region, service, imageUrl }) => {
901
934
  if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
902
935
  throw new Error(
903
936
  'Container image URL must be specified and be a non-empty string.'
@@ -931,6 +964,6 @@ export const registerToolsRemote = async (
931
964
  ],
932
965
  };
933
966
  }
934
- }
967
+ })
935
968
  );
936
969
  };
@@ -1,19 +0,0 @@
1
- import { execSync } from 'child_process';
2
-
3
- const tests = [
4
- 'npm run test:deploy',
5
- 'npm run test:service-logs',
6
- 'npm run test:gcp-auth',
7
- 'npm run test:create-project',
8
- 'npm run test:prompts',
9
- ];
10
-
11
- for (const test of tests) {
12
- try {
13
- console.log(`Running: ${test}`);
14
- execSync(test, { stdio: 'inherit' });
15
- } catch (error) {
16
- console.error(`Failed to run: ${test}`);
17
- process.exit(1);
18
- }
19
- }