11import { ToolNameSchema } from '../../../../schema' ;
2- import { ConfirmReview , Panel , Screen , StepIndicator , TextInput , WizardSelect } from '../../components' ;
2+ import { ConfirmReview , Panel , Screen , SecretInput , StepIndicator , TextInput , WizardSelect } from '../../components' ;
33import type { SelectableItem } from '../../components' ;
44import { HELP_TEXT } from '../../constants' ;
55import { useListNavigation } from '../../hooks' ;
66import { generateUniqueName } from '../../utils' ;
7+ import { useCreateIdentity , useExistingCredentialNames } from '../identity/useCreateIdentity.js' ;
78import type { AddGatewayTargetConfig } from './types' ;
8- import { MCP_TOOL_STEP_LABELS , SKIP_FOR_NOW } from './types' ;
9+ import { MCP_TOOL_STEP_LABELS , OUTBOUND_AUTH_OPTIONS , SKIP_FOR_NOW } from './types' ;
910import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard' ;
1011import { Box , Text } from 'ink' ;
11- import React , { useMemo } from 'react' ;
12+ import React , { useMemo , useState } from 'react' ;
1213
1314interface AddGatewayTargetScreenProps {
1415 existingGateways : string [ ] ;
@@ -24,6 +25,17 @@ export function AddGatewayTargetScreen({
2425 onExit,
2526} : AddGatewayTargetScreenProps ) {
2627 const wizard = useAddGatewayTargetWizard ( existingGateways ) ;
28+ const { names : existingCredentialNames } = useExistingCredentialNames ( ) ;
29+ const { createIdentity } = useCreateIdentity ( ) ;
30+
31+ // Outbound auth sub-step state
32+ const [ outboundAuthType , setOutboundAuthTypeLocal ] = useState < string | null > ( null ) ;
33+ const [ credentialName , setCredentialNameLocal ] = useState < string | null > ( null ) ;
34+ const [ isCreatingCredential , setIsCreatingCredential ] = useState ( false ) ;
35+ const [ oauthSubStep , setOauthSubStep ] = useState < 'name' | 'client-id' | 'client-secret' | 'discovery-url' > ( 'name' ) ;
36+ const [ oauthFields , setOauthFields ] = useState ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
37+ const [ apiKeySubStep , setApiKeySubStep ] = useState < 'name' | 'api-key' > ( 'name' ) ;
38+ const [ apiKeyFields , setApiKeyFields ] = useState ( { name : '' , apiKey : '' } ) ;
2739
2840 const gatewayItems : SelectableItem [ ] = useMemo (
2941 ( ) => [
@@ -33,7 +45,23 @@ export function AddGatewayTargetScreen({
3345 [ existingGateways ]
3446 ) ;
3547
48+ const outboundAuthItems : SelectableItem [ ] = useMemo (
49+ ( ) => OUTBOUND_AUTH_OPTIONS . map ( o => ( { id : o . id , title : o . title , description : o . description } ) ) ,
50+ [ ]
51+ ) ;
52+
53+ const credentialItems : SelectableItem [ ] = useMemo ( ( ) => {
54+ const items : SelectableItem [ ] = [
55+ { id : 'create-new' , title : 'Create new credential' , description : 'Create a new credential inline' } ,
56+ ] ;
57+ existingCredentialNames . forEach ( name => {
58+ items . push ( { id : name , title : name , description : 'Use existing credential' } ) ;
59+ } ) ;
60+ return items ;
61+ } , [ existingCredentialNames ] ) ;
62+
3663 const isGatewayStep = wizard . step === 'gateway' ;
64+ const isOutboundAuthStep = wizard . step === 'outbound-auth' ;
3765 const isTextStep = wizard . step === 'name' || wizard . step === 'endpoint' ;
3866 const isConfirmStep = wizard . step === 'confirm' ;
3967 const noGatewaysAvailable = isGatewayStep && existingGateways . length === 0 ;
@@ -45,16 +73,167 @@ export function AddGatewayTargetScreen({
4573 isActive : isGatewayStep && ! noGatewaysAvailable ,
4674 } ) ;
4775
76+ const outboundAuthNav = useListNavigation ( {
77+ items : outboundAuthItems ,
78+ onSelect : item => {
79+ const authType = item . id as 'OAUTH' | 'API_KEY' | 'NONE' ;
80+ setOutboundAuthTypeLocal ( authType ) ;
81+ if ( authType === 'NONE' ) {
82+ wizard . setOutboundAuth ( { type : 'NONE' } ) ;
83+ }
84+ } ,
85+ onExit : ( ) => wizard . goBack ( ) ,
86+ isActive : isOutboundAuthStep && ! outboundAuthType ,
87+ } ) ;
88+
89+ const credentialNav = useListNavigation ( {
90+ items : credentialItems ,
91+ onSelect : item => {
92+ if ( item . id === 'create-new' ) {
93+ setIsCreatingCredential ( true ) ;
94+ if ( outboundAuthType === 'OAUTH' ) {
95+ setOauthSubStep ( 'name' ) ;
96+ } else {
97+ setApiKeySubStep ( 'name' ) ;
98+ }
99+ } else {
100+ setCredentialNameLocal ( item . id ) ;
101+ wizard . setOutboundAuth ( { type : outboundAuthType as 'OAUTH' | 'API_KEY' , credentialName : item . id } ) ;
102+ }
103+ } ,
104+ onExit : ( ) => {
105+ setOutboundAuthTypeLocal ( null ) ;
106+ setCredentialNameLocal ( null ) ;
107+ setIsCreatingCredential ( false ) ;
108+ } ,
109+ isActive :
110+ isOutboundAuthStep &&
111+ ! ! outboundAuthType &&
112+ outboundAuthType !== 'NONE' &&
113+ ! credentialName &&
114+ ! isCreatingCredential ,
115+ } ) ;
116+
48117 useListNavigation ( {
49118 items : [ { id : 'confirm' , title : 'Confirm' } ] ,
50119 onSelect : ( ) => onComplete ( wizard . config ) ,
51- onExit : ( ) => wizard . goBack ( ) ,
120+ onExit : ( ) => {
121+ setOutboundAuthTypeLocal ( null ) ;
122+ setCredentialNameLocal ( null ) ;
123+ setIsCreatingCredential ( false ) ;
124+ setOauthSubStep ( 'name' ) ;
125+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
126+ setApiKeySubStep ( 'name' ) ;
127+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
128+ wizard . goBack ( ) ;
129+ } ,
52130 isActive : isConfirmStep ,
53131 } ) ;
54132
133+ // OAuth creation handlers
134+ const handleOauthFieldSubmit = ( value : string ) => {
135+ const newFields = { ...oauthFields } ;
136+
137+ if ( oauthSubStep === 'name' ) {
138+ newFields . name = value ;
139+ setOauthFields ( newFields ) ;
140+ setOauthSubStep ( 'client-id' ) ;
141+ } else if ( oauthSubStep === 'client-id' ) {
142+ newFields . clientId = value ;
143+ setOauthFields ( newFields ) ;
144+ setOauthSubStep ( 'client-secret' ) ;
145+ } else if ( oauthSubStep === 'client-secret' ) {
146+ newFields . clientSecret = value ;
147+ setOauthFields ( newFields ) ;
148+ setOauthSubStep ( 'discovery-url' ) ;
149+ } else if ( oauthSubStep === 'discovery-url' ) {
150+ newFields . discoveryUrl = value ;
151+ setOauthFields ( newFields ) ;
152+
153+ // Create the credential
154+ void createIdentity ( {
155+ type : 'OAuthCredentialProvider' ,
156+ name : newFields . name ,
157+ clientId : newFields . clientId ,
158+ clientSecret : newFields . clientSecret ,
159+ discoveryUrl : newFields . discoveryUrl ,
160+ } )
161+ . then ( result => {
162+ if ( result . ok ) {
163+ wizard . setOutboundAuth ( { type : 'OAUTH' , credentialName : newFields . name } ) ;
164+ } else {
165+ setIsCreatingCredential ( false ) ;
166+ setOauthSubStep ( 'name' ) ;
167+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
168+ }
169+ } )
170+ . catch ( ( ) => {
171+ setIsCreatingCredential ( false ) ;
172+ setOauthSubStep ( 'name' ) ;
173+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
174+ } ) ;
175+ }
176+ } ;
177+
178+ const handleOauthFieldCancel = ( ) => {
179+ if ( oauthSubStep === 'name' ) {
180+ setIsCreatingCredential ( false ) ;
181+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
182+ } else if ( oauthSubStep === 'client-id' ) {
183+ setOauthSubStep ( 'name' ) ;
184+ } else if ( oauthSubStep === 'client-secret' ) {
185+ setOauthSubStep ( 'client-id' ) ;
186+ } else if ( oauthSubStep === 'discovery-url' ) {
187+ setOauthSubStep ( 'client-secret' ) ;
188+ }
189+ } ;
190+
191+ // API Key creation handlers
192+ const handleApiKeyFieldSubmit = ( value : string ) => {
193+ const newFields = { ...apiKeyFields } ;
194+
195+ if ( apiKeySubStep === 'name' ) {
196+ newFields . name = value ;
197+ setApiKeyFields ( newFields ) ;
198+ setApiKeySubStep ( 'api-key' ) ;
199+ } else if ( apiKeySubStep === 'api-key' ) {
200+ newFields . apiKey = value ;
201+ setApiKeyFields ( newFields ) ;
202+
203+ void createIdentity ( {
204+ type : 'ApiKeyCredentialProvider' ,
205+ name : newFields . name ,
206+ apiKey : newFields . apiKey ,
207+ } )
208+ . then ( result => {
209+ if ( result . ok ) {
210+ wizard . setOutboundAuth ( { type : 'API_KEY' , credentialName : newFields . name } ) ;
211+ } else {
212+ setIsCreatingCredential ( false ) ;
213+ setApiKeySubStep ( 'name' ) ;
214+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
215+ }
216+ } )
217+ . catch ( ( ) => {
218+ setIsCreatingCredential ( false ) ;
219+ setApiKeySubStep ( 'name' ) ;
220+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
221+ } ) ;
222+ }
223+ } ;
224+
225+ const handleApiKeyFieldCancel = ( ) => {
226+ if ( apiKeySubStep === 'name' ) {
227+ setIsCreatingCredential ( false ) ;
228+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
229+ } else if ( apiKeySubStep === 'api-key' ) {
230+ setApiKeySubStep ( 'name' ) ;
231+ }
232+ } ;
233+
55234 const helpText = isConfirmStep
56235 ? HELP_TEXT . CONFIRM_CANCEL
57- : isTextStep
236+ : isTextStep || isCreatingCredential
58237 ? HELP_TEXT . TEXT_INPUT
59238 : HELP_TEXT . NAVIGATE_SELECT ;
60239
@@ -74,6 +253,107 @@ export function AddGatewayTargetScreen({
74253
75254 { noGatewaysAvailable && < NoGatewaysMessage /> }
76255
256+ { isOutboundAuthStep && ! outboundAuthType && (
257+ < WizardSelect
258+ title = "Select outbound authentication"
259+ description = "How will this tool authenticate to external services?"
260+ items = { outboundAuthItems }
261+ selectedIndex = { outboundAuthNav . selectedIndex }
262+ />
263+ ) }
264+
265+ { isOutboundAuthStep &&
266+ outboundAuthType &&
267+ outboundAuthType !== 'NONE' &&
268+ ! credentialName &&
269+ ! isCreatingCredential && (
270+ < WizardSelect
271+ title = "Select credential"
272+ description = { `Choose a credential for ${ outboundAuthType } authentication` }
273+ items = { credentialItems }
274+ selectedIndex = { credentialNav . selectedIndex }
275+ />
276+ ) }
277+
278+ { isOutboundAuthStep && isCreatingCredential && outboundAuthType === 'OAUTH' && (
279+ < >
280+ { oauthSubStep === 'name' && (
281+ < TextInput
282+ key = "oauth-name"
283+ prompt = "Credential name"
284+ initialValue = { generateUniqueName ( 'MyOAuth' , existingCredentialNames ) }
285+ onSubmit = { handleOauthFieldSubmit }
286+ onCancel = { handleOauthFieldCancel }
287+ customValidation = { value => ! existingCredentialNames . includes ( value ) || 'Credential name already exists' }
288+ />
289+ ) }
290+ { oauthSubStep === 'client-id' && (
291+ < TextInput
292+ key = "oauth-client-id"
293+ prompt = "Client ID"
294+ onSubmit = { handleOauthFieldSubmit }
295+ onCancel = { handleOauthFieldCancel }
296+ customValidation = { value => value . trim ( ) . length > 0 || 'Client ID is required' }
297+ />
298+ ) }
299+ { oauthSubStep === 'client-secret' && (
300+ < SecretInput
301+ key = "oauth-client-secret"
302+ prompt = "Client Secret"
303+ onSubmit = { handleOauthFieldSubmit }
304+ onCancel = { handleOauthFieldCancel }
305+ customValidation = { value => value . trim ( ) . length > 0 || 'Client secret is required' }
306+ revealChars = { 4 }
307+ />
308+ ) }
309+ { oauthSubStep === 'discovery-url' && (
310+ < TextInput
311+ key = "oauth-discovery-url"
312+ prompt = "Discovery URL"
313+ placeholder = "https://example.com/.well-known/openid_configuration"
314+ onSubmit = { handleOauthFieldSubmit }
315+ onCancel = { handleOauthFieldCancel }
316+ customValidation = { value => {
317+ try {
318+ const url = new URL ( value ) ;
319+ if ( url . protocol !== 'http:' && url . protocol !== 'https:' ) {
320+ return 'Discovery URL must use http:// or https:// protocol' ;
321+ }
322+ return true ;
323+ } catch {
324+ return 'Must be a valid URL' ;
325+ }
326+ } }
327+ />
328+ ) }
329+ </ >
330+ ) }
331+
332+ { isOutboundAuthStep && isCreatingCredential && outboundAuthType === 'API_KEY' && (
333+ < >
334+ { apiKeySubStep === 'name' && (
335+ < TextInput
336+ key = "apikey-name"
337+ prompt = "Credential name"
338+ initialValue = { generateUniqueName ( 'MyApiKey' , existingCredentialNames ) }
339+ onSubmit = { handleApiKeyFieldSubmit }
340+ onCancel = { handleApiKeyFieldCancel }
341+ customValidation = { value => ! existingCredentialNames . includes ( value ) || 'Credential name already exists' }
342+ />
343+ ) }
344+ { apiKeySubStep === 'api-key' && (
345+ < SecretInput
346+ key = "apikey-value"
347+ prompt = "API Key"
348+ onSubmit = { handleApiKeyFieldSubmit }
349+ onCancel = { handleApiKeyFieldCancel }
350+ customValidation = { value => value . trim ( ) . length > 0 || 'API key is required' }
351+ revealChars = { 4 }
352+ />
353+ ) }
354+ </ >
355+ ) }
356+
77357 { isTextStep && (
78358 < TextInput
79359 key = { wizard . step }
@@ -107,16 +387,15 @@ export function AddGatewayTargetScreen({
107387 < ConfirmReview
108388 fields = { [
109389 { label : 'Name' , value : wizard . config . name } ,
110- {
111- label : 'Source' ,
112- value : wizard . config . source === 'existing-endpoint' ? 'Existing endpoint' : 'Create new' ,
113- } ,
114390 ...( wizard . config . endpoint ? [ { label : 'Endpoint' , value : wizard . config . endpoint } ] : [ ] ) ,
115- ...( wizard . config . source === 'create-new' ? [ { label : 'Language' , value : wizard . config . language } ] : [ ] ) ,
116391 ...( wizard . config . gateway ? [ { label : 'Gateway' , value : wizard . config . gateway } ] : [ ] ) ,
117392 ...( ! wizard . config . gateway ? [ { label : 'Gateway' , value : '(none - assign later)' } ] : [ ] ) ,
118- ...( wizard . config . source === 'create-new' ? [ { label : 'Host' , value : wizard . config . host } ] : [ ] ) ,
119- ...( wizard . config . source === 'create-new' ? [ { label : 'Source' , value : wizard . config . sourcePath } ] : [ ] ) ,
393+ ...( wizard . config . outboundAuth
394+ ? [
395+ { label : 'Auth Type' , value : wizard . config . outboundAuth . type } ,
396+ { label : 'Credential' , value : wizard . config . outboundAuth . credentialName ?? 'None' } ,
397+ ]
398+ : [ ] ) ,
120399 ] }
121400 />
122401 ) }
0 commit comments