@google-cloud/cloud-run-mcp 1.7.0 → 1.9.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/.env.example ADDED
@@ -0,0 +1,13 @@
1
+ PORT = 3000
2
+ GCP_STDIO = false
3
+
4
+ OAUTH_ENABLED = true
5
+ GOOGLE_OAUTH_CLIENT_ID = YOUR_CLIENT_ID
6
+ GOOGLE_OAUTH_CLIENT_SECRET = YOUR_CLIENT_SECRET
7
+ GOOGLE_OAUTH_AUDIENCE = YOUR_AUDIENCE # generally, same as your client id
8
+ GOOGLE_OAUTH_REDIRECT_URI = http://localhost:7777/oauth/callback
9
+
10
+ OAUTH_PROTECTED_RESOURCE = http://localhost:${PORT}/mcp
11
+ OAUTH_AUTHORIZATION_SERVER = http://localhost:${PORT}/auth/google
12
+ OAUTH_AUTHORIZATION_ENDPOINT = https://accounts.google.com/o/oauth2/v2/auth
13
+ OAUTH_TOKEN_ENDPOINT = https://oauth2.googleapis.com/token
@@ -0,0 +1,13 @@
1
+ PORT = 3000
2
+ GCP_STDIO = false
3
+
4
+ OAUTH_ENABLED = true
5
+ GOOGLE_OAUTH_CLIENT_ID = 32555940559.apps.googleusercontent.com
6
+ GOOGLE_OAUTH_CLIENT_SECRET = ZmssLNjJy2998hD4CTg2ejr2
7
+ GOOGLE_OAUTH_AUDIENCE = 32555940559.apps.googleusercontent.com
8
+ GOOGLE_OAUTH_REDIRECT_URI = http://localhost:7777/oauth/callback
9
+
10
+ OAUTH_PROTECTED_RESOURCE = http://localhost:${PORT}/mcp
11
+ OAUTH_AUTHORIZATION_SERVER = http://localhost:${PORT}/auth/google
12
+ OAUTH_AUTHORIZATION_ENDPOINT = https://accounts.google.com/o/oauth2/v2/auth
13
+ OAUTH_TOKEN_ENDPOINT = https://oauth2.googleapis.com/token
package/README.md CHANGED
@@ -235,4 +235,42 @@ With this option, you will only be able to deploy code to the same Google Cloud
235
235
  }
