From 7ff6ba11ead905359024b1d73156ab34afed4807 Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:00:51 +0000 Subject: [PATCH 1/6] fix: redirect auth messages to stderr for MCP compatibility In headless/VM environments, auth URL messages were printed to stdout via console.log, breaking MCP clients that expect only JSON-RPC on stdout. Changed console.log to console.error in: - open-wrapper.ts (browser launch failure messages) - AuthManager.ts (waiting for auth message) --- workspace-server/src/auth/AuthManager.ts | 2 +- workspace-server/src/utils/open-wrapper.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace-server/src/auth/AuthManager.ts b/workspace-server/src/auth/AuthManager.ts index acd7f76..2f23c56 100644 --- a/workspace-server/src/auth/AuthManager.ts +++ b/workspace-server/src/auth/AuthManager.ts @@ -156,7 +156,7 @@ export class AuthManager { const webLogin = await this.authWithWeb(oAuth2Client); await open(webLogin.authUrl); - console.log('Waiting for authentication...'); + console.error('Waiting for authentication...'); // Add timeout to prevent infinite waiting when browser tab gets stuck const authTimeout = 5 * 60 * 1000; // 5 minutes timeout diff --git a/workspace-server/src/utils/open-wrapper.ts b/workspace-server/src/utils/open-wrapper.ts index 3575e86..26c4d5d 100644 --- a/workspace-server/src/utils/open-wrapper.ts +++ b/workspace-server/src/utils/open-wrapper.ts @@ -33,7 +33,7 @@ const createMockChildProcess = () => ({ const openWrapper = async (url: string): Promise => { // Check if we should launch the browser if (!shouldLaunchBrowser()) { - console.log(`Browser launch not supported. Please open this URL in your browser: ${url}`); + console.error(`Browser launch not supported. Please open this URL in your browser: ${url}`); return createMockChildProcess(); } @@ -42,7 +42,7 @@ const openWrapper = async (url: string): Promise => { await openBrowserSecurely(url); return createMockChildProcess(); } catch { - console.log(`Failed to open browser. Please open this URL in your browser: ${url}`); + console.error(`Failed to open browser. Please open this URL in your browser: ${url}`); return createMockChildProcess(); } }; From b2e727c7b69464678cf8e44de61be2eea2e1ccc7 Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:10:25 +0000 Subject: [PATCH 2/6] fix: always print auth URL to stderr for headless environments Print the auth URL immediately before attempting browser launch, ensuring the URL is always visible in headless/VM environments even when DISPLAY is set for port-forwarding. --- workspace-server/src/utils/open-wrapper.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/workspace-server/src/utils/open-wrapper.ts b/workspace-server/src/utils/open-wrapper.ts index 26c4d5d..a71c6aa 100644 --- a/workspace-server/src/utils/open-wrapper.ts +++ b/workspace-server/src/utils/open-wrapper.ts @@ -31,20 +31,21 @@ const createMockChildProcess = () => ({ }); const openWrapper = async (url: string): Promise => { - // Check if we should launch the browser + // Always print the URL to stderr first for headless/VM environments + console.error(`\nPlease open this URL in your browser to authenticate:\n${url}\n`); + + // Check if we should also try to launch the browser if (!shouldLaunchBrowser()) { - console.error(`Browser launch not supported. Please open this URL in your browser: ${url}`); return createMockChildProcess(); } - // Try to open the browser securely + // Try to open the browser securely (best effort, don't fail if it doesn't work) try { await openBrowserSecurely(url); - return createMockChildProcess(); } catch { - console.error(`Failed to open browser. Please open this URL in your browser: ${url}`); - return createMockChildProcess(); + // Browser launch failed, but URL is already printed above } + return createMockChildProcess(); }; // Use standard ES Module export and let the compiler generate the CommonJS correct output. From 7af77458d54e9c7302e20437f3be2a51c7576ef6 Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:27:06 +0000 Subject: [PATCH 3/6] feat: improve headless/remote VM authentication experience Changes: - Redirect auth messages to stderr (keep stdout clean for MCP JSON-RPC) - Add GEMINI_CLI_WORKSPACE_HEADLESS env var for port-forwarding setups - Print port-forwarding instructions when in headless mode - Improve manual flow credentials page with OS-specific instructions: - Linux CLI (secret-tool command, expanded by default) - macOS Keychain Access - Windows Credential Manager - Add PR.md with full documentation --- PR.md | 96 +++++++++++++++++++ cloud_function/index.js | 44 +++++++-- workspace-server/src/auth/AuthManager.ts | 20 +++- .../src/utils/secure-browser-launcher.ts | 6 ++ 4 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 0000000..0364408 --- /dev/null +++ b/PR.md @@ -0,0 +1,96 @@ +# PR: Improve Headless/Remote VM Authentication Experience + +## Summary + +This PR improves the authentication experience for users running the Google Workspace MCP server in headless or remote VM environments where traditional browser-based OAuth flows don't work well. + +## Problem + +When running the MCP server in a VM/headless environment: +1. **stdout pollution**: Auth messages were printed to `stdout`, breaking MCP clients that expect only JSON-RPC +2. **Stuck terminal**: When browser launch fails, the callback server waits forever for a redirect that never comes +3. **Poor manual flow UX**: The credentials page lacked clear instructions for different operating systems + +## Changes + +### 1. Redirect Auth Messages to stderr +**Files:** `workspace-server/src/utils/open-wrapper.ts`, `workspace-server/src/auth/AuthManager.ts` + +- Changed `console.log` to `console.error` for all auth-related messages +- Auth URL is now always printed to stderr immediately (visible to users) +- Keeps stdout clean for MCP JSON-RPC protocol + +### 2. Add `GEMINI_CLI_WORKSPACE_HEADLESS` Environment Variable +**Files:** `workspace-server/src/utils/secure-browser-launcher.ts`, `workspace-server/src/auth/AuthManager.ts` + +New environment variable for headless environments with port-forwarding: + +```python +env = { + "GEMINI_CLI_WORKSPACE_HEADLESS": "true", + "OAUTH_CALLBACK_PORT": "8585" +} +``` + +**Behavior:** +- Prints auth URL to stderr (no browser launch attempt) +- OAuth callback server still runs for redirect +- Port-forwarding instructions printed to help remote users + +### 3. Improved Manual Flow Instructions +**Files:** `cloud_function/index.js` + +Enhanced the "Success! Credentials Ready" page with: +- **Linux CLI instructions** (expanded by default) - `secret-tool` command for remote VMs +- **macOS instructions** - Keychain Access step-by-step +- **Windows instructions** - Credential Manager step-by-step +- Collapsible sections for each OS +- Clear "restart your MCP server" instruction + +## Usage + +### Option A: Headless Mode with Port-Forwarding + +```python +from mcp import StdioServerParameters + +server_params = StdioServerParameters( + command = "npx", + args = ["-y", "github:philschmid/workspace"], + env = { + "GEMINI_CLI_WORKSPACE_HEADLESS": "true", + "OAUTH_CALLBACK_PORT": "8585" + } +) +``` + +1. Port-forward 8585 from VM: `ssh -L 8585:localhost:8585 your-vm` +2. Run your MCP client +3. Copy the auth URL from stderr +4. Authenticate in browser +5. Callback completes automatically + +### Option B: Manual Flow (No Port-Forwarding) + +1. Run MCP client (without `GEMINI_CLI_WORKSPACE_HEADLESS`) +2. Copy the auth URL from stderr +3. Authenticate in browser +4. Copy JSON credentials from the success page +5. Store using `secret-tool` (Linux) or Keychain/Credential Manager +6. Restart MCP server + +## Testing + +- [x] Build passes (`npm run build`) +- [ ] Test headless mode with port-forwarding +- [ ] Test manual flow credential storage +- [ ] Verify MCP client receives only JSON-RPC on stdout + +## Environment Variables Reference + +| Variable | Description | +|----------|-------------| +| `GEMINI_CLI_WORKSPACE_HEADLESS` | Skip browser launch, print URL to stderr, keep callback server | +| `OAUTH_CALLBACK_PORT` | Fixed port for OAuth callback (default: dynamic) | +| `OAUTH_CALLBACK_HOST` | Callback hostname (default: localhost) | +| `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE` | Force encrypted file storage instead of keychain | diff --git a/cloud_function/index.js b/cloud_function/index.js index 75d8787..a88509c 100644 --- a/cloud_function/index.js +++ b/cloud_function/index.js @@ -183,16 +183,40 @@ async function handleCallback(req, res) {

Keychain Storage Instructions:

-
    -
  1. Open your OS Keychain/Credential Manager.
  2. -
  3. Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).
  4. -
  5. Set the **Service** (or equivalent field) to: ${KEYCHAIN_SERVICE_NAME}
  6. -
  7. Set the **Account** (or username field) to: ${KEYCHAIN_ACCOUNT_NAME}
  8. -
  9. Paste the copied JSON into the **Password/Secret** field.
  10. -
  11. Save the entry.
  12. -
