44 *--------------------------------------------------------------------------------------------*/
55
66import { Action } from '../../../../base/common/actions.js' ;
7- import { assertNever } from '../../../../base/common/assert.js' ;
7+ import { assertNever , softAssertNever } from '../../../../base/common/assert.js' ;
88import { CancellationToken } from '../../../../base/common/cancellation.js' ;
9+ import { CancellationError } from '../../../../base/common/errors.js' ;
10+ import { MarkdownString } from '../../../../base/common/htmlContent.js' ;
911import { DisposableStore } from '../../../../base/common/lifecycle.js' ;
12+ import { autorun } from '../../../../base/common/observable.js' ;
1013import { isDefined } from '../../../../base/common/types.js' ;
14+ import { URI } from '../../../../base/common/uri.js' ;
1115import { localize } from '../../../../nls.js' ;
1216import { INotificationService , Severity } from '../../../../platform/notification/common/notification.js' ;
17+ import { IOpenerService } from '../../../../platform/opener/common/opener.js' ;
1318import { IQuickInputService , IQuickPick , IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js' ;
1419import { ChatElicitationRequestPart } from '../../chat/browser/chatElicitationRequestPart.js' ;
1520import { ChatModel } from '../../chat/common/chatModel.js' ;
1621import { IChatService } from '../../chat/common/chatService.js' ;
1722import { LocalChatSessionUri } from '../../chat/common/chatUri.js' ;
18- import { IMcpElicitationService , IMcpServer , IMcpToolCallContext } from '../common/mcpTypes.js' ;
23+ import { ElicitationKind , ElicitResult , IFormModeElicitResult , IMcpElicitationService , IMcpServer , IMcpToolCallContext , IUrlModeElicitResult , McpConnectionState , MpcResponseError } from '../common/mcpTypes.js' ;
1924import { mcpServerToSourceData } from '../common/mcpTypesUtils.js' ;
2025import { MCP } from '../common/modelContextProtocol.js' ;
2126
2227const noneItem : IQuickPickItem = { id : undefined , label : localize ( 'mcp.elicit.enum.none' , 'None' ) , description : localize ( 'mcp.elicit.enum.none.description' , 'No selection' ) , alwaysShow : true } ;
2328
29+ function isFormElicitation ( params : MCP . ElicitRequest [ 'params' ] ) : params is MCP . ElicitRequestFormParams {
30+ return params . mode === 'form' ;
31+ }
32+
33+ function isUrlElicitation ( params : MCP . ElicitRequest [ 'params' ] ) : params is MCP . ElicitRequestURLParams {
34+ return params . mode === 'url' ;
35+ }
36+
2437function isLegacyTitledEnumSchema ( schema : MCP . PrimitiveSchemaDefinition ) : schema is MCP . LegacyTitledEnumSchema & { enumNames : string [ ] } {
2538 const cast = schema as MCP . LegacyTitledEnumSchema ;
2639 return cast . type === 'string' && Array . isArray ( cast . enum ) && Array . isArray ( cast . enumNames ) ;
@@ -53,11 +66,23 @@ export class McpElicitationService implements IMcpElicitationService {
5366 @INotificationService private readonly _notificationService : INotificationService ,
5467 @IQuickInputService private readonly _quickInputService : IQuickInputService ,
5568 @IChatService private readonly _chatService : IChatService ,
69+ @IOpenerService private readonly _openerService : IOpenerService ,
5670 ) { }
5771
58- public elicit ( server : IMcpServer , context : IMcpToolCallContext | undefined , elicitation : MCP . ElicitRequest [ 'params' ] , token : CancellationToken ) : Promise < MCP . ElicitResult > {
72+ public elicit ( server : IMcpServer , context : IMcpToolCallContext | undefined , elicitation : MCP . ElicitRequest [ 'params' ] , token : CancellationToken ) : Promise < ElicitResult > {
73+ if ( isFormElicitation ( elicitation ) ) {
74+ return this . _elicitForm ( server , context , elicitation , token ) ;
75+ } else if ( isUrlElicitation ( elicitation ) ) {
76+ return this . _elicitUrl ( server , context , elicitation , token ) ;
77+ } else {
78+ softAssertNever ( elicitation ) ;
79+ return Promise . reject ( new MpcResponseError ( 'Unsupported elicitation type' , MCP . INVALID_PARAMS , undefined ) ) ;
80+ }
81+ }
82+
83+ private async _elicitForm ( server : IMcpServer , context : IMcpToolCallContext | undefined , elicitation : MCP . ElicitRequestFormParams , token : CancellationToken ) : Promise < IFormModeElicitResult > {
5984 const store = new DisposableStore ( ) ;
60- return new Promise < MCP . ElicitResult > ( resolve => {
85+ const value = await new Promise < MCP . ElicitResult > ( resolve => {
6186 const chatModel = context ?. chatSessionId && this . _chatService . getSession ( LocalChatSessionUri . forSession ( context . chatSessionId ) ) ;
6287 if ( chatModel instanceof ChatModel ) {
6388 const request = chatModel . getRequests ( ) . at ( - 1 ) ;
@@ -69,7 +94,7 @@ export class McpElicitationService implements IMcpElicitationService {
6994 localize ( 'mcp.elicit.accept' , 'Respond' ) ,
7095 localize ( 'mcp.elicit.reject' , 'Cancel' ) ,
7196 async ( ) => {
72- const p = this . _doElicit ( elicitation , token ) ;
97+ const p = this . _doElicitForm ( elicitation , token ) ;
7398 resolve ( p ) ;
7499 const result = await p ;
75100 part . state = result . action === 'accept' ? 'accepted' : 'rejected' ;
@@ -90,7 +115,7 @@ export class McpElicitationService implements IMcpElicitationService {
90115 source : localize ( 'mcp.elicit.source' , 'MCP Server ({0})' , server . definition . label ) ,
91116 severity : Severity . Info ,
92117 actions : {
93- primary : [ store . add ( new Action ( 'mcp.elicit.give' , localize ( 'mcp.elicit.give' , 'Respond' ) , undefined , true , ( ) => resolve ( this . _doElicit ( elicitation , token ) ) ) ) ] ,
118+ primary : [ store . add ( new Action ( 'mcp.elicit.give' , localize ( 'mcp.elicit.give' , 'Respond' ) , undefined , true , ( ) => resolve ( this . _doElicitForm ( elicitation , token ) ) ) ) ] ,
94119 secondary : [ store . add ( new Action ( 'mcp.elicit.cancel' , localize ( 'mcp.elicit.cancel' , 'Cancel' ) , undefined , true , ( ) => resolve ( { action : 'decline' } ) ) ) ] ,
95120 }
96121 } ) ;
@@ -99,9 +124,99 @@ export class McpElicitationService implements IMcpElicitationService {
99124 }
100125
101126 } ) . finally ( ( ) => store . dispose ( ) ) ;
127+
128+ return { kind : ElicitationKind . Form , value, dispose : ( ) => { } } ;
129+ }
130+
131+ private async _elicitUrl ( server : IMcpServer , context : IMcpToolCallContext | undefined , elicitation : MCP . ElicitRequestURLParams , token : CancellationToken ) : Promise < IUrlModeElicitResult > {
132+ const promiseStore = new DisposableStore ( ) ;
133+
134+ // We create this ahead of time in case e.g. a user manually opens the URL beforehand
135+ const completePromise = new Promise < void > ( ( resolve , reject ) => {
136+ promiseStore . add ( token . onCancellationRequested ( ( ) => reject ( new CancellationError ( ) ) ) ) ;
137+ promiseStore . add ( autorun ( reader => {
138+ const cnx = server . connection . read ( reader ) ;
139+ const handler = cnx ?. handler . read ( reader ) ;
140+ if ( handler ) {
141+ reader . store . add ( handler . onDidReceiveElicitationCompleteNotification ( e => {
142+ if ( e . params . elicitationId === elicitation . elicitationId ) {
143+ resolve ( ) ;
144+ }
145+ } ) ) ;
146+ } else if ( ! McpConnectionState . isRunning ( server . connectionState . read ( reader ) ) ) {
147+ reject ( new CancellationError ( ) ) ;
148+ }
149+ } ) ) ;
150+ } ) . finally ( ( ) => promiseStore . dispose ( ) ) ;
151+
152+ const store = new DisposableStore ( ) ;
153+ const value = await new Promise < MCP . ElicitResult > ( resolve => {
154+ const chatModel = context ?. chatSessionId && this . _chatService . getSession ( LocalChatSessionUri . forSession ( context . chatSessionId ) ) ;
155+ if ( chatModel instanceof ChatModel ) {
156+ const request = chatModel . getRequests ( ) . at ( - 1 ) ;
157+ if ( request ) {
158+ const part = new ChatElicitationRequestPart (
159+ localize ( 'mcp.elicit.url.title' , 'Authorization Required' ) ,
160+ new MarkdownString ( ) . appendText ( elicitation . message )
161+ . appendMarkdown ( '\n\n' + localize ( 'mcp.elicit.url.instruction' , 'Open this URL?' ) )
162+ . appendCodeblock ( '' , elicitation . url ) ,
163+ localize ( 'msg.subtitle' , "{0} (MCP Server)" , server . definition . label ) ,
164+ localize ( 'mcp.elicit.url.open' , 'Open {0}' , URI . parse ( elicitation . url ) . authority ) ,
165+ localize ( 'mcp.elicit.reject' , 'Cancel' ) ,
166+ async ( ) => {
167+ const result = await this . _doElicitUrl ( elicitation , token ) ;
168+ resolve ( result ) ;
169+ part . state = result . action === 'accept' ? 'accepted' : 'rejected' ;
170+ } ,
171+ ( ) => {
172+ resolve ( { action : 'decline' } ) ;
173+ part . state = 'rejected' ;
174+ return Promise . resolve ( ) ;
175+ } ,
176+ mcpServerToSourceData ( server ) ,
177+ ) ;
178+ chatModel . acceptResponseProgress ( request , part ) ;
179+ }
180+ } else {
181+ const handle = this . _notificationService . notify ( {
182+ message : elicitation . message + ' ' + localize ( 'mcp.elicit.url.instruction2' , 'This will open {0}' , elicitation . url ) ,
183+ source : localize ( 'mcp.elicit.source' , 'MCP Server ({0})' , server . definition . label ) ,
184+ severity : Severity . Info ,
185+ actions : {
186+ primary : [ store . add ( new Action ( 'mcp.elicit.url.open2' , localize ( 'mcp.elicit.url.open2' , 'Open URL' ) , undefined , true , ( ) => resolve ( this . _doElicitUrl ( elicitation , token ) ) ) ) ] ,
187+ secondary : [ store . add ( new Action ( 'mcp.elicit.cancel' , localize ( 'mcp.elicit.cancel' , 'Cancel' ) , undefined , true , ( ) => resolve ( { action : 'decline' } ) ) ) ] ,
188+ }
189+ } ) ;
190+ store . add ( handle . onDidClose ( ( ) => resolve ( { action : 'cancel' } ) ) ) ;
191+ store . add ( token . onCancellationRequested ( ( ) => resolve ( { action : 'cancel' } ) ) ) ;
192+ }
193+ } ) . finally ( ( ) => store . dispose ( ) ) ;
194+
195+ return {
196+ kind : ElicitationKind . URL ,
197+ value,
198+ wait : completePromise ,
199+ dispose : ( ) => promiseStore . dispose ( ) ,
200+ } ;
201+ }
202+
203+ private async _doElicitUrl ( elicitation : MCP . ElicitRequestURLParams , token : CancellationToken ) : Promise < MCP . ElicitResult > {
204+ if ( token . isCancellationRequested ) {
205+ return { action : 'cancel' } ;
206+ }
207+
208+ try {
209+ if ( await this . _openerService . open ( elicitation . url , { allowCommands : false } ) ) {
210+ return { action : 'accept' } ;
211+ }
212+ } catch {
213+ // ignored
214+ }
215+
216+ return { action : 'decline' } ;
102217 }
103218
104- private async _doElicit ( elicitation : MCP . ElicitRequest [ 'params' ] , token : CancellationToken ) : Promise < MCP . ElicitResult > {
219+ private async _doElicitForm ( elicitation : MCP . ElicitRequestFormParams , token : CancellationToken ) : Promise < MCP . ElicitResult > {
105220 const quickPick = this . _quickInputService . createQuickPick < IQuickPickItem > ( ) ;
106221 const store = new DisposableStore ( ) ;
107222
0 commit comments