@google-cloud/cloud-run-mcp 1.2.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 +8 -0
- package/README.md +22 -17
- package/gemini-extension/{Gemini.md → GEMINI.md} +2 -2
- package/gemini-extension.json +1 -1
- package/lib/cloud-run-deploy.js +162 -215
- package/lib/cloud-run-services.js +16 -4
- package/lib/gcp-api-helpers.js +143 -0
- package/lib/gcp-projects.js +1 -1
- package/mcp-server.js +8 -1
- package/package.json +3 -3
- package/test/local/gemini-extension.test.js +35 -0
- package/test/local/mcp-server.test.js +0 -1
- package/test/local/notifications.test.js +45 -0
- package/test/local/npx.test.js +36 -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
|
@@ -15,6 +15,7 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import protofiles from 'google-proto-files';
|
|
18
|
+
import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
|
|
18
19
|
|
|
19
20
|
let runClient;
|
|
20
21
|
let loggingClient;
|
|
@@ -32,13 +33,19 @@ export async function listServices(projectId, location) {
|
|
|
32
33
|
const { ServicesClient } = v2;
|
|
33
34
|
runClient = new ServicesClient({ projectId });
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
await ensureApisEnabled(projectId, ['run.googleapis.com']);
|
|
38
|
+
|
|
35
39
|
const parent = runClient.locationPath(projectId, location);
|
|
36
40
|
|
|
37
41
|
try {
|
|
38
42
|
console.log(
|
|
39
43
|
`Listing Cloud Run services in project ${projectId}, location ${location}...`
|
|
40
44
|
);
|
|
41
|
-
const [services] = await
|
|
45
|
+
const [services] = await callWithRetry(
|
|
46
|
+
() => runClient.listServices({ parent }),
|
|
47
|
+
'listServices'
|
|
48
|
+
);
|
|
42
49
|
return services;
|
|
43
50
|
} catch (error) {
|
|
44
51
|
console.error(`Error listing Cloud Run services:`, error);
|
|
@@ -66,7 +73,10 @@ export async function getService(projectId, location, serviceId) {
|
|
|
66
73
|
console.log(
|
|
67
74
|
`Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
|
|
68
75
|
);
|
|
69
|
-
const [service] = await
|
|
76
|
+
const [service] = await callWithRetry(
|
|
77
|
+
() => runClient.getService({ name: servicePath }),
|
|
78
|
+
'getService'
|
|
79
|
+
);
|
|
70
80
|
return service;
|
|
71
81
|
} catch (error) {
|
|
72
82
|
console.error(
|
|
@@ -123,8 +133,10 @@ export async function getServiceLogs(
|
|
|
123
133
|
console.log(`Request options: ${JSON.stringify(options)}`);
|
|
124
134
|
|
|
125
135
|
// getEntries returns the entries and the full API response
|
|
126
|
-
const [entries, nextRequestOptions, apiResponse] =
|
|
127
|
-
|
|
136
|
+
const [entries, nextRequestOptions, apiResponse] = await callWithRetry(
|
|
137
|
+
() => loggingClient.getEntries(options),
|
|
138
|
+
'getEntries'
|
|
139
|
+
);
|
|
128
140
|
|
|
129
141
|
const formattedLogLines = entries
|
|
130
142
|
.map((entry) => formatLogEntry(entry))
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Calls a function with retry logic for GCP API calls.
|
|
19
|
+
* Retries on gRPC error code 7 (PERMISSION_DENIED).
|
|
20
|
+
* @param {Function} fn The function to call.
|
|
21
|
+
* @param {string} description A description of the function being called, for logging.
|
|
22
|
+
* @returns {Promise<any>} The result of the function.
|
|
23
|
+
*/
|
|
24
|
+
export async function callWithRetry(fn, description) {
|
|
25
|
+
const maxRetries = 7;
|
|
26
|
+
const initialBackoff = 1000; // 1 second
|
|
27
|
+
let retries = 0;
|
|
28
|
+
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 7 && retries < maxRetries) {
|
|
34
|
+
retries++;
|
|
35
|
+
let backoff;
|
|
36
|
+
if (retries === 1) {
|
|
37
|
+
backoff = 15000; // 15 seconds for the first retry
|
|
38
|
+
} else {
|
|
39
|
+
backoff = initialBackoff * Math.pow(2, retries - 2);
|
|
40
|
+
}
|
|
41
|
+
console.warn(
|
|
42
|
+
`API call "${description}" failed with PERMISSION_DENIED. Retrying in ${
|
|
43
|
+
backoff / 1000
|
|
44
|
+
}s... (attempt ${retries}/${maxRetries})`
|
|
45
|
+
);
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
47
|
+
} else {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a single Google Cloud API is enabled and enables it if not.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} serviceUsageClient - The Service Usage client.
|
|
58
|
+
* @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
|
|
59
|
+
* @param {string} api - The API identifier (e.g., 'run.googleapis.com').
|
|
60
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
61
|
+
* @returns {Promise<void>} A promise that resolves when the API is enabled.
|
|
62
|
+
* @throws {Error} If the API fails to enable or if there's an issue checking its status.
|
|
63
|
+
*/
|
|
64
|
+
async function checkAndEnableApi(
|
|
65
|
+
serviceUsageClient,
|
|
66
|
+
serviceName,
|
|
67
|
+
api,
|
|
68
|
+
progressCallback
|
|
69
|
+
) {
|
|
70
|
+
const [service] = await callWithRetry(
|
|
71
|
+
() => serviceUsageClient.getService({ name: serviceName }),
|
|
72
|
+
`getService ${api}`
|
|
73
|
+
);
|
|
74
|
+
if (service.state !== 'ENABLED') {
|
|
75
|
+
const message = `API [${api}] is not enabled. Enabling...`;
|
|
76
|
+
console.log(message);
|
|
77
|
+
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
78
|
+
|
|
79
|
+
const [operation] = await callWithRetry(
|
|
80
|
+
() => serviceUsageClient.enableService({ name: serviceName }),
|
|
81
|
+
`enableService ${api}`
|
|
82
|
+
);
|
|
83
|
+
await operation.promise();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ensures that the specified Google Cloud APIs are enabled for the given project.
|
|
89
|
+
* If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
|
|
90
|
+
* Throws an error if an API cannot be enabled.
|
|
91
|
+
*
|
|
92
|
+
* @async
|
|
93
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
94
|
+
* @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
|
|
95
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
96
|
+
* The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
|
|
97
|
+
* @throws {Error} If an API fails to enable or if there's an issue checking its status.
|
|
98
|
+
* @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
|
|
99
|
+
*/
|
|
100
|
+
export async function ensureApisEnabled(projectId, apis, progressCallback) {
|
|
101
|
+
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
102
|
+
const serviceUsageClient = new ServiceUsageClient({ projectId });
|
|
103
|
+
const message = 'Checking and enabling required APIs...';
|
|
104
|
+
console.log(message);
|
|
105
|
+
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
for (const api of apis) {
|
|
109
|
+
const serviceName = `projects/${projectId}/services/${api}`;
|
|
110
|
+
try {
|
|
111
|
+
await checkAndEnableApi(
|
|
112
|
+
serviceUsageClient,
|
|
113
|
+
serviceName,
|
|
114
|
+
api,
|
|
115
|
+
progressCallback
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// First attempt failed, log a warning and retry once after a delay.
|
|
119
|
+
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
120
|
+
console.warn(warnMsg);
|
|
121
|
+
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
122
|
+
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
124
|
+
try {
|
|
125
|
+
await checkAndEnableApi(
|
|
126
|
+
serviceUsageClient,
|
|
127
|
+
serviceName,
|
|
128
|
+
api,
|
|
129
|
+
progressCallback
|
|
130
|
+
);
|
|
131
|
+
} catch (retryError) {
|
|
132
|
+
// If the retry also fails, throw an error.
|
|
133
|
+
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
134
|
+
console.error(errorMessage, retryError);
|
|
135
|
+
if (progressCallback) progressCallback({ level: 'error', data: errorMessage });
|
|
136
|
+
throw new Error(errorMessage);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const successMsg = 'All required APIs are enabled.';
|
|
141
|
+
console.log(successMsg);
|
|
142
|
+
if (progressCallback) progressCallback({ level: 'info', data: successMsg });
|
|
143
|
+
}
|
package/lib/gcp-projects.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function createProject(projectId) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
|
-
const projectPayload = { projectId: 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
|
@@ -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,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,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
|
+
});
|
|
@@ -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
|
+
});
|
package/test/local/tools.test.js
CHANGED
|
@@ -8,11 +8,11 @@ describe('registerTools', () => {
|
|
|
8
8
|
registerTool: mock.fn(),
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
const { registerTools } = await esmock('../../tools.js', {});
|
|
11
|
+
const { registerTools } = await esmock('../../tools/tools.js', {});
|
|
12
12
|
|
|
13
13
|
registerTools(server);
|
|
14
14
|
|
|
15
|
-
assert.strictEqual(server.registerTool.mock.callCount(),
|
|
15
|
+
assert.strictEqual(server.registerTool.mock.callCount(), 8);
|
|
16
16
|
const toolNames = server.registerTool.mock.calls.map(
|
|
17
17
|
(call) => call.arguments[0]
|
|
18
18
|
);
|
|
@@ -22,7 +22,6 @@ describe('registerTools', () => {
|
|
|
22
22
|
'create_project',
|
|
23
23
|
'deploy_container_image',
|
|
24
24
|
'deploy_file_contents',
|
|
25
|
-
'deploy_local_files',
|
|
26
25
|
'deploy_local_folder',
|
|
27
26
|
'get_service',
|
|
28
27
|
'get_service_log',
|
|
@@ -38,7 +37,7 @@ describe('registerTools', () => {
|
|
|
38
37
|
registerTool: mock.fn(),
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
42
41
|
'../../lib/gcp-projects.js': {
|
|
43
42
|
listProjects: () =>
|
|
44
43
|
Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
|
|
@@ -69,7 +68,7 @@ describe('registerTools', () => {
|
|
|
69
68
|
registerTool: mock.fn(),
|
|
70
69
|
};
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
73
72
|
'../../lib/gcp-projects.js': {
|
|
74
73
|
createProjectAndAttachBilling: (projectId) =>
|
|
75
74
|
Promise.resolve({
|
|
@@ -101,7 +100,7 @@ describe('registerTools', () => {
|
|
|
101
100
|
registerTool: mock.fn(),
|
|
102
101
|
};
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
105
104
|
'../../lib/gcp-projects.js': {
|
|
106
105
|
createProjectAndAttachBilling: () =>
|
|
107
106
|
Promise.resolve({
|
|
@@ -135,7 +134,7 @@ describe('registerTools', () => {
|
|
|
135
134
|
registerTool: mock.fn(),
|
|
136
135
|
};
|
|
137
136
|
|
|
138
|
-
|
|
137
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
139
138
|
'../../lib/cloud-run-services.js': {
|
|
140
139
|
listServices: () =>
|
|
141
140
|
Promise.resolve([
|
|
@@ -172,7 +171,7 @@ describe('registerTools', () => {
|
|
|
172
171
|
registerTool: mock.fn(),
|
|
173
172
|
};
|
|
174
173
|
|
|
175
|
-
|
|
174
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
176
175
|
'../../lib/cloud-run-services.js': {
|
|
177
176
|
getService: () =>
|
|
178
177
|
Promise.resolve({
|
|
@@ -223,7 +222,7 @@ describe('registerTools', () => {
|
|
|
223
222
|
return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
|
|
224
223
|
};
|
|
225
224
|
|
|
226
|
-
|
|
225
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
227
226
|
'../../lib/cloud-run-services.js': {
|
|
228
227
|
getServiceLogs: getServiceLogs,
|
|
229
228
|
},
|
|
@@ -251,40 +250,7 @@ describe('registerTools', () => {
|
|
|
251
250
|
});
|
|
252
251
|
});
|
|
253
252
|
|
|
254
|
-
|
|
255
|
-
it('should deploy local files', async () => {
|
|
256
|
-
const server = {
|
|
257
|
-
registerTool: mock.fn(),
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const { registerTools } = await esmock('../../tools.js', {
|
|
261
|
-
'../../lib/cloud-run-deploy.js': {
|
|
262
|
-
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
registerTools(server, { gcpCredentialsAvailable: true });
|
|
267
|
-
|
|
268
|
-
const handler = server.registerTool.mock.calls.find(
|
|
269
|
-
(call) => call.arguments[0] === 'deploy_local_files'
|
|
270
|
-
).arguments[2];
|
|
271
|
-
const result = await handler({
|
|
272
|
-
project: 'my-project',
|
|
273
|
-
region: 'my-region',
|
|
274
|
-
service: 'my-service',
|
|
275
|
-
files: ['file1', 'file2'],
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
assert.deepStrictEqual(result, {
|
|
279
|
-
content: [
|
|
280
|
-
{
|
|
281
|
-
type: 'text',
|
|
282
|
-
text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
});
|
|
253
|
+
|
|
288
254
|
|
|
289
255
|
describe('deploy_local_folder', () => {
|
|
290
256
|
it('should deploy local folder', async () => {
|
|
@@ -292,7 +258,7 @@ describe('registerTools', () => {
|
|
|
292
258
|
registerTool: mock.fn(),
|
|
293
259
|
};
|
|
294
260
|
|
|
295
|
-
|
|
261
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
296
262
|
'../../lib/cloud-run-deploy.js': {
|
|
297
263
|
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
298
264
|
},
|
|
@@ -303,12 +269,15 @@ describe('registerTools', () => {
|
|
|
303
269
|
const handler = server.registerTool.mock.calls.find(
|
|
304
270
|
(call) => call.arguments[0] === 'deploy_local_folder'
|
|
305
271
|
).arguments[2];
|
|
306
|
-
const result = await handler(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
272
|
+
const result = await handler(
|
|
273
|
+
{
|
|
274
|
+
project: 'my-project',
|
|
275
|
+
region: 'my-region',
|
|
276
|
+
service: 'my-service',
|
|
277
|
+
folderPath: '/my/folder',
|
|
278
|
+
},
|
|
279
|
+
{ sendNotification: mock.fn() }
|
|
280
|
+
);
|
|
312
281
|
|
|
313
282
|
assert.deepStrictEqual(result, {
|
|
314
283
|
content: [
|
|
@@ -327,7 +296,7 @@ describe('registerTools', () => {
|
|
|
327
296
|
registerTool: mock.fn(),
|
|
328
297
|
};
|
|
329
298
|
|
|
330
|
-
|
|
299
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
331
300
|
'../../lib/cloud-run-deploy.js': {
|
|
332
301
|
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
333
302
|
},
|
|
@@ -338,12 +307,15 @@ describe('registerTools', () => {
|
|
|
338
307
|
const handler = server.registerTool.mock.calls.find(
|
|
339
308
|
(call) => call.arguments[0] === 'deploy_file_contents'
|
|
340
309
|
).arguments[2];
|
|
341
|
-
const result = await handler(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
310
|
+
const result = await handler(
|
|
311
|
+
{
|
|
312
|
+
project: 'my-project',
|
|
313
|
+
region: 'my-region',
|
|
314
|
+
service: 'my-service',
|
|
315
|
+
files: [{ filename: 'file1', content: 'content1' }],
|
|
316
|
+
},
|
|
317
|
+
{ sendNotification: mock.fn() }
|
|
318
|
+
);
|
|
347
319
|
|
|
348
320
|
assert.deepStrictEqual(result, {
|
|
349
321
|
content: [
|
|
@@ -362,7 +334,7 @@ describe('registerTools', () => {
|
|
|
362
334
|
registerTool: mock.fn(),
|
|
363
335
|
};
|
|
364
336
|
|
|
365
|
-
|
|
337
|
+
const { registerTools } = await esmock('../../tools/tools.js', {}, {
|
|
366
338
|
'../../lib/cloud-run-deploy.js': {
|
|
367
339
|
deployImage: () => Promise.resolve({ uri: 'my-uri' }),
|
|
368
340
|
},
|
|
@@ -373,12 +345,15 @@ describe('registerTools', () => {
|
|
|
373
345
|
const handler = server.registerTool.mock.calls.find(
|
|
374
346
|
(call) => call.arguments[0] === 'deploy_container_image'
|
|
375
347
|
).arguments[2];
|
|
376
|
-
const result = await handler(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
348
|
+
const result = await handler(
|
|
349
|
+
{
|
|
350
|
+
project: 'my-project',
|
|
351
|
+
region: 'my-region',
|
|
352
|
+
service: 'my-service',
|
|
353
|
+
imageUrl: 'gcr.io/my-project/my-image',
|
|
354
|
+
},
|
|
355
|
+
{ sendNotification: mock.fn() }
|
|
356
|
+
);
|
|
382
357
|
|
|
383
358
|
assert.deepStrictEqual(result, {
|
|
384
359
|
content: [
|
|
@@ -397,7 +372,7 @@ describe('registerTools', () => {
|
|
|
397
372
|
registerTool: mock.fn(),
|
|
398
373
|
};
|
|
399
374
|
|
|
400
|
-
|
|
375
|
+
const { registerTools } = await esmock('../../tools/tools.js', {});
|
|
401
376
|
|
|
402
377
|
registerTools(server, { gcpCredentialsAvailable: false });
|
|
403
378
|
|