@google-cloud/cloud-run-mcp 1.5.0 → 1.7.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/.c8rc.json +9 -0
- package/.github/workflows/lint-checker.yml +17 -0
- package/.github/workflows/npm-publish.yml +1 -1
- package/.kokoro/kokoro_build.sh +22 -0
- package/.kokoro/presubmit.cfg +21 -0
- package/.kokoro/run_tests.sh +31 -0
- package/CONTRIBUTING.md +24 -9
- package/README.md +52 -11
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +1 -1
- package/lib/cloud-api/billing.js +183 -0
- package/lib/cloud-api/build.js +200 -0
- package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +81 -35
- package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
- package/lib/cloud-api/registry.js +98 -0
- package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
- package/lib/cloud-api/storage.js +123 -0
- package/lib/deployment/constants.js +54 -0
- package/lib/deployment/deployer.js +621 -0
- package/lib/deployment/helpers.js +194 -0
- package/lib/deployment/source-processor.js +124 -0
- package/lib/util/archive.js +104 -0
- package/lib/util/helpers.js +42 -0
- package/mcp-server.js +21 -10
- package/package.json +8 -5
- package/test/local/cloud-api/build.test.js +436 -0
- package/test/local/cloud-api/projects.test.js +18 -0
- package/test/local/deployment-helpers.test.js +231 -0
- package/test/local/mcp-server-stdio.test.js +59 -0
- package/test/local/mcp-server-streamable-http.test.js +61 -0
- package/test/local/notifications.test.js +9 -6
- package/test/local/source-processor.test.js +290 -0
- package/test/local/test-utils.js +51 -0
- package/test/local/tools.test.js +114 -70
- package/test/need-gcp/cloud-run-services.test.js +44 -44
- package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +3 -15
- package/test/need-gcp/test-helpers.js +175 -0
- package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
- package/tools/register-tools.js +106 -97
- package/tools/tools.js +2 -9
- package/lib/cloud-run-deploy.js +0 -937
- package/lib/gcp-billing-accounts.js +0 -98
- /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
- /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
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 path from 'path';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import { DEPLOYMENT_CONFIG, RUNTIMES } from './constants.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the input is a single folder path.
|
|
23
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
24
|
+
* @returns {boolean} - true if files contains exactly one string which is a directory.
|
|
25
|
+
*/
|
|
26
|
+
function isFolder(files) {
|
|
27
|
+
if (files.length !== 1 || typeof files[0] !== 'string') return false;
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(files[0]).isDirectory();
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a Dockerfile exists in the provided files array.
|
|
37
|
+
* @param {Array<string|Object>} files - An array of file paths or objects with filename properties.
|
|
38
|
+
* @returns {boolean} - Returns true if a Dockerfile is found, false otherwise.
|
|
39
|
+
*/
|
|
40
|
+
function checkIfDockerFileExists(files) {
|
|
41
|
+
if (isFolder(files)) {
|
|
42
|
+
// Handle folder deployment: check for Dockerfile inside the folder
|
|
43
|
+
return (
|
|
44
|
+
fs.existsSync(path.join(files[0], 'Dockerfile')) ||
|
|
45
|
+
fs.existsSync(path.join(files[0], 'dockerfile'))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle file list deployment or file content deployment
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (typeof file === 'string') {
|
|
52
|
+
if (path.basename(file).toLowerCase() === 'dockerfile') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
} else if (typeof file === 'object' && file.filename) {
|
|
56
|
+
if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if the provided folder appears to be a Node.js project.
|
|
66
|
+
* It looks for a `package.json` file in the root of the first path in the `files` array.
|
|
67
|
+
* @param {string[]} files - Array of file paths
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function checkIfNodeJsRuntime(files) {
|
|
71
|
+
const packageJsonPath = path.join(files[0], 'package.json');
|
|
72
|
+
return fs.existsSync(packageJsonPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns an empty deployment attributes object.
|
|
77
|
+
* This is used as a default or when attributes cannot be determined.
|
|
78
|
+
* @returns {{runtime: undefined, cmd: undefined, args: undefined, baseImage: undefined}}
|
|
79
|
+
*/
|
|
80
|
+
function getEmptyDeploymentAttrs() {
|
|
81
|
+
return {
|
|
82
|
+
runtime: undefined,
|
|
83
|
+
cmd: undefined,
|
|
84
|
+
args: undefined,
|
|
85
|
+
baseImage: undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extracts deployment attributes (runtime, command, args, baseImage) from a Node.js project's `package.json`.
|
|
91
|
+
* This function is used for direct source deployments, bypassing Cloud Build.
|
|
92
|
+
* It returns empty attributes if a `start` script is missing, invalid, or if `engines.node` is specified (indicating a need for Cloud Build).
|
|
93
|
+
* @param {string[]} files - Array of file paths
|
|
94
|
+
* @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
95
|
+
*/
|
|
96
|
+
function getNodeJsDeploymentAttrs(files) {
|
|
97
|
+
const packageJsonPath = path.join(files[0], 'package.json');
|
|
98
|
+
let packageJson;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// If package.json exists but is invalid, we can't extract attributes.
|
|
104
|
+
return getEmptyDeploymentAttrs();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const startScript = packageJson?.scripts?.start;
|
|
108
|
+
const hasNodeEngine = packageJson?.engines?.node;
|
|
109
|
+
|
|
110
|
+
if (typeof startScript !== 'string' || !startScript.trim() || hasNodeEngine) {
|
|
111
|
+
return getEmptyDeploymentAttrs();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle multiple spaces properly
|
|
115
|
+
const parts = startScript.trim().split(/\s+/);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
runtime: RUNTIMES.NODEJS,
|
|
119
|
+
cmd: [parts[0]],
|
|
120
|
+
args: parts.slice(1),
|
|
121
|
+
baseImage: DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE, // latest default base image
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Determines deployment attributes based on the file list/folder content.
|
|
127
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
128
|
+
* @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
129
|
+
*/
|
|
130
|
+
function getDeploymentAttrs(files) {
|
|
131
|
+
// Currently only support detection of NodeJs runtime
|
|
132
|
+
if (!isFolder(files) || !checkIfNodeJsRuntime(files)) {
|
|
133
|
+
// TODO: support file list deployment runtime detection
|
|
134
|
+
return getEmptyDeploymentAttrs();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return getNodeJsDeploymentAttrs(files);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates metadata for the file deployment, including Dockerfile presence and runtime attributes.
|
|
142
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects to be deployed.
|
|
143
|
+
* @returns {{hasDockerfile: boolean, deploymentAttrs: {runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}}} Metadata object.
|
|
144
|
+
*/
|
|
145
|
+
export function makeFileDeploymentMetadata(files) {
|
|
146
|
+
return {
|
|
147
|
+
hasDockerfile: checkIfDockerFileExists(files),
|
|
148
|
+
deploymentAttrs: getDeploymentAttrs(files),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if a zip-based source deployment (no-build) is feasible.
|
|
154
|
+
* @param {object} metadata - The deployment metadata.
|
|
155
|
+
* @param {boolean} metadata.hasDockerfile - Whether a Dockerfile is present.
|
|
156
|
+
* @param {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined}} metadata.deploymentAttrs - Deployment attributes.
|
|
157
|
+
* @returns {boolean} True if zip deployment is feasible.
|
|
158
|
+
*/
|
|
159
|
+
export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
|
|
160
|
+
return !!(
|
|
161
|
+
!hasDockerfile &&
|
|
162
|
+
deploymentAttrs &&
|
|
163
|
+
deploymentAttrs.runtime &&
|
|
164
|
+
deploymentAttrs.cmd &&
|
|
165
|
+
deploymentAttrs.args
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Creates the container spec for a direct source deployment (no Cloud Build).
|
|
171
|
+
* @param {object} params - The parameters for creating the container spec.
|
|
172
|
+
* @param {string} params.bucketName - The GCS bucket name where the source is uploaded.
|
|
173
|
+
* @param {string} params.fileName - The GCS object name (e.g., source.tar.gz).
|
|
174
|
+
* @param {{cmd: string[], args: string[], baseImage: string}} params.deploymentAttrs - Deployment attributes including command, args, and the base image to use.
|
|
175
|
+
* @returns {object} The container specification object for the Cloud Run service.
|
|
176
|
+
*/
|
|
177
|
+
export function createDirectSourceDeploymentContainer({
|
|
178
|
+
bucketName,
|
|
179
|
+
fileName,
|
|
180
|
+
deploymentAttrs,
|
|
181
|
+
}) {
|
|
182
|
+
return {
|
|
183
|
+
image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
|
|
184
|
+
baseImageUri: deploymentAttrs.baseImage,
|
|
185
|
+
sourceCode: {
|
|
186
|
+
cloudStorageSource: {
|
|
187
|
+
bucket: bucketName,
|
|
188
|
+
object: fileName,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
command: deploymentAttrs.cmd,
|
|
192
|
+
args: deploymentAttrs.args,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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 path from 'node:path';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import fsp from 'node:fs/promises';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import util from 'node:util';
|
|
22
|
+
import { exec } from 'node:child_process';
|
|
23
|
+
import { TEMP_PATHS, RUNTIMES } from './constants.js';
|
|
24
|
+
import { logAndProgress } from '../util/helpers.js';
|
|
25
|
+
|
|
26
|
+
const execAsync = util.promisify(exec);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prepares a temporary directory with the source code for deployment.
|
|
30
|
+
* Handles both file paths and content objects.
|
|
31
|
+
*
|
|
32
|
+
* @param {Array<string|{filename: string, content: string|Buffer}>} files - Files to deploy.
|
|
33
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
34
|
+
* @returns {Promise<string>} The path to the prepared temporary directory.
|
|
35
|
+
*/
|
|
36
|
+
export async function prepareSourceDirectory(files, progressCallback) {
|
|
37
|
+
const homeDir = os.homedir();
|
|
38
|
+
const tempBaseDir = path.join(homeDir, TEMP_PATHS.BASE, TEMP_PATHS.SUBDIR);
|
|
39
|
+
const sourceName = path.basename(files[0]);
|
|
40
|
+
|
|
41
|
+
const tempDir = path.join(tempBaseDir, `${sourceName}-${Date.now()}`);
|
|
42
|
+
|
|
43
|
+
// Creates a temporary directory to store the source code.
|
|
44
|
+
await fsp.mkdir(tempDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
// Copies the source code to the temporary directory.
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const stats = await fsp.stat(file);
|
|
49
|
+
if (stats.isDirectory()) {
|
|
50
|
+
// Copy directory contents
|
|
51
|
+
await execAsync(`cp -R "${file}/." "${tempDir}/"`);
|
|
52
|
+
} else {
|
|
53
|
+
const destPath = path.join(tempDir, path.basename(file));
|
|
54
|
+
await fsp.copyFile(file, destPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return tempDir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Installs Node.js dependencies in the target directory if a package.json exists.
|
|
63
|
+
* Skips if node_modules already exists (assumed pre-installed or copied).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} targetDir - The directory containing the source code.
|
|
66
|
+
*/
|
|
67
|
+
async function installNodeDependencies(targetDir) {
|
|
68
|
+
const nodeModulesPath = path.join(targetDir, 'node_modules');
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
71
|
+
console.log(
|
|
72
|
+
`Existing node_modules detected. Skipping dependency installation.`
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for lock file to decide between install and ci
|
|
78
|
+
const hasLockFile = fs.existsSync(path.join(targetDir, 'package-lock.json'));
|
|
79
|
+
const npmCommand = hasLockFile
|
|
80
|
+
? 'npm ci --omit=dev'
|
|
81
|
+
: 'npm install --omit=dev';
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await execAsync(npmCommand, { cwd: targetDir });
|
|
85
|
+
console.log(`${npmCommand} completed successfully`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log(`Dependency installation failed: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generic wrapper to install dependencies for the detected runtime.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} targetDir - The directory containing the source code.
|
|
95
|
+
* @param {Object} deploymentAttrs - Deployment attributes containing runtime info.
|
|
96
|
+
*/
|
|
97
|
+
export async function installDependencies(targetDir, deploymentAttrs) {
|
|
98
|
+
switch (deploymentAttrs?.runtime) {
|
|
99
|
+
case RUNTIMES.NODEJS:
|
|
100
|
+
await installNodeDependencies(targetDir);
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
console.log(
|
|
104
|
+
`No specific dependency installation logic for runtime: ${deploymentAttrs?.runtime}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cleans up the temporary directory.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} dirPath - Path to the directory to remove.
|
|
113
|
+
*/
|
|
114
|
+
export async function cleanupTempDirectory(dirPath) {
|
|
115
|
+
if (!dirPath) return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(dirPath)) {
|
|
119
|
+
await fsp.rm(dirPath, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
} catch (cleanupError) {
|
|
122
|
+
console.error(`Failed to cleanup temp directory ${dirPath}:`, cleanupError);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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 {boolean} [useTarGz] - If true, creates a tar.gz archive instead of a zip.
|
|
27
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
28
|
+
* @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
|
|
29
|
+
* @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
|
|
30
|
+
*/
|
|
31
|
+
export async function zipFiles(files, useTarGz, progressCallback) {
|
|
32
|
+
const path = await import('path');
|
|
33
|
+
const fs = await import('fs');
|
|
34
|
+
const archiver = (await import('archiver')).default;
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
logAndProgress('Creating in-memory archive...', progressCallback);
|
|
38
|
+
const chunks = [];
|
|
39
|
+
|
|
40
|
+
let archive;
|
|
41
|
+
if (useTarGz) {
|
|
42
|
+
archive = archiver('tar', {
|
|
43
|
+
gzip: { level: 9 },
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
archive = archiver('zip', {
|
|
47
|
+
zlib: { level: 9 },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
archive.on('data', (chunk) => chunks.push(chunk));
|
|
52
|
+
archive.on('end', () => {
|
|
53
|
+
logAndProgress(
|
|
54
|
+
`Archive created successfully. Total size: ${archive.pointer()} bytes`,
|
|
55
|
+
progressCallback
|
|
56
|
+
);
|
|
57
|
+
resolve(Buffer.concat(chunks));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
archive.on('warning', (err) => {
|
|
61
|
+
const warningMessage = `Archiver warning: ${err}`;
|
|
62
|
+
logAndProgress(warningMessage, progressCallback, 'warn');
|
|
63
|
+
if (err.code !== 'ENOENT') {
|
|
64
|
+
// ENOENT is often just a warning, others might be more critical for zip
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
archive.on('error', (err) => {
|
|
70
|
+
const errorMessage = `Archiver error: ${err.message}`;
|
|
71
|
+
console.error(errorMessage, err);
|
|
72
|
+
logAndProgress(errorMessage, progressCallback, 'error');
|
|
73
|
+
reject(err);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
files.forEach((file) => {
|
|
77
|
+
if (typeof file === 'object' && 'filename' in file && 'content' in file) {
|
|
78
|
+
archive.append(file.content, { name: file.filename });
|
|
79
|
+
} else if (typeof file === 'string') {
|
|
80
|
+
let pathInput = file;
|
|
81
|
+
|
|
82
|
+
// 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'
|
|
83
|
+
if (pathInput.startsWith('/c')) {
|
|
84
|
+
pathInput = `/mnt${pathInput}`;
|
|
85
|
+
}
|
|
86
|
+
const filePath = path.resolve(pathInput);
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
throw new Error(`File or directory not found: ${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stats = fs.statSync(filePath);
|
|
92
|
+
if (stats.isDirectory()) {
|
|
93
|
+
archive.directory(filePath, false);
|
|
94
|
+
} else {
|
|
95
|
+
archive.file(filePath, { name: path.basename(filePath) });
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
archive.finalize();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -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
|
|
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/
|
|
30
|
-
import { ensureGCPCredentials } from './lib/
|
|
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
|
-
|
|
129
|
+
// non-stdio mode
|
|
130
|
+
console.log('Stdio transport mode is turned off.');
|
|
129
131
|
gcpCredentialsAvailable = await ensureGCPCredentials();
|
|
130
|
-
|
|
131
|
-
app
|
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@google-cloud/cloud-run-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cloud Run MCP deployment tool",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
|
|
12
|
-
"test:
|
|
12
|
+
"test:workflows": "node --test test/need-gcp/workflows/*.test.js",
|
|
13
|
+
"test:deploy": "node test/need-gcp/deployer.test.js",
|
|
13
14
|
"test:projects": "node test/need-gcp/gcp-projects.test.js",
|
|
14
15
|
"test:services": "node test/need-gcp/cloud-run-services.test.js",
|
|
15
16
|
"test:gcp-auth": "node test/need-gcp/gcp-auth-check.test.js",
|
|
16
17
|
"test:prompts": "node --test test/local/prompts.test.js",
|
|
17
18
|
"test:mcp": "node --test test/local/mcp-server.test.js",
|
|
18
19
|
"test:tools": "node --test test/local/tools.test.js",
|
|
19
|
-
"test": "node --test test/local/*.test.js",
|
|
20
|
+
"test:local": "node --test test/local/*.test.js test/local/**/*.test.js",
|
|
21
|
+
"test": "c8 --check-coverage npm run test:local",
|
|
20
22
|
"mcp-inspector": "npx @modelcontextprotocol/inspector node mcp-server.js",
|
|
21
23
|
"start": "node mcp-server.js",
|
|
22
24
|
"lint:check": "prettier --check .",
|
|
@@ -43,10 +45,10 @@
|
|
|
43
45
|
"@google-cloud/cloudbuild": "^5.0.1",
|
|
44
46
|
"@google-cloud/logging": "^11.2.0",
|
|
45
47
|
"@google-cloud/resource-manager": "^6.0.1",
|
|
46
|
-
"@google-cloud/run": "^
|
|
48
|
+
"@google-cloud/run": "^3.1.0",
|
|
47
49
|
"@google-cloud/service-usage": "^4.1.0",
|
|
48
50
|
"@google-cloud/storage": "^7.16.0",
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
50
52
|
"archiver": "^7.0.1",
|
|
51
53
|
"dotenv": "^17.2.1",
|
|
52
54
|
"express": "^5.1.0",
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
"zod": "^3.25.76"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
59
|
+
"c8": "^10.1.3",
|
|
57
60
|
"esmock": "^2.7.1",
|
|
58
61
|
"prettier": "3.6.2"
|
|
59
62
|
}
|