@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.
- package/.prettierignore +2 -0
- package/.prettierrc.json +8 -0
- package/CONTRIBUTING.md +107 -0
- package/Dockerfile +7 -0
- package/LICENSE +201 -0
- package/README.md +191 -0
- package/example-sources-to-deploy/Dockerfile +49 -0
- package/example-sources-to-deploy/go.mod +3 -0
- package/example-sources-to-deploy/main.go +52 -0
- package/gemini-extension.json +10 -0
- package/lib/cloud-run-deploy.js +934 -0
- package/lib/cloud-run-services.js +181 -0
- package/lib/gcp-auth-check.js +79 -0
- package/lib/gcp-billing-accounts.js +98 -0
- package/lib/gcp-metadata.js +62 -0
- package/lib/gcp-projects.js +156 -0
- package/mcp-server.js +221 -0
- package/package.json +60 -0
- package/prompts.js +96 -0
- package/test/run-all-tests.js +19 -0
- package/test/test-create-project.js +77 -0
- package/test/test-deploy.js +158 -0
- package/test/test-gcp-auth.js +317 -0
- package/test/test-mcp-server.js +56 -0
- package/test/test-prompts.js +174 -0
- package/test/test-service-logs.js +109 -0
- package/test/test-tools.js +393 -0
- package/tools.js +936 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it, mock } from 'node:test';
|
|
3
|
+
import esmock from 'esmock';
|
|
4
|
+
|
|
5
|
+
describe('registerTools', () => {
|
|
6
|
+
it('should register all tools', async () => {
|
|
7
|
+
const server = {
|
|
8
|
+
registerTool: mock.fn(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { registerTools } = await esmock('../tools.js', {});
|
|
12
|
+
|
|
13
|
+
registerTools(server);
|
|
14
|
+
|
|
15
|
+
assert.strictEqual(server.registerTool.mock.callCount(), 9);
|
|
16
|
+
const toolNames = server.registerTool.mock.calls.map(
|
|
17
|
+
(call) => call.arguments[0]
|
|
18
|
+
);
|
|
19
|
+
assert.deepStrictEqual(
|
|
20
|
+
toolNames.sort(),
|
|
21
|
+
[
|
|
22
|
+
'create_project',
|
|
23
|
+
'deploy_container_image',
|
|
24
|
+
'deploy_file_contents',
|
|
25
|
+
'deploy_local_files',
|
|
26
|
+
'deploy_local_folder',
|
|
27
|
+
'get_service',
|
|
28
|
+
'get_service_log',
|
|
29
|
+
'list_projects',
|
|
30
|
+
'list_services',
|
|
31
|
+
].sort()
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('list_projects', () => {
|
|
36
|
+
it('should list projects', async () => {
|
|
37
|
+
const server = {
|
|
38
|
+
registerTool: mock.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
42
|
+
'../lib/gcp-projects.js': {
|
|
43
|
+
listProjects: () =>
|
|
44
|
+
Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
registerTools(server);
|
|
49
|
+
|
|
50
|
+
const handler = server.registerTool.mock.calls.find(
|
|
51
|
+
(call) => call.arguments[0] === 'list_projects'
|
|
52
|
+
).arguments[2];
|
|
53
|
+
const result = await handler({});
|
|
54
|
+
|
|
55
|
+
assert.deepStrictEqual(result, {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: 'Available GCP Projects:\n- project1\n- project2',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('create_project', () => {
|
|
67
|
+
it('should create a project with a provided id', async () => {
|
|
68
|
+
const server = {
|
|
69
|
+
registerTool: mock.fn(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
73
|
+
'../lib/gcp-projects.js': {
|
|
74
|
+
createProjectAndAttachBilling: (projectId) =>
|
|
75
|
+
Promise.resolve({
|
|
76
|
+
projectId: projectId,
|
|
77
|
+
billingMessage: 'billing message',
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
registerTools(server);
|
|
83
|
+
|
|
84
|
+
const handler = server.registerTool.mock.calls.find(
|
|
85
|
+
(call) => call.arguments[0] === 'create_project'
|
|
86
|
+
).arguments[2];
|
|
87
|
+
const result = await handler({ projectId: 'my-project' });
|
|
88
|
+
|
|
89
|
+
assert.deepStrictEqual(result, {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: 'Successfully created GCP project with ID "my-project". You can now use this project ID for deployments.',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should create a project with a generated id', async () => {
|
|
100
|
+
const server = {
|
|
101
|
+
registerTool: mock.fn(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
105
|
+
'../lib/gcp-projects.js': {
|
|
106
|
+
createProjectAndAttachBilling: () =>
|
|
107
|
+
Promise.resolve({
|
|
108
|
+
projectId: 'generated-project',
|
|
109
|
+
billingMessage: 'billing message',
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
registerTools(server);
|
|
115
|
+
|
|
116
|
+
const handler = server.registerTool.mock.calls.find(
|
|
117
|
+
(call) => call.arguments[0] === 'create_project'
|
|
118
|
+
).arguments[2];
|
|
119
|
+
const result = await handler({});
|
|
120
|
+
|
|
121
|
+
assert.deepStrictEqual(result, {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: 'Successfully created GCP project with ID "generated-project". You can now use this project ID for deployments.',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('list_services', () => {
|
|
133
|
+
it('should list services', async () => {
|
|
134
|
+
const server = {
|
|
135
|
+
registerTool: mock.fn(),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
139
|
+
'../lib/cloud-run-services.js': {
|
|
140
|
+
listServices: () =>
|
|
141
|
+
Promise.resolve([
|
|
142
|
+
{ name: 'service1', uri: 'uri1' },
|
|
143
|
+
{ name: 'service2', uri: 'uri2' },
|
|
144
|
+
]),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
registerTools(server);
|
|
149
|
+
|
|
150
|
+
const handler = server.registerTool.mock.calls.find(
|
|
151
|
+
(call) => call.arguments[0] === 'list_services'
|
|
152
|
+
).arguments[2];
|
|
153
|
+
const result = await handler({
|
|
154
|
+
project: 'my-project',
|
|
155
|
+
region: 'my-region',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
assert.deepStrictEqual(result, {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: 'Services in project my-project (location my-region):\n- service1 (URL: uri1)\n- service2 (URL: uri2)',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('get_service', () => {
|
|
170
|
+
it('should get a service', async () => {
|
|
171
|
+
const server = {
|
|
172
|
+
registerTool: mock.fn(),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
176
|
+
'../lib/cloud-run-services.js': {
|
|
177
|
+
getService: () =>
|
|
178
|
+
Promise.resolve({
|
|
179
|
+
name: 'my-service',
|
|
180
|
+
uri: 'my-uri',
|
|
181
|
+
lastModifier: 'me',
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
registerTools(server);
|
|
187
|
+
|
|
188
|
+
const handler = server.registerTool.mock.calls.find(
|
|
189
|
+
(call) => call.arguments[0] === 'get_service'
|
|
190
|
+
).arguments[2];
|
|
191
|
+
const result = await handler({
|
|
192
|
+
project: 'my-project',
|
|
193
|
+
region: 'my-region',
|
|
194
|
+
service: 'my-service',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.deepStrictEqual(result, {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: 'text',
|
|
201
|
+
text: 'Name: my-service\nRegion: my-region\nProject: my-project\nURL: my-uri\nLast deployed by: me',
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('get_service_log', () => {
|
|
209
|
+
it('should get service logs', async () => {
|
|
210
|
+
const server = {
|
|
211
|
+
registerTool: mock.fn(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let callCount = 0;
|
|
215
|
+
const getServiceLogs = () => {
|
|
216
|
+
callCount++;
|
|
217
|
+
if (callCount === 1) {
|
|
218
|
+
return Promise.resolve({
|
|
219
|
+
logs: 'log1\nlog2',
|
|
220
|
+
requestOptions: { pageToken: 'nextPage' },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
227
|
+
'../lib/cloud-run-services.js': {
|
|
228
|
+
getServiceLogs: getServiceLogs,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
registerTools(server);
|
|
233
|
+
|
|
234
|
+
const handler = server.registerTool.mock.calls.find(
|
|
235
|
+
(call) => call.arguments[0] === 'get_service_log'
|
|
236
|
+
).arguments[2];
|
|
237
|
+
const result = await handler({
|
|
238
|
+
project: 'my-project',
|
|
239
|
+
region: 'my-region',
|
|
240
|
+
service: 'my-service',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
assert.deepStrictEqual(result, {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: 'log1\nlog2\nlog3\nlog4',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('deploy_local_files', () => {
|
|
255
|
+
it('should deploy local files', async () => {
|
|
256
|
+
const server = {
|
|
257
|
+
registerTool: mock.fn(),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
261
|
+
'../lib/cloud-run-deploy.js': {
|
|
262
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
registerTools(server);
|
|
267
|
+
|
|
268
|
+
const handler = server.registerTool.mock.calls.find(
|
|
269
|
+
(call) => call.arguments[0] === 'deploy_local_files'
|
|
270
|
+
).arguments[2];
|
|
271
|
+
const result = await handler({
|
|
272
|
+
project: 'my-project',
|
|
273
|
+
region: 'my-region',
|
|
274
|
+
service: 'my-service',
|
|
275
|
+
files: ['file1', 'file2'],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
assert.deepStrictEqual(result, {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('deploy_local_folder', () => {
|
|
290
|
+
it('should deploy local folder', async () => {
|
|
291
|
+
const server = {
|
|
292
|
+
registerTool: mock.fn(),
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
296
|
+
'../lib/cloud-run-deploy.js': {
|
|
297
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
registerTools(server);
|
|
302
|
+
|
|
303
|
+
const handler = server.registerTool.mock.calls.find(
|
|
304
|
+
(call) => call.arguments[0] === 'deploy_local_folder'
|
|
305
|
+
).arguments[2];
|
|
306
|
+
const result = await handler({
|
|
307
|
+
project: 'my-project',
|
|
308
|
+
region: 'my-region',
|
|
309
|
+
service: 'my-service',
|
|
310
|
+
folderPath: '/my/folder',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
assert.deepStrictEqual(result, {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: 'text',
|
|
317
|
+
text: 'Cloud Run service my-service deployed from folder /my/folder in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('deploy_file_contents', () => {
|
|
325
|
+
it('should deploy file contents', async () => {
|
|
326
|
+
const server = {
|
|
327
|
+
registerTool: mock.fn(),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
331
|
+
'../lib/cloud-run-deploy.js': {
|
|
332
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
registerTools(server);
|
|
337
|
+
|
|
338
|
+
const handler = server.registerTool.mock.calls.find(
|
|
339
|
+
(call) => call.arguments[0] === 'deploy_file_contents'
|
|
340
|
+
).arguments[2];
|
|
341
|
+
const result = await handler({
|
|
342
|
+
project: 'my-project',
|
|
343
|
+
region: 'my-region',
|
|
344
|
+
service: 'my-service',
|
|
345
|
+
files: [{ filename: 'file1', content: 'content1' }],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
assert.deepStrictEqual(result, {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('deploy_container_image', () => {
|
|
360
|
+
it('should deploy container image', async () => {
|
|
361
|
+
const server = {
|
|
362
|
+
registerTool: mock.fn(),
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const { registerTools } = await esmock('../tools.js', {
|
|
366
|
+
'../lib/cloud-run-deploy.js': {
|
|
367
|
+
deployImage: () => Promise.resolve({ uri: 'my-uri' }),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
registerTools(server);
|
|
372
|
+
|
|
373
|
+
const handler = server.registerTool.mock.calls.find(
|
|
374
|
+
(call) => call.arguments[0] === 'deploy_container_image'
|
|
375
|
+
).arguments[2];
|
|
376
|
+
const result = await handler({
|
|
377
|
+
project: 'my-project',
|
|
378
|
+
region: 'my-region',
|
|
379
|
+
service: 'my-service',
|
|
380
|
+
imageUrl: 'gcr.io/my-project/my-image',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
assert.deepStrictEqual(result, {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: 'text',
|
|
387
|
+
text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|