nitrostack 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +123 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +85 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +42 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { Shield, Key, CheckCircle2, XCircle, Lock, ExternalLink, AlertCircle } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export default function AuthPage() {
|
|
9
|
+
const { oauthState, setOAuthState, jwtToken, setJwtToken, apiKey, setApiKey } = useStudioStore();
|
|
10
|
+
const [serverUrl, setServerUrl] = useState('');
|
|
11
|
+
const [clientName, setClientName] = useState('NitroStack Studio');
|
|
12
|
+
const [redirectUri, setRedirectUri] = useState('http://localhost:3000/auth/callback');
|
|
13
|
+
const [manualToken, setManualToken] = useState('');
|
|
14
|
+
const [manualApiKey, setManualApiKey] = useState('');
|
|
15
|
+
const [discovering, setDiscovering] = useState(false);
|
|
16
|
+
const [registering, setRegistering] = useState(false);
|
|
17
|
+
const [manualClientId, setManualClientId] = useState('');
|
|
18
|
+
const [manualClientSecret, setManualClientSecret] = useState('');
|
|
19
|
+
|
|
20
|
+
const handleDiscover = async () => {
|
|
21
|
+
setDiscovering(true);
|
|
22
|
+
try {
|
|
23
|
+
const resourceMetadata = await api.discoverAuth(serverUrl, 'resource');
|
|
24
|
+
const authServerUrl = resourceMetadata.authorization_servers[0];
|
|
25
|
+
const authServerMetadata = await api.discoverAuth(authServerUrl, 'auth-server');
|
|
26
|
+
|
|
27
|
+
setOAuthState({
|
|
28
|
+
authServerUrl: serverUrl,
|
|
29
|
+
resourceMetadata,
|
|
30
|
+
authServerMetadata,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
alert('Discovery successful!');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Discovery failed:', error);
|
|
36
|
+
alert('Discovery failed. See console for details.');
|
|
37
|
+
} finally {
|
|
38
|
+
setDiscovering(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleRegister = async () => {
|
|
43
|
+
if (!oauthState.authServerMetadata?.registration_endpoint) {
|
|
44
|
+
alert('Dynamic registration not supported');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setRegistering(true);
|
|
49
|
+
try {
|
|
50
|
+
const registration = await api.registerClient(
|
|
51
|
+
oauthState.authServerMetadata.registration_endpoint,
|
|
52
|
+
{
|
|
53
|
+
client_name: clientName,
|
|
54
|
+
redirect_uris: [redirectUri],
|
|
55
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
56
|
+
response_types: ['code'],
|
|
57
|
+
scope: oauthState.selectedScopes.join(' '),
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
setOAuthState({ clientRegistration: registration });
|
|
62
|
+
alert('Client registered successfully!');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Registration failed:', error);
|
|
65
|
+
alert('Registration failed. See console for details.');
|
|
66
|
+
} finally {
|
|
67
|
+
setRegistering(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleManualCredentials = () => {
|
|
72
|
+
if (!manualClientId.trim()) {
|
|
73
|
+
alert('Please enter Client ID');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Store manual credentials
|
|
78
|
+
setOAuthState({
|
|
79
|
+
...oauthState,
|
|
80
|
+
clientRegistration: {
|
|
81
|
+
client_id: manualClientId.trim(),
|
|
82
|
+
client_secret: manualClientSecret.trim() || undefined,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
alert('Client credentials saved!');
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleUseManualToken = () => {
|
|
90
|
+
if (!manualToken.trim()) {
|
|
91
|
+
alert('Please enter a token');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setJwtToken(manualToken);
|
|
96
|
+
setManualToken('');
|
|
97
|
+
alert('Token set successfully!');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleUseApiKey = () => {
|
|
101
|
+
if (!manualApiKey.trim()) {
|
|
102
|
+
alert('Please enter an API key');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setApiKey(manualApiKey);
|
|
107
|
+
setManualApiKey('');
|
|
108
|
+
alert('API key set successfully!');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleStartOAuthFlow = async () => {
|
|
112
|
+
if (!oauthState.clientRegistration?.client_id) {
|
|
113
|
+
alert('Please register a client or enter manual credentials first');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!oauthState.authServerMetadata?.authorization_endpoint) {
|
|
118
|
+
alert('Authorization endpoint not found in server metadata');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Generate PKCE challenge
|
|
124
|
+
const codeVerifier = generateCodeVerifier();
|
|
125
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
126
|
+
|
|
127
|
+
// Store verifier for callback
|
|
128
|
+
sessionStorage.setItem('oauth_code_verifier', codeVerifier);
|
|
129
|
+
sessionStorage.setItem('oauth_state', Math.random().toString(36).substring(7));
|
|
130
|
+
|
|
131
|
+
// Build authorization URL
|
|
132
|
+
const authUrl = new URL(oauthState.authServerMetadata.authorization_endpoint);
|
|
133
|
+
authUrl.searchParams.set('client_id', oauthState.clientRegistration.client_id);
|
|
134
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
135
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
136
|
+
authUrl.searchParams.set('scope', oauthState.resourceMetadata?.scopes_supported?.join(' ') || 'openid profile');
|
|
137
|
+
authUrl.searchParams.set('state', sessionStorage.getItem('oauth_state')!);
|
|
138
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
139
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
140
|
+
|
|
141
|
+
// Open authorization URL
|
|
142
|
+
window.location.href = authUrl.toString();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to start OAuth flow:', error);
|
|
145
|
+
alert('Failed to start OAuth flow. See console for details.');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// PKCE helpers
|
|
150
|
+
const generateCodeVerifier = () => {
|
|
151
|
+
const array = new Uint8Array(32);
|
|
152
|
+
crypto.getRandomValues(array);
|
|
153
|
+
return base64UrlEncode(array);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const generateCodeChallenge = async (verifier: string) => {
|
|
157
|
+
const encoder = new TextEncoder();
|
|
158
|
+
const data = encoder.encode(verifier);
|
|
159
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
160
|
+
return base64UrlEncode(new Uint8Array(hash));
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const base64UrlEncode = (buffer: Uint8Array) => {
|
|
164
|
+
const base64 = btoa(String.fromCharCode(...buffer));
|
|
165
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="min-h-screen bg-background p-8">
|
|
170
|
+
{/* Header */}
|
|
171
|
+
<div className="mb-8">
|
|
172
|
+
<div className="flex items-center gap-3 mb-4">
|
|
173
|
+
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-violet-500 to-purple-500 flex items-center justify-center">
|
|
174
|
+
<Shield className="w-6 h-6 text-white" />
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<h1 className="text-3xl font-bold text-foreground">Authentication</h1>
|
|
178
|
+
<p className="text-muted-foreground mt-1">OAuth 2.1 / JWT / API Key authentication</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="max-w-4xl mx-auto space-y-6">
|
|
184
|
+
{/* JWT Token Management */}
|
|
185
|
+
<div className="card card-hover p-6">
|
|
186
|
+
<h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
187
|
+
<Key className="w-5 h-5 text-primary" />
|
|
188
|
+
JWT Token
|
|
189
|
+
</h2>
|
|
190
|
+
|
|
191
|
+
{/* Status Badge */}
|
|
192
|
+
<div className="flex items-center gap-4 mb-6">
|
|
193
|
+
<div className={`w-14 h-14 rounded-xl flex items-center justify-center ${jwtToken ? 'bg-emerald-500/10' : 'bg-rose-500/10'}`}>
|
|
194
|
+
{jwtToken ? (
|
|
195
|
+
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
|
196
|
+
) : (
|
|
197
|
+
<XCircle className="w-8 h-8 text-rose-500" />
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex-1">
|
|
201
|
+
<p className="font-semibold text-foreground">
|
|
202
|
+
{jwtToken ? 'Token Active' : 'No Token Set'}
|
|
203
|
+
</p>
|
|
204
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
205
|
+
{jwtToken
|
|
206
|
+
? 'Token is automatically included in all tool calls and chat requests'
|
|
207
|
+
: 'Set a token manually or login via tools to authenticate'}
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Token Input/Display */}
|
|
213
|
+
<div className="space-y-3">
|
|
214
|
+
<label className="block text-sm font-medium text-foreground">
|
|
215
|
+
Token Value
|
|
216
|
+
{jwtToken && <span className="ml-2 text-xs text-emerald-500">(Currently Active)</span>}
|
|
217
|
+
</label>
|
|
218
|
+
<div className="flex gap-2">
|
|
219
|
+
<input
|
|
220
|
+
type="text"
|
|
221
|
+
value={manualToken || jwtToken || ''}
|
|
222
|
+
onChange={(e) => setManualToken(e.target.value)}
|
|
223
|
+
placeholder="Paste or edit JWT token here..."
|
|
224
|
+
className="input flex-1 font-mono text-sm"
|
|
225
|
+
/>
|
|
226
|
+
<button
|
|
227
|
+
onClick={handleUseManualToken}
|
|
228
|
+
className="btn btn-primary gap-2"
|
|
229
|
+
disabled={!manualToken.trim()}
|
|
230
|
+
>
|
|
231
|
+
<Key className="w-4 h-4" />
|
|
232
|
+
{jwtToken ? 'Update' : 'Set'} Token
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{jwtToken && (
|
|
237
|
+
<div className="flex gap-2">
|
|
238
|
+
<button
|
|
239
|
+
onClick={() => {
|
|
240
|
+
setManualToken(jwtToken);
|
|
241
|
+
alert('Token copied to input for editing');
|
|
242
|
+
}}
|
|
243
|
+
className="btn btn-secondary btn-sm gap-2"
|
|
244
|
+
>
|
|
245
|
+
<Lock className="w-3 h-3" />
|
|
246
|
+
Edit Current Token
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => {
|
|
250
|
+
setJwtToken(null);
|
|
251
|
+
setManualToken('');
|
|
252
|
+
alert('Token cleared');
|
|
253
|
+
}}
|
|
254
|
+
className="btn btn-secondary btn-sm gap-2"
|
|
255
|
+
>
|
|
256
|
+
<XCircle className="w-3 h-3" />
|
|
257
|
+
Clear Token
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Info Box */}
|
|
264
|
+
<div className="mt-4 p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
|
265
|
+
<div className="flex items-start gap-2">
|
|
266
|
+
<AlertCircle className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" />
|
|
267
|
+
<div className="text-sm text-blue-200">
|
|
268
|
+
<p className="font-medium mb-1">How Token Management Works:</p>
|
|
269
|
+
<ul className="list-disc list-inside space-y-1 text-blue-300/80">
|
|
270
|
+
<li>Login via tool execution automatically saves token here</li>
|
|
271
|
+
<li>Login via chat automatically saves token here</li>
|
|
272
|
+
<li>Manually paste token here to use it globally</li>
|
|
273
|
+
<li>Token persists across page refresh</li>
|
|
274
|
+
<li>Token is sent with all subsequent requests</li>
|
|
275
|
+
</ul>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* API Key Management */}
|
|
282
|
+
<div className="card card-hover p-6">
|
|
283
|
+
<h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
284
|
+
<Key className="w-5 h-5 text-primary" />
|
|
285
|
+
API Key
|
|
286
|
+
</h2>
|
|
287
|
+
|
|
288
|
+
{/* Status Badge */}
|
|
289
|
+
<div className="flex items-center gap-4 mb-6">
|
|
290
|
+
<div className={`w-14 h-14 rounded-xl flex items-center justify-center ${apiKey ? 'bg-emerald-500/10' : 'bg-rose-500/10'}`}>
|
|
291
|
+
{apiKey ? (
|
|
292
|
+
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
|
293
|
+
) : (
|
|
294
|
+
<XCircle className="w-8 h-8 text-rose-500" />
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex-1">
|
|
298
|
+
<p className="font-semibold text-foreground">
|
|
299
|
+
{apiKey ? 'API Key Active' : 'No API Key Set'}
|
|
300
|
+
</p>
|
|
301
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
302
|
+
{apiKey
|
|
303
|
+
? 'API key is automatically included in all tool calls and chat requests'
|
|
304
|
+
: 'Set an API key to access protected tools'}
|
|
305
|
+
</p>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{/* API Key Input/Display */}
|
|
310
|
+
<div className="space-y-3">
|
|
311
|
+
<label className="block text-sm font-medium text-foreground">
|
|
312
|
+
API Key Value
|
|
313
|
+
{apiKey && <span className="ml-2 text-xs text-emerald-500">(Currently Active)</span>}
|
|
314
|
+
</label>
|
|
315
|
+
<div className="flex gap-2">
|
|
316
|
+
<input
|
|
317
|
+
type="password"
|
|
318
|
+
value={manualApiKey || apiKey || ''}
|
|
319
|
+
onChange={(e) => setManualApiKey(e.target.value)}
|
|
320
|
+
placeholder="Enter your API key here (e.g., sk_...)..."
|
|
321
|
+
className="input flex-1 font-mono text-sm"
|
|
322
|
+
/>
|
|
323
|
+
<button
|
|
324
|
+
onClick={handleUseApiKey}
|
|
325
|
+
className="btn btn-primary gap-2"
|
|
326
|
+
disabled={!manualApiKey.trim()}
|
|
327
|
+
>
|
|
328
|
+
<Key className="w-4 h-4" />
|
|
329
|
+
{apiKey ? 'Update' : 'Set'} Key
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{apiKey && (
|
|
334
|
+
<div className="flex gap-2">
|
|
335
|
+
<button
|
|
336
|
+
onClick={() => {
|
|
337
|
+
setManualApiKey(apiKey);
|
|
338
|
+
alert('API key copied to input for editing');
|
|
339
|
+
}}
|
|
340
|
+
className="btn btn-secondary btn-sm gap-2"
|
|
341
|
+
>
|
|
342
|
+
<Lock className="w-3 h-3" />
|
|
343
|
+
Edit Current Key
|
|
344
|
+
</button>
|
|
345
|
+
<button
|
|
346
|
+
onClick={() => {
|
|
347
|
+
setApiKey(null);
|
|
348
|
+
setManualApiKey('');
|
|
349
|
+
alert('API key cleared');
|
|
350
|
+
}}
|
|
351
|
+
className="btn btn-secondary btn-sm gap-2"
|
|
352
|
+
>
|
|
353
|
+
<XCircle className="w-3 h-3" />
|
|
354
|
+
Clear Key
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{/* Info Box */}
|
|
361
|
+
<div className="mt-4 p-4 bg-purple-500/10 border border-purple-500/20 rounded-lg">
|
|
362
|
+
<div className="flex items-start gap-2">
|
|
363
|
+
<AlertCircle className="w-5 h-5 text-purple-400 mt-0.5 flex-shrink-0" />
|
|
364
|
+
<div className="text-sm text-purple-200">
|
|
365
|
+
<p className="font-medium mb-1">API Key Authentication:</p>
|
|
366
|
+
<ul className="list-disc list-inside space-y-1 text-purple-300/80">
|
|
367
|
+
<li>API keys are simpler than JWT tokens</li>
|
|
368
|
+
<li>Common format: sk_xxx (secret key) or pk_xxx (public key)</li>
|
|
369
|
+
<li>Keys are sent in X-API-Key header or _meta.apiKey field</li>
|
|
370
|
+
<li>Ideal for service-to-service authentication</li>
|
|
371
|
+
<li>Can be used together with JWT tokens for multi-auth</li>
|
|
372
|
+
</ul>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{/* OAuth 2.1 Flow */}
|
|
379
|
+
<div className="card card-hover p-6">
|
|
380
|
+
<h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
381
|
+
<Shield className="w-5 h-5 text-primary" />
|
|
382
|
+
OAuth 2.1 Flow
|
|
383
|
+
</h2>
|
|
384
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
385
|
+
For OpenAI Apps SDK and OAuth 2.1 compliant servers
|
|
386
|
+
</p>
|
|
387
|
+
|
|
388
|
+
{/* Step 1: Discovery */}
|
|
389
|
+
<div className="mb-6">
|
|
390
|
+
<h3 className="font-medium text-foreground mb-3">1. Discover Server Auth</h3>
|
|
391
|
+
<input
|
|
392
|
+
type="url"
|
|
393
|
+
value={serverUrl}
|
|
394
|
+
onChange={(e) => setServerUrl(e.target.value)}
|
|
395
|
+
placeholder="https://mcp.example.com"
|
|
396
|
+
className="input mb-3"
|
|
397
|
+
/>
|
|
398
|
+
<button
|
|
399
|
+
onClick={handleDiscover}
|
|
400
|
+
className="btn btn-primary gap-2"
|
|
401
|
+
disabled={discovering || !serverUrl}
|
|
402
|
+
>
|
|
403
|
+
<ExternalLink className="w-4 h-4" />
|
|
404
|
+
{discovering ? 'Discovering...' : 'Discover Auth Config'}
|
|
405
|
+
</button>
|
|
406
|
+
|
|
407
|
+
{oauthState.resourceMetadata && (
|
|
408
|
+
<div className="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
409
|
+
<div className="flex items-center gap-2 mb-2">
|
|
410
|
+
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
|
411
|
+
<p className="text-sm font-semibold text-emerald-600 dark:text-emerald-400">Discovery Successful</p>
|
|
412
|
+
</div>
|
|
413
|
+
<details className="text-sm text-muted-foreground">
|
|
414
|
+
<summary className="cursor-pointer hover:text-foreground font-medium">
|
|
415
|
+
View Metadata
|
|
416
|
+
</summary>
|
|
417
|
+
<pre className="mt-3 p-3 bg-background rounded-lg overflow-auto max-h-40 font-mono text-xs text-foreground border border-border">
|
|
418
|
+
{JSON.stringify(oauthState.resourceMetadata, null, 2)}
|
|
419
|
+
</pre>
|
|
420
|
+
</details>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
{/* Step 2a: Manual Client Credentials (Alternative to Registration) */}
|
|
426
|
+
{oauthState.resourceMetadata && !oauthState.clientRegistration && (
|
|
427
|
+
<div className="mb-6 border-t border-border pt-6">
|
|
428
|
+
<h3 className="font-medium text-foreground mb-3">2a. Use Existing Client (Optional)</h3>
|
|
429
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
430
|
+
If you already have a Client ID and Secret from your OAuth provider, enter them here instead of dynamic registration.
|
|
431
|
+
</p>
|
|
432
|
+
<div className="space-y-3 mb-4">
|
|
433
|
+
<input
|
|
434
|
+
type="text"
|
|
435
|
+
value={manualClientId}
|
|
436
|
+
onChange={(e) => setManualClientId(e.target.value)}
|
|
437
|
+
placeholder="Client ID"
|
|
438
|
+
className="input font-mono"
|
|
439
|
+
/>
|
|
440
|
+
<input
|
|
441
|
+
type="password"
|
|
442
|
+
value={manualClientSecret}
|
|
443
|
+
onChange={(e) => setManualClientSecret(e.target.value)}
|
|
444
|
+
placeholder="Client Secret (optional for public clients)"
|
|
445
|
+
className="input font-mono"
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
<button
|
|
449
|
+
onClick={handleManualCredentials}
|
|
450
|
+
className="btn btn-primary gap-2"
|
|
451
|
+
disabled={!manualClientId.trim()}
|
|
452
|
+
>
|
|
453
|
+
<Key className="w-4 h-4" />
|
|
454
|
+
Save Client Credentials
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{/* Step 2b: Registration */}
|
|
460
|
+
{oauthState.authServerMetadata && !oauthState.clientRegistration && (
|
|
461
|
+
<div className="mb-6 border-t border-border pt-6">
|
|
462
|
+
<h3 className="font-medium text-foreground mb-3">2b. Register New Client (Dynamic)</h3>
|
|
463
|
+
<div className="space-y-3 mb-4">
|
|
464
|
+
<input
|
|
465
|
+
type="text"
|
|
466
|
+
value={clientName}
|
|
467
|
+
onChange={(e) => setClientName(e.target.value)}
|
|
468
|
+
placeholder="Client Name"
|
|
469
|
+
className="input"
|
|
470
|
+
/>
|
|
471
|
+
<input
|
|
472
|
+
type="url"
|
|
473
|
+
value={redirectUri}
|
|
474
|
+
onChange={(e) => setRedirectUri(e.target.value)}
|
|
475
|
+
placeholder="Redirect URI"
|
|
476
|
+
className="input"
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
<button
|
|
480
|
+
onClick={handleRegister}
|
|
481
|
+
className="btn btn-primary gap-2"
|
|
482
|
+
disabled={registering}
|
|
483
|
+
>
|
|
484
|
+
<Shield className="w-4 h-4" />
|
|
485
|
+
{registering ? 'Registering...' : 'Register Client'}
|
|
486
|
+
</button>
|
|
487
|
+
|
|
488
|
+
{oauthState.clientRegistration && (
|
|
489
|
+
<div className="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
490
|
+
<div className="flex items-center gap-2 mb-2">
|
|
491
|
+
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
|
492
|
+
<p className="text-sm font-semibold text-emerald-600 dark:text-emerald-400">Registration Successful</p>
|
|
493
|
+
</div>
|
|
494
|
+
<p className="text-sm text-muted-foreground font-mono">
|
|
495
|
+
Client ID: {oauthState.clientRegistration.client_id}
|
|
496
|
+
</p>
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
{/* Step 3: Start Flow */}
|
|
503
|
+
{oauthState.clientRegistration && (
|
|
504
|
+
<div className="border-t border-border pt-6">
|
|
505
|
+
<h3 className="font-medium text-foreground mb-3">3. Start OAuth Flow</h3>
|
|
506
|
+
<button
|
|
507
|
+
onClick={handleStartOAuthFlow}
|
|
508
|
+
className="btn btn-primary gap-2"
|
|
509
|
+
>
|
|
510
|
+
<ExternalLink className="w-4 h-4" />
|
|
511
|
+
Start Authorization Flow
|
|
512
|
+
</button>
|
|
513
|
+
<p className="text-sm text-muted-foreground mt-2 flex items-center gap-1">
|
|
514
|
+
<AlertCircle className="w-4 h-4" />
|
|
515
|
+
This will redirect you to the authorization server for login
|
|
516
|
+
</p>
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* API Keys */}
|
|
522
|
+
<div className="card card-hover p-6">
|
|
523
|
+
<h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
524
|
+
<Key className="w-5 h-5 text-primary" />
|
|
525
|
+
API Keys
|
|
526
|
+
</h2>
|
|
527
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
528
|
+
For servers that use API key authentication
|
|
529
|
+
</p>
|
|
530
|
+
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
|
531
|
+
<div className="flex items-start gap-2">
|
|
532
|
+
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
533
|
+
<p className="text-sm text-foreground">
|
|
534
|
+
API key authentication is typically configured per-tool in the server configuration.
|
|
535
|
+
Refer to your server's documentation for details.
|
|
536
|
+
</p>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|