-

Your local MCP server will now be able to find and use these credentials automatically.

-

(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)

+ +
+ Linux (CLI / Remote VM) +

Run this command in your terminal, then paste the JSON and press Ctrl+D:

+
secret-tool store --label="Gemini Workspace OAuth" service ${KEYCHAIN_SERVICE_NAME} username ${KEYCHAIN_ACCOUNT_NAME}
+

Note: Requires libsecret-tools. Install with: sudo apt install libsecret-tools

+
+ +
+ macOS +
    +
  1. Open Keychain Access (Applications → Utilities).
  2. +
  3. Click File → New Password Item.
  4. +
  5. Set Keychain Item Name to: ${KEYCHAIN_SERVICE_NAME}
  6. +
  7. Set Account Name to: ${KEYCHAIN_ACCOUNT_NAME}
  8. +
  9. Paste the JSON into the Password field.
  10. +
  11. Click Add.
  12. +
+
+ +
+ Windows +
    +
  1. Open Credential Manager (search in Start menu).
  2. +
  3. Select Windows CredentialsAdd a generic credential.
  4. +
  5. Set Internet or network address to: ${KEYCHAIN_SERVICE_NAME}
  6. +
  7. Set User name to: ${KEYCHAIN_ACCOUNT_NAME}
  8. +
  9. Paste the JSON into the Password field.
  10. +
  11. Click OK.
  12. +
