@google-cloud/cloud-run-mcp 1.9.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 (50) 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/gemini-extension.json +12 -0
  14. package/lib/clients.js +104 -3
  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 +11 -0
  21. package/lib/deployment/deployer.js +334 -30
  22. package/lib/deployment/helpers.js +99 -49
  23. package/lib/deployment/source-processor.js +0 -48
  24. package/lib/deployment/universal-maker.js +147 -0
  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 +4 -0
  29. package/package.json +6 -3
  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 +224 -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 +126 -79
  40. package/test/local/source-processor.test.js +0 -165
  41. package/test/local/test-utils.js +2 -18
  42. package/test/local/tools.test.js +43 -1
  43. package/test/local/universal-maker.test.js +157 -0
  44. package/test/local/util.test.js +361 -0
  45. package/test/need-gcp/deployer.test.js +1 -1
  46. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  47. package/tools/register-tools.js +19 -3
  48. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  49. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  50. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -75,171 +75,6 @@ describe('Source Processor', () => {
75
75
  assert.ok(cpCall, 'Should call cp -R for directories');
76
76
  });
77
77
 
78
- test('installDependencies runs npm install for Node.js', async () => {
79
- // We need to mock fs.existsSync as well
80
- const fsMock = {
81
- existsSync: mock.fn(),
82
- };
83
-
84
- const sourceProcessor = await esmock(
85
- '../../lib/deployment/source-processor.js',
86
- {
87
- 'node:fs': fsMock,
88
- 'node:fs/promises': fspMock,
89
- 'node:child_process': childProcessMock,
90
- }
91
- );
92
-
93
- const targetDir = '/temp/dir';
94
- const deploymentAttrs = {
95
- runtime: 'nodejs',
96
- cmd: ['node', 'server.js'],
97
- args: ['server.js'],
98
- baseImage: 'node:22',
99
- };
100
- const progressCallback = mock.fn();
101
-
102
- // Mock existsSync
103
- fsMock.existsSync.mock.mockImplementation((p) => {
104
- if (p.includes('node_modules')) return false; // not installed yet
105
- if (p.includes('package-lock.json')) return false; // no lock file
106
- return false;
107
- });
108
-
109
- // Mock exec for npm install
110
- childProcessMock.exec.mock.mockImplementation((cmd, opts, callback) => {
111
- if (typeof opts === 'function') callback = opts;
112
- if (callback) callback(null, 'ok', '');
113
- });
114
-
115
- await sourceProcessor.installDependencies(
116
- targetDir,
117
- deploymentAttrs,
118
- progressCallback
119
- );
120
-
121
- const calls = childProcessMock.exec.mock.calls;
122
- const npmCall = calls.find((call) =>
123
- call.arguments[0].includes('npm install')
124
- );
125
- assert.ok(npmCall, 'Should run npm install');
126
- assert.strictEqual(npmCall.arguments[1].cwd, targetDir);
127
- });
128
-
129
- test('installDependencies uses npm ci if lockfile exists', async () => {
130
- const fsMock = {
131
- existsSync: mock.fn(),
132
- };
133
-
134
- const sourceProcessor = await esmock(
135
- '../../lib/deployment/source-processor.js',
136
- {
137
- 'node:fs': fsMock,
138
- 'node:child_process': childProcessMock,
139
- }
140
- );
141
-
142
- const targetDir = '/temp/dir';
143
- const deploymentAttrs = {
144
- runtime: 'nodejs',
145
- cmd: ['node', 'server.js'],
146
- args: ['server.js'],
147
- baseImage: 'node:22',
148
- };
149
-
150
- // Mock existsSync
151
- fsMock.existsSync.mock.mockImplementation((p) => {
152
- if (p.includes('node_modules')) return false;
153
- if (p.includes('package-lock.json')) return true; // has lock file
154
- return false;
155
- });
156
-
157
- childProcessMock.exec.mock.mockImplementation((cmd, opts, callback) => {
158
- if (typeof opts === 'function') callback = opts;
159
- if (callback) callback(null, 'ok', '');
160
- });
161
-
162
- await sourceProcessor.installDependencies(
163
- targetDir,
164
- deploymentAttrs,
165
- () => {}
166
- );
167
-
168
- const calls = childProcessMock.exec.mock.calls;
169
- const npmCall = calls.find((call) => call.arguments[0].includes('npm ci'));
170
- assert.ok(npmCall, 'Should run npm ci');
171
- });
172
-
173
- test('installDependencies skips if node_modules exists', async () => {
174
- const fsMock = {
175
- existsSync: mock.fn(),
176
- };
177
-
178
- const sourceProcessor = await esmock(
179
- '../../lib/deployment/source-processor.js',
180
- {
181
- 'node:fs': fsMock,
182
- 'node:child_process': childProcessMock,
183
- }
184
- );
185
-
186
- fsMock.existsSync.mock.mockImplementation((p) => {
187
- if (p.includes('node_modules')) return true;
188
- return false;
189
- });
190
-
191
- // clear mock calls
192
- childProcessMock.exec.mock.resetCalls();
193
-
194
- await sourceProcessor.installDependencies(
195
- '/dir',
196
- {
197
- runtime: 'nodejs',
198
- cmd: ['node', 'server.js'],
199
- args: ['server.js'],
200
- baseImage: 'node:22',
201
- },
202
- () => {}
203
- );
204
-
205
- assert.equal(childProcessMock.exec.mock.callCount(), 0);
206
- });
207
-
208
- test('installDependencies logs warn on failure but does not throw', async () => {
209
- const fsMock = { existsSync: mock.fn(() => false) };
210
- const sourceProcessor = await esmock(
211
- '../../lib/deployment/source-processor.js',
212
- {
213
- 'node:fs': fsMock,
214
- 'node:child_process': childProcessMock,
215
- }
216
- );
217
-
218
- childProcessMock.exec.mock.mockImplementation((cmd, opts, callback) => {
219
- if (typeof opts === 'function') callback = opts;
220
- callback(new Error('npm failed'));
221
- });
222
-
223
- const consoleLogMock = mock.method(console, 'log');
224
-
225
- await assert.doesNotReject(async () => {
226
- await sourceProcessor.installDependencies('/dir', { runtime: 'nodejs' });
227
- });
228
-
229
- assert.equal(
230
- consoleLogMock.mock.callCount(),
231
- 1,
232
- 'console.log should be called once'
233
- );
234
- const logArgs = consoleLogMock.mock.calls[0].arguments;
235
- assert.ok(
236
- logArgs[0].includes('Dependency installation failed: npm failed'),
237
- 'console.log should contain the error message'
238
- );
239
-
240
- consoleLogMock.mock.restore();
241
- });
242
-
243
78
  test('cleanupTempDirectory removes directory', async () => {
244
79
  const fsMock = {
245
80
  existsSync: mock.fn(() => true),
@@ -1,19 +1,3 @@
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
1
  /**
18
2
  * Waits for a specific string to appear in a stream's output.
19
3
  * This is useful for synchronizing tests with asynchronous child processes,
@@ -22,11 +6,11 @@ limitations under the License.
22
6
  *
23
7
  * @param {ReadableStream} stream - The stream to listen to (e.g., process.stdout or process.stderr).
24
8
  * @param {string} str - The string to wait for in the stream's output.
25
- * @param {number} [timeoutMs=7000] - The maximum time in milliseconds to wait before rejecting.
9
+ * @param {number} [timeoutMs=10000] - The maximum time in milliseconds to wait before rejecting.
26
10
  * @returns {Promise<string>} A promise that resolves with the accumulated data
27
11
  * when the string is found, or rejects if the timeout is reached.
28
12
  */
29
- export async function waitForString(stream, str, timeoutMs = 7000) {
13
+ export async function waitForString(stream, str, timeoutMs = 10000) {
30
14
  let accumulatedData = '';
31
15
  return new Promise((resolve, reject) => {
32
16
  const timeout = setTimeout(() => {
@@ -207,6 +207,7 @@ describe('registerTools', () => {
207
207
  name: 'my-service',
208
208
  uri: 'my-uri',
209
209
  lastModifier: 'me',
210
+ updateTime: { seconds: '1735732800', nanos: 0 }, // 2025-01-01T12:00:00Z
210
211
  }),
211
212
  },
212
213
  }
@@ -223,11 +224,12 @@ describe('registerTools', () => {
223
224
  service: 'my-service',
224
225
  });
225
226
 
227
+ const expectedDate = new Date('2025-01-01T12:00:00Z').toUTCString();
226
228
  assert.deepStrictEqual(result, {
227
229
  content: [
228
230
  {
229
231
  type: 'text',
230
- text: 'Name: my-service\nRegion: my-region\nProject: my-project\nURL: my-uri\nLast deployed by: me',
232
+ text: `Name: my-service\nRegion: my-region\nProject: my-project\nURL: my-uri\nConsole URL: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nLast deployed by: me\nLast deployed date: ${expectedDate}`,
231
233
  },
232
234
  ],
233
235
  });
@@ -324,6 +326,46 @@ describe('registerTools', () => {
324
326
  ],
325
327
  });
326
328
  });
329
+
330
+ it('should deploy local folder with ingress', async () => {
331
+ const server = {
332
+ registerTool: mock.fn(),
333
+ };
334
+ const deployMock = mock.fn(() => Promise.resolve({ uri: 'my-uri' }));
335
+
336
+ const { registerTools } = await esmock(
337
+ '../../tools/tools.js',
338
+ {},
339
+ {
340
+ '../../lib/deployment/deployer.js': {
341
+ deploy: deployMock,
342
+ },
343
+ }
344
+ );
345
+
346
+ registerTools(server, {
347
+ gcpCredentialsAvailable: true,
348
+ ingress: 'INGRESS_TRAFFIC_INTERNAL_ONLY',
349
+ });
350
+
351
+ const handler = server.registerTool.mock.calls.find(
352
+ (call) => call.arguments[0] === 'deploy_local_folder'
353
+ ).arguments[2];
354
+ await handler(
355
+ {
356
+ project: 'my-project',
357
+ region: 'my-region',
358
+ service: 'my-service',
359
+ folderPath: '/my/folder',
360
+ },
361
+ { sendNotification: mock.fn() }
362
+ );
363
+
364
+ assert.strictEqual(
365
+ deployMock.mock.calls[0].arguments[0].ingress,
366
+ 'INGRESS_TRAFFIC_INTERNAL_ONLY'
367
+ );
368
+ });
327
369
  });
328
370
 
329
371
  describe('deploy_file_contents', () => {
@@ -0,0 +1,157 @@
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 { test, describe, mock } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import esmock from 'esmock';
20
+
21
+ describe('Universal Maker', () => {
22
+ const fsMock = {
23
+ existsSync: mock.fn(),
24
+ mkdirSync: mock.fn(),
25
+ readFileSync: mock.fn(),
26
+ rmSync: mock.fn(),
27
+ };
28
+
29
+ const childProcessMock = {
30
+ exec: mock.fn(),
31
+ };
32
+
33
+ const osMock = {
34
+ homedir: () => '/home/user',
35
+ tmpdir: () => '/tmp',
36
+ };
37
+
38
+ const helpersMock = {
39
+ logAndProgress: mock.fn(),
40
+ };
41
+
42
+ const artifactsMock = {
43
+ ensureRepositoryDownloaded: mock.fn(),
44
+ };
45
+
46
+ test('runUniversalMaker skips download if binary exists and runs successfully', async () => {
47
+ fsMock.existsSync.mock.resetCalls();
48
+ fsMock.readFileSync.mock.resetCalls();
49
+ childProcessMock.exec.mock.resetCalls();
50
+ artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
51
+
52
+ // Mock binary exists and is up to date
53
+ artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
54
+ () => '/home/user/.cloud-run-mcp/bin/universal_maker'
55
+ );
56
+
57
+ fsMock.existsSync.mock.mockImplementation((p) => {
58
+ if (p.includes('build_output.json')) return true;
59
+ return false;
60
+ });
61
+
62
+ fsMock.readFileSync.mock.mockImplementation((p) => {
63
+ if (p.includes('build_output.json')) {
64
+ return JSON.stringify({
65
+ command: 'node',
66
+ args: ['index.js'],
67
+ runtime: 'nodejs20',
68
+ envVars: { DEBUG: 'true' },
69
+ });
70
+ }
71
+ return '';
72
+ });
73
+
74
+ const um = await esmock('../../lib/deployment/universal-maker.js', {
75
+ fs: fsMock,
76
+ child_process: childProcessMock,
77
+ os: osMock,
78
+ '../../lib/util/helpers.js': helpersMock,
79
+ '../../lib/util/artifacts.js': artifactsMock,
80
+ });
81
+
82
+ // Mock exec only for binary execution
83
+ childProcessMock.exec.mock.mockImplementation((cmd, cb) => {
84
+ cb(null, { stdout: '', stderr: '' });
85
+ });
86
+
87
+ // runUniversalMaker(appDir, accessToken, progressCallback)
88
+ const result = await um.runUniversalMaker(
89
+ '/app/dir',
90
+ 'fake-token',
91
+ mock.fn()
92
+ );
93
+
94
+ assert.ok(result);
95
+ assert.equal(result.command, 'node');
96
+ assert.deepEqual(result.args, ['index.js']);
97
+ assert.equal(result.runtime, 'nodejs20');
98
+
99
+ // Verify ensureRepositoryDownloaded was called
100
+ assert.strictEqual(
101
+ artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
102
+ 1
103
+ );
104
+ });
105
+
106
+ test('runUniversalMaker behaves correctly when binary fails to download', async () => {
107
+ artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
108
+ artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
109
+ () => null
110
+ );
111
+
112
+ const um = await esmock('../../lib/deployment/universal-maker.js', {
113
+ fs: fsMock,
114
+ child_process: childProcessMock,
115
+ os: osMock,
116
+ '../../lib/util/helpers.js': helpersMock,
117
+ '../../lib/util/artifacts.js': artifactsMock,
118
+ });
119
+
120
+ const result = await um.runUniversalMaker(
121
+ '/app/dir',
122
+ 'fake-token',
123
+ mock.fn()
124
+ );
125
+
126
+ assert.equal(result, null);
127
+ assert.strictEqual(
128
+ artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
129
+ 1
130
+ );
131
+ });
132
+
133
+ test('runUniversalMaker returns null if binary not supported on platform', async () => {
134
+ // Save original platform
135
+ const originalPlatform = process.platform;
136
+ Object.defineProperty(process, 'platform', { value: 'win32' });
137
+
138
+ try {
139
+ const um = await esmock('../../lib/deployment/universal-maker.js', {
140
+ fs: fsMock,
141
+ child_process: childProcessMock,
142
+ os: osMock,
143
+ '../../lib/util/helpers.js': helpersMock,
144
+ '../../lib/util/artifacts.js': artifactsMock,
145
+ });
146
+
147
+ const result = await um.runUniversalMaker(
148
+ '/app/dir',
149
+ 'fake-token',
150
+ mock.fn()
151
+ );
152
+ assert.equal(result, null);
153
+ } finally {
154
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
155
+ }
156
+ });
157
+ });