@google-cloud/cloud-run-mcp 1.3.0 → 1.5.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/.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/local-tests.yml +1 -1
- package/CONTRIBUTING.md +6 -0
- package/README.md +19 -14
- package/gemini-extension/GEMINI.md +2 -2
- package/lib/cloud-run-deploy.js +78 -58
- package/mcp-server.js +8 -1
- package/package.json +3 -3
- package/test/local/mcp-server.test.js +0 -1
- package/test/local/notifications.test.js +45 -0
- package/test/local/tools.test.js +40 -65
- package/test/need-gcp/cloud-run-deploy.test.js +101 -135
- package/test/need-gcp/cloud-run-services.test.js +57 -77
- package/test/need-gcp/gcp-projects.test.js +14 -56
- package/{tools.js → tools/register-tools.js} +95 -461
- package/tools/tools.js +55 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/CONTRIBUTING.md
CHANGED
|
@@ -25,6 +25,12 @@ This project follows
|
|
|
25
25
|
|
|
26
26
|
## Contribution process
|
|
27
27
|
|
|
28
|
+
### Start with an issue
|
|
29
|
+
|
|
30
|
+
Before sending a pull request, please open an issue describing the bug or feature
|
|
31
|
+
you would like to address. This will allow maintainers of this project to guide
|
|
32
|
+
you in your design and implementation.
|
|
33
|
+
|
|
28
34
|
### Code reviews
|
|
29
35
|
|
|
30
36
|
All submissions, including submissions by project members, require review. We
|
package/README.md
CHANGED
|
@@ -11,13 +11,17 @@ Enable MCP-compatible AI agents to deploy apps to Cloud Run.
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
Deploy from Gemini CLI and other AI-powered CLI agents:
|
|
15
|
+
|
|
16
|
+
<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploycli.gif" width="800">
|
|
17
|
+
|
|
14
18
|
Deploy from AI-powered IDEs:
|
|
15
19
|
|
|
16
|
-
<img src="https://
|
|
20
|
+
<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_ide.gif" width="800">
|
|
17
21
|
|
|
18
22
|
Deploy from AI assistant apps:
|
|
19
23
|
|
|
20
|
-
<img src="https://
|
|
24
|
+
<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_apps.gif" width="800">
|
|
21
25
|
|
|
22
26
|
Deploy from agent SDKs, like the [Google Gen AI SDK](https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#use_model_context_protocol_mcp) or [Agent Development Kit](https://google.github.io/adk-docs/tools/mcp-tools/).
|
|
23
27
|
|
|
@@ -30,7 +34,7 @@ Deploy from agent SDKs, like the [Google Gen AI SDK](https://ai.google.dev/gemin
|
|
|
30
34
|
- `list-services`: Lists Cloud Run services in a given project and region.
|
|
31
35
|
- `get-service`: Gets details for a specific Cloud Run service.
|
|
32
36
|
- `get-service-log`: Gets Logs and Error Messages for a specific Cloud Run service.
|
|
33
|
-
|
|
37
|
+
|
|
34
38
|
- `deploy-local-folder`\*: Deploys a local folder to a Google Cloud Run service.
|
|
35
39
|
- `list-projects`\*: Lists available GCP projects.
|
|
36
40
|
- `create-project`\*: Creates a new GCP project and attach it to the first available billing account. A project ID can be optionally specified.
|
|
@@ -44,6 +48,18 @@ Prompts are natural language commands that can be used to perform common tasks.
|
|
|
44
48
|
- `deploy`: Deploys the current working directory to Cloud Run. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.
|
|
45
49
|
- `logs`: Gets the logs for a Cloud Run service. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.
|
|
46
50
|
|
|
51
|
+
|
|
52
|
+
## Use as a Gemini CLI extension
|
|
53
|
+
|
|
54
|
+
To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
|
|
58
|
+
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
|
|
59
|
+
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/GEMINI.md > ~/.gemini/extensions/cloud-run/gemini-extension/GEMINI.md
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
47
63
|
## Use as local MCP server
|
|
48
64
|
|
|
49
65
|
Run the Cloud Run MCP server on your local machine using local Google Cloud credentials. This is best if you are using an AI-assisted IDE (e.g. Cursor) or a desktop AI application (e.g. Claude).
|
|
@@ -118,19 +134,8 @@ See Docker's [MCP catalog](https://hub.docker.com/mcp/server/cloud-run-mcp/overv
|
|
|
118
134
|
}
|
|
119
135
|
```
|
|
120
136
|
|
|
121
|
-
## Use as a Gemini CLI extension
|
|
122
|
-
|
|
123
|
-
To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
|
|
127
|
-
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
|
|
128
|
-
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/Gemini.md > ~/.gemini/extensions/cloud-run/gemini-extension/Gemini.md
|
|
129
|
-
```
|
|
130
|
-
|
|
131
137
|
## Use as remote MCP server
|
|
132
138
|
|
|
133
|
-
|
|
134
139
|
> [!WARNING]
|
|
135
140
|
> Do not use the remote MCP server without authentication. In the following instructions, we will use IAM authentication to secure the connection to the MCP server from your local machine. This is important to prevent unauthorized access to your Google Cloud resources.
|
|
136
141
|
|
|
@@ -47,11 +47,11 @@ server {
|
|
|
47
47
|
The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.
|
|
48
48
|
|
|
49
49
|
If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
|
|
50
|
-
The user
|
|
50
|
+
The user must follow these steps:
|
|
51
51
|
|
|
52
52
|
1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.
|
|
53
53
|
|
|
54
54
|
2. Set up application credentials using the command:
|
|
55
55
|
```bash
|
|
56
56
|
gcloud auth application-default login
|
|
57
|
-
```
|
|
57
|
+
```
|
package/lib/cloud-run-deploy.js
CHANGED
|
@@ -48,7 +48,7 @@ let runClient;
|
|
|
48
48
|
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
49
49
|
* @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
|
|
50
50
|
*/
|
|
51
|
-
function logAndProgress(message, progressCallback, severity = 'info') {
|
|
51
|
+
async function logAndProgress(message, progressCallback, severity = 'info') {
|
|
52
52
|
switch (severity) {
|
|
53
53
|
case 'error':
|
|
54
54
|
console.error(message);
|
|
@@ -89,14 +89,14 @@ async function checkCloudRunServiceExists(
|
|
|
89
89
|
() => runClient.getService({ name: servicePath }),
|
|
90
90
|
`getService ${serviceId}`
|
|
91
91
|
);
|
|
92
|
-
logAndProgress(
|
|
92
|
+
await logAndProgress(
|
|
93
93
|
`Cloud Run service ${serviceId} already exists.`,
|
|
94
94
|
progressCallback
|
|
95
95
|
);
|
|
96
96
|
return true;
|
|
97
97
|
} catch (error) {
|
|
98
98
|
if (error.code === 5) {
|
|
99
|
-
logAndProgress(
|
|
99
|
+
await logAndProgress(
|
|
100
100
|
`Cloud Run service ${serviceId} does not exist.`,
|
|
101
101
|
progressCallback
|
|
102
102
|
);
|
|
@@ -104,7 +104,7 @@ async function checkCloudRunServiceExists(
|
|
|
104
104
|
}
|
|
105
105
|
const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
|
|
106
106
|
console.error(`Error checking Cloud Run service ${serviceId}:`, error);
|
|
107
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
107
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
108
108
|
throw error;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -160,7 +160,7 @@ async function deployToCloudRun(
|
|
|
160
160
|
|
|
161
161
|
// Always perform a dry run first for general validation
|
|
162
162
|
try {
|
|
163
|
-
logAndProgress(
|
|
163
|
+
await logAndProgress(
|
|
164
164
|
`Performing dry run for service ${serviceId}...`,
|
|
165
165
|
progressCallback,
|
|
166
166
|
'debug'
|
|
@@ -189,13 +189,13 @@ async function deployToCloudRun(
|
|
|
189
189
|
`createService (dry run) ${serviceId}`
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
|
-
logAndProgress(
|
|
192
|
+
await logAndProgress(
|
|
193
193
|
`Dry run successful for ${serviceId} with current configuration.`,
|
|
194
194
|
progressCallback,
|
|
195
195
|
'debug'
|
|
196
196
|
);
|
|
197
197
|
} catch (dryRunError) {
|
|
198
|
-
logAndProgress(
|
|
198
|
+
await logAndProgress(
|
|
199
199
|
`Dry run for ${serviceId} failed: ${dryRunError.message}`,
|
|
200
200
|
progressCallback,
|
|
201
201
|
'warn'
|
|
@@ -209,7 +209,7 @@ async function deployToCloudRun(
|
|
|
209
209
|
dryRunError.message.toLowerCase().includes('iam policy violation') ||
|
|
210
210
|
dryRunError.code === 3) /* INVALID_ARGUMENT */
|
|
211
211
|
) {
|
|
212
|
-
logAndProgress(
|
|
212
|
+
await logAndProgress(
|
|
213
213
|
`Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
|
|
214
214
|
progressCallback,
|
|
215
215
|
'warn'
|
|
@@ -218,14 +218,14 @@ async function deployToCloudRun(
|
|
|
218
218
|
} else {
|
|
219
219
|
// For any other validation errors, rethrow to stop the deployment
|
|
220
220
|
const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
|
|
221
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
221
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
222
222
|
throw new Error(errorMessage);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
let operation;
|
|
227
227
|
if (exists) {
|
|
228
|
-
logAndProgress(
|
|
228
|
+
await logAndProgress(
|
|
229
229
|
`Updating existing service ${serviceId}...`,
|
|
230
230
|
progressCallback
|
|
231
231
|
);
|
|
@@ -235,7 +235,7 @@ async function deployToCloudRun(
|
|
|
235
235
|
`updateService ${serviceId}`
|
|
236
236
|
);
|
|
237
237
|
} else {
|
|
238
|
-
logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
238
|
+
await logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
239
239
|
[operation] = await callWithRetry(
|
|
240
240
|
() =>
|
|
241
241
|
runClient.createService({
|
|
@@ -247,10 +247,13 @@ async function deployToCloudRun(
|
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
logAndProgress(
|
|
250
|
+
await logAndProgress(
|
|
251
|
+
`Deploying ${serviceId} to Cloud Run...`,
|
|
252
|
+
progressCallback
|
|
253
|
+
);
|
|
251
254
|
const [response] = await operation.promise();
|
|
252
255
|
|
|
253
|
-
logAndProgress(
|
|
256
|
+
await logAndProgress(
|
|
254
257
|
`Service deployed/updated successfully: ${response.uri}`,
|
|
255
258
|
progressCallback
|
|
256
259
|
);
|
|
@@ -258,7 +261,7 @@ async function deployToCloudRun(
|
|
|
258
261
|
} catch (error) {
|
|
259
262
|
const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
|
|
260
263
|
console.error(`Error deploying/updating service ${serviceId}:`, error);
|
|
261
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
264
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
262
265
|
throw error;
|
|
263
266
|
}
|
|
264
267
|
}
|
|
@@ -286,10 +289,13 @@ async function ensureStorageBucketExists(
|
|
|
286
289
|
`storage.bucket.exists ${bucketName}`
|
|
287
290
|
);
|
|
288
291
|
if (exists) {
|
|
289
|
-
logAndProgress(
|
|
292
|
+
await logAndProgress(
|
|
293
|
+
`Bucket ${bucketName} already exists.`,
|
|
294
|
+
progressCallback
|
|
295
|
+
);
|
|
290
296
|
return bucket;
|
|
291
297
|
} else {
|
|
292
|
-
logAndProgress(
|
|
298
|
+
await logAndProgress(
|
|
293
299
|
`Bucket ${bucketName} does not exist. Creating in location ${location}...`,
|
|
294
300
|
progressCallback
|
|
295
301
|
);
|
|
@@ -298,7 +304,7 @@ async function ensureStorageBucketExists(
|
|
|
298
304
|
() => storage.createBucket(bucketName, { location: location }),
|
|
299
305
|
`storage.createBucket ${bucketName}`
|
|
300
306
|
);
|
|
301
|
-
logAndProgress(
|
|
307
|
+
await logAndProgress(
|
|
302
308
|
`Storage bucket ${createdBucket.name} created successfully in ${location}.`,
|
|
303
309
|
progressCallback
|
|
304
310
|
);
|
|
@@ -309,14 +315,14 @@ async function ensureStorageBucketExists(
|
|
|
309
315
|
`Failed to create storage bucket ${bucketName}. Error details:`,
|
|
310
316
|
createError
|
|
311
317
|
);
|
|
312
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
318
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
313
319
|
throw createError;
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
} catch (error) {
|
|
317
323
|
const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
|
|
318
324
|
console.error(`Error checking/creating bucket ${bucketName}:`, error);
|
|
319
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
325
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
320
326
|
throw error;
|
|
321
327
|
}
|
|
322
328
|
}
|
|
@@ -417,7 +423,7 @@ async function uploadToStorageBucket(
|
|
|
417
423
|
progressCallback
|
|
418
424
|
) {
|
|
419
425
|
try {
|
|
420
|
-
logAndProgress(
|
|
426
|
+
await logAndProgress(
|
|
421
427
|
`Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
|
|
422
428
|
progressCallback
|
|
423
429
|
);
|
|
@@ -425,7 +431,7 @@ async function uploadToStorageBucket(
|
|
|
425
431
|
() => bucket.file(destinationBlobName).save(buffer),
|
|
426
432
|
`storage.bucket.file.save ${destinationBlobName}`
|
|
427
433
|
);
|
|
428
|
-
logAndProgress(
|
|
434
|
+
await logAndProgress(
|
|
429
435
|
`File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
|
|
430
436
|
progressCallback
|
|
431
437
|
);
|
|
@@ -433,7 +439,7 @@ async function uploadToStorageBucket(
|
|
|
433
439
|
} catch (error) {
|
|
434
440
|
const errorMessage = `Error uploading buffer: ${error.message}`;
|
|
435
441
|
console.error(`Error uploading buffer:`, error);
|
|
436
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
442
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
437
443
|
throw error;
|
|
438
444
|
}
|
|
439
445
|
}
|
|
@@ -470,14 +476,14 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
470
476
|
() => artifactRegistryClient.getRepository({ name: repoPath }),
|
|
471
477
|
`artifactRegistry.getRepository ${repositoryId}`
|
|
472
478
|
);
|
|
473
|
-
logAndProgress(
|
|
479
|
+
await logAndProgress(
|
|
474
480
|
`Repository ${repositoryId} already exists in ${location}.`,
|
|
475
481
|
progressCallback
|
|
476
482
|
);
|
|
477
483
|
return repository;
|
|
478
484
|
} catch (error) {
|
|
479
485
|
if (error.code === 5) {
|
|
480
|
-
logAndProgress(
|
|
486
|
+
await logAndProgress(
|
|
481
487
|
`Repository ${repositoryId} does not exist in ${location}. Creating...`,
|
|
482
488
|
progressCallback
|
|
483
489
|
);
|
|
@@ -494,12 +500,12 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
494
500
|
}),
|
|
495
501
|
`artifactRegistry.createRepository ${repositoryId}`
|
|
496
502
|
);
|
|
497
|
-
logAndProgress(
|
|
503
|
+
await logAndProgress(
|
|
498
504
|
`Creating Artifact Registry repository ${repositoryId}...`,
|
|
499
505
|
progressCallback
|
|
500
506
|
);
|
|
501
507
|
const [result] = await operation.promise();
|
|
502
|
-
logAndProgress(
|
|
508
|
+
await logAndProgress(
|
|
503
509
|
`Artifact Registry repository ${result.name} created successfully.`,
|
|
504
510
|
progressCallback
|
|
505
511
|
);
|
|
@@ -510,7 +516,7 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
510
516
|
`Error checking/creating repository ${repositoryId}:`,
|
|
511
517
|
error
|
|
512
518
|
);
|
|
513
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
519
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
514
520
|
throw error;
|
|
515
521
|
}
|
|
516
522
|
}
|
|
@@ -580,7 +586,7 @@ async function triggerCloudBuild(
|
|
|
580
586
|
images: [targetImageUrl],
|
|
581
587
|
};
|
|
582
588
|
|
|
583
|
-
logAndProgress(
|
|
589
|
+
await logAndProgress(
|
|
584
590
|
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
585
591
|
progressCallback
|
|
586
592
|
);
|
|
@@ -589,7 +595,7 @@ async function triggerCloudBuild(
|
|
|
589
595
|
'cloudBuild.createBuild'
|
|
590
596
|
);
|
|
591
597
|
|
|
592
|
-
logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
598
|
+
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
593
599
|
const buildId = operation.metadata.build.id;
|
|
594
600
|
let completedBuild;
|
|
595
601
|
while (true) {
|
|
@@ -605,7 +611,7 @@ async function triggerCloudBuild(
|
|
|
605
611
|
completedBuild = getBuildOperation;
|
|
606
612
|
break;
|
|
607
613
|
}
|
|
608
|
-
logAndProgress(
|
|
614
|
+
await logAndProgress(
|
|
609
615
|
`Build status: ${getBuildOperation.status}. Waiting...`,
|
|
610
616
|
progressCallback,
|
|
611
617
|
'debug'
|
|
@@ -614,20 +620,20 @@ async function triggerCloudBuild(
|
|
|
614
620
|
}
|
|
615
621
|
|
|
616
622
|
if (completedBuild.status === 'SUCCESS') {
|
|
617
|
-
logAndProgress(
|
|
623
|
+
await logAndProgress(
|
|
618
624
|
`Cloud Build job ${buildId} completed successfully.`,
|
|
619
625
|
progressCallback
|
|
620
626
|
);
|
|
621
|
-
logAndProgress(
|
|
627
|
+
await logAndProgress(
|
|
622
628
|
`Image built: ${completedBuild.results.images[0].name}`,
|
|
623
629
|
progressCallback
|
|
624
630
|
);
|
|
625
631
|
return completedBuild;
|
|
626
632
|
} else {
|
|
627
633
|
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
628
|
-
logAndProgress(failureMessage, progressCallback, 'error');
|
|
634
|
+
await logAndProgress(failureMessage, progressCallback, 'error');
|
|
629
635
|
const logsMessage = `Build logs: ${completedBuild.logUrl}`;
|
|
630
|
-
logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
636
|
+
await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
631
637
|
|
|
632
638
|
let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
|
|
633
639
|
try {
|
|
@@ -635,7 +641,7 @@ async function triggerCloudBuild(
|
|
|
635
641
|
const loggingClient = new Logging({ projectId });
|
|
636
642
|
|
|
637
643
|
const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
|
|
638
|
-
logAndProgress(
|
|
644
|
+
await logAndProgress(
|
|
639
645
|
`Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
|
|
640
646
|
progressCallback,
|
|
641
647
|
'debug'
|
|
@@ -662,14 +668,14 @@ async function triggerCloudBuild(
|
|
|
662
668
|
const logLines = entries.reverse().map((entry) => entry.data || '');
|
|
663
669
|
if (logLines.length > 0) {
|
|
664
670
|
buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
|
|
665
|
-
logAndProgress(
|
|
671
|
+
await logAndProgress(
|
|
666
672
|
`Successfully fetched snippet of build logs for ${buildId}.`,
|
|
667
673
|
progressCallback,
|
|
668
674
|
'info'
|
|
669
675
|
);
|
|
670
676
|
}
|
|
671
677
|
} else {
|
|
672
|
-
logAndProgress(
|
|
678
|
+
await logAndProgress(
|
|
673
679
|
`No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
|
|
674
680
|
progressCallback,
|
|
675
681
|
'warn'
|
|
@@ -677,7 +683,7 @@ async function triggerCloudBuild(
|
|
|
677
683
|
}
|
|
678
684
|
} catch (logError) {
|
|
679
685
|
console.error(`Error fetching build logs for ${buildId}:`, logError);
|
|
680
|
-
logAndProgress(
|
|
686
|
+
await logAndProgress(
|
|
681
687
|
`Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
|
|
682
688
|
progressCallback,
|
|
683
689
|
'warn'
|
|
@@ -710,14 +716,21 @@ export async function deploy({
|
|
|
710
716
|
if (!projectId) {
|
|
711
717
|
const errorMsg =
|
|
712
718
|
'Error: projectId is required in the configuration object.';
|
|
713
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
714
|
-
|
|
719
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
720
|
+
throw new Error(errorMsg);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!serviceName) {
|
|
724
|
+
const errorMsg =
|
|
725
|
+
'Error: serviceName is required in the configuration object.';
|
|
726
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
727
|
+
throw new Error(errorMsg);
|
|
715
728
|
}
|
|
716
729
|
|
|
717
730
|
if (!files || !Array.isArray(files) || files.length === 0) {
|
|
718
731
|
const errorMsg =
|
|
719
732
|
'Error: files array is required in the configuration object.';
|
|
720
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
733
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
721
734
|
if (typeof process !== 'undefined' && process.exit) {
|
|
722
735
|
process.exit(1);
|
|
723
736
|
} else {
|
|
@@ -750,10 +763,10 @@ export async function deploy({
|
|
|
750
763
|
const bucketName = `${projectId}-source-bucket`;
|
|
751
764
|
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
|
|
752
765
|
|
|
753
|
-
logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
754
|
-
logAndProgress(`Region: ${region}`, progressCallback);
|
|
755
|
-
logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
756
|
-
logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
766
|
+
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
767
|
+
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
768
|
+
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
769
|
+
await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
757
770
|
|
|
758
771
|
let hasDockerfile = false;
|
|
759
772
|
if (
|
|
@@ -786,7 +799,7 @@ export async function deploy({
|
|
|
786
799
|
}
|
|
787
800
|
}
|
|
788
801
|
}
|
|
789
|
-
logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
802
|
+
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
790
803
|
|
|
791
804
|
const bucket = await ensureStorageBucketExists(
|
|
792
805
|
bucketName,
|
|
@@ -801,7 +814,7 @@ export async function deploy({
|
|
|
801
814
|
ZIP_FILE_NAME,
|
|
802
815
|
progressCallback
|
|
803
816
|
);
|
|
804
|
-
logAndProgress('Source code uploaded successfully', progressCallback);
|
|
817
|
+
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
805
818
|
|
|
806
819
|
await ensureArtifactRegistryRepoExists(
|
|
807
820
|
projectId,
|
|
@@ -833,12 +846,12 @@ export async function deploy({
|
|
|
833
846
|
skipIamCheck
|
|
834
847
|
);
|
|
835
848
|
|
|
836
|
-
logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
849
|
+
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
837
850
|
return service;
|
|
838
851
|
} catch (error) {
|
|
839
852
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
840
853
|
console.error(`Deployment Failed`, error);
|
|
841
|
-
logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
854
|
+
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
842
855
|
throw error;
|
|
843
856
|
}
|
|
844
857
|
}
|
|
@@ -866,13 +879,20 @@ export async function deployImage({
|
|
|
866
879
|
if (!projectId) {
|
|
867
880
|
const errorMsg =
|
|
868
881
|
'Error: projectId is required in the configuration object.';
|
|
869
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
870
|
-
|
|
882
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
883
|
+
throw new Error(errorMsg);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (!serviceName) {
|
|
887
|
+
const errorMsg =
|
|
888
|
+
'Error: serviceName is required in the configuration object.';
|
|
889
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
890
|
+
throw new Error(errorMsg);
|
|
871
891
|
}
|
|
872
892
|
|
|
873
893
|
if (!imageUrl) {
|
|
874
894
|
const errorMsg = 'Error: imageUrl is required in the configuration object.';
|
|
875
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
895
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
876
896
|
if (typeof process !== 'undefined' && process.exit) {
|
|
877
897
|
process.exit(1);
|
|
878
898
|
} else {
|
|
@@ -892,10 +912,10 @@ export async function deployImage({
|
|
|
892
912
|
|
|
893
913
|
runClient = new ServicesClient({ projectId });
|
|
894
914
|
|
|
895
|
-
logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
896
|
-
logAndProgress(`Region: ${region}`, progressCallback);
|
|
897
|
-
logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
898
|
-
logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
915
|
+
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
916
|
+
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
917
|
+
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
918
|
+
await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
899
919
|
|
|
900
920
|
const service = await deployToCloudRun(
|
|
901
921
|
projectId,
|
|
@@ -906,12 +926,12 @@ export async function deployImage({
|
|
|
906
926
|
skipIamCheck
|
|
907
927
|
);
|
|
908
928
|
|
|
909
|
-
logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
929
|
+
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
910
930
|
return service;
|
|
911
931
|
} catch (error) {
|
|
912
932
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
913
933
|
console.error(`Deployment Failed`, error);
|
|
914
|
-
logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
934
|
+
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
915
935
|
throw error;
|
|
916
936
|
}
|
|
917
937
|
}
|
package/mcp-server.js
CHANGED
|
@@ -23,7 +23,8 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
23
23
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
24
24
|
// Support stdio, as it is easier to use locally
|
|
25
25
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
|
-
import { registerTools, registerToolsRemote } from './tools.js';
|
|
26
|
+
import { registerTools, registerToolsRemote } from './tools/tools.js';
|
|
27
|
+
import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
27
28
|
import { registerPrompts } from './prompts.js';
|
|
28
29
|
import { checkGCP } from './lib/gcp-metadata.js';
|
|
29
30
|
import { ensureGCPCredentials } from './lib/gcp-auth-check.js';
|
|
@@ -71,6 +72,12 @@ async function getServer() {
|
|
|
71
72
|
{ capabilities: { logging: {} } }
|
|
72
73
|
);
|
|
73
74
|
|
|
75
|
+
// this is no-op handler is required for mcp-inspector to function due to a mismatch between the SDK mcp-inspector
|
|
76
|
+
server.server.setRequestHandler(SetLevelRequestSchema, (request) => {
|
|
77
|
+
console.log(`Log Level: ${request.params.level}`);
|
|
78
|
+
return {};
|
|
79
|
+
});
|
|
80
|
+
|
|
74
81
|
// Get GCP metadata info once
|
|
75
82
|
const gcpInfo = await checkGCP();
|
|
76
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@google-cloud/cloud-run-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cloud Run MCP deployment tool",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
|
|
12
12
|
"test:deploy": "node test/need-gcp/cloud-run-deploy.test.js",
|
|
13
|
-
"test:
|
|
14
|
-
"test:
|
|
13
|
+
"test:projects": "node test/need-gcp/gcp-projects.test.js",
|
|
14
|
+
"test:services": "node test/need-gcp/cloud-run-services.test.js",
|
|
15
15
|
"test:gcp-auth": "node test/need-gcp/gcp-auth-check.test.js",
|
|
16
16
|
"test:prompts": "node --test test/local/prompts.test.js",
|
|
17
17
|
"test:mcp": "node --test test/local/mcp-server.test.js",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it, mock } from 'node:test';
|
|
3
|
+
import esmock from 'esmock';
|
|
4
|
+
|
|
5
|
+
describe('Tool Notifications', () => {
|
|
6
|
+
it('should send notifications during deployment', async () => {
|
|
7
|
+
const server = {
|
|
8
|
+
registerTool: mock.fn(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
12
|
+
'../../lib/cloud-run-deploy.js': {
|
|
13
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
registerTools(server, { gcpCredentialsAvailable: true });
|
|
19
|
+
|
|
20
|
+
const handler = server.registerTool.mock.calls.find(
|
|
21
|
+
(call) => call.arguments[0] === 'deploy_local_folder'
|
|
22
|
+
).arguments[2];
|
|
23
|
+
|
|
24
|
+
const sendNotification = mock.fn();
|
|
25
|
+
|
|
26
|
+
await handler(
|
|
27
|
+
{
|
|
28
|
+
project: 'my-project',
|
|
29
|
+
region: 'my-region',
|
|
30
|
+
service: 'my-service',
|
|
31
|
+
folderPath: '/my/folder',
|
|
32
|
+
},
|
|
33
|
+
{ sendNotification }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(sendNotification.mock.callCount(), 1);
|
|
37
|
+
assert.deepStrictEqual(sendNotification.mock.calls[0].arguments[0], {
|
|
38
|
+
method: 'notifications/message',
|
|
39
|
+
params: {
|
|
40
|
+
level: 'info',
|
|
41
|
+
data: 'Starting deployment of local folder for service my-service in project my-project...',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|