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.
Files changed (51) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/studio/README.md +140 -0
  6. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  7. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  8. package/src/studio/app/api/chat/route.ts +123 -0
  9. package/src/studio/app/api/health/checks/route.ts +42 -0
  10. package/src/studio/app/api/health/route.ts +13 -0
  11. package/src/studio/app/api/init/route.ts +85 -0
  12. package/src/studio/app/api/ping/route.ts +13 -0
  13. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  14. package/src/studio/app/api/prompts/route.ts +13 -0
  15. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  16. package/src/studio/app/api/resources/route.ts +13 -0
  17. package/src/studio/app/api/roots/route.ts +13 -0
  18. package/src/studio/app/api/sampling/route.ts +14 -0
  19. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  20. package/src/studio/app/api/tools/route.ts +23 -0
  21. package/src/studio/app/api/widget-examples/route.ts +44 -0
  22. package/src/studio/app/auth/callback/page.tsx +160 -0
  23. package/src/studio/app/auth/page.tsx +543 -0
  24. package/src/studio/app/chat/page.tsx +530 -0
  25. package/src/studio/app/chat/page.tsx.backup +390 -0
  26. package/src/studio/app/globals.css +410 -0
  27. package/src/studio/app/health/page.tsx +177 -0
  28. package/src/studio/app/layout.tsx +48 -0
  29. package/src/studio/app/page.tsx +337 -0
  30. package/src/studio/app/page.tsx.backup +346 -0
  31. package/src/studio/app/ping/page.tsx +204 -0
  32. package/src/studio/app/prompts/page.tsx +228 -0
  33. package/src/studio/app/resources/page.tsx +313 -0
  34. package/src/studio/components/EnlargeModal.tsx +116 -0
  35. package/src/studio/components/Sidebar.tsx +133 -0
  36. package/src/studio/components/ToolCard.tsx +108 -0
  37. package/src/studio/components/WidgetRenderer.tsx +99 -0
  38. package/src/studio/lib/api.ts +207 -0
  39. package/src/studio/lib/llm-service.ts +361 -0
  40. package/src/studio/lib/mcp-client.ts +168 -0
  41. package/src/studio/lib/store.ts +192 -0
  42. package/src/studio/lib/theme-provider.tsx +50 -0
  43. package/src/studio/lib/types.ts +107 -0
  44. package/src/studio/lib/widget-loader.ts +90 -0
  45. package/src/studio/middleware.ts +27 -0
  46. package/src/studio/next.config.js +16 -0
  47. package/src/studio/package-lock.json +2696 -0
  48. package/src/studio/package.json +34 -0
  49. package/src/studio/postcss.config.mjs +10 -0
  50. package/src/studio/tailwind.config.ts +67 -0
  51. 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
+ }