@google-cloud/cloud-run-mcp 1.5.0 → 1.6.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 (37) hide show
  1. package/.c8rc.json +9 -0
  2. package/.github/workflows/lint-checker.yml +17 -0
  3. package/.kokoro/kokoro_build.sh +22 -0
  4. package/.kokoro/presubmit.cfg +21 -0
  5. package/.kokoro/run_tests.sh +31 -0
  6. package/CONTRIBUTING.md +23 -8
  7. package/README.md +52 -11
  8. package/REVIEWING.md +20 -0
  9. package/gemini-extension/GEMINI.md +1 -1
  10. package/lib/cloud-api/build.js +200 -0
  11. package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
  12. package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
  13. package/lib/cloud-api/registry.js +98 -0
  14. package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
  15. package/lib/cloud-api/storage.js +123 -0
  16. package/lib/deployment/deployer.js +474 -0
  17. package/lib/util/archive.js +95 -0
  18. package/lib/util/helpers.js +42 -0
  19. package/mcp-server.js +21 -10
  20. package/package.json +7 -4
  21. package/test/local/cloud-api/build.test.js +436 -0
  22. package/test/local/cloud-api/projects.test.js +18 -0
  23. package/test/local/mcp-server-streamable-http.test.js +57 -0
  24. package/test/local/notifications.test.js +9 -6
  25. package/test/local/tools.test.js +114 -70
  26. package/test/need-gcp/cloud-run-services.test.js +44 -44
  27. package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
  28. package/test/need-gcp/gcp-auth-check.test.js +1 -1
  29. package/test/need-gcp/gcp-projects.test.js +3 -15
  30. package/test/need-gcp/test-helpers.js +175 -0
  31. package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
  32. package/tools/register-tools.js +106 -97
  33. package/tools/tools.js +2 -9
  34. package/lib/cloud-run-deploy.js +0 -937
  35. /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
  36. /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
  37. /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
@@ -0,0 +1,100 @@
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 fs from 'fs/promises';
18
+ import assert from 'node:assert';
19
+ import { test, describe, before, after } from 'node:test';
20
+ import path from 'path';
21
+
22
+ import { deploy, deployImage } from '../../../lib/deployment/deployer.js';
23
+ import {
24
+ cleanupProject,
25
+ setSourceDeployProjectPermissions,
26
+ setupProject,
27
+ } from '../test-helpers.js';
28
+
29
+ describe('Deployment workflows', () => {
30
+ let projectId;
31
+
32
+ before(async () => {
33
+ try {
34
+ projectId = await setupProject();
35
+ await setSourceDeployProjectPermissions(projectId);
36
+ } catch (err) {
37
+ console.error('Error during project creation and setup:', err);
38
+ throw err;
39
+ }
40
+ });
41
+
42
+ test('Scenario-1: Starting deployment of hello image...', async () => {
43
+ const configImageDeploy = {
44
+ projectId: projectId,
45
+ serviceName: 'hello-scenario',
46
+ region: 'europe-west1',
47
+ imageUrl: 'gcr.io/cloudrun/hello',
48
+ };
49
+ await deployImage(configImageDeploy);
50
+
51
+ console.log('Scenario-1: Deployment completed.');
52
+ });
53
+
54
+ test('Scenario-2: Starting deployment with invalid files...', async () => {
55
+ const configFailingBuild = {
56
+ projectId: projectId,
57
+ serviceName: 'example-failing-app',
58
+ region: 'europe-west1',
59
+ files: [
60
+ {
61
+ filename: 'main.txt',
62
+ content:
63
+ 'This is not a valid application source file and should cause a build failure.',
64
+ },
65
+ ],
66
+ };
67
+ await assert.rejects(
68
+ deploy(configFailingBuild),
69
+ { message: /ERROR: failed to detect: no buildpacks participating/ },
70
+ 'Deployment should have failed with a buildpack detection error'
71
+ );
72
+ });
73
+
74
+ test('Scenario-3: Starting deployment of Go app with file content...', async () => {
75
+ const mainGoContent = await fs.readFile(
76
+ path.resolve('example-sources-to-deploy/main.go'),
77
+ 'utf-8'
78
+ );
79
+ const goModContent = await fs.readFile(
80
+ path.resolve('example-sources-to-deploy/go.mod'),
81
+ 'utf-8'
82
+ );
83
+ const configGoWithContent = {
84
+ projectId: projectId,
85
+ serviceName: 'example-go-app-content',
86
+ region: 'europe-west1',
87
+ files: [
88
+ { filename: 'main.go', content: mainGoContent },
89
+ { filename: 'go.mod', content: goModContent },
90
+ ],
91
+ };
92
+ await deploy(configGoWithContent);
93
+ console.log('Scenario-3: Deployment completed.');
94
+ });
95
+
96
+ after(async () => {
97
+ // Clean up: delete the project created for tests
98
+ cleanupProject(projectId);
99
+ });
100
+ });
@@ -15,9 +15,16 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { z } from 'zod';
18
- import { listProjects, createProjectAndAttachBilling } from '../lib/gcp-projects.js';
19
- import { listServices, getService, getServiceLogs } from '../lib/cloud-run-services.js';
20
- import { deploy, deployImage } from '../lib/cloud-run-deploy.js';
18
+ import {
19
+ listProjects,
20
+ createProjectAndAttachBilling,
21
+ } from '../lib/cloud-api/projects.js';
22
+ import {
23
+ listServices,
24
+ getService,
25
+ getServiceLogs,
26
+ } from '../lib/cloud-api/run.js';
27
+ import { deploy, deployImage } from '../lib/deployment/deployer.js';
21
28
 
