@google-cloud/cloud-run-mcp 1.0.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.
@@ -0,0 +1,181 @@
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 protofiles from 'google-proto-files';
18
+
19
+ let runClient;
20
+ let loggingClient;
21
+
22
+ /**
23
+ * Lists all Cloud Run services in a given project and location.
24
+ * @param {string} projectId - The Google Cloud project ID.
25
+ * @param {string} location - The Google Cloud location (e.g., 'europe-west1').
26
+ * @returns {Promise<Array<object>>} - A promise that resolves to an array of service objects.
27
+ */
28
+ export async function listServices(projectId, location) {
29
+ // TODO make location optional, if not provided, list all services from all regions
30
+ if (!runClient) {
31
+ const { v2 } = await import('@google-cloud/run');
32
+ const { ServicesClient } = v2;
33
+ runClient = new ServicesClient({ projectId });
34
+ }
35
+ const parent = runClient.locationPath(projectId, location);
36
+
37
+ try {
38
+ console.log(
39
+ `Listing Cloud Run services in project ${projectId}, location ${location}...`
40
+ );
41
+ const [services] = await runClient.listServices({ parent });
42
+ return services;
43
+ } catch (error) {
44
+ console.error(`Error listing Cloud Run services:`, error);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Gets details for a specific Cloud Run service.
51
+ * @param {string} projectId - The Google Cloud project ID.
52
+ * @param {string} location - The Google Cloud location (e.g., 'europe-west1').
53
+ * @param {string} serviceId - The ID of the Cloud Run service.
54
+ * @returns {Promise<object>} - A promise that resolves to the service object.
55
+ */
56
+ export async function getService(projectId, location, serviceId) {
57
+ if (!runClient) {
58
+ const { v2 } = await import('@google-cloud/run');
59
+ const { ServicesClient } = v2;
60
+ runClient = new ServicesClient({ projectId });
61
+ }
62
+
63
+ const servicePath = runClient.servicePath(projectId, location, serviceId);
64
+
65
+ try {
66
+ console.log(
67
+ `Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
68
+ );
69
+ const [service] = await runClient.getService({ name: servicePath });
70
+ return service;
71
+ } catch (error) {
72
+ console.error(
73
+ `Error getting details for Cloud Run service ${serviceId}:`,
74
+ error
75
+ );
76
+ // Check if the error is a "not found" error (gRPC code 5)
77
+ if (error.code === 5) {
78
+ console.log(`Cloud Run service ${serviceId} not found.`);
79
+ return null; // Or throw a custom error, or handle as needed
80
+ }
81
+ throw error; // Re-throw other errors
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetches a paginated list of logs for a specific Cloud Run service.
87
+ * @param {string} projectId - The Google Cloud project ID.
88
+ * @param {string} location - The Google Cloud location (e.g., 'europe-west1').
89
+ * @param {string} serviceId - The ID of the Cloud Run service.
90
+ * @param {string} [requestOptions] - The token for the next page of results.
91
+ * @returns {Promise<{logs: string, requestOptions: object | undefined }>} - A promise that resolves to an object with log entries and a token for the next page.
92
+ */
93
+ export async function getServiceLogs(
94
+ projectId,
95
+ location,
96
+ serviceId,
97
+ requestOptions
98
+ ) {
99
+ if (!loggingClient) {
100
+ const { Logging } = await import('@google-cloud/logging');
101
+ loggingClient = new Logging({ projectId });
102
+ }
103
+
104
+ try {
105
+ const LOG_SEVERITY = 'DEFAULT'; // e.g., 'DEFAULT', 'INFO', 'WARNING', 'ERROR'
106
+ const PAGE_SIZE = 100; // Number of log entries to retrieve per page
107
+
108
+ const filter = `resource.type="cloud_run_revision"
109
+ resource.labels.service_name="${serviceId}"
110
+ resource.labels.location="${location}"
111
+ severity>=${LOG_SEVERITY}`;
112
+
113
+ console.log(
114
+ `Fetching logs for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
115
+ );
116
+
117
+ // Options for the getEntries API call
118
+ const options = requestOptions || {
119
+ filter: filter,
120
+ orderBy: 'timestamp desc', // Get the latest logs first
121
+ pageSize: PAGE_SIZE,
122
+ };
123
+ console.log(`Request options: ${JSON.stringify(options)}`);
124
+
125
+ // getEntries returns the entries and the full API response
126
+ const [entries, nextRequestOptions, apiResponse] =
127
+ await loggingClient.getEntries(options);
128
+
129
+ const formattedLogLines = entries
130
+ .map((entry) => formatLogEntry(entry))
131
+ .join('\n');
132
+
133
+ // The nextPageToken is available in the apiResponse object
134
+ const nextOptions = apiResponse?.nextPageToken
135
+ ? nextRequestOptions
136
+ : undefined;
137
+
138
+ return {
139
+ logs: formattedLogLines,
140
+ requestOptions: nextOptions,
141
+ };
142
+ } catch (error) {
143
+ console.error(
144
+ `Error fetching logs for Cloud Run service ${serviceId}:`,
145
+ error
146
+ );
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Formats a single log entry for display.
153
+ * @param {object} entry - A log entry object from the Cloud Logging API.
154
+ * @returns {string} - A formatted string representation of the log entry.
155
+ */
156
+ function formatLogEntry(entry) {
157
+ const timestampStr = entry.metadata.timestamp.toISOString() || 'N/A';
158
+ const severity = entry.metadata.severity || 'N/A';
159
+ let responseData = '';
160
+ if (entry.metadata.httpRequest) {
161
+ const responseMethod = entry.metadata.httpRequest.requestMethod;
162
+ const responseCode = entry.metadata.httpRequest.status;
163
+ const requestUrl = entry.metadata.httpRequest.requestUrl;
164
+ const responseSize = entry.metadata.httpRequest.responseSize;
165
+ responseData = `HTTP Request: ${responseMethod} StatusCode: ${responseCode} ResponseSize: ${responseSize} Byte - ${requestUrl}`;
166
+ }
167
+
168
+ let data = '';
169
+ if (entry.data && entry.data.value) {
170
+ const protopath = protofiles.getProtoPath(
171
+ '../google/cloud/audit/audit_log.proto'
172
+ );
173
+ const root = protofiles.loadSync(protopath);
174
+ const type = root.lookupType('google.cloud.audit.AuditLog');
175
+ const value = type.decode(entry.data.value);
176
+ data = `${value.methodName}: ${value.status?.message || ''}${value.authenticationInfo?.principalEmail || ''}`;
177
+ } else if (entry.data) {
178
+ data = entry.data;
179
+ }
180
+ return `[${timestampStr}] [${severity}] ${responseData} ${data}`;
181
+ }
@@ -0,0 +1,79 @@
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 { GoogleAuth } from 'google-auth-library';
18
+
19
+ /**
20
+ * Checks for the presence of properly setup Google Cloud Platform (GCP) authentication
21
+ * using the official Google Auth Library for Node.js. If GCP auth is not found, it logs an
22
+ * error message and exits the process, instructing the user on how to set them up.
23
+ * This check is primarily for local/stdio environments where explicit setup is often needed.
24
+ * @async
25
+ * @returns {Promise<void>} A promise that resolves if GCP auth are found, or rejects and exits the process if not.
26
+ */
27
+ export async function ensureGCPCredentials() {
28
+ console.error('Checking for Google Cloud Application Default Credentials...');
29
+ try {
30
+ const auth = new GoogleAuth();
31
+ // Attempt to get credentials. This will throw an error if ADC are not found.
32
+ const client = await auth.getClient();
33
+ // Attempt to get an access token to verify credentials are usable.
34
+ // This is done because getClient() might succeed but credentials might be invalid/expired.
35
+ await client.getAccessToken();
36
+
37
+ console.log('Application Default Credentials found.');
38
+ } catch (error) {
39
+ console.error(
40
+ 'ERROR: Google Cloud Application Default Credentials are not set up.'
41
+ );
42
+
43
+ if (error.response && error.response.status) {
44
+ console.error(
45
+ `An HTTP error occurred (Status: ${error.response.status}). This often means misconfigured, expired credentials, or a network issue.`
46
+ );
47
+ } else if (error instanceof TypeError) {
48
+ // Catches TypeErrors specifically, which might indicate a malformed response or unexpected data type
49
+ console.error(
50
+ 'An unexpected error occurred during credential verification (e.g., malformed response or invalid type).'
51
+ );
52
+ } else {
53
+ // General fallback for any other unexpected errors
54
+ console.error(
55
+ 'An unexpected error occurred during credential verification.'
56
+ );
57
+ }
58
+
59
+ console.error('\nFor more details or alternative setup methods, consider:');
60
+ console.error(
61
+ '1. If running locally, run: gcloud auth application-default login.'
62
+ );
63
+ console.error(
64
+ '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.'
65
+ );
66
+ console.error(
67
+ '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.'
68
+ );
69
+ console.error(
70
+ `\nOriginal error message from Google Auth Library: ${error.message}`
71
+ );
72
+
73
+ // Print the stack for debugging
74
+ if (error.stack) {
75
+ console.error('Error stack:', error.stack);
76
+ }
77
+ // process.exit(1);
78
+ }
79
+ }
@@ -0,0 +1,98 @@
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
+ * Lists all accessible Google Cloud Billing Accounts.
19
+ * @async
20
+ * @function listBillingAccounts
21
+ * @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
22
+ * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
23
+ */
24
+ export async function listBillingAccounts() {
25
+ const { CloudBillingClient } = await import('@google-cloud/billing');
26
+ const client = new CloudBillingClient();
27
+ try {
28
+ const [accounts] = await client.listBillingAccounts();
29
+ if (!accounts || accounts.length === 0) {
30
+ console.log('No billing accounts found.');
31
+ return [];
32
+ }
33
+ return accounts.map((account) => ({
34
+ name: account.name, // e.g., billingAccounts/0X0X0X-0X0X0X-0X0X0X
35
+ displayName: account.displayName,
36
+ open: account.open,
37
+ }));
38
+ } catch (error) {
39
+ console.error('Error listing GCP billing accounts:', error);
40
+ return [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Attaches a Google Cloud Project to a specified Billing Account.
46
+ * @async
47
+ * @function attachProjectToBillingAccount
48
+ * @param {string} projectId - The ID of the project to attach.
49
+ * @param {string} billingAccountName - The resource name of the billing account (e.g., 'billingAccounts/0X0X0X-0X0X0X-0X0X0X').
50
+ * @returns {Promise<object|null>} A promise that resolves to the updated project billing information object if successful, or null on error.
51
+ */
52
+ export async function attachProjectToBillingAccount(
53
+ projectId,
54
+ billingAccountName
55
+ ) {
56
+ const { CloudBillingClient } = await import('@google-cloud/billing');
57
+ const client = new CloudBillingClient();
58
+ const projectName = `projects/${projectId}`;
59
+
60
+ if (!projectId) {
61
+ console.error('Error: projectId is required.');
62
+ return null;
63
+ }
64
+ if (
65
+ !billingAccountName ||
66
+ !billingAccountName.startsWith('billingAccounts/')
67
+ ) {
68
+ console.error(
69
+ 'Error: billingAccountName is required and must be in the format "billingAccounts/XXXXXX-XXXXXX-XXXXXX".'
70
+ );
71
+ return null;
72
+ }
73
+
74
+ try {
75
+ console.log(
76
+ `Attempting to attach project ${projectId} to billing account ${billingAccountName}...`
77
+ );
78
+ const [updatedBillingInfo] = await client.updateProjectBillingInfo({
79
+ name: projectName,
80
+ projectBillingInfo: {
81
+ billingAccountName: billingAccountName,
82
+ },
83
+ });
84
+ console.log(
85
+ `Successfully attached project ${projectId} to billing account ${billingAccountName}.`
86
+ );
87
+ console.log(`Billing enabled: ${updatedBillingInfo.billingEnabled}`);
88
+ return updatedBillingInfo;
89
+ } catch (error) {
90
+ console.error(
91
+ `Error attaching project ${projectId} to billing account ${billingAccountName}:`,
92
+ error.message || error
93
+ );
94
+ // Log more details if available, e.g. error.details
95
+ // if (error.details) console.error("Error details:", error.details);
96
+ return null;
97
+ }
98
+ }
@@ -0,0 +1,62 @@
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
+ // Helper function to fetch data from GCP metadata server
18
+ /**
19
+ * Fetches metadata from the Google Cloud metadata server.
20
+ *
21
+ * @param {string} path - The metadata path to fetch (e.g., `/computeMetadata/v1/...`).
22
+ * @returns {Promise<string>} A promise that resolves to the metadata value as a string.
23
+ * @throws {Error} If the metadata request fails with a non-OK status.
24
+ */
25
+ async function fetchMetadata(path) {
26
+ const response = await fetch(`http://metadata.google.internal${path}`, {
27
+ headers: {
28
+ 'Metadata-Flavor': 'Google',
29
+ },
30
+ });
31
+ if (!response.ok) {
32
+ throw new Error(`Metadata request failed with status ${response.status}`);
33
+ }
34
+ return await response.text();
35
+ }
36
+
37
+ /**
38
+ * Checks if the GCP metadata server is available and retrieves project ID and region.
39
+ * @returns {Promise<Object|null>} A promise that resolves to an object { project: string, region: string } or null if not available or an error occurs.
40
+ */
41
+ export async function checkGCP() {
42
+ try {
43
+ const projectId = await fetchMetadata(
44
+ '/computeMetadata/v1/project/project-id'
45
+ );
46
+ // Expected format: projects/PROJECT_NUMBER/regions/REGION_NAME
47
+ const regionPath = await fetchMetadata(
48
+ '/computeMetadata/v1/instance/region'
49
+ );
50
+
51
+ if (projectId && regionPath) {
52
+ const regionParts = regionPath.split('/');
53
+ const region = regionParts[regionParts.length - 1];
54
+ return { project: projectId, region: region };
55
+ }
56
+ return null;
57
+ } catch (error) {
58
+ // Intentionally suppress error logging for cleaner output if metadata server is not available.
59
+ // console.warn('Failed to fetch GCP metadata:', error.message); // Uncomment for debugging
60
+ return null;
61
+ }
62
+ }
@@ -0,0 +1,156 @@
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 {
18
+ listBillingAccounts,
19
+ attachProjectToBillingAccount,
20
+ } from './gcp-billing-accounts.js';
21
+
22
+ /**
23
+ * Lists all accessible Google Cloud Platform projects.
24
+ * @async
25
+ * @function listProjects
26
+ * @returns {Promise<Array<{id: string}>>} A promise that resolves to an array of project objects, each with an 'id' property. Returns an empty array on error.
27
+ */
28
+ export async function listProjects() {
29
+ const { ProjectsClient } = await import('@google-cloud/resource-manager');
30
+ const client = new ProjectsClient();
31
+ try {
32
+ const [projects] = await client.searchProjects();
33
+ return projects.map((project) => ({
34
+ id: project.projectId,
35
+ }));
36
+ } catch (error) {
37
+ console.error('Error listing GCP projects:', error);
38
+ return [];
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Creates a new Google Cloud Platform project.
44
+ * @async
45
+ * @function createProject
46
+ * @param {string} [projectId] - Optional. The desired ID for the new project. If not provided, a compliant ID will be generated automatically (e.g., app-cvc-cvc).
47
+ * @returns {Promise<{projectId: string}|null>} A promise that resolves to an object containing the new project's ID.
48
+ */
49
+ export async function createProject(projectId) {
50
+ const { ProjectsClient } = await import('@google-cloud/resource-manager');
51
+ const client = new ProjectsClient();
52
+ let projectIdToUse = projectId;
53
+
54
+ if (!projectIdToUse) {
55
+ const consonants = 'bcdfghjklmnpqrstvwxyz';
56
+ const vowels = 'aeiou';
57
+
58
+ const getRandomChar = (source) =>
59
+ source.charAt(Math.floor(Math.random() * source.length));
60
+
61
+ const generateCVC = () => {
62
+ const c1 = getRandomChar(consonants);
63
+ const v = getRandomChar(vowels);
64
+ const c2 = getRandomChar(consonants);
65
+ return `${c1}${v}${c2}`;
66
+ };
67
+
68
+ const cvc1 = generateCVC();
69
+ const cvc2 = generateCVC();
70
+ projectIdToUse = `mcp-${cvc1}-${cvc2}`;
71
+ console.log(`Project ID not provided, generated ID: ${projectIdToUse}`);
72
+ }
73
+
74
+ try {
75
+ const projectPayload = { projectId: projectIdToUse, name: projectIdToUse };
76
+
77
+ console.log(`Attempting to create project with ID: ${projectIdToUse}`);
78
+
79
+ const [operation] = await client.createProject({ project: projectPayload });
80
+
81
+ const [createdProjectResponse] = await operation.promise();
82
+
83
+ console.log(
84
+ `Project ${createdProjectResponse.projectId} created successfully.`
85
+ );
86
+ return {
87
+ projectId: createdProjectResponse.projectId,
88
+ };
89
+ } catch (error) {
90
+ console.error(
91
+ `Error creating GCP project ${projectIdToUse}:`,
92
+ error.message
93
+ );
94
+ throw error; // Re-throw to be caught by the caller
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Creates a new Google Cloud Platform project and attempts to attach it to the first available billing account.
100
+ * @async
101
+ * @function createProjectAndAttachBilling
102
+ * @param {string} [projectIdParam] - Optional. The desired ID for the new project.
103
+ * @returns {Promise<{projectId: string, billingMessage: string}>} A promise that resolves to an object containing the project ID and a billing status message.
104
+ */
105
+ export async function createProjectAndAttachBilling(projectIdParam) {
106
+ let newProject;
107
+ try {
108
+ newProject = await createProject(projectIdParam);
109
+ } catch (error) {
110
+ throw new Error(`Failed to create project: ${error.message}`);
111
+ }
112
+
113
+ if (!newProject || !newProject.projectId) {
114
+ throw new Error('Project creation did not return a valid project ID.');
115
+ }
116
+
117
+ const { projectId } = newProject;
118
+ let billingMessage = `Project ${projectId} created.`;
119
+
120
+ try {
121
+ const billingAccounts = await listBillingAccounts();
122
+ if (billingAccounts && billingAccounts.length > 0) {
123
+ const firstBillingAccount = billingAccounts.find((acc) => acc.open); // Prefer an open account
124
+ if (firstBillingAccount) {
125
+ console.log(
126
+ `Found billing account: ${firstBillingAccount.displayName} (${firstBillingAccount.name}). Attempting to attach project ${projectId}.`
127
+ );
128
+ const billingInfo = await attachProjectToBillingAccount(
129
+ projectId,
130
+ firstBillingAccount.name
131
+ );
132
+ if (billingInfo && billingInfo.billingEnabled) {
133
+ billingMessage += ` Successfully attached to billing account ${firstBillingAccount.displayName}.`;
134
+ } else {
135
+ billingMessage += ` Could not attach to billing account ${firstBillingAccount.displayName} or billing not enabled. Please check manually. Console: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
136
+ }
137
+ } else {
138
+ const allBillingAccounts = billingAccounts
139
+ .map((b) => `${b.displayName} (Open: ${b.open})`)
140
+ .join(', ');
141
+ billingMessage += ` No open billing accounts found. Available (may not be usable): ${allBillingAccounts || 'None'}. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
142
+ }
143
+ } else {
144
+ billingMessage +=
145
+ ' No billing accounts found. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}';
146
+ }
147
+ } catch (billingError) {
148
+ console.error(
149
+ `Error during billing operations for project ${projectId}:`,
150
+ billingError
151
+ );
152
+ billingMessage += ` Error during billing operations: ${billingError.message}. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
153
+ }
154
+
155
+ return { projectId, billingMessage };
156
+ }