@google-cloud/cloud-run-mcp 1.3.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.
- package/.c8rc.json +9 -0
- package/.github/images/deploy_from_apps.gif +0 -0
- package/.github/images/deploy_from_ide.gif +0 -0
- package/.github/images/deploycli.gif +0 -0
- package/.github/workflows/lint-checker.yml +17 -0
- package/.github/workflows/local-tests.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 +29 -8
- package/README.md +65 -19
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +3 -3
- package/lib/cloud-api/build.js +200 -0
- package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
- 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/deployer.js +474 -0
- package/lib/util/archive.js +95 -0
- package/lib/util/helpers.js +42 -0
- package/mcp-server.js +29 -11
- package/package.json +9 -6
- package/test/local/cloud-api/build.test.js +436 -0
- package/test/local/cloud-api/projects.test.js +18 -0
- package/test/local/mcp-server-streamable-http.test.js +57 -0
- package/test/local/mcp-server.test.js +0 -1
- package/test/local/notifications.test.js +48 -0
- package/test/local/tools.test.js +143 -124
- package/test/need-gcp/cloud-run-services.test.js +57 -77
- package/test/need-gcp/deployer.test.js +71 -0
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +6 -60
- 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 +612 -0
- package/tools/tools.js +48 -0
- package/lib/cloud-run-deploy.js +0 -917
- package/test/need-gcp/cloud-run-deploy.test.js +0 -158
- package/tools.js +0 -969
- /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
- /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
- /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
package/tools.js
DELETED
|
@@ -1,969 +0,0 @@
|
|
|
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 { z } from 'zod';
|
|
18
|
-
import { deploy, deployImage } from './lib/cloud-run-deploy.js';
|
|
19
|
-
import {
|
|
20
|
-
listServices,
|
|
21
|
-
getService,
|
|
22
|
-
getServiceLogs,
|
|
23
|
-
} from './lib/cloud-run-services.js';
|
|
24
|
-
import {
|
|
25
|
-
listProjects,
|
|
26
|
-
createProjectAndAttachBilling,
|
|
27
|
-
} from './lib/gcp-projects.js';
|
|
28
|
-
|
|
29
|
-
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
|
-
|
|
31
|
-
function gcpTool(gcpCredentialsAvailable, fn) {
|
|
32
|
-
if (!gcpCredentialsAvailable) {
|
|
33
|
-
return () => ({
|
|
34
|
-
content: [
|
|
35
|
-
{
|
|
36
|
-
type: 'text',
|
|
37
|
-
text: 'GCP credentials are not available. Please configure your environment.',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return fn;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const registerTools = (
|
|
46
|
-
server,
|
|
47
|
-
{
|
|
48
|
-
defaultProjectId,
|
|
49
|
-
defaultRegion,
|
|
50
|
-
defaultServiceName,
|
|
51
|
-
skipIamCheck,
|
|
52
|
-
gcpCredentialsAvailable,
|
|
53
|
-
} = {}
|
|
54
|
-
) => {
|
|
55
|
-
// Tool to list GCP projects
|
|
56
|
-
server.registerTool(
|
|
57
|
-
'list_projects',
|
|
58
|
-
{
|
|
59
|
-
description: 'Lists available GCP projects',
|
|
60
|
-
inputSchema: {},
|
|
61
|
-
},
|
|
62
|
-
gcpTool(gcpCredentialsAvailable, async () => {
|
|
63
|
-
try {
|
|
64
|
-
const projects = await listProjects();
|
|
65
|
-
return {
|
|
66
|
-
content: [
|
|
67
|
-
{
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: `Available GCP Projects:\n${projects.map((p) => `- ${p.id}`).join('\n')}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
};
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: 'text',
|
|
78
|
-
text: `Error listing GCP projects: ${error.message}`,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Tool to create a new GCP project
|
|
87
|
-
server.registerTool(
|
|
88
|
-
'create_project',
|
|
89
|
-
{
|
|
90
|
-
description:
|
|
91
|
-
'Creates a new GCP project and attempts to attach it to the first available billing account. A project ID can be optionally specified; otherwise it will be automatically generated.',
|
|
92
|
-
inputSchema: {
|
|
93
|
-
projectId: z
|
|
94
|
-
.string()
|
|
95
|
-
.optional()
|
|
96
|
-
.describe(
|
|
97
|
-
'Optional. The desired ID for the new GCP project. If not provided, an ID will be auto-generated.'
|
|
98
|
-
),
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
gcpTool(gcpCredentialsAvailable, async ({ projectId }) => {
|
|
102
|
-
if (
|
|
103
|
-
projectId !== undefined &&
|
|
104
|
-
(typeof projectId !== 'string' || projectId.trim() === '')
|
|
105
|
-
) {
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: 'Error: If provided, Project ID must be a non-empty string.',
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const result = await createProjectAndAttachBilling(projectId);
|
|
117
|
-
return {
|
|
118
|
-
content: [
|
|
119
|
-
{
|
|
120
|
-
type: 'text',
|
|
121
|
-
text: `Successfully created GCP project with ID "${result.projectId}". You can now use this project ID for deployments.`,
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
};
|
|
125
|
-
} catch (error) {
|
|
126
|
-
return {
|
|
127
|
-
content: [
|
|
128
|
-
{
|
|
129
|
-
type: 'text',
|
|
130
|
-
text: `Error creating GCP project or attaching billing: ${error.message}`,
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// Listing Cloud Run services
|
|
139
|
-
server.registerTool(
|
|
140
|
-
'list_services',
|
|
141
|
-
{
|
|
142
|
-
description: 'Lists Cloud Run services in a given project and region.',
|
|
143
|
-
inputSchema: {
|
|
144
|
-
project: z
|
|
145
|
-
.string()
|
|
146
|
-
.describe('Google Cloud project ID')
|
|
147
|
-
.default(defaultProjectId),
|
|
148
|
-
region: z
|
|
149
|
-
.string()
|
|
150
|
-
.describe('Region where the services are located')
|
|
151
|
-
.default(defaultRegion),
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
gcpTool(gcpCredentialsAvailable, async ({ project, region }) => {
|
|
155
|
-
if (typeof project !== 'string') {
|
|
156
|
-
return {
|
|
157
|
-
content: [
|
|
158
|
-
{
|
|
159
|
-
type: 'text',
|
|
160
|
-
text: 'Error: Project ID must be provided and be a non-empty string.',
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const services = await listServices(project, region);
|
|
168
|
-
const serviceList = services
|
|
169
|
-
.map((s) => {
|
|
170
|
-
const serviceName = s.name.split('/').pop();
|
|
171
|
-
return `- ${serviceName} (URL: ${s.uri})`;
|
|
172
|
-
})
|
|
173
|
-
.join('\n');
|
|
174
|
-
return {
|
|
175
|
-
content: [
|
|
176
|
-
{
|
|
177
|
-
type: 'text',
|
|
178
|
-
text: `Services in project ${project} (location ${region}):\n${serviceList}`,
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
};
|
|
182
|
-
} catch (error) {
|
|
183
|
-
return {
|
|
184
|
-
content: [
|
|
185
|
-
{
|
|
186
|
-
type: 'text',
|
|
187
|
-
text: `Error listing services for project ${project} (region ${region}): ${error.message}`,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// Dynamic resource for getting a specific service
|
|
196
|
-
server.registerTool(
|
|
197
|
-
'get_service',
|
|
198
|
-
{
|
|
199
|
-
description: 'Gets details for a specific Cloud Run service.',
|
|
200
|
-
inputSchema: {
|
|
201
|
-
project: z
|
|
202
|
-
.string()
|
|
203
|
-
.describe('Google Cloud project ID containing the service')
|
|
204
|
-
.default(defaultProjectId),
|
|
205
|
-
region: z
|
|
206
|
-
.string()
|
|
207
|
-
.describe('Region where the service is located')
|
|
208
|
-
.default(defaultRegion),
|
|
209
|
-
service: z
|
|
210
|
-
.string()
|
|
211
|
-
.describe('Name of the Cloud Run service')
|
|
212
|
-
.default(defaultServiceName),
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
|
|
216
|
-
if (typeof project !== 'string') {
|
|
217
|
-
return {
|
|
218
|
-
content: [
|
|
219
|
-
{ type: 'text', text: 'Error: Project ID must be provided.' },
|
|
220
|
-
],
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (typeof service !== 'string') {
|
|
224
|
-
return {
|
|
225
|
-
content: [
|
|
226
|
-
{ type: 'text', text: 'Error: Service name must be provided.' },
|
|
227
|
-
],
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
const serviceDetails = await getService(project, region, service);
|
|
232
|
-
if (serviceDetails) {
|
|
233
|
-
return {
|
|
234
|
-
content: [
|
|
235
|
-
{
|
|
236
|
-
type: 'text',
|
|
237
|
-
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
};
|
|
241
|
-
} else {
|
|
242
|
-
return {
|
|
243
|
-
content: [
|
|
244
|
-
{
|
|
245
|
-
type: 'text',
|
|
246
|
-
text: `Service ${service} not found in project ${project} (region ${region}).`,
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
return {
|
|
253
|
-
content: [
|
|
254
|
-
{
|
|
255
|
-
type: 'text',
|
|
256
|
-
text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// Logs for a service
|
|
265
|
-
server.registerTool(
|
|
266
|
-
'get_service_log',
|
|
267
|
-
{
|
|
268
|
-
description:
|
|
269
|
-
'Gets Logs and Error Messages for a specific Cloud Run service.',
|
|
270
|
-
inputSchema: {
|
|
271
|
-
project: z
|
|
272
|
-
.string()
|
|
273
|
-
.describe('Google Cloud project ID containing the service')
|
|
274
|
-
.default(defaultProjectId),
|
|
275
|
-
region: z
|
|
276
|
-
.string()
|
|
277
|
-
.describe('Region where the service is located')
|
|
278
|
-
.default(defaultRegion),
|
|
279
|
-
service: z
|
|
280
|
-
.string()
|
|
281
|
-
.describe('Name of the Cloud Run service')
|
|
282
|
-
.default(defaultServiceName),
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
|
|
286
|
-
let allLogs = [];
|
|
287
|
-
let requestOptions;
|
|
288
|
-
try {
|
|
289
|
-
do {
|
|
290
|
-
// Fetch a page of logs
|
|
291
|
-
const response = await getServiceLogs(
|
|
292
|
-
project,
|
|
293
|
-
region,
|
|
294
|
-
service,
|
|
295
|
-
requestOptions
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
if (response.logs) {
|
|
299
|
-
allLogs.push(response.logs);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Set the requestOptions incl pagintion token for the next iteration
|
|
303
|
-
|
|
304
|
-
requestOptions = response.requestOptions;
|
|
305
|
-
} while (requestOptions); // Continue as long as there is a next page token
|
|
306
|
-
return {
|
|
307
|
-
content: [
|
|
308
|
-
{
|
|
309
|
-
type: 'text',
|
|
310
|
-
text: allLogs.join('\n'),
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return {
|
|
316
|
-
content: [
|
|
317
|
-
{
|
|
318
|
-
type: 'text',
|
|
319
|
-
text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
server.registerTool(
|
|
328
|
-
'deploy_local_files',
|
|
329
|
-
{
|
|
330
|
-
description:
|
|
331
|
-
'Deploy local files to Cloud Run. Takes an array of absolute file paths from the local filesystem that will be deployed. Use this tool if the files exists on the user local filesystem.',
|
|
332
|
-
inputSchema: {
|
|
333
|
-
project: z
|
|
334
|
-
.string()
|
|
335
|
-
.describe(
|
|
336
|
-
'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
|
|
337
|
-
)
|
|
338
|
-
.default(defaultProjectId),
|
|
339
|
-
region: z
|
|
340
|
-
.string()
|
|
341
|
-
.optional()
|
|
342
|
-
.default(defaultRegion)
|
|
343
|
-
.describe('Region to deploy the service to'),
|
|
344
|
-
service: z
|
|
345
|
-
.string()
|
|
346
|
-
.optional()
|
|
347
|
-
.default(defaultServiceName)
|
|
348
|
-
.describe('Name of the Cloud Run service to deploy to'),
|
|
349
|
-
files: z
|
|
350
|
-
.array(z.string())
|
|
351
|
-
.describe(
|
|
352
|
-
'Array of absolute file paths to deploy (e.g. ["/home/user/project/src/index.js", "/home/user/project/package.json"])'
|
|
353
|
-
),
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
gcpTool(
|
|
357
|
-
gcpCredentialsAvailable,
|
|
358
|
-
async ({ project, region, service, files }) => {
|
|
359
|
-
if (typeof project !== 'string') {
|
|
360
|
-
throw new Error(
|
|
361
|
-
'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
if (typeof files !== 'object' || !Array.isArray(files)) {
|
|
365
|
-
throw new Error('Files must specified');
|
|
366
|
-
}
|
|
367
|
-
if (files.length === 0) {
|
|
368
|
-
throw new Error('No files specified for deployment');
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Deploy to Cloud Run
|
|
372
|
-
try {
|
|
373
|
-
// TODO: Should we return intermediate progress messages? we'd need to use sendNotification for that, see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/server/jsonResponseStreamableHttp.ts#L46C24-L46C41
|
|
374
|
-
const response = await deploy({
|
|
375
|
-
projectId: project,
|
|
376
|
-
serviceName: service,
|
|
377
|
-
region: region,
|
|
378
|
-
files: files,
|
|
379
|
-
skipIamCheck: skipIamCheck, // Pass the new flag
|
|
380
|
-
});
|
|
381
|
-
return {
|
|
382
|
-
content: [
|
|
383
|
-
{
|
|
384
|
-
type: 'text',
|
|
385
|
-
text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
};
|
|
389
|
-
} catch (error) {
|
|
390
|
-
return {
|
|
391
|
-
content: [
|
|
392
|
-
{
|
|
393
|
-
type: 'text',
|
|
394
|
-
text: `Error deploying to Cloud Run: ${error.message || error}`,
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
server.registerTool(
|
|
404
|
-
'deploy_local_folder',
|
|
405
|
-
{
|
|
406
|
-
description:
|
|
407
|
-
'Deploy a local folder to Cloud Run. Takes an absolute folder path from the local filesystem that will be deployed. Use this tool if the entire folder content needs to be deployed.',
|
|
408
|
-
inputSchema: {
|
|
409
|
-
project: z
|
|
410
|
-
.string()
|
|
411
|
-
.describe(
|
|
412
|
-
'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
|
|
413
|
-
)
|
|
414
|
-
.default(defaultProjectId),
|
|
415
|
-
region: z
|
|
416
|
-
.string()
|
|
417
|
-
.optional()
|
|
418
|
-
.default(defaultRegion)
|
|
419
|
-
.describe('Region to deploy the service to'),
|
|
420
|
-
service: z
|
|
421
|
-
.string()
|
|
422
|
-
.optional()
|
|
423
|
-
.default(defaultServiceName)
|
|
424
|
-
.describe('Name of the Cloud Run service to deploy to'),
|
|
425
|
-
folderPath: z
|
|
426
|
-
.string()
|
|
427
|
-
.describe(
|
|
428
|
-
'Absolute path to the folder to deploy (e.g. "/home/user/project/src")'
|
|
429
|
-
),
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
gcpTool(
|
|
433
|
-
gcpCredentialsAvailable,
|
|
434
|
-
async ({ project, region, service, folderPath }) => {
|
|
435
|
-
if (typeof project !== 'string') {
|
|
436
|
-
throw new Error(
|
|
437
|
-
'Project must be specified, please prompt the user for a valid existing Google Cloud project ID.'
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
if (typeof folderPath !== 'string' || folderPath.trim() === '') {
|
|
441
|
-
throw new Error(
|
|
442
|
-
'Folder path must be specified and be a non-empty string.'
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Deploy to Cloud Run
|
|
447
|
-
try {
|
|
448
|
-
const response = await deploy({
|
|
449
|
-
projectId: project,
|
|
450
|
-
serviceName: service,
|
|
451
|
-
region: region,
|
|
452
|
-
files: [folderPath],
|
|
453
|
-
skipIamCheck: skipIamCheck, // Pass the new flag
|
|
454
|
-
});
|
|
455
|
-
return {
|
|
456
|
-
content: [
|
|
457
|
-
{
|
|
458
|
-
type: 'text',
|
|
459
|
-
text: `Cloud Run service ${service} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
|
|
460
|
-
},
|
|
461
|
-
],
|
|
462
|
-
};
|
|
463
|
-
} catch (error) {
|
|
464
|
-
return {
|
|
465
|
-
content: [
|
|
466
|
-
{
|
|
467
|
-
type: 'text',
|
|
468
|
-
text: `Error deploying folder to Cloud Run: ${error.message || error}`,
|
|
469
|
-
},
|
|
470
|
-
],
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
)
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
server.registerTool(
|
|
478
|
-
'deploy_file_contents',
|
|
479
|
-
{
|
|
480
|
-
description:
|
|
481
|
-
'Deploy files to Cloud Run by providing their contents directly. Takes an array of file objects containing filename and content. Use this tool if the files only exist in the current chat context.',
|
|
482
|
-
inputSchema: {
|
|
483
|
-
project: z
|
|
484
|
-
.string()
|
|
485
|
-
.describe(
|
|
486
|
-
'Google Cloud project ID. Leave unset for the app to be deployed in a new project. If provided, make sure the user confirms the project ID they want to deploy to.'
|
|
487
|
-
)
|
|
488
|
-
.default(defaultProjectId),
|
|
489
|
-
region: z
|
|
490
|
-
.string()
|
|
491
|
-
.optional()
|
|
492
|
-
.default(defaultRegion)
|
|
493
|
-
.describe('Region to deploy the service to'),
|
|
494
|
-
service: z
|
|
495
|
-
.string()
|
|
496
|
-
.optional()
|
|
497
|
-
.default(defaultServiceName)
|
|
498
|
-
.describe('Name of the Cloud Run service to deploy to'),
|
|
499
|
-
files: z
|
|
500
|
-
.array(
|
|
501
|
-
z.object({
|
|
502
|
-
filename: z
|
|
503
|
-
.string()
|
|
504
|
-
.describe(
|
|
505
|
-
'Name and path of the file (e.g. "src/index.js" or "data/config.json")'
|
|
506
|
-
),
|
|
507
|
-
content: z
|
|
508
|
-
.string()
|
|
509
|
-
.optional()
|
|
510
|
-
.describe('Text content of the file'),
|
|
511
|
-
})
|
|
512
|
-
)
|
|
513
|
-
.describe('Array of file objects containing filename and content'),
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
gcpTool(
|
|
517
|
-
gcpCredentialsAvailable,
|
|
518
|
-
async ({ project, region, service, files }) => {
|
|
519
|
-
if (typeof project !== 'string') {
|
|
520
|
-
throw new Error(
|
|
521
|
-
'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
if (typeof files !== 'object' || !Array.isArray(files)) {
|
|
525
|
-
throw new Error('Files must be specified');
|
|
526
|
-
}
|
|
527
|
-
if (files.length === 0) {
|
|
528
|
-
throw new Error('No files specified for deployment');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Validate that each file has either content
|
|
532
|
-
for (const file of files) {
|
|
533
|
-
if (!file.content) {
|
|
534
|
-
throw new Error(`File ${file.filename} must have content`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Deploy to Cloud Run
|
|
539
|
-
try {
|
|
540
|
-
const response = await deploy({
|
|
541
|
-
projectId: project,
|
|
542
|
-
serviceName: service,
|
|
543
|
-
region: region,
|
|
544
|
-
files: files,
|
|
545
|
-
skipIamCheck: skipIamCheck, // Pass the new flag
|
|
546
|
-
});
|
|
547
|
-
return {
|
|
548
|
-
content: [
|
|
549
|
-
{
|
|
550
|
-
type: 'text',
|
|
551
|
-
text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
|
|
552
|
-
},
|
|
553
|
-
],
|
|
554
|
-
};
|
|
555
|
-
} catch (error) {
|
|
556
|
-
return {
|
|
557
|
-
content: [
|
|
558
|
-
{
|
|
559
|
-
type: 'text',
|
|
560
|
-
text: `Error deploying to Cloud Run: ${error.message || error}`,
|
|
561
|
-
},
|
|
562
|
-
],
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
)
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
server.registerTool(
|
|
570
|
-
'deploy_container_image',
|
|
571
|
-
{
|
|
572
|
-
description:
|
|
573
|
-
'Deploys a container image to Cloud Run. Use this tool if the user provides a container image URL.',
|
|
574
|
-
inputSchema: {
|
|
575
|
-
project: z
|
|
576
|
-
.string()
|
|
577
|
-
.describe(
|
|
578
|
-
'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
|
|
579
|
-
)
|
|
580
|
-
.default(defaultProjectId),
|
|
581
|
-
region: z
|
|
582
|
-
.string()
|
|
583
|
-
.optional()
|
|
584
|
-
.default(defaultRegion)
|
|
585
|
-
.describe('Region to deploy the service to'),
|
|
586
|
-
service: z
|
|
587
|
-
.string()
|
|
588
|
-
.optional()
|
|
589
|
-
.default(defaultServiceName)
|
|
590
|
-
.describe('Name of the Cloud Run service to deploy to'),
|
|
591
|
-
imageUrl: z
|
|
592
|
-
.string()
|
|
593
|
-
.describe(
|
|
594
|
-
'The URL of the container image to deploy (e.g. "gcr.io/cloudrun/hello")'
|
|
595
|
-
),
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
gcpTool(
|
|
599
|
-
gcpCredentialsAvailable,
|
|
600
|
-
async ({ project, region, service, imageUrl }) => {
|
|
601
|
-
if (typeof project !== 'string') {
|
|
602
|
-
throw new Error(
|
|
603
|
-
'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
|
|
607
|
-
throw new Error(
|
|
608
|
-
'Container image URL must be specified and be a non-empty string.'
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Deploy to Cloud Run
|
|
613
|
-
try {
|
|
614
|
-
const response = await deployImage({
|
|
615
|
-
projectId: project,
|
|
616
|
-
serviceName: service,
|
|
617
|
-
region: region,
|
|
618
|
-
imageUrl: imageUrl,
|
|
619
|
-
skipIamCheck: skipIamCheck,
|
|
620
|
-
});
|
|
621
|
-
return {
|
|
622
|
-
content: [
|
|
623
|
-
{
|
|
624
|
-
type: 'text',
|
|
625
|
-
text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
|
|
626
|
-
},
|
|
627
|
-
],
|
|
628
|
-
};
|
|
629
|
-
} catch (error) {
|
|
630
|
-
return {
|
|
631
|
-
content: [
|
|
632
|
-
{
|
|
633
|
-
type: 'text',
|
|
634
|
-
text: `Error deploying to Cloud Run: ${error.message || error}`,
|
|
635
|
-
},
|
|
636
|
-
],
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
)
|
|
641
|
-
);
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
export const registerToolsRemote = async (
|
|
645
|
-
server,
|
|
646
|
-
{
|
|
647
|
-
defaultProjectId,
|
|
648
|
-
defaultRegion,
|
|
649
|
-
defaultServiceName,
|
|
650
|
-
skipIamCheck = false,
|
|
651
|
-
gcpCredentialsAvailable,
|
|
652
|
-
} = {}
|
|
653
|
-
) => {
|
|
654
|
-
// We no longer call checkGCP here; the effective defaults are passed in.
|
|
655
|
-
const currentProject = defaultProjectId; // Use the passed effective project ID
|
|
656
|
-
const currentRegion = defaultRegion; // Use the passed effective region
|
|
657
|
-
|
|
658
|
-
if (!currentProject) {
|
|
659
|
-
throw new Error(
|
|
660
|
-
'Cannot register remote tools: GCP project ID could not be determined. Please ensure GOOGLE_CLOUD_PROJECT environment variable is set or the server is running on GCP.'
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Listing Cloud Run services (Remote)
|
|
665
|
-
server.registerTool(
|
|
666
|
-
'list_services',
|
|
667
|
-
{
|
|
668
|
-
description: `Lists Cloud Run services in GCP project ${currentProject} and a given region.`,
|
|
669
|
-
inputSchema: {
|
|
670
|
-
region: z
|
|
671
|
-
.string()
|
|
672
|
-
.describe('Region where the services are located')
|
|
673
|
-
.default(currentRegion),
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
gcpTool(gcpCredentialsAvailable, async ({ region }) => {
|
|
677
|
-
try {
|
|
678
|
-
const services = await listServices(currentProject, region);
|
|
679
|
-
const serviceList = services
|
|
680
|
-
.map((s) => {
|
|
681
|
-
const serviceName = s.name.split('/').pop();
|
|
682
|
-
return `- ${serviceName} (URL: ${s.uri})`;
|
|
683
|
-
})
|
|
684
|
-
.join('\n');
|
|
685
|
-
return {
|
|
686
|
-
content: [
|
|
687
|
-
{
|
|
688
|
-
type: 'text',
|
|
689
|
-
text: `Services in project ${currentProject} (location ${region}):\n${serviceList}`,
|
|
690
|
-
},
|
|
691
|
-
],
|
|
692
|
-
};
|
|
693
|
-
} catch (error) {
|
|
694
|
-
return {
|
|
695
|
-
content: [
|
|
696
|
-
{
|
|
697
|
-
type: 'text',
|
|
698
|
-
text: `Error listing services for project ${currentProject} (region ${currentRegion}): ${error.message}`,
|
|
699
|
-
},
|
|
700
|
-
],
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
})
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
// Dynamic resource for getting a specific service (Remote)
|
|
707
|
-
server.registerTool(
|
|
708
|
-
'get_service',
|
|
709
|
-
{
|
|
710
|
-
description: `Gets details for a specific Cloud Run service in GCP project ${currentProject}.`,
|
|
711
|
-
inputSchema: {
|
|
712
|
-
region: z
|
|
713
|
-
.string()
|
|
714
|
-
.describe('Region where the service is located')
|
|
715
|
-
.default(currentRegion),
|
|
716
|
-
service: z
|
|
717
|
-
.string()
|
|
718
|
-
.describe('Name of the Cloud Run service')
|
|
719
|
-
.default(defaultServiceName),
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
gcpTool(gcpCredentialsAvailable, async ({ region, service }) => {
|
|
723
|
-
if (typeof service !== 'string') {
|
|
724
|
-
return {
|
|
725
|
-
content: [
|
|
726
|
-
{ type: 'text', text: 'Error: Service name must be provided.' },
|
|
727
|
-
],
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
try {
|
|
731
|
-
const serviceDetails = await getService(
|
|
732
|
-
currentProject,
|
|
733
|
-
region,
|
|
734
|
-
service
|
|
735
|
-
);
|
|
736
|
-
if (serviceDetails) {
|
|
737
|
-
return {
|
|
738
|
-
content: [
|
|
739
|
-
{
|
|
740
|
-
type: 'text',
|
|
741
|
-
text: `Name: ${service}\nRegion: ${region}\nProject: ${currentProject}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
|
|
742
|
-
},
|
|
743
|
-
],
|
|
744
|
-
};
|
|
745
|
-
} else {
|
|
746
|
-
return {
|
|
747
|
-
content: [
|
|
748
|
-
{
|
|
749
|
-
type: 'text',
|
|
750
|
-
text: `Service ${service} not found in project ${currentProject} (region ${currentRegion}).`,
|
|
751
|
-
},
|
|
752
|
-
],
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
} catch (error) {
|
|
756
|
-
return {
|
|
757
|
-
content: [
|
|
758
|
-
{
|
|
759
|
-
type: 'text',
|
|
760
|
-
text: `Error getting service ${service} in project ${currentProject} (region ${currentRegion}): ${error.message}`,
|
|
761
|
-
},
|
|
762
|
-
],
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
);
|
|
767
|
-
|
|
768
|
-
// Logs for a service
|
|
769
|
-
server.registerTool(
|
|
770
|
-
'get_service_log',
|
|
771
|
-
{
|
|
772
|
-
description:
|
|
773
|
-
'Gets Logs and Error Messages for a specific Cloud Run service.',
|
|
774
|
-
inputSchema: {
|
|
775
|
-
project: z
|
|
776
|
-
.string()
|
|
777
|
-
.describe('Google Cloud project ID containing the service')
|
|
778
|
-
.default(currentProject), // Use currentProject
|
|
779
|
-
region: z
|
|
780
|
-
.string()
|
|
781
|
-
.describe('Region where the service is located')
|
|
782
|
-
.default(currentRegion), // Use currentRegion
|
|
783
|
-
service: z
|
|
784
|
-
.string()
|
|
785
|
-
.describe('Name of the Cloud Run service')
|
|
786
|
-
.default(defaultServiceName),
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
gcpTool(gcpCredentialsAvailable, async ({ project, region, service }) => {
|
|
790
|
-
let allLogs = [];
|
|
791
|
-
let requestOptions;
|
|
792
|
-
try {
|
|
793
|
-
do {
|
|
794
|
-
// Fetch a page of logs
|
|
795
|
-
const response = await getServiceLogs(
|
|
796
|
-
project,
|
|
797
|
-
region,
|
|
798
|
-
service,
|
|
799
|
-
requestOptions
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
if (response.logs) {
|
|
803
|
-
allLogs.push(response.logs);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Set the requestOptions incl pagintion token for the next iteration
|
|
807
|
-
|
|
808
|
-
requestOptions = response.requestOptions;
|
|
809
|
-
} while (requestOptions); // Continue as long as there is a next page token
|
|
810
|
-
return {
|
|
811
|
-
content: [
|
|
812
|
-
{
|
|
813
|
-
type: 'text',
|
|
814
|
-
text: allLogs.join('\n'),
|
|
815
|
-
},
|
|
816
|
-
],
|
|
817
|
-
};
|
|
818
|
-
} catch (error) {
|
|
819
|
-
return {
|
|
820
|
-
content: [
|
|
821
|
-
{
|
|
822
|
-
type: 'text',
|
|
823
|
-
text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
})
|
|
829
|
-
);
|
|
830
|
-
|
|
831
|
-
// Deploy file contents to Cloud Run (Remote)
|
|
832
|
-
server.registerTool(
|
|
833
|
-
'deploy_file_contents',
|
|
834
|
-
{
|
|
835
|
-
description: `Deploy files to Cloud Run by providing their contents directly to the GCP project ${currentProject}.`,
|
|
836
|
-
inputSchema: {
|
|
837
|
-
region: z
|
|
838
|
-
.string()
|
|
839
|
-
.optional()
|
|
840
|
-
.default(currentRegion)
|
|
841
|
-
.describe('Region to deploy the service to'),
|
|
842
|
-
service: z
|
|
843
|
-
.string()
|
|
844
|
-
.optional()
|
|
845
|
-
.default(defaultServiceName)
|
|
846
|
-
.describe('Name of the Cloud Run service to deploy to'), // Use defaultServiceName
|
|
847
|
-
files: z
|
|
848
|
-
.array(
|
|
849
|
-
z.object({
|
|
850
|
-
filename: z
|
|
851
|
-
.string()
|
|
852
|
-
.describe(
|
|
853
|
-
'Name and path of the file (e.g. "src/index.js" or "data/config.json")'
|
|
854
|
-
),
|
|
855
|
-
content: z.string().describe('Text content of the file'),
|
|
856
|
-
})
|
|
857
|
-
)
|
|
858
|
-
.describe('Array of file objects containing filename and content'),
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
gcpTool(gcpCredentialsAvailable, async ({ region, service, files }) => {
|
|
862
|
-
console.log(
|
|
863
|
-
`New deploy request (remote): ${JSON.stringify({ project: currentProject, region, service, files })}`
|
|
864
|
-
);
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
typeof files !== 'object' ||
|
|
868
|
-
!Array.isArray(files) ||
|
|
869
|
-
files.length === 0
|
|
870
|
-
) {
|
|
871
|
-
throw new Error('Files must be specified');
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Validate that each file has content
|
|
875
|
-
for (const file of files) {
|
|
876
|
-
if (!file.content) {
|
|
877
|
-
throw new Error(`File ${file.filename} must have content`);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Deploy to Cloud Run
|
|
882
|
-
try {
|
|
883
|
-
const response = await deploy({
|
|
884
|
-
projectId: currentProject,
|
|
885
|
-
serviceName: service,
|
|
886
|
-
region: region,
|
|
887
|
-
files: files,
|
|
888
|
-
skipIamCheck: skipIamCheck, // Pass the new flag
|
|
889
|
-
});
|
|
890
|
-
return {
|
|
891
|
-
content: [
|
|
892
|
-
{
|
|
893
|
-
type: 'text',
|
|
894
|
-
text: `Cloud Run service ${service} deployed in project ${currentProject}\nCloud Console: https://console.cloud. google.com/run/detail/${region}/${service}?project=${currentProject}\nService URL: ${response.uri}`,
|
|
895
|
-
},
|
|
896
|
-
],
|
|
897
|
-
};
|
|
898
|
-
} catch (error) {
|
|
899
|
-
return {
|
|
900
|
-
content: [
|
|
901
|
-
{
|
|
902
|
-
type: 'text',
|
|
903
|
-
text: `Error deploying to Cloud Run: ${error.message || error}`,
|
|
904
|
-
},
|
|
905
|
-
],
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
})
|
|
909
|
-
);
|
|
910
|
-
|
|
911
|
-
server.registerTool(
|
|
912
|
-
'deploy_container_image',
|
|
913
|
-
{
|
|
914
|
-
description: `Deploys a container image to Cloud Run in the GCP project ${currentProject}. Use this tool if the user provides a container image URL.`,
|
|
915
|
-
inputSchema: {
|
|
916
|
-
region: z
|
|
917
|
-
.string()
|
|
918
|
-
.optional()
|
|
919
|
-
.default(currentRegion)
|
|
920
|
-
.describe('Region to deploy the service to'),
|
|
921
|
-
service: z
|
|
922
|
-
.string()
|
|
923
|
-
.optional()
|
|
924
|
-
.default(defaultServiceName)
|
|
925
|
-
.describe('Name of the Cloud Run service to deploy to'),
|
|
926
|
-
imageUrl: z
|
|
927
|
-
.string()
|
|
928
|
-
.describe(
|
|
929
|
-
'The URL of the container image to deploy (e.g. "gcr.io/cloudrun/hello")'
|
|
930
|
-
),
|
|
931
|
-
},
|
|
932
|
-
},
|
|
933
|
-
gcpTool(gcpCredentialsAvailable, async ({ region, service, imageUrl }) => {
|
|
934
|
-
if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
|
|
935
|
-
throw new Error(
|
|
936
|
-
'Container image URL must be specified and be a non-empty string.'
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// Deploy to Cloud Run
|
|
941
|
-
try {
|
|
942
|
-
const response = await deployImage({
|
|
943
|
-
projectId: currentProject,
|
|
944
|
-
serviceName: service,
|
|
945
|
-
region: region,
|
|
946
|
-
imageUrl: imageUrl,
|
|
947
|
-
skipIamCheck: skipIamCheck,
|
|
948
|
-
});
|
|
949
|
-
return {
|
|
950
|
-
content: [
|
|
951
|
-
{
|
|
952
|
-
type: 'text',
|
|
953
|
-
text: `Cloud Run service ${service} deployed in project ${currentProject}\nCloud Console: https://console.cloud. google.com/run/detail/${region}/${service}?project=${currentProject}\nService URL: ${response.uri}`,
|
|
954
|
-
},
|
|
955
|
-
],
|
|
956
|
-
};
|
|
957
|
-
} catch (error) {
|
|
958
|
-
return {
|
|
959
|
-
content: [
|
|
960
|
-
{
|
|
961
|
-
type: 'text',
|
|
962
|
-
text: `Error deploying to Cloud Run: ${error.message || error}`,
|
|
963
|
-
},
|
|
964
|
-
],
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
})
|
|
968
|
-
);
|
|
969
|
-
};
|