@wonderwhy-er/desktop-commander 0.2.13 → 0.2.15

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 CHANGED
@@ -86,6 +86,11 @@ For debugging mode (allows Node.js inspector connection):
86
86
  ```
87
87
  npx @wonderwhy-er/desktop-commander@latest setup --debug
88
88
  ```
89
+
90
+ **Command line options during setup:**
91
+ - `--debug`: Enable debugging mode for Node.js inspector
92
+ - `--no-onboarding`: Disable onboarding prompts for new users
93
+
89
94
  Restart Claude if running.
90
95
 
91
96
  **✅ Auto-Updates:** Yes - automatically updates when you restart Claude
@@ -652,6 +657,44 @@ set_config_value({ "key": "fileWriteLineLimit", "value": 25 })
652
657
 
653
658
  4. **Always verify configuration after changes**: Use `get_config({})` to confirm your changes were applied correctly.
654
659
 
660
+ ## Command Line Options
661
+
662
+ Desktop Commander supports several command line options for customizing behavior:
663
+
664
+ ### Disable Onboarding
665
+
666
+ By default, Desktop Commander shows helpful onboarding prompts to new users (those with fewer than 10 tool calls). You can disable this behavior:
667
+
668
+ ```bash
669
+ # Disable onboarding for this session
670
+ node dist/index.js --no-onboarding
671
+
672
+ # Or if using npm scripts
673
+ npm run start:no-onboarding
674
+
675
+ # For npx installations, modify your claude_desktop_config.json:
676
+ {
677
+ "mcpServers": {
678
+ "desktop-commander": {
679
+ "command": "npx",
680
+ "args": [
681
+ "-y",
682
+ "@wonderwhy-er/desktop-commander@latest",
683
+ "--no-onboarding"
684
+ ]
685
+ }
686
+ }
687
+ }
688
+ ```
689
+
690
+ **When onboarding is automatically disabled:**
691
+ - When the MCP client name is set to "desktop-commander"
692
+ - When using the `--no-onboarding` flag
693
+ - After users have used onboarding prompts or made 10+ tool calls
694
+
695
+ **Debug information:**
696
+ The server will log when onboarding is disabled: `"Onboarding disabled via --no-onboarding flag"`
697
+
655
698
  ## Using Different Shells
656
699
 
657
700
  You can specify which shell to use for command execution:
@@ -22,14 +22,16 @@ export class FilteredStdioServerTransport extends StdioServerTransport {
22
22
  this.setupConsoleRedirection();
23
23
  // Setup stdout filtering for any other output
24
24
  this.setupStdoutFiltering();
25
- // Send initialization notification
26
- this.sendLogNotification('info', ['Enhanced FilteredStdioServerTransport initialized']);
25
+ // Note: We defer the initialization notification until enableNotifications() is called
26
+ // to ensure MCP protocol compliance - notifications must not be sent before initialization
27
27
  }
28
28
  /**
29
29
  * Call this method after MCP initialization is complete to enable JSON-RPC notifications
30
30
  */
