Skip to content

Commit 07c79b1

Browse files
authored
Merge branch 'main' into main
2 parents f82b0ac + 7c3a840 commit 07c79b1

30 files changed

+2250
-1053
lines changed

docs/cli/authentication.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
3333
```bash
3434
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
3535
```
36-
- For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
36+
- For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files).
37+
38+
- Alternatively you can export the API key from your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
39+
3740
```bash
3841
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
3942
source ~/.bashrc
4043
```
4144
45+
:warning: Be advised that when you export your API key inside your shell configuration file, any other process executed from the shell can read it.
46+
4247
3. **Vertex AI:**
4348
- Obtain your Google Cloud API key: [Get an API Key](https://cloud.google.com/vertex-ai/generative-ai/docs/start/api-keys?usertype=newuser)
4449
- Set the `GOOGLE_API_KEY` environment variable. In the following methods, replace `YOUR_GOOGLE_API_KEY` with your Vertex AI API key:
@@ -63,17 +68,25 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
6368
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
6469
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
6570
```
66-
- For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
71+
- For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files)
72+
73+
- Alternatively you can export the environment variables from your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
74+
6775
```bash
6876
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
6977
echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
7078
source ~/.bashrc
7179
```
80+
81+
:warning: Be advised that when you export your API key inside your shell configuration file, any other process executed from the shell can read it.
82+
7283
4. **Cloud Shell:**
7384
- This option is only available when running in a Google Cloud Shell environment.
7485
- It automatically uses the credentials of the logged-in user in the Cloud Shell environment.
7586
- This is the default authentication method when running in Cloud Shell and no other method is configured.
7687
88+
:warning: Be advised that when you export your API key inside your shell configuration file, any other process executed from the shell can read it.
89+
7790
### Persisting Environment Variables with `.env` Files
7891
7992
You can create a **`.gemini/.env`** file in your project directory or in your home directory. Creating a plain **`.env`** file also works, but `.gemini/.env` is recommended to keep Gemini variables isolated from other tools.
@@ -107,3 +120,24 @@ GOOGLE_CLOUD_PROJECT="your-project-id"
107120
GEMINI_API_KEY="your-gemini-api-key"
108121
EOF
109122
```
123+
124+
## Non-Interactive Mode / Headless Environments
125+
126+
When running the Gemini CLI in a non-interactive environment, you cannot use the interactive login flow.
127+
Instead, you must configure authentication using environment variables.
128+
129+
The CLI will automatically detect if it is running in a non-interactive terminal and will use one of the
130+
following authentication methods if available:
131+
132+
1. **Gemini API Key:**
133+
- Set the `GEMINI_API_KEY` environment variable.
134+
- The CLI will use this key to authenticate with the Gemini API.
135+
136+
2. **Vertex AI:**
137+
- Set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
138+
- **Using an API Key:** Set the `GOOGLE_API_KEY` environment variable.
139+
- **Using Application Default Credentials (ADC):**
140+
- Run `gcloud auth application-default login` in your environment to configure ADC.
141+
- Ensure the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables are set.
142+
143+
If none of these environment variables are set in a non-interactive session, the CLI will exit with an error.

docs/tools/mcp-server.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,90 @@ Each server configuration supports the following properties:
9595
- **`includeTools`** (string[]): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (whitelist behavior). If not specified, all tools from the server are enabled by default.
9696
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
9797

98+
### OAuth Support for Remote MCP Servers
99+
100+
The Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using SSE or HTTP transports. This enables secure access to MCP servers that require authentication.
101+
102+
#### Automatic OAuth Discovery
103+
104+
For servers that support OAuth discovery, you can omit the OAuth configuration and let the CLI discover it automatically:
105+
106+
```json
107+
{
108+
"mcpServers": {
109+
"discoveredServer": {
110+
"url": "https://api.example.com/sse"
111+
}
112+
}
113+
}
114+
```
115+
116+
The CLI will automatically:
117+
118+
- Detect when a server requires OAuth authentication (401 responses)
119+
- Discover OAuth endpoints from server metadata
120+
- Perform dynamic client registration if supported
121+
- Handle the OAuth flow and token management
122+
123+
#### Authentication Flow
124+
125+
When connecting to an OAuth-enabled server:
126+
127+
1. **Initial connection attempt** fails with 401 Unauthorized
128+
2. **OAuth discovery** finds authorization and token endpoints
129+
3. **Browser opens** for user authentication (requires local browser access)
130+
4. **Authorization code** is exchanged for access tokens
131+
5. **Tokens are stored** securely for future use
132+
6. **Connection retry** succeeds with valid tokens
133+
134+
#### Browser Redirect Requirements
135+
136+
**Important:** OAuth authentication requires that your local machine can:
137+
138+
- Open a web browser for authentication
139+
- Receive redirects on `http://localhost:7777/oauth/callback`
140+
141+
This feature will not work in:
142+
143+
- Headless environments without browser access
144+
- Remote SSH sessions without X11 forwarding
145+
- Containerized environments without browser support
146+
147+
#### Managing OAuth Authentication
148+
149+
Use the `/mcp auth` command to manage OAuth authentication:
150+
151+
```bash
152+
# List servers requiring authentication
153+
/mcp auth
154+
155+
# Authenticate with a specific server
156+
/mcp auth serverName
157+
158+
# Re-authenticate if tokens expire
159+
/mcp auth serverName
160+
```
161+
162+
#### OAuth Configuration Properties
163+
164+
- **`enabled`** (boolean): Enable OAuth for this server
165+
- **`clientId`** (string): OAuth client identifier (optional with dynamic registration)
166+
- **`clientSecret`** (string): OAuth client secret (optional for public clients)
167+
- **`authorizationUrl`** (string): OAuth authorization endpoint (auto-discovered if omitted)
168+
- **`tokenUrl`** (string): OAuth token endpoint (auto-discovered if omitted)
169+
- **`scopes`** (string[]): Required OAuth scopes
170+
- **`redirectUri`** (string): Custom redirect URI (defaults to `http://localhost:7777/oauth/callback`)
171+
- **`tokenParamName`** (string): Query parameter name for tokens in SSE URLs
172+
173+
#### Token Management
174+
175+
OAuth tokens are automatically:
176+
177+
- **Stored securely** in `~/.gemini/mcp-oauth-tokens.json`
178+
- **Refreshed** when expired (if refresh tokens are available)
179+
- **Validated** before each connection attempt
180+
- **Cleaned up** when invalid or expired
181+
98182
### Example Configurations
99183

100184
#### Python MCP Server (Stdio)

packages/cli/src/gemini.tsx

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { start_sandbox } from './utils/sandbox.js';
1717
import {
1818
LoadedSettings,
1919
loadSettings,
20-
USER_SETTINGS_PATH,
2120
SettingScope,
2221
} from './config/settings.js';
2322
import { themeManager } from './ui/themes/theme-manager.js';
@@ -40,6 +39,7 @@ import {
4039
} from '@google/gemini-cli-core';
4140
import { validateAuthMethod } from './config/auth.js';
4241
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
42+
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
4343

4444
function getNodeMemoryArgs(config: Config): string[] {
4545
const totalMemoryMB = os.totalmem() / (1024 * 1024);
@@ -320,33 +320,8 @@ async function loadNonInteractiveConfig(
320320
await finalConfig.initialize();
321321
}
322322

323-
return await validateNonInterActiveAuth(
323+
return await validateNonInteractiveAuth(
324324
settings.merged.selectedAuthType,
325325
finalConfig,
326326
);
327327
}
328-
329-
async function validateNonInterActiveAuth(
330-
selectedAuthType: AuthType | undefined,
331-
nonInteractiveConfig: Config,
332-
) {
333-
// making a special case for the cli. many headless environments might not have a settings.json set
334-
// so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll
335-
// still expect that exists
336-
if (!selectedAuthType && !process.env.GEMINI_API_KEY) {
337-
console.error(
338-
`Please set an Auth method in your ${USER_SETTINGS_PATH} OR specify GEMINI_API_KEY env variable file before running`,
339-
);
340-
process.exit(1);
341-
}
342-
343-
selectedAuthType = selectedAuthType || AuthType.USE_GEMINI;
344-
const err = validateAuthMethod(selectedAuthType);
345-
if (err != null) {
346-
console.error(err);
347-
process.exit(1);
348-
}
349-
350-
await nonInteractiveConfig.refreshAuth(selectedAuthType);
351-
return nonInteractiveConfig;
352-
}

packages/cli/src/ui/commands/mcpCommand.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
3030
...actual,
3131
getMCPServerStatus: vi.fn(),
3232
getMCPDiscoveryState: vi.fn(),
33+
MCPOAuthProvider: {
34+
authenticate: vi.fn(),
35+
},
36+
MCPOAuthTokenStorage: {
37+
getToken: vi.fn(),
38+
isTokenExpired: vi.fn(),
39+
},
3340
};
3441
});
3542