22
29
  function createProgressCallback(sendNotification) {
23
30
  return (progress) => {
@@ -111,7 +118,7 @@ function registerCreateProjectTool(server, options) {
111
118
  content: [
112
119
  {
113
120
  type: 'text',
114
- text: `Successfully created GCP project with ID "${result.projectId}". You can now use this project ID for deployments.`,
121
+ text: result.billingMessage,
115
122
  },
116
123
  ],
117
124
  };
@@ -134,19 +141,15 @@ function registerListServicesTool(server, options) {
134
141
  server.registerTool(
135
142
  'list_services',
136
143
  {
137
- description: 'Lists Cloud Run services in a given project and region.',
144
+ description: 'Lists all Cloud Run services in a given project.',
138
145
  inputSchema: {
139
146
  project: z
140
147
  .string()
141
148
  .describe('Google Cloud project ID')
142
149
  .default(options.defaultProjectId),
143
- region: z
144
- .string()
145
- .describe('Region where the services are located')
146
- .default(options.defaultRegion),
147
150
  },
148
151
  },
149
- gcpTool(options.gcpCredentialsAvailable, async ({ project, region }) => {
152
+ gcpTool(options.gcpCredentialsAvailable, async ({ project }) => {
150
153
  if (typeof project !== 'string') {
151
154
  return {
152
155
  content: [
@@ -157,29 +160,29 @@ function registerListServicesTool(server, options) {
157
160
  ],
158
161
  };
159
162
  }
160
-
161
163
  try {
162
- const services = await listServices(project, region);
163
- const serviceList = services
164
- .map((s) => {
165
- const serviceName = s.name.split('/').pop();
166
- return `- ${serviceName} (URL: ${s.uri})`;
167
- })
168
- .join('\n');
169
- return {
170
- content: [
171
- {
172
- type: 'text',
173
- text: `Services in project ${project} (location ${region}):\n${serviceList}`,
174
- },
175
- ],
176
- };
164
+ const allServices = await listServices(project);
165
+ const content = [];
166
+ for (const region of Object.keys(allServices)) {
167
+ const serviceList = allServices[region];
168
+ const servicesText = serviceList
169
+ .map((s) => {
170
+ const serviceName = s.name.split('/').pop();
171
+ return `- ${serviceName} (URL: ${s.uri})`;
172
+ })
173
+ .join('\n');
174
+ content.push({
175
+ type: 'text',
176
+ text: `Services in project ${project} (location ${region}):\n${servicesText}`,
177
+ });
178
+ }
179
+ return { content };
177
180
  } catch (error) {
178
181
  return {
179
182
  content: [
180
183
  {
181
184
  type: 'text',
182
- text: `Error listing services for project ${project} (region ${region}): ${error.message}`,
185
+ text: `Error listing services for project ${project}: ${error.message}`,
183
186
  },
184
187
  ],
185
188
  };
@@ -209,53 +212,56 @@ function registerGetServiceTool(server, options) {
209
212
  .default(options.defaultServiceName),
210
213
  },
211
214
  },
212
- gcpTool(options.gcpCredentialsAvailable, async ({ project, region, service }) => {
213
- if (typeof project !== 'string') {
214
- return {
215
- content: [
216
- { type: 'text', text: 'Error: Project ID must be provided.' },
217
- ],
218
- };
219
- }
220
- if (typeof service !== 'string') {
221
- return {
222
- content: [
223
- { type: 'text', text: 'Error: Service name must be provided.' },
224
- ],
225
- };
226
- }
227
- try {
228
- const serviceDetails = await getService(project, region, service);
229
- if (serviceDetails) {
215
+ gcpTool(
216
+ options.gcpCredentialsAvailable,
217
+ async ({ project, region, service }) => {
218
+ if (typeof project !== 'string') {
230
219
  return {
231
220
  content: [
232
- {
233
- type: 'text',
234
- text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
235
- },
221
+ { type: 'text', text: 'Error: Project ID must be provided.' },
222
+ ],
223
+ };
224
+ }
225
+ if (typeof service !== 'string') {
226
+ return {
227
+ content: [
228
+ { type: 'text', text: 'Error: Service name must be provided.' },
236
229
  ],
237
230
  };
238
- } else {
231
+ }
232
+ try {
233
+ const serviceDetails = await getService(project, region, service);
234
+ if (serviceDetails) {
235
+ return {
236
+ content: [
237
+ {
238
+ type: 'text',
239
+ text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
240
+ },
241
+ ],
242
+ };
243
+ } else {
244
+ return {
245
+ content: [
246
+ {
247
+ type: 'text',
248
+ text: `Service ${service} not found in project ${project} (region ${region}).`,
249
+ },
250
+ ],
251
+ };
252
+ }
253
+ } catch (error) {
239
254
  return {
240
255
  content: [
241
256
  {
242
257
  type: 'text',
243
- text: `Service ${service} not found in project ${project} (region ${region}).`,
258
+ text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
244
259
  },
245
260
  ],
246
261
  };
247
262
  }
248
- } catch (error) {
249
- return {
250
- content: [
251
- {
252
- type: 'text',
253
- text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
254
- },
255
- ],
256
- };
257
263
  }
258
- })
264
+ )
259
265
  );
260
266
  }
261
267
 
@@ -281,46 +287,49 @@ function registerGetServiceLogTool(server, options) {
281
287
  .default(options.defaultServiceName),
282
288
  },
283
289
  },
284
- gcpTool(options.gcpCredentialsAvailable, async ({ project, region, service }) => {
285
- let allLogs = [];
286
- let requestOptions;
287
- try {
288
- do {
289
- // Fetch a page of logs
290
- const response = await getServiceLogs(
291
- project,
292
- region,
293
- service,
294
- requestOptions
295
- );
290
+ gcpTool(
291
+ options.gcpCredentialsAvailable,
292
+ async ({ project, region, service }) => {
293
+ let allLogs = [];
294
+ let requestOptions;
295
+ try {
296
+ do {
297
+ // Fetch a page of logs
298
+ const response = await getServiceLogs(
299
+ project,
300
+ region,
301
+ service,
302
+ requestOptions
303
+ );
296
304
 
297
- if (response.logs) {
298
- allLogs.push(response.logs);
299
- }
305
+ if (response.logs) {
306
+ allLogs.push(response.logs);
307
+ }
300
308
 
301
- // Set the requestOptions incl pagintion token for the next iteration
309
+ // Set the requestOptions incl pagintion token for the next iteration
302
310
 
303
- requestOptions = response.requestOptions;
304
- } while (requestOptions); // Continue as long as there is a next page token
305
- return {
306
- content: [
307
- {
308
- type: 'text',
309
- text: allLogs.join('\n'),
310
- },
311
- ],
312
- };
313
- } catch (error) {
314
- return {
315
- content: [
316
- {
317
- type: 'text',
318
- text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
319
- },
320
- ],
321
- };
311
+ requestOptions = response.requestOptions;
312
+ } while (requestOptions); // Continue as long as there is a next page token
313
+ return {
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: allLogs.join('\n'),
318
+ },
319
+ ],
320
+ };
321
+ } catch (error) {
322
+ return {
323
+ content: [
324
+ {
325
+ type: 'text',
326
+ text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
327
+ },
328
+ ],
329
+ };
330
+ }
322
331
  }
323
- })
332
+ )
324
333
  );
325
334
  }
326
335
 
package/tools/tools.js CHANGED
@@ -27,10 +27,7 @@ import {
27
27
  registerDeployContainerImageTool,
28
28
  } from './register-tools.js';
29
29
 
30
- export const registerTools = (
31
- server,
32
- options = {}
33
- ) => {
30
+ export const registerTools = (server, options = {}) => {
34
31
  registerListProjectsTool(server, options);
35
32
  registerCreateProjectTool(server, options);
36
33
  registerListServicesTool(server, options);
@@ -41,10 +38,7 @@ export const registerTools = (
41
38
  registerDeployContainerImageTool(server, options);
42
39
  };
43
40
 
44
- export const registerToolsRemote = (
45
- server,
46
- options = {}
47
- ) => {
41
+ export const registerToolsRemote = (server, options = {}) => {
48
42
  // For remote, use the same registration functions but with effective project/region passed in options
49
43
  registerListServicesTool(server, options);
50
44
  registerGetServiceTool(server, options);
@@ -52,4 +46,3 @@ export const registerToolsRemote = (
52
46
  registerDeployFileContentsTool(server, options);
53
47
  registerDeployContainerImageTool(server, options);
54
48
  };
55
-