Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ Shows your schedule for today or a specified date.

Searches your Google Drive for files matching the given query.

## Headless / Remote Environments

If you're using the extension over SSH, WSL, Cloud Shell, or another environment
without a local browser, you can authenticate using the headless login tool:

```bash
node scripts/auth-utils.js login
```

This prints an OAuth URL you can open in any browser (local machine, phone,
etc.). After signing in, paste the credentials JSON into the CLI. Credentials
are read securely from `/dev/tty` and are never exposed to the AI model. See the
[development docs](docs/development.md#headless--remote-environments) for more
details.

## Deployment

If you want to host your own version of this extension's infrastructure, see the
Expand Down
30 changes: 19 additions & 11 deletions cloud_function/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,17 +192,25 @@ async function handleCallback(req, res) {
<span id="copy-status">Copied!</span>

<div class="instructions">
<h4>Keychain Storage Instructions:</h4>
<ol>
<li>Open your OS Keychain/Credential Manager.</li>
<li>Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).</li>
<li>Set the **Service** (or equivalent field) to: <code>${KEYCHAIN_SERVICE_NAME}</code></li>
<li>Set the **Account** (or username field) to: <code>${KEYCHAIN_ACCOUNT_NAME}</code></li>
<li>Paste the copied JSON into the **Password/Secret** field.</li>
<li>Save the entry.</li>
</ol>
<p>Your local MCP server will now be able to find and use these credentials automatically.</p>
<p><small>(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)</small></p>
<h4>CLI Login (Recommended):</h4>
<p>In your terminal, run:</p>
<pre style="background:#eee;padding:0.75rem;border-radius:4px;overflow-x:auto;"><code>node dist/headless-login.js</code></pre>
<p>Then paste the JSON above when prompted. The CLI will securely store your credentials.</p>

<details style="margin-top: 1.5rem;">
<summary style="cursor:pointer;color:#555;"><strong>Advanced: Manual Keychain Storage</strong></summary>
<div style="margin-top: 0.5rem;">
<ol>
<li>Open your OS Keychain/Credential Manager.</li>
<li>Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).</li>
<li>Set the <strong>Service</strong> (or equivalent field) to: <code>${KEYCHAIN_SERVICE_NAME}</code></li>
<li>Set the <strong>Account</strong> (or username field) to: <code>${KEYCHAIN_ACCOUNT_NAME}</code></li>
<li>Paste the copied JSON into the <strong>Password/Secret</strong> field.</li>
<li>Save the entry.</li>
</ol>
<p><small>(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)</small></p>
</div>
</details>
</div>
</div>

Expand Down
26 changes: 26 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ used to maintain dot notation and avoid breaking existing configurations.
- `src/`: Contains the source code for the server.
- `__tests__/`: Contains all the tests.
- `auth/`: Handles authentication.
- `cli/`: CLI tools (e.g., headless OAuth login).
- `services/`: Contains the business logic for each service.
- `utils/`: Contains utility functions.
- `config/`: Contains configuration files.
Expand All @@ -176,7 +177,32 @@ node scripts/auth-utils.js <command>

### Commands

- `login`: Authenticate via headless OAuth flow (for SSH/WSL/Cloud Shell). Reads
credentials securely from `/dev/tty` so they are not visible to AI models.
- `clear`: Clear all authentication credentials.
- `expire`: Force the access token to expire (for testing refresh).
- `status`: Show current authentication status.
- `help`: Show the help message.

### Headless / Remote Environments

If you are running the server in an environment without a browser (SSH, WSL,
Cloud Shell, VMs), authentication requires manual steps:

1. Run the login tool:
```bash
node scripts/auth-utils.js login
```
Or, from the `workspace-server` directory:
```bash
node dist/headless-login.js
```
2. Open the printed OAuth URL in any browser (your local machine, phone, etc.).
3. Complete Google sign-in. The browser will display a credentials JSON block.
4. Copy the JSON and paste it into the CLI when prompted.

