@google-cloud/cloud-run-mcp 1.5.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/workflows/lint-checker.yml +17 -0
- package/.kokoro/kokoro_build.sh +22 -0
- package/.kokoro/presubmit.cfg +21 -0
- package/.kokoro/run_tests.sh +31 -0
- package/CONTRIBUTING.md +23 -8
- package/README.md +52 -11
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +1 -1
- 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 +21 -10
- package/package.json +7 -4
- 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/notifications.test.js +9 -6
- package/test/local/tools.test.js +114 -70
- package/test/need-gcp/cloud-run-services.test.js +44 -44
- package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +3 -15
- package/test/need-gcp/test-helpers.js +175 -0
- package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
- package/tools/register-tools.js +106 -97
- package/tools/tools.js +2 -9
- package/lib/cloud-run-deploy.js +0 -937
- /package/lib/{gcp-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
|
@@ -0,0 +1,100 @@
|
|
|
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 fs from 'fs/promises';
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { test, describe, before, after } from 'node:test';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
import { deploy, deployImage } from '../../../lib/deployment/deployer.js';
|
|
23
|
+
import {
|
|
24
|
+
cleanupProject,
|
|
25
|
+
setSourceDeployProjectPermissions,
|
|
26
|
+
setupProject,
|
|
27
|
+
} from '../test-helpers.js';
|
|
28
|
+
|
|
29
|
+
describe('Deployment workflows', () => {
|
|
30
|
+
let projectId;
|
|
31
|
+
|
|
32
|
+
before(async () => {
|
|
33
|
+
try {
|
|
34
|
+
projectId = await setupProject();
|
|
35
|
+
await setSourceDeployProjectPermissions(projectId);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Error during project creation and setup:', err);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('Scenario-1: Starting deployment of hello image...', async () => {
|
|
43
|
+
const configImageDeploy = {
|
|
44
|
+
projectId: projectId,
|
|
45
|
+
serviceName: 'hello-scenario',
|
|
46
|
+
region: 'europe-west1',
|
|
47
|
+
imageUrl: 'gcr.io/cloudrun/hello',
|
|
48
|
+
};
|
|
49
|
+
await deployImage(configImageDeploy);
|
|
50
|
+
|
|
51
|
+
console.log('Scenario-1: Deployment completed.');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Scenario-2: Starting deployment with invalid files...', async () => {
|
|
55
|
+
const configFailingBuild = {
|
|
56
|
+
projectId: projectId,
|
|
57
|
+
serviceName: 'example-failing-app',
|
|
58
|
+
region: 'europe-west1',
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
filename: 'main.txt',
|
|
62
|
+
content:
|
|
63
|
+
'This is not a valid application source file and should cause a build failure.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
await assert.rejects(
|
|
68
|
+
deploy(configFailingBuild),
|
|
69
|
+
{ message: /ERROR: failed to detect: no buildpacks participating/ },
|
|
70
|
+
'Deployment should have failed with a buildpack detection error'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Scenario-3: Starting deployment of Go app with file content...', async () => {
|
|
75
|
+
const mainGoContent = await fs.readFile(
|
|
76
|
+
path.resolve('example-sources-to-deploy/main.go'),
|
|
77
|
+
'utf-8'
|
|
78
|
+
);
|
|
79
|
+
const goModContent = await fs.readFile(
|
|
80
|
+
path.resolve('example-sources-to-deploy/go.mod'),
|
|
81
|
+
'utf-8'
|
|
82
|
+
);
|
|
83
|
+
const configGoWithContent = {
|
|
84
|
+
projectId: projectId,
|
|
85
|
+
serviceName: 'example-go-app-content',
|
|
86
|
+
region: 'europe-west1',
|
|
87
|
+
files: [
|
|
88
|
+
{ filename: 'main.go', content: mainGoContent },
|
|
89
|
+
{ filename: 'go.mod', content: goModContent },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
await deploy(configGoWithContent);
|
|
93
|
+
console.log('Scenario-3: Deployment completed.');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
after(async () => {
|
|
97
|
+
// Clean up: delete the project created for tests
|
|
98
|
+
cleanupProject(projectId);
|
|
99
|
+
});
|
|
100
|
+
});
|
package/tools/register-tools.js
CHANGED
|
@@ -15,9 +15,16 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
import {
|
|
19
|
+
listProjects,
|
|
20
|
+
createProjectAndAttachBilling,
|
|
21
|
+
} from '../lib/cloud-api/projects.js';
|
|
22
|
+
import {
|
|
23
|
+
listServices,
|
|
24
|
+
getService,
|
|
25
|
+
getServiceLogs,
|
|
26
|
+
} from '../lib/cloud-api/run.js';
|
|
27
|
+
import { deploy, deployImage } from '../lib/deployment/deployer.js';
|
|
21
28
|
|
|
22
29
|
function createProgressCallback(sendNotification) {
|
|
23
30
|
return (progress) => {
|
|
@@ -111,7 +118,7 @@ function registerCreateProjectTool(server, options) {
|
|
|
111
118
|
content: [
|
|
112
119
|
{
|
|
113
120
|
type: 'text',
|
|
114
|
-
text:
|
|
121
|
+
text: result.billingMessage,
|
|
115
122
|
},
|
|
116
123
|
],
|
|
117
124
|
};
|
|
@@ -134,19 +141,15 @@ function registerListServicesTool(server, options) {
|
|
|
134
141
|
server.registerTool(
|
|
135
142
|
'list_services',
|
|
136
143
|
{
|
|
137
|
-
description: 'Lists Cloud Run services in a given project
|
|
144
|
+
description: 'Lists all Cloud Run services in a given project.',
|
|
138
145
|
inputSchema: {
|
|
139
146
|
project: z
|
|
140
147
|
.string()
|
|
141
148
|
.describe('Google Cloud project ID')
|
|
142
149
|
.default(options.defaultProjectId),
|
|
143
|
-
region: z
|
|
144
|
-
.string()
|
|
145
|
-
.describe('Region where the services are located')
|
|
146
|
-
.default(options.defaultRegion),
|
|
147
150
|
},
|
|
148
151
|
},
|
|
149
|
-
gcpTool(options.gcpCredentialsAvailable, async ({ project
|
|
152
|
+
gcpTool(options.gcpCredentialsAvailable, async ({ project }) => {
|
|
150
153
|
if (typeof project !== 'string') {
|
|
151
154
|
return {
|
|
152
155
|
content: [
|
|
@@ -157,29 +160,29 @@ function registerListServicesTool(server, options) {
|
|
|
157
160
|
],
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
|
-
|
|
161
163
|
try {
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
}
|
|
164
|
+
const allServices = await listServices(project);
|
|
165
|
+
const content = [];
|
|
166
|
+
for (const region of Object.keys(allServices)) {
|
|
167
|
+
const serviceList = allServices[region];
|
|
168
|
+
const servicesText = serviceList
|
|
169
|
+
.map((s) => {
|
|
170
|
+
const serviceName = s.name.split('/').pop();
|
|
171
|
+
return `- ${serviceName} (URL: ${s.uri})`;
|
|
172
|
+
})
|
|
173
|
+
.join('\n');
|
|
174
|
+
content.push({
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: `Services in project ${project} (location ${region}):\n${servicesText}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { content };
|
|
177
180
|
} catch (error) {
|
|
178
181
|
return {
|
|
179
182
|
content: [
|
|
180
183
|
{
|
|
181
184
|
type: 'text',
|
|
182
|
-
text: `Error listing services for project ${project}
|
|
185
|
+
text: `Error listing services for project ${project}: ${error.message}`,
|
|
183
186
|
},
|
|
184
187
|
],
|
|
185
188
|
};
|
|
@@ -209,53 +212,56 @@ function registerGetServiceTool(server, options) {
|
|
|
209
212
|
.default(options.defaultServiceName),
|
|
210
213
|
},
|
|
211
214
|
},
|
|
212
|
-
gcpTool(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{ type: 'text', text: 'Error: Project ID must be provided.' },
|
|
217
|
-
],
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (typeof service !== 'string') {
|
|
221
|
-
return {
|
|
222
|
-
content: [
|
|
223
|
-
{ type: 'text', text: 'Error: Service name must be provided.' },
|
|
224
|
-
],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
const serviceDetails = await getService(project, region, service);
|
|
229
|
-
if (serviceDetails) {
|
|
215
|
+
gcpTool(
|
|
216
|
+
options.gcpCredentialsAvailable,
|
|
217
|
+
async ({ project, region, service }) => {
|
|
218
|
+
if (typeof project !== 'string') {
|
|
230
219
|
return {
|
|
231
220
|
content: [
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
221
|
+
{ type: 'text', text: 'Error: Project ID must be provided.' },
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (typeof service !== 'string') {
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{ type: 'text', text: 'Error: Service name must be provided.' },
|
|
236
229
|
],
|
|
237
230
|
};
|
|
238
|
-
}
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const serviceDetails = await getService(project, region, service);
|
|
234
|
+
if (serviceDetails) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: 'text',
|
|
239
|
+
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
} else {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `Service ${service} not found in project ${project} (region ${region}).`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
239
254
|
return {
|
|
240
255
|
content: [
|
|
241
256
|
{
|
|
242
257
|
type: 'text',
|
|
243
|
-
text: `
|
|
258
|
+
text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
244
259
|
},
|
|
245
260
|
],
|
|
246
261
|
};
|
|
247
262
|
}
|
|
248
|
-
} catch (error) {
|
|
249
|
-
return {
|
|
250
|
-
content: [
|
|
251
|
-
{
|
|
252
|
-
type: 'text',
|
|
253
|
-
text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
};
|
|
257
263
|
}
|
|
258
|
-
|
|
264
|
+
)
|
|
259
265
|
);
|
|
260
266
|
}
|
|
261
267
|
|
|
@@ -281,46 +287,49 @@ function registerGetServiceLogTool(server, options) {
|
|
|
281
287
|
.default(options.defaultServiceName),
|
|
282
288
|
},
|
|
283
289
|
},
|
|
284
|
-
gcpTool(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
gcpTool(
|
|
291
|
+
options.gcpCredentialsAvailable,
|
|
292
|
+
async ({ project, region, service }) => {
|
|
293
|
+
let allLogs = [];
|
|
294
|
+
let requestOptions;
|
|
295
|
+
try {
|
|
296
|
+
do {
|
|
297
|
+
// Fetch a page of logs
|
|
298
|
+
const response = await getServiceLogs(
|
|
299
|
+
project,
|
|
300
|
+
region,
|
|
301
|
+
service,
|
|
302
|
+
requestOptions
|
|
303
|
+
);
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
if (response.logs) {
|
|
306
|
+
allLogs.push(response.logs);
|
|
307
|
+
}
|
|
300
308
|
|
|
301
|
-
|
|
309
|
+
// Set the requestOptions incl pagintion token for the next iteration
|
|
302
310
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
311
|
+
requestOptions = response.requestOptions;
|
|
312
|
+
} while (requestOptions); // Continue as long as there is a next page token
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: 'text',
|
|
317
|
+
text: allLogs.join('\n'),
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
322
331
|
}
|
|
323
|
-
|
|
332
|
+
)
|
|
324
333
|
);
|
|
325
334
|
}
|
|
326
335
|
|
package/tools/tools.js
CHANGED
|
@@ -27,10 +27,7 @@ import {
|
|
|
27
27
|
registerDeployContainerImageTool,
|
|
28
28
|
} from './register-tools.js';
|
|
29
29
|
|
|
30
|
-
export const registerTools = (
|
|
31
|
-
server,
|
|
32
|
-
options = {}
|
|
33
|
-
) => {
|
|
30
|
+
export const registerTools = (server, options = {}) => {
|
|
34
31
|
registerListProjectsTool(server, options);
|
|
35
32
|
registerCreateProjectTool(server, options);
|
|
36
33
|
registerListServicesTool(server, options);
|
|
@@ -41,10 +38,7 @@ export const registerTools = (
|
|
|
41
38
|
registerDeployContainerImageTool(server, options);
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
export const registerToolsRemote = (
|
|
45
|
-
server,
|
|
46
|
-
options = {}
|
|
47
|
-
) => {
|
|
41
|
+
export const registerToolsRemote = (server, options = {}) => {
|
|
48
42
|
// For remote, use the same registration functions but with effective project/region passed in options
|
|
49
43
|
registerListServicesTool(server, options);
|
|
50
44
|
registerGetServiceTool(server, options);
|
|
@@ -52,4 +46,3 @@ export const registerToolsRemote = (
|
|
|
52
46
|
registerDeployFileContentsTool(server, options);
|
|
53
47
|
registerDeployContainerImageTool(server, options);
|
|
54
48
|
};
|
|
55
|
-
|