@@ -810,4 +817,163 @@ describe('mcpCommand', () => {
810817
}
811818
});
812819
});
820+
821+
describe('auth subcommand', () => {
822+
beforeEach(() => {
823+
vi.clearAllMocks();
824+
});
825+
826+
it('should list OAuth-enabled servers when no server name is provided', async () => {
827+
const context = createMockCommandContext({
828+
services: {
829+
config: {
830+
getMcpServers: vi.fn().mockReturnValue({
831+
'oauth-server': { oauth: { enabled: true } },
832+
'regular-server': {},
833+
'another-oauth': { oauth: { enabled: true } },
834+
}),
835+
},
836+
},
837+
});
838+
839+
const authCommand = mcpCommand.subCommands?.find(
840+
(cmd) => cmd.name === 'auth',
841+
);
842+
expect(authCommand).toBeDefined();
843+
844+
const result = await authCommand!.action!(context, '');
845+
expect(isMessageAction(result)).toBe(true);
846+
if (isMessageAction(result)) {
847+
expect(result.messageType).toBe('info');
848+
expect(result.content).toContain('oauth-server');
849+
expect(result.content).toContain('another-oauth');
850+
expect(result.content).not.toContain('regular-server');
851+
expect(result.content).toContain('/mcp auth <server-name>');
852+
}
853+
});
854+
855+
it('should show message when no OAuth servers are configured', async () => {
856+
const context = createMockCommandContext({
857+
services: {
858+
config: {
859+
getMcpServers: vi.fn().mockReturnValue({
860+
'regular-server': {},
861+
}),
862+
},
863+
},
864+
});
865+
866+
const authCommand = mcpCommand.subCommands?.find(
867+
(cmd) => cmd.name === 'auth',
868+
);
869+
const result = await authCommand!.action!(context, '');
870+
871+
expect(isMessageAction(result)).toBe(true);
872+
if (isMessageAction(result)) {
873+
expect(result.messageType).toBe('info');
874+
expect(result.content).toBe(
875+
'No MCP servers configured with OAuth authentication.',
876+
);
877+
}
878+
});
879+
880+
it('should authenticate with a specific server', async () => {
881+
const mockToolRegistry = {
882+
discoverToolsForServer: vi.fn(),
883+
};
884+
const mockGeminiClient = {
885+
setTools: vi.fn(),
886+
};
887+
888+
const context = createMockCommandContext({
889+
services: {
890+
config: {
891+
getMcpServers: vi.fn().mockReturnValue({
892+
'test-server': {
893+
url: 'http://localhost:3000',
894+
oauth: { enabled: true },
895+
},
896+
}),
897+
getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
898+
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
899+
},
900+
},
901+
});
902+
903+
const { MCPOAuthProvider } = await import('@google/gemini-cli-core');
904+
905+
const authCommand = mcpCommand.subCommands?.find(
906+
(cmd) => cmd.name === 'auth',
907+
);
908+
const result = await authCommand!.action!(context, 'test-server');
909+
910+
expect(MCPOAuthProvider.authenticate).toHaveBeenCalledWith(
911+
'test-server',
912+
{ enabled: true },
913+
'http://localhost:3000',
914+
);
915+
expect(mockToolRegistry.discoverToolsForServer).toHaveBeenCalledWith(
916+
'test-server',
917+
);
918+
expect(mockGeminiClient.setTools).toHaveBeenCalled();
919+
920+
expect(isMessageAction(result)).toBe(true);
921+
if (isMessageAction(result)) {
922+
expect(result.messageType).toBe('info');
923+
expect(result.content).toContain('Successfully authenticated');
924+
}
925+
});
926+
927+
it('should handle authentication errors', async () => {
928+
const context = createMockCommandContext({
929+
services: {
930+
config: {
931+
getMcpServers: vi.fn().mockReturnValue({
932+
'test-server': { oauth: { enabled: true } },
933+
}),
934+
},
935+
},
936+
});
937+
938+
const { MCPOAuthProvider } = await import('@google/gemini-cli-core');
939+
(
940+
MCPOAuthProvider.authenticate as ReturnType<typeof vi.fn>
941+
).mockRejectedValue(new Error('Auth failed'));
942+
943+
const authCommand = mcpCommand.subCommands?.find(
944+
(cmd) => cmd.name === 'auth',
945+
);
946+
const result = await authCommand!.action!(context, 'test-server');
947+
948+
expect(isMessageAction(result)).toBe(true);
949+
if (isMessageAction(result)) {
950+
expect(result.messageType).toBe('error');
951+
expect(result.content).toContain('Failed to authenticate');
952+
expect(result.content).toContain('Auth failed');
953+
}
954+
});
955+
956+
it('should handle non-existent server', async () => {
957+
const context = createMockCommandContext({
958+
services: {
959+
config: {
960+
getMcpServers: vi.fn().mockReturnValue({
961+
'existing-server': {},
962+
}),
963+
},
964+
},
965+
});
966+
967+
const authCommand = mcpCommand.subCommands?.find(
968+
(cmd) => cmd.name === 'auth',
969+
);
970+
const result = await authCommand!.action!(context, 'non-existent');
971+
972+
expect(isMessageAction(result)).toBe(true);
973+
if (isMessageAction(result)) {
974+
expect(result.messageType).toBe('error');
975+
expect(result.content).toContain("MCP server 'non-existent' not found");
976+
}
977+
});
978+
});
813979
});

0 commit comments

Comments
 (0)