2025-07-15 22:35:05 -04:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-26 00:04:53 +02:00
import type {
2025-07-15 22:35:05 -04:00
SlashCommand ,
SlashCommandActionReturn ,
CommandContext ,
} from './types.js' ;
2025-08-26 00:04:53 +02:00
import { CommandKind } from './types.js' ;
2025-12-12 17:43:43 -08:00
import type { MessageActionReturn } from '@google/gemini-cli-core' ;
2025-07-15 22:35:05 -04:00
import {
DiscoveredMCPTool ,
getMCPDiscoveryState ,
getMCPServerStatus ,
MCPDiscoveryState ,
MCPServerStatus ,
2025-07-22 10:05:36 -04:00
getErrorMessage ,
2025-09-04 16:42:47 -04:00
MCPOAuthTokenStorage ,
2025-12-02 20:01:33 -05:00
mcpServerRequiresOAuth ,
2025-07-15 22:35:05 -04:00
} from '@google/gemini-cli-core' ;
2025-09-18 00:25:33 +02:00
import { appEvents , AppEvent } from '../../utils/events.js' ;
2025-09-29 14:27:06 -07:00
import { MessageType , type HistoryItemMcpStatus } from '../types.js' ;
2026-01-22 15:38:06 -08:00
import {
McpServerEnablementManager ,
normalizeServerId ,
canLoadServer ,
} from '../../config/mcp/mcpServerEnablement.js' ;
import { loadSettings } from '../../config/settings.js' ;
2025-07-15 22:35:05 -04:00
2025-07-22 10:05:36 -04:00
const authCommand : SlashCommand = {
name : 'auth' ,
description : 'Authenticate with an OAuth-enabled MCP server' ,
kind : CommandKind.BUILT_IN ,
2025-12-08 16:32:39 -05:00
autoExecute : true ,
2025-07-22 10:05:36 -04:00
action : async (
context : CommandContext ,
args : string ,
) : Promise < MessageActionReturn > = > {
const serverName = args . trim ( ) ;
const { config } = context . services ;
if ( ! config ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Config not loaded.' ,
} ;
}
2025-11-04 07:51:18 -08:00
const mcpServers = config . getMcpClientManager ( ) ? . getMcpServers ( ) ? ? { } ;
2025-07-22 10:05:36 -04:00
if ( ! serverName ) {
2025-12-02 20:01:33 -05:00
// List servers that support OAuth from two sources:
// 1. Servers with oauth.enabled in config
// 2. Servers detected as requiring OAuth (returned 401)
const configuredOAuthServers = Object . entries ( mcpServers )
2025-07-22 10:05:36 -04:00
. filter ( ( [ _ , server ] ) = > server . oauth ? . enabled )
. map ( ( [ name , _ ] ) = > name ) ;
2025-12-02 20:01:33 -05:00
const detectedOAuthServers = Array . from (
mcpServerRequiresOAuth . keys ( ) ,
) . filter ( ( name ) = > mcpServers [ name ] ) ; // Only include configured servers
// Combine and deduplicate
const allOAuthServers = [
. . . new Set ( [ . . . configuredOAuthServers , . . . detectedOAuthServers ] ) ,
] ;
if ( allOAuthServers . length === 0 ) {
2025-07-22 10:05:36 -04:00
return {
type : 'message' ,
messageType : 'info' ,
content : 'No MCP servers configured with OAuth authentication.' ,
} ;
}
return {
type : 'message' ,
messageType : 'info' ,
2025-12-02 20:01:33 -05:00
content : ` MCP servers with OAuth authentication: \ n ${ allOAuthServers . map ( ( s ) = > ` - ${ s } ` ) . join ( '\n' ) } \ n \ nUse /mcp auth <server-name> to authenticate. ` ,
2025-07-22 10:05:36 -04:00
} ;
}
const server = mcpServers [ serverName ] ;
if ( ! server ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` MCP server ' ${ serverName } ' not found. ` ,
} ;
}
// Always attempt OAuth authentication, even if not explicitly configured
// The authentication process will discover OAuth requirements automatically
2025-09-18 00:25:33 +02:00
const displayListener = ( message : string ) = > {
2026-01-13 14:15:04 -05:00
context . ui . addItem ( { type : 'info' , text : message } ) ;
2025-09-18 00:25:33 +02:00
} ;
appEvents . on ( AppEvent . OauthDisplayMessage , displayListener ) ;
2025-07-22 10:05:36 -04:00
try {
2026-01-13 14:15:04 -05:00
context . ui . addItem ( {
type : 'info' ,
text : ` Starting OAuth authentication for MCP server ' ${ serverName } '... ` ,
} ) ;
2025-07-22 10:05:36 -04:00
// Import dynamically to avoid circular dependencies
const { MCPOAuthProvider } = await import ( '@google/gemini-cli-core' ) ;
2025-07-25 20:56:33 +00:00
let oauthConfig = server . oauth ;
if ( ! oauthConfig ) {
oauthConfig = { enabled : false } ;
}
2025-07-22 10:05:36 -04:00
const mcpServerUrl = server . httpUrl || server . url ;
2025-09-04 16:42:47 -04:00
const authProvider = new MCPOAuthProvider ( new MCPOAuthTokenStorage ( ) ) ;
2025-09-18 00:25:33 +02:00
await authProvider . authenticate (
serverName ,
oauthConfig ,
mcpServerUrl ,
appEvents ,
) ;
2025-07-22 10:05:36 -04:00
2026-01-13 14:15:04 -05:00
context . ui . addItem ( {
type : 'info' ,
text : ` ✅ Successfully authenticated with MCP server ' ${ serverName } '! ` ,
} ) ;
2025-07-22 10:05:36 -04:00
// Trigger tool re-discovery to pick up authenticated server
2025-11-04 07:51:18 -08:00
const mcpClientManager = config . getMcpClientManager ( ) ;
if ( mcpClientManager ) {
2026-01-13 14:15:04 -05:00
context . ui . addItem ( {
type : 'info' ,
text : ` Restarting MCP server ' ${ serverName } '... ` ,
} ) ;
2025-11-04 07:51:18 -08:00
await mcpClientManager . restartServer ( serverName ) ;
2025-07-22 10:05:36 -04:00
}
// Update the client with the new tools
const geminiClient = config . getGeminiClient ( ) ;
2025-11-04 07:51:18 -08:00
if ( geminiClient ? . isInitialized ( ) ) {
2025-07-22 10:05:36 -04:00
await geminiClient . setTools ( ) ;
}
2025-08-13 19:31:24 +00:00
// Reload the slash commands to reflect the changes.
context . ui . reloadCommands ( ) ;
2025-07-22 10:05:36 -04:00
return {
type : 'message' ,
messageType : 'info' ,
content : ` Successfully authenticated and refreshed tools for ' ${ serverName } '. ` ,
} ;
} catch ( error ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` Failed to authenticate with MCP server ' ${ serverName } ': ${ getErrorMessage ( error ) } ` ,
} ;
2025-09-18 00:25:33 +02:00
} finally {
appEvents . removeListener ( AppEvent . OauthDisplayMessage , displayListener ) ;
2025-07-22 10:05:36 -04:00
}
} ,
completion : async ( context : CommandContext , partialArg : string ) = > {
const { config } = context . services ;
if ( ! config ) return [ ] ;
2025-11-04 07:51:18 -08:00
const mcpServers = config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ;
2025-07-22 10:05:36 -04:00
return Object . keys ( mcpServers ) . filter ( ( name ) = >
name . startsWith ( partialArg ) ,
) ;
} ,
} ;
2025-10-17 12:35:14 -07:00
const listAction = async (
context : CommandContext ,
showDescriptions = false ,
showSchema = false ,
) : Promise < void | MessageActionReturn > = > {
const { config } = context . services ;
if ( ! config ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Config not loaded.' ,
} ;
}
const toolRegistry = config . getToolRegistry ( ) ;
if ( ! toolRegistry ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Could not retrieve tool registry.' ,
} ;
}
2025-11-04 07:51:18 -08:00
const mcpServers = config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ;
2025-10-17 12:35:14 -07:00
const serverNames = Object . keys ( mcpServers ) ;
2025-11-04 07:51:18 -08:00
const blockedMcpServers =
config . getMcpClientManager ( ) ? . getBlockedMcpServers ( ) || [ ] ;
2025-10-17 12:35:14 -07:00
const connectingServers = serverNames . filter (
( name ) = > getMCPServerStatus ( name ) === MCPServerStatus . CONNECTING ,
) ;
const discoveryState = getMCPDiscoveryState ( ) ;
const discoveryInProgress =
discoveryState === MCPDiscoveryState . IN_PROGRESS ||
connectingServers . length > 0 ;
const allTools = toolRegistry . getAllTools ( ) ;
2025-12-12 17:43:43 -08:00
const mcpTools = allTools . filter ( ( tool ) = > tool instanceof DiscoveredMCPTool ) ;
2025-10-17 12:35:14 -07:00
2025-12-16 21:28:18 -08:00
const promptRegistry = config . getPromptRegistry ( ) ;
2025-10-17 12:35:14 -07:00
const mcpPrompts = promptRegistry
. getAllPrompts ( )
. filter (
( prompt ) = >
2025-12-12 17:43:43 -08:00
'serverName' in prompt && serverNames . includes ( prompt . serverName ) ,
) ;
2025-10-17 12:35:14 -07:00
2025-12-09 03:43:12 +01:00
const resourceRegistry = config . getResourceRegistry ( ) ;
const mcpResources = resourceRegistry
. getAllResources ( )
2025-12-12 17:43:43 -08:00
. filter ( ( entry ) = > serverNames . includes ( entry . serverName ) ) ;
2025-12-09 03:43:12 +01:00
2025-10-17 12:35:14 -07:00
const authStatus : HistoryItemMcpStatus [ 'authStatus' ] = { } ;
const tokenStorage = new MCPOAuthTokenStorage ( ) ;
for ( const serverName of serverNames ) {
const server = mcpServers [ serverName ] ;
2025-12-02 20:01:33 -05:00
// Check auth status for servers with oauth.enabled OR detected as requiring OAuth
if ( server . oauth ? . enabled || mcpServerRequiresOAuth . has ( serverName ) ) {
2025-10-17 12:35:14 -07:00
const creds = await tokenStorage . getCredentials ( serverName ) ;
if ( creds ) {
if ( creds . token . expiresAt && creds . token . expiresAt < Date . now ( ) ) {
authStatus [ serverName ] = 'expired' ;
2025-09-29 14:27:06 -07:00
} else {
2025-10-17 12:35:14 -07:00
authStatus [ serverName ] = 'authenticated' ;
2025-09-29 14:27:06 -07:00
}
} else {
2025-10-17 12:35:14 -07:00
authStatus [ serverName ] = 'unauthenticated' ;
2025-09-29 14:27:06 -07:00
}
2025-10-17 12:35:14 -07:00
} else {
authStatus [ serverName ] = 'not-configured' ;
2025-09-29 14:27:06 -07:00
}
2025-10-17 12:35:14 -07:00
}
2026-01-22 15:38:06 -08:00
// Get enablement state for all servers
const enablementManager = McpServerEnablementManager . getInstance ( ) ;
const enablementState : HistoryItemMcpStatus [ 'enablementState' ] = { } ;
for ( const serverName of serverNames ) {
enablementState [ serverName ] =
await enablementManager . getDisplayState ( serverName ) ;
}
2025-10-17 12:35:14 -07:00
const mcpStatusItem : HistoryItemMcpStatus = {
type : MessageType . MCP_STATUS ,
servers : mcpServers ,
tools : mcpTools.map ( ( tool ) = > ( {
serverName : tool.serverName ,
name : tool.name ,
description : tool.description ,
schema : tool.schema ,
} ) ) ,
prompts : mcpPrompts.map ( ( prompt ) = > ( {
2025-12-12 17:43:43 -08:00
serverName : prompt.serverName ,
2025-10-17 12:35:14 -07:00
name : prompt.name ,
description : prompt.description ,
} ) ) ,
2025-12-09 03:43:12 +01:00
resources : mcpResources.map ( ( resource ) = > ( {
serverName : resource.serverName ,
name : resource.name ,
uri : resource.uri ,
mimeType : resource.mimeType ,
description : resource.description ,
} ) ) ,
2025-10-17 12:35:14 -07:00
authStatus ,
2026-01-22 15:38:06 -08:00
enablementState ,
2025-10-17 12:35:14 -07:00
blockedServers : blockedMcpServers ,
discoveryInProgress ,
connectingServers ,
showDescriptions ,
showSchema ,
} ;
2026-01-13 14:15:04 -05:00
context . ui . addItem ( mcpStatusItem ) ;
2025-10-17 12:35:14 -07:00
} ;
2025-09-29 14:27:06 -07:00
2025-10-17 12:35:14 -07:00
const listCommand : SlashCommand = {
name : 'list' ,
altNames : [ 'ls' , 'nodesc' , 'nodescription' ] ,
description : 'List configured MCP servers and tools' ,
kind : CommandKind.BUILT_IN ,
2025-12-01 12:29:03 -05:00
autoExecute : true ,
2025-10-17 12:35:14 -07:00
action : ( context ) = > listAction ( context ) ,
} ;
2025-09-29 14:27:06 -07:00
2025-10-17 12:35:14 -07:00
const descCommand : SlashCommand = {
name : 'desc' ,
altNames : [ 'description' ] ,
description : 'List configured MCP servers and tools with descriptions' ,
kind : CommandKind.BUILT_IN ,
2025-12-01 12:29:03 -05:00
autoExecute : true ,
2025-10-17 12:35:14 -07:00
action : ( context ) = > listAction ( context , true ) ,
} ;
const schemaCommand : SlashCommand = {
name : 'schema' ,
description :
'List configured MCP servers and tools with descriptions and schemas' ,
kind : CommandKind.BUILT_IN ,
2025-12-01 12:29:03 -05:00
autoExecute : true ,
2025-10-17 12:35:14 -07:00
action : ( context ) = > listAction ( context , true , true ) ,
2025-07-15 22:35:05 -04:00
} ;
2025-07-22 10:05:36 -04:00
2025-07-25 03:14:45 +02:00
const refreshCommand : SlashCommand = {
name : 'refresh' ,
2025-10-17 13:20:15 -07:00
description : 'Restarts MCP servers' ,
2025-07-25 03:14:45 +02:00
kind : CommandKind.BUILT_IN ,
2025-12-01 12:29:03 -05:00
autoExecute : true ,
2025-07-25 03:14:45 +02:00
action : async (
context : CommandContext ,
2025-09-29 14:27:06 -07:00
) : Promise < void | SlashCommandActionReturn > = > {
2025-07-25 03:14:45 +02:00
const { config } = context . services ;
if ( ! config ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Config not loaded.' ,
} ;
}
2025-11-04 07:51:18 -08:00
const mcpClientManager = config . getMcpClientManager ( ) ;
if ( ! mcpClientManager ) {
2025-07-25 03:14:45 +02:00
return {
type : 'message' ,
messageType : 'error' ,
2025-11-04 07:51:18 -08:00
content : 'Could not retrieve mcp client manager.' ,
2025-07-25 03:14:45 +02:00
} ;
}
2026-01-13 14:15:04 -05:00
context . ui . addItem ( {
type : 'info' ,
text : 'Restarting MCP servers...' ,
} ) ;
2025-07-25 03:14:45 +02:00
2025-11-04 07:51:18 -08:00
await mcpClientManager . restart ( ) ;
2025-07-25 03:14:45 +02:00
// Update the client with the new tools
const geminiClient = config . getGeminiClient ( ) ;
2025-11-04 07:51:18 -08:00
if ( geminiClient ? . isInitialized ( ) ) {
2025-07-25 03:14:45 +02:00
await geminiClient . setTools ( ) ;
}
2025-08-13 19:31:24 +00:00
// Reload the slash commands to reflect the changes.
context . ui . reloadCommands ( ) ;
2025-09-29 14:27:06 -07:00
return listCommand . action ! ( context , '' ) ;
2025-07-25 03:14:45 +02:00
} ,
} ;
2026-01-22 15:38:06 -08:00
async function handleEnableDisable (
context : CommandContext ,
args : string ,
enable : boolean ,
) : Promise < MessageActionReturn > {
const { config } = context . services ;
if ( ! config ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Config not loaded.' ,
} ;
}
const parts = args . trim ( ) . split ( /\s+/ ) ;
const isSession = parts . includes ( '--session' ) ;
const serverName = parts . filter ( ( p ) = > p !== '--session' ) [ 0 ] ;
const action = enable ? 'enable' : 'disable' ;
if ( ! serverName ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` Server name required. Usage: /mcp ${ action } <server-name> [--session] ` ,
} ;
}
const name = normalizeServerId ( serverName ) ;
// Validate server exists
const servers = config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ;
const normalizedServerNames = Object . keys ( servers ) . map ( normalizeServerId ) ;
if ( ! normalizedServerNames . includes ( name ) ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` Server ' ${ serverName } ' not found. Use /mcp list to see available servers. ` ,
} ;
}
// Check if server is from an extension
const serverKey = Object . keys ( servers ) . find (
( key ) = > normalizeServerId ( key ) === name ,
) ;
const server = serverKey ? servers [ serverKey ] : undefined ;
if ( server ? . extension ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` Server ' ${ serverName } ' is provided by extension ' ${ server . extension . name } '. \ nUse '/extensions ${ action } ${ server . extension . name } ' to manage this extension. ` ,
} ;
}
const manager = McpServerEnablementManager . getInstance ( ) ;
if ( enable ) {
const settings = loadSettings ( ) ;
const result = await canLoadServer ( name , {
adminMcpEnabled : settings.merged.admin?.mcp?.enabled ? ? true ,
allowedList : settings.merged.mcp?.allowed ,
excludedList : settings.merged.mcp?.excluded ,
} ) ;
if (
! result . allowed &&
( result . blockType === 'allowlist' || result . blockType === 'excludelist' )
) {
return {
type : 'message' ,
messageType : 'error' ,
content : result.reason ? ? 'Blocked by settings.' ,
} ;
}
if ( isSession ) {
manager . clearSessionDisable ( name ) ;
} else {
await manager . enable ( name ) ;
}
if ( result . blockType === 'admin' ) {
context . ui . addItem (
{
type : 'warning' ,
text : 'MCP disabled by admin. Will load when enabled.' ,
} ,
Date . now ( ) ,
) ;
}
} else {
if ( isSession ) {
manager . disableForSession ( name ) ;
} else {
await manager . disable ( name ) ;
}
}
const msg = ` MCP server ' ${ name } ' ${ enable ? 'enabled' : 'disabled' } ${ isSession ? ' for this session' : '' } . ` ;
const mcpClientManager = config . getMcpClientManager ( ) ;
if ( mcpClientManager ) {
context . ui . addItem (
{ type : 'info' , text : 'Restarting MCP servers...' } ,
Date . now ( ) ,
) ;
await mcpClientManager . restart ( ) ;
}
if ( config . getGeminiClient ( ) ? . isInitialized ( ) )
await config . getGeminiClient ( ) . setTools ( ) ;
context . ui . reloadCommands ( ) ;
return { type : 'message' , messageType : 'info' , content : msg } ;
}
async function getEnablementCompletion (
context : CommandContext ,
partialArg : string ,
showEnabled : boolean ,
) : Promise < string [ ] > {
const { config } = context . services ;
if ( ! config ) return [ ] ;
const servers = Object . keys (
config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ,
) ;
const manager = McpServerEnablementManager . getInstance ( ) ;
const results : string [ ] = [ ] ;
for ( const n of servers ) {
const state = await manager . getDisplayState ( n ) ;
if ( state . enabled === showEnabled && n . startsWith ( partialArg ) ) {
results . push ( n ) ;
}
}
return results ;
}
const enableCommand : SlashCommand = {
name : 'enable' ,
description : 'Enable a disabled MCP server' ,
kind : CommandKind.BUILT_IN ,
autoExecute : true ,
action : ( ctx , args ) = > handleEnableDisable ( ctx , args , true ) ,
completion : ( ctx , arg ) = > getEnablementCompletion ( ctx , arg , false ) ,
} ;
const disableCommand : SlashCommand = {
name : 'disable' ,
description : 'Disable an MCP server' ,
kind : CommandKind.BUILT_IN ,
autoExecute : true ,
action : ( ctx , args ) = > handleEnableDisable ( ctx , args , false ) ,
completion : ( ctx , arg ) = > getEnablementCompletion ( ctx , arg , true ) ,
} ;
2025-07-22 10:05:36 -04:00
export const mcpCommand : SlashCommand = {
name : 'mcp' ,
2025-10-17 12:35:14 -07:00
description : 'Manage configured Model Context Protocol (MCP) servers' ,
2025-07-22 10:05:36 -04:00
kind : CommandKind.BUILT_IN ,
2025-12-01 12:29:03 -05:00
autoExecute : false ,
2025-10-17 12:35:14 -07:00
subCommands : [
listCommand ,
descCommand ,
schemaCommand ,
authCommand ,
refreshCommand ,
2026-01-22 15:38:06 -08:00
enableCommand ,
disableCommand ,
2025-10-17 12:35:14 -07:00
] ,
action : async ( context : CommandContext ) = > listAction ( context ) ,
2025-07-22 10:05:36 -04:00
} ;