@google-cloud/cloud-run-mcp 1.7.0 → 1.9.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/.env.example +13 -0
- package/.env.gcloud-sdk-oauth +13 -0
- package/README.md +38 -0
- package/constants.js +8 -0
- package/lib/clients.js +292 -0
- package/lib/cloud-api/auth.js +1 -0
- package/lib/cloud-api/billing.js +21 -14
- package/lib/cloud-api/build.js +98 -49
- package/lib/cloud-api/helpers.js +10 -17
- package/lib/cloud-api/projects.js +16 -13
- package/lib/cloud-api/registry.js +9 -5
- package/lib/cloud-api/run.js +20 -38
- package/lib/cloud-api/storage.js +7 -7
- package/lib/deployment/deployer.js +33 -61
- package/lib/deployment/helpers.js +1 -1
- package/lib/middleware/oauth.js +92 -0
- package/lib/util/helpers.js +12 -0
- package/mcp-server.js +51 -7
- package/package.json +3 -3
- package/test/local/clients.test.js +176 -0
- package/test/local/cloud-api/build.test.js +293 -309
- package/test/local/mcp-server-stdio.test.js +0 -1
- package/test/local/mcp-server-streamable-http.test.js +69 -0
- package/test/local/mcp-server.test.js +1 -0
- package/test/local/middleware/oauth.test.js +156 -0
- package/test/local/mock-clients.js +35 -0
- package/test/local/tools.test.js +2 -2
- package/test/need-gcp/test-helpers.js +1 -1
- package/tools/register-tools.js +18 -5
- package/.kokoro/kokoro_build.sh +0 -22
- package/.kokoro/presubmit.cfg +0 -21
- package/.kokoro/run_tests.sh +0 -31
|
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { getRunClient } from '../clients.js';
|
|
17
18
|
import { callWithRetry, ensureApisEnabled } from '../cloud-api/helpers.js';
|
|
18
19
|
import { zipFiles } from '../util/archive.js';
|
|
19
20
|
import {
|
|
@@ -58,7 +59,6 @@ import {
|
|
|
58
59
|
* @throws {Error} If the deployment or update process fails.
|
|
59
60
|
*/
|
|
60
61
|
async function deployToCloudRun(
|
|
61
|
-
context,
|
|
62
62
|
projectId,
|
|
63
63
|
location,
|
|
64
64
|
serviceId,
|
|
@@ -66,15 +66,13 @@ async function deployToCloudRun(
|
|
|
66
66
|
progressCallback,
|
|
67
67
|
skipIamCheck,
|
|
68
68
|
deploymentType,
|
|
69
|
+
accessToken,
|
|
69
70
|
zippedSourceContainer,
|
|
70
71
|
runtime
|
|
71
72
|
) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
location,
|
|
76
|
-
serviceId
|
|
77
|
-
);
|
|
73
|
+
const runClient = await getRunClient(projectId, accessToken);
|
|
74
|
+
const parent = runClient.locationPath(projectId, location);
|
|
75
|
+
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
78
76
|
const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
|
|
79
77
|
|
|
80
78
|
const containers =
|
|
@@ -101,10 +99,10 @@ async function deployToCloudRun(
|
|
|
101
99
|
|
|
102
100
|
try {
|
|
103
101
|
const exists = await checkCloudRunServiceExists(
|
|
104
|
-
context,
|
|
105
102
|
projectId,
|
|
106
103
|
location,
|
|
107
104
|
serviceId,
|
|
105
|
+
accessToken,
|
|
108
106
|
progressCallback
|
|
109
107
|
);
|
|
110
108
|
|
|
@@ -121,7 +119,7 @@ async function deployToCloudRun(
|
|
|
121
119
|
dryRunServiceConfig.name = servicePath;
|
|
122
120
|
await callWithRetry(
|
|
123
121
|
() =>
|
|
124
|
-
|
|
122
|
+
runClient.updateService({
|
|
125
123
|
service: dryRunServiceConfig,
|
|
126
124
|
validateOnly: true,
|
|
127
125
|
}),
|
|
@@ -130,7 +128,7 @@ async function deployToCloudRun(
|
|
|
130
128
|
} else {
|
|
131
129
|
await callWithRetry(
|
|
132
130
|
() =>
|
|
133
|
-
|
|
131
|
+
runClient.createService({
|
|
134
132
|
parent: parent,
|
|
135
133
|
service: dryRunServiceConfig,
|
|
136
134
|
serviceId: serviceId,
|
|
@@ -181,7 +179,7 @@ async function deployToCloudRun(
|
|
|
181
179
|
);
|
|
182
180
|
service.name = servicePath;
|
|
183
181
|
[operation] = await callWithRetry(
|
|
184
|
-
() =>
|
|
182
|
+
() => runClient.updateService({ service }),
|
|
185
183
|
`updateService ${serviceId}`
|
|
186
184
|
);
|
|
187
185
|
} else {
|
|
@@ -191,7 +189,7 @@ async function deployToCloudRun(
|
|
|
191
189
|
);
|
|
192
190
|
[operation] = await callWithRetry(
|
|
193
191
|
() =>
|
|
194
|
-
|
|
192
|
+
runClient.createService({
|
|
195
193
|
parent: parent,
|
|
196
194
|
service: service, // 'service' object might have invokerIamDisabled removed
|
|
197
195
|
serviceId: serviceId,
|
|
@@ -242,7 +240,6 @@ async function deployToCloudRun(
|
|
|
242
240
|
* @throws {Error} If any part of the deployment process fails.
|
|
243
241
|
*/
|
|
244
242
|
async function deployWithZip(
|
|
245
|
-
context,
|
|
246
243
|
projectId,
|
|
247
244
|
region,
|
|
248
245
|
serviceName,
|
|
@@ -250,7 +247,8 @@ async function deployWithZip(
|
|
|
250
247
|
bucketName,
|
|
251
248
|
deploymentAttrs,
|
|
252
249
|
progressCallback,
|
|
253
|
-
skipIamCheck
|
|
250
|
+
skipIamCheck,
|
|
251
|
+
accessToken
|
|
254
252
|
) {
|
|
255
253
|
await logAndProgress(
|
|
256
254
|
`Attempting direct source deployment...`,
|
|
@@ -278,14 +276,14 @@ async function deployWithZip(
|
|
|
278
276
|
}
|
|
279
277
|
|
|
280
278
|
const bucket = await ensureStorageBucketExists(
|
|
281
|
-
|
|
279
|
+
projectId,
|
|
282
280
|
bucketName,
|
|
283
281
|
region,
|
|
282
|
+
accessToken,
|
|
284
283
|
progressCallback
|
|
285
284
|
);
|
|
286
285
|
|
|
287
286
|
await uploadToStorageBucket(
|
|
288
|
-
context,
|
|
289
287
|
bucket,
|
|
290
288
|
archiveBuffer,
|
|
291
289
|
archiveName,
|
|
@@ -300,7 +298,6 @@ async function deployWithZip(
|
|
|
300
298
|
});
|
|
301
299
|
|
|
302
300
|
const service = await deployToCloudRun(
|
|
303
|
-
context,
|
|
304
301
|
projectId,
|
|
305
302
|
region,
|
|
306
303
|
serviceName,
|
|
@@ -308,6 +305,7 @@ async function deployWithZip(
|
|
|
308
305
|
progressCallback,
|
|
309
306
|
skipIamCheck,
|
|
310
307
|
DEPLOYMENT_TYPES.NO_BUILD,
|
|
308
|
+
accessToken,
|
|
311
309
|
container,
|
|
312
310
|
deploymentAttrs.runtime
|
|
313
311
|
);
|
|
@@ -337,7 +335,6 @@ async function deployWithZip(
|
|
|
337
335
|
* @throws {Error} If any part of the build or deployment process fails.
|
|
338
336
|
*/
|
|
339
337
|
async function deployWithBuild(
|
|
340
|
-
context,
|
|
341
338
|
projectId,
|
|
342
339
|
region,
|
|
343
340
|
serviceName,
|
|
@@ -345,14 +342,16 @@ async function deployWithBuild(
|
|
|
345
342
|
hasDockerfile,
|
|
346
343
|
bucketName,
|
|
347
344
|
progressCallback,
|
|
348
|
-
skipIamCheck
|
|
345
|
+
skipIamCheck,
|
|
346
|
+
accessToken
|
|
349
347
|
) {
|
|
350
348
|
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
351
349
|
|
|
352
350
|
const bucket = await ensureStorageBucketExists(
|
|
353
|
-
|
|
351
|
+
projectId,
|
|
354
352
|
bucketName,
|
|
355
353
|
region,
|
|
354
|
+
accessToken,
|
|
356
355
|
progressCallback
|
|
357
356
|
);
|
|
358
357
|
|
|
@@ -360,7 +359,6 @@ async function deployWithBuild(
|
|
|
360
359
|
const archiveName = DEPLOYMENT_CONFIG.ZIP_FILE_NAME;
|
|
361
360
|
|
|
362
361
|
await uploadToStorageBucket(
|
|
363
|
-
context,
|
|
364
362
|
bucket,
|
|
365
363
|
archiveBuffer,
|
|
366
364
|
archiveName,
|
|
@@ -369,8 +367,8 @@ async function deployWithBuild(
|
|
|
369
367
|
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
370
368
|
|
|
371
369
|
await ensureArtifactRegistryRepoExists(
|
|
372
|
-
context,
|
|
373
370
|
projectId,
|
|
371
|
+
accessToken,
|
|
374
372
|
region,
|
|
375
373
|
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
376
374
|
'DOCKER',
|
|
@@ -380,7 +378,6 @@ async function deployWithBuild(
|
|
|
380
378
|
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
381
379
|
|
|
382
380
|
const buildResult = await triggerCloudBuild(
|
|
383
|
-
context,
|
|
384
381
|
projectId,
|
|
385
382
|
region,
|
|
386
383
|
bucketName,
|
|
@@ -388,20 +385,21 @@ async function deployWithBuild(
|
|
|
388
385
|
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
389
386
|
imageUrl,
|
|
390
387
|
hasDockerfile,
|
|
388
|
+
accessToken,
|
|
391
389
|
progressCallback
|
|
392
390
|
);
|
|
393
391
|
|
|
394
392
|
const builtImageUrl = buildResult.results.images[0].name;
|
|
395
393
|
|
|
396
394
|
const service = await deployToCloudRun(
|
|
397
|
-
context,
|
|
398
395
|
projectId,
|
|
399
396
|
region,
|
|
400
397
|
serviceName,
|
|
401
398
|
builtImageUrl,
|
|
402
399
|
progressCallback,
|
|
403
400
|
skipIamCheck,
|
|
404
|
-
DEPLOYMENT_TYPES.WITH_BUILD
|
|
401
|
+
DEPLOYMENT_TYPES.WITH_BUILD,
|
|
402
|
+
accessToken
|
|
405
403
|
);
|
|
406
404
|
|
|
407
405
|
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
@@ -426,6 +424,7 @@ export async function deploy({
|
|
|
426
424
|
files,
|
|
427
425
|
progressCallback,
|
|
428
426
|
skipIamCheck,
|
|
427
|
+
accessToken,
|
|
429
428
|
}) {
|
|
430
429
|
if (!projectId) {
|
|
431
430
|
const errorMsg =
|
|
@@ -452,30 +451,11 @@ export async function deploy({
|
|
|
452
451
|
}
|
|
453
452
|
}
|
|
454
453
|
|
|
455
|
-
const { Storage } = await import('@google-cloud/storage');
|
|
456
|
-
const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
|
|
457
|
-
const { ArtifactRegistryClient } = await import(
|
|
458
|
-
'@google-cloud/artifact-registry'
|
|
459
|
-
);
|
|
460
|
-
const { v2: CloudRunV2Module } = await import('@google-cloud/run');
|
|
461
|
-
const { ServicesClient } = CloudRunV2Module;
|
|
462
|
-
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
463
|
-
const { Logging } = await import('@google-cloud/logging');
|
|
464
|
-
|
|
465
454
|
try {
|
|
466
|
-
const context = {
|
|
467
|
-
storage: new Storage({ projectId }),
|
|
468
|
-
cloudBuildClient: new CloudBuildClient({ projectId }),
|
|
469
|
-
artifactRegistryClient: new ArtifactRegistryClient({ projectId }),
|
|
470
|
-
runClient: new ServicesClient({ projectId }),
|
|
471
|
-
serviceUsageClient: new ServiceUsageClient({ projectId }),
|
|
472
|
-
loggingClient: new Logging({ projectId }),
|
|
473
|
-
};
|
|
474
|
-
|
|
475
455
|
await ensureApisEnabled(
|
|
476
|
-
context,
|
|
477
456
|
projectId,
|
|
478
457
|
REQUIRED_APIS.SOURCE_DEPLOY,
|
|
458
|
+
accessToken,
|
|
479
459
|
progressCallback
|
|
480
460
|
);
|
|
481
461
|
|
|
@@ -495,7 +475,6 @@ export async function deploy({
|
|
|
495
475
|
if (canDeployWithoutBuild({ hasDockerfile, deploymentAttrs })) {
|
|
496
476
|
try {
|
|
497
477
|
return await deployWithZip(
|
|
498
|
-
context,
|
|
499
478
|
projectId,
|
|
500
479
|
region,
|
|
501
480
|
serviceName,
|
|
@@ -503,7 +482,8 @@ export async function deploy({
|
|
|
503
482
|
bucketName,
|
|
504
483
|
deploymentAttrs,
|
|
505
484
|
progressCallback,
|
|
506
|
-
skipIamCheck
|
|
485
|
+
skipIamCheck,
|
|
486
|
+
accessToken
|
|
507
487
|
);
|
|
508
488
|
} catch (error) {
|
|
509
489
|
await logAndProgress(
|
|
@@ -515,7 +495,6 @@ export async function deploy({
|
|
|
515
495
|
}
|
|
516
496
|
|
|
517
497
|
return await deployWithBuild(
|
|
518
|
-
context,
|
|
519
498
|
projectId,
|
|
520
499
|
region,
|
|
521
500
|
serviceName,
|
|
@@ -523,7 +502,8 @@ export async function deploy({
|
|
|
523
502
|
hasDockerfile,
|
|
524
503
|
bucketName,
|
|
525
504
|
progressCallback,
|
|
526
|
-
skipIamCheck
|
|
505
|
+
skipIamCheck,
|
|
506
|
+
accessToken
|
|
527
507
|
);
|
|
528
508
|
} catch (error) {
|
|
529
509
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
@@ -552,6 +532,7 @@ export async function deployImage({
|
|
|
552
532
|
imageUrl,
|
|
553
533
|
progressCallback,
|
|
554
534
|
skipIamCheck,
|
|
535
|
+
accessToken,
|
|
555
536
|
}) {
|
|
556
537
|
if (!projectId) {
|
|
557
538
|
const errorMsg =
|
|
@@ -577,20 +558,11 @@ export async function deployImage({
|
|
|
577
558
|
}
|
|
578
559
|
}
|
|
579
560
|
|
|
580
|
-
const { v2: CloudRunV2Module } = await import('@google-cloud/run');
|
|
581
|
-
const { ServicesClient } = CloudRunV2Module;
|
|
582
|
-
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
583
|
-
|
|
584
561
|
try {
|
|
585
|
-
const context = {
|
|
586
|
-
runClient: new ServicesClient({ projectId }),
|
|
587
|
-
serviceUsageClient: new ServiceUsageClient({ projectId }),
|
|
588
|
-
};
|
|
589
|
-
|
|
590
562
|
await ensureApisEnabled(
|
|
591
|
-
context,
|
|
592
563
|
projectId,
|
|
593
564
|
REQUIRED_APIS.IMAGE_DEPLOY,
|
|
565
|
+
accessToken,
|
|
594
566
|
progressCallback
|
|
595
567
|
);
|
|
596
568
|
|
|
@@ -600,14 +572,14 @@ export async function deployImage({
|
|
|
600
572
|
await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
601
573
|
|
|
602
574
|
const service = await deployToCloudRun(
|
|
603
|
-
context,
|
|
604
575
|
projectId,
|
|
605
576
|
region,
|
|
606
577
|
serviceName,
|
|
607
578
|
imageUrl,
|
|
608
579
|
progressCallback,
|
|
609
580
|
skipIamCheck,
|
|
610
|
-
DEPLOYMENT_TYPES.IMAGE
|
|
581
|
+
DEPLOYMENT_TYPES.IMAGE,
|
|
582
|
+
accessToken
|
|
611
583
|
);
|
|
612
584
|
|
|
613
585
|
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
@@ -170,7 +170,7 @@ export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
|
|
|
170
170
|
* Creates the container spec for a direct source deployment (no Cloud Build).
|
|
171
171
|
* @param {object} params - The parameters for creating the container spec.
|
|
172
172
|
* @param {string} params.bucketName - The GCS bucket name where the source is uploaded.
|
|
173
|
-
* @param {string} params.fileName - The GCS object name (
|
|
173
|
+
* @param {string} params.fileName - The GCS object name (eg., source.tar.gz).
|
|
174
174
|
* @param {{cmd: string[], args: string[], baseImage: string}} params.deploymentAttrs - Deployment attributes including command, args, and the base image to use.
|
|
175
175
|
* @returns {object} The container specification object for the Cloud Run service.
|
|
176
176
|
*/
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getOAuthClient } from '../clients.js';
|
|
2
|
+
import { extractAccessToken } from '../util/helpers.js';
|
|
3
|
+
|
|
4
|
+
const TOOLS_CALL_METHOD = 'tools/call';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifies the validity of an access token and checks the audience.
|
|
8
|
+
* @param {string} accessToken - The access token to verify.
|
|
9
|
+
* @param {string} [audience] - Audience to check against the token's audience.
|
|
10
|
+
* @returns {Promise<object>} - The token info if valid.
|
|
11
|
+
* @throws {Error} - If the token is invalid or the audience does not match.
|
|
12
|
+
*/
|
|
13
|
+
async function verifyAccessToken(accessToken, audience) {
|
|
14
|
+
try {
|
|
15
|
+
const client = await getOAuthClient(accessToken);
|
|
16
|
+
const tokenInfo = await client.getTokenInfo(accessToken);
|
|
17
|
+
|
|
18
|
+
//An expired token will not have audience
|
|
19
|
+
if (audience && tokenInfo.aud !== audience) {
|
|
20
|
+
throw new Error(`Invalid audience: expected ${audience}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('Access token verified successfully.');
|
|
24
|
+
return tokenInfo;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error verifying access token:', error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ensures that a valid OAuth token is present in the request headers
|
|
33
|
+
* and verifies it against the configured audience.
|
|
34
|
+
* @param {object} req - The request object.
|
|
35
|
+
* @param {object} res - The response object.
|
|
36
|
+
* @throws {Error} - If the token is missing or invalid.
|
|
37
|
+
*/
|
|
38
|
+
async function ensureOAuthTokenInEnv(req, res) {
|
|
39
|
+
try {
|
|
40
|
+
const audience = process.env.GOOGLE_OAUTH_AUDIENCE;
|
|
41
|
+
console.log('Verifying token');
|
|
42
|
+
if (req.headers.authorization === undefined) {
|
|
43
|
+
console.log('No authorization header found in request');
|
|
44
|
+
throw new Error('No authorization header');
|
|
45
|
+
}
|
|
46
|
+
console.log('Authorization header found.. Verifying token');
|
|
47
|
+
await verifyAccessToken(
|
|
48
|
+
extractAccessToken(req.headers.authorization),
|
|
49
|
+
audience
|
|
50
|
+
);
|
|
51
|
+
console.log('Token verified');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Authentication failed: ', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Middleware to check for OAuth token if OAuth is enabled.
|
|
60
|
+
* If OAUTH_ENABLED is 'true', it verifies the Authorization header.
|
|
61
|
+
*
|
|
62
|
+
* @param {import('express').Request} req
|
|
63
|
+
* @param {import('express').Response} res
|
|
64
|
+
* @param {import('express').NextFunction} next
|
|
65
|
+
*/
|
|
66
|
+
export const oauthMiddleware = async (req, res, next) => {
|
|
67
|
+
//If OAUTH_ENABLED is not true or the request is not a tools/call, skip the middleware
|
|
68
|
+
if (
|
|
69
|
+
process.env.OAUTH_ENABLED !== 'true' ||
|
|
70
|
+
req.body.method !== TOOLS_CALL_METHOD
|
|
71
|
+
) {
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await ensureOAuthTokenInEnv(req, res);
|
|
77
|
+
next();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('OAuth Middleware Error:', error);
|
|
80
|
+
// ensureOAuthTokenInEnv throws an error if auth fails.
|
|
81
|
+
// We catch it and send a 401 response.
|
|
82
|
+
res.status(401).json({
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
error: {
|
|
85
|
+
code: -32001, // Custom auth error code or standard -32000
|
|
86
|
+
message: 'Authentication failed',
|
|
87
|
+
data: error.message,
|
|
88
|
+
},
|
|
89
|
+
id: null,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
package/lib/util/helpers.js
CHANGED
|
@@ -40,3 +40,15 @@ export async function logAndProgress(
|
|
|
40
40
|
progressCallback({ level: severity, data: message });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the access token from the Authorization header.
|
|
46
|
+
* @param {string} authorizationHeader - The Authorization header string.
|
|
47
|
+
* @returns {string | undefined} - The extracted access token or undefined if not found.
|
|
48
|
+
*/
|
|
49
|
+
export function extractAccessToken(authorizationHeader) {
|
|
50
|
+
if (!authorizationHeader) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return authorizationHeader.split(' ')[1];
|
|
54
|
+
}
|
package/mcp-server.js
CHANGED
|
@@ -28,7 +28,18 @@ import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
28
28
|
import { registerPrompts } from './prompts.js';
|
|
29
29
|
import { checkGCP } from './lib/cloud-api/metadata.js';
|
|
30
30
|
import { ensureGCPCredentials } from './lib/cloud-api/auth.js';
|
|
31
|
-
import '
|
|
31
|
+
import { extractAccessToken } from './lib/util/helpers.js';
|
|
32
|
+
import { oauthMiddleware } from './lib/middleware/oauth.js';
|
|
33
|
+
import { config } from '@dotenvx/dotenvx';
|
|
34
|
+
import {
|
|
35
|
+
SCOPES,
|
|
36
|
+
GCLOUD_AUTH,
|
|
37
|
+
BEARER_METHODS_SUPPORTED,
|
|
38
|
+
RESPONSE_TYPES_SUPPORTED,
|
|
39
|
+
} from './constants.js';
|
|
40
|
+
|
|
41
|
+
//Suppress the warning related to missing .env file in case of non-OAuth mode
|
|
42
|
+
config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
|
|
32
43
|
|
|
33
44
|
const gcpInfo = await checkGCP();
|
|
34
45
|
let gcpCredentialsAvailable = false;
|
|
@@ -63,7 +74,7 @@ const allowedHosts = process.env.ALLOWED_HOSTS
|
|
|
63
74
|
? process.env.ALLOWED_HOSTS.split(',')
|
|
64
75
|
: undefined;
|
|
65
76
|
|
|
66
|
-
async function getServer() {
|
|
77
|
+
async function getServer(accessToken = GCLOUD_AUTH) {
|
|
67
78
|
// Create an MCP server with implementation details
|
|
68
79
|
const server = new McpServer(
|
|
69
80
|
{
|
|
@@ -97,6 +108,7 @@ async function getServer() {
|
|
|
97
108
|
defaultServiceName,
|
|
98
109
|
skipIamCheck,
|
|
99
110
|
gcpCredentialsAvailable,
|
|
111
|
+
accessToken,
|
|
100
112
|
});
|
|
101
113
|
} else {
|
|
102
114
|
console.log(
|
|
@@ -109,6 +121,7 @@ async function getServer() {
|
|
|
109
121
|
defaultServiceName,
|
|
110
122
|
skipIamCheck,
|
|
111
123
|
gcpCredentialsAvailable,
|
|
124
|
+
accessToken,
|
|
112
125
|
});
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -118,7 +131,28 @@ async function getServer() {
|
|
|
118
131
|
return server;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
const getOAuthProtectedResource = (req, res) => {
|
|
135
|
+
res.json({
|
|
136
|
+
resource: process.env.OAUTH_PROTECTED_RESOURCE,
|
|
137
|
+
authorization_servers: [process.env.OAUTH_AUTHORIZATION_SERVER],
|
|
138
|
+
scopes_supported: [SCOPES.OPENID, SCOPES.EMAIL, SCOPES.CLOUD_PLATFORM],
|
|
139
|
+
bearer_methods_supported: [...BEARER_METHODS_SUPPORTED],
|
|
140
|
+
});
|
|
141
|
+
res.status(200).send();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const getOAuthAuthorizationServer = (req, res) => {
|
|
145
|
+
res.json({
|
|
146
|
+
issuer: process.env.OAUTH_PROTECTED_RESOURCE,
|
|
147
|
+
authorization_endpoint: process.env.OAUTH_AUTHORIZATION_ENDPOINT,
|
|
148
|
+
token_endpoint: process.env.OAUTH_TOKEN_ENDPOINT,
|
|
149
|
+
scopes_supported: [SCOPES.OPENID, SCOPES.EMAIL, SCOPES.CLOUD_PLATFORM],
|
|
150
|
+
response_types_supported: [...RESPONSE_TYPES_SUPPORTED],
|
|
151
|
+
});
|
|
152
|
+
res.status(200).send();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// stdio mode
|
|
122
156
|
if (shouldStartStdio()) {
|
|
123
157
|
gcpCredentialsAvailable = await ensureGCPCredentials();
|
|
124
158
|
const stdioTransport = new StdioServerTransport();
|
|
@@ -128,7 +162,8 @@ if (shouldStartStdio()) {
|
|
|
128
162
|
} else {
|
|
129
163
|
// non-stdio mode
|
|
130
164
|
console.log('Stdio transport mode is turned off.');
|
|
131
|
-
gcpCredentialsAvailable =
|
|
165
|
+
gcpCredentialsAvailable =
|
|
166
|
+
process.env.OAUTH_ENABLED === 'true' || (await ensureGCPCredentials());
|
|
132
167
|
|
|
133
168
|
const app = enableHostValidation
|
|
134
169
|
? createMcpExpressApp({ allowedHosts })
|
|
@@ -141,9 +176,17 @@ if (shouldStartStdio()) {
|
|
|
141
176
|
);
|
|
142
177
|
}
|
|
143
178
|
|
|
144
|
-
app.
|
|
179
|
+
app.get('/.well-known/oauth-protected-resource', getOAuthProtectedResource);
|
|
180
|
+
|
|
181
|
+
app.get(
|
|
182
|
+
'/.well-known/oauth-authorization-server',
|
|
183
|
+
getOAuthAuthorizationServer
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
app.post('/mcp', oauthMiddleware, async (req, res) => {
|
|
145
187
|
console.log('/mcp Received:', req.body);
|
|
146
|
-
const
|
|
188
|
+
const accessToken = extractAccessToken(req.headers.authorization);
|
|
189
|
+
const server = await getServer(accessToken);
|
|
147
190
|
try {
|
|
148
191
|
const transport = new StreamableHTTPServerTransport({
|
|
149
192
|
sessionIdGenerator: undefined,
|
|
@@ -204,7 +247,8 @@ if (shouldStartStdio()) {
|
|
|
204
247
|
// Legacy SSE endpoint for older clients
|
|
205
248
|
app.get('/sse', async (req, res) => {
|
|
206
249
|
console.log('/sse Received:', req.body);
|
|
207
|
-
const
|
|
250
|
+
const accessToken = extractAccessToken(req.headers.authorization);
|
|
251
|
+
const server = await getServer(accessToken);
|
|
208
252
|
// Create SSE transport for legacy clients
|
|
209
253
|
const transport = new SSEServerTransport('/messages', res);
|
|
210
254
|
sseTransports[transport.sessionId] = transport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@google-cloud/cloud-run-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cloud Run MCP deployment tool",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@google-cloud/storage": "^7.16.0",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
52
52
|
"archiver": "^7.0.1",
|
|
53
|
-
"
|
|
53
|
+
"@dotenvx/dotenvx": "^1.52.0",
|
|
54
54
|
"express": "^5.1.0",
|
|
55
55
|
"google-proto-files": "^5.0.0",
|
|
56
56
|
"zod": "^3.25.76"
|
|
@@ -58,6 +58,6 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"c8": "^10.1.3",
|
|
60
60
|
"esmock": "^2.7.1",
|
|
61
|
-
"prettier": "3.
|
|
61
|
+
"prettier": "^3.8.0"
|
|
62
62
|
}
|
|
63
63
|
}
|