2025-07-25 20:56:33 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-26 00:04:53 +02:00
import type { Config } from '@google/gemini-cli-core' ;
import { getErrorMessage , getMCPServerPrompts } from '@google/gemini-cli-core' ;
import type {
2025-07-25 20:56:33 +00:00
CommandContext ,
SlashCommand ,
SlashCommandActionReturn ,
} from '../ui/commands/types.js' ;
2025-08-26 00:04:53 +02:00
import { CommandKind } from '../ui/commands/types.js' ;
import type { ICommandLoader } from './types.js' ;
import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js' ;
2025-07-25 20:56:33 +00:00
/ * *
* Discovers and loads executable slash commands from prompts exposed by
* Model - Context - Protocol ( MCP ) servers .
* /
export class McpPromptLoader implements ICommandLoader {
constructor ( private readonly config : Config | null ) { }
/ * *
* Loads all available prompts from all configured MCP servers and adapts
* them into executable SlashCommand objects .
*
* @param _signal An AbortSignal ( unused for this synchronous loader ) .
* @returns A promise that resolves to an array of loaded SlashCommands .
* /
loadCommands ( _signal : AbortSignal ) : Promise < SlashCommand [ ] > {
const promptCommands : SlashCommand [ ] = [ ] ;
if ( ! this . config ) {
return Promise . resolve ( [ ] ) ;
}
2025-11-04 07:51:18 -08:00
const mcpServers = this . config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ;
2025-07-25 20:56:33 +00:00
for ( const serverName in mcpServers ) {
const prompts = getMCPServerPrompts ( this . config , serverName ) || [ ] ;
for ( const prompt of prompts ) {
2025-11-19 09:44:30 -05:00
// Sanitize prompt names to ensure they are valid slash commands (e.g. "Prompt Name" -> "Prompt-Name")
const commandName = ` ${ prompt . name } ` . trim ( ) . replace ( /\s+/g , '-' ) ;
2025-07-25 20:56:33 +00:00
const newPromptCommand : SlashCommand = {
name : commandName ,
description : prompt.description || ` Invoke prompt ${ prompt . name } ` ,
kind : CommandKind.MCP_PROMPT ,
2025-12-04 11:33:45 -05:00
autoExecute : ! prompt . arguments || prompt . arguments . length === 0 ,
2025-07-25 20:56:33 +00:00
subCommands : [
{
name : 'help' ,
description : 'Show help for this prompt' ,
kind : CommandKind.MCP_PROMPT ,
action : async ( ) : Promise < SlashCommandActionReturn > = > {
if ( ! prompt . arguments || prompt . arguments . length === 0 ) {
return {
type : 'message' ,
messageType : 'info' ,
content : ` Prompt " ${ prompt . name } " has no arguments. ` ,
} ;
}
let helpMessage = ` Arguments for " ${ prompt . name } ": \ n \ n ` ;
if ( prompt . arguments && prompt . arguments . length > 0 ) {
helpMessage += ` You can provide arguments by name (e.g., --argName="value") or by position. \ n \ n ` ;
helpMessage += ` e.g., ${ prompt . name } ${ prompt . arguments ? . map ( ( _ ) = > ` "foo" ` ) } is equivalent to ${ prompt . name } ${ prompt . arguments ? . map ( ( arg ) = > ` -- ${ arg . name } ="foo" ` ) } \ n \ n ` ;
}
for ( const arg of prompt . arguments ) {
helpMessage += ` -- ${ arg . name } \ n ` ;
if ( arg . description ) {
helpMessage += ` ${ arg . description } \ n ` ;
}
helpMessage += ` (required: ${
arg . required ? 'yes' : 'no'
} ) \ n \ n ` ;
}
return {
type : 'message' ,
messageType : 'info' ,
content : helpMessage ,
} ;
} ,
} ,
] ,
action : async (
context : CommandContext ,
args : string ,
) : Promise < SlashCommandActionReturn > = > {
if ( ! this . config ) {
return {
type : 'message' ,
messageType : 'error' ,
content : 'Config not loaded.' ,
} ;
}
const promptInputs = this . parseArgs ( args , prompt . arguments ) ;
if ( promptInputs instanceof Error ) {
return {
type : 'message' ,
messageType : 'error' ,
content : promptInputs.message ,
} ;
}
try {
2025-11-04 07:51:18 -08:00
const mcpServers =
this . config . getMcpClientManager ( ) ? . getMcpServers ( ) || { } ;
2025-07-25 20:56:33 +00:00
const mcpServerConfig = mcpServers [ serverName ] ;
if ( ! mcpServerConfig ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` MCP server config not found for ' ${ serverName } '. ` ,
} ;
}
const result = await prompt . invoke ( promptInputs ) ;
2025-08-17 12:43:21 -04:00
if ( result [ 'error' ] ) {
2025-07-25 20:56:33 +00:00
return {
type : 'message' ,
messageType : 'error' ,
2025-08-17 12:43:21 -04:00
content : ` Error invoking prompt: ${ result [ 'error' ] } ` ,
2025-07-25 20:56:33 +00:00
} ;
}
2025-11-19 12:52:27 -08:00
const maybeContent = result . messages ? . [ 0 ] ? . content ;
if ( maybeContent . type !== 'text' ) {
2025-07-25 20:56:33 +00:00
return {
type : 'message' ,
messageType : 'error' ,
content :
'Received an empty or invalid prompt response from the server.' ,
} ;
}
return {
type : 'submit_prompt' ,
2025-11-19 12:52:27 -08:00
content : JSON.stringify ( maybeContent . text ) ,
2025-07-25 20:56:33 +00:00
} ;
} catch ( error ) {
return {
type : 'message' ,
messageType : 'error' ,
content : ` Error: ${ getErrorMessage ( error ) } ` ,
} ;
}
} ,
2025-09-15 10:13:21 -05:00
completion : async (
commandContext : CommandContext ,
partialArg : string ,
) = > {
const invocation = commandContext . invocation ;
if ( ! prompt || ! prompt . arguments || ! invocation ) {
2025-07-25 20:56:33 +00:00
return [ ] ;
}
2025-09-15 10:13:21 -05:00
const indexOfFirstSpace = invocation . raw . indexOf ( ' ' ) + 1 ;
let promptInputs =
indexOfFirstSpace === 0
? { }
: this . parseArgs (
invocation . raw . substring ( indexOfFirstSpace ) ,
prompt . arguments ,
) ;
if ( promptInputs instanceof Error ) {
promptInputs = { } ;
}
2025-07-25 20:56:33 +00:00
2025-09-15 10:13:21 -05:00
const providedArgNames = Object . keys ( promptInputs ) ;
const unusedArguments =
prompt . arguments
. filter ( ( arg ) = > {
// If this arguments is not in the prompt inputs
// add it to unusedArguments
if ( ! providedArgNames . includes ( arg . name ) ) {
return true ;
}
// The parseArgs method assigns the value
// at the end of the prompt as a final value
// The argument should still be suggested
// Example /add --numberOne="34" --num
// numberTwo would be assigned a value of --num
// numberTwo should still be considered unused
const argValue = promptInputs [ arg . name ] ;
return argValue === partialArg ;
} )
. map ( ( argument ) = > ` -- ${ argument . name } =" ` ) || [ ] ;
2025-07-25 20:56:33 +00:00
2025-09-15 10:13:21 -05:00
const exactlyMatchingArgumentAtTheEnd = prompt . arguments
. map ( ( argument ) = > ` -- ${ argument . name } =" ` )
. filter ( ( flagArgument ) = > {
const regex = new RegExp ( ` ${ flagArgument } [^"]* $ ` ) ;
return regex . test ( invocation . raw ) ;
} ) ;
if ( exactlyMatchingArgumentAtTheEnd . length === 1 ) {
if ( exactlyMatchingArgumentAtTheEnd [ 0 ] === partialArg ) {
return [ ` ${ partialArg } " ` ] ;
}
if ( partialArg . endsWith ( '"' ) ) {
return [ partialArg ] ;
2025-07-25 20:56:33 +00:00
}
2025-09-15 10:13:21 -05:00
return [ ` ${ partialArg } " ` ] ;
2025-07-25 20:56:33 +00:00
}
2025-09-15 10:13:21 -05:00
const matchingArguments = unusedArguments . filter ( ( flagArgument ) = >
flagArgument . startsWith ( partialArg ) ,
) ;
return matchingArguments ;
2025-07-25 20:56:33 +00:00
} ,
} ;
promptCommands . push ( newPromptCommand ) ;
}
}
return Promise . resolve ( promptCommands ) ;
}
2025-08-22 10:28:11 -07:00
/ * *
* Parses the ` userArgs ` string representing the prompt arguments ( all the text
* after the command ) into a record matching the shape of the ` promptArgs ` .
*
* @param userArgs
* @param promptArgs
* @returns A record of the parsed arguments
* @visibleForTesting
* /
parseArgs (
2025-07-25 20:56:33 +00:00
userArgs : string ,
promptArgs : PromptArgument [ ] | undefined ,
) : Record < string , unknown > | Error {
const argValues : { [ key : string ] : string } = { } ;
const promptInputs : Record < string , unknown > = { } ;
// arg parsing: --key="value" or --key=value
2025-08-22 10:28:11 -07:00
const namedArgRegex = /--([^=]+)=(?:"((?:\\.|[^"\\])*)"|([^ ]+))/g ;
2025-07-25 20:56:33 +00:00
let match ;
let lastIndex = 0 ;
2025-08-22 10:28:11 -07:00
const positionalParts : string [ ] = [ ] ;
2025-07-25 20:56:33 +00:00
while ( ( match = namedArgRegex . exec ( userArgs ) ) !== null ) {
const key = match [ 1 ] ;
2025-08-22 10:28:11 -07:00
// Extract the quoted or unquoted argument and remove escape chars.
const value = ( match [ 2 ] ? ? match [ 3 ] ) . replace ( /\\(.)/g , '$1' ) ;
2025-07-25 20:56:33 +00:00
argValues [ key ] = value ;
// Capture text between matches as potential positional args
if ( match . index > lastIndex ) {
2025-08-22 10:28:11 -07:00
positionalParts . push ( userArgs . substring ( lastIndex , match . index ) ) ;
2025-07-25 20:56:33 +00:00
}
lastIndex = namedArgRegex . lastIndex ;
}
// Capture any remaining text after the last named arg
if ( lastIndex < userArgs . length ) {
2025-08-22 10:28:11 -07:00
positionalParts . push ( userArgs . substring ( lastIndex ) ) ;
2025-07-25 20:56:33 +00:00
}
2025-08-22 10:28:11 -07:00
const positionalArgsString = positionalParts . join ( '' ) . trim ( ) ;
// extracts either quoted strings or non-quoted sequences of non-space characters.
const positionalArgRegex = /(?:"((?:\\.|[^"\\])*)"|([^ ]+))/g ;
const positionalArgs : string [ ] = [ ] ;
while ( ( match = positionalArgRegex . exec ( positionalArgsString ) ) !== null ) {
// Extract the quoted or unquoted argument and remove escape chars.
positionalArgs . push ( ( match [ 1 ] ? ? match [ 2 ] ) . replace ( /\\(.)/g , '$1' ) ) ;
}
2025-07-25 20:56:33 +00:00
if ( ! promptArgs ) {
return promptInputs ;
}
for ( const arg of promptArgs ) {
if ( argValues [ arg . name ] ) {
promptInputs [ arg . name ] = argValues [ arg . name ] ;
}
}
const unfilledArgs = promptArgs . filter (
( arg ) = > arg . required && ! promptInputs [ arg . name ] ,
) ;
2025-08-22 10:28:11 -07:00
if ( unfilledArgs . length === 1 ) {
// If we have only one unfilled arg, we don't require quotes we just
// join all the given arguments together as if they were quoted.
promptInputs [ unfilledArgs [ 0 ] . name ] = positionalArgs . join ( ' ' ) ;
} else {
const missingArgs : string [ ] = [ ] ;
for ( let i = 0 ; i < unfilledArgs . length ; i ++ ) {
if ( positionalArgs . length > i ) {
promptInputs [ unfilledArgs [ i ] . name ] = positionalArgs [ i ] ;
} else {
missingArgs . push ( unfilledArgs [ i ] . name ) ;
}
}
if ( missingArgs . length > 0 ) {
const missingArgNames = missingArgs
. map ( ( name ) = > ` -- ${ name } ` )
. join ( ', ' ) ;
return new Error ( ` Missing required argument(s): ${ missingArgNames } ` ) ;
2025-07-25 20:56:33 +00:00
}
}
return promptInputs ;
}
}