@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.
- 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/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.js +59 -9
- package/dist/tools/schemas.d.ts +3 -0
- package/dist/tools/schemas.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -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
|
+
});
|
|
@@ -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.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
|
-
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
|
|
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: [] }));
|
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/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.14";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
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.
|
|
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.
|
|
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",
|