@google-cloud/cloud-run-mcp 1.6.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.
@@ -2,7 +2,7 @@ name: Publish to npm
2
2
 
3
3
  on:
4
4
  release:
5
- types: [created]
5
+ types: [published]
6
6
 
7
7
  jobs:
8
8
  publish-npm:
package/CONTRIBUTING.md CHANGED
@@ -126,7 +126,7 @@ This script requires an existing Google Cloud Project ID to be provided when pro
126
126
 
127
127
  ## Publishing to npm
128
128
 
129
- To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
129
+ To update the [npm package](https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
130
130
 
131
131
  Run the following:
132
132
 
@@ -96,3 +96,88 @@ export async function attachProjectToBillingAccount(
96
96
  return null;
97
97
  }
98
98
  }
99
+
100
+ /**
101
+ * Checks if billing is enabled for a given project.
102
+ * @async
103
+ * @function isBillingEnabled
104
+ * @param {string} projectId - The ID of the project to check.
105
+ * @returns {Promise<boolean>} A promise that resolves to true if billing is enabled, false otherwise.
106
+ */
107
+ export async function isBillingEnabled(projectId) {
108
+ const { CloudBillingClient } = await import('@google-cloud/billing');
109
+ const client = new CloudBillingClient();
110
+ const projectName = `projects/${projectId}`;
111
+ try {
112
+ // getProjectBillingInfo requires cloudbilling.googleapis.com API to check billing status
113
+ // https://docs.cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo
114
+ const [billingInfo] = await client.getProjectBillingInfo({
115
+ name: projectName,
116
+ });
117
+ return billingInfo.billingEnabled;
118
+ } catch (error) {
119
+ console.error(
120
+ `Error checking billing status for project ${projectId}:`,
121
+ error.message || error
122
+ );
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Ensures billing is enabled for a project. If not, attempts to enable it
129
+ * by attaching the single open billing account if one is found.
130
+ * @param {object} context - The context object containing clients and other parameters.
131
+ * @param {string} projectId The project ID to check billing for.
132
+ * @param {function} progressCallback A callback function for progress updates.
133
+ */
134
+ export async function ensureBillingEnabled(
135
+ context,
136
+ projectId,
137
+ progressCallback
138
+ ) {
139
+ if (!(await isBillingEnabled(projectId))) {
140
+ // Billing is disabled, try to fix it.
141
+ const accounts = await listBillingAccounts();
142
+
143
+ if (accounts && accounts.length === 1 && accounts[0].open) {
144
+ // Exactly one open account found, try to attach it.
145
+ const account = accounts[0];
146
+ const attemptMessage = `Billing is not enabled for project ${projectId}. Found one open billing account: ${account.displayName} (${account.name}). Attempting to attach it...`;
147
+ console.log(attemptMessage);
148
+ if (progressCallback)
149
+ progressCallback({ level: 'info', data: attemptMessage });
150
+
151
+ const attachmentResult = await attachProjectToBillingAccount(
152
+ projectId,
153
+ account.name
154
+ );
155
+
156
+ if (!attachmentResult || !attachmentResult.billingEnabled) {
157
+ const attachFailMessage = `Failed to automatically attach project ${projectId} to billing account ${account.name}. Please enable billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
158
+ if (progressCallback)
159
+ progressCallback({ level: 'error', data: attachFailMessage });
160
+ throw new Error(attachFailMessage);
161
+ }
162
+ const attachSuccessMessage = `Successfully attached project ${projectId} to billing account ${account.name}.`;
163
+ console.log(attachSuccessMessage);
164
+ if (progressCallback)
165
+ progressCallback({ level: 'info', data: attachSuccessMessage });
166
+ // If we get here, billing is now enabled, and we can proceed to API checks.
167
+ } else {
168
+ // Cannot auto-attach. Throw error.
169
+ let reason;
170
+ if (!accounts || accounts.length === 0) {
171
+ reason = 'no billing accounts were found';
172
+ } else if (accounts.length > 1) {
173
+ reason = 'multiple billing accounts were found';
174
+ } else {
175
+ reason = `the only available billing account '${accounts[0].displayName}' is not open`;
176
+ }
177
+ const errorMessage = `Billing is not enabled for project ${projectId}, and it could not be enabled automatically because ${reason}. Please enable billing to use Google Cloud services: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
178
+ if (progressCallback)
179
+ progressCallback({ level: 'error', data: errorMessage });
180
+ throw new Error(errorMessage);
181
+ }
182
+ }
183
+ }
@@ -14,6 +14,13 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { ensureBillingEnabled } from './billing.js';
18
+
19
+ const PREREQUISITE_APIS = [
20
+ 'serviceusage.googleapis.com',
21
+ 'cloudbilling.googleapis.com',
22
+ ];
23
+
17
24
  /**
18
25
  * Calls a function with retry logic for GCP API calls.
19
26
  * Retries on gRPC error code 7 (PERMISSION_DENIED).
@@ -84,6 +91,54 @@ async function checkAndEnableApi(
84
91
  }
85
92
  }
86
93
 
94
+ /**
95
+ * Enables a single Google Cloud API with retry logic.
96
+ * @param {object} serviceUsageClient - The Service Usage client.
97
+ * @param {string} projectId - The Google Cloud project ID.
98
+ * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
99
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
100
+ * @returns {Promise<void>} A promise that resolves when the API is enabled.
101
+ * @throws {Error} If the API fails to enable or if there's an issue checking its status.
102
+ */
103
+ export async function enableApiWithRetry(
104
+ serviceUsageClient,
105
+ projectId,
106
+ api,
107
+ progressCallback
108
+ ) {
109
+ const serviceName = `projects/${projectId}/services/${api}`;
110
+ try {
111
+ await checkAndEnableApi(
112
+ serviceUsageClient,
113
+ serviceName,
114
+ api,
115
+ progressCallback
116
+ );
117
+ } catch (error) {
118
+ // First attempt failed, log a warning and retry once after a delay.
119
+ const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
120
+ console.warn(warnMsg);
121
+ if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
122
+
123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
124
+ try {
125
+ await checkAndEnableApi(
126
+ serviceUsageClient,
127
+ serviceName,
128
+ api,
129
+ progressCallback
130
+ );
131
+ } catch (retryError) {
132
+ // If the retry also fails, throw an error.
133
+ const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
134
+ console.error(errorMessage, retryError);
135
+ if (progressCallback)
136
+ progressCallback({ level: 'error', data: errorMessage });
137
+ throw new Error(errorMessage);
138
+ }
139
+ }
140
+ }
141
+
87
142
  /**
88
143
  * Ensures that the specified Google Cloud APIs are enabled for the given project.
89
144
  * If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
@@ -104,42 +159,29 @@ export async function ensureApisEnabled(
104
159
  apis,
105
160
  progressCallback
106
161
  ) {
162
+ for (const api of PREREQUISITE_APIS) {
163
+ await enableApiWithRetry(
164
+ context.serviceUsageClient,
165
+ projectId,
166
+ api,
167
+ progressCallback
168
+ );
169
+ }
170
+
171
+ // Before enabling other APIs, ensure billing is enabled.
172
+ await ensureBillingEnabled(context, projectId, progressCallback);
173
+
107
174
  const message = 'Checking and enabling required APIs...';
108
175
  console.log(message);
109
176
  if (progressCallback) progressCallback({ level: 'info', data: message });
110
177
 
111
178
  for (const api of apis) {
112
- const serviceName = `projects/${projectId}/services/${api}`;
113
- try {
114
- await checkAndEnableApi(
115
- context.serviceUsageClient,
116
- serviceName,
117
- api,
118
- progressCallback
119
- );
120
- } catch (error) {
121
- // First attempt failed, log a warning and retry once after a delay.
122
- const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
123
- console.warn(warnMsg);
124
- if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
125
-
126
- await new Promise((resolve) => setTimeout(resolve, 1000));
127
- try {
128
- await checkAndEnableApi(
129
- context.serviceUsageClient,
130
- serviceName,
131
- api,
132
- progressCallback
133
- );
134
- } catch (retryError) {
135
- // If the retry also fails, throw an error.
136
- const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
137
- console.error(errorMessage, retryError);
138
- if (progressCallback)
139
- progressCallback({ level: 'error', data: errorMessage });
140
- throw new Error(errorMessage);
141
- }
142
- }
179
+ await enableApiWithRetry(
180
+ context.serviceUsageClient,
181
+ projectId,
182
+ api,
183
+ progressCallback
184
+ );
143
185
  }
144
186
  const successMsg = 'All required APIs are enabled.';
145
187
  console.log(successMsg);
@@ -0,0 +1,54 @@
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
+ export const DEPLOYMENT_CONFIG = {
18
+ REPO_NAME: 'mcp-cloud-run-deployments',
19
+ ZIP_FILE_NAME: 'source.zip',
20
+ TARGZ_FILE_NAME: 'source.tar.gz',
21
+ IMAGE_TAG: 'latest',
22
+ LABEL_CREATED_BY: 'cloud-run-mcp',
23
+ DEFAULT_NODE_BASE_IMAGE: 'nodejs22',
24
+ NO_BUILD_IMAGE_TYPE: 'scratch',
25
+ };
26
+
27
+ export const TEMP_PATHS = {
28
+ BASE: '.cloud-run-mcp',
29
+ SUBDIR: 'source',
30
+ };
31
+
32
+ export const REQUIRED_APIS = {
33
+ SOURCE_DEPLOY: [
34
+ 'serviceusage.googleapis.com',
35
+ 'iam.googleapis.com',
36
+ 'storage.googleapis.com',
37
+ 'cloudbuild.googleapis.com',
38
+ 'artifactregistry.googleapis.com',
39
+ 'run.googleapis.com',
40
+ ],
41
+ IMAGE_DEPLOY: ['serviceusage.googleapis.com', 'run.googleapis.com'],
42
+ };
43
+
44
+ export const DEPLOYMENT_TYPES = {
45
+ NO_BUILD: 'no-build',
46
+ IMAGE: 'image',
47
+ WITH_BUILD: 'with-build',
48
+ };
49
+
50
+ export const RUNTIMES = {
51
+ NODEJS: 'nodejs',
52
+ };
53
+
54
+ export const MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES = 250 * 1024 * 1024; // 250 MiB