@wonderwhy-er/desktop-commander 0.2.17 → 0.2.18-alpha.1

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.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ console.log('šŸš€ Starting Desktop Commander with Named Cloudflare Tunnel');
4
+ console.log('');
5
+ // Configuration - can be overridden with environment variables
6
+ const TUNNEL_NAME = process.env.TUNNEL_NAME || 'desktop-commander';
7
+ const TUNNEL_URL = process.env.TUNNEL_URL; // Must be provided if tunnel exists
8
+ const PORT = process.env.PORT || '3000';
9
+ // Check if cloudflared is available
10
+ const checkCloudflared = spawn('which', ['cloudflared']);
11
+ checkCloudflared.on('close', (code) => {
12
+ if (code !== 0) {
13
+ console.error('āŒ cloudflared not found');
14
+ console.error('');
15
+ console.error('šŸ“¦ Install cloudflared:');
16
+ console.error(' macOS: brew install cloudflare/cloudflare/cloudflared');
17
+ console.error(' Linux: https://github.com/cloudflare/cloudflared');
18
+ console.error(' Windows: https://github.com/cloudflare/cloudflared');
19
+ console.error('');
20
+ process.exit(1);
21
+ }
22
+ checkTunnelExists();
23
+ });
24
+ function checkTunnelExists() {
25
+ console.log('šŸ” Checking for existing tunnel...');
26
+ // List existing tunnels
27
+ const listTunnels = spawn('cloudflared', ['tunnel', 'list']);
28
+ let output = '';
29
+ listTunnels.stdout.on('data', (data) => {
30
+ output += data.toString();
31
+ });
32
+ listTunnels.stderr.on('data', (data) => {
33
+ // Ignore stderr for now
34
+ });
35
+ listTunnels.on('close', (code) => {
36
+ if (code !== 0) {
37
+ console.error('āŒ Failed to list tunnels. Make sure you are logged in:');
38
+ console.error(' Run: cloudflared tunnel login');
39
+ console.error('');
40
+ process.exit(1);
41
+ }
42
+ // Check if tunnel exists
43
+ const tunnelExists = output.includes(TUNNEL_NAME);
44
+ if (!tunnelExists) {
45
+ console.log(`āŒ Tunnel "${TUNNEL_NAME}" not found`);
46
+ console.log('');
47
+ console.log('šŸ“ Setup Instructions:');
48
+ console.log('');
49
+ console.log('1ļøāƒ£ Login to Cloudflare:');
50
+ console.log(' cloudflared tunnel login');
51
+ console.log('');
52
+ console.log('2ļøāƒ£ Create the tunnel:');
53
+ console.log(` cloudflared tunnel create ${TUNNEL_NAME}`);
54
+ console.log('');
55
+ console.log('3ļøāƒ£ Get the tunnel credentials:');
56
+ console.log(' - Note the Tunnel ID from the output');
57
+ console.log(` - Find credentials at: ~/.cloudflared/<TUNNEL-ID>.json`);
58
+ console.log('');
59
+ console.log('4ļøāƒ£ Create config file at ~/.cloudflared/config.yml:');
60
+ console.log(' tunnel: <YOUR-TUNNEL-ID>');
61
+ console.log(' credentials-file: ~/.cloudflared/<YOUR-TUNNEL-ID>.json');
62
+ console.log(' ingress:');
63
+ console.log(` - hostname: dc.yourdomain.com # Your domain or use *.trycloudflare.com`);
64
+ console.log(` - service: http://localhost:${PORT}`);
65
+ console.log(' - service: http_status:404');
66
+ console.log('');
67
+ console.log('5ļøāƒ£ (Optional) Route DNS if using custom domain:');
68
+ console.log(` cloudflared tunnel route dns ${TUNNEL_NAME} dc.yourdomain.com`);
69
+ console.log('');
70
+ console.log('6ļøāƒ£ Set your tunnel URL and run:');
71
+ console.log(' export TUNNEL_URL=https://dc.yourdomain.com');
72
+ console.log(` npm run start:named-tunnel`);
73
+ console.log('');
74
+ process.exit(1);
75
+ }
76
+ if (!TUNNEL_URL) {
77
+ console.error('āŒ TUNNEL_URL environment variable not set');
78
+ console.error('');
79
+ console.error('Set your stable tunnel URL:');
80
+ console.error(' export TUNNEL_URL=https://your-tunnel-url.com');
81
+ console.error(` npm run start:named-tunnel`);
82
+ console.error('');
83
+ console.error('Or check your config at ~/.cloudflared/config.yml for the hostname');
84
+ console.error('');
85
+ process.exit(1);
86
+ }
87
+ console.log(`āœ… Found tunnel: ${TUNNEL_NAME}`);
88
+ console.log(` URL: ${TUNNEL_URL}`);
89
+ console.log('');
90
+ startNamedTunnel();
91
+ });
92
+ }
93
+ function startNamedTunnel() {
94
+ // Start the HTTP server first with the known URL
95
+ console.log('šŸ“” Starting Desktop Commander HTTP server...');
96
+ const server = startServer(TUNNEL_URL);
97
+ // Give server a moment to start
98
+ setTimeout(() => {
99
+ console.log('');
100
+ console.log('🌐 Starting named Cloudflare Tunnel...');
101
+ console.log(` Tunnel name: ${TUNNEL_NAME}`);
102
+ console.log(` Public URL: ${TUNNEL_URL}`);
103
+ console.log('');
104
+ const tunnel = spawn('cloudflared', ['tunnel', 'run', TUNNEL_NAME]);
105
+ tunnel.stdout.on('data', (data) => {
106
+ process.stdout.write(data);
107
+ });
108
+ tunnel.stderr.on('data', (data) => {
109
+ process.stderr.write(data);
110
+ });
111
+ tunnel.on('error', (err) => {
112
+ console.error('āŒ Tunnel failed to start:', err);
113
+ console.error('');
114
+ console.error('Troubleshooting:');
115
+ console.error('1. Check config file: ~/.cloudflared/config.yml');
116
+ console.error('2. Verify credentials file exists');
117
+ console.error(`3. Try: cloudflared tunnel info ${TUNNEL_NAME}`);
118
+ console.error('');
119
+ server.kill();
120
+ process.exit(1);
121
+ });
122
+ tunnel.on('close', (code) => {
123
+ console.log('');
124
+ console.log('šŸ”“ Tunnel closed with code:', code);
125
+ server.kill();
126
+ process.exit(code || 0);
127
+ });
128
+ // Cleanup on exit
129
+ process.on('SIGINT', () => {
130
+ console.log('');
131
+ console.log('šŸ›‘ Shutting down...');
132
+ tunnel.kill();
133
+ server.kill();
134
+ process.exit(0);
135
+ });
136
+ process.on('SIGTERM', () => {
137
+ tunnel.kill();
138
+ server.kill();
139
+ process.exit(0);
140
+ });
141
+ }, 1000); // Wait 1 second for server to initialize
142
+ }
143
+ function startServer(baseURL) {
144
+ const server = spawn('node', ['dist/http-server.js'], {
145
+ env: {
146
+ ...process.env,
147
+ BASE_URL: baseURL,
148
+ PORT: PORT
149
+ },
150
+ stdio: ['ignore', 'pipe', 'pipe']
151
+ });
152
+ server.stdout.on('data', (data) => {
153
+ process.stdout.write(data);
154
+ });
155
+ server.stderr.on('data', (data) => {
156
+ process.stderr.write(data);
157
+ });
158
+ server.on('error', (err) => {
159
+ console.error('āŒ Server failed to start:', err);
160
+ process.exit(1);
161
+ });
162
+ server.on('close', (code) => {
163
+ console.log('');
164
+ console.log('šŸ”“ Server closed with code:', code);
165
+ });
166
+ return server;
167
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ console.log('šŸš€ Starting Desktop Commander HTTP Server with Cloudflare Tunnel');
4
+ console.log('');
5
+ // Check if cloudflared is available
6
+ const checkCloudflared = spawn('which', ['cloudflared']);
7
+ checkCloudflared.on('close', (code) => {
8
+ if (code !== 0) {
9
+ console.error('āŒ cloudflared not found');
10
+ console.error('');
11
+ console.error('šŸ“¦ Install cloudflared:');
12
+ console.error(' macOS: brew install cloudflare/cloudflare/cloudflared');
13
+ console.error(' Linux: https://github.com/cloudflare/cloudflared');
14
+ console.error(' Windows: https://github.com/cloudflare/cloudflared');
15
+ console.error('');
16
+ process.exit(1);
17
+ }
18
+ startTunnel();
19
+ });
20
+ function startTunnel() {
21
+ console.log('🌐 Starting Cloudflare Tunnel...');
22
+ console.log('ā³ Waiting for tunnel URL...');
23
+ console.log('');
24
+ // Start cloudflare tunnel
25
+ const tunnel = spawn('cloudflared', ['tunnel', '--url', 'http://localhost:3000']);
26
+ let tunnelURL = null;
27
+ let server = null;
28
+ tunnel.stdout.on('data', (data) => {
29
+ const output = data.toString();
30
+ process.stdout.write(data);
31
+ // Look for the tunnel URL in cloudflared output
32
+ // Format: https://random-name.trycloudflare.com
33
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
34
+ if (urlMatch && !tunnelURL) {
35
+ tunnelURL = urlMatch[0];
36
+ console.log('');
37
+ console.log('āœ… Tunnel URL detected:', tunnelURL);
38
+ console.log('');
39
+ // Now start the server with the correct BASE_URL (tunnelURL is guaranteed to be non-null here)
40
+ server = startServer(tunnelURL);
41
+ }
42
+ });
43
+ tunnel.stderr.on('data', (data) => {
44
+ const output = data.toString();
45
+ process.stderr.write(data);
46
+ // Also check stderr for the URL
47
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
48
+ if (urlMatch && !tunnelURL) {
49
+ tunnelURL = urlMatch[0];
50
+ console.log('');
51
+ console.log('āœ… Tunnel URL detected:', tunnelURL);
52
+ console.log('');
53
+ // Now start the server with the correct BASE_URL (tunnelURL is guaranteed to be non-null here)
54
+ server = startServer(tunnelURL);
55
+ }
56
+ });
57
+ tunnel.on('error', (err) => {
58
+ console.error('āŒ Tunnel failed to start:', err);
59
+ process.exit(1);
60
+ });
61
+ tunnel.on('close', (code) => {
62
+ console.log('');
63
+ console.log('šŸ”“ Tunnel closed with code:', code);
64
+ if (server)
65
+ server.kill();
66
+ process.exit(code);
67
+ });
68
+ // Cleanup on exit
69
+ process.on('SIGINT', () => {
70
+ console.log('');
71
+ console.log('šŸ›‘ Shutting down...');
72
+ tunnel.kill();
73
+ if (server)
74
+ server.kill();
75
+ process.exit(0);
76
+ });
77
+ process.on('SIGTERM', () => {
78
+ tunnel.kill();
79
+ if (server)
80
+ server.kill();
81
+ process.exit(0);
82
+ });
83
+ }
84
+ function startServer(baseURL) {
85
+ console.log('šŸ“” Starting Desktop Commander HTTP server with BASE_URL:', baseURL);
86
+ console.log('');
87
+ // Start the http-server with the tunnel URL
88
+ const server = spawn('node', ['dist/http-server.js'], {
89
+ env: {
90
+ ...process.env,
91
+ BASE_URL: baseURL,
92
+ PORT: '3000'
93
+ },
94
+ stdio: ['ignore', 'pipe', 'pipe']
95
+ });
96
+ server.stdout.on('data', (data) => {
97
+ process.stdout.write(data);
98
+ });
99
+ server.stderr.on('data', (data) => {
100
+ process.stderr.write(data);
101
+ });
102
+ server.on('error', (err) => {
103
+ console.error('āŒ Server failed to start:', err);
104
+ process.exit(1);
105
+ });
106
+ server.on('close', (code) => {
107
+ console.log('');
108
+ console.log('šŸ”“ Server closed with code:', code);
109
+ });
110
+ return server;
111
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
7
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
8
+ import { server as baseServer } from './server.js';
9
+ import { configManager } from './config-manager.js';
10
+ import { OAuthManager, createOAuthRoutes, createAuthMiddleware } from './oauth/index.js';
11
+ // Configuration
12
+ const MCP_PORT = process.env.PORT || 3000;
13
+ const BASE_URL = process.env.BASE_URL || `http://localhost:${MCP_PORT}`;
14
+ const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true';
15
+ console.log(`šŸš€ Starting Desktop Commander HTTP Server`);
16
+ console.log(` Port: ${MCP_PORT}`);
17
+ console.log(` Base URL: ${BASE_URL}`);
18
+ console.log(` Auth: ${REQUIRE_AUTH ? 'ENABLED (OAuth)' : 'DISABLED (development mode)'}`);
19
+ const app = express();
20
+ // Log ALL incoming requests for debugging
21
+ app.use((req, res, next) => {
22
+ console.log(`\n🌐 ${req.method} ${req.path}`);
23
+ if (Object.keys(req.query).length > 0) {
24
+ console.log(` Query:`, req.query);
25
+ }
26
+ if (req.headers.authorization) {
27
+ console.log(` Auth: Bearer token present`);
28
+ }
29
+ // Capture response
30
+ const originalJson = res.json.bind(res);
31
+ const originalSend = res.send.bind(res);
32
+ const originalEnd = res.end.bind(res);
33
+ res.json = function (data) {
34
+ console.log(` šŸ“¤ Response ${res.statusCode}:`, JSON.stringify(data, null, 2).substring(0, 500));
35
+ return originalJson(data);
36
+ };
37
+ res.send = function (data) {
38
+ if (typeof data === 'string' && data.length < 200) {
39
+ console.log(` šŸ“¤ Response ${res.statusCode}: ${data.substring(0, 200)}`);
40
+ }
41
+ else {
42
+ console.log(` šŸ“¤ Response ${res.statusCode}: [${typeof data}]`);
43
+ }
44
+ return originalSend(data);
45
+ };
46
+ res.end = function (data) {
47
+ console.log(` šŸ“¤ Response ended: ${res.statusCode}`);
48
+ return originalEnd(data);
49
+ };
50
+ next();
51
+ });
52
+ // Initialize OAuth if enabled
53
+ const oauthManager = REQUIRE_AUTH ? new OAuthManager(BASE_URL) : null;
54
+ // Middleware
55
+ app.use(cors({
56
+ origin: '*',
57
+ exposedHeaders: ['Mcp-Session-Id']
58
+ }));
59
+ app.use(express.json());
60
+ app.use(express.urlencoded({ extended: true }));
61
+ // Add OAuth routes if enabled
62
+ if (oauthManager) {
63
+ const oauthRoutes = createOAuthRoutes(oauthManager, BASE_URL);
64
+ app.use(oauthRoutes);
65
+ console.log('šŸ” OAuth routes registered');
66
+ }
67
+ // Create auth middleware
68
+ const authMiddleware = createAuthMiddleware(oauthManager, BASE_URL, REQUIRE_AUTH);
69
+ // Map to store transports by session ID (session-based mode like oauth-test)
70
+ const transports = {};
71
+ // Health check endpoint
72
+ app.get('/', (req, res) => {
73
+ res.json({
74
+ name: 'Desktop Commander HTTP Server',
75
+ version: '0.3.0-alpha',
76
+ status: 'ready',
77
+ auth: REQUIRE_AUTH ? 'enabled' : 'disabled',
78
+ mode: 'session-based',
79
+ endpoints: {
80
+ mcp: '/mcp',
81
+ health: '/',
82
+ ...(REQUIRE_AUTH ? {
83
+ oauth_discovery: '/.well-known/oauth-authorization-server',
84
+ resource_metadata: '/.well-known/oauth-protected-resource',
85
+ register: '/register',
86
+ authorize: '/authorize',
87
+ token: '/token'
88
+ } : {})
89
+ }
90
+ });
91
+ });
92
+ // MCP POST endpoint - handles initialization and tool calls
93
+ app.post('/mcp', authMiddleware, async (req, res) => {
94
+ const sessionId = req.headers['mcp-session-id'];
95
+ console.log(`\nšŸ“„ POST /mcp`);
96
+ console.log(` Session: ${sessionId || 'new'}`);
97
+ console.log(` Method: ${req.body?.method}`);
98
+ if (req.auth) {
99
+ console.log(` User: ${req.auth.username} (${req.auth.client_id})`);
100
+ }
101
+ console.log(` Headers:`, JSON.stringify({
102
+ 'content-type': req.headers['content-type'],
103
+ 'accept': req.headers['accept'],
104
+ 'mcp-session-id': req.headers['mcp-session-id'],
105
+ 'user-agent': req.headers['user-agent']
106
+ }, null, 2));
107
+ try {
108
+ let transport;
109
+ // Check if this is an existing session
110
+ if (sessionId && transports[sessionId]) {
111
+ transport = transports[sessionId];
112
+ console.log(` ā™»ļø Using existing session`);
113
+ }
114
+ // Check if this is a new initialize request
115
+ else if (!sessionId && isInitializeRequest(req.body)) {
116
+ console.log(` šŸ†• Creating new session`);
117
+ const eventStore = new InMemoryEventStore();
118
+ transport = new StreamableHTTPServerTransport({
119
+ sessionIdGenerator: () => randomUUID(),
120
+ eventStore,
121
+ enableJsonResponse: true, // CRITICAL: Avoid SSE through cloudflare tunnel
122
+ onsessioninitialized: (sid) => {
123
+ console.log(` āœ… Session initialized: ${sid}`);
124
+ transports[sid] = transport;
125
+ }
126
+ });
127
+ transport.onclose = () => {
128
+ const sid = transport.sessionId;
129
+ if (sid && transports[sid]) {
130
+ console.log(` šŸ”“ Session closed: ${sid}`);
131
+ delete transports[sid];
132
+ }
133
+ };
134
+ // Connect the shared base server to this transport
135
+ // Note: MCP SDK allows one server to be connected to multiple transports
136
+ await baseServer.connect(transport);
137
+ await transport.handleRequest(req, res, req.body);
138
+ return;
139
+ }
140
+ // Invalid request
141
+ else {
142
+ console.log(` āŒ Invalid request: no session ID and not an initialize request`);
143
+ res.status(400).json({
144
+ jsonrpc: '2.0',
145
+ error: {
146
+ code: -32000,
147
+ message: 'Bad Request: No valid session ID or not an initialize request'
148
+ },
149
+ id: null
150
+ });
151
+ return;
152
+ }
153
+ await transport.handleRequest(req, res, req.body);
154
+ }
155
+ catch (error) {
156
+ console.error('āŒ Error handling MCP request:', error);
157
+ if (!res.headersSent) {
158
+ res.status(500).json({
159
+ jsonrpc: '2.0',
160
+ error: {
161
+ code: -32603,
162
+ message: 'Internal error',
163
+ data: error instanceof Error ? error.message : String(error)
164
+ },
165
+ id: null
166
+ });
167
+ }
168
+ }
169
+ });
170
+ // MCP GET endpoint - handles Server-Sent Events (SSE) for notifications
171
+ // DISABLED when using enableJsonResponse: true
172
+ app.get('/mcp', authMiddleware, async (req, res) => {
173
+ const sessionId = req.headers['mcp-session-id'];
174
+ console.log(`\nšŸ“„ GET /mcp (SSE) - Session: ${sessionId}`);
175
+ console.log(` āŒ SSE not supported in JSON-only mode`);
176
+ // Return error - SSE not supported when using JSON-only mode
177
+ return res.status(400).json({
178
+ jsonrpc: '2.0',
179
+ error: {
180
+ code: -32000,
181
+ message: 'SSE not supported. Use POST with mcp-session-id header for all requests.'
182
+ },
183
+ id: null
184
+ });
185
+ });
186
+ // MCP DELETE endpoint - handles session termination
187
+ app.delete('/mcp', authMiddleware, async (req, res) => {
188
+ const sessionId = req.headers['mcp-session-id'];
189
+ console.log(`\nšŸ—‘ļø DELETE /mcp - Session: ${sessionId}`);
190
+ if (!sessionId || !transports[sessionId]) {
191
+ return res.status(400).send('Invalid session ID');
192
+ }
193
+ const transport = transports[sessionId];
194
+ try {
195
+ await transport.handleRequest(req, res);
196
+ }
197
+ catch (error) {
198
+ console.error('āŒ Error handling DELETE:', error);
199
+ if (!res.headersSent) {
200
+ res.status(500).send('Error terminating session');
201
+ }
202
+ }
203
+ });
204
+ // Start the server
205
+ async function startServer() {
206
+ // Load configuration
207
+ try {
208
+ console.log('šŸ“ Loading configuration...');
209
+ await configManager.loadConfig();
210
+ console.log('āœ… Configuration loaded successfully');
211
+ }
212
+ catch (configError) {
213
+ console.error('āš ļø Failed to load configuration:', configError instanceof Error ? configError.message : String(configError));
214
+ console.log(' Continuing with default configuration...');
215
+ }
216
+ app.listen(MCP_PORT, () => {
217
+ console.log(`\nāœ… Desktop Commander HTTP Server listening on port ${MCP_PORT}`);
218
+ console.log(`\nšŸ“‹ Available endpoints:`);
219
+ console.log(` GET / - Health check`);
220
+ console.log(` POST /mcp - MCP requests`);
221
+ console.log(` GET /mcp - Server-Sent Events (SSE)`);
222
+ console.log(` DELETE /mcp - Terminate session`);
223
+ if (REQUIRE_AUTH) {
224
+ console.log(`\nšŸ” OAuth endpoints:`);
225
+ console.log(` GET /.well-known/oauth-authorization-server - Discovery`);
226
+ console.log(` GET /.well-known/oauth-protected-resource - Resource metadata`);
227
+ console.log(` POST /register - Client registration`);
228
+ console.log(` GET /authorize - Authorization (login page)`);
229
+ console.log(` POST /token - Token exchange`);
230
+ console.log(`\nšŸ‘¤ Demo credentials: admin / password123`);
231
+ }
232
+ console.log(`\nšŸ”— Test with MCP Inspector:`);
233
+ console.log(` npx @modelcontextprotocol/inspector ${BASE_URL}/mcp`);
234
+ console.log(`\nšŸ“ Mode: Session-based ${REQUIRE_AUTH ? '(OAuth enabled)' : '(no auth)'}`);
235
+ console.log(``);
236
+ }).on('error', (error) => {
237
+ console.error('āŒ Failed to start HTTP server:', error);
238
+ process.exit(1);
239
+ });
240
+ }
241
+ // Handle errors
242
+ process.on('uncaughtException', (error) => {
243
+ console.error('āŒ Uncaught exception:', error);
244
+ console.error('Stack trace:', error.stack);
245
+ process.exit(1);
246
+ });
247
+ process.on('unhandledRejection', (reason, promise) => {
248
+ console.error('āŒ Unhandled rejection at:', promise);
249
+ console.error('Reason:', reason);
250
+ process.exit(1);
251
+ });
252
+ // Graceful shutdown
253
+ process.on('SIGINT', async () => {
254
+ console.log('\nšŸ›‘ Shutting down...');
255
+ for (const sessionId in transports) {
256
+ try {
257
+ await transports[sessionId].close();
258
+ delete transports[sessionId];
259
+ }
260
+ catch (error) {
261
+ console.error(`Error closing ${sessionId}:`, error);
262
+ }
263
+ }
264
+ process.exit(0);
265
+ });
266
+ // Start the server
267
+ startServer().catch((error) => {
268
+ console.error('āŒ Failed to start server:', error);
269
+ process.exit(1);
270
+ });
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { FilteredStdioServerTransport } from './custom-stdio.js';
3
3
  import { server, flushDeferredMessages } from './server.js';
4
4
  import { configManager } from './config-manager.js';
5
+ import { featureFlagManager } from './utils/feature-flags.js';
5
6
  import { runSetup } from './npm-scripts/setup.js';
6
7
  import { runUninstall } from './npm-scripts/uninstall.js';
7
8
  import { capture } from './utils/capture.js';
@@ -34,6 +35,9 @@ async function runServer() {
34
35
  deferLog('info', 'Loading configuration...');
35
36
  await configManager.loadConfig();
36
37
  deferLog('info', 'Configuration loaded successfully');
38
+ // Initialize feature flags (non-blocking)
39
+ deferLog('info', 'Initializing feature flags...');
40
+ await featureFlagManager.initialize();
37
41
  }
38
42
  catch (configError) {
39
43
  deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
@@ -0,0 +1,20 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { OAuthManager } from './oauth-manager.js';
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ auth?: {
7
+ username: string;
8
+ client_id: string;
9
+ scope: string;
10
+ };
11
+ }
12
+ }
13
+ }
14
+ /**
15
+ * Create authentication middleware for MCP endpoints
16
+ *
17
+ * ChatGPT Workaround: Allow unauthenticated tools/list and initialize requests
18
+ * but require auth for actual tool calls
19
+ */
20
+ export declare function createAuthMiddleware(oauthManager: OAuthManager | null, baseUrl: string, requireAuth: boolean): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Create authentication middleware for MCP endpoints
3
+ *
4
+ * ChatGPT Workaround: Allow unauthenticated tools/list and initialize requests
5
+ * but require auth for actual tool calls
6
+ */
7
+ export function createAuthMiddleware(oauthManager, baseUrl, requireAuth) {
8
+ return async (req, res, next) => {
9
+ // Skip auth if disabled
10
+ if (!requireAuth || !oauthManager) {
11
+ return next();
12
+ }
13
+ // CHATGPT WORKAROUND: Allow unauthenticated discovery requests
14
+ // ChatGPT needs to see tools before authenticating
15
+ const method = req.body?.method;
16
+ if (method === 'initialize' || method === 'tools/list') {
17
+ console.log(`āš ļø Allowing unauthenticated ${method} for ChatGPT compatibility`);
18
+ return next();
19
+ }
20
+ const auth = req.headers.authorization;
21
+ // Check for Bearer token
22
+ if (!auth || !auth.startsWith('Bearer ')) {
23
+ console.log(`āŒ Auth failed: No Bearer token provided for ${method}`);
24
+ return res.status(401)
25
+ .set('WWW-Authenticate', `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`)
26
+ .json({
27
+ jsonrpc: '2.0',
28
+ error: {
29
+ code: -32001,
30
+ message: 'Authorization required',
31
+ data: 'Bearer token must be provided in Authorization header'
32
+ },
33
+ id: null
34
+ });
35
+ }
36
+ // Extract token
37
+ const token = auth.substring(7); // Remove "Bearer " prefix
38
+ // Validate token
39
+ const validation = oauthManager.validateToken(token);
40
+ if (!validation.valid) {
41
+ console.log(`āŒ Auth failed: ${validation.error}`);
42
+ return res.status(401).json({
43
+ jsonrpc: '2.0',
44
+ error: {
45
+ code: -32001,
46
+ message: 'Invalid or expired token',
47
+ data: validation.error
48
+ },
49
+ id: null
50
+ });
51
+ }
52
+ // Attach user info to request
53
+ req.auth = {
54
+ username: validation.username,
55
+ client_id: validation.client_id,
56
+ scope: validation.scope
57
+ };
58
+ // Log authenticated request
59
+ console.log(`āœ… Authenticated: ${req.auth.username} (${req.auth.client_id})`);
60
+ next();
61
+ };
62
+ }
@@ -0,0 +1,3 @@
1
+ export { OAuthManager } from './oauth-manager.js';
2
+ export { createOAuthRoutes } from './oauth-routes.js';
3
+ export { createAuthMiddleware } from './auth-middleware.js';
@@ -0,0 +1,3 @@
1
+ export { OAuthManager } from './oauth-manager.js';
2
+ export { createOAuthRoutes } from './oauth-routes.js';
3
+ export { createAuthMiddleware } from './auth-middleware.js';