@@ -33,9 +33,12 @@ export const ServerRemote = ({
3333 headers :
3434 userConfig ?. headers ??
3535 ( remote . headers ? Object . fromEntries ( remote . headers . map ( ( ev ) => [ ev . name , ev . value ?? ev . default ?? '' ] ) ) : { } ) ,
36+ variables : remote . variables
37+ ? Object . fromEntries ( Object . entries ( remote . variables ) . map ( ( [ k , v ] ) => [ k , v . value ?? v . default ?? '' ] ) )
38+ : { } ,
3639 } ;
3740
38- // Build a per-remote zod schema so we can mark remote-declared headers as required
41+ // Build a per-remote zod schema so we can mark remote-declared headers and variables as required
3942 const formSchema = useMemo ( ( ) => {
4043 let headersSchema = z . record ( z . string ( ) , z . string ( ) ) ;
4144 if ( remote . headers && remote . headers . length > 0 ) {
@@ -56,8 +59,28 @@ export const ServerRemote = ({
5659 } ) ;
5760 }
5861 }
59- return z . object ( { headers : headersSchema } ) ;
60- } , [ remote . headers ] ) ;
62+ let variablesSchema = z . record ( z . string ( ) , z . string ( ) ) ;
63+ if ( remote . variables ) {
64+ const requiredVarNames = Object . entries ( remote . variables )
65+ . filter ( ( [ _ , v ] ) => v . isRequired )
66+ . map ( ( [ k , _ ] ) => k ) ;
67+ if ( requiredVarNames . length > 0 ) {
68+ variablesSchema = variablesSchema . superRefine ( ( rec : Record < string , unknown > , ctx ) => {
69+ requiredVarNames . forEach ( ( name : string ) => {
70+ const v = rec [ name ] ;
71+ if ( typeof v !== 'string' || v . trim ( ) . length === 0 ) {
72+ ctx . addIssue ( {
73+ code : 'custom' ,
74+ message : `${ name } is required` ,
75+ path : [ name ] ,
76+ } ) ;
77+ }
78+ } ) ;
79+ } ) ;
80+ }
81+ }
82+ return z . object ( { headers : headersSchema , variables : variablesSchema } ) ;
83+ } , [ remote . headers , remote . variables ] ) ;
6184
6285 const form = useForm < z . infer < typeof formSchema > > ( {
6386 resolver : zodResolver ( formSchema ) ,
@@ -68,10 +91,20 @@ export const ServerRemote = ({
6891 const watchedValues = useWatch ( { control : form . control } ) as z . infer < typeof formSchema > | undefined ;
6992 const formValues = useMemo ( ( ) => {
7093 const config : McpIdeConfigRemote = { type : remote . type || '' } ;
71- if ( remote . url ) config . url = remote . url ;
94+ // Resolve URL with variables if present
95+ if ( remote . url ) {
96+ let resolvedUrl = remote . url ;
97+ if ( watchedValues ?. variables && Object . keys ( watchedValues . variables ) . length > 0 ) {
98+ Object . entries ( watchedValues . variables ) . forEach ( ( [ key , value ] ) => {
99+ resolvedUrl = resolvedUrl . replace ( `{${ key } }` , value ) ;
100+ } ) ;
101+ }
102+ config . url = resolvedUrl ;
103+ }
72104 if ( watchedValues ?. headers && Object . keys ( watchedValues . headers ) . length > 0 ) {
73105 config . headers = watchedValues . headers ;
74106 }
107+ // Note: variables are NOT included in the final config, they're only used to resolve the URL
75108 return config ;
76109 } , [ remote . type , remote . url , watchedValues ] ) ;
77110
@@ -99,15 +132,15 @@ export const ServerRemote = ({
99132 < DialogHeader >
100133 < DialogTitle className = "flex gap-2" >
101134 < a
102- href = { remote . url }
135+ href = { formValues . url || remote . url }
103136 target = "_blank"
104137 rel = "noopener noreferrer"
105138 className = "flex gap-2 items-center hover:text-muted-foreground"
106139 >
107140 { getRemoteIcon ( remote ) }
108- { remote . url }
141+ { formValues . url || remote . url }
109142 </ a >
110- < CopyButton content = { remote . url } variant = "outline" size = "sm" />
143+ < CopyButton content = { formValues . url || remote . url || '' } variant = "outline" size = "sm" />
111144 </ DialogTitle >
112145 < DialogDescription className = "mt-2" >
113146 < span > 🚛 Transport:</ span > < code className = "text-primary" > { remote . type } </ code >
@@ -116,6 +149,60 @@ export const ServerRemote = ({
116149 { /* Remote server details */ }
117150 < Form { ...form } >
118151 < form onSubmit = { form . handleSubmit ( onSubmit ) } className = "space-y-4" >
152+ { remote . variables && Object . keys ( remote . variables ) . length > 0 && (
153+ < div >
154+ < span className = "text-muted-foreground" > ⚙️ URL Variables:</ span >
155+ < div className = "mt-2 space-y-2" >
156+ { Object . entries ( remote . variables ) . map ( ( [ varName , varDef ] ) => (
157+ < FormField
158+ key = { varName }
159+ control = { form . control }
160+ name = { `variables.${ varName } ` }
161+ render = { ( { field } ) => (
162+ < div className = "text-xs" >
163+ < div className = "flex items-center gap-2" >
164+ < FormItem className = "flex-1" >
165+ < FormLabel >
166+ < code > { varName } </ code >
167+ { varDef . format && < Badge variant = "outline" > { varDef . format } </ Badge > } { ' ' }
168+ { varDef . isRequired && < span className = "text-red-500" > *</ span > } { ' ' }
169+ { varDef . isSecret && < span > 🔒</ span > }
170+ </ FormLabel >
171+ < FormControl >
172+ { varDef . isSecret ? (
173+ < PasswordInput
174+ required = { varDef . isRequired }
175+ placeholder = { varDef . default ?? varName ?? '' }
176+ { ...field }
177+ />
178+ ) : (
179+ < Input
180+ required = { varDef . isRequired }
181+ placeholder = { varDef . default ?? varName ?? '' }
182+ { ...field }
183+ />
184+ ) }
185+ </ FormControl >
186+ < FormDescription > { varDef . description } </ FormDescription >
187+ < FormMessage />
188+ </ FormItem >
189+ </ div >
190+ { varDef . choices && varDef . choices . length > 0 && (
191+ < div className = "ml-4 mt-1 flex flex-wrap md:flex-nowrap gap-1" >
192+ { varDef . choices . map ( ( choice : string ) => (
193+ < Badge key = { choice } variant = "secondary" className = "text-xs px-1 py-0" >
194+ { choice }
195+ </ Badge >
196+ ) ) }
197+ </ div >
198+ ) }
199+ </ div >
200+ ) }
201+ />
202+ ) ) }
203+ </ div >
204+ </ div >
205+ ) }
119206 { remote . headers && remote . headers . length > 0 && (
120207 < div >
121208 < span className = "text-muted-foreground" > ⚙️ Headers:</ span >
0 commit comments