@google-cloud/cloud-run-mcp 1.0.0 → 1.3.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.
@@ -19,10 +19,9 @@ import { GoogleAuth } from 'google-auth-library';
19
19
  /**
20
20
  * Checks for the presence of properly setup Google Cloud Platform (GCP) authentication
21
21
  * using the official Google Auth Library for Node.js. If GCP auth is not found, it logs an
22
- * error message and exits the process, instructing the user on how to set them up.
23
- * This check is primarily for local/stdio environments where explicit setup is often needed.
22
+ * error message and returns false.
24
23
  * @async
25
- * @returns {Promise<void>} A promise that resolves if GCP auth are found, or rejects and exits the process if not.
24
+ * @returns {Promise<boolean>} A promise that resolves to true if GCP auth are found, and false otherwise.
26
25
  */
27
26
  export async function ensureGCPCredentials() {
28
27
  console.error('Checking for Google Cloud Application Default Credentials...');
@@ -35,6 +34,7 @@ export async function ensureGCPCredentials() {
35
34
  await client.getAccessToken();
36
35
 
37
36
  console.log('Application Default Credentials found.');
37
+ return true;
38
38
  } catch (error) {
39
39
  console.error(
40
40
  'ERROR: Google Cloud Application Default Credentials are not set up.'
@@ -74,6 +74,6 @@ export async function ensureGCPCredentials() {
74
74
  if (error.stack) {
75
75
  console.error('Error stack:', error.stack);
76
76
  }
77
- // process.exit(1);
77
+ return false;
78
78
  }
79
79
  }
@@ -72,7 +72,7 @@ export async function createProject(projectId) {
72
72
  }
73
73
 
74
74
  try {
75
- const projectPayload = { projectId: projectIdToUse, name: projectIdToUse };
75
+ const projectPayload = { projectId: projectIdToUse };
76
76
 
77
77
  console.log(`Attempting to create project with ID: ${projectIdToUse}`);
78
78
 
package/mcp-server.js CHANGED
@@ -30,6 +30,7 @@ import { ensureGCPCredentials } from './lib/gcp-auth-check.js';
30
30
  import 'dotenv/config';
31
31
 
32
32
  const gcpInfo = await checkGCP();
33
+ let gcpCredentialsAvailable = false;
33
34
 
34
35
  /**
35
36
  * Ensure that console.log and console.error are compatible with stdio.
@@ -52,7 +53,6 @@ function shouldStartStdio() {
52
53
 
53
54
  if (shouldStartStdio()) {
54
55
  makeLoggingCompatibleWithStdio();
55
- await ensureGCPCredentials();
56
56
  }
57
57
 
58
58
  // Read default configurations from environment variables
@@ -88,6 +88,7 @@ async function getServer() {
88
88
  defaultRegion: effectiveRegion,
89
89
  defaultServiceName,
90
90
  skipIamCheck,
91
+ gcpCredentialsAvailable,
91
92
  });
92
93
  } else {
93
94
  console.log(
@@ -99,6 +100,7 @@ async function getServer() {
99
100
  defaultRegion: effectiveRegion,
100
101
  defaultServiceName,
101
102
  skipIamCheck,
103
+ gcpCredentialsAvailable,
102
104
  });
103
105
  }
104
106
 
@@ -110,13 +112,14 @@ async function getServer() {
110
112
 
111
113
  // stdio
112
114
  if (shouldStartStdio()) {
115
+ gcpCredentialsAvailable = await ensureGCPCredentials();
113
116
  const stdioTransport = new StdioServerTransport();
114
117
  const server = await getServer();
115
118
  await server.connect(stdioTransport);
116
119
  console.log('Cloud Run MCP server stdio transport connected');
117
120
  } else {
118
121
  console.log('Running on GCP, stdio transport will not be started.');
119
- await ensureGCPCredentials();
122
+ gcpCredentialsAvailable = await ensureGCPCredentials();
120
123
  const app = express();
121
124
  app.use(express.json());
122
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/cloud-run-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Cloud Run MCP deployment tool",
6
6
  "main": "mcp-server.js",
@@ -9,14 +9,14 @@
9
9
  },
10
10
  "scripts": {
11
11
  "deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
12
- "test:deploy": "node test/test-deploy.js",
13
- "test:create-project": "node test/test-create-project.js",
14
- "test:service-logs": "node test/test-service-logs.js",
15
- "test:gcp-auth": "node test/test-gcp-auth.js",
16
- "test:prompts": "node --test test/test-prompts.js",
17
- "test:mcp": "node --test test/test-mcp-server.js",
18
- "test:tools": "node --test test/test-tools.js",
19
- "test": "node test/run-all-tests.js",
12
+ "test:deploy": "node test/need-gcp/cloud-run-deploy.test.js",
13
+ "test:create-project": "node test/need-gcp/gcp-projects.test.js",
14
+ "test:service-logs": "node test/need-gcp/cloud-run-services.test.js",
15
+ "test:gcp-auth": "node test/need-gcp/gcp-auth-check.test.js",
16
+ "test:prompts": "node --test test/local/prompts.test.js",
17
+ "test:mcp": "node --test test/local/mcp-server.test.js",
18
+ "test:tools": "node --test test/local/tools.test.js",
19
+ "test": "node --test test/local/*.test.js",
20
20
  "mcp-inspector": "npx @modelcontextprotocol/inspector node mcp-server.js",
21
21
  "start": "node mcp-server.js",
22
22
  "lint:check": "prettier --check .",
@@ -0,0 +1,35 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const rootDir = path.resolve(__dirname, '..', '..');
10
+ const geminiExtensionJsonPath = path.resolve(rootDir, 'gemini-extension.json');
11
+
12
+ describe('Gemini CLI extension', () => {
13
+ test('gemini-extension.json should be at the root', () => {
14
+ assert.ok(
15
+ fs.existsSync(geminiExtensionJsonPath),
16
+ 'gemini-extension.json not found at the project root'
17
+ );
18
+ });
19
+
20
+ test('contextFileName from gemini-extension.json should exist', () => {
21
+ const geminiExtensionJson = JSON.parse(
22
+ fs.readFileSync(geminiExtensionJsonPath, 'utf-8')
23
+ );
24
+ const contextFileName = geminiExtensionJson.contextFileName;
25
+ assert.ok(
26
+ contextFileName,
27
+ 'contextFileName not found in gemini-extension.json'
28
+ );
29
+ const contextFilePath = path.resolve(rootDir, contextFileName);
30
+ assert.ok(
31
+ fs.existsSync(contextFilePath),
32
+ `context file name '${contextFileName}' not found`
33
+ );
34
+ });
35
+ });
@@ -0,0 +1,36 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
10
+ const mcpServerPath = path.resolve(__dirname, '..', '..', 'mcp-server.js');
11
+
12
+ describe('The repository is properly structured to be executed using npx', () => {
13
+ test('package.json should be at the root', () => {
14
+ assert.ok(
15
+ fs.existsSync(packageJsonPath),
16
+ 'package.json not found at the project root'
17
+ );
18
+ });
19
+
20
+ test('package.json should have a bin attribute for cloud-run-mcp', () => {
21
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
22
+ assert.ok(packageJson.bin, 'bin attribute not found in package.json');
23
+ assert.ok(
24
+ packageJson.bin['cloud-run-mcp'],
25
+ 'cloud-run-mcp not found in bin attribute'
26
+ );
27
+ });
28
+
29
+ test('mcp-server.js should start with #!/usr/bin/env node', () => {
30
+ const mcpServerContent = fs.readFileSync(mcpServerPath, 'utf-8');
31
+ assert.ok(
32
+ mcpServerContent.startsWith('#!/usr/bin/env node'),
33
+ 'mcp-server.js does not start with #!/usr/bin/env node'
34
+ );
35
+ });
36
+ });
@@ -16,7 +16,7 @@ limitations under the License.
16
16
 
17
17
  import assert from 'node:assert/strict';
18
18
  import { describe, it, mock } from 'node:test';
19
- import { registerPrompts } from '../prompts.js';
19
+ import { registerPrompts } from '../../prompts.js';
20
20
 
21
21
  describe('registerPrompts', () => {
22
22
  it('should register deploy and logs prompts', () => {
@@ -8,7 +8,7 @@ describe('registerTools', () => {
8
8
  registerTool: mock.fn(),
9
9
  };
10
10
 
11
- const { registerTools } = await esmock('../tools.js', {});
11
+ const { registerTools } = await esmock('../../tools.js', {});
12
12
 
13
13
  registerTools(server);
14
14
 
@@ -38,14 +38,14 @@ describe('registerTools', () => {
38
38
  registerTool: mock.fn(),
39
39
  };
40
40
 
41
- const { registerTools } = await esmock('../tools.js', {
42
- '../lib/gcp-projects.js': {
41
+ const { registerTools } = await esmock('../../tools.js', {
42
+ '../../lib/gcp-projects.js': {
43
43
  listProjects: () =>
44
44
  Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
45
45
  },
46
46
  });
47
47
 
48
- registerTools(server);
48
+ registerTools(server, { gcpCredentialsAvailable: true });
49
49
 
50
50
  const handler = server.registerTool.mock.calls.find(
51
51
  (call) => call.arguments[0] === 'list_projects'
@@ -69,8 +69,8 @@ describe('registerTools', () => {
69
69
  registerTool: mock.fn(),
70
70
  };
71
71
 
72
- const { registerTools } = await esmock('../tools.js', {
73
- '../lib/gcp-projects.js': {
72
+ const { registerTools } = await esmock('../../tools.js', {
73
+ '../../lib/gcp-projects.js': {
74
74
  createProjectAndAttachBilling: (projectId) =>
75
75
  Promise.resolve({
76
76
  projectId: projectId,
@@ -79,7 +79,7 @@ describe('registerTools', () => {
79
79
  },
80
80
  });
81
81
 
82
- registerTools(server);
82
+ registerTools(server, { gcpCredentialsAvailable: true });
83
83
 
84
84
  const handler = server.registerTool.mock.calls.find(
85
85
  (call) => call.arguments[0] === 'create_project'
@@ -101,8 +101,8 @@ describe('registerTools', () => {
101
101
  registerTool: mock.fn(),
102
102
  };
103
103
 
104
- const { registerTools } = await esmock('../tools.js', {
105
- '../lib/gcp-projects.js': {
104
+ const { registerTools } = await esmock('../../tools.js', {
105
+ '../../lib/gcp-projects.js': {
106
106
  createProjectAndAttachBilling: () =>
107
107
  Promise.resolve({
108
108
  projectId: 'generated-project',
@@ -111,7 +111,7 @@ describe('registerTools', () => {
111
111
  },
112
112
  });
113
113
 
114
- registerTools(server);
114
+ registerTools(server, { gcpCredentialsAvailable: true });
115
115
 
116
116
  const handler = server.registerTool.mock.calls.find(
117
117
  (call) => call.arguments[0] === 'create_project'
@@ -135,8 +135,8 @@ describe('registerTools', () => {
135
135
  registerTool: mock.fn(),
136
136
  };
137
137
 
138
- const { registerTools } = await esmock('../tools.js', {
139
- '../lib/cloud-run-services.js': {
138
+ const { registerTools } = await esmock('../../tools.js', {
139
+ '../../lib/cloud-run-services.js': {
140
140
  listServices: () =>
141
141
  Promise.resolve([
142
142
  { name: 'service1', uri: 'uri1' },
@@ -145,7 +145,7 @@ describe('registerTools', () => {
145
145
  },
146
146
  });
147
147
 
148
- registerTools(server);
148
+ registerTools(server, { gcpCredentialsAvailable: true });
149
149
 
150
150
  const handler = server.registerTool.mock.calls.find(
151
151
  (call) => call.arguments[0] === 'list_services'
@@ -172,8 +172,8 @@ describe('registerTools', () => {
172
172
  registerTool: mock.fn(),
173
173
  };
174
174
 
175
- const { registerTools } = await esmock('../tools.js', {
176
- '../lib/cloud-run-services.js': {
175
+ const { registerTools } = await esmock('../../tools.js', {
176
+ '../../lib/cloud-run-services.js': {
177
177
  getService: () =>
178
178
  Promise.resolve({
179
179
  name: 'my-service',
@@ -183,7 +183,7 @@ describe('registerTools', () => {
183
183
  },
184
184
  });
185
185
 
186
- registerTools(server);
186
+ registerTools(server, { gcpCredentialsAvailable: true });
187
187
 
188
188
  const handler = server.registerTool.mock.calls.find(
189
189
  (call) => call.arguments[0] === 'get_service'
@@ -223,13 +223,13 @@ describe('registerTools', () => {
223
223
  return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
224
224
  };
225
225
 
226
- const { registerTools } = await esmock('../tools.js', {
227
- '../lib/cloud-run-services.js': {
226
+ const { registerTools } = await esmock('../../tools.js', {
227
+ '../../lib/cloud-run-services.js': {
228
228
  getServiceLogs: getServiceLogs,
229
229
  },
230
230
  });
231
231
 
232
- registerTools(server);
232
+ registerTools(server, { gcpCredentialsAvailable: true });
233
233
 
234
234
  const handler = server.registerTool.mock.calls.find(
235
235
  (call) => call.arguments[0] === 'get_service_log'
@@ -257,13 +257,13 @@ describe('registerTools', () => {
257
257
  registerTool: mock.fn(),
258
258
  };
259
259
 
260
- const { registerTools } = await esmock('../tools.js', {
261
- '../lib/cloud-run-deploy.js': {
260
+ const { registerTools } = await esmock('../../tools.js', {
261
+ '../../lib/cloud-run-deploy.js': {
262
262
  deploy: () => Promise.resolve({ uri: 'my-uri' }),
263
263
  },
264
264
  });
265
265
 
266
- registerTools(server);
266
+ registerTools(server, { gcpCredentialsAvailable: true });
267
267
 
268
268
  const handler = server.registerTool.mock.calls.find(
269
269
  (call) => call.arguments[0] === 'deploy_local_files'
@@ -292,13 +292,13 @@ describe('registerTools', () => {
292
292
  registerTool: mock.fn(),
293
293
  };
294
294
 
295
- const { registerTools } = await esmock('../tools.js', {
296
- '../lib/cloud-run-deploy.js': {
295
+ const { registerTools } = await esmock('../../tools.js', {
296
+ '../../lib/cloud-run-deploy.js': {
297
297
  deploy: () => Promise.resolve({ uri: 'my-uri' }),
298
298
  },
299
299
  });
300
300
 
301
- registerTools(server);
301
+ registerTools(server, { gcpCredentialsAvailable: true });
302
302
 
303
303
  const handler = server.registerTool.mock.calls.find(
304
304
  (call) => call.arguments[0] === 'deploy_local_folder'
@@ -327,13 +327,13 @@ describe('registerTools', () => {
327
327
  registerTool: mock.fn(),
328
328
  };
329
329
 
330
- const { registerTools } = await esmock('../tools.js', {
331
- '../lib/cloud-run-deploy.js': {
330
+ const { registerTools } = await esmock('../../tools.js', {
331
+ '../../lib/cloud-run-deploy.js': {
332
332
  deploy: () => Promise.resolve({ uri: 'my-uri' }),
333
333
  },
334
334
  });
335
335
 
336
- registerTools(server);
336
+ registerTools(server, { gcpCredentialsAvailable: true });
337
337
 
338
338
  const handler = server.registerTool.mock.calls.find(
339
339
  (call) => call.arguments[0] === 'deploy_file_contents'
@@ -362,13 +362,13 @@ describe('registerTools', () => {
362
362
  registerTool: mock.fn(),
363
363
  };
364
364
 
365
- const { registerTools } = await esmock('../tools.js', {
366
- '../lib/cloud-run-deploy.js': {
365
+ const { registerTools } = await esmock('../../tools.js', {
366
+ '../../lib/cloud-run-deploy.js': {
367
367
  deployImage: () => Promise.resolve({ uri: 'my-uri' }),
368
368
  },
369
369
  });
370
370
 
371
- registerTools(server);
371
+ registerTools(server, { gcpCredentialsAvailable: true });
372
372
 
373
373
  const handler = server.registerTool.mock.calls.find(
374
374
  (call) => call.arguments[0] === 'deploy_container_image'
@@ -390,4 +390,35 @@ describe('registerTools', () => {
390
390
  });
391
391
  });
392
392
  });
393
+
394
+ describe('when gcp credentials are not available', () => {
395
+ it('should return an error for all tools', async () => {
396
+ const server = {
397
+ registerTool: mock.fn(),
398
+ };
399
+
400
+ const { registerTools } = await esmock('../../tools.js', {});
401
+
402
+ registerTools(server, { gcpCredentialsAvailable: false });
403
+
404
+ const toolNames = server.registerTool.mock.calls.map(
405
+ (call) => call.arguments[0]
406
+ );
407
+
408
+ for (const toolName of toolNames) {
409
+ const handler = server.registerTool.mock.calls.find(
410
+ (call) => call.arguments[0] === toolName
411
+ ).arguments[2];
412
+ const result = await handler({});
413
+ assert.deepStrictEqual(result, {
414
+ content: [
415
+ {
416
+ type: 'text',
417
+ text: 'GCP credentials are not available. Please configure your environment.',
418
+ },
419
+ ],
420
+ });
421
+ }
422
+ });
423
+ });
393
424
  });
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { deploy, deployImage } from '../lib/cloud-run-deploy.js';
17
+ import { deploy, deployImage } from '../../lib/cloud-run-deploy.js';
18
18
  import readline from 'readline/promises';
19
19
  import { stdin as input, stdout as output } from 'process';
20
20
  import fs from 'fs/promises';
@@ -16,7 +16,7 @@ limitations under the License.
16
16
 
17
17
  import readline from 'readline/promises';
18
18
  import { stdin as input, stdout as output } from 'process';
19
- import { getServiceLogs } from '../lib/cloud-run-services.js';
19
+ import { getServiceLogs } from '../../lib/cloud-run-services.js';
20
20
 
21
21
  /**
22
22
  * Prompts the user for required service information