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.
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +3 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/mcp-dev-wrapper.js +2 -1
- package/dist/cli/mcp-dev-wrapper.js.map +1 -1
- package/dist/core/app-decorator.d.ts.map +1 -1
- package/dist/core/app-decorator.js +24 -2
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/di/container.d.ts.map +1 -1
- package/dist/core/di/container.js +14 -1
- package/dist/core/di/container.js.map +1 -1
- package/dist/core/oauth-module.d.ts +15 -42
- package/dist/core/oauth-module.d.ts.map +1 -1
- package/dist/core/oauth-module.js +130 -5
- package/dist/core/oauth-module.js.map +1 -1
- package/dist/core/server.d.ts +7 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +99 -23
- package/dist/core/server.js.map +1 -1
- package/dist/core/transports/discovery-http-server.d.ts +13 -0
- package/dist/core/transports/discovery-http-server.d.ts.map +1 -0
- package/dist/core/transports/discovery-http-server.js +54 -0
- package/dist/core/transports/discovery-http-server.js.map +1 -0
- package/dist/core/transports/http-server.d.ts +6 -0
- package/dist/core/transports/http-server.d.ts.map +1 -1
- package/dist/core/transports/http-server.js +8 -0
- package/dist/core/transports/http-server.js.map +1 -1
- package/dist/core/transports/streamable-http.d.ts +5 -0
- package/dist/core/transports/streamable-http.d.ts.map +1 -1
- package/dist/core/transports/streamable-http.js +7 -0
- package/dist/core/transports/streamable-http.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/auth/fetch-metadata/route.ts +2 -2
- package/src/studio/app/auth/page.tsx +16 -3
- package/src/studio/app/logs/page.tsx +279 -0
- package/src/studio/next.config.js +4 -0
- package/templates/typescript-auth/package.json +2 -1
- package/templates/typescript-auth/src/index.ts +6 -25
- package/templates/typescript-auth-api-key/package.json +3 -1
- package/templates/typescript-auth-api-key/src/index.ts +6 -26
- package/templates/typescript-oauth/.env.example +35 -24
- package/templates/typescript-oauth/OAUTH_SETUP.md +306 -120
- package/templates/typescript-oauth/README.md +75 -31
- package/templates/typescript-oauth/package.json +3 -1
- package/templates/typescript-oauth/src/index.ts +6 -27
- package/templates/typescript-starter/package.json +2 -1
- 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
|
|
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
|
-
//
|
|
38
|
-
|
|
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
|
|
8
|
-
# This MUST match the
|
|
9
|
-
#
|
|
10
|
-
|
|
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://
|
|
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
|
|
24
|
-
TOKEN_AUDIENCE=https://
|
|
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
|
-
#
|
|
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://
|
|
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
|
-
#
|
|
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://
|
|
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
|