Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
075eb5d
chore: add integration test for gcloud invocation
burkedavison Nov 5, 2025
fcd02f0
chore: formatting
burkedavison Nov 5, 2025
8196255
FIX: Fix Windows Platform Support
Jackxu9946 Nov 10, 2025
7119299
FIX: Windows support
Jackxu9946 Nov 10, 2025
d8e3103
FEAT: fix finding root sdk directory
Jackxu9946 Nov 13, 2025
4bc01b1
local save
Jackxu9946 Nov 13, 2025
c0a29c1
fix: refactor logic
Jackxu9946 Nov 14, 2025
761e0a4
fix: refactor logic
Jackxu9946 Nov 14, 2025
a5bb653
FEAT: Working on windows
Jackxu9946 Nov 17, 2025
005ac3e
chore: clean up
Jackxu9946 Nov 17, 2025
f646d0b
chore: cleanup unused
Jackxu9946 Nov 17, 2025
6596246
chore delete file
Jackxu9946 Nov 17, 2025
7420537
Refactor
Jackxu9946 Nov 18, 2025
6abe001
Refactor
Jackxu9946 Nov 18, 2025
3be0b13
final state. Time for tests
Jackxu9946 Nov 18, 2025
054ad29
chore: linter
Jackxu9946 Nov 18, 2025
9ddf63c
chore: linter
Jackxu9946 Nov 18, 2025
fda1cd8
chore: minor refactoring
Jackxu9946 Nov 18, 2025
42a7cfb
chore: refactoring
Jackxu9946 Nov 18, 2025
1bd4c15
chore: add tests and minor refactoring
Jackxu9946 Nov 18, 2025
66b4dfc
chore: refactoring
Jackxu9946 Nov 18, 2025
19eaea0
chore: add unit test
Jackxu9946 Nov 18, 2025
67c9b5a
chore: linter
Jackxu9946 Nov 18, 2025
3204261
fix: linter
Jackxu9946 Nov 18, 2025
e0349d0
fix: fail gcloud-mcp start up if not valid windows configuration is f…
Jackxu9946 Nov 18, 2025
c60ba91
chore: actual refactoring
Jackxu9946 Nov 18, 2025
bfdfebc
chore: fix test
Jackxu9946 Nov 18, 2025
088806d
fix: fail server initialization if no valid python is found on windows
Jackxu9946 Nov 18, 2025
4b128f8
fix: linter
Jackxu9946 Nov 19, 2025
1125a9c
Merge branch 'burkedavison/it-gcloud-invocation' into user/xujack/fix…
Jackxu9946 Nov 19, 2025
4789be4
fix: make function async and fix test
Jackxu9946 Nov 19, 2025
341784a
fix: linter
Jackxu9946 Nov 19, 2025
9f33549
fix: use async
Jackxu9946 Nov 19, 2025
73103c2
fix: use async and fix test
Jackxu9946 Nov 19, 2025
d761b37
chore: integration test
Jackxu9946 Nov 20, 2025
203c7de
fix: change to list of strings
Jackxu9946 Nov 20, 2025
fbce461
fix: async tests
Jackxu9946 Nov 20, 2025
a0365a7
Merge branch 'user/xujack/fixWindowsPlatform' of https://github.com/g…
Jackxu9946 Nov 20, 2025
632617b
fix: remove sync
Jackxu9946 Nov 20, 2025
cb0ad28
chore: local save
Jackxu9946 Nov 20, 2025
7b79ef3
chore: add additioanl integration test
Jackxu9946 Nov 21, 2025
a2a6969
chore: linter
Jackxu9946 Nov 21, 2025
f9ecfc7
chore: linter
Jackxu9946 Nov 21, 2025
9ddc6d4
chore: cleanup
Jackxu9946 Nov 21, 2025
28ae879
Merge remote-tracking branch 'origin/main' into HEAD
burkedavison Nov 21, 2025
aec6028
refactor: windows support
burkedavison Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,16 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run tests
run: |
npm run test:i
- name: Run unit tests
run: npm run test:i

- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db' # v3
with:
version: '>= 530.0.0'

- name: Run integration tests
run: npm run test:integration --workspace packages/gcloud-mcp

coverage:
if: github.actor != 'dependabot[bot]'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Thumbs.db
coverage/
packages/*/coverage/
junit.xml
junit.integration.xml
packages/*/junit.xml

