@wonderwhy-er/desktop-commander 0.2.16 ā 0.2.18-alpha.0
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/README.md +3 -5
- package/dist/data/spec-kit-prompts.json +123 -0
- package/dist/handlers/filesystem-handlers.js +5 -2
- package/dist/handlers/history-handlers.d.ts +5 -0
- package/dist/handlers/history-handlers.js +35 -0
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +1 -0
- package/dist/http-index.d.ts +45 -0
- package/dist/http-index.js +51 -0
- package/dist/http-server-auto-tunnel.d.ts +1 -0
- package/dist/http-server-auto-tunnel.js +667 -0
- package/dist/http-server-named-tunnel.d.ts +2 -0
- package/dist/http-server-named-tunnel.js +167 -0
- package/dist/http-server-tunnel.d.ts +2 -0
- package/dist/http-server-tunnel.js +111 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.js +270 -0
- package/dist/index.js +4 -0
- package/dist/oauth/auth-middleware.d.ts +20 -0
- package/dist/oauth/auth-middleware.js +62 -0
- package/dist/oauth/index.d.ts +3 -0
- package/dist/oauth/index.js +3 -0
- package/dist/oauth/oauth-manager.d.ts +80 -0
- package/dist/oauth/oauth-manager.js +179 -0
- package/dist/oauth/oauth-routes.d.ts +3 -0
- package/dist/oauth/oauth-routes.js +377 -0
- package/dist/server.js +316 -210
- package/dist/setup-claude-server.js +29 -5
- package/dist/terminal-manager.d.ts +1 -1
- package/dist/terminal-manager.js +56 -1
- package/dist/tools/config.js +15 -1
- package/dist/tools/feedback.js +2 -2
- package/dist/tools/filesystem.d.ts +1 -1
- package/dist/tools/filesystem.js +51 -3
- package/dist/tools/improved-process-tools.js +179 -58
- package/dist/tools/schemas.d.ts +25 -0
- package/dist/tools/schemas.js +10 -0
- package/dist/types.d.ts +19 -0
- package/dist/utils/feature-flags.d.ts +43 -0
- package/dist/utils/feature-flags.js +147 -0
- package/dist/utils/toolHistory.d.ts +73 -0
- package/dist/utils/toolHistory.js +192 -0
- package/dist/utils/usageTracker.d.ts +4 -0
- package/dist/utils/usageTracker.js +63 -37
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
|
@@ -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,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,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
|
+
}
|