236
236
  ```
237
237
 
238
+ ## Using MCP Server with OAuth
239
+
240
+ Cloud Run MCP server supports OAuth as an authentication mechanism. In order to use OAuth, create the OAuth client, and configure a `.env` file with the appropriate values pertaining to your OAuth client. A `.env.example` is provided for reference.
241
+
242
+ The Cloud Run MCP server works seamlessly with Google Cloud SDK OAuth client. In order to leverage the Google Cloud SDK OAuth client, you can use the `.env.gcloud-sdk-oauth` file as your `.env` file as follows:
243
+
244
+ ```bash
245
+ cp .env.gcloud-sdk-oauth .env
246
+ node mcp-server.js
247
+ ```
248
+
249
+ ### Configure MCP Server on Gemini CLI to use OAuth
250
+
251
+ When the Cloud Run MCP server is started in the OAuth mode, the MCP client should also be configured to use OAuth. You can setup the MCP server in OAuth mode in the Gemini CLI by using the following JSON in the `~/.gemini/settings.json` file:
252
+
253
+ ```json
254
+ {
255
+ "mcpServers": {
256
+ "cloud-run": {
257
+ "httpUrl": "http://localhost:3000/mcp",
258
+ "oauth": {
259
+ "enabled": true,
260
+ "clientId": "<OAUTH_CLIENT_ID>",
261
+ "clientSecret": "<OAUTH_CLIENT_SECRET>"
262
+ }
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
268
+ Post the configuration changes as shown above, start the Gemini CLI. You should authenticate the Cloud Run MCP server using the following prompt in the Gemini CLI:
269
+
270
+ ```
271
+ /mcp auth cloud-run
272
+ ```
273
+
274
+ This will take you to the authentication page on your browser, wherein you need to sign in using the appropriate gmail id, and accept the terms and conditions. Once the authentication is succcessful, you can come back to the Gemini CLI, and the Cloud Run MCP server will be ready to use.
275
+
238
276
  The Google Cloud Platform Terms of Service (available at https://cloud.google.com/terms/) and the Data Processing and Security Terms (available at https://cloud.google.com/terms/data-processing-terms) do not apply to any component of the Cloud Run MCP Server software.
package/constants.js ADDED
@@ -0,0 +1,8 @@
1
+ export const SCOPES = {
2
+ OPENID: 'openid',
3
+ EMAIL: 'https://www.googleapis.com/auth/userinfo.email',
4
+ CLOUD_PLATFORM: 'https://www.googleapis.com/auth/cloud-platform',
5
+ };
6
+ export const BEARER_METHODS_SUPPORTED = ['header'];
7
+ export const RESPONSE_TYPES_SUPPORTED = ['code'];
8
+ export const GCLOUD_AUTH = 'gcloud_auth';
package/lib/clients.js ADDED
@@ -0,0 +1,292 @@
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 { OAuth2Client } from 'google-auth-library';
18
+ import { GCLOUD_AUTH } from '../constants.js';
19
+
20
+ const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID;
21
+ const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
22
+ const REDIRECT_URI = process.env.GOOGLE_OAUTH_REDIRECT_URI;
23
+ const AUTHORIZATION_HEADER = 'Authorization';
24
+ const BEARER_PREFIX = 'Bearer';
25
+
26
+ function keyGenerator(projectId, accessToken) {
27
+ return accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
28
+ }
29
+
30
+ const clients = {
31
+ run: new Map(),
32
+ builds: new Map(),
33
+ serviceUsage: new Map(),
34
+ storage: new Map(),
35
+ cloudBuild: new Map(),
36
+ artifactRegistry: new Map(),
37
+ logging: new Map(),
38
+ billing: new Map(),
39
+ projects: new Map(),
40
+ oauth: new Map(),
41
+ };
42
+
43
+ function getAuthClient(accessToken) {
44
+ return {
45
+ getRequestHeaders: () => {
46
+ const headers = new Map();
47
+ headers.set(AUTHORIZATION_HEADER, `${BEARER_PREFIX} ${accessToken}`);
48
+ return headers;
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Wraps an OAuth2Client to be compatible with gRPC-based Google Cloud clients.
55
+ *
56
+ * Some Google Cloud SDK clients (like Cloud Run, Service Usage, etc.) use gRPC
57
+ * and expect the `getRequestHeaders()` method of the auth client to return a
58
+ * `Map` of headers. However, the standard `OAuth2Client` from `google-auth-library`
59
+ * returns a plain Javascript object (e.g., `{ Authorization: 'Bearer ...' }`).
60
+ *
61
+ * This wrapper uses a Proxy to intercept calls to `getRequestHeaders`. It calls
62
+ * the original method and converts the result from a plain object to a `Map`
63
+ * if necessary, ensuring compatibility with gRPC-based clients while maintaining
64
+ * the original behavior for other properties.
65
+ *
66
+ * @param {OAuth2Client} authClient - The original OAuth2Client instance.
67
+ * @returns {Proxy<OAuth2Client>} A proxy compatible with gRPC clients.
68
+ */
69
+ function wrapForGrpc(authClient) {
70
+ return new Proxy(authClient, {
71
+ get(target, prop, receiver) {
72
+ if (prop === 'getRequestHeaders') {
73
+ return async (...args) => {
74
+ const headers = await target.getRequestHeaders(...args);
75
+ if (headers instanceof Map) {
76
+ return headers;
77
+ }
78
+ // Convert plain object (from OAuth2Client) to Map (expected by grpc-js)
79
+ const headerMap = new Map();
80
+ for (const [k, v] of Object.entries(headers)) {
81
+ headerMap.set(k, v);
82
+ }
83
+ return headerMap;
84
+ };
85
+ }
86
+ return Reflect.get(target, prop, receiver);
87
+ },
88
+ });
89
+ }
90
+
91
+ // Services that use HTTP/REST (or prefer Object headers) instead of gRPC (Map headers)
92
+ const HTTP_SERVICES = ['storage', 'logging'];
93
+
94
+ export async function getClient(
95
+ service,
96
+ key,
97
+ loadClient,
98
+ options = {},
99
+ accessToken = null
100
+ ) {
101
+ if (!clients[service].has(key)) {
102
+ const ClientClass = await loadClient();
103
+ const finalOptions = { ...options };
104
+ if (accessToken && accessToken !== GCLOUD_AUTH) {
105
+ const oauthClient = await getOAuthClient(accessToken);
106
+ // Storage and Logging use HTTP and expect the native OAuth2Client (headers as Object).
107
+ // Other services (Run, ServiceUsage, etc.) use gRPC and expect headers as Map.
108
+ if (HTTP_SERVICES.includes(service)) {
109
+ finalOptions.authClient = oauthClient;
110
+ } else {
111
+ finalOptions.authClient = wrapForGrpc(oauthClient);
112
+ }
113
+ }
114
+ clients[service].set(key, new ClientClass(finalOptions));
115
+ }
116
+ return clients[service].get(key);
117
+ }
118
+
119
+ /**
120
+ * Gets a Cloud Run Services Client for the specified project.
121
+ * @param {string} projectId - The Google Cloud project ID.
122
+ * @returns {Promise<import('@google-cloud/run').v2.ServicesClient>}
123
+ */
124
+ export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
125
+ const key = keyGenerator(projectId, accessToken);
126
+ return getClient(
127
+ 'run',
128
+ key,
129
+ async () => (await import('@google-cloud/run')).v2.ServicesClient,
130
+ { projectId },
131
+ accessToken
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Gets a Cloud Run Build Client for the specified project.
137
+ * @param {string} projectId - The Google Cloud project ID.
138
+ * @returns {Promise<import('@google-cloud/run').v2.BuildsClient>}
139
+ */
140
+ export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
141
+ const key = keyGenerator(projectId, accessToken);
142
+ return getClient(
143
+ 'builds',
144
+ key,
145
+ async () => (await import('@google-cloud/run')).v2.BuildsClient,
146
+ { projectId },
147
+ accessToken
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Gets a Service Usage Client for the specified project.
153
+ * @param {string} projectId - The Google Cloud project ID.
154
+ * @returns {Promise<import('@google-cloud/service-usage').ServiceUsageClient>}
155
+ */
156
+ export async function getServiceUsageClient(
157
+ projectId,
158
+ accessToken = GCLOUD_AUTH
159
+ ) {
160
+ const key = keyGenerator(projectId, accessToken);
161
+ return getClient(
162
+ 'serviceUsage',
163
+ key,
164
+ async () =>
165
+ (await import('@google-cloud/service-usage')).ServiceUsageClient,
166
+ { projectId },
167
+ accessToken
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Gets a Storage Client for the specified project.
173
+ * @param {string} projectId - The Google Cloud project ID.
174
+ * @returns {Promise<import('@google-cloud/storage').Storage>}
175
+ */
176
+ export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
177
+ const key = keyGenerator(projectId, accessToken);
178
+ return getClient(
179
+ 'storage',
180
+ key,
181
+ async () => (await import('@google-cloud/storage')).Storage,
182
+ { projectId },
183
+ accessToken
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Gets a Cloud Build Client for the specified project.
189
+ * @param {string} projectId - The Google Cloud project ID.
190
+ * @returns {Promise<import('@google-cloud/cloudbuild').CloudBuildClient>}
191
+ */
192
+ export async function getCloudBuildClient(
193
+ projectId,
194
+ accessToken = GCLOUD_AUTH
195
+ ) {
196
+ const key = keyGenerator(projectId, accessToken);
197
+ return getClient(
198
+ 'cloudBuild',
199
+ key,
200
+ async () => (await import('@google-cloud/cloudbuild')).CloudBuildClient,
201
+ { projectId },
202
+ accessToken
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Gets an Artifact Registry Client for the specified project.
208
+ * @param {string} projectId - The Google Cloud project ID.
209
+ * @returns {Promise<import('@google-cloud/artifact-registry').ArtifactRegistryClient>}
210
+ */
211
+ export async function getArtifactRegistryClient(
212
+ projectId,
213
+ accessToken = GCLOUD_AUTH
214
+ ) {
215
+ const key = keyGenerator(projectId, accessToken);
216
+ return getClient(
217
+ 'artifactRegistry',
218
+ key,
219
+ async () =>
220
+ (await import('@google-cloud/artifact-registry')).ArtifactRegistryClient,
221
+ { projectId },
222
+ accessToken
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Gets a Logging Client for the specified project.
228
+ * @param {string} projectId - The Google Cloud project ID.
229
+ * @returns {Promise<import('@google-cloud/logging').Logging>}
230
+ */
231
+ export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
232
+ const key = keyGenerator(projectId, accessToken);
233
+ return getClient(
234
+ 'logging',
235
+ key,
236
+ async () => (await import('@google-cloud/logging')).Logging,
237
+ { projectId },
238
+ accessToken
239
+ );
240
+ }
241
+
242
+ /**
243
+ * Gets a Billing Client for the specified project.
244
+ * Note: BillingClient usually doesn't take projectId in constructor for listing accounts,
245
+ * but might for project billing info. We will cache by projectId anyway or 'global' if projectId is null.
246
+ * @param {string} [projectId] - The Google Cloud project ID (optional).
247
+ * @returns {Promise<import('@google-cloud/billing').CloudBillingClient>}
248
+ */
249
+ export async function getBillingClient(
250
+ projectId = 'global',
251
+ accessToken = GCLOUD_AUTH
252
+ ) {
253
+ const key = keyGenerator(projectId, accessToken);
254
+ return getClient(
255
+ 'billing',
256
+ key,
257
+ async () => (await import('@google-cloud/billing')).CloudBillingClient,
258
+ projectId !== 'global' ? { projectId } : {},
259
+ accessToken
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Gets a Projects Client (Resource Manager).
265
+ * @returns {Promise<import('@google-cloud/resource-manager').ProjectsClient>}
266
+ */
267
+ export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
268
+ const key = keyGenerator('global', accessToken);
269
+ return getClient(
270
+ 'projects',
271
+ key,
272
+ async () => (await import('@google-cloud/resource-manager')).ProjectsClient,
273
+ {},
274
+ accessToken
275
+ );
276
+ }
277
+
278
+ /**
279
+ * Gets an OAuth2 Client for the specified access token.
280
+ * @param {string} accessToken - The access token.
281
+ * @returns {Promise<OAuth2Client>}
282
+ */
283
+ export async function getOAuthClient(accessToken) {
284
+ // Use the access token itself as the key since it's unique per session/user
285
+ if (!clients.oauth.has(accessToken)) {
286
+ const client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
287
+ client.setCredentials({ access_token: accessToken });
288
+ clients.oauth.set(accessToken, client);
289
+ }
290
+
291
+ return clients.oauth.get(accessToken);
292
+ }
@@ -23,6 +23,7 @@ import { GoogleAuth } from 'google-auth-library';
23
23
  * @async
24
24
  * @returns {Promise<boolean>} A promise that resolves to true if GCP auth are found, and false otherwise.
25
25
  */
26
+ //TODO: Rename ensureGCPCredentials to ensureADCCredentials
26
27
  export async function ensureGCPCredentials() {
27
28
  console.error('Checking for Google Cloud Application Default Credentials...');
28
29
  try {
@@ -21,9 +21,17 @@ limitations under the License.
21
21
  * @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
22
22
  * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
23
23
  */
24
- export async function listBillingAccounts() {
25
- const { CloudBillingClient } = await import('@google-cloud/billing');
26
- const client = new CloudBillingClient();
24
+ import { getBillingClient } from '../clients.js';
25
+
26
+ /**
27
+ * Lists all accessible Google Cloud Billing Accounts.
28
+ * @async
29
+ * @function listBillingAccounts
30
+ * @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
31
+ * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
32
+ */
33
+ export async function listBillingAccounts(accessToken) {
34
+ const client = await getBillingClient('global', accessToken);
27
35
  try {
28
36
  const [accounts] = await client.listBillingAccounts();
29
37
  if (!accounts || accounts.length === 0) {
@@ -51,10 +59,10 @@ export async function listBillingAccounts() {
51
59
  */
52
60
  export async function attachProjectToBillingAccount(
53
61
  projectId,
54
- billingAccountName
62
+ billingAccountName,
63
+ accessToken
55
64
  ) {
56
- const { CloudBillingClient } = await import('@google-cloud/billing');
57
- const client = new CloudBillingClient();
65
+ const client = await getBillingClient(projectId, accessToken);
58
66
  const projectName = `projects/${projectId}`;
59
67
 
60
68
  if (!projectId) {
@@ -104,9 +112,8 @@ export async function attachProjectToBillingAccount(
104
112
  * @param {string} projectId - The ID of the project to check.
105
113
  * @returns {Promise<boolean>} A promise that resolves to true if billing is enabled, false otherwise.
106
114
  */
107
- export async function isBillingEnabled(projectId) {
108
- const { CloudBillingClient } = await import('@google-cloud/billing');
109
- const client = new CloudBillingClient();
115
+ export async function isBillingEnabled(projectId, accessToken) {
116
+ const client = await getBillingClient(projectId, accessToken);
110
117
  const projectName = `projects/${projectId}`;
111
118
  try {
112
119
  // getProjectBillingInfo requires cloudbilling.googleapis.com API to check billing status
@@ -127,18 +134,17 @@ export async function isBillingEnabled(projectId) {
127
134
  /**
128
135
  * Ensures billing is enabled for a project. If not, attempts to enable it
129
136
  * by attaching the single open billing account if one is found.
130
- * @param {object} context - The context object containing clients and other parameters.
131
137
  * @param {string} projectId The project ID to check billing for.
132
138
  * @param {function} progressCallback A callback function for progress updates.
133
139
  */
134
140
  export async function ensureBillingEnabled(
135
- context,
136
141
  projectId,
142
+ accessToken,
137
143
  progressCallback
138
144
  ) {
139
- if (!(await isBillingEnabled(projectId))) {
145
+ if (!(await isBillingEnabled(projectId, accessToken))) {
140
146
  // Billing is disabled, try to fix it.
141
- const accounts = await listBillingAccounts();
147
+ const accounts = await listBillingAccounts(accessToken);
142
148
 
143
149
  if (accounts && accounts.length === 1 && accounts[0].open) {
144
150
  // Exactly one open account found, try to attach it.
@@ -150,7 +156,8 @@ export async function ensureBillingEnabled(
150
156
 
151
157
  const attachmentResult = await attachProjectToBillingAccount(
152
158
  projectId,
153
- account.name
159
+ account.name,
160
+ accessToken
154
161
  );
155
162
 
156
163
  if (!attachmentResult || !attachmentResult.billingEnabled) {
@@ -14,8 +14,15 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import {
18
+ getCloudBuildClient,
19
+ getLoggingClient,
20
+ getRunClient,
21
+ getBuildsClient,
22
+ } from '../clients.js';
17
23
  import { callWithRetry } from './helpers.js';
18
24
  import { logAndProgress } from '../util/helpers.js';
25
+ import { checkCloudRunServiceExists } from './run.js';
19
26
 
20
27
  const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
21
28
  const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
@@ -23,10 +30,10 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
23
30
  /**
24
31
  * Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
25
32
  * It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
33
+ * It performs a dry-run deployment check before starting the build.
26
34
  * Waits for the build to complete and returns the build result.
27
35
  *
28
36
  * @async
29
- * @param {object} context - The context object containing clients and other parameters.
30
37
  * @param {string} projectId - The Google Cloud project ID.
31
38
  * @param {string} location - The Google Cloud region for the build.
32
39
  * @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
@@ -39,7 +46,6 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
39
46
  * @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
40
47
  */
41
48
  export async function triggerCloudBuild(
42
- context,
43
49
  projectId,
44
50
  location,
45
51
  sourceBucketName,
@@ -47,67 +53,111 @@ export async function triggerCloudBuild(
47
53
  targetRepoName,
48
54
  targetImageUrl,
49
55
  hasDockerfile,
56
+ accessToken,
50
57
  progressCallback
51
58
  ) {
59
+ const serviceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
60
+ const parent = `projects/${projectId}/locations/${location}`;
61
+ const serviceFullName = `${parent}/services/${serviceName}`;
62
+ const buildsClient = await getBuildsClient(projectId, accessToken);
63
+ const runClient = await getRunClient(projectId, accessToken);
64
+ const serviceExists = await checkCloudRunServiceExists(
65
+ projectId,
66
+ location,
67
+ serviceName,
68
+ accessToken,
69
+ progressCallback
70
+ );
71
+ const cloudBuildClient = await getCloudBuildClient(projectId, accessToken);
72
+ const loggingClient = await getLoggingClient(projectId, accessToken);
52
73
  let buildSteps;
53
74
 
54
- if (hasDockerfile) {
55
- buildSteps = [
56
- {
57
- name: 'gcr.io/cloud-builders/docker',
58
- args: ['build', '-t', targetImageUrl, '.'],
59
- dir: '/workspace',
60
- },
61
- ];
62
- } else {
63
- buildSteps = [
64
- {
65
- name: 'gcr.io/k8s-skaffold/pack',
66
- entrypoint: 'pack',
67
- args: [
68
- 'build',
69
- targetImageUrl,
70
- '--builder',
71
- 'gcr.io/buildpacks/builder:latest',
72
- ],
73
- dir: '/workspace',
74
- },
75
- ];
75
+ const servicePatch = {
76
+ template: {
77
+ containers: [{ image: targetImageUrl }],
78
+ },
79
+ };
80
+
81
+ try {
82
+ if (serviceExists) {
83
+ await logAndProgress(
84
+ `Performing dry-run update for service ${serviceName}...`,
85
+ progressCallback
86
+ );
87
+ servicePatch.name = serviceFullName;
88
+ await callWithRetry(
89
+ () =>
90
+ runClient.updateService({
91
+ service: servicePatch,
92
+ validateOnly: true,
93
+ }),
94
+ 'run.updateService-dryrun'
95
+ );
96
+ } else {
97
+ await logAndProgress(
98
+ `Performing dry-run creation for service ${serviceName}...`,
99
+ progressCallback
100
+ );
101
+ await callWithRetry(
102
+ () =>
103
+ runClient.createService({
104
+ parent: parent,
105
+ service: servicePatch,
106
+ serviceId: serviceName,
107
+ validateOnly: true,
108
+ }),
109
+ 'run.createService-dryrun'
110
+ );
111
+ }
112
+ await logAndProgress(
113
+ `Dry-run validation successful.`,
114
+ progressCallback,
115
+ 'debug'
116
+ );
117
+ } catch (err) {
118
+ await logAndProgress(
119
+ `Dry-run validation failed for service ${serviceName}.`,
120
+ progressCallback,
121
+ 'error'
122
+ );
123
+ throw new Error(`Dry-run deployment failed: ${err.message}`);
76
124
  }
77
125
 
78
- const build = {
79
- source: {
80
- storageSource: {
81
- bucket: sourceBucketName,
82
- object: sourceBlobName,
83
- },
126
+ const request = {
127
+ parent: `projects/${projectId}/locations/${location}`,
128
+ storageSource: {
129
+ bucket: sourceBucketName,
130
+ object: sourceBlobName,
84
131
  },
85
- steps: buildSteps,
86
- images: [targetImageUrl],
132
+ imageUri: targetImageUrl,
133
+ client: 'cloud-run-mcp',
87
134
  };
135
+ if (hasDockerfile) {
136
+ request.dockerBuild = {};
137
+ } else {
138
+ request.buildpackBuild = {};
139
+ }
88
140
 
89
141
  await logAndProgress(
90
142
  `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
91
143
  progressCallback
92
144
  );
93
- const [operation] = await callWithRetry(
94
- () =>
95
- context.cloudBuildClient.createBuild({
96
- projectId: projectId,
97
- build: build,
98
- }),
99
- 'cloudBuild.createBuild'
100
- );
101
145
 
146
+ const [submitBuildResponse] = await callWithRetry(
147
+ () => buildsClient.submitBuild(request),
148
+ 'builds.submitBuild'
149
+ );
102
150
  await logAndProgress(`Cloud Build job started...`, progressCallback);
103
- const buildId = operation.metadata.build.id;
151
+ const encodedBuildId = submitBuildResponse.buildOperation.name
152
+ .split('/')
153
+ .pop();
154
+ const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
104
155
  let completedBuild;
105
156
  while (true) {
106
157
  const [getBuildOperation] = await callWithRetry(
107
158
  () =>
108
- context.cloudBuildClient.getBuild({
109
- projectId: projectId,
110
- id: buildId,
159
+ cloudBuildClient.getBuild({
160
+ name: `projects/${projectId}/locations/${location}/builds/${buildId}`,
111
161
  }),
112
162
  `cloudBuild.getBuild ${buildId}`
113
163
  );
@@ -132,10 +182,9 @@ export async function triggerCloudBuild(
132
182
  `Cloud Build job ${buildId} completed successfully.`,
133
183
  progressCallback
134
184
  );
135
- await logAndProgress(
136
- `Image built: ${completedBuild.results.images[0].name}`,
137
- progressCallback
138
- );
185
+ const builtImage = completedBuild.results.images[0].name;
186
+ await logAndProgress(`Image built: ${builtImage}`, progressCallback);
187
+
139
188
  return completedBuild;
140
189
  } else {
141
190
  const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
@@ -160,7 +209,7 @@ export async function triggerCloudBuild(
160
209
  // Fetch the most recent N log entries
161
210
  const [entries] = await callWithRetry(
162
211
  () =>
163
- context.loggingClient.getEntries({
212
+ loggingClient.getEntries({
164
213
  filter: logFilter,
165
214
  orderBy: 'timestamp desc', // Get latest logs first
166
215
  pageSize: BUILD_LOGS_LINES_TO_FETCH,