Skip to content

Commit 7bdfa3f

Browse files
authored
fix: remote mcp doc link and validation (#909)
* fix: callback_port required only for dynamic client registration * fix: oauth label and link to doc * fix: show pkce for oauth and oidc type * leftover
1 parent d1f11f2 commit 7bdfa3f

File tree

6 files changed

+67
-66
lines changed

6 files changed

+67
-66
lines changed

renderer/src/common/lib/form-schema-mcp.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,7 @@ export const createMcpBaseSchema = (workloads: CoreWorkload[]) => {
282282

283283
const remoteMcpOauthConfigSchema = z.object({
284284
authorize_url: z.string().optional(),
285-
callback_port: z
286-
.number()
287-
.optional()
288-
.refine(
289-
(val) => val !== undefined && val !== null,
290-
'Callback port is required'
291-
),
292-
285+
callback_port: z.number().optional(),
293286
client_id: z.string().optional(),
294287
client_secret: z
295288
.object({

renderer/src/common/lib/workloads/remote/form-schema-remote-mcp.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ const OAUTH_VALIDATION_RULES = {
1919
message: 'Client ID is required for OAuth2',
2020
path: ['oauth_config', 'client_id'],
2121
},
22-
{
23-
field: 'client_secret',
24-
message: 'Client Secret is required for OAuth2',
25-
path: ['oauth_config', 'client_secret'],
26-
},
2722
],
2823
oidc: [
2924
{
@@ -49,6 +44,9 @@ const validateClientSecretField = (
4944
): boolean =>
5045
Boolean(value && value.value.secret && value.value.secret.trim() !== '')
5146

47+
const validateCallbackPortField = (value: number | undefined): boolean =>
48+
Boolean(value !== undefined && value !== null && value > 0)
49+
5250
export const getFormSchemaRemoteMcp = (
5351
workloads: CoreWorkload[],
5452
editingServerName?: string
@@ -61,9 +59,19 @@ export const getFormSchemaRemoteMcp = (
6159
(data, ctx) => {
6260
const { auth_type, oauth_config } = data
6361

64-
// Skip validation if no authentication is required
65-
if (auth_type === 'none') return
62+
// Validate callback_port is required when auth_type is 'none'
63+
if (auth_type === 'none') {
64+
if (!validateCallbackPortField(oauth_config?.callback_port)) {
65+
ctx.addIssue({
66+
code: 'custom',
67+
message: 'Callback port is required',
68+
path: ['oauth_config', 'callback_port'],
69+
})
70+
}
71+
return
72+
}
6673

74+
// Validate OAuth/OIDC specific fields
6775
const validationRules =
6876
OAUTH_VALIDATION_RULES[auth_type as keyof typeof OAUTH_VALIDATION_RULES]
6977

renderer/src/features/mcp-servers/components/remote-mcp/dialog-form-remote-mcp.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ export function DialogFormRemoteMcp({
344344
rel="noopener noreferrer"
345345
className="flex cursor-pointer items-center gap-1
346346
underline"
347-
href="https://docs.stacklok.com/toolhive/guides-ui/run-mcp-servers#remote-mcp-server"
347+
href="https://docs.stacklok.com/toolhive/guides-ui/run-mcp-servers#custom-mcp-server"
348348
target="_blank"
349349
>
350350
documentation <ExternalLinkIcon size={12} />
@@ -364,7 +364,7 @@ export function DialogFormRemoteMcp({
364364
<SelectItem value="none">
365365
Dynamic Client Registration
366366
</SelectItem>
367-
<SelectItem value="oauth2">OAuth2</SelectItem>
367+
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
368368
<SelectItem value="oidc">OIDC</SelectItem>
369369
</SelectContent>
370370
</Select>

renderer/src/features/mcp-servers/components/remote-mcp/form-fields-auth.tsx

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,11 @@ import {
77
} from '@/common/components/ui/form'
88
import { Input } from '@/common/components/ui/input'
99
import { TooltipInfoIcon } from '@/common/components/ui/tooltip-info-icon'
10-
import {
11-
Select,
12-
SelectTrigger,
13-
SelectValue,
14-
SelectContent,
15-
SelectItem,
16-
} from '@/common/components/ui/select'
1710
import { type UseFormReturn } from 'react-hook-form'
1811
import { SecretStoreCombobox } from '@/common/components/secrets/secret-store-combobox'
1912
import type { FormSchemaRemoteMcp } from '@/common/lib/workloads/remote/form-schema-remote-mcp'
2013
import { cn } from '@/common/lib/utils'
14+
import { Checkbox } from '@/common/components/ui/checkbox'
2115

2216
function ClientAuthFields({
2317
form,
@@ -90,7 +84,6 @@ function ClientAuthFields({
9084
>
9185
<div>
9286
<FormLabel
93-
required={true}
9487
htmlFor={`oauth_config.client_secret.value`}
9588
className={cn(
9689
`text-muted-foreground !border-input h-full items-center
@@ -222,38 +215,6 @@ export function FormFieldsAuth({
222215
/>
223216

224217
<ClientAuthFields form={form} authType={authType} />
225-
226-
<FormField
227-
control={form.control}
228-
name="oauth_config.use_pkce"
229-
render={({ field }) => (
230-
<FormItem>
231-
<div className="flex items-center gap-1">
232-
<FormLabel htmlFor={field.name}>PKCE</FormLabel>
233-
<TooltipInfoIcon>
234-
Proof Key for Code Exchange (RFC 7636), automatically
235-
enables PKCE flow without client_secret.
236-
</TooltipInfoIcon>
237-
</div>
238-
<FormControl>
239-
<Select
240-
onValueChange={(value) => field.onChange(value === 'true')}
241-
value={field.value ? 'true' : 'false'}
242-
name={field.name}
243-
>
244-
<SelectTrigger id={field.name} className="w-full">
245-
<SelectValue placeholder="Select PKCE" />
246-
</SelectTrigger>
247-
<SelectContent>
248-
<SelectItem value="true">True</SelectItem>
249-
<SelectItem value="false">False</SelectItem>
250-
</SelectContent>
251-
</Select>
252-
</FormControl>
253-
<FormMessage />
254-
</FormItem>
255-
)}
256-
/>
257218
</>
258219
)}
259220

@@ -349,6 +310,35 @@ export function FormFieldsAuth({
349310
/>
350311
</>
351312
)}
313+
314+
{authType !== 'none' && (
315+
<FormField
316+
control={form.control}
317+
name="oauth_config.use_pkce"
318+
render={({ field }) => (
319+
<FormItem>
320+
<div className="flex items-center gap-1">
321+
<FormLabel htmlFor={field.name}>PKCE</FormLabel>
322+
<TooltipInfoIcon>
323+
Proof Key for Code Exchange (RFC 7636), automatically enables
324+
PKCE flow without client_secret.
325+
</TooltipInfoIcon>
326+
</div>
327+
<FormControl>
328+
<Checkbox
329+
id={field.name}
330+
name={field.name}
331+
checked={field.value}
332+
onCheckedChange={(checked) => {
333+
field.onChange(checked)
334+
}}
335+
/>
336+
</FormControl>
337+
<FormMessage />
338+
</FormItem>
339+
)}
340+
/>
341+
)}
352342
</>
353343
)
354344
}

renderer/src/features/registry-servers/components/__tests__/dialog-form-remote-registry-mcp.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ describe('DialogFormRemoteRegistryMcp', () => {
205205

206206
await userEvent.click(
207207
screen.getByRole('option', {
208-
name: /oauth2/i,
208+
name: 'OAuth 2.0',
209209
})
210210
)
211211

@@ -306,7 +306,7 @@ describe('DialogFormRemoteRegistryMcp', () => {
306306

307307
// Select OAuth2 authentication
308308
await userEvent.click(screen.getByLabelText('Authorization method'))
309-
await userEvent.click(screen.getByRole('option', { name: 'OAuth2' }))
309+
await userEvent.click(screen.getByRole('option', { name: 'OAuth 2.0' }))
310310

311311
// OAuth2 fields should now be visible
312312
await waitFor(() => {
@@ -377,7 +377,7 @@ describe('DialogFormRemoteRegistryMcp', () => {
377377

378378
// Select OAuth2 authentication
379379
await userEvent.click(screen.getByLabelText('Authorization method'))
380-
await userEvent.click(screen.getByRole('option', { name: 'OAuth2' }))
380+
await userEvent.click(screen.getByRole('option', { name: 'OAuth 2.0' }))
381381

382382
// Secrets section should be hidden for OAuth2
383383
await waitFor(() => {

renderer/src/features/registry-servers/components/dialog-form-remote-registry-mcp.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
SelectContent,
3636
SelectItem,
3737
} from '@/common/components/ui/select'
38+
import { ExternalLinkIcon } from 'lucide-react'
3839

3940
const DEFAULT_FORM_VALUES: FormSchemaRemoteMcp = {
4041
name: '',
@@ -105,9 +106,7 @@ export function DialogFormRemoteRegistryMcp({
105106
const workloads = data?.workloads ?? []
106107

107108
const form = useForm<FormSchemaRemoteMcp>({
108-
resolver: zodV4Resolver(
109-
getFormSchemaRemoteMcp(workloads, server?.name || undefined)
110-
),
109+
resolver: zodV4Resolver(getFormSchemaRemoteMcp(workloads)),
111110
defaultValues: DEFAULT_FORM_VALUES,
112111
reValidateMode: 'onChange',
113112
mode: 'onChange',
@@ -288,7 +287,16 @@ export function DialogFormRemoteRegistryMcp({
288287
</FormLabel>
289288
<TooltipInfoIcon>
290289
The authorization method the MCP server uses to
291-
authenticate clients.
290+
authenticate clients. Refer to the{' '}
291+
<a
292+
rel="noopener noreferrer"
293+
className="flex cursor-pointer items-center gap-1
294+
underline"
295+
href="https://docs.stacklok.com/toolhive/guides-ui/run-mcp-servers#configure-server"
296+
target="_blank"
297+
>
298+
documentation <ExternalLinkIcon size={12} />
299+
</a>
292300
</TooltipInfoIcon>
293301
</div>
294302
<FormControl>
@@ -301,8 +309,10 @@ export function DialogFormRemoteRegistryMcp({
301309
<SelectValue placeholder="Select authorization method" />
302310
</SelectTrigger>
303311
<SelectContent>
304-
<SelectItem value="none">None</SelectItem>
305-
<SelectItem value="oauth2">OAuth2</SelectItem>
312+
<SelectItem value="none">
313+
Dynamic Client Registration
314+
</SelectItem>
315+
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
306316
<SelectItem value="oidc">OIDC</SelectItem>
307317
</SelectContent>
308318
</Select>

0 commit comments

Comments
 (0)