@wonderwhy-er/desktop-commander 0.2.13 → 0.2.14

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.
@@ -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
+ });
@@ -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.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
@@ -257,24 +257,72 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
257
257
  description: `
258
258
  Start a streaming search that can return results progressively.
259
259
 
260
+ SEARCH STRATEGY GUIDE:
261
+ Choose the right search type based on what the user is looking for:
262
+
263
+ USE searchType="files" WHEN:
264
+ - User asks for specific files: "find package.json", "locate config files"
265
+ - Pattern looks like a filename: "*.js", "README.md", "test-*.tsx"
266
+ - User wants to find files by name/extension: "all TypeScript files", "Python scripts"
267
+ - Looking for configuration/setup files: ".env", "dockerfile", "tsconfig.json"
268
+
269
+ USE searchType="content" WHEN:
270
+ - User asks about code/logic: "authentication logic", "error handling", "API calls"
271
+ - Looking for functions/variables: "getUserData function", "useState hook"
272
+ - Searching for text/comments: "TODO items", "FIXME comments", "documentation"
273
+ - Finding patterns in code: "console.log statements", "import statements"
274
+ - User describes functionality: "components that handle login", "files with database queries"
275
+
276
+ WHEN UNSURE OR USER REQUEST IS AMBIGUOUS:
277
+ Run TWO searches in parallel - one for files and one for content:
278
+
279
+ Example approach for ambiguous queries like "find authentication stuff":
280
+ 1. Start file search: searchType="files", pattern="auth"
281
+ 2. Simultaneously start content search: searchType="content", pattern="authentication"
282
+ 3. Present combined results: "Found 3 auth-related files and 8 files containing authentication code"
283
+
260
284
  SEARCH TYPES:
261
285
  - searchType="files": Find files by name (pattern matches file names)
262
286
  - searchType="content": Search inside files for text patterns
263
287
 
288
+ PATTERN MATCHING MODES:
289
+ - Default (literalSearch=false): Patterns are treated as regular expressions
290
+ - Literal (literalSearch=true): Patterns are treated as exact strings
291
+
292
+ WHEN TO USE literalSearch=true:
293
+ Use literal search when searching for code patterns with special characters:
294
+ - Function calls with parentheses and quotes
295
+ - Array access with brackets
296
+ - Object methods with dots and parentheses
297
+ - File paths with backslashes
298
+ - Any pattern containing: . * + ? ^ $ { } [ ] | \\ ( )
299
+
264
300
  IMPORTANT PARAMETERS:
265
301
  - pattern: What to search for (file names OR content text)
302
+ - literalSearch: Use exact string matching instead of regex (default: false)
266
303
  - filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json")
267
304
  - ignoreCase: Case-insensitive search (default: true). Works for both file names and content.
268
305
  - earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches)
269
306
 
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
307
+ DECISION EXAMPLES:
308
+ - "find package.json" → searchType="files", pattern="package.json" (specific file)
309
+ - "find authentication components" → searchType="content", pattern="authentication" (looking for functionality)
310
+ - "locate all React components" → searchType="files", pattern="*.tsx" or "*.jsx" (file pattern)
311
+ - "find TODO comments" → searchType="content", pattern="TODO" (text in files)
312
+ - "show me login files" → AMBIGUOUS → run both: files with "login" AND content with "login"
313
+ - "find config" → AMBIGUOUS → run both: config files AND files containing config code
314
+
315
+ COMPREHENSIVE SEARCH EXAMPLES:
316
+ - Find package.json files: searchType="files", pattern="package.json"
317
+ - Find all JS files: searchType="files", pattern="*.js"
318
+ - Search for TODO in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
319
+ - Search for exact code: searchType="content", pattern="toast.error('test')", literalSearch=true
320
+ - Ambiguous request "find auth stuff": Run two searches:
321
+ 1. searchType="files", pattern="auth"
322
+ 2. searchType="content", pattern="authentication"
323
+
324
+ PRO TIP: When user requests are ambiguous about whether they want files or content,
325
+ run both searches concurrently and combine results for comprehensive coverage.
278
326
 
279
327
  Unlike regular search tools, this starts a background search process and returns
280
328
  immediately with a session ID. Use get_more_search_results to get results as they
@@ -982,3 +1030,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
982
1030
  };
983
1031
  }
984
1032
  });
1033
+ // Add no-op handlers so Visual Studio initialization succeeds
1034
+ 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/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.13";
1
+ export declare const VERSION = "0.2.14";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.13';
1
+ export const VERSION = '0.2.14';
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.14",
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",