# Ignore package-lock.json files in subdirectories
Expand Down
1 change: 1 addition & 0 deletions packages/gcloud-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"scripts": {
"build": "tsc --noemit && node build.js",
"test": "vitest run",
"test:integration": "vitest run --config vitest.config.integration.ts",
"start": "node dist/bundle.js",
"lint": "prettier --check . && eslint . --max-warnings 0",
"fix": "prettier --write . && eslint . --fix",
Expand Down
116 changes: 116 additions & 0 deletions packages/gcloud-mcp/src/gcloud-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as child_process from 'child_process';
import { getWindowsCloudSDKSettingsAsync } from './windows_gcloud_utils.js';
import * as path from 'path';

export const isWindows = (): boolean => process.platform === 'win32';

export interface GcloudExecutionResult {
code: number | null;
stdout: string;
stderr: string;
}

export interface GcloudExecutor {
execute: (args: string[]) => Promise<GcloudExecutionResult>
}

export const findExecutable = async (): Promise<GcloudExecutor> => {
const executor = await createExecutor();
return {
execute: async (args: string[]): Promise<GcloudExecutionResult> =>
new Promise(async (resolve, reject) => {
let stdout = '';
let stderr = '';

const gcloud = executor.execute(args);

gcloud.stdout.on('data', (data) => {
stdout += data.toString();
});
gcloud.stderr.on('data', (data) => {
stderr += data.toString();
});

gcloud.on('close', (code) => {
// All responses from gcloud, including non-zero codes.
resolve({ code, stdout, stderr });
});
gcloud.on('error', (err) => {
// Process failed to start. gcloud isn't able to be invoked.
reject(err);
});
})
}
}

const isAvailable = (): Promise<boolean> =>
new Promise((resolve) => {
const which = child_process.spawn(isWindows() ? 'where' : 'which', ['gcloud']);
which.on('close', (code) => {
resolve(code === 0);
});
which.on('error', () => {
resolve(false);
});
});

const createExecutor = async () => {
if(!await isAvailable()) {
throw Error('gcloud executable not found');
}
if (isWindows()) {
return await createWindowsExecutor();
}
return createDirectExecutor();
}

/** Creates an executor that directly invokes the gcloud binary on the current PATH. */
const createDirectExecutor = () => ({
execute: (args: string[]) => child_process.spawn('gcloud', args, {
stdio: ['ignore', 'pipe', 'pipe'],
})
});

const createWindowsExecutor = async () => {
const settings = await getWindowsCloudSDKSettingsAsync();

if (settings == null || settings.noWorkingPythonFound) {
throw Error('no working Python installation found for Windows gcloud execution.');
}

const windowsPathForGcloudPy = path.join(
settings.cloudSdkRootDir,
'lib',
'gcloud.py',
);

const pythonPath = path.normalize(
settings.cloudSdkPython,
);

return {
execute: (args: string[]) => child_process.spawn(pythonPath, [
...settings.cloudSdkPythonArgsList,
windowsPathForGcloudPy,
...args
], {
stdio: ['ignore', 'pipe', 'pipe']
})
}
}
91 changes: 91 additions & 0 deletions packages/gcloud-mcp/src/gcloud.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { test, expect, assert } from 'vitest';
import * as gcloud from './gcloud.js';

test('gcloud is available', async () => {
const result = await gcloud.isAvailable();
expect(result).toBe(true);
});

test('can invoke gcloud to lint a command', async () => {
const result = await gcloud.lint('compute instances list');
assert(result.success);
expect(result.parsedCommand).toBe('compute instances list');
}, 10000);

test('cannot inject a command by appending arguments', async () => {
const result = await gcloud.invoke(['config', 'list', '&&', 'echo', 'asdf']);
expect(result.stdout).not.toContain('asdf');
expect(result.code).toBeGreaterThan(0);
}, 10000);

test('cannot inject a command by appending command', async () => {
const result = await gcloud.invoke(['config', 'list', '&&', 'echo asdf']);
expect(result.code).toBeGreaterThan(0);
}, 10000);

test('cannot inject a command with a final argument', async () => {
const result = await gcloud.invoke(['config', 'list', '&& echo asdf']);
expect(result.code).toBeGreaterThan(0);
}, 10000);

test('cannot inject a command with a single argument', async () => {
const result = await gcloud.invoke(['config list && echo asdf']);
expect(result.code).toBeGreaterThan(0);
}, 10000);

test('can invoke windows gcloud when there are multiple python args', async () => {
// Set the environment variables correctly and then reimport gcloud to force it to reload
process.env['CLOUDSDK_PYTHON_ARGS'] = '-S -u -B';
const gcloud = await import('./gcloud.js');
const result = await gcloud.invoke(['config', 'list']);
expect(result.code).toBe(0);
}, 10000);

test('can invoke windows gcloud when there are 1 python args', async () => {
// Set the environment variables correctly and then reimport gcloud to force it to reload
process.env['CLOUDSDK_PYTHON_ARGS'] = '-u';
const gcloud = await import('./gcloud.js');
const result = await gcloud.invoke(['config', 'list']);
expect(result.code).toBe(0);
}, 10000);

test('can invoke windows gcloud when there are no python args', async () => {
// Set the environment variables correctly and then reimport gcloud to force it to reload
process.env['CLOUDSDK_PYTHON_ARGS'] = '';
const gcloud = await import('./gcloud.js');
const result = await gcloud.invoke(['config', 'list']);
expect(result.code).toBe(0);
}, 10000);

test('can invoke windows gcloud when site packages are enabled', async () => {
// Set the environment variables correctly and then reimport gcloud to force it to reload
process.env['CLOUDSDK_PYTHON_SITEPACKAGES'] = '1';
const gcloud = await import('./gcloud.js');
const result = await gcloud.invoke(['config', 'list']);
expect(result.code).toBe(0);
}, 10000);

test('can invoke windows gcloud when site packages are enabled and python args exists', async () => {
// Set the environment variables correctly and then reimport gcloud to force it to reload
process.env['CLOUDSDK_PYTHON_SITEPACKAGES'] = '1';
process.env['CLOUDSDK_PYTHON_ARGS'] = '-u';
const gcloud = await import('./gcloud.js');
const result = await gcloud.invoke(['config', 'list']);
expect(result.code).toBe(0);
}, 10000);
Loading
Loading