@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,474 @@
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 { callWithRetry, ensureApisEnabled } from '../cloud-api/helpers.js';
18
+ import { zipFiles } from '../util/archive.js';
19
+ import {
20
+ ensureStorageBucketExists,
21
+ uploadToStorageBucket,
22
+ } from '../cloud-api/storage.js';
23
+ import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
24
+ import { triggerCloudBuild } from '../cloud-api/build.js';
25
+ import { logAndProgress } from '../util/helpers.js';
26
+ import { checkCloudRunServiceExists } from '../cloud-api/run.js';
27
+
28
+ // Configuration
29
+ const REPO_NAME = 'mcp-cloud-run-deployments';
30
+ const ZIP_FILE_NAME = 'source.zip';
31
+ const IMAGE_TAG = 'latest';
32
+
33
+ // APIs required for deploying from source code.
34
+ const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
35
+ 'iam.googleapis.com',
36
+ 'storage.googleapis.com',
37
+ 'cloudbuild.googleapis.com',
38
+ 'artifactregistry.googleapis.com',
39
+ 'run.googleapis.com',
40
+ 'serviceusage.googleapis.com',
41
+ ];
42
+
43
+ // APIs required for deploying a container image.
44
+ const REQUIRED_APIS_FOR_IMAGE_DEPLOY = [
45
+ 'run.googleapis.com',
46
+ 'serviceusage.googleapis.com',
47
+ ];
48
+
49
+ /**
50
+ * Deploys or updates a Cloud Run service with the specified container image.
51
+ * If the service exists, it's updated; otherwise, a new service is created.
52
+ * The service is configured to be publicly accessible.
53
+ *
54
+ * @async
55
+ * @param {object} context - The context object containing clients and other parameters.
56
+ * @param {string} projectId - The Google Cloud project ID.
57
+ * @param {string} location - The Google Cloud region for the deployment.
58
+ * @param {string} serviceId - The ID for the Cloud Run service.
59
+ * @param {string} imgUrl - The URL of the container image to deploy.
60
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
61
+ * @returns {Promise<object>} A promise that resolves with the Cloud Run service object upon successful deployment or update.
62
+ * @throws {Error} If the deployment or update process fails.
63
+ */
64
+ async function deployToCloudRun(
65
+ context,
66
+ projectId,
67
+ location,
68
+ serviceId,
69
+ imgUrl,
70
+ progressCallback,
71
+ skipIamCheck
72
+ ) {
73
+ const parent = context.runClient.locationPath(projectId, location);
74
+ const servicePath = context.runClient.servicePath(
75
+ projectId,
76
+ location,
77
+ serviceId
78
+ );
79
+ const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
80
+
81
+ const service = {
82
+ template: {
83
+ revision: revisionName,
84
+ containers: [{ image: imgUrl }],
85
+ },
86
+ labels: {
87
+ 'created-by': 'cloud-run-mcp',
88
+ },
89
+ };
90
+
91
+ // Conditionally set invokerIamDisabled based on the skipIamCheck flag
92
+ if (skipIamCheck) {
93
+ service.invokerIamDisabled = true;
94
+ }
95
+
96
+ try {
97
+ const exists = await checkCloudRunServiceExists(
98
+ context,
99
+ projectId,
100
+ location,
101
+ serviceId,
102
+ progressCallback
103
+ );
104
+
105
+ // Always perform a dry run first for general validation
106
+ try {
107
+ await logAndProgress(
108
+ `Performing dry run for service ${serviceId}...`,
109
+ progressCallback,
110
+ 'debug'
111
+ );
112
+ const dryRunServiceConfig = JSON.parse(JSON.stringify(service)); // Deep copy for dry run
113
+
114
+ if (exists) {
115
+ dryRunServiceConfig.name = servicePath;
116
+ await callWithRetry(
117
+ () =>
118
+ context.runClient.updateService({
119
+ service: dryRunServiceConfig,
120
+ validateOnly: true,
121
+ }),
122
+ `updateService (dry run) ${serviceId}`
123
+ );
124
+ } else {
125
+ await callWithRetry(
126
+ () =>
127
+ context.runClient.createService({
128
+ parent: parent,
129
+ service: dryRunServiceConfig,
130
+ serviceId: serviceId,
131
+ validateOnly: true,
132
+ }),
133
+ `createService (dry run) ${serviceId}`
134
+ );
135
+ }
136
+ await logAndProgress(
137
+ `Dry run successful for ${serviceId} with current configuration.`,
138
+ progressCallback,
139
+ 'debug'
140
+ );
141
+ } catch (dryRunError) {
142
+ await logAndProgress(
143
+ `Dry run for ${serviceId} failed: ${dryRunError.message}`,
144
+ progressCallback,
145
+ 'warn'
146
+ );
147
+
148
+ // Check if the error is related to invokerIamDisabled (this is a heuristic)
149
+ if (
150
+ skipIamCheck &&
151
+ dryRunError.message &&
152
+ (dryRunError.message.toLowerCase().includes('invokeriamdisabled') ||
153
+ dryRunError.message.toLowerCase().includes('iam policy violation') ||
154
+ dryRunError.code === 3) /* INVALID_ARGUMENT */
155
+ ) {
156
+ await logAndProgress(
157
+ `Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
158
+ progressCallback,
159
+ 'warn'
160
+ );
161
+ delete service.invokerIamDisabled; // Modify the main service object for actual deployment
162
+ } else {
163
+ // For any other validation errors, rethrow to stop the deployment
164
+ const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
165
+ await logAndProgress(errorMessage, progressCallback, 'error');
166
+ throw new Error(errorMessage);
167
+ }
168
+ }
169
+
170
+ let operation;
171
+ if (exists) {
172
+ await logAndProgress(
173
+ `Updating existing service ${serviceId}...`,
174
+ progressCallback
175
+ );
176
+ service.name = servicePath;
177
+ [operation] = await callWithRetry(
178
+ () => context.runClient.updateService({ service }),
179
+ `updateService ${serviceId}`
180
+ );
181
+ } else {
182
+ await logAndProgress(
183
+ `Creating new service ${serviceId}...`,
184
+ progressCallback
185
+ );
186
+ [operation] = await callWithRetry(
187
+ () =>
188
+ context.runClient.createService({
189
+ parent: parent,
190
+ service: service, // 'service' object might have invokerIamDisabled removed
191
+ serviceId: serviceId,
192
+ }),
193
+ `createService ${serviceId}`
194
+ );
195
+ }
196
+
197
+ await logAndProgress(
198
+ `Deploying ${serviceId} to Cloud Run...`,
199
+ progressCallback
200
+ );
201
+ const [response] = await operation.promise();
202
+
203
+ await logAndProgress(
204
+ `Service deployed/updated successfully: ${response.uri}`,
205
+ progressCallback
206
+ );
207
+ return response;
208
+ } catch (error) {
209
+ const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
210
+ console.error(`Error deploying/updating service ${serviceId}:`, error);
211
+ await logAndProgress(errorMessage, progressCallback, 'error');
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Deploys a service to Google Cloud Run.
218
+ * @param {object} config - The deployment configuration.
219
+ * @param {string} config.projectId - The Google Cloud project ID.
220
+ * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
221
+ * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
222
+ * @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
223
+ * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
224
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
225
+ * @throws {Error} If deployment fails or required configuration is missing.
226
+ */
227
+ export async function deploy({
228
+ projectId,
229
+ serviceName,
230
+ region,
231
+ files,
232
+ progressCallback,
233
+ skipIamCheck,
234
+ }) {
235
+ if (!projectId) {
236
+ const errorMsg =
237
+ 'Error: projectId is required in the configuration object.';
238
+ await logAndProgress(errorMsg, progressCallback, 'error');
239
+ throw new Error(errorMsg);
240
+ }
241
+
242
+ if (!serviceName) {
243
+ const errorMsg =
244
+ 'Error: serviceName is required in the configuration object.';
245
+ await logAndProgress(errorMsg, progressCallback, 'error');
246
+ throw new Error(errorMsg);
247
+ }
248
+
249
+ if (!files || !Array.isArray(files) || files.length === 0) {
250
+ const errorMsg =
251
+ 'Error: files array is required in the configuration object.';
252
+ await logAndProgress(errorMsg, progressCallback, 'error');
253
+ if (typeof process !== 'undefined' && process.exit) {
254
+ process.exit(1);
255
+ } else {
256
+ throw new Error(errorMsg);
257
+ }
258
+ }
259
+
260
+ const path = await import('path');
261
+ const fs = await import('fs');
262
+ const { Storage } = await import('@google-cloud/storage');
263
+ const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
264
+ const { ArtifactRegistryClient } = await import(
265
+ '@google-cloud/artifact-registry'
266
+ );
267
+ const { v2: CloudRunV2Module } = await import('@google-cloud/run');
268
+ const { ServicesClient } = CloudRunV2Module;
269
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
270
+ const { Logging } = await import('@google-cloud/logging');
271
+
272
+ try {
273
+ const context = {
274
+ storage: new Storage({ projectId }),
275
+ cloudBuildClient: new CloudBuildClient({ projectId }),
276
+ artifactRegistryClient: new ArtifactRegistryClient({ projectId }),
277
+ runClient: new ServicesClient({ projectId }),
278
+ serviceUsageClient: new ServiceUsageClient({ projectId }),
279
+ loggingClient: new Logging({ projectId }),
280
+ };
281
+
282
+ await ensureApisEnabled(
283
+ context,
284
+ projectId,
285
+ REQUIRED_APIS_FOR_SOURCE_DEPLOY,
286
+ progressCallback
287
+ );
288
+
289
+ const bucketName = `${projectId}-source-bucket`;
290
+ const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
291
+
292
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
293
+ await logAndProgress(`Region: ${region}`, progressCallback);
294
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
295
+ await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
296
+
297
+ let hasDockerfile = false;
298
+ if (
299
+ files.length === 1 &&
300
+ typeof files[0] === 'string' &&
301
+ fs.statSync(files[0]).isDirectory()
302
+ ) {
303
+ // Handle folder deployment: check for Dockerfile inside the folder
304
+ const dockerfilePath = path.join(files[0], 'Dockerfile');
305
+ const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
306
+ if (
307
+ fs.existsSync(dockerfilePath) ||
308
+ fs.existsSync(dockerfilePathLowerCase)
309
+ ) {
310
+ hasDockerfile = true;
311
+ }
312
+ } else {
313
+ // Handle file list deployment or file content deployment
314
+ for (const file of files) {
315
+ if (typeof file === 'string') {
316
+ if (path.basename(file).toLowerCase() === 'dockerfile') {
317
+ hasDockerfile = true;
318
+ break;
319
+ }
320
+ } else if (typeof file === 'object' && file.filename) {
321
+ if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
322
+ hasDockerfile = true;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ }
328
+ await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
329
+
330
+ const bucket = await ensureStorageBucketExists(
331
+ context,
332
+ bucketName,
333
+ region,
334
+ progressCallback
335
+ );
336
+
337
+ const zipBuffer = await zipFiles(files, progressCallback);
338
+ await uploadToStorageBucket(
339
+ context,
340
+ bucket,
341
+ zipBuffer,
342
+ ZIP_FILE_NAME,
343
+ progressCallback
344
+ );
345
+ await logAndProgress('Source code uploaded successfully', progressCallback);
346
+
347
+ await ensureArtifactRegistryRepoExists(
348
+ context,
349
+ projectId,
350
+ region,
351
+ REPO_NAME,
352
+ 'DOCKER',
353
+ progressCallback
354
+ );
355
+
356
+ const buildResult = await triggerCloudBuild(
357
+ context,
358
+ projectId,
359
+ region,
360
+ bucketName,
361
+ ZIP_FILE_NAME,
362
+ REPO_NAME,
363
+ imageUrl,
364
+ hasDockerfile,
365
+ progressCallback
366
+ );
367
+
368
+ const builtImageUrl = buildResult.results.images[0].name;
369
+
370
+ const service = await deployToCloudRun(
371
+ context,
372
+ projectId,
373
+ region,
374
+ serviceName,
375
+ builtImageUrl,
376
+ progressCallback,
377
+ skipIamCheck
378
+ );
379
+
380
+ await logAndProgress(`Deployment Completed Successfully`, progressCallback);
381
+ return service;
382
+ } catch (error) {
383
+ const deployFailedMessage = `Deployment Failed: ${error.message}`;
384
+ console.error(`Deployment Failed`, error);
385
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
386
+ throw error;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Deploys a container image to Google Cloud Run.
392
+ * @param {object} config - The deployment configuration.
393
+ * @param {string} config.projectId - The Google Cloud project ID.
394
+ * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
395
+ * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
396
+ * @param {string} config.imageUrl - The URL of the container image to deploy.
397
+ * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
398
+ * @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
399
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
400
+ * @throws {Error} If deployment fails or required configuration is missing.
401
+ */
402
+ export async function deployImage({
403
+ projectId,
404
+ serviceName,
405
+ region,
406
+ imageUrl,
407
+ progressCallback,
408
+ skipIamCheck,
409
+ }) {
410
+ if (!projectId) {
411
+ const errorMsg =
412
+ 'Error: projectId is required in the configuration object.';
413
+ await logAndProgress(errorMsg, progressCallback, 'error');
414
+ throw new Error(errorMsg);
415
+ }
416
+
417
+ if (!serviceName) {
418
+ const errorMsg =
419
+ 'Error: serviceName is required in the configuration object.';
420
+ await logAndProgress(errorMsg, progressCallback, 'error');
421
+ throw new Error(errorMsg);
422
+ }
423
+
424
+ if (!imageUrl) {
425
+ const errorMsg = 'Error: imageUrl is required in the configuration object.';
426
+ await logAndProgress(errorMsg, progressCallback, 'error');
427
+ if (typeof process !== 'undefined' && process.exit) {
428
+ process.exit(1);
429
+ } else {
430
+ throw new Error(errorMsg);
431
+ }
432
+ }
433
+
434
+ const { v2: CloudRunV2Module } = await import('@google-cloud/run');
435
+ const { ServicesClient } = CloudRunV2Module;
436
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
437
+
438
+ try {
439
+ const context = {
440
+ runClient: new ServicesClient({ projectId }),
441
+ serviceUsageClient: new ServiceUsageClient({ projectId }),
442
+ };
443
+
444
+ await ensureApisEnabled(
445
+ context,
446
+ projectId,
447
+ REQUIRED_APIS_FOR_IMAGE_DEPLOY,
448
+ progressCallback
449
+ );
450
+
451
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
452
+ await logAndProgress(`Region: ${region}`, progressCallback);
453
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
454
+ await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
455
+
456
+ const service = await deployToCloudRun(
457
+ context,
458
+ projectId,
459
+ region,
460
+ serviceName,
461
+ imageUrl,
462
+ progressCallback,
463
+ skipIamCheck
464
+ );
465
+
466
+ await logAndProgress(`Deployment Completed Successfully`, progressCallback);
467
+ return service;
468
+ } catch (error) {
469
+ const deployFailedMessage = `Deployment Failed: ${error.message}`;
470
+ console.error(`Deployment Failed`, error);
471
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
472
+ throw error;
473
+ }
474
+ }
@@ -0,0 +1,95 @@
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 { logAndProgress } from './helpers.js';
18
+
19
+ /**
20
+ * Creates a zip archive in memory from a list of file paths and/or file objects.
21
+ * File objects should have `filename` (string) and `content` (Buffer or string) properties.
22
+ *
23
+ * @param {Array<string|{filename: string, content: Buffer|string}>} files - An array of items to zip.
24
+ * Each item can be a string representing a file/directory path, or an object
25
+ * with `filename` and `content` properties for in-memory files.
26
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
27
+ * @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
28
+ * @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
29
+ */
30
+ export async function zipFiles(files, progressCallback) {
31
+ const path = await import('path');
32
+ const fs = await import('fs');
33
+ const archiver = (await import('archiver')).default;
34
+
35
+ return new Promise((resolve, reject) => {
36
+ logAndProgress('Creating in-memory zip archive...', progressCallback);
37
+ const chunks = [];
38
+ const archive = archiver('zip', {
39
+ zlib: { level: 9 },
40
+ });
41
+
42
+ archive.on('data', (chunk) => chunks.push(chunk));
43
+ archive.on('end', () => {
44
+ logAndProgress(
45
+ `Files zipped successfully. Total size: ${archive.pointer()} bytes`,
46
+ progressCallback
47
+ );
48
+ resolve(Buffer.concat(chunks));
49
+ });
50
+
51
+ archive.on('warning', (err) => {
52
+ const warningMessage = `Archiver warning: ${err}`;
53
+ logAndProgress(warningMessage, progressCallback, 'warn');
54
+ if (err.code !== 'ENOENT') {
55
+ // ENOENT is often just a warning, others might be more critical for zip
56
+ reject(err);
57
+ }
58
+ });
59
+
60
+ archive.on('error', (err) => {
61
+ const errorMessage = `Archiver error: ${err.message}`;
62
+ console.error(errorMessage, err);
63
+ logAndProgress(errorMessage, progressCallback, 'error');
64
+ reject(err);
65
+ });
66
+
67
+ files.forEach((file) => {
68
+ if (typeof file === 'object' && 'filename' in file && 'content' in file) {
69
+ archive.append(file.content, { name: file.filename });
70
+ } else if (typeof file === 'string') {
71
+ let pathInput = file;
72
+
73
+ // This is a "hack" to better support WSL on Windows. AI agents tend to send path that start with '/c' in that case. Re-write it to '/mnt/c'
74
+ if (pathInput.startsWith('/c')) {
75
+ pathInput = `/mnt${pathInput}`;
76
+ }
77
+ const filePath = path.resolve(pathInput);
78
+ if (!fs.existsSync(filePath)) {
79
+ throw new Error(`File or directory not found: ${filePath}`);
80
+ }
81
+
82
+ const stats = fs.statSync(filePath);
83
+ if (stats.isDirectory()) {
84
+ archive.directory(filePath, false);
85
+ } else {
86
+ archive.file(filePath, { name: path.basename(filePath) });
87
+ }
88
+ } else {
89
+ throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
90
+ }
91
+ });
92
+
93
+ archive.finalize();
94
+ });
95
+ }
@@ -0,0 +1,42 @@
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
+ /**
18
+ * Helper function to log a message and call the progress callback.
19
+ * @param {string} message - The message to log.
20
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
21
+ * @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
22
+ */
23
+ export async function logAndProgress(
24
+ message,
25
+ progressCallback,
26
+ severity = 'info'
27
+ ) {
28
+ switch (severity) {
29
+ case 'error':
30
+ console.error(message);
31
+ break;
32
+ case 'warn':
33
+ case 'info':
34
+ case 'debug':
35
+ default:
36
+ console.log(message);
37
+ break;
38
+ }
39
+ if (progressCallback) {
40
+ progressCallback({ level: severity, data: message });
41
+ }
42
+ }
package/mcp-server.js CHANGED
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
16
16
  limitations under the License.
17
17
  */
18
18
 
19
- import express from 'express';
19
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
20
20
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
21
21
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
22
22
  // Support SSE for backward compatibility
@@ -26,8 +26,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
26
26
  import { registerTools, registerToolsRemote } from './tools/tools.js';
27
27
  import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
28
28
  import { registerPrompts } from './prompts.js';
29
- import { checkGCP } from './lib/gcp-metadata.js';
30
- import { ensureGCPCredentials } from './lib/gcp-auth-check.js';
29
+ import { checkGCP } from './lib/cloud-api/metadata.js';
30
+ import { ensureGCPCredentials } from './lib/cloud-api/auth.js';
31
31
  import 'dotenv/config';
32
32
 
33
33
  const gcpInfo = await checkGCP();
@@ -43,10 +43,7 @@ function makeLoggingCompatibleWithStdio() {
43
43
  }
44
44
 
45
45
  function shouldStartStdio() {
46
- if (process.env.GCP_STDIO) {
47
- return true;
48
- }
49
- if (gcpInfo && gcpInfo.project) {
46
+ if (process.env.GCP_STDIO === 'false' || (gcpInfo && gcpInfo.project)) {
50
47
  return false;
51
48
  }
52
49
  return true;
@@ -61,6 +58,10 @@ const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
61
58
  const envRegion = process.env.GOOGLE_CLOUD_REGION;
62
59
  const defaultServiceName = process.env.DEFAULT_SERVICE_NAME;
63
60
  const skipIamCheck = process.env.SKIP_IAM_CHECK !== 'false';
61
+ const enableHostValidation = process.env.ENABLE_HOST_VALIDATION === 'true';
62
+ const allowedHosts = process.env.ALLOWED_HOSTS
63
+ ? process.env.ALLOWED_HOSTS.split(',')
64
+ : undefined;
64
65
 
65
66
  async function getServer() {
66
67
  // Create an MCP server with implementation details
@@ -125,10 +126,20 @@ if (shouldStartStdio()) {
125
126
  await server.connect(stdioTransport);
126
127
  console.log('Cloud Run MCP server stdio transport connected');
127
128
  } else {
128
- console.log('Running on GCP, stdio transport will not be started.');
129
+ // non-stdio mode
130
+ console.log('Stdio transport mode is turned off.');
129
131
  gcpCredentialsAvailable = await ensureGCPCredentials();
130
- const app = express();
131
- app.use(express.json());
132
+
133
+ const app = enableHostValidation
134
+ ? createMcpExpressApp({ allowedHosts })
135
+ : createMcpExpressApp({ host: null });
136
+
137
+ if (!enableHostValidation) {
138
+ console.warn(
139
+ `Warning: Server is running without DNS rebinding protection. ` +
140
+ 'Consider enabling host validation by passing env variable ENABLE_HOST_VALIDATION=true and adding the ALLOWED_HOSTS to restrict allowed hosts'
141
+ );
142
+ }
132
143
 
133
144
  app.post('/mcp', async (req, res) => {
134
145
  console.log('/mcp Received:', req.body);