nitrostack 1.0.22 → 1.0.24

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 (47) hide show
  1. package/dist/cli/commands/dev.d.ts.map +1 -1
  2. package/dist/cli/commands/dev.js +3 -1
  3. package/dist/cli/commands/dev.js.map +1 -1
  4. package/dist/cli/mcp-dev-wrapper.js +2 -1
  5. package/dist/cli/mcp-dev-wrapper.js.map +1 -1
  6. package/dist/core/app-decorator.d.ts.map +1 -1
  7. package/dist/core/app-decorator.js +24 -2
  8. package/dist/core/app-decorator.js.map +1 -1
  9. package/dist/core/di/container.d.ts.map +1 -1
  10. package/dist/core/di/container.js +14 -1
  11. package/dist/core/di/container.js.map +1 -1
  12. package/dist/core/oauth-module.d.ts +15 -42
  13. package/dist/core/oauth-module.d.ts.map +1 -1
  14. package/dist/core/oauth-module.js +130 -5
  15. package/dist/core/oauth-module.js.map +1 -1
  16. package/dist/core/server.d.ts +7 -1
  17. package/dist/core/server.d.ts.map +1 -1
  18. package/dist/core/server.js +99 -23
  19. package/dist/core/server.js.map +1 -1
  20. package/dist/core/transports/discovery-http-server.d.ts +13 -0
  21. package/dist/core/transports/discovery-http-server.d.ts.map +1 -0
  22. package/dist/core/transports/discovery-http-server.js +54 -0
  23. package/dist/core/transports/discovery-http-server.js.map +1 -0
  24. package/dist/core/transports/http-server.d.ts +6 -0
  25. package/dist/core/transports/http-server.d.ts.map +1 -1
  26. package/dist/core/transports/http-server.js +8 -0
  27. package/dist/core/transports/http-server.js.map +1 -1
  28. package/dist/core/transports/streamable-http.d.ts +5 -0
  29. package/dist/core/transports/streamable-http.d.ts.map +1 -1
  30. package/dist/core/transports/streamable-http.js +7 -0
  31. package/dist/core/transports/streamable-http.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/studio/app/api/auth/fetch-metadata/route.ts +2 -2
  34. package/src/studio/app/auth/page.tsx +16 -3
  35. package/src/studio/app/logs/page.tsx +279 -0
  36. package/src/studio/next.config.js +4 -0
  37. package/templates/typescript-auth/package.json +2 -1
  38. package/templates/typescript-auth/src/index.ts +6 -25
  39. package/templates/typescript-auth-api-key/package.json +3 -1
  40. package/templates/typescript-auth-api-key/src/index.ts +6 -26
  41. package/templates/typescript-oauth/.env.example +35 -24
  42. package/templates/typescript-oauth/OAUTH_SETUP.md +306 -120
  43. package/templates/typescript-oauth/README.md +75 -31
  44. package/templates/typescript-oauth/package.json +3 -1
  45. package/templates/typescript-oauth/src/index.ts +6 -27
  46. package/templates/typescript-starter/package.json +2 -1
  47. package/templates/typescript-starter/src/index.ts +6 -25
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { Terminal, Download, Copy, Trash2, Play, Pause, Filter } from 'lucide-react';
5
+
6
+ interface LogEntry {
7
+ timestamp: string;
8
+ level: 'info' | 'error' | 'warn' | 'debug';
9
+ message: string;
10
+ data?: any;
11
+ }
12
+
13
+ export default function LogsPage() {
14
+ const [logs, setLogs] = useState<LogEntry[]>([]);
15
+ const [isStreaming, setIsStreaming] = useState(true);
16
+ const [autoScroll, setAutoScroll] = useState(true);
17
+ const [filter, setFilter] = useState<string>('all');
18
+ const [error, setError] = useState<string | null>(null);
19
+ const logsEndRef = useRef<HTMLDivElement>(null);
20
+ const eventSourceRef = useRef<EventSource | null>(null);
21
+
22
+ // Auto-scroll to bottom
23
+ useEffect(() => {
24
+ if (autoScroll && logsEndRef.current) {
25
+ logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
26
+ }
27
+ }, [logs, autoScroll]);
28
+
29
+ // Connect to SSE stream
30
+ useEffect(() => {
31
+ if (!isStreaming) return;
32
+
33
+ const eventSource = new EventSource('http://localhost:3004/mcp-logs');
34
+ eventSourceRef.current = eventSource;
35
+
36
+ eventSource.onopen = () => {
37
+ setError(null);
38
+ };
39
+
40
+ eventSource.onmessage = (event) => {
41
+ try {
42
+ const { level, message, timestamp, ...data } = JSON.parse(event.data);
43
+ const log: LogEntry = { level, message, timestamp, data };
44
+ setLogs((prev) => [...prev, log]);
45
+ } catch (error) {
46
+ console.error('Failed to parse log:', error);
47
+ }
48
+ };
49
+
50
+ eventSource.onerror = (error) => {
51
+ console.error('SSE error:', error);
52
+ setError('Failed to connect to log stream. Is the MCP server running?');
53
+ // Don't close the event source here, it will try to reconnect automatically
54
+ };
55
+
56
+ return () => {
57
+ eventSource.close();
58
+ };
59
+ }, [isStreaming]);
60
+
61
+ const clearLogs = () => {
62
+ setLogs([]);
63
+ };
64
+
65
+ const downloadLogs = () => {
66
+ const logsText = logs.map(log =>
67
+ `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}${log.data ? '\n' + JSON.stringify(log.data, null, 2) : ''}`
68
+ ).join('\n\n');
69
+
70
+ const blob = new Blob([logsText], { type: 'text/plain' });
71
+ const url = URL.createObjectURL(blob);
72
+ const a = document.createElement('a');
73
+ a.href = url;
74
+ a.download = `nitrostack-logs-${new Date().toISOString()}.txt`;
75
+ a.click();
76
+ URL.revokeObjectURL(url);
77
+ };
78
+
79
+ const copyLogs = async () => {
80
+ const logsText = logs.map(log =>
81
+ `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}${log.data ? '\n' + JSON.stringify(log.data, null, 2) : ''}`
82
+ ).join('\n\n');
83
+
84
+ await navigator.clipboard.writeText(logsText);
85
+ // Could add a toast notification here
86
+ };
87
+
88
+ const toggleStreaming = () => {
89
+ setIsStreaming(!isStreaming);
90
+ };
91
+
92
+ const filteredLogs = filter === 'all'
93
+ ? logs
94
+ : logs.filter(log => log.level === filter);
95
+
96
+ const getLevelColor = (level: string) => {
97
+ switch (level) {
98
+ case 'error': return 'text-red-400';
99
+ case 'warn': return 'text-yellow-400';
100
+ case 'debug': return 'text-blue-400';
101
+ default: return 'text-emerald-400';
102
+ }
103
+ };
104
+
105
+ const getLevelBg = (level: string) => {
106
+ switch (level) {
107
+ case 'error': return 'bg-red-500/10 border-red-500/20';
108
+ case 'warn': return 'bg-yellow-500/10 border-yellow-500/20';
109
+ case 'debug': return 'bg-blue-500/10 border-blue-500/20';
110
+ default: return 'bg-emerald-500/10 border-emerald-500/20';
111
+ }
112
+ };
113
+
114
+ const formatData = (data: any) => {
115
+ try {
116
+ return JSON.stringify(data, null, 2);
117
+ } catch {
118
+ return String(data);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div className="flex flex-col h-screen bg-black">
124
+ {/* Header */}
125
+ <div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-b border-slate-700">
126
+ <div className="flex items-center gap-3">
127
+ <div className="p-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
128
+ <Terminal className="w-5 h-5 text-emerald-400" />
129
+ </div>
130
+ <div>
131
+ <h1 className="text-xl font-bold text-white">Server Logs</h1>
132
+ <p className="text-sm text-slate-400">Real-time MCP server logging</p>
133
+ </div>
134
+ </div>
135
+
136
+ <div className="flex items-center gap-2">
137
+ {/* Filter */}
138
+ <select
139
+ value={filter}
140
+ onChange={(e) => setFilter(e.target.value)}
141
+ className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"
142
+ >
143
+ <option value="all">All Levels</option>
144
+ <option value="info">Info</option>
145
+ <option value="warn">Warnings</option>
146
+ <option value="error">Errors</option>
147
+ <option value="debug">Debug</option>
148
+ </select>
149
+
150
+ {/* Auto-scroll toggle */}
151
+ <button
152
+ onClick={() => setAutoScroll(!autoScroll)}
153
+ className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
154
+ autoScroll
155
+ ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
156
+ : 'bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700'
157
+ }`}
158
+ >
159
+ Auto-scroll
160
+ </button>
161
+
162
+ {/* Streaming toggle */}
163
+ <button
164
+ onClick={toggleStreaming}
165
+ className={`p-2 rounded-lg transition-colors ${
166
+ isStreaming
167
+ ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
168
+ : 'bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700'
169
+ }`}
170
+ title={isStreaming ? 'Pause streaming' : 'Resume streaming'}
171
+ >
172
+ {isStreaming ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
173
+ </button>
174
+
175
+ {/* Copy */}
176
+ <button
177
+ onClick={copyLogs}
178
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
179
+ title="Copy logs"
180
+ >
181
+ <Copy className="w-4 h-4" />
182
+ </button>
183
+
184
+ {/* Download */}
185
+ <button
186
+ onClick={downloadLogs}
187
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
188
+ title="Download logs"
189
+ >
190
+ <Download className="w-4 h-4" />
191
+ </button>
192
+
193
+ {/* Clear */}
194
+ <button
195
+ onClick={clearLogs}
196
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-red-900/50 hover:text-red-400 hover:border-red-500/30 transition-colors"
197
+ title="Clear logs"
198
+ >
199
+ <Trash2 className="w-4 h-4" />
200
+ </button>
201
+ </div>
202
+ </div>
203
+
204
+ {/* Error Display */}
205
+ {error && (
206
+ <div className="bg-red-500/10 text-red-400 px-6 py-3 border-b border-red-500/20 text-sm">
207
+ <strong>Connection Error:</strong> {error}
208
+ </div>
209
+ )}
210
+
211
+ {/* Stats Bar */}
212
+ <div className="flex items-center gap-4 px-6 py-3 bg-slate-900/50 border-b border-slate-800">
213
+ <div className="flex items-center gap-2 text-sm">
214
+ <span className="text-slate-400">Total:</span>
215
+ <span className="font-mono font-bold text-white">{logs.length}</span>
216
+ </div>
217
+ <div className="flex items-center gap-2 text-sm">
218
+ <span className="text-slate-400">Filtered:</span>
219
+ <span className="font-mono font-bold text-white">{filteredLogs.length}</span>
220
+ </div>
221
+ <div className="flex items-center gap-2 text-sm">
222
+ <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
223
+ <span className="text-slate-400">
224
+ {isStreaming ? 'Streaming' : 'Paused'}
225
+ </span>
226
+ </div>
227
+ </div>
228
+
229
+ {/* Logs Container */}
230
+ <div className="flex-1 overflow-y-auto bg-black font-mono text-sm">
231
+ {filteredLogs.length === 0 ? (
232
+ <div className="flex flex-col items-center justify-center h-full text-slate-600">
233
+ <Terminal className="w-16 h-16 mb-4 opacity-20" />
234
+ <p className="text-lg font-medium">No logs yet</p>
235
+ <p className="text-sm">Logs will appear here as they are generated</p>
236
+ </div>
237
+ ) : (
238
+ <div className="p-4 space-y-2">
239
+ {filteredLogs.map((log, index) => (
240
+ <div
241
+ key={index}
242
+ className={`p-3 rounded-lg border ${getLevelBg(log.level)} hover:border-opacity-50 transition-all`}
243
+ >
244
+ <div className="flex items-start gap-3">
245
+ {/* Timestamp */}
246
+ <span className="text-slate-500 text-xs whitespace-nowrap">
247
+ {log.timestamp}
248
+ </span>
249
+
250
+ {/* Level Badge */}
251
+ <span
252
+ className={`px-2 py-0.5 rounded text-xs font-bold uppercase ${getLevelColor(log.level)}`}
253
+ >
254
+ {log.level}
255
+ </span>
256
+
257
+ {/* Message */}
258
+ <div className="flex-1">
259
+ <p className="text-white break-words">{log.message}</p>
260
+
261
+ {/* Data (if present) */}
262
+ {log.data && (
263
+ <pre className="mt-2 p-3 bg-black/50 rounded border border-slate-800 overflow-x-auto">
264
+ <code className="text-xs text-slate-300">
265
+ {formatData(log.data)}
266
+ </code>
267
+ </pre>
268
+ )}
269
+ </div>
270
+ </div>
271
+ </div>
272
+ ))}
273
+ <div ref={logsEndRef} />
274
+ </div>
275
+ )}
276
+ </div>
277
+ </div>
278
+ );
279
+ }
@@ -28,6 +28,10 @@ const nextConfig = {
28
28
  // Do NOT inject custom TS rules; rely on Next's defaults + transpilePackages
29
29
  return config;
30
30
  },
31
+ env: {
32
+ MCP_SERVER_PORT: process.env.MCP_SERVER_PORT,
33
+ MCP_TRANSPORT_TYPE: process.env.MCP_TRANSPORT_TYPE,
34
+ },
31
35
  };
32
36
 
33
37
  export default nextConfig;
@@ -6,7 +6,8 @@
6
6
  "scripts": {
7
7
  "dev": "nitrostack dev",
8
8
  "build": "nitrostack build",
9
- "start": "nitrostack start",
9
+ "start": "npm run build && nitrostack start",
10
+ "start:prod": "nitrostack start",
10
11
  "widget": "npm --prefix src/widgets",
11
12
  "setup-db": "node --loader ts-node/esm src/db/setup.ts"
12
13
  },
@@ -9,39 +9,20 @@
9
9
  * - Production (NODE_ENV=production): Dual transport (STDIO + HTTP SSE)
10
10
  */
11
11
 
12
+ import { config } from 'dotenv';
12
13
  import { McpApplicationFactory } from 'nitrostack';
13
14
  import { AppModule } from './app.module.js';
14
15
 
16
+ // Load environment variables from .env file
17
+ config();
18
+
15
19
  /**
16
20
  * Bootstrap the application
17
21
  */
18
22
  async function bootstrap() {
19
- // Create the MCP server from AppModule
23
+ // Create and start the MCP server
20
24
  const server = await McpApplicationFactory.create(AppModule);
21
-
22
- // Determine transport based on environment
23
- const isDevelopment = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
24
-
25
- if (isDevelopment) {
26
- // Development: STDIO only (for local testing with MCP Inspector, Claude Desktop)
27
- await server.start('stdio');
28
- console.error('🚀 Server running in DEVELOPMENT mode (STDIO only)');
29
- } else {
30
- // Production: Dual transport (STDIO + HTTP SSE)
31
- const port = parseInt(process.env.PORT || '3002');
32
- const host = process.env.HOST || '0.0.0.0';
33
-
34
- await server.start('dual', {
35
- port,
36
- host,
37
- endpoint: '/mcp',
38
- enableCors: process.env.ENABLE_CORS !== 'false', // Enable by default, disable with ENABLE_CORS=false
39
- });
40
-
41
- console.error('🚀 Server running in PRODUCTION mode (DUAL)');
42
- console.error(` 📡 STDIO: Ready for direct connections`);
43
- console.error(` 🌐 HTTP SSE: http://${host}:${port}/mcp`);
44
- }
25
+ await server.start();
45
26
  }
