@google-cloud/cloud-run-mcp 1.10.0 → 1.11.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 (47) hide show
  1. package/README.md +24 -0
  2. package/SECURITY.md +7 -0
  3. package/cloudbuild.yaml +37 -0
  4. package/example-sources-to-deploy/java/pom.xml +41 -0
  5. package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
  6. package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
  7. package/example-sources-to-deploy/nodejs/index.js +15 -0
  8. package/example-sources-to-deploy/nodejs/package.json +12 -0
  9. package/example-sources-to-deploy/python/pip-project/main.py +11 -0
  10. package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
  11. package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
  12. package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
  13. package/lib/clients.js +104 -3
  14. package/lib/cloud-api/build.js +192 -28
  15. package/lib/cloud-api/run.js +62 -0
  16. package/lib/cloud-api/secrets.js +196 -0
  17. package/lib/cloud-api/storage.js +164 -16
  18. package/lib/deployment/compose.js +517 -0
  19. package/lib/deployment/constants.js +8 -0
  20. package/lib/deployment/deployer.js +283 -28
  21. package/lib/deployment/helpers.js +58 -1
  22. package/lib/deployment/universal-maker.js +21 -148
  23. package/lib/util/archive.js +3 -3
  24. package/lib/util/artifacts.js +189 -0
  25. package/lib/util/helpers.js +109 -0
  26. package/mcp-server.js +4 -0
  27. package/package.json +6 -3
  28. package/skills/cloud-run/SKILL.md +109 -0
  29. package/test/local/archive.test.js +88 -0
  30. package/test/local/clients.test.js +224 -2
  31. package/test/local/cloud-api/build.test.js +330 -25
  32. package/test/local/cloud-api/run.test.js +129 -0
  33. package/test/local/cloud-api/secrets.test.js +291 -0
  34. package/test/local/cloud-api/storage.test.js +405 -0
  35. package/test/local/compose.test.js +618 -0
  36. package/test/local/deploy.test.js +408 -0
  37. package/test/local/deployment-helpers.test.js +100 -1
  38. package/test/local/test-utils.js +2 -18
  39. package/test/local/tools.test.js +43 -1
  40. package/test/local/universal-maker.test.js +23 -114
  41. package/test/local/util.test.js +361 -0
  42. package/test/need-gcp/deployer.test.js +1 -1
  43. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  44. package/tools/register-tools.js +19 -3
  45. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  46. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  47. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -26,6 +26,27 @@ import {
26
26
  setupProject,
27
27
  } from '../test-helpers.js';
28
28
 