+
+ +

After storing, restart your MCP server. It will automatically find and use the credentials.

+

(If keychain is unavailable, set GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true to use encrypted file storage.)

diff --git a/workspace-server/src/auth/AuthManager.ts b/workspace-server/src/auth/AuthManager.ts index 2f23c56..cb7c8e9 100644 --- a/workspace-server/src/auth/AuthManager.ts +++ b/workspace-server/src/auth/AuthManager.ts @@ -155,6 +155,16 @@ export class AuthManager { } const webLogin = await this.authWithWeb(oAuth2Client); + + // Print port-forwarding instructions when in headless mode + const isHeadless = process.env['GEMINI_CLI_WORKSPACE_HEADLESS'] === 'true' || + process.env['GEMINI_CLI_WORKSPACE_HEADLESS'] === '1'; + if (isHeadless) { + const port = process.env['OAUTH_CALLBACK_PORT'] || 'dynamically assigned'; + console.error(`\n[Headless Mode] If you are on a remote instance, ensure port ${port} is forwarded to your local machine.`); + console.error(`Example: ssh -L ${port}:localhost:${port} your-remote-host\n`); + } + await open(webLogin.authUrl); console.error('Waiting for authentication...'); @@ -275,13 +285,19 @@ export class AuthManager { const isGuiAvailable = shouldLaunchBrowser(); + // GEMINI_CLI_WORKSPACE_HEADLESS: Don't launch browser, but still use callback server for redirect + // This enables headless/VM environments with port-forwarding to work + const isMcpHeadless = process.env['GEMINI_CLI_WORKSPACE_HEADLESS'] === 'true' || process.env['GEMINI_CLI_WORKSPACE_HEADLESS'] === '1'; + const useCallbackServer = isGuiAvailable || isMcpHeadless; + // SECURITY: Generate a random token for CSRF protection. const csrfToken = crypto.randomBytes(32).toString('hex'); // The state now contains a JSON payload indicating the flow mode and CSRF token. + // When using callback server (GUI mode or MCP_HEADLESS), include the URI and set manual: false const statePayload = { - uri: isGuiAvailable ? localRedirectUri : undefined, - manual: !isGuiAvailable, + uri: useCallbackServer ? localRedirectUri : undefined, + manual: !useCallbackServer, csrf: csrfToken, }; const state = Buffer.from(JSON.stringify(statePayload)).toString('base64'); diff --git a/workspace-server/src/utils/secure-browser-launcher.ts b/workspace-server/src/utils/secure-browser-launcher.ts index 6126d44..e721630 100644 --- a/workspace-server/src/utils/secure-browser-launcher.ts +++ b/workspace-server/src/utils/secure-browser-launcher.ts @@ -192,6 +192,12 @@ export async function openBrowserSecurely( * @returns True if the tool should attempt to launch a browser */ export function shouldLaunchBrowser(): boolean { + // GEMINI_CLI_WORKSPACE_HEADLESS: Skip browser launch but still use callback server for OAuth redirect + // This is useful for headless/VM environments with port-forwarding + if (process.env.GEMINI_CLI_WORKSPACE_HEADLESS === 'true' || process.env.GEMINI_CLI_WORKSPACE_HEADLESS === '1') { + return false; + } + // A list of browser names that indicate we should not attempt to open a // web browser for the user. const browserBlocklist = ['www-browser']; From 69c4dd21d78a4879c715de3df30ba227b5c965ae Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:56:04 +0000 Subject: [PATCH 4/6] chore: untrack PR.md --- .gitignore | 2 +- PR.md | 96 ------------------------------------------------------ 2 files changed, 1 insertion(+), 97 deletions(-) delete mode 100644 PR.md diff --git a/.gitignore b/.gitignore index 3dd9aa5..0431454 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,4 @@ release/ # VitePress docs/.vitepress/dist -docs/.vitepress/cache \ No newline at end of file +docs/.vitepress/cachePR.md diff --git a/PR.md b/PR.md deleted file mode 100644 index 0364408..0000000 --- a/PR.md +++ /dev/null @@ -1,96 +0,0 @@ -# PR: Improve Headless/Remote VM Authentication Experience - -## Summary - -This PR improves the authentication experience for users running the Google Workspace MCP server in headless or remote VM environments where traditional browser-based OAuth flows don't work well. - -## Problem - -When running the MCP server in a VM/headless environment: -1. **stdout pollution**: Auth messages were printed to `stdout`, breaking MCP clients that expect only JSON-RPC -2. **Stuck terminal**: When browser launch fails, the callback server waits forever for a redirect that never comes -3. **Poor manual flow UX**: The credentials page lacked clear instructions for different operating systems - -## Changes - -### 1. Redirect Auth Messages to stderr -**Files:** `workspace-server/src/utils/open-wrapper.ts`, `workspace-server/src/auth/AuthManager.ts` - -- Changed `console.log` to `console.error` for all auth-related messages -- Auth URL is now always printed to stderr immediately (visible to users) -- Keeps stdout clean for MCP JSON-RPC protocol - -### 2. Add `GEMINI_CLI_WORKSPACE_HEADLESS` Environment Variable -**Files:** `workspace-server/src/utils/secure-browser-launcher.ts`, `workspace-server/src/auth/AuthManager.ts` - -New environment variable for headless environments with port-forwarding: - -```python -env = { - "GEMINI_CLI_WORKSPACE_HEADLESS": "true", - "OAUTH_CALLBACK_PORT": "8585" -} -``` - -**Behavior:** -- Prints auth URL to stderr (no browser launch attempt) -- OAuth callback server still runs for redirect -- Port-forwarding instructions printed to help remote users - -### 3. Improved Manual Flow Instructions -**Files:** `cloud_function/index.js` - -Enhanced the "Success! Credentials Ready" page with: -- **Linux CLI instructions** (expanded by default) - `secret-tool` command for remote VMs -- **macOS instructions** - Keychain Access step-by-step -- **Windows instructions** - Credential Manager step-by-step -- Collapsible sections for each OS -- Clear "restart your MCP server" instruction - -## Usage - -### Option A: Headless Mode with Port-Forwarding - -```python -from mcp import StdioServerParameters - -server_params = StdioServerParameters( - command = "npx", - args = ["-y", "github:philschmid/workspace"], - env = { - "GEMINI_CLI_WORKSPACE_HEADLESS": "true", - "OAUTH_CALLBACK_PORT": "8585" - } -) -``` - -1. Port-forward 8585 from VM: `ssh -L 8585:localhost:8585 your-vm` -2. Run your MCP client -3. Copy the auth URL from stderr -4. Authenticate in browser -5. Callback completes automatically - -### Option B: Manual Flow (No Port-Forwarding) - -1. Run MCP client (without `GEMINI_CLI_WORKSPACE_HEADLESS`) -2. Copy the auth URL from stderr -3. Authenticate in browser -4. Copy JSON credentials from the success page -5. Store using `secret-tool` (Linux) or Keychain/Credential Manager -6. Restart MCP server - -## Testing - -- [x] Build passes (`npm run build`) -- [ ] Test headless mode with port-forwarding -- [ ] Test manual flow credential storage -- [ ] Verify MCP client receives only JSON-RPC on stdout - -## Environment Variables Reference - -| Variable | Description | -|----------|-------------| -| `GEMINI_CLI_WORKSPACE_HEADLESS` | Skip browser launch, print URL to stderr, keep callback server | -| `OAUTH_CALLBACK_PORT` | Fixed port for OAuth callback (default: dynamic) | -| `OAUTH_CALLBACK_HOST` | Callback hostname (default: localhost) | -| `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE` | Force encrypted file storage instead of keychain | From 05febc1bbf5bde7d9b1dfdbf417f25db794ea852 Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:58:03 +0000 Subject: [PATCH 5/6] docs: add headless mode / remote VM instructions to README Added section explaining how to use GEMINI_CLI_WORKSPACE_HEADLESS with port-forwarding for authentication on remote VMs. --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 8744edb..3372834 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,24 @@ Searches your Google Drive for files matching the given query. - [Documentation](docs/index.md): Detailed documentation on all the available tools. - [GitHub Issues](https://github.com/gemini-cli-extensions/workspace/issues): Report bugs or request features. +## Headless Mode / Remote VMs + +If you're running the MCP server on a remote VM or headless environment where a browser cannot be opened, you can use headless mode with port-forwarding: + +```python +env = { + "GEMINI_CLI_WORKSPACE_HEADLESS": "true", + "OAUTH_CALLBACK_PORT": "8585" +} +``` + +**Steps:** +1. Set up port-forwarding from your VM: `ssh -L 8585:localhost:8585 your-vm` +2. Run the MCP server with the environment variables above +3. Copy the authentication URL printed to stderr +4. Open the URL in your local browser and complete authentication +5. The OAuth callback will be forwarded to the VM, completing the flow + ## Important security consideration: Indirect Prompt Injection Risk When exposing any language model to untrusted data, there's a risk of an [indirect prompt injection attack](https://en.wikipedia.org/wiki/Prompt_injection). Agentic tools like Gemini CLI, connected to MCP servers, have access to a wide array of tools and APIs. From f07f9288c06989f2a5e6f3080ff376b8514ac6ef Mon Sep 17 00:00:00 2001 From: philschmid Date: Mon, 12 Jan 2026 17:58:49 +0000 Subject: [PATCH 6/6] fix --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0431454..6ad23df 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ release/ # VitePress docs/.vitepress/dist -docs/.vitepress/cachePR.md +docs/.vitepress/cache +