The CLI reads input from `/dev/tty` (Unix) or `CON` (Windows) rather than
process stdin, so credentials are never exposed to an AI model that may have
spawned the process.

Use `--force` to re-authenticate if credentials already exist.
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ The extension provides the following tools:
assistant). Defaults to the authenticated user and supports filtering by
relation type.

### Authentication

- `auth.clear`: Clears authentication credentials, forcing a re-login on the
next request.
- `auth.refreshToken`: Manually triggers the token refresh process.

## Custom Commands

The extension includes several pre-configured commands for common tasks:
Expand Down
17 changes: 17 additions & 0 deletions scripts/auth-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,33 @@ async function showStatus() {
}
}

async function login() {
try {
require('../workspace-server/dist/headless-login.js');
} catch (error) {
console.error(
'❌ Failed to load headless-login module. Run "npm run build:headless-login" first.',
);
console.error(error.message);
process.exit(1);
}
}

function showHelp() {
console.log(`
Auth Management CLI

Usage: node scripts/auth-utils.js <command>

Commands:
login Authenticate via headless OAuth flow (for SSH/WSL/Cloud Shell)
clear Clear all authentication credentials
expire Force the access token to expire (for testing refresh)
status Show current authentication status
help Show this help message

Examples:
node scripts/auth-utils.js login
node scripts/auth-utils.js clear
node scripts/auth-utils.js expire
node scripts/auth-utils.js status
Expand All @@ -97,6 +111,9 @@ async function main() {
const command = process.argv[2];

switch (command) {
case 'login':
await login();
break;
case 'clear':
await clearAuth();
break;
Expand Down
33 changes: 33 additions & 0 deletions workspace-server/esbuild.headless-login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const esbuild = require('esbuild');

async function buildHeadlessLogin() {
try {
await esbuild.build({
entryPoints: ['src/cli/headless-login.ts'],
bundle: true,
platform: 'node',
target: 'node20',
outfile: 'dist/headless-login.js',
minify: true,
sourcemap: true,
external: [
'keytar', // keytar is a native module and should not be bundled
],
format: 'cjs',
logLevel: 'info',
});

console.log('Headless Login build completed successfully!');
} catch (error) {
console.error('Headless Login build failed:', error);
process.exit(1);
}
}

buildHeadlessLogin();
5 changes: 3 additions & 2 deletions workspace-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"test:ci": "cd .. && node --max-old-space-size=4096 node_modules/jest/bin/jest.js --ci --coverage --maxWorkers=2",
"start": "ts-node src/index.ts",
"clean": "rm -rf dist node_modules",
"build": "node esbuild.config.js",
"build:auth-utils": "node esbuild.auth-utils.js"
"build": "node esbuild.config.js && node esbuild.headless-login.js",
"build:auth-utils": "node esbuild.auth-utils.js",
"build:headless-login": "node esbuild.headless-login.js"
},
"keywords": [],
"author": "Allen Hutchison",
Expand Down
10 changes: 10 additions & 0 deletions workspace-server/src/auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ export class AuthManager {
}
}

// Fail fast in headless environments instead of hanging for 5 minutes
if (!shouldLaunchBrowser()) {
throw new Error(
'No browser available for authentication. ' +
'Please run: node dist/headless-login.js\n' +
'(from the workspace-server directory)\n' +
'After authenticating, retry your request.',
);
}

const webLogin = await this.authWithWeb(oAuth2Client);
await open(webLogin.authUrl);
const msg = 'Waiting for authentication... Check your browser.';
Expand Down
23 changes: 23 additions & 0 deletions workspace-server/src/auth/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* OAuth scopes required by the Google Workspace MCP server.
* Shared between the MCP server and the headless login CLI.
*/
export const SCOPES = [
'https://www.googleapis.com/auth/documents',
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/chat.spaces',
'https://www.googleapis.com/auth/chat.messages',
'https://www.googleapis.com/auth/chat.memberships',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/directory.readonly',
'https://www.googleapis.com/auth/presentations.readonly',
'https://www.googleapis.com/auth/spreadsheets.readonly',
];
Loading
Loading