@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 +43 -0
- package/dist/custom-stdio.js +4 -2
- package/dist/handlers/search-handlers.js +1 -0
- package/dist/index-oauth.d.ts +2 -0
- package/dist/index-oauth.js +201 -0
- package/dist/index.js +28 -10
- package/dist/oauth/provider.d.ts +22 -0
- package/dist/oauth/provider.js +124 -0
- package/dist/oauth/server.d.ts +18 -0
- package/dist/oauth/server.js +160 -0
- package/dist/oauth/types.d.ts +54 -0
- package/dist/oauth/types.js +2 -0
- package/dist/search-manager.d.ts +1 -0
- package/dist/search-manager.js +4 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +76 -14
- package/dist/tools/schemas.d.ts +3 -0
- package/dist/tools/schemas.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/usageTracker.js +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
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:
|
package/dist/custom-stdio.js
CHANGED
|
@@ -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
|
-
//
|
|
26
|
-
|
|
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,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
|
-
|
|
34
|
+
deferLog('info', 'Loading configuration...');
|
|
23
35
|
await configManager.loadConfig();
|
|
24
|
-
|
|
36
|
+
deferLog('info', 'Configuration loaded successfully');
|
|
25
37
|
}
|
|
26
38
|
catch (configError) {
|
|
27
|
-
|
|
39
|
+
deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
28
40
|
if (configError instanceof Error && configError.stack) {
|
|
29
|
-
|
|
41
|
+
deferLog('debug', `Stack trace: ${configError.stack}`);
|
|
30
42
|
}
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
73
|
-
|
|
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
|
+
}
|
package/dist/search-manager.d.ts
CHANGED
package/dist/search-manager.js
CHANGED
|
@@ -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
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
|
-
|
|
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
|
-
//
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
|
|
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: [] }));
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/schemas.js
CHANGED
|
@@ -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.
|
|
1
|
+
export declare const VERSION = "0.2.15";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
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.
|
|
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.
|
|
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",
|