@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,317 @@
|
|
|
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 assert from 'node:assert/strict';
|
|
18
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
19
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
20
|
+
import { ensureGCPCredentials } from '../lib/gcp-auth-check.js';
|
|
21
|
+
|
|
22
|
+
describe('ensureGCPCredentials', () => {
|
|
23
|
+
let originalConsoleLog;
|
|
24
|
+
let originalConsoleError;
|
|
25
|
+
let originalProcessExit;
|
|
26
|
+
|
|
27
|
+
let capturedConsoleOutput;
|
|
28
|
+
|
|
29
|
+
let consoleLogMockFn;
|
|
30
|
+
let consoleErrorMockFn;
|
|
31
|
+
let processExitMockFn;
|
|
32
|
+
|
|
33
|
+
let getClientMock;
|
|
34
|
+
let getAccessTokenMock;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Store original methods
|
|
38
|
+
originalConsoleLog = console.log;
|
|
39
|
+
originalConsoleError = console.error;
|
|
40
|
+
originalProcessExit = process.exit;
|
|
41
|
+
|
|
42
|
+
capturedConsoleOutput = []; // Reset for each test
|
|
43
|
+
|
|
44
|
+
// Create mock.fn instances and assign them to global console/process
|
|
45
|
+
consoleLogMockFn = mock.fn((...args) => {
|
|
46
|
+
capturedConsoleOutput.push(args.join(' '));
|
|
47
|
+
});
|
|
48
|
+
consoleErrorMockFn = mock.fn((...args) => {
|
|
49
|
+
capturedConsoleOutput.push(args.join(' '));
|
|
50
|
+
});
|
|
51
|
+
processExitMockFn = mock.fn((code) => {
|
|
52
|
+
throw new Error(`Mocked process.exit called with code ${code}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Overwrite the global console and process methods
|
|
56
|
+
console.log = consoleLogMockFn;
|
|
57
|
+
console.error = consoleErrorMockFn;
|
|
58
|
+
process.exit = processExitMockFn;
|
|
59
|
+
|
|
60
|
+
// Mock GoogleAuth.prototype.getClient and AuthClient.prototype.getAccessToken.
|
|
61
|
+
const mockClient = {
|
|
62
|
+
getAccessToken: mock.fn(async () => ({ token: 'mock-token' })),
|
|
63
|
+
};
|
|
64
|
+
getAccessTokenMock = mockClient.getAccessToken;
|
|
65
|
+
getClientMock = mock.method(
|
|
66
|
+
GoogleAuth.prototype,
|
|
67
|
+
'getClient',
|
|
68
|
+
async () => mockClient
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
// Restore original methods
|
|
74
|
+
console.log = originalConsoleLog;
|
|
75
|
+
console.error = originalConsoleError;
|
|
76
|
+
process.exit = originalProcessExit;
|
|
77
|
+
|
|
78
|
+
// Restore other mocks created with `mock.method`
|
|
79
|
+
mock.restoreAll();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should resolve successfully when ADC are found', async () => {
|
|
83
|
+
await assert.doesNotThrow(async () => {
|
|
84
|
+
await ensureGCPCredentials();
|
|
85
|
+
}, 'ensureGCPCredentials should resolve on success');
|
|
86
|
+
|
|
87
|
+
await Promise.resolve();
|
|
88
|
+
|
|
89
|
+
assert.deepStrictEqual(
|
|
90
|
+
capturedConsoleOutput,
|
|
91
|
+
[
|
|
92
|
+
'Checking for Google Cloud Application Default Credentials...',
|
|
93
|
+
'Application Default Credentials found.',
|
|
94
|
+
],
|
|
95
|
+
'Console output should indicate successful ADC discovery'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
assert.strictEqual(
|
|
99
|
+
processExitMockFn.mock.callCount(),
|
|
100
|
+
0,
|
|
101
|
+
'process.exit should not be called on success'
|
|
102
|
+
);
|
|
103
|
+
assert.strictEqual(
|
|
104
|
+
consoleErrorMockFn.mock.callCount(),
|
|
105
|
+
1,
|
|
106
|
+
'console.error should be called once (checking message)'
|
|
107
|
+
);
|
|
108
|
+
assert.strictEqual(
|
|
109
|
+
consoleLogMockFn.mock.callCount(),
|
|
110
|
+
1,
|
|
111
|
+
'console.log should be called once (for success message)'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
assert.strictEqual(
|
|
115
|
+
getClientMock.mock.callCount(),
|
|
116
|
+
1,
|
|
117
|
+
'GoogleAuth.getClient should be called once'
|
|
118
|
+
);
|
|
119
|
+
assert.strictEqual(
|
|
120
|
+
getAccessTokenMock.mock.callCount(),
|
|
121
|
+
1,
|
|
122
|
+
'client.getAccessToken should be called once'
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should log error and exit when ADC are not found (generic error)', async () => {
|
|
127
|
+
const errorMessage = 'Failed to find credentials.';
|
|
128
|
+
const errorWithStack = new Error(errorMessage);
|
|
129
|
+
getClientMock.mock.mockImplementation(async () => {
|
|
130
|
+
throw errorWithStack;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await ensureGCPCredentials();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
assert.ok(
|
|
137
|
+
err.message.includes('Mocked process.exit called with code 1'),
|
|
138
|
+
'Error message from mocked process.exit should be caught'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
// Add `await Promise.resolve()` here too for consistency, if needed
|
|
142
|
+
await Promise.resolve();
|
|
143
|
+
|
|
144
|
+
const expectedOutput = [
|
|
145
|
+
'Checking for Google Cloud Application Default Credentials...',
|
|
146
|
+
'ERROR: Google Cloud Application Default Credentials are not set up.',
|
|
147
|
+
'An unexpected error occurred during credential verification.',
|
|
148
|
+
'\nFor more details or alternative setup methods, consider:',
|
|
149
|
+
'1. If running locally, run: gcloud auth application-default login.',
|
|
150
|
+
'2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
|
|
151
|
+
'3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
|
|
152
|
+
`\nOriginal error message from Google Auth Library: ${errorMessage}`,
|
|
153
|
+
`Error stack: ${errorWithStack.stack}`,
|
|
154
|
+
];
|
|
155
|
+
assert.deepStrictEqual(
|
|
156
|
+
capturedConsoleOutput,
|
|
157
|
+
expectedOutput,
|
|
158
|
+
'Console output should match generic error messages'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(
|
|
162
|
+
processExitMockFn.mock.callCount(),
|
|
163
|
+
1,
|
|
164
|
+
'process.exit should be called once'
|
|
165
|
+
);
|
|
166
|
+
assert.strictEqual(
|
|
167
|
+
processExitMockFn.mock.calls[0].arguments[0],
|
|
168
|
+
1,
|
|
169
|
+
'process.exit should be called with code 1'
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should log HTTP error and exit when google-auth-library throws an HTTP error', async () => {
|
|
174
|
+
const httpErrorMessage = 'Request failed with status code 401';
|
|
175
|
+
const httpError = new Error(httpErrorMessage);
|
|
176
|
+
httpError.response = { status: 401 };
|
|
177
|
+
const errorWithStack = httpError;
|
|
178
|
+
|
|
179
|
+
getClientMock.mock.mockImplementation(async () => {
|
|
180
|
+
throw httpError;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await ensureGCPCredentials();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
assert.ok(
|
|
187
|
+
err.message.includes('Mocked process.exit called with code 1'),
|
|
188
|
+
'Error message from mocked process.exit should be caught'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
await Promise.resolve();
|
|
192
|
+
|
|
193
|
+
const expectedOutput = [
|
|
194
|
+
'Checking for Google Cloud Application Default Credentials...',
|
|
195
|
+
'ERROR: Google Cloud Application Default Credentials are not set up.',
|
|
196
|
+
`An HTTP error occurred (Status: 401). This often means misconfigured, expired credentials, or a network issue.`,
|
|
197
|
+
'\nFor more details or alternative setup methods, consider:',
|
|
198
|
+
'1. If running locally, run: gcloud auth application-default login.',
|
|
199
|
+
'2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
|
|
200
|
+
'3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
|
|
201
|
+
`\nOriginal error message from Google Auth Library: ${httpErrorMessage}`,
|
|
202
|
+
`Error stack: ${errorWithStack.stack}`,
|
|
203
|
+
];
|
|
204
|
+
assert.deepStrictEqual(
|
|
205
|
+
capturedConsoleOutput,
|
|
206
|
+
expectedOutput,
|
|
207
|
+
'Console output should match HTTP error messages'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.strictEqual(
|
|
211
|
+
processExitMockFn.mock.callCount(),
|
|
212
|
+
1,
|
|
213
|
+
'process.exit should be called once'
|
|
214
|
+
);
|
|
215
|
+
assert.strictEqual(
|
|
216
|
+
processExitMockFn.mock.calls[0].arguments[0],
|
|
217
|
+
1,
|
|
218
|
+
'process.exit should be called with code 1'
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should log TypeError and exit when google-auth-library throws a TypeError', async () => {
|
|
223
|
+
const typeErrorMessage = 'Unexpected token in JSON at position 0';
|
|
224
|
+
const typeError = new TypeError(typeErrorMessage);
|
|
225
|
+
const errorWithStack = typeError;
|
|
226
|
+
|
|
227
|
+
getClientMock.mock.mockImplementation(async () => {
|
|
228
|
+
throw typeError;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await ensureGCPCredentials();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
assert.ok(
|
|
235
|
+
err.message.includes('Mocked process.exit called with code 1'),
|
|
236
|
+
'Error message from mocked process.exit should be caught'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
await Promise.resolve();
|
|
240
|
+
|
|
241
|
+
const expectedOutput = [
|
|
242
|
+
'Checking for Google Cloud Application Default Credentials...',
|
|
243
|
+
'ERROR: Google Cloud Application Default Credentials are not set up.',
|
|
244
|
+
'An unexpected error occurred during credential verification (e.g., malformed response or invalid type).',
|
|
245
|
+
'\nFor more details or alternative setup methods, consider:',
|
|
246
|
+
'1. If running locally, run: gcloud auth application-default login.',
|
|
247
|
+
'2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
|
|
248
|
+
'3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
|
|
249
|
+
`\nOriginal error message from Google Auth Library: ${typeErrorMessage}`,
|
|
250
|
+
`Error stack: ${errorWithStack.stack}`,
|
|
251
|
+
];
|
|
252
|
+
assert.deepStrictEqual(
|
|
253
|
+
capturedConsoleOutput,
|
|
254
|
+
expectedOutput,
|
|
255
|
+
'Console output should match TypeError messages'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
assert.strictEqual(
|
|
259
|
+
processExitMockFn.mock.callCount(),
|
|
260
|
+
1,
|
|
261
|
+
'process.exit should be called once'
|
|
262
|
+
);
|
|
263
|
+
assert.strictEqual(
|
|
264
|
+
processExitMockFn.mock.calls[0].arguments[0],
|
|
265
|
+
1,
|
|
266
|
+
'process.exit should be called with code 1'
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should log general unexpected error and exit for other errors', async () => {
|
|
271
|
+
const unknownErrorMessage = 'Something unexpected happened.';
|
|
272
|
+
const unknownError = new Error(unknownErrorMessage);
|
|
273
|
+
const errorWithStack = unknownError;
|
|
274
|
+
|
|
275
|
+
getClientMock.mock.mockImplementation(async () => {
|
|
276
|
+
throw unknownError;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await ensureGCPCredentials();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
assert.ok(
|
|
283
|
+
err.message.includes('Mocked process.exit called with code 1'),
|
|
284
|
+
'Error message from mocked process.exit should be caught'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
await Promise.resolve();
|
|
288
|
+
|
|
289
|
+
const expectedOutput = [
|
|
290
|
+
'Checking for Google Cloud Application Default Credentials...',
|
|
291
|
+
'ERROR: Google Cloud Application Default Credentials are not set up.',
|
|
292
|
+
'An unexpected error occurred during credential verification.',
|
|
293
|
+
'\nFor more details or alternative setup methods, consider:',
|
|
294
|
+
'1. If running locally, run: gcloud auth application-default login.',
|
|
295
|
+
'2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
|
|
296
|
+
'3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
|
|
297
|
+
`\nOriginal error message from Google Auth Library: ${unknownErrorMessage}`,
|
|
298
|
+
`Error stack: ${errorWithStack.stack}`,
|
|
299
|
+
];
|
|
300
|
+
assert.deepStrictEqual(
|
|
301
|
+
capturedConsoleOutput,
|
|
302
|
+
expectedOutput,
|
|
303
|
+
'Console output should match general error messages'
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
assert.strictEqual(
|
|
307
|
+
processExitMockFn.mock.callCount(),
|
|
308
|
+
1,
|
|
309
|
+
'process.exit should be called once'
|
|
310
|
+
);
|
|
311
|
+
assert.strictEqual(
|
|
312
|
+
processExitMockFn.mock.calls[0].arguments[0],
|
|
313
|
+
1,
|
|
314
|
+
'process.exit should be called with code 1'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { test, describe, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
|
|
6
|
+
describe('MCP Server in stdio mode', () => {
|
|
7
|
+
let client;
|
|
8
|
+
let transport;
|
|
9
|
+
|
|
10
|
+
before(async () => {
|
|
11
|
+
transport = new StdioClientTransport({
|
|
12
|
+
command: 'node',
|
|
13
|
+
args: ['mcp-server.js'],
|
|
14
|
+
});
|
|
15
|
+
client = new Client({
|
|
16
|
+
name: 'test-client',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
});
|
|
19
|
+
await client.connect(transport);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
after(() => {
|
|
23
|
+
client.close();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should list tools', async () => {
|
|
27
|
+
const response = await client.listTools();
|
|
28
|
+
|
|
29
|
+
const tools = response.tools;
|
|
30
|
+
assert(Array.isArray(tools));
|
|
31
|
+
const toolNames = tools.map((t) => t.name);
|
|
32
|
+
assert.deepStrictEqual(
|
|
33
|
+
toolNames.sort(),
|
|
34
|
+
[
|
|
35
|
+
'create_project',
|
|
36
|
+
'deploy_container_image',
|
|
37
|
+
'deploy_file_contents',
|
|
38
|
+
'deploy_local_files',
|
|
39
|
+
'deploy_local_folder',
|
|
40
|
+
'get_service',
|
|
41
|
+
'get_service_log',
|
|
42
|
+
'list_projects',
|
|
43
|
+
'list_services',
|
|
44
|
+
].sort()
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should list prompts', async () => {
|
|
49
|
+
const response = await client.listPrompts();
|
|
50
|
+
|
|
51
|
+
const prompts = response.prompts;
|
|
52
|
+
assert(Array.isArray(prompts));
|
|
53
|
+
const promptNames = prompts.map((p) => p.name);
|
|
54
|
+
assert.deepStrictEqual(promptNames.sort(), ['deploy', 'logs'].sort());
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
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 assert from 'node:assert/strict';
|
|
18
|
+
import { describe, it, mock } from 'node:test';
|
|
19
|
+
import { registerPrompts } from '../prompts.js';
|
|
20
|
+
|
|
21
|
+
describe('registerPrompts', () => {
|
|
22
|
+
it('should register deploy and logs prompts', () => {
|
|
23
|
+
const server = {
|
|
24
|
+
registerPrompt: mock.fn(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
registerPrompts(server);
|
|
28
|
+
|
|
29
|
+
assert.strictEqual(server.registerPrompt.mock.callCount(), 2);
|
|
30
|
+
assert.strictEqual(
|
|
31
|
+
server.registerPrompt.mock.calls[0].arguments[0],
|
|
32
|
+
'deploy'
|
|
33
|
+
);
|
|
34
|
+
assert.strictEqual(
|
|
35
|
+
server.registerPrompt.mock.calls[1].arguments[0],
|
|
36
|
+
'logs'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('deploy prompt', () => {
|
|
41
|
+
it('should use the provided name', async () => {
|
|
42
|
+
const server = {
|
|
43
|
+
registerPrompt: mock.fn(),
|
|
44
|
+
};
|
|
45
|
+
registerPrompts(server);
|
|
46
|
+
const handler = server.registerPrompt.mock.calls[0].arguments[2];
|
|
47
|
+
const result = await handler({ name: 'my-service' });
|
|
48
|
+
assert.deepStrictEqual(result, {
|
|
49
|
+
messages: [
|
|
50
|
+
{
|
|
51
|
+
role: 'user',
|
|
52
|
+
content: {
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: `Use the deploy_local_folder tool to deploy the current folder. The service name should be my-service`,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should use the current directory name', async () => {
|
|
62
|
+
const server = {
|
|
63
|
+
registerPrompt: mock.fn(),
|
|
64
|
+
};
|
|
65
|
+
registerPrompts(server);
|
|
66
|
+
const handler = server.registerPrompt.mock.calls[0].arguments[2];
|
|
67
|
+
const result = await handler({});
|
|
68
|
+
const serviceName =
|
|
69
|
+
'a name for the application based on the current working directory.';
|
|
70
|
+
assert.deepStrictEqual(result, {
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
role: 'user',
|
|
74
|
+
content: {
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `Use the deploy_local_folder tool to deploy the current folder. The service name should be ${serviceName}`,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should use the provided project and region', async () => {
|
|
84
|
+
const server = {
|
|
85
|
+
registerPrompt: mock.fn(),
|
|
86
|
+
};
|
|
87
|
+
registerPrompts(server);
|
|
88
|
+
const handler = server.registerPrompt.mock.calls[0].arguments[2];
|
|
89
|
+
const result = await handler({
|
|
90
|
+
name: 'my-service',
|
|
91
|
+
project: 'my-project',
|
|
92
|
+
region: 'my-region',
|
|
93
|
+
});
|
|
94
|
+
assert.deepStrictEqual(result, {
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
role: 'user',
|
|
98
|
+
content: {
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: `Use the deploy_local_folder tool to deploy the current folder in project my-project in region my-region. The service name should be my-service`,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('logs prompt', () => {
|
|
109
|
+
it('should use the provided service name', async () => {
|
|
110
|
+
const server = {
|
|
111
|
+
registerPrompt: mock.fn(),
|
|
112
|
+
};
|
|
113
|
+
registerPrompts(server);
|
|
114
|
+
const handler = server.registerPrompt.mock.calls[1].arguments[2];
|
|
115
|
+
const result = await handler({ service: 'my-service' });
|
|
116
|
+
assert.deepStrictEqual(result, {
|
|
117
|
+
messages: [
|
|
118
|
+
{
|
|
119
|
+
role: 'user',
|
|
120
|
+
content: {
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: `Use get_service_log to get logs for the service my-service`,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should use the current directory name for the service name', async () => {
|
|
130
|
+
const server = {
|
|
131
|
+
registerPrompt: mock.fn(),
|
|
132
|
+
};
|
|
133
|
+
registerPrompts(server);
|
|
134
|
+
const handler = server.registerPrompt.mock.calls[1].arguments[2];
|
|
135
|
+
const result = await handler({});
|
|
136
|
+
const serviceName = 'named for the current working directory';
|
|
137
|
+
assert.deepStrictEqual(result, {
|
|
138
|
+
messages: [
|
|
139
|
+
{
|
|
140
|
+
role: 'user',
|
|
141
|
+
content: {
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Use get_service_log to get logs for the service ${serviceName}`,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should use the provided project and region', async () => {
|
|
151
|
+
const server = {
|
|
152
|
+
registerPrompt: mock.fn(),
|
|
153
|
+
};
|
|
154
|
+
registerPrompts(server);
|
|
155
|
+
const handler = server.registerPrompt.mock.calls[1].arguments[2];
|
|
156
|
+
const result = await handler({
|
|
157
|
+
service: 'my-service',
|
|
158
|
+
project: 'my-project',
|
|
159
|
+
region: 'my-region',
|
|
160
|
+
});
|
|
161
|
+
assert.deepStrictEqual(result, {
|
|
162
|
+
messages: [
|
|
163
|
+
{
|
|
164
|
+
role: 'user',
|
|
165
|
+
content: {
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: `Use get_service_log to get logs in project my-project in region my-region for the service my-service`,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
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 readline from 'readline/promises';
|
|
18
|
+
import { stdin as input, stdout as output } from 'process';
|
|
19
|
+
import { getServiceLogs } from '../lib/cloud-run-services.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prompts the user for required service information
|
|
23
|
+
* @returns {Promise<{projectId: string, region: string, serviceId: string}>} The service details
|
|
24
|
+
*/
|
|
25
|
+
async function getServiceDetails() {
|
|
26
|
+
const rl = readline.createInterface({ input, output });
|
|
27
|
+
|
|
28
|
+
// Check command line arguments first
|
|
29
|
+
const [, , projectId, region = 'europe-west1', serviceId] = process.argv;
|
|
30
|
+
|
|
31
|
+
if (projectId && serviceId) {
|
|
32
|
+
console.log(`Using command line arguments:
|
|
33
|
+
Project ID: ${projectId}
|
|
34
|
+
Region: ${region}
|
|
35
|
+
Service ID: ${serviceId}`);
|
|
36
|
+
rl.close();
|
|
37
|
+
return { projectId, region, serviceId };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If not provided via command line, prompt for the values
|
|
41
|
+
const projectIdInput = await rl.question('Enter the Project ID: ');
|
|
42
|
+
const regionInput = await rl.question(
|
|
43
|
+
'Enter the region (press Enter for europe-west1): '
|
|
44
|
+
);
|
|
45
|
+
const serviceIdInput = await rl.question('Enter the Service ID: ');
|
|
46
|
+
|
|
47
|
+
rl.close();
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
projectId: projectIdInput.trim(),
|
|
51
|
+
region: regionInput.trim() || 'europe-west1',
|
|
52
|
+
serviceId: serviceIdInput.trim(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
try {
|
|
58
|
+
const { projectId, region, serviceId } = await getServiceDetails();
|
|
59
|
+
|
|
60
|
+
if (!projectId || !serviceId) {
|
|
61
|
+
console.error('Both Project ID and Service ID are required.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
`\nFetching logs for service "${serviceId}" in project "${projectId}" (region: ${region})...`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
let requestOptions;
|
|
70
|
+
let pageCount = 0;
|
|
71
|
+
const MAX_PAGES = 3; // Limit the number of pages to avoid too much output
|
|
72
|
+
|
|
73
|
+
do {
|
|
74
|
+
const result = await getServiceLogs(
|
|
75
|
+
projectId,
|
|
76
|
+
region,
|
|
77
|
+
serviceId,
|
|
78
|
+
requestOptions
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (result.logs) {
|
|
82
|
+
console.log('\nLog entries:');
|
|
83
|
+
console.log(result.logs);
|
|
84
|
+
} else {
|
|
85
|
+
console.log('No logs found for this service.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
requestOptions = result.requestOptions;
|
|
89
|
+
pageCount++;
|
|
90
|
+
|
|
91
|
+
if (requestOptions && pageCount < MAX_PAGES) {
|
|
92
|
+
const rl = readline.createInterface({ input, output });
|
|
93
|
+
const answer = await rl.question('\nFetch more logs? (y/N): ');
|
|
94
|
+
rl.close();
|
|
95
|
+
|
|
96
|
+
if (answer.toLowerCase() !== 'y') {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} while (requestOptions && pageCount < MAX_PAGES);
|
|
101
|
+
|
|
102
|
+
console.log('\nService logs test completed successfully.');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error during service logs test:', error.message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|