46
27
 
47
28
  // Start the application
@@ -6,10 +6,12 @@
6
6
  "scripts": {
7
7
  "dev": "nitrostack dev",
8
8
  "build": "nitrostack build",
9
- "start": "nitrostack start",
9
+ "start": "npm run build && nitrostack start",
10
+ "start:prod": "nitrostack start",
10
11
  "widget": "npm --prefix src/widgets"
11
12
  },
12
13
  "dependencies": {
14
+ "dotenv": "^16.4.5",
13
15
  "nitrostack": "^1",
14
16
  "zod": "^3.23.8"
15
17
  },
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { config } from 'dotenv';
2
3
  import { McpApplicationFactory } from 'nitrostack';
3
4
  import { AppModule } from './app.module.js';
4
5
 
6
+ // Load environment variables from .env file
7
+ config();
8
+
5
9
  /**
6
10
  * API Key Authentication MCP Server
7
11
  *
@@ -34,32 +38,8 @@ async function bootstrap() {
34
38
  console.error(' - Protected tools require valid API key');
35
39
  console.error(' - Public tools accessible without authentication\n');
36
40
 
37
- // Determine transport based on environment
38
- const isDevelopment = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
39
-
40
- if (isDevelopment) {
41
- // Development: STDIO only (for local testing)
42
- await app.start('stdio');
43
- console.error('🚀 Server running in DEVELOPMENT mode (STDIO only)');
44
- console.error(' Use MCP Inspector or Claude Desktop for testing\n');
45
- } else {
46
- // Production: Dual transport (STDIO + HTTP SSE)
47
- const port = parseInt(process.env.PORT || '3002');
48
- const host = process.env.HOST || '0.0.0.0';
49
-
50
- await app.start('dual', {
51
- port,
52
- host,
53
- endpoint: '/mcp',
54
- enableCors: process.env.ENABLE_CORS !== 'false', // Enable by default, disable with ENABLE_CORS=false
55
- });
56
-
57
- console.error('🚀 Server running in PRODUCTION mode (DUAL)');
58
- console.error(` 📡 STDIO: Ready for direct connections`);
59
- console.error(` 🌐 HTTP SSE: http://${host}:${port}/mcp`);
60
- console.error(' Open NitroStack Studio to test the server');
61
- console.error(' Set your API key in Studio → Auth → API Key section\n');
62
- }
41
+ // Start the server
42
+ await app.start();
63
43
 
64
44
  } catch (error) {
65
45
  console.error('❌ Failed to start server:', error);
@@ -1,59 +1,70 @@
1
1
  # OAuth 2.1 MCP Server Configuration
2
2
 
3
+ # =============================================================================
4
+ # TRANSPORT MODE (AUTO-CONFIGURED)
5
+ # =============================================================================
6
+ # When OAuth is configured, the server automatically runs in DUAL mode:
7
+ # - STDIO: For MCP protocol communication with Studio/Claude
8
+ # - HTTP: For OAuth metadata endpoints (/.well-known/oauth-protected-resource)
9
+ # Both transports run simultaneously on different channels.
10
+
3
11
  # =============================================================================
4
12
  # REQUIRED: Server Configuration
5
13
  # =============================================================================
6
14
 
7
- # Your MCP server's public URL (used for token audience binding - RFC 8707)
8
- # This MUST match the URL where your MCP server is accessible
9
- # Example: https://mcp.yourapp.com or http://localhost:3000 for development
10
- RESOURCE_URI=https://mcp.example.com
15
+ # Your MCP server's resource URI (used for token audience binding - RFC 8707)
16
+ # ⚠️ CRITICAL: This MUST match EXACTLY the "API Identifier" in your OAuth provider
17
+ #
18
+ # For Auth0: Copy the "Identifier" from APIs → Your API → Settings
19
+ # For development: Can be any unique URI like https://mcplocal or http://localhost:3005
20
+ # For production: Use your actual domain like https://api.yourapp.com
21
+ #
22
+ # ⚠️ This value is used for:
23
+ # 1. Token audience validation (security critical!)
24
+ # 2. OAuth discovery metadata
25
+ # 3. The "audience" parameter sent to your OAuth provider
26
+ RESOURCE_URI=https://mcplocal
11
27
 
12
28
  # Your OAuth 2.1 authorization server URL
13
29
  # This is the base URL of your OAuth provider (Auth0, Okta, Keycloak, etc.)
14
30
  # Example for Auth0: https://your-tenant.auth0.com
15
31
  # Example for Okta: https://your-domain.okta.com
16
- AUTH_SERVER_URL=https://auth.example.com
32
+ AUTH_SERVER_URL=https://your-tenant.auth0.com
17
33
 
18
34
  # =============================================================================
19
35
  # OPTIONAL: Token Configuration
20
36
  # =============================================================================
21
37
 
22
38
  # Expected token audience (defaults to RESOURCE_URI if not set)
23
- # This MUST match the audience claim in access tokens
24
- TOKEN_AUDIENCE=https://mcp.example.com
39
+ # ⚠️ This MUST match EXACTLY the RESOURCE_URI above
40
+ TOKEN_AUDIENCE=https://mcplocal
25
41
 
26
42
  # Expected token issuer (recommended for security)
27
43
  # This MUST match the issuer claim in access tokens
28
- # Example for Auth0: https://your-tenant.auth0.com/
44
+ # ⚠️ For Auth0: Add trailing slash! https://your-tenant.auth0.com/
29
45
  # Example for Okta: https://your-domain.okta.com/oauth2/default
30
- TOKEN_ISSUER=https://auth.example.com/
46
+ TOKEN_ISSUER=https://your-tenant.auth0.com/
31
47
 
32
- # =============================================================================
33
- # OPTIONAL: Token Introspection (for opaque tokens)
34
- # =============================================================================
35
48
 
36
- # If your OAuth provider issues opaque tokens (not JWTs), configure these:
37
49
 
38
- # Token introspection endpoint (RFC 7662)
39
- # Example for Auth0: https://your-tenant.auth0.com/oauth/token/introspection
40
- # Example for Okta: https://your-domain.okta.com/oauth2/default/v1/introspect
41
- # INTROSPECTION_ENDPOINT=https://auth.example.com/oauth/introspect
42
50
 
43
- # Client credentials for introspection
44
- # These are separate from your MCP client credentials
45
- # INTROSPECTION_CLIENT_ID=your-introspection-client-id
46
- # INTROSPECTION_CLIENT_SECRET=your-introspection-client-secret
47
51
 
48
52
  # =============================================================================
49
53
  # Provider-Specific Examples
50
54
  # =============================================================================
51
55
 
52
- # --- Auth0 Example ---
53
- # RESOURCE_URI=https://mcp.yourapp.com
56
+ # --- Auth0 Example (RECOMMENDED FOR TESTING) ---
57
+ # Step 1: Create API in Auth0 with Identifier = https://mcplocal
58
+ # Step 2: Create "Regular Web Application" in Auth0
59
+ # Step 3: Authorize the Application to access the API
60
+ # Step 4: Use these settings:
61
+ #
62
+ # RESOURCE_URI=https://mcplocal
54
63
  # AUTH_SERVER_URL=https://your-tenant.auth0.com
55
- # TOKEN_AUDIENCE=https://mcp.yourapp.com
64
+ # TOKEN_AUDIENCE=https://mcplocal
56
65
  # TOKEN_ISSUER=https://your-tenant.auth0.com/
66
+ #
67
+ # ⚠️ CRITICAL: RESOURCE_URI must match Auth0 API Identifier EXACTLY!
57
68
 
58
69
  # --- Okta Example ---
59
70
  # RESOURCE_URI=https://mcp.yourapp.com