Skip to content

Commit c99a17f

Browse files
committed
feat: Add OAuth Authorize dialog
1 parent 92ebedb commit c99a17f

8 files changed

Lines changed: 175 additions & 23 deletions

File tree

tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
interface Props {
1111
open: boolean;
1212
onOpenChange?: (open: boolean) => void;
13+
onServerAdded?: (serverId: string) => void;
1314
}
1415
15-
let { open = $bindable(), onOpenChange }: Props = $props();
16+
let { open = $bindable(), onOpenChange, onServerAdded }: Props = $props();
1617
1718
let newServerUrl = $state('');
1819
let newServerHeaders = $state('');
@@ -53,6 +54,10 @@
5354
5455
conversationsStore.setMcpServerOverride(newServerId, true);
5556
57+
if (newServerOauth) {
58+
onServerAdded?.(newServerId);
59+
}
60+
5661
handleOpenChange(false);
5762
}
5863
</script>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
import { KeyRound } from '@lucide/svelte';
3+
import * as AlertDialog from '$lib/components/ui/alert-dialog';
4+
import type { MCPServerSettingsEntry } from '$lib/types';
5+
import { mcpStore } from '$lib/stores/mcp.svelte';
6+
import { BrowserMcpOAuthProvider } from '$lib/services/mcp-oauth.service';
7+
import { McpServerIdentity } from '$lib/components/app/mcp';
8+
9+
interface Props {
10+
open: boolean;
11+
server?: MCPServerSettingsEntry | null;
12+
onOpenChange?: (open: boolean) => void;
13+
}
14+
15+
let { open = $bindable(), server = null, onOpenChange }: Props = $props();
16+
17+
let displayName = $derived(server ? mcpStore.getServerLabel(server) : 'this server');
18+
let faviconUrl = $derived(server ? mcpStore.getServerFavicon(server.id) : null);
19+
20+
function handleOpenChange(newOpen: boolean) {
21+
open = newOpen;
22+
onOpenChange?.(newOpen);
23+
}
24+
25+
function handleAuthorize() {
26+
if (!server) return;
27+
28+
// Start the OAuth flow by opening a window and beginning authorization
29+
const authorizationWindow = window.open('about:blank', '_blank');
30+
BrowserMcpOAuthProvider.beginInteractiveAuthorization(authorizationWindow);
31+
32+
// Trigger health check to complete the connection after OAuth
33+
mcpStore.runHealthCheck(server);
34+
35+
handleOpenChange(false);
36+
}
37+
</script>
38+
39+
<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
40+
<AlertDialog.Content>
41+
<AlertDialog.Header>
42+
<AlertDialog.Title class="flex items-center gap-2">
43+
<KeyRound class="h-5 w-5 text-primary" />
44+
45+
Authorize MCP Server
46+
</AlertDialog.Title>
47+
48+
<AlertDialog.Description>
49+
Would you like to authorize
50+
<span class="font-medium">
51+
{displayName}
52+
</span>
53+
to connect using OAuth?
54+
</AlertDialog.Description>
55+
</AlertDialog.Header>
56+
57+
{#if server}
58+
<div class="rounded-lg border bg-muted/30 p-3">
59+
<McpServerIdentity
60+
{displayName}
61+
{faviconUrl}
62+
iconClass="h-6 w-6"
63+
iconRounded="rounded"
64+
nameClass="leading-5 text-sm font-medium"
65+
/>
66+
</div>
67+
{/if}
68+
69+
<AlertDialog.Footer>
70+
<AlertDialog.Cancel onclick={() => handleOpenChange(false)}>Cancel</AlertDialog.Cancel>
71+
<AlertDialog.Action onclick={handleAuthorize}>Authorize</AlertDialog.Action>
72+
</AlertDialog.Footer>
73+
</AlertDialog.Content>
74+
</AlertDialog.Root>

tools/ui/src/lib/components/app/dialogs/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,37 @@
1515
*
1616
* Modal dialog for adding a new MCP server with URL and optional headers.
1717
* Validates URL format and integrates with mcpStore and conversationsStore.
18+
* Triggers onServerAdded callback when a new OAuth server is added.
1819
*/
1920
export { default as DialogMcpServerAddNew } from './DialogMcpServerAddNew.svelte';
2021

22+
/**
23+
* **DialogMcpServerAuthorize** - OAuth authorization confirmation dialog
24+
*
25+
* Alert dialog for confirming and initiating OAuth authorization for MCP servers.
26+
* Displays server name and favicon, prompting user to authorize.
27+
*
28+
* **Architecture:**
29+
* - Uses ShadCN AlertDialog
30+
* - Shows server identity (name, favicon) for confirmation
31+
* - Authorize button starts the OAuth flow via BrowserMcpOAuthProvider
32+
*
33+
* **Features:**
34+
* - Server display with icon and name
35+
* - OAuth authorization initiation
36+
* - Health check triggered after authorization
37+
*
38+
* @example
39+
* ```svelte
40+
* <DialogMcpServerAuthorize
41+
* bind:open={showAuthorize}
42+
* {server}
43+
* onOpenChange={(open) => showAuthorize = open}
44+
* />
45+
* ```
46+
*/
47+
export { default as DialogMcpServerAuthorize } from './DialogMcpServerAuthorize.svelte';
48+
2149
/**
2250
* **DialogExportSettings** - Settings export dialog with sensitive data warning
2351
*

tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type { MCPServerSettingsEntry, HealthCheckState } from '$lib/types';
66
import { HealthCheckStatus } from '$lib/enums';
77
import { mcpStore } from '$lib/stores/mcp.svelte';
8-
import { BrowserMcpOAuthProvider } from '$lib/services/mcp-oauth.service';
8+
import { DialogMcpServerAuthorize } from '$lib/components/app/dialogs';
99
import {
1010
McpServerCardActions,
1111
McpServerCardDeleteDialog,
@@ -66,11 +66,10 @@
6666
mcpStore.runHealthCheck(server);
6767
}
6868
69-
function handleAuthorize() {
70-
const authorizationWindow = window.open('about:blank', '_blank');
71-
BrowserMcpOAuthProvider.beginInteractiveAuthorization(authorizationWindow);
69+
let showAuthorizeDialog = $state(false);
7270
73-
mcpStore.runHealthCheck(server);
71+
function handleAuthorize() {
72+
showAuthorizeDialog = true;
7473
}
7574
7675
async function startEditing() {
@@ -208,3 +207,9 @@
208207
onOpenChange={(open) => (showDeleteDialog = open)}
209208
onConfirm={onDelete}
210209
/>
210+
211+
<DialogMcpServerAuthorize
212+
bind:open={showAuthorizeDialog}
213+
{server}
214+
onOpenChange={(open: boolean) => (showAuthorizeDialog = open)}
215+
/>

tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardActions.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
onDelete: () => void;
1212
}
1313
14-
let { isHealthChecking, showAuthorize = false, onEdit, onRefresh, onAuthorize, onDelete }: Props =
15-
$props();
14+
let {
15+
isHealthChecking,
16+
showAuthorize = false,
17+
onEdit,
18+
onRefresh,
19+
onAuthorize,
20+
onDelete
21+
}: Props = $props();
1622
</script>
1723

1824
<div class="flex shrink-0 items-center gap-1">

tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardEditForm.svelte

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
onCancel: () => void;
1212
}
1313
14-
let { serverId, serverUrl, serverUseProxy = false, serverOauth = false, onSave, onCancel }: Props =
15-
$props();
14+
let {
15+
serverId,
16+
serverUrl,
17+
serverUseProxy = false,
18+
serverOauth = false,
19+
onSave,
20+
onCancel
21+
}: Props = $props();
1622
1723
let editUrl = $derived(serverUrl);
1824
let editHeaders = $state('');
@@ -36,7 +42,12 @@
3642
onSave(editUrl.trim(), editHeaders.trim(), editUseProxy, editOauth);
3743
}
3844
39-
export function setInitialValues(url: string, headers: string, useProxy: boolean, oauth: boolean) {
45+
export function setInitialValues(
46+
url: string,
47+
headers: string,
48+
useProxy: boolean,
49+
oauth: boolean
50+
) {
4051
editUrl = url;
4152
editHeaders = headers;
4253
editUseProxy = useProxy;

tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { conversationsStore } from '$lib/stores/conversations.svelte';
66
import { toolsStore } from '$lib/stores/tools.svelte';
77
import { McpServerCard, McpServerCardSkeleton } from '$lib/components/app/mcp';
8-
import { DialogMcpServerAddNew } from '$lib/components/app/dialogs';
8+
import { DialogMcpServerAddNew, DialogMcpServerAuthorize } from '$lib/components/app/dialogs';
99
import { HealthCheckStatus } from '$lib/enums';
1010
import { fade } from 'svelte/transition';
1111
import { onMount } from 'svelte';
@@ -23,6 +23,8 @@
2323
2424
let initialLoadComplete = $state(false);
2525
let isAddingServer = $state(false);
26+
let pendingAuthorizeServerId = $state<string | null>(null);
27+
let showAuthorizeDialog = $derived(initialLoadComplete && pendingAuthorizeServerId);
2628
2729
onMount(() => {
2830
if (page.url.searchParams.has('add')) {
@@ -52,6 +54,14 @@
5254
initialLoadComplete = true;
5355
}
5456
});
57+
58+
let pendingAuthorizeServer = $derived(
59+
pendingAuthorizeServerId ? mcpStore.getServerById(pendingAuthorizeServerId) : null
60+
);
61+
62+
function handleServerAdded(serverId: string) {
63+
pendingAuthorizeServerId = serverId;
64+
}
5565
</script>
5666

5767
<div in:fade={{ duration: 150 }} class="h-full max-h-[100dvh] overflow-y-auto">
@@ -69,7 +79,7 @@
6979
</Button>
7080
</div>
7181

72-
<DialogMcpServerAddNew bind:open={isAddingServer} />
82+
<DialogMcpServerAddNew bind:open={isAddingServer} onServerAdded={handleServerAdded} />
7383

7484
<div class="grid gap-5 md:space-y-4 {className}">
7585
{#if servers.length === 0 && !isAddingServer}
@@ -105,4 +115,16 @@
105115
</div>
106116
{/if}
107117
</div>
118+
119+
<DialogMcpServerAuthorize
120+
bind:open={showAuthorizeDialog}
121+
server={pendingAuthorizeServer}
122+
onOpenChange={(open: boolean) => {
123+
showAuthorizeDialog = open;
124+
125+
if (!open) {
126+
pendingAuthorizeServerId = null;
127+
}
128+
}}
129+
/>
108130
</div>

tools/ui/src/lib/services/mcp-oauth.service.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,19 @@ function readSession(serverId: string): StoredMcpOAuthSession | undefined {
131131
return loadState().sessions[serverId];
132132
}
133133

134-
function writeSession(serverId: string, update: Partial<StoredMcpOAuthSession>): StoredMcpOAuthSession {
134+
function writeSession(
135+
serverId: string,
136+
update: Partial<StoredMcpOAuthSession>
137+
): StoredMcpOAuthSession {
135138
const state = loadState();
136139
const previous = state.sessions[serverId];
137-
const base: StoredMcpOAuthSession =
138-
previous ??
139-
{
140-
serverId,
141-
serverUrl: update.serverUrl ?? '',
142-
redirectUrl: update.redirectUrl ?? getDefaultRedirectUrl(),
143-
returnUrl: update.returnUrl ?? getDefaultReturnUrl(),
144-
createdAt: Date.now()
145-
};
140+
const base: StoredMcpOAuthSession = previous ?? {
141+
serverId,
142+
serverUrl: update.serverUrl ?? '',
143+
redirectUrl: update.redirectUrl ?? getDefaultRedirectUrl(),
144+
returnUrl: update.returnUrl ?? getDefaultReturnUrl(),
145+
createdAt: Date.now()
146+
};
146147
const next: StoredMcpOAuthSession = {
147148
...base,
148149
...update,

0 commit comments

Comments
 (0)