29
+ const GCP_REGION = 'asia-southeast1';
30
+ const RUN_INGRESS_POLICY = process.env.RUN_INGRESS_POLICY || undefined;
31
+ const ZIP_DEPLOY_SUCCESS_MESSAGE =
32
+ 'Deployment completed successfully with zip deploy';
33
+ const SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE =
34
+ 'Deployment completed successfully with source build deploy';
35
+
36
+ async function assertDeploymentSuccess(config, expectedMessage) {
37
+ let successMessage = '';
38
+ config.progressCallback = (p) => {
39
+ if (p.data === expectedMessage) {
40
+ successMessage = p.data;
41
+ }
42
+ };
43
+ if (RUN_INGRESS_POLICY) {
44
+ config.ingress = RUN_INGRESS_POLICY;
45
+ }
46
+ await deploy(config);
47
+ assert.strictEqual(successMessage, expectedMessage);
48
+ }
49
+
29
50
  describe('Deployment workflows', () => {
30
51
  let projectId;
31
52
 
@@ -33,6 +54,8 @@ describe('Deployment workflows', () => {
33
54
  try {
34
55
  projectId = await setupProject();
35
56
  await setSourceDeployProjectPermissions(projectId);
57
+ console.log('Waiting 2 minutes for IAM propagation...');
58
+ await new Promise((resolve) => setTimeout(resolve, 120000));
36
59
  } catch (err) {
37
60
  console.error('Error during project creation and setup:', err);
38
61
  throw err;
@@ -43,9 +66,12 @@ describe('Deployment workflows', () => {
43
66
  const configImageDeploy = {
44
67
  projectId: projectId,
45
68
  serviceName: 'hello-scenario',
46
- region: 'us-central1',
69
+ region: GCP_REGION,
47
70
  imageUrl: 'gcr.io/cloudrun/hello',
48
71
  };
72
+ if (RUN_INGRESS_POLICY) {
73
+ configImageDeploy.ingress = RUN_INGRESS_POLICY;
74
+ }
49
75
  await deployImage(configImageDeploy);
50
76
 
51
77
  console.log('Scenario-1: Deployment completed.');
@@ -55,7 +81,7 @@ describe('Deployment workflows', () => {
55
81
  const configFailingBuild = {
56
82
  projectId: projectId,
57
83
  serviceName: 'example-failing-app',
58
- region: 'us-central1',
84
+ region: GCP_REGION,
59
85
  files: [
60
86
  {
61
87
  filename: 'main.txt',
@@ -64,6 +90,9 @@ describe('Deployment workflows', () => {
64
90
  },
65
91
  ],
66
92
  };
93
+ if (RUN_INGRESS_POLICY) {
94
+ configFailingBuild.ingress = RUN_INGRESS_POLICY;
95
+ }
67
96
  await assert.rejects(
68
97
  deploy(configFailingBuild),
69
98
  { message: /ERROR: failed to detect: no buildpacks participating/ },
@@ -73,26 +102,163 @@ describe('Deployment workflows', () => {
73
102
 
74
103
  test('Scenario-3: Starting deployment of Go app with file content...', async () => {
75
104
  const mainGoContent = await fs.readFile(
76
- path.resolve('example-sources-to-deploy/main.go'),
105
+ path.resolve('example-sources-to-deploy/golang/main.go'),
77
106
  'utf-8'
78
107
  );
79
108
  const goModContent = await fs.readFile(
80
- path.resolve('example-sources-to-deploy/go.mod'),
109
+ path.resolve('example-sources-to-deploy/golang/go.mod'),
81
110
  'utf-8'
82
111
  );
83
112
  const configGoWithContent = {
84
113
  projectId: projectId,
85
114
  serviceName: 'example-go-app-content',
86
- region: 'us-central1',
115
+ region: GCP_REGION,
87
116
  files: [
88
117
  { filename: 'main.go', content: mainGoContent },
89
118
  { filename: 'go.mod', content: goModContent },
90
119
  ],
91
120
  };
92
- await deploy(configGoWithContent);
121
+ await assertDeploymentSuccess(
122
+ configGoWithContent,
123
+ SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
124
+ );
93
125
  console.log('Scenario-3: Deployment completed.');
94
126
  });
95
127
 
128
+ test('Scenario-4: Starting deployment of pip-based Python app with folder path... uses zip deploy', async () => {
129
+ const configPipProject = {
130
+ projectId: projectId,
131
+ serviceName: 'example-pip-project-folder-path',
132
+ region: GCP_REGION,
133
+ files: ['example-sources-to-deploy/python/pip-project'],
134
+ };
135
+ await assertDeploymentSuccess(configPipProject, ZIP_DEPLOY_SUCCESS_MESSAGE);
136
+ console.log('Scenario-4: Deployment completed.');
137
+ });
138
+
139
+ test('Scenario-5: Starting deployment of pip-based Python app with file-based content... uses source build deploy', async () => {
140
+ const mainPyContent = await fs.readFile(
141
+ path.resolve('example-sources-to-deploy/python/pip-project/main.py'),
142
+ 'utf-8'
143
+ );
144
+ const requirementsTxtContent = await fs.readFile(
145
+ path.resolve(
146
+ 'example-sources-to-deploy/python/pip-project/requirements.txt'
147
+ ),
148
+ 'utf-8'
149
+ );
150
+ const configPipProject = {
151
+ projectId: projectId,
152
+ serviceName: 'example-pip-project-file-content',
153
+ region: GCP_REGION,
154
+ files: [
155
+ { filename: 'main.py', content: mainPyContent },
156
+ { filename: 'requirements.txt', content: requirementsTxtContent },
157
+ ],
158
+ };
159
+ await assertDeploymentSuccess(
160
+ configPipProject,
161
+ SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
162
+ );
163
+ console.log('Scenario-5: Deployment completed.');
164
+ });
165
+
166
+ test('Scenario-6: Starting deployment of pyproject-based Python app with folder path... uses zip deploy', async () => {
167
+ const configPyprojectProject = {
168
+ projectId: projectId,
169
+ serviceName: 'example-pyproject-project-folder-path',
170
+ region: GCP_REGION,
171
+ files: ['example-sources-to-deploy/python/pyproject-project'],
172
+ };
173
+ await assertDeploymentSuccess(
174
+ configPyprojectProject,
175
+ ZIP_DEPLOY_SUCCESS_MESSAGE
176
+ );
177
+ console.log('Scenario-6: Deployment completed.');
178
+ });
179
+
180
+ test('Scenario-7: Starting deployment of pyproject-based Python app with file-based content... uses source build deploy', async () => {
181
+ const mainPyContent = await fs.readFile(
182
+ path.resolve(
183
+ 'example-sources-to-deploy/python/pyproject-project/main.py'
184
+ ),
185
+ 'utf-8'
186
+ );
187
+ const pyprojectContent = await fs.readFile(
188
+ path.resolve(
189
+ 'example-sources-to-deploy/python/pyproject-project/pyproject.toml'
190
+ ),
191
+ 'utf-8'
192
+ );
193
+ const configPyprojectProject = {
194
+ projectId: projectId,
195
+ serviceName: 'example-pyproject-project-file-content',
196
+ region: GCP_REGION,
197
+ files: [
198
+ { filename: 'main.py', content: mainPyContent },
199
+ { filename: 'pyproject.toml', content: pyprojectContent },
200
+ ],
201
+ };
202
+ await assertDeploymentSuccess(
203
+ configPyprojectProject,
204
+ SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
205
+ );
206
+ console.log('Scenario-7: Deployment completed.');
207
+ });
208
+
209
+ test('Scenario-8: Starting deployment of Node.js app with folder path... uses zip deploy', async () => {
210
+ const configNodeProject = {
211
+ projectId: projectId,
212
+ serviceName: 'example-node-project-folder-path',
213
+ region: GCP_REGION,
214
+ files: ['example-sources-to-deploy/nodejs'],
215
+ };
216
+ await assertDeploymentSuccess(
217
+ configNodeProject,
218
+ ZIP_DEPLOY_SUCCESS_MESSAGE
219
+ );
220
+ console.log('Scenario-8: Deployment completed.');
221
+ });
222
+
223
+ test('Scenario-9: Starting deployment of Node.js app with file content... uses build deploy', async () => {
224
+ const packageJsonContent = await fs.readFile(
225
+ path.resolve('example-sources-to-deploy/nodejs/package.json'),
226
+ 'utf-8'
227
+ );
228
+ const indexJsContent = await fs.readFile(
229
+ path.resolve('example-sources-to-deploy/nodejs/index.js'),
230
+ 'utf-8'
231
+ );
232
+ const configNodeWithContent = {
233
+ projectId: projectId,
234
+ serviceName: 'example-node-project-file-content',
235
+ region: GCP_REGION,
236
+ files: [
237
+ { filename: 'package.json', content: packageJsonContent },
238
+ { filename: 'index.js', content: indexJsContent },
239
+ ],
240
+ };
241
+ await assertDeploymentSuccess(
242
+ configNodeWithContent,
243
+ SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
244
+ );
245
+ console.log('Scenario-9: Deployment completed.');
246
+ });
247
+
248
+ test('Scenario-10: Starting deployment of Java app with folder path... uses source build deploy', async () => {
249
+ const configJavaProject = {
250
+ projectId: projectId,
251
+ serviceName: 'example-java-project-folder-path',
252
+ region: GCP_REGION,
253
+ files: ['example-sources-to-deploy/java'],
254
+ };
255
+ await assertDeploymentSuccess(
256
+ configJavaProject,
257
+ SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
258
+ );
259
+ console.log('Scenario-10: Deployment completed.');
260
+ });
261
+
96
262
  after(async () => {
97
263
  // Clean up: delete the project created for tests
98
264
  cleanupProject(projectId);
@@ -241,11 +241,21 @@ function registerGetServiceTool(server, options) {
241
241
  options.accessToken
242
242
  );
243
243
  if (serviceDetails) {
244
+ // Format last deployment date, handling both Protobuf Timestamp (with
245
+ // seconds as string or number) and standard Date/ISO strings.
246
+ const lastDeployedDate = serviceDetails.updateTime
247
+ ? new Date(
248
+ serviceDetails.updateTime.seconds
249
+ ? Number(serviceDetails.updateTime.seconds) * 1000
250
+ : serviceDetails.updateTime
251
+ ).toUTCString()
252
+ : 'N/A';
253
+ const consoleUrl = `https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}`;
244
254
  return {
245
255
  content: [
246
256
  {
247
257
  type: 'text',
248
- text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
258
+ text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nConsole URL: ${consoleUrl}\nLast deployed by: ${serviceDetails.lastModifier}\nLast deployed date: ${lastDeployedDate}`,
249
259
  },
250
260
  ],
251
261
  };
@@ -366,7 +376,9 @@ function registerDeployLocalFolderTool(server, options) {
366
376
  .string()
367
377
  .optional()
368
378
  .default(options.defaultServiceName)
369
- .describe('Name of the Cloud Run service to deploy to'),
379
+ .describe(
380
+ 'Name of the Cloud Run service to deploy to. If not provided, it will be inferred.'
381
+ ),
370
382
  folderPath: z
371
383
  .string()
372
384
  .describe(
@@ -405,13 +417,15 @@ function registerDeployLocalFolderTool(server, options) {
405
417
  files: [folderPath],
406
418
  skipIamCheck: options.skipIamCheck, // Pass the new flag
407
419
  accessToken: options.accessToken,
420
+ ingress: options.ingress,
408
421
  progressCallback,
409
422
  });
423
+ const serviceName = service || response.service;
410
424
  return {
411
425
  content: [
412
426
  {
413
427
  type: 'text',
414
- 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}`,
428
+ text: `Cloud Run service ${serviceName} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${serviceName}?project=${project}\nService URL: ${response.uri}`,
415
429
  },
416
430
  ],
417
431
  };
@@ -507,6 +521,7 @@ function registerDeployFileContentsTool(server, options) {
507
521
  files: files,
508
522
  skipIamCheck: options.skipIamCheck, // Pass the new flag
509
523
  accessToken: options.accessToken,
524
+ ingress: options.ingress,
510
525
  progressCallback,
511
526
  });
512
527
  return {
@@ -588,6 +603,7 @@ function registerDeployContainerImageTool(server, options) {
588
603
  imageUrl: imageUrl,
589
604
  skipIamCheck: options.skipIamCheck,
590
605
  accessToken: options.accessToken,
606
+ ingress: options.ingress,
591
607
  progressCallback,
592
608
  });
593
609
  return {