@google-cloud/cloud-run-mcp 1.0.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 ADDED
@@ -0,0 +1,936 @@
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
+ import { z } from 'zod';
18
+ import { deploy, deployImage } from './lib/cloud-run-deploy.js';
19
+ import {
20
+ listServices,
21
+ getService,
22
+ getServiceLogs,
23
+ } from './lib/cloud-run-services.js';
24
+ import {
25
+ listProjects,
26
+ createProjectAndAttachBilling,
27
+ } from './lib/gcp-projects.js';
28
+ import { checkGCP } from './lib/gcp-metadata.js';
29
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
30
+
31
+ export const registerTools = (
32
+ server,
33
+ { defaultProjectId, defaultRegion, defaultServiceName, skipIamCheck } = {}
34
+ ) => {
35
+ // Tool to list GCP projects
36
+ server.registerTool(
37
+ 'list_projects',
38
+ {
39
+ description: 'Lists available GCP projects',
40
+ inputSchema: {},
41
+ },
42
+ async () => {
43
+ try {
44
+ const projects = await listProjects();
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: `Available GCP Projects:\n${projects.map((p) => `- ${p.id}`).join('\n')}`,
50
+ },
51
+ ],
52
+ };
53
+ } catch (error) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: `Error listing GCP projects: ${error.message}`,
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ }
64
+ );
65
+
66
+ // Tool to create a new GCP project
67
+ server.registerTool(
68
+ 'create_project',
69
+ {
70
+ description:
71
+ 'Creates a new GCP project and attempts to attach it to the first available billing account. A project ID can be optionally specified; otherwise it will be automatically generated.',
72
+ inputSchema: {
73
+ projectId: z
74
+ .string()
75
+ .optional()
76
+ .describe(
77
+ 'Optional. The desired ID for the new GCP project. If not provided, an ID will be auto-generated.'
78
+ ),
79
+ },
80
+ },
81
+ async ({ projectId }) => {
82
+ if (
83
+ projectId !== undefined &&
84
+ (typeof projectId !== 'string' || projectId.trim() === '')
85
+ ) {
86
+ return {
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: 'Error: If provided, Project ID must be a non-empty string.',
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ try {
96
+ const result = await createProjectAndAttachBilling(projectId);
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: `Successfully created GCP project with ID "${result.projectId}". You can now use this project ID for deployments.`,
102
+ },
103
+ ],
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: `Error creating GCP project or attaching billing: ${error.message}`,
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ }
116
+ );
117
+
118
+ // Listing Cloud Run services
119
+ server.registerTool(
120
+ 'list_services',
121
+ {
122
+ description: 'Lists Cloud Run services in a given project and region.',
123
+ inputSchema: {
124
+ project: z
125
+ .string()
126
+ .describe('Google Cloud project ID')
127
+ .default(defaultProjectId),
128
+ region: z
129
+ .string()
130
+ .describe('Region where the services are located')
131
+ .default(defaultRegion),
132
+ },
133
+ },
134
+ async ({ project, region }) => {
135
+ if (typeof project !== 'string') {
136
+ return {
137
+ content: [
138
+ {
139
+ type: 'text',
140
+ text: 'Error: Project ID must be provided and be a non-empty string.',
141
+ },
142
+ ],
143
+ };
144
+ }
145
+
146
+ try {
147
+ const services = await listServices(project, region);
148
+ const serviceList = services
149
+ .map((s) => {
150
+ const serviceName = s.name.split('/').pop();
151
+ return `- ${serviceName} (URL: ${s.uri})`;
152
+ })
153
+ .join('\n');
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: `Services in project ${project} (location ${region}):\n${serviceList}`,
159
+ },
160
+ ],
161
+ };
162
+ } catch (error) {
163
+ return {
164
+ content: [
165
+ {
166
+ type: 'text',
167
+ text: `Error listing services for project ${project} (region ${region}): ${error.message}`,
168
+ },
169
+ ],
170
+ };
171
+ }
172
+ }
173
+ );
174
+
175
+ // Dynamic resource for getting a specific service
176
+ server.registerTool(
177
+ 'get_service',
178
+ {
179
+ description: 'Gets details for a specific Cloud Run service.',
180
+ inputSchema: {
181
+ project: z
182
+ .string()
183
+ .describe('Google Cloud project ID containing the service')
184
+ .default(defaultProjectId),
185
+ region: z
186
+ .string()
187
+ .describe('Region where the service is located')
188
+ .default(defaultRegion),
189
+ service: z
190
+ .string()
191
+ .describe('Name of the Cloud Run service')
192
+ .default(defaultServiceName),
193
+ },
194
+ },
195
+ async ({ project, region, service }) => {
196
+ if (typeof project !== 'string') {
197
+ return {
198
+ content: [
199
+ { type: 'text', text: 'Error: Project ID must be provided.' },
200
+ ],
201
+ };
202
+ }
203
+ if (typeof service !== 'string') {
204
+ return {
205
+ content: [
206
+ { type: 'text', text: 'Error: Service name must be provided.' },
207
+ ],
208
+ };
209
+ }
210
+ try {
211
+ const serviceDetails = await getService(project, region, service);
212
+ if (serviceDetails) {
213
+ return {
214
+ content: [
215
+ {
216
+ type: 'text',
217
+ text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
218
+ },
219
+ ],
220
+ };
221
+ } else {
222
+ return {
223
+ content: [
224
+ {
225
+ type: 'text',
226
+ text: `Service ${service} not found in project ${project} (region ${region}).`,
227
+ },
228
+ ],
229
+ };
230
+ }
231
+ } catch (error) {
232
+ return {
233
+ content: [
234
+ {
235
+ type: 'text',
236
+ text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ }
242
+ );
243
+
244
+ // Logs for a service
245
+ server.registerTool(
246
+ 'get_service_log',
247
+ {
248
+ description:
249
+ 'Gets Logs and Error Messages for a specific Cloud Run service.',
250
+ inputSchema: {
251
+ project: z
252
+ .string()
253
+ .describe('Google Cloud project ID containing the service')
254
+ .default(defaultProjectId),
255
+ region: z
256
+ .string()
257
+ .describe('Region where the service is located')
258
+ .default(defaultRegion),
259
+ service: z
260
+ .string()
261
+ .describe('Name of the Cloud Run service')
262
+ .default(defaultServiceName),
263
+ },
264
+ },
265
+ async ({ project, region, service }) => {
266
+ let allLogs = [];
267
+ let requestOptions;
268
+ try {
269
+ do {
270
+ // Fetch a page of logs
271
+ const response = await getServiceLogs(
272
+ project,
273
+ region,
274
+ service,
275
+ requestOptions
276
+ );
277
+
278
+ if (response.logs) {
279
+ allLogs.push(response.logs);
280
+ }
281
+
282
+ // Set the requestOptions incl pagintion token for the next iteration
283
+
284
+ requestOptions = response.requestOptions;
285
+ } while (requestOptions); // Continue as long as there is a next page token
286
+ return {
287
+ content: [
288
+ {
289
+ type: 'text',
290
+ text: allLogs.join('\n'),
291
+ },
292
+ ],
293
+ };
294
+ } catch (error) {
295
+ return {
296
+ content: [
297
+ {
298
+ type: 'text',
299
+ text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
300
+ },
301
+ ],
302
+ };
303
+ }
304
+ }
305
+ );
306
+
307
+ server.registerTool(
308
+ 'deploy_local_files',
309
+ {
310
+ description:
311
+ 'Deploy local files to Cloud Run. Takes an array of absolute file paths from the local filesystem that will be deployed. Use this tool if the files exists on the user local filesystem.',
312
+ inputSchema: {
313
+ project: z
314
+ .string()
315
+ .describe(
316
+ 'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
317
+ )
318
+ .default(defaultProjectId),
319
+ region: z
320
+ .string()
321
+ .optional()
322
+ .default(defaultRegion)
323
+ .describe('Region to deploy the service to'),
324
+ service: z
325
+ .string()
326
+ .optional()
327
+ .default(defaultServiceName)
328
+ .describe('Name of the Cloud Run service to deploy to'),
329
+ files: z
330
+ .array(z.string())
331
+ .describe(
332
+ 'Array of absolute file paths to deploy (e.g. ["/home/user/project/src/index.js", "/home/user/project/package.json"])'
333
+ ),
334
+ },
335
+ },
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
+ }
348
+
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
+ };
376
+ }
377
+ }
378
+ );
379
+
380
+ server.registerTool(
381
+ 'deploy_local_folder',
382
+ {
383
+ description:
384
+ 'Deploy a local folder to Cloud Run. Takes an absolute folder path from the local filesystem that will be deployed. Use this tool if the entire folder content needs to be deployed.',
385
+ inputSchema: {
386
+ project: z
387
+ .string()
388
+ .describe(
389
+ 'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
390
+ )
391
+ .default(defaultProjectId),
392
+ region: z
393
+ .string()
394
+ .optional()
395
+ .default(defaultRegion)
396
+ .describe('Region to deploy the service to'),
397
+ service: z
398
+ .string()
399
+ .optional()
400
+ .default(defaultServiceName)
401
+ .describe('Name of the Cloud Run service to deploy to'),
402
+ folderPath: z
403
+ .string()
404
+ .describe(
405
+ 'Absolute path to the folder to deploy (e.g. "/home/user/project/src")'
406
+ ),
407
+ },
408
+ },
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
+ }
420
+
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
+ };
447
+ }
448
+ }
449
+ );
450
+
451
+ server.registerTool(
452
+ 'deploy_file_contents',
453
+ {
454
+ description:
455
+ 'Deploy files to Cloud Run by providing their contents directly. Takes an array of file objects containing filename and content. Use this tool if the files only exist in the current chat context.',
456
+ inputSchema: {
457
+ project: z
458
+ .string()
459
+ .describe(
460
+ 'Google Cloud project ID. Leave unset for the app to be deployed in a new project. If provided, make sure the user confirms the project ID they want to deploy to.'
461
+ )
462
+ .default(defaultProjectId),
463
+ region: z
464
+ .string()
465
+ .optional()
466
+ .default(defaultRegion)
467
+ .describe('Region to deploy the service to'),
468
+ service: z
469
+ .string()
470
+ .optional()
471
+ .default(defaultServiceName)
472
+ .describe('Name of the Cloud Run service to deploy to'),
473
+ files: z
474
+ .array(
475
+ z.object({
476
+ filename: z
477
+ .string()
478
+ .describe(
479
+ 'Name and path of the file (e.g. "src/index.js" or "data/config.json")'
480
+ ),
481
+ content: z
482
+ .string()
483
+ .optional()
484
+ .describe('Text content of the file'),
485
+ })
486
+ )
487
+ .describe('Array of file objects containing filename and content'),
488
+ },
489
+ },
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
+ }
502
+
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`);
507
+ }
508
+ }
509
+
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
+ };
536
+ }
537
+ }
538
+ );
539
+
540
+ server.registerTool(
541
+ 'deploy_container_image',
542
+ {
543
+ description:
544
+ 'Deploys a container image to Cloud Run. Use this tool if the user provides a container image URL.',
545
+ inputSchema: {
546
+ project: z
547
+ .string()
548
+ .describe(
549
+ 'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
550
+ )
551
+ .default(defaultProjectId),
552
+ region: z
553
+ .string()
554
+ .optional()
555
+ .default(defaultRegion)
556
+ .describe('Region to deploy the service to'),
557
+ service: z
558
+ .string()
559
+ .optional()
560
+ .default(defaultServiceName)
561
+ .describe('Name of the Cloud Run service to deploy to'),
562
+ imageUrl: z
563
+ .string()
564
+ .describe(
565
+ 'The URL of the container image to deploy (e.g. "gcr.io/cloudrun/hello")'
566
+ ),
567
+ },
568
+ },
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
+ }
580
+
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
+ };
607
+ }
608
+ }
609
+ );
610
+ };
611
+
612
+ export const registerToolsRemote = async (
613
+ server,
614
+ {
615
+ defaultProjectId,
616
+ defaultRegion,
617
+ defaultServiceName,
618
+ skipIamCheck = false,
619
+ } = {}
620
+ ) => {
621
+ // We no longer call checkGCP here; the effective defaults are passed in.
622
+ const currentProject = defaultProjectId; // Use the passed effective project ID
623
+ const currentRegion = defaultRegion; // Use the passed effective region
624
+
625
+ if (!currentProject) {
626
+ throw new Error(
627
+ 'Cannot register remote tools: GCP project ID could not be determined. Please ensure GOOGLE_CLOUD_PROJECT environment variable is set or the server is running on GCP.'
628
+ );
629
+ }
630
+
631
+ // Listing Cloud Run services (Remote)
632
+ server.registerTool(
633
+ 'list_services',
634
+ {
635
+ description: `Lists Cloud Run services in GCP project ${currentProject} and a given region.`,
636
+ inputSchema: {
637
+ region: z
638
+ .string()
639
+ .describe('Region where the services are located')
640
+ .default(currentRegion),
641
+ },
642
+ },
643
+ async ({ region }) => {
644
+ try {
645
+ const services = await listServices(currentProject, region);
646
+ const serviceList = services
647
+ .map((s) => {
648
+ const serviceName = s.name.split('/').pop();
649
+ return `- ${serviceName} (URL: ${s.uri})`;
650
+ })
651
+ .join('\n');
652
+ return {
653
+ content: [
654
+ {
655
+ type: 'text',
656
+ text: `Services in project ${currentProject} (location ${region}):\n${serviceList}`,
657
+ },
658
+ ],
659
+ };
660
+ } catch (error) {
661
+ return {
662
+ content: [
663
+ {
664
+ type: 'text',
665
+ text: `Error listing services for project ${currentProject} (region ${currentRegion}): ${error.message}`,
666
+ },
667
+ ],
668
+ };
669
+ }
670
+ }
671
+ );
672
+
673
+ // Dynamic resource for getting a specific service (Remote)
674
+ server.registerTool(
675
+ 'get_service',
676
+ {
677
+ description: `Gets details for a specific Cloud Run service in GCP project ${currentProject}.`,
678
+ inputSchema: {
679
+ region: z
680
+ .string()
681
+ .describe('Region where the service is located')
682
+ .default(currentRegion),
683
+ service: z
684
+ .string()
685
+ .describe('Name of the Cloud Run service')
686
+ .default(defaultServiceName),
687
+ },
688
+ },
689
+ async ({ region, service }) => {
690
+ if (typeof service !== 'string') {
691
+ return {
692
+ content: [
693
+ { type: 'text', text: 'Error: Service name must be provided.' },
694
+ ],
695
+ };
696
+ }
697
+ try {
698
+ const serviceDetails = await getService(
699
+ currentProject,
700
+ region,
701
+ service
702
+ );
703
+ if (serviceDetails) {
704
+ return {
705
+ content: [
706
+ {
707
+ type: 'text',
708
+ text: `Name: ${service}\nRegion: ${region}\nProject: ${currentProject}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
709
+ },
710
+ ],
711
+ };
712
+ } else {
713
+ return {
714
+ content: [
715
+ {
716
+ type: 'text',
717
+ text: `Service ${service} not found in project ${currentProject} (region ${currentRegion}).`,
718
+ },
719
+ ],
720
+ };
721
+ }
722
+ } catch (error) {
723
+ return {
724
+ content: [
725
+ {
726
+ type: 'text',
727
+ text: `Error getting service ${service} in project ${currentProject} (region ${currentRegion}): ${error.message}`,
728
+ },
729
+ ],
730
+ };
731
+ }
732
+ }
733
+ );
734
+
735
+ // Logs for a service
736
+ server.registerTool(
737
+ 'get_service_log',
738
+ {
739
+ description:
740
+ 'Gets Logs and Error Messages for a specific Cloud Run service.',
741
+ inputSchema: {
742
+ project: z
743
+ .string()
744
+ .describe('Google Cloud project ID containing the service')
745
+ .default(currentProject), // Use currentProject
746
+ region: z
747
+ .string()
748
+ .describe('Region where the service is located')
749
+ .default(currentRegion), // Use currentRegion
750
+ service: z
751
+ .string()
752
+ .describe('Name of the Cloud Run service')
753
+ .default(defaultServiceName),
754
+ },
755
+ },
756
+ async ({ project, region, service }) => {
757
+ let allLogs = [];
758
+ let requestOptions;
759
+ try {
760
+ do {
761
+ // Fetch a page of logs
762
+ const response = await getServiceLogs(
763
+ project,
764
+ region,
765
+ service,
766
+ requestOptions
767
+ );
768
+
769
+ if (response.logs) {
770
+ allLogs.push(response.logs);
771
+ }
772
+
773
+ // Set the requestOptions incl pagintion token for the next iteration
774
+
775
+ requestOptions = response.requestOptions;
776
+ } while (requestOptions); // Continue as long as there is a next page token
777
+ return {
778
+ content: [
779
+ {
780
+ type: 'text',
781
+ text: allLogs.join('\n'),
782
+ },
783
+ ],
784
+ };
785
+ } catch (error) {
786
+ return {
787
+ content: [
788
+ {
789
+ type: 'text',
790
+ text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
791
+ },
792
+ ],
793
+ };
794
+ }
795
+ }
796
+ );
797
+
798
+ // Deploy file contents to Cloud Run (Remote)
799
+ server.registerTool(
800
+ 'deploy_file_contents',
801
+ {
802
+ description: `Deploy files to Cloud Run by providing their contents directly to the GCP project ${currentProject}.`,
803
+ inputSchema: {
804
+ region: z
805
+ .string()
806
+ .optional()
807
+ .default(currentRegion)
808
+ .describe('Region to deploy the service to'),
809
+ service: z
810
+ .string()
811
+ .optional()
812
+ .default(defaultServiceName)
813
+ .describe('Name of the Cloud Run service to deploy to'), // Use defaultServiceName
814
+ files: z
815
+ .array(
816
+ z.object({
817
+ filename: z
818
+ .string()
819
+ .describe(
820
+ 'Name and path of the file (e.g. "src/index.js" or "data/config.json")'
821
+ ),
822
+ content: z.string().describe('Text content of the file'),
823
+ })
824
+ )
825
+ .describe('Array of file objects containing filename and content'),
826
+ },
827
+ },
828
+ async ({ region, service, files }) => {
829
+ console.log(
830
+ `New deploy request (remote): ${JSON.stringify({ project: currentProject, region, service, files })}`
831
+ );
832
+
833
+ if (
834
+ typeof files !== 'object' ||
835
+ !Array.isArray(files) ||
836
+ files.length === 0
837
+ ) {
838
+ throw new Error('Files must be specified');
839
+ }
840
+
841
+ // Validate that each file has content
842
+ for (const file of files) {
843
+ if (!file.content) {
844
+ throw new Error(`File ${file.filename} must have content`);
845
+ }
846
+ }
847
+
848
+ // Deploy to Cloud Run
849
+ try {
850
+ const response = await deploy({
851
+ projectId: currentProject,
852
+ serviceName: service,
853
+ region: region,
854
+ files: files,
855
+ skipIamCheck: skipIamCheck, // Pass the new flag
856
+ });
857
+ return {
858
+ content: [
859
+ {
860
+ type: 'text',
861
+ text: `Cloud Run service ${service} deployed in project ${currentProject}\nCloud Console: https://console.cloud. google.com/run/detail/${region}/${service}?project=${currentProject}\nService URL: ${response.uri}`,
862
+ },
863
+ ],
864
+ };
865
+ } catch (error) {
866
+ return {
867
+ content: [
868
+ {
869
+ type: 'text',
870
+ text: `Error deploying to Cloud Run: ${error.message || error}`,
871
+ },
872
+ ],
873
+ };
874
+ }
875
+ }
876
+ );
877
+
878
+ server.registerTool(
879
+ 'deploy_container_image',
880
+ {
881
+ description: `Deploys a container image to Cloud Run in the GCP project ${currentProject}. Use this tool if the user provides a container image URL.`,
882
+ inputSchema: {
883
+ region: z
884
+ .string()
885
+ .optional()
886
+ .default(currentRegion)
887
+ .describe('Region to deploy the service to'),
888
+ service: z
889
+ .string()
890
+ .optional()
891
+ .default(defaultServiceName)
892
+ .describe('Name of the Cloud Run service to deploy to'),
893
+ imageUrl: z
894
+ .string()
895
+ .describe(
896
+ 'The URL of the container image to deploy (e.g. "gcr.io/cloudrun/hello")'
897
+ ),
898
+ },
899
+ },
900
+ async ({ region, service, imageUrl }) => {
901
+ if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
902
+ throw new Error(
903
+ 'Container image URL must be specified and be a non-empty string.'
904
+ );
905
+ }
906
+
907
+ // Deploy to Cloud Run
908
+ try {
909
+ const response = await deployImage({
910
+ projectId: currentProject,
911
+ serviceName: service,
912
+ region: region,
913
+ imageUrl: imageUrl,
914
+ skipIamCheck: skipIamCheck,
915
+ });
916
+ return {
917
+ content: [
918
+ {
919
+ type: 'text',
920
+ text: `Cloud Run service ${service} deployed in project ${currentProject}\nCloud Console: https://console.cloud. google.com/run/detail/${region}/${service}?project=${currentProject}\nService URL: ${response.uri}`,
921
+ },
922
+ ],
923
+ };
924
+ } catch (error) {
925
+ return {
926
+ content: [
927
+ {
928
+ type: 'text',
929
+ text: `Error deploying to Cloud Run: ${error.message || error}`,
930
+ },
931
+ ],
932
+ };
933
+ }
934
+ }
935
+ );
936
+ };