31
31
  enableNotifications() {
32
32
  this.isInitialized = true;
33
+ // Send the deferred initialization notification first
34
+ this.sendLogNotification('info', ['Enhanced FilteredStdioServerTransport initialized']);
33
35
  // Replay all buffered messages in chronological order
34
36
  if (this.messageBuffer.length > 0) {
35
37
  this.sendLogNotification('info', [`Replaying ${this.messageBuffer.length} buffered initialization messages`]);
@@ -24,6 +24,7 @@ export async function handleStartSearch(args) {
24
24
  contextLines: parsed.data.contextLines,
25
25
  timeout: parsed.data.timeout_ms,
26
26
  earlyTermination: parsed.data.earlyTermination,
27
+ literalSearch: parsed.data.literalSearch,
27
28
  });
28
29
  const searchTypeText = parsed.data.searchType === 'content' ? 'content search' : 'file search';
29
30
  let output = `Started ${searchTypeText} session: ${result.sessionId}\n`;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ import { FilteredStdioServerTransport } from './custom-stdio.js';
3
+ import { server } from './server.js';
4
+ import { configManager } from './config-manager.js';
5
+ import { runSetup } from './npm-scripts/setup.js';
6
+ import { runUninstall } from './npm-scripts/uninstall.js';
7
+ import { capture } from './utils/capture.js';
8
+ import { logToStderr, logger } from './utils/logger.js';
9
+ import { OAuthHttpServer } from './oauth/server.js';
10
+ async function runServer() {
11
+ try {
12
+ // Check if first argument is "setup"
13
+ if (process.argv[2] === 'setup') {
14
+ await runSetup();
15
+ return;
16
+ }
17
+ // Check if first argument is "remove"
18
+ if (process.argv[2] === 'remove') {
19
+ await runUninstall();
20
+ return;
21
+ }
22
+ // Check for HTTP mode with OAuth
23
+ const httpMode = process.argv.includes('--http') || process.argv.includes('--oauth');
24
+ const port = getPortFromArgs() || 8000;
25
+ if (httpMode) {
26
+ await runHttpServer(port);
27
+ }
28
+ else {
29
+ await runStdioServer();
30
+ }
31
+ }
32
+ catch (error) {
33
+ logger.error('Failed to start server:', error);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ async function runStdioServer() {
38
+ logger.info('Loading server.ts');
39
+ logger.info('Setting up request handlers...');
40
+ try {
41
+ logger.info('Loading configuration...');
42
+ await configManager.loadConfig();
43
+ logger.info('Configuration loaded successfully');
44
+ const transport = new FilteredStdioServerTransport();
45
+ logger.info('Enhanced FilteredStdioServerTransport initialized');
46
+ logger.info('Connecting server...');
47
+ await server.connect(transport);
48
+ logger.info('Server connected successfully');
49
+ }
50
+ catch (error) {
51
+ await capture('error_start_stdio_server', { error });
52
+ logToStderr('error', `Failed to start stdio server: ${error}`);
53
+ throw error;
54
+ }
55
+ }
56
+ async function runHttpServer(port) {
57
+ logger.info(`Starting HTTP server with OAuth on port ${port}`);
58
+ try {
59
+ logger.info('Loading configuration...');
60
+ await configManager.loadConfig();
61
+ logger.info('Configuration loaded successfully');
62
+ // OAuth configuration
63
+ const baseUrl = `http://localhost:${port}`;
64
+ const oauthConfig = {
65
+ enabled: true,
66
+ clientId: 'desktop-commander-mcp',
67
+ clientSecret: 'dc-secret-' + Math.random().toString(36).substring(7),
68
+ redirectUri: `${baseUrl}/callback`,
69
+ authorizationUrl: `${baseUrl}/authorize`,
70
+ tokenUrl: `${baseUrl}/token`,
71
+ scope: 'mcp:access mcp:tools mcp:resources',
72
+ issuer: baseUrl
73
+ };
74
+ // Create MCP handler for HTTP requests
75
+ const mcpHandler = createMcpHttpHandler();
76
+ // Create OAuth HTTP server
77
+ const oauthServer = new OAuthHttpServer(oauthConfig, mcpHandler);
78
+ oauthServer.listen(port, () => {
79
+ logger.info(`HTTP server running on port ${port}`);
80
+ logger.info(`Authorization Server Metadata: ${baseUrl}/.well-known/oauth-authorization-server`);
81
+ logger.info(`MCP Endpoint: ${baseUrl}/mcp`);
82
+ logger.info(`SSE Endpoint: ${baseUrl}/sse`);
83
+ console.log(`\n🚀 DesktopCommanderMCP HTTP Server Started!`);
84
+ console.log(`📍 Server URL: ${baseUrl}`);
85
+ console.log(`🔐 OAuth Metadata: ${baseUrl}/.well-known/oauth-authorization-server`);
86
+ console.log(`🔧 MCP Endpoint: ${baseUrl}/mcp`);
87
+ console.log(`⚡ SSE Endpoint: ${baseUrl}/sse`);
88
+ console.log(`\n📝 For Claude Custom Connectors:`);
89
+ console.log(` URL: ${baseUrl}/sse`);
90
+ console.log(` Authentication: OAuth`);
91
+ });
92
+ // Graceful shutdown
93
+ process.on('SIGINT', () => {
94
+ logger.info('Received SIGINT, shutting down gracefully...');
95
+ oauthServer.close(() => {
96
+ process.exit(0);
97
+ });
98
+ });
99
+ }
100
+ catch (error) {
101
+ await capture('error_start_http_server', { error });
102
+ logToStderr('error', `Failed to start HTTP server: ${error}`);
103
+ throw error;
104
+ }
105
+ }
106
+ function createMcpHttpHandler() {
107
+ return (req, res) => {
108
+ const url = new URL(req.url, `http://${req.headers.host}`);
109
+ // Handle SSE endpoint for MCP clients
110
+ if (url.pathname === '/sse') {
111
+ handleSSE(req, res);
112
+ return;
113
+ }
114
+ // Handle Streamable HTTP endpoint
115
+ if (url.pathname === '/mcp') {
116
+ handleStreamableHttp(req, res);
117
+ return;
118
+ }
119
+ // Health check
120
+ if (url.pathname === '/health') {
121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
122
+ res.end(JSON.stringify({ status: 'ok', service: 'DesktopCommanderMCP' }));
123
+ return;
124
+ }
125
+ // 404 for other paths
126
+ res.writeHead(404, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({ error: 'Not found' }));
128
+ };
129
+ }
130
+ function handleSSE(req, res) {
131
+ // Set SSE headers
132
+ res.writeHead(200, {
133
+ 'Content-Type': 'text/event-stream',
134
+ 'Cache-Control': 'no-cache',
135
+ 'Connection': 'keep-alive',
136
+ 'Access-Control-Allow-Origin': '*',
137
+ 'Access-Control-Allow-Headers': 'Cache-Control'
138
+ });
139
+ // SSE implementation would go here
140
+ // For now, just indicate that SSE is available
141
+ res.write('event: connected\n');
142
+ res.write('data: {"type":"connected","message":"DesktopCommanderMCP SSE endpoint"}\n\n');
143
+ // Keep connection alive
144
+ const heartbeat = setInterval(() => {
145
+ res.write('event: heartbeat\n');
146
+ res.write('data: {"type":"heartbeat","timestamp":' + Date.now() + '}\n\n');
147
+ }, 30000);
148
+ req.on('close', () => {
149
+ clearInterval(heartbeat);
150
+ });
151
+ }
152
+ function handleStreamableHttp(req, res) {
153
+ // Handle Streamable HTTP for MCP
154
+ res.writeHead(200, { 'Content-Type': 'application/json' });
155
+ if (req.method === 'GET') {
156
+ // Return server info
157
+ res.end(JSON.stringify({
158
+ name: 'DesktopCommanderMCP',
159
+ version: '0.2.13',
160
+ transport: 'streamable-http',
161
+ authenticated: !!req.user
162
+ }));
163
+ return;
164
+ }
165
+ // Handle MCP protocol messages
166
+ if (req.method === 'POST') {
167
+ let body = '';
168
+ req.on('data', chunk => body += chunk.toString());
169
+ req.on('end', () => {
170
+ try {
171
+ const message = JSON.parse(body);
172
+ // Process MCP message through the server
173
+ // This would need to be integrated with the existing MCP server
174
+ res.end(JSON.stringify({
175
+ jsonrpc: '2.0',
176
+ id: message.id,
177
+ result: { message: 'MCP message received' }
178
+ }));
179
+ }
180
+ catch (error) {
181
+ res.writeHead(400, { 'Content-Type': 'application/json' });
182
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
183
+ }
184
+ });
185
+ return;
186
+ }
187
+ res.writeHead(405, { 'Content-Type': 'application/json' });
188
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
189
+ }
190
+ function getPortFromArgs() {
191
+ const portIndex = process.argv.findIndex(arg => arg === '--port');
192
+ if (portIndex !== -1 && process.argv[portIndex + 1]) {
193
+ return parseInt(process.argv[portIndex + 1], 10);
194
+ }
195
+ return null;
196
+ }
197
+ // Run the server
198
+ runServer().catch(error => {
199
+ console.error('Fatal error:', error);
200
+ process.exit(1);
201
+ });
package/dist/index.js CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { FilteredStdioServerTransport } from './custom-stdio.js';
3
- import { server } from './server.js';
3
+ import { server, flushDeferredMessages } from './server.js';
4
4
  import { configManager } from './config-manager.js';
5
5
  import { runSetup } from './npm-scripts/setup.js';
6
6
  import { runUninstall } from './npm-scripts/uninstall.js';
7
7
  import { capture } from './utils/capture.js';
8
8
  import { logToStderr, logger } from './utils/logger.js';
9
+ // Store messages to defer until after initialization
10
+ const deferredMessages = [];
11
+ function deferLog(level, message) {
12
+ deferredMessages.push({ level, message });
13
+ }
9
14
  async function runServer() {
10
15
  try {
11
16
  // Check if first argument is "setup"
@@ -18,17 +23,24 @@ async function runServer() {
18
23
  await runUninstall();
19
24
  return;
20
25
  }
26
+ // Parse command line arguments for onboarding control
27
+ const DISABLE_ONBOARDING = process.argv.includes('--no-onboarding');
28
+ if (DISABLE_ONBOARDING) {
29
+ logToStderr('info', 'Onboarding disabled via --no-onboarding flag');
30
+ }
31
+ // Set global flag for onboarding control
32
+ global.disableOnboarding = DISABLE_ONBOARDING;
21
33
  try {
22
- logToStderr('info', 'Loading configuration...');
34
+ deferLog('info', 'Loading configuration...');
23
35
  await configManager.loadConfig();
24
- logToStderr('info', 'Configuration loaded successfully');
36
+ deferLog('info', 'Configuration loaded successfully');
25
37
  }
26
38
  catch (configError) {
27
- logToStderr('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
39
+ deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
28
40
  if (configError instanceof Error && configError.stack) {
29
- logToStderr('debug', `Stack trace: ${configError.stack}`);
41
+ deferLog('debug', `Stack trace: ${configError.stack}`);
30
42
  }
31
- logToStderr('warning', 'Continuing with in-memory configuration only');
43
+ deferLog('warning', 'Continuing with in-memory configuration only');
32
44
  // Continue anyway - we'll use an in-memory config
33
45
  }
34
46
  const transport = new FilteredStdioServerTransport();
@@ -63,17 +75,23 @@ async function runServer() {
63
75
  process.exit(1);
64
76
  });
65
77
  capture('run_server_start');
66
- logToStderr('info', 'Connecting server...');
78
+ deferLog('info', 'Connecting server...');
67
79
  // Set up event-driven initialization completion handler
68
80
  server.oninitialized = () => {
69
81
  // This callback is triggered after the client sends the "initialized" notification
70
82
  // At this point, the MCP protocol handshake is fully complete
71
83
  transport.enableNotifications();
72
- // Use the transport to send a proper JSON-RPC notification
73
- transport.sendLog('info', 'MCP fully initialized, notifications enabled');
84
+ // Flush all deferred messages from both index.ts and server.ts
85
+ while (deferredMessages.length > 0) {
86
+ const msg = deferredMessages.shift();
87
+ transport.sendLog('info', msg.message);
88
+ }
89
+ flushDeferredMessages();
90
+ // Now we can send regular logging messages
91
+ transport.sendLog('info', 'Server connected successfully');
92
+ transport.sendLog('info', 'MCP fully initialized, all startup messages sent');
74
93
  };
75
94
  await server.connect(transport);
76
- logToStderr('info', 'Server connected successfully');
77
95
  }
78
96
  catch (error) {
79
97
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,22 @@
1
+ import type { OAuthConfig, AuthorizationServerMetadata, TokenResponse, AccessToken, AuthorizationRequest, TokenRequest } from './types.js';
2
+ export declare class OAuthProvider {
3
+ private config;
4
+ private users;
5
+ private authCodes;
6
+ private accessTokens;
7
+ constructor(config: OAuthConfig);
8
+ static generatePKCE(): {
9
+ codeVerifier: string;
10
+ codeChallenge: string;
11
+ };
12
+ getAuthorizationServerMetadata(): AuthorizationServerMetadata;
13
+ handleAuthorizationRequest(params: AuthorizationRequest): {
14
+ authUrl: string;
15
+ authCode?: string;
16
+ error?: string;
17
+ };
18
+ handleTokenRequest(params: TokenRequest): Promise<TokenResponse | {
19
+ error: string;
20
+ }>;
21
+ validateAccessToken(token: string): AccessToken | null;
22
+ }
@@ -0,0 +1,124 @@
1
+ import crypto from 'crypto';
2
+ export class OAuthProvider {
3
+ constructor(config) {
4
+ this.users = new Map();
5
+ this.authCodes = new Map();
6
+ this.accessTokens = new Map();
7
+ this.config = config;
8
+ // Add a default user for testing
9
+ this.users.set('admin', {
10
+ email: 'admin@localhost',
11
+ password: 'admin123' // In production, hash this!
12
+ });
13
+ }
14
+ // Generate PKCE code verifier and challenge
15
+ static generatePKCE() {
16
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
17
+ const codeChallenge = crypto
18
+ .createHash('sha256')
19
+ .update(codeVerifier)
20
+ .digest('base64url');
21
+ return { codeVerifier, codeChallenge };
22
+ }
23
+ // Get Authorization Server Metadata (required by MCP spec)
24
+ getAuthorizationServerMetadata() {
25
+ const baseUrl = this.config.issuer;
26
+ return {
27
+ issuer: baseUrl,
28
+ authorization_endpoint: `${baseUrl}/authorize`,
29
+ token_endpoint: `${baseUrl}/token`,
30
+ registration_endpoint: `${baseUrl}/register`,
31
+ revocation_endpoint: `${baseUrl}/revoke`,
32
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
33
+ response_types_supported: ['code'],
34
+ grant_types_supported: ['authorization_code', 'refresh_token'],
35
+ token_endpoint_auth_methods_supported: ['none', 'client_secret_basic'],
36
+ code_challenge_methods_supported: ['S256'],
37
+ scopes_supported: ['mcp:access', 'mcp:tools', 'mcp:resources']
38
+ };
39
+ }
40
+ // Handle authorization request
41
+ handleAuthorizationRequest(params) {
42
+ if (!params.client_id || !params.redirect_uri || !params.code_challenge) {
43
+ return { authUrl: '', error: 'Missing required parameters' };
44
+ }
45
+ if (params.code_challenge_method !== 'S256') {
46
+ return { authUrl: '', error: 'Unsupported code_challenge_method' };
47
+ }
48
+ // Generate authorization code
49
+ const authCode = crypto.randomBytes(32).toString('hex');
50
+ const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
51
+ // Store authorization code
52
+ this.authCodes.set(authCode, {
53
+ userId: 'admin', // For simplicity, auto-approve for admin user
54
+ clientId: params.client_id,
55
+ redirectUri: params.redirect_uri,
56
+ scope: params.scope || 'mcp:access',
57
+ codeChallenge: params.code_challenge,
58
+ codeChallengeMethod: params.code_challenge_method,
59
+ expiresAt
60
+ });
61
+ // Build redirect URL with auth code
62
+ const redirectUrl = new URL(params.redirect_uri);
63
+ redirectUrl.searchParams.set('code', authCode);
64
+ if (params.state) {
65
+ redirectUrl.searchParams.set('state', params.state);
66
+ }
67
+ return { authUrl: redirectUrl.toString(), authCode };
68
+ }
69
+ // Exchange authorization code for access token
70
+ async handleTokenRequest(params) {
71
+ const authCodeData = this.authCodes.get(params.code);
72
+ if (!authCodeData) {
73
+ return { error: 'Invalid authorization code' };
74
+ }
75
+ if (Date.now() > authCodeData.expiresAt) {
76
+ this.authCodes.delete(params.code);
77
+ return { error: 'Authorization code expired' };
78
+ }
79
+ // Verify PKCE
80
+ const computedChallenge = crypto
81
+ .createHash('sha256')
82
+ .update(params.code_verifier)
83
+ .digest('base64url');
84
+ if (computedChallenge !== authCodeData.codeChallenge) {
85
+ return { error: 'Invalid code_verifier' };
86
+ }
87
+ // Verify client and redirect URI
88
+ if (params.client_id !== authCodeData.clientId ||
89
+ params.redirect_uri !== authCodeData.redirectUri) {
90
+ return { error: 'Invalid client_id or redirect_uri' };
91
+ }
92
+ // Generate access token
93
+ const accessToken = crypto.randomBytes(32).toString('hex');
94
+ const expiresIn = 3600; // 1 hour
95
+ const tokenData = {
96
+ sub: authCodeData.userId,
97
+ aud: params.client_id,
98
+ iss: this.config.issuer,
99
+ exp: Math.floor(Date.now() / 1000) + expiresIn,
100
+ iat: Math.floor(Date.now() / 1000),
101
+ scope: authCodeData.scope
102
+ };
103
+ this.accessTokens.set(accessToken, tokenData);
104
+ this.authCodes.delete(params.code);
105
+ return {
106
+ access_token: accessToken,
107
+ token_type: 'Bearer',
108
+ expires_in: expiresIn,
109
+ scope: authCodeData.scope
110
+ };
111
+ }
112
+ // Validate access token
113
+ validateAccessToken(token) {
114
+ const tokenData = this.accessTokens.get(token);
115
+ if (!tokenData) {
116
+ return null;
117
+ }
118
+ if (Date.now() / 1000 > tokenData.exp) {
119
+ this.accessTokens.delete(token);
120
+ return null;
121
+ }
122
+ return tokenData;
123
+ }
124
+ }
@@ -0,0 +1,18 @@
1
+ import http from 'http';
2
+ import type { OAuthConfig } from './types.js';
3
+ export declare class OAuthHttpServer {
4
+ private server;
5
+ private oauthProvider;
6
+ private mcpHandler;
7
+ constructor(config: OAuthConfig, mcpHandler: (req: http.IncomingMessage, res: http.ServerResponse) => void);
8
+ private handleRequest;
9
+ private setCorsHeaders;
10
+ private handleAuthServerMetadata;
11
+ private handleAuthorize;
12
+ private handleToken;
13
+ private handleCallback;
14
+ private sendUnauthorized;
15
+ private getRequestBody;
16
+ listen(port: number, callback?: () => void): void;
17
+ close(callback?: () => void): void;
18
+ }
@@ -0,0 +1,160 @@
1
+ import http from 'http';
2
+ import { URL } from 'url';
3
+ import { OAuthProvider } from './provider.js';
4
+ export class OAuthHttpServer {
5
+ constructor(config, mcpHandler) {
6
+ this.oauthProvider = new OAuthProvider(config);
7
+ this.mcpHandler = mcpHandler;
8
+ this.server = http.createServer(this.handleRequest.bind(this));
9
+ }
10
+ async handleRequest(req, res) {
11
+ const url = new URL(req.url, `http://${req.headers.host}`);
12
+ // Enable CORS
13
+ this.setCorsHeaders(res);
14
+ if (req.method === 'OPTIONS') {
15
+ res.writeHead(200);
16
+ res.end();
17
+ return;
18
+ }
19
+ try {
20
+ // OAuth endpoints
21
+ if (url.pathname === '/.well-known/oauth-authorization-server') {
22
+ return this.handleAuthServerMetadata(res);
23
+ }
24
+ if (url.pathname === '/authorize') {
25
+ return this.handleAuthorize(url, res);
26
+ }
27
+ if (url.pathname === '/token' && req.method === 'POST') {
28
+ return this.handleToken(req, res);
29
+ }
30
+ if (url.pathname === '/callback') {
31
+ return this.handleCallback(url, res);
32
+ }
33
+ // Protected MCP endpoints - require authentication
34
+ if (url.pathname.startsWith('/mcp') || url.pathname === '/sse') {
35
+ const authHeader = req.headers.authorization;
36
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
37
+ return this.sendUnauthorized(res);
38
+ }
39
+ const token = authHeader.substring(7);
40
+ const tokenData = this.oauthProvider.validateAccessToken(token);
41
+ if (!tokenData) {
42
+ return this.sendUnauthorized(res);
43
+ }
44
+ // Add user info to request for MCP handler
45
+ req.user = tokenData;
46
+ }
47
+ // Forward to MCP handler
48
+ this.mcpHandler(req, res);
49
+ }
50
+ catch (error) {
51
+ console.error('OAuth server error:', error);
52
+ res.writeHead(500, { 'Content-Type': 'application/json' });
53
+ res.end(JSON.stringify({ error: 'Internal server error' }));
54
+ }
55
+ }
56
+ setCorsHeaders(res) {
57
+ res.setHeader('Access-Control-Allow-Origin', '*');
58
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
59
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
60
+ res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');
61
+ }
62
+ handleAuthServerMetadata(res) {
63
+ const metadata = this.oauthProvider.getAuthorizationServerMetadata();
64
+ res.writeHead(200, { 'Content-Type': 'application/json' });
65
+ res.end(JSON.stringify(metadata, null, 2));
66
+ }
67
+ handleAuthorize(url, res) {
68
+ const params = {
69
+ response_type: url.searchParams.get('response_type'),
70
+ client_id: url.searchParams.get('client_id'),
71
+ redirect_uri: url.searchParams.get('redirect_uri'),
72
+ scope: url.searchParams.get('scope') || undefined,
73
+ state: url.searchParams.get('state') || undefined,
74
+ code_challenge: url.searchParams.get('code_challenge'),
75
+ code_challenge_method: url.searchParams.get('code_challenge_method')
76
+ };
77
+ const result = this.oauthProvider.handleAuthorizationRequest(params);
78
+ if (result.error) {
79
+ res.writeHead(400, { 'Content-Type': 'application/json' });
80
+ res.end(JSON.stringify({ error: result.error }));
81
+ return;
82
+ }
83
+ // Redirect to the callback URL with auth code
84
+ res.writeHead(302, { 'Location': result.authUrl });
85
+ res.end();
86
+ }
87
+ async handleToken(req, res) {
88
+ const body = await this.getRequestBody(req);
89
+ const params = new URLSearchParams(body);
90
+ const tokenRequest = {
91
+ grant_type: params.get('grant_type'),
92
+ code: params.get('code'),
93
+ redirect_uri: params.get('redirect_uri'),
94
+ client_id: params.get('client_id'),
95
+ code_verifier: params.get('code_verifier')
96
+ };
97
+ const result = await this.oauthProvider.handleTokenRequest(tokenRequest);
98
+ if ('error' in result) {
99
+ res.writeHead(400, { 'Content-Type': 'application/json' });
100
+ res.end(JSON.stringify(result));
101
+ return;
102
+ }
103
+ res.writeHead(200, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify(result));
105
+ }
106
+ handleCallback(url, res) {
107
+ const code = url.searchParams.get('code');
108
+ const state = url.searchParams.get('state');
109
+ // Simple success page
110
+ const html = `
111
+ <!DOCTYPE html>
112
+ <html>
113
+ <head><title>Authorization Successful</title></head>
114
+ <body>
115
+ <h1>Authorization Successful!</h1>
116
+ <p>You can now close this window.</p>
117
+ <script>
118
+ // Try to close the window (works if opened by script)
119
+ try { window.close(); } catch(e) {}
120
+
121
+ // Post message to parent if in iframe
122
+ if (window.opener) {
123
+ window.opener.postMessage({
124
+ type: 'oauth_success',
125
+ code: '${code}',
126
+ state: '${state}'
127
+ }, '*');
128
+ }
129
+ </script>
130
+ </body>
131
+ </html>
132
+ `;
133
+ res.writeHead(200, { 'Content-Type': 'text/html' });
134
+ res.end(html);
135
+ }
136
+ sendUnauthorized(res) {
137
+ res.writeHead(401, {
138
+ 'Content-Type': 'application/json',
139
+ 'WWW-Authenticate': 'Bearer realm="mcp", error="invalid_token"'
140
+ });
141
+ res.end(JSON.stringify({
142
+ error: 'unauthorized',
143
+ message: 'Valid access token required'
144
+ }));
145
+ }
146
+ async getRequestBody(req) {
147
+ return new Promise((resolve, reject) => {
148
+ let body = '';
149
+ req.on('data', chunk => body += chunk.toString());
150
+ req.on('end', () => resolve(body));
151
+ req.on('error', reject);
152
+ });
153
+ }
154
+ listen(port, callback) {
155
+ this.server.listen(port, callback);
156
+ }
157
+ close(callback) {
158
+ this.server.close(callback);
159
+ }
160
+ }
@@ -0,0 +1,54 @@
1
+ export interface OAuthConfig {
2
+ enabled: boolean;
3
+ clientId: string;
4
+ clientSecret: string;
5
+ redirectUri: string;
6
+ authorizationUrl: string;
7
+ tokenUrl: string;
8
+ scope: string;
9
+ issuer: string;
10
+ }
11
+ export interface AuthorizationServerMetadata {
12
+ issuer: string;
13
+ authorization_endpoint: string;
14
+ token_endpoint: string;
15
+ registration_endpoint?: string;
16
+ revocation_endpoint?: string;
17
+ jwks_uri?: string;
18
+ response_types_supported: string[];
19
+ grant_types_supported: string[];
20
+ token_endpoint_auth_methods_supported: string[];
21
+ code_challenge_methods_supported: string[];
22
+ scopes_supported?: string[];
23
+ }
24
+ export interface TokenResponse {
25
+ access_token: string;
26
+ token_type: string;
27
+ expires_in?: number;
28
+ refresh_token?: string;
29
+ scope?: string;
30
+ }
31
+ export interface AccessToken {
32
+ sub: string;
33
+ aud: string | string[];
34
+ iss: string;
35
+ exp: number;
36
+ iat: number;
37
+ scope?: string;
38
+ }
39
+ export interface AuthorizationRequest {
40
+ response_type: 'code';
41
+ client_id: string;
42
+ redirect_uri: string;
43
+ scope?: string;
44
+ state?: string;
45
+ code_challenge: string;
46
+ code_challenge_method: 'S256';
47
+ }
48
+ export interface TokenRequest {
49
+ grant_type: 'authorization_code';
50
+ code: string;
51
+ redirect_uri: string;
52
+ client_id: string;
53
+ code_verifier: string;
54
+ }
@@ -0,0 +1,2 @@
1
+ // OAuth 2.1 types for MCP authorization
2
+ export {};
@@ -30,6 +30,7 @@ export interface SearchSessionOptions {
30
30
  contextLines?: number;
31
31
  timeout?: number;
32
32
  earlyTermination?: boolean;
33
+ literalSearch?: boolean;
33
34
  }
34
35
  /**
35
36
  * Search Session Manager - handles ripgrep processes like terminal sessions
@@ -212,6 +212,10 @@ import { capture } from './utils/capture.js';
212
212
  if (options.searchType === 'content') {
213
213
  // Content search mode
214
214
  args.push('--json', '--line-number');
215
+ // Add literal search support for content searches
216
+ if (options.literalSearch) {
217
+ args.push('-F'); // Fixed string matching (literal)
218
+ }
215
219
  if (options.contextLines && options.contextLines > 0) {
216
220
  args.push('-C', options.contextLines.toString());
217
221
  }
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function flushDeferredMessages(): void;
2
3
  export declare const server: Server<{
3
4
  method: string;
4
5
  params?: {
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { zodToJsonSchema } from "zod-to-json-schema";
4
4
  import { getSystemInfo, getOSSpecificGuidance, getPathGuidance, getDevelopmentToolGuidance } from './utils/system-info.js';
5
5
  // Get system information once at startup
@@ -18,8 +18,20 @@ import { usageTracker } from './utils/usageTracker.js';
18
18
  import { processDockerPrompt } from './utils/dockerPrompt.js';
19
19
  import { VERSION } from './version.js';
20
20
  import { capture, capture_call_tool } from "./utils/capture.js";
21
- import { logToStderr } from './utils/logger.js';
22
- logToStderr('info', 'Loading server.ts');
21
+ import { logToStderr, logger } from './utils/logger.js';
22
+ // Store startup messages to send after initialization
23
+ const deferredMessages = [];
24
+ function deferLog(level, message) {
25
+ deferredMessages.push({ level, message });
26
+ }
27
+ // Function to flush deferred messages after initialization
28
+ export function flushDeferredMessages() {
29
+ while (deferredMessages.length > 0) {
30
+ const msg = deferredMessages.shift();
31
+ logger.info(msg.message);
32
+ }
33
+ }
34
+ deferLog('info', 'Loading server.ts');
23
35
  export const server = new Server({
24
36
  name: "desktop-commander",
25
37
  version: VERSION,
@@ -57,8 +69,8 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
57
69
  name: clientInfo.name || 'unknown',
58
70
  version: clientInfo.version || 'unknown'
59
71
  };
60
- // Send JSON-RPC notification about client connection
61
- logToStderr('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
72
+ // Defer client connection message until after initialization
73
+ deferLog('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
62
74
  }
63
75
  // Return standard initialization response
64
76
  return {
@@ -82,7 +94,7 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
82
94
  });
83
95
  // Export current client info for access by other modules
84
96
  export { currentClient };
85
- logToStderr('info', 'Setting up request handlers...');
97
+ deferLog('info', 'Setting up request handlers...');
86
98
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
99
  try {
88
100
  logToStderr('debug', 'Generating tools list...');
@@ -257,24 +269,72 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
257
269
  description: `
258
270
  Start a streaming search that can return results progressively.
259
271
 
272
+ SEARCH STRATEGY GUIDE:
273
+ Choose the right search type based on what the user is looking for:
274
+
275
+ USE searchType="files" WHEN:
276
+ - User asks for specific files: "find package.json", "locate config files"
277
+ - Pattern looks like a filename: "*.js", "README.md", "test-*.tsx"
278
+ - User wants to find files by name/extension: "all TypeScript files", "Python scripts"
279
+ - Looking for configuration/setup files: ".env", "dockerfile", "tsconfig.json"
280
+
281
+ USE searchType="content" WHEN:
282
+ - User asks about code/logic: "authentication logic", "error handling", "API calls"
283
+ - Looking for functions/variables: "getUserData function", "useState hook"
284
+ - Searching for text/comments: "TODO items", "FIXME comments", "documentation"
285
+ - Finding patterns in code: "console.log statements", "import statements"
286
+ - User describes functionality: "components that handle login", "files with database queries"
287
+
288
+ WHEN UNSURE OR USER REQUEST IS AMBIGUOUS:
289
+ Run TWO searches in parallel - one for files and one for content:
290
+
291
+ Example approach for ambiguous queries like "find authentication stuff":
292
+ 1. Start file search: searchType="files", pattern="auth"
293
+ 2. Simultaneously start content search: searchType="content", pattern="authentication"
294
+ 3. Present combined results: "Found 3 auth-related files and 8 files containing authentication code"
295
+
260
296
  SEARCH TYPES:
261
297
  - searchType="files": Find files by name (pattern matches file names)
262
298
  - searchType="content": Search inside files for text patterns
263
299
 
300
+ PATTERN MATCHING MODES:
301
+ - Default (literalSearch=false): Patterns are treated as regular expressions
302
+ - Literal (literalSearch=true): Patterns are treated as exact strings
303
+
304
+ WHEN TO USE literalSearch=true:
305
+ Use literal search when searching for code patterns with special characters:
306
+ - Function calls with parentheses and quotes
307
+ - Array access with brackets
308
+ - Object methods with dots and parentheses
309
+ - File paths with backslashes
310
+ - Any pattern containing: . * + ? ^ $ { } [ ] | \\ ( )
311
+
264
312
  IMPORTANT PARAMETERS:
265
313
  - pattern: What to search for (file names OR content text)
314
+ - literalSearch: Use exact string matching instead of regex (default: false)
266
315
  - filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json")
267
316
  - ignoreCase: Case-insensitive search (default: true). Works for both file names and content.
268
317
  - earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches)
269
318
 
270
- EXAMPLES:
271
- - Find package.json files: searchType="files", pattern="package.json", filePattern="package.json"
272
- - Find all JS files: searchType="files", pattern="*.js" (or use filePattern="*.js")
273
- - Search for "TODO" in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
274
- - Case-sensitive file search: searchType="files", pattern="README", ignoreCase=false
275
- - Case-insensitive file search: searchType="files", pattern="readme", ignoreCase=true
276
- - Find exact file, stop after first match: searchType="files", pattern="config.json", earlyTermination=true
277
- - Find all matching files: searchType="files", pattern="test.js", earlyTermination=false
319
+ DECISION EXAMPLES:
320
+ - "find package.json" searchType="files", pattern="package.json" (specific file)
321
+ - "find authentication components" searchType="content", pattern="authentication" (looking for functionality)
322
+ - "locate all React components" searchType="files", pattern="*.tsx" or "*.jsx" (file pattern)
323
+ - "find TODO comments" searchType="content", pattern="TODO" (text in files)
324
+ - "show me login files" → AMBIGUOUS → run both: files with "login" AND content with "login"
325
+ - "find config" AMBIGUOUS run both: config files AND files containing config code
326
+
327
+ COMPREHENSIVE SEARCH EXAMPLES:
328
+ - Find package.json files: searchType="files", pattern="package.json"
329
+ - Find all JS files: searchType="files", pattern="*.js"
330
+ - Search for TODO in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
331
+ - Search for exact code: searchType="content", pattern="toast.error('test')", literalSearch=true
332
+ - Ambiguous request "find auth stuff": Run two searches:
333
+ 1. searchType="files", pattern="auth"
334
+ 2. searchType="content", pattern="authentication"
335
+
336
+ PRO TIP: When user requests are ambiguous about whether they want files or content,
337
+ run both searches concurrently and combine results for comprehensive coverage.
278
338
 
279
339
  Unlike regular search tools, this starts a background search process and returns
280
340
  immediately with a session ID. Use get_more_search_results to get results as they
@@ -982,3 +1042,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
982
1042
  };
983
1043
  }
984
1044
  });
1045
+ // Add no-op handlers so Visual Studio initialization succeeds
1046
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] }));
@@ -161,6 +161,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
161
161
  contextLines: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
162
162
  timeout_ms: z.ZodOptional<z.ZodNumber>;
163
163
  earlyTermination: z.ZodOptional<z.ZodBoolean>;
164
+ literalSearch: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
164
165
  }, "strip", z.ZodTypeAny, {
165
166
  path: string;
166
167
  pattern: string;
@@ -168,6 +169,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
168
169
  ignoreCase: boolean;
169
170
  includeHidden: boolean;
170
171
  contextLines: number;
172
+ literalSearch: boolean;
171
173
  timeout_ms?: number | undefined;
172
174
  filePattern?: string | undefined;
173
175
  maxResults?: number | undefined;
@@ -183,6 +185,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
183
185
  includeHidden?: boolean | undefined;
184
186
  contextLines?: number | undefined;
185
187
  earlyTermination?: boolean | undefined;
188
+ literalSearch?: boolean | undefined;
186
189
  }>;
187
190
  export declare const GetMoreSearchResultsArgsSchema: z.ZodObject<{
188
191
  sessionId: z.ZodString;
@@ -95,6 +95,7 @@ export const StartSearchArgsSchema = z.object({
95
95
  contextLines: z.number().optional().default(5),
96
96
  timeout_ms: z.number().optional(), // Match process naming convention
97
97
  earlyTermination: z.boolean().optional(), // Stop search early when exact filename match is found (default: true for files, false for content)
98
+ literalSearch: z.boolean().optional().default(false), // Force literal string matching (-F flag) instead of regex
98
99
  });
99
100
  export const GetMoreSearchResultsArgsSchema = z.object({
100
101
  sessionId: z.string(),
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ import { ChildProcess } from 'child_process';
2
2
  import { FilteredStdioServerTransport } from './custom-stdio.js';
3
3
  declare global {
4
4
  var mcpTransport: FilteredStdioServerTransport | undefined;
5
+ var disableOnboarding: boolean | undefined;
5
6
  }
6
7
  export interface ProcessInfo {
7
8
  pid: number;
@@ -299,6 +299,21 @@ class UsageTracker {
299
299
  * Check if user should see onboarding invitation - SIMPLE VERSION
300
300
  */
301
301
  async shouldShowOnboarding() {
302
+ // Check if onboarding is disabled via command line argument
303
+ if (global.disableOnboarding) {
304
+ return false;
305
+ }
306
+ // Check if client is desktop-commander (disable for this client)
307
+ try {
308
+ const { currentClient } = await import('../server.js');
309
+ if (currentClient?.name === 'desktop-commander') {
310
+ return false;
311
+ }
312
+ }
313
+ catch (error) {
314
+ // If we can't import server, continue with other checks
315
+ console.log('[ONBOARDING DEBUG] Could not check client name, continuing...');
316
+ }
302
317
  const stats = await this.getStats();
303
318
  const onboardingState = await this.getOnboardingState();
304
319
  const now = Date.now();
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.13";
1
+ export declare const VERSION = "0.2.15";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.13';
1
+ export const VERSION = '0.2.15';
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "MCP server for terminal operations and file editing",
5
+ "mcpName": "io.github.wonderwhy-er/desktop-commander",
5
6
  "license": "MIT",
6
7
  "author": "Eduards Ruzga",
7
8
  "homepage": "https://github.com/wonderwhy-er/DesktopCommanderMCP",
@@ -69,7 +70,7 @@
69
70
  "file-operations"
70
71
  ],
71
72
  "dependencies": {
72
- "@modelcontextprotocol/sdk": "^1.8.0",
73
+ "@modelcontextprotocol/sdk": "^1.9.0",
73
74
  "@vscode/ripgrep": "^1.15.9",
74
75
  "cross-fetch": "^4.1.0",
75
76
  "fastest-levenshtein": "^1.0.16",