@wonderwhy-er/desktop-commander 0.2.29-alpha.4 → 0.2.29-alpha.6

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.
@@ -10,7 +10,7 @@ export declare class DesktopCommanderIntegration {
10
10
  private isReady;
11
11
  initialize(): Promise<void>;
12
12
  resolveMcpConfig(): Promise<McpConfig | null>;
13
- executeTool(toolName: string, args: any): Promise<{
13
+ callClientTool(toolName: string, args: any, metadata?: any): Promise<{
14
14
  [x: string]: unknown;
15
15
  content: ({
16
16
  type: "text";
@@ -102,7 +102,7 @@ export declare class DesktopCommanderIntegration {
102
102
  } | undefined;
103
103
  } | undefined;
104
104
  }>;
105
- getCapabilities(): Promise<{
105
+ listClientTools(): Promise<{
106
106
  tools: {
107
107
  inputSchema: {
108
108
  [x: string]: unknown;
@@ -75,16 +75,17 @@ export class DesktopCommanderIntegration {
75
75
  }
76
76
  return null;
77
77
  }
78
- async executeTool(toolName, args) {
78
+ async callClientTool(toolName, args, metadata) {
79
79
  if (!this.isReady || !this.mcpClient) {
80
80
  throw new Error('DesktopIntegration not initialized');
81
81
  }
82
82
  // Proxy other tools to MCP server
83
83
  try {
84
- console.log(`Forwarding tool call ${toolName} to MCP server`);
84
+ console.log(`Forwarding tool call ${toolName} to MCP server`, metadata);
85
85
  const result = await this.mcpClient.callTool({
86
86
  name: toolName,
87
- arguments: args
87
+ arguments: args,
88
+ _meta: { remote: true, ...metadata || {} }
88
89
  });
89
90
  return result;
90
91
  }
@@ -93,7 +94,7 @@ export class DesktopCommanderIntegration {
93
94
  throw error;
94
95
  }
95
96
  }
96
- async getCapabilities() {
97
+ async listClientTools() {
97
98
  if (!this.mcpClient)
98
99
  return { tools: [] };
99
100
  try {
@@ -6,8 +6,10 @@ export declare class DeviceAuthenticator {
6
6
  private baseServerUrl;
7
7
  constructor(baseServerUrl: string);
8
8
  authenticate(): Promise<AuthSession>;
9
- private isDesktopEnvironment;
10
- private authenticateDesktop;
11
- private authenticateHeadless;
9
+ private generatePKCE;
10
+ private requestDeviceCode;
11
+ private displayUserInstructions;
12
+ private pollForAuthorization;
13
+ private sleep;
12
14
  }
13
15
  export {};
@@ -1,154 +1,118 @@
1
- import express from 'express';
2
- import { createServer } from 'http';
3
1
  import open from 'open';
4
- import readline from 'readline';
5
- import { authSuccessHtml } from './templates/auth-success.js';
6
- function escapeHtml(text) {
7
- if (text === null || text === undefined)
8
- return '';
9
- return String(text)
10
- .replace(/&/g, "&amp;")
11
- .replace(/</g, "&lt;")
12
- .replace(/>/g, "&gt;")
13
- .replace(/"/g, "&quot;")
14
- .replace(/'/g, "&#039;");
15
- }
16
- const CALLBACK_PORT = 8121;
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+ const CLIENT_ID = 'mcp-device';
17
5
  export class DeviceAuthenticator {
18
6
  constructor(baseServerUrl) {
19
7
  this.baseServerUrl = baseServerUrl;
20
8
  }
21
9
  async authenticate() {
22
- // Detect environment
23
- const isDesktop = this.isDesktopEnvironment();
24
- console.log(`🔐 Starting authentication (${isDesktop ? 'desktop' : 'headless'} mode)...`);
25
- if (isDesktop) {
26
- return this.authenticateDesktop();
27
- }
28
- else {
29
- return this.authenticateHeadless();
30
- }
10
+ console.log('🔐 Starting device authorization flow...\n');
11
+ // Generate PKCE
12
+ const pkce = this.generatePKCE();
13
+ // Step 1: Request device code
14
+ const deviceAuth = await this.requestDeviceCode(pkce.challenge);
15
+ // Step 2: Display user instructions and open browser
16
+ this.displayUserInstructions(deviceAuth);
17
+ // Step 3: Poll for authorization
18
+ const tokens = await this.pollForAuthorization(deviceAuth, pkce.verifier);
19
+ console.log(' - ✅ Authorization successful!\n');
20
+ return tokens;
31
21
  }
32
- isDesktopEnvironment() {
33
- // Check if we're in a desktop environment
34
- return process.platform === 'darwin' ||
35
- process.platform === 'win32' ||
36
- (process.platform === 'linux' && !!process.env.DISPLAY);
22
+ generatePKCE() {
23
+ const verifier = crypto.randomBytes(32).toString('base64url');
24
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
25
+ return { verifier, challenge };
37
26
  }
38
- async authenticateDesktop() {
39
- const app = express();
40
- const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
41
- return new Promise((resolve, reject) => {
42
- let server;
43
- // Setup callback handler
44
- app.get('/callback', (req, res) => {
45
- const { access_token, refresh_token, code, error, error_description } = req.query;
46
- // Extract the actual token (could be in access_token or code parameter)
47
- const token = access_token || code;
48
- if (error) {
49
- const safeError = escapeHtml(error);
50
- const safeErrorDesc = escapeHtml(error_description || 'Unknown error');
51
- res.send(`
52
- <h2>Authentication Failed</h2>
53
- <p>Error: ${safeError}</p>
54
- <p>Description: ${safeErrorDesc}</p>
55
- <p>You can close this window.</p>
56
- `);
57
- server.close();
58
- reject(new Error(`${error}: ${error_description}`));
59
- }
60
- else if (token) {
61
- res.send(authSuccessHtml);
62
- server.close();
63
- console.log(' - ✅ Authentication successful, token received');
64
- resolve({
65
- access_token: token,
66
- refresh_token: refresh_token || null
67
- });
68
- }
69
- else {
70
- console.log('❌ No token found in callback:', req.query);
71
- const safeParams = escapeHtml(Object.keys(req.query).join(', '));
72
- res.send(`
73
- <h2>Authentication Failed</h2>
74
- <p>No access token received</p>
75
- <p>Received parameters: ${safeParams}</p>
76
- <p>You can close this window.</p>
77
- `);
78
- server.close();
79
- reject(new Error('No access token received'));
80
- }
81
- });
82
- // Start callback server
83
- server = createServer(app);
84
- server.listen(CALLBACK_PORT, () => {
85
- const authUrl = `${this.baseServerUrl}/?redirect_uri=${encodeURIComponent(callbackUrl)}&device=true`;
86
- console.log(' - 🌐 Opening browser for authentication...');
87
- console.log(` - If browser doesn't open, visit: ${authUrl}`);
88
- // Open browser
89
- open(authUrl).catch(() => {
90
- console.log(' - Could not open browser automatically.');
91
- console.log(` - Please visit: ${authUrl}`);
92
- });
93
- });
94
- server.on('error', (err) => {
95
- reject(new Error(`Failed to start callback server: ${err.message}`));
96
- });
97
- // Timeout after 5 minutes
98
- setTimeout(() => {
99
- if (server.listening) {
100
- server.close();
101
- reject(new Error(' - Authentication timeout - no response received'));
102
- }
103
- }, 5 * 60 * 1000);
27
+ async requestDeviceCode(codeChallenge) {
28
+ console.log(' - 📡 Requesting device code...');
29
+ const response = await fetch(`${this.baseServerUrl}/device/start`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({
33
+ client_id: CLIENT_ID,
34
+ scope: 'mcp:tools',
35
+ device_name: os.hostname(),
36
+ device_type: 'mcp',
37
+ code_challenge: codeChallenge,
38
+ code_challenge_method: 'S256',
39
+ }),
104
40
  });
41
+ if (!response.ok) {
42
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
43
+ throw new Error(error.error_description || 'Failed to start device flow');
44
+ }
45
+ const data = await response.json();
46
+ console.log(' - ✅ Device code received\n');
47
+ return data;
105
48
  }
106
- async authenticateHeadless() {
107
- console.log('\n🔗 Manual Authentication Required:');
108
- console.log(''.repeat(50));
109
- console.log(`1. Open this URL in a browser: ${this.baseServerUrl}/`);
110
- console.log('2. Complete the authentication process');
111
- console.log('3. You will be redirected to a URL with parameters.');
112
- console.log(' If using device mode, look for access_token and refresh_token.');
113
- console.log('4. Copy the access_token (and refresh_token if available) and paste here.');
114
- console.log(' Format: access_token OR {"access_token":"...", "refresh_token":"..."}');
115
- console.log(''.repeat(50));
116
- const rl = readline.createInterface({
117
- input: process.stdin,
118
- output: process.stdout
49
+ displayUserInstructions(deviceAuth) {
50
+ console.log('📋 Please complete authentication:\n');
51
+ console.log(' 1. Open this URL in your browser:');
52
+ console.log(` ${deviceAuth.verification_uri}\n`);
53
+ console.log(' 2. Enter this code when prompted:');
54
+ console.log(` ${deviceAuth.user_code}\n`);
55
+ console.log(` Code expires in ${Math.floor(deviceAuth.expires_in / 60)} minutes.\n`);
56
+ // Try to open browser automatically
57
+ open(deviceAuth.verification_uri_complete).catch(() => {
58
+ console.log(' - Could not open browser automatically.');
59
+ console.log(` - Please visit: ${deviceAuth.verification_uri}\n`);
119
60
  });
120
- return new Promise((resolve, reject) => {
121
- rl.question('\n🔑 Enter Access Token or JSON: ', (input) => {
122
- rl.close();
123
- const trimmedInput = input.trim();
124
- if (!trimmedInput) {
125
- reject(new Error('Empty input provided'));
126
- return;
127
- }
128
- try {
129
- // Try parsing as JSON first
130
- const json = JSON.parse(trimmedInput);
131
- if (json.access_token) {
132
- resolve({
133
- access_token: json.access_token,
134
- refresh_token: json.refresh_token || null
135
- });
136
- return;
61
+ console.log(' - Waiting for authorization...\n');
62
+ }
63
+ async pollForAuthorization(deviceAuth, codeVerifier) {
64
+ const interval = (deviceAuth.interval || 5) * 1000;
65
+ const maxAttempts = Math.floor(deviceAuth.expires_in / (deviceAuth.interval || 5));
66
+ let attempt = 0;
67
+ while (attempt < maxAttempts) {
68
+ attempt++;
69
+ // Wait before polling
70
+ await this.sleep(interval);
71
+ try {
72
+ const response = await fetch(`${this.baseServerUrl}/device/poll`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({
76
+ device_code: deviceAuth.device_code,
77
+ client_id: CLIENT_ID,
78
+ code_verifier: codeVerifier,
79
+ }),
80
+ });
81
+ if (response.ok) {
82
+ const tokens = await response.json();
83
+ if (tokens.access_token) {
84
+ return {
85
+ access_token: tokens.access_token,
86
+ refresh_token: tokens.refresh_token || null,
87
+ };
137
88
  }
138
89
  }
139
- catch (e) {
140
- // Not JSON, treat as raw token
90
+ const error = await response.json().catch(() => ({ error: 'unknown' }));
91
+ // Check error type
92
+ if (error.error === 'authorization_pending') {
93
+ // Still waiting - continue polling
94
+ continue;
141
95
  }
142
- if (trimmedInput.length < 10) {
143
- reject(new Error('Invalid token format (too short)'));
96
+ if (error.error === 'slow_down') {
97
+ // Server requested slower polling
98
+ await this.sleep(interval);
99
+ continue;
144
100
  }
145
- else {
146
- resolve({
147
- access_token: trimmedInput,
148
- refresh_token: null
149
- });
101
+ // Terminal error
102
+ throw new Error(error.error_description || error.error || 'Authorization failed');
103
+ }
104
+ catch (fetchError) {
105
+ // Network error - retry unless we're out of attempts
106
+ if (attempt >= maxAttempts) {
107
+ throw fetchError;
150
108
  }
151
- });
152
- });
109
+ // Continue polling on network errors
110
+ continue;
111
+ }
112
+ }
113
+ throw new Error('Authorization timeout - user did not authorize within the time limit');
114
+ }
115
+ sleep(ms) {
116
+ return new Promise((resolve) => setTimeout(resolve, ms));
153
117
  }
154
118
  }
@@ -24,15 +24,16 @@ export class MCPDevice {
24
24
  const handleShutdown = async (signal) => {
25
25
  if (this.isShuttingDown) {
26
26
  console.log(`\n${signal} received, but already shutting down...`);
27
+ // Force exit if we get multiple signals
28
+ process.exit(1);
27
29
  return;
28
30
  }
29
- this.isShuttingDown = true;
30
31
  console.log(`\n${signal} received, initiating graceful shutdown...`);
31
- // Force exit after 3 seconds if graceful shutdown hangs
32
+ // Force exit after 2 seconds if graceful shutdown hangs
32
33
  const forceExit = setTimeout(() => {
33
- console.error('⚠️ Graceful shutdown timed out, forcing exit...');
34
+ console.error('\n⚠️ Graceful shutdown timed out, forcing exit...');
34
35
  process.exit(1);
35
- }, 3000);
36
+ }, 2000);
36
37
  try {
37
38
  await this.shutdown();
38
39
  clearTimeout(forceExit);
@@ -43,11 +44,21 @@ export class MCPDevice {
43
44
  process.exit(1);
44
45
  }
45
46
  };
47
+ // Remove any existing SIGINT/SIGTERM listeners to prevent default behavior
48
+ process.removeAllListeners('SIGINT');
49
+ process.removeAllListeners('SIGTERM');
50
+ // Add our custom handlers
46
51
  process.on('SIGINT', () => {
47
- handleShutdown('SIGINT');
52
+ handleShutdown('SIGINT').catch((error) => {
53
+ console.error('Fatal error during shutdown:', error);
54
+ process.exit(1);
55
+ });
48
56
  });
49
57
  process.on('SIGTERM', () => {
50
- handleShutdown('SIGTERM');
58
+ handleShutdown('SIGTERM').catch((error) => {
59
+ console.error('Fatal error during shutdown:', error);
60
+ process.exit(1);
61
+ });
51
62
  });
52
63
  }
53
64
  async start() {
@@ -106,13 +117,14 @@ export class MCPDevice {
106
117
  this.user = user;
107
118
  const deviceName = os.hostname();
108
119
  // Register as device
109
- this.deviceId = await this.remoteChannel.registerDevice(this.user.id, await this.desktop.getCapabilities(), this.deviceId, deviceName);
120
+ this.deviceId = await this.remoteChannel.registerDevice(this.user.id, await this.desktop.listClientTools(), this.deviceId, deviceName);
110
121
  // Also save session again just in case (optional, but harmless)
111
122
  const { data: { session: currentSession } } = await this.remoteChannel.getSession();
112
123
  await this.savePersistedConfig(currentSession);
113
124
  // Subscribe to tool calls
114
125
  await this.remoteChannel.subscribe(this.user.id, (payload) => this.handleNewToolCall(payload));
115
126
  console.log('✅ Device ready:');
127
+ console.log(` - User: ${this.user.email}`);
116
128
  console.log(` - Device ID: ${this.deviceId}`);
117
129
  console.log(` - Device Name: ${deviceName}`);
118
130
  // Keep process alive
@@ -161,7 +173,7 @@ export class MCPDevice {
161
173
  // Ensure the config directory exists
162
174
  await fs.mkdir(path.dirname(this.configPath), { recursive: true });
163
175
  await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
164
- // if (session) console.debug('💾 Session saved to device.json');
176
+ // if (session) console.debug('💾 Session saved to ' + this.configPath);
165
177
  }
166
178
  catch (error) {
167
179
  console.error(' - ❌ Failed to save config:', error.message);
@@ -176,7 +188,7 @@ export class MCPDevice {
176
188
  const config = await response.json();
177
189
  return {
178
190
  supabaseUrl: config.supabaseUrl,
179
- anonKey: config.supabaseAnonKey
191
+ anonKey: config.supabasePublishableKey
180
192
  };
181
193
  }
182
194
  // Methods moved to RemoteChannel
@@ -185,12 +197,12 @@ export class MCPDevice {
185
197
  // Assuming database also renames agent_id to device_id, but user only said rename agent -> device everywhere but only inside src/remote-device
186
198
  // If the database column is still agent_id, we need a mapping.
187
199
  // However, the user said "literally all agent should be renamed to device everywhere", so we assume DB column is device_id.
188
- const { id: call_id, tool_name, tool_args, device_id } = toolCall;
200
+ const { id: call_id, tool_name, tool_args, device_id, metadata = {} } = toolCall;
189
201
  // Only process jobs for this device
190
202
  if (device_id && device_id !== this.deviceId) {
191
203
  return;
192
204
  }
193
- console.log(`🔧 Received tool call ${call_id}: ${tool_name} ${JSON.stringify(tool_args)}`);
205
+ console.log(`🔧 Received tool call ${call_id}: ${tool_name} ${JSON.stringify(tool_args)} metadata: ${JSON.stringify(metadata)}`);
194
206
  try {
195
207
  // Update call status to executing
196
208
  await this.remoteChannel.markCallExecuting(call_id);
@@ -220,7 +232,7 @@ export class MCPDevice {
220
232
  }
221
233
  else {
222
234
  // Execute other tools using desktop integration
223
- result = await this.desktop.executeTool(tool_name, tool_args);
235
+ result = await this.desktop.callClientTool(tool_name, tool_args, metadata);
224
236
  }
225
237
  console.log(`✅ Tool call ${tool_name} completed:\r\n ${JSON.stringify(result)}`);
226
238
  // Update database with result
@@ -240,8 +252,11 @@ export class MCPDevice {
240
252
  this.isShuttingDown = true;
241
253
  console.log('\n🛑 Shutting down device...');
242
254
  try {
243
- // Remote shutdown
255
+ // Stop heartbeat first to prevent new operations
256
+ this.remoteChannel.stopHeartbeat();
257
+ // Unsubscribe from channel
244
258
  await this.remoteChannel.unsubscribe();
259
+ // Mark device offline
245
260
  await this.remoteChannel.setOffline(this.deviceId);
246
261
  // Shutdown desktop integration
247
262
  await this.desktop.shutdown();
@@ -42,6 +42,7 @@ export declare class RemoteChannel {
42
42
  updateCallResult(callId: string, status: string, result?: any, errorMessage?: string | null): Promise<void>;
43
43
  updateHeartbeat(deviceId: string): Promise<void>;
44
44
  startHeartbeat(deviceId: string): void;
45
+ stopHeartbeat(): void;
45
46
  setOffline(deviceId: string | null): Promise<void>;
46
47
  unsubscribe(): Promise<void>;
47
48
  }
@@ -79,7 +79,7 @@ export class RemoteChannel {
79
79
  await this.updateDevice(existingDevice.id, {
80
80
  status: 'online',
81
81
  last_seen: new Date().toISOString(),
82
- capabilities: capabilities,
82
+ capabilities: {}, // Not used atm
83
83
  device_name: deviceName
84
84
  });
85
85
  return existingDevice.id;
@@ -94,7 +94,7 @@ export class RemoteChannel {
94
94
  const { data: newDevice, error } = await this.createDevice({
95
95
  user_id: userId,
96
96
  device_name: deviceName,
97
- capabilities: capabilities,
97
+ capabilities: {}, // Not used atm
98
98
  status: 'online',
99
99
  last_seen: new Date().toISOString()
100
100
  });
@@ -108,7 +108,7 @@ export class RemoteChannel {
108
108
  async subscribe(userId, onToolCall) {
109
109
  if (!this.client)
110
110
  throw new Error('Client not initialized');
111
- console.debug(` - ⏳ Subscribing to call queue...`);
111
+ console.debug(` - ⏳ Subscribing to tool call channel...`);
112
112
  return new Promise((resolve, reject) => {
113
113
  if (!this.client)
114
114
  return reject(new Error('Client not initialized'));
@@ -121,16 +121,16 @@ export class RemoteChannel {
121
121
  }, (payload) => onToolCall(payload))
122
122
  .subscribe((status, err) => {
123
123
  if (status === 'SUBSCRIBED') {
124
- console.debug(' - 🔌 Connected to call queue');
124
+ console.debug(' - 🔌 Connected to tool call channel');
125
125
  resolve();
126
126
  }
127
127
  else if (status === 'CHANNEL_ERROR') {
128
- console.error(' - ❌ Failed to connect to call queue:', err);
129
- reject(err || new Error('Failed to initialize call queue subscription'));
128
+ console.error(' - ❌ Failed to connect to tool call channel:', err);
129
+ reject(err || new Error('Failed to initialize tool call channel subscription'));
130
130
  }
131
131
  else if (status === 'TIMED_OUT') {
132
- console.error(' - ❌ Connection to call queue timed out');
133
- reject(new Error('Call queue subscription timed out'));
132
+ console.error(' - ❌ Connection to tool call channel timed out');
133
+ reject(new Error('Tool call channel subscription timed out'));
134
134
  }
135
135
  });
136
136
  });
@@ -178,6 +178,12 @@ export class RemoteChannel {
178
178
  await this.updateHeartbeat(deviceId);
179
179
  }, HEARTBEAT_INTERVAL);
180
180
  }
181
+ stopHeartbeat() {
182
+ if (this.heartbeatInterval) {
183
+ clearInterval(this.heartbeatInterval);
184
+ this.heartbeatInterval = null;
185
+ }
186
+ }
181
187
  async setOffline(deviceId) {
182
188
  if (deviceId && this.client) {
183
189
  await this.client
@@ -189,10 +195,9 @@ export class RemoteChannel {
189
195
  }
190
196
  async unsubscribe() {
191
197
  if (this.channel) {
192
- if (this.heartbeatInterval)
193
- clearInterval(this.heartbeatInterval);
194
198
  await this.channel.unsubscribe();
195
- console.log('✓ Unsubscribed from channel');
199
+ this.channel = null;
200
+ console.log('✓ Unsubscribed from tool call channel');
196
201
  }
197
202
  }
198
203
  }
package/dist/server.js CHANGED
@@ -61,23 +61,34 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
61
61
  });
62
62
  // Store current client info (simple variable)
63
63
  let currentClient = { name: 'uninitialized', version: 'uninitialized' };
64
+ /**
65
+ * Unified way to update client information
66
+ */
67
+ async function updateCurrentClient(clientInfo) {
68
+ if (clientInfo.name !== currentClient.name || clientInfo.version !== currentClient.version) {
69
+ const nameChanged = clientInfo.name !== currentClient.name;
70
+ currentClient = {
71
+ name: clientInfo.name || currentClient.name,
72
+ version: clientInfo.version || currentClient.version
73
+ };
74
+ // Configure transport for client-specific behavior only if name changed
75
+ if (nameChanged) {
76
+ const transport = global.mcpTransport;
77
+ if (transport && typeof transport.configureForClient === 'function') {
78
+ transport.configureForClient(currentClient.name);
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+ return false;
84
+ }
64
85
  // Add handler for initialization method - capture client info
65
86
  server.setRequestHandler(InitializeRequestSchema, async (request) => {
66
87
  try {
67
88
  // Extract and store current client information
68
89
  const clientInfo = request.params?.clientInfo;
69
90
  if (clientInfo) {
70
- currentClient = {
71
- name: clientInfo.name || 'unknown',
72
- version: clientInfo.version || 'unknown'
73
- };
74
- // Configure transport for client-specific behavior
75
- const transport = global.mcpTransport;
76
- if (transport && typeof transport.configureForClient === 'function') {
77
- transport.configureForClient(currentClient.name);
78
- }
79
- // Defer client connection message until after initialization
80
- deferLog('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
91
+ await updateCurrentClient(clientInfo);
81
92
  // Welcome page for new claude-ai users (A/B test controlled)
82
93
  if (currentClient.name === 'claude-ai' && !global.disableOnboarding) {
83
94
  await handleWelcomePageOnboarding();
@@ -1032,8 +1043,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1032
1043
  const { name, arguments: args } = request.params;
1033
1044
  const startTime = Date.now();
1034
1045
  try {
1046
+ // Include _meta in debug log if present
1035
1047
  // Prepare telemetry data - add config key for set_config_value
1036
1048
  const telemetryData = { name };
1049
+ // Extract metadata from _meta field if present
1050
+ const metadata = request.params._meta;
1051
+ if (metadata && typeof metadata === 'object') {
1052
+ // add remote flag if present
1053
+ if (metadata.remote) {
1054
+ telemetryData.remote = metadata.remote;
1055
+ }
1056
+ // Dynamically update client info if provided in _meta
1057
+ // To use in capture later
1058
+ if (metadata.clientInfo) {
1059
+ await updateCurrentClient(metadata.clientInfo);
1060
+ telemetryData.client_name = metadata.clientInfo.name;
1061
+ telemetryData.client_version = metadata.clientInfo.version;
1062
+ }
1063
+ }
1037
1064
  if (name === 'set_config_value' && args && typeof args === 'object' && 'key' in args) {
1038
1065
  telemetryData.set_config_value_key_name = args.key;
1039
1066
  }
@@ -1049,6 +1076,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1049
1076
  }
1050
1077
  }
1051
1078
  capture_call_tool('server_call_tool', telemetryData);
1079
+ // console.log(`[TELEMETRY DEBUG] Captured for tool ${name}:`, JSON.stringify(telemetryData, null, 2));
1052
1080
  // Log every tool request name
1053
1081
  // logger.info(`Tool request: ${name}`, { toolName: name, timestamp: new Date().toISOString() });
1054
1082
  // Track tool call
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.29-alpha.4";
1
+ export declare const VERSION = "0.2.29-alpha.6";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.29-alpha.4';
1
+ export const VERSION = '0.2.29-alpha.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.29-alpha.4",
3
+ "version": "0.2.29-alpha.6",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",
@@ -85,10 +85,8 @@
85
85
  "@opendocsg/pdf2md": "^0.2.2",
86
86
  "@supabase/supabase-js": "^2.89.0",
87
87
  "@vscode/ripgrep": "^1.15.9",
88
- "@wonderwhy-er/desktop-commander": "^0.2.29-alpha.2",
89
88
  "cross-fetch": "^4.1.0",
90
89
  "exceljs": "^4.4.0",
91
- "express": "^4.22.1",
92
90
  "fastest-levenshtein": "^1.0.16",
93
91
  "file-type": "^21.1.1",
94
92
  "glob": "^10.3.10",
@@ -99,7 +97,6 @@
99
97
  "remark": "^15.0.1",
100
98
  "remark-gfm": "^4.0.1",
101
99
  "remark-parse": "^11.0.0",
102
- "remote": "^0.2.6",
103
100
  "sharp": "^0.34.5",
104
101
  "unified": "^11.0.5",
105
102
  "unpdf": "^1.4.0",
@@ -108,7 +105,6 @@
108
105
  },
109
106
  "devDependencies": {
110
107
  "@anthropic-ai/mcpb": "^1.2.0",
111
- "@types/express": "^5.0.6",
112
108
  "@types/node": "^20.17.24",
113
109
  "commander": "^13.1.0",
114
110
  "nexe": "^5.0.0-beta.4",
@@ -1,44 +0,0 @@
1
- export interface RemoteClientConfig {
2
- serverUrl: string;
3
- deviceToken: string;
4
- retryInterval?: number;
5
- maxRetries?: number;
6
- }
7
- export interface MCPRequest {
8
- jsonrpc: string;
9
- id: string | number;
10
- method: string;
11
- params?: any;
12
- }
13
- export interface MCPResponse {
14
- jsonrpc: string;
15
- id: string | number;
16
- result?: any;
17
- error?: {
18
- code: number;
19
- message: string;
20
- data?: any;
21
- };
22
- }
23
- export declare class RemoteClient {
24
- private ws;
25
- private config;
26
- private isAuthenticated;
27
- private deviceId;
28
- private pendingRequests;
29
- private reconnectAttempts;
30
- private reconnectTimer;
31
- constructor(config: RemoteClientConfig);
32
- connect(): Promise<void>;
33
- private handleMessage;
34
- private handleReconnect;
35
- private sendMessage;
36
- sendMCPRequest(request: MCPRequest): Promise<MCPResponse>;
37
- isConnected(): boolean;
38
- disconnect(): void;
39
- getStatus(): {
40
- connected: boolean;
41
- authenticated: boolean;
42
- deviceId: string | null;
43
- };
44
- }
@@ -1,174 +0,0 @@
1
- import WebSocket from 'ws';
2
- import { logger } from './utils/logger.js';
3
- export class RemoteClient {
4
- constructor(config) {
5
- this.ws = null;
6
- this.isAuthenticated = false;
7
- this.deviceId = null;
8
- this.pendingRequests = new Map();
9
- this.reconnectAttempts = 0;
10
- this.reconnectTimer = null;
11
- this.config = {
12
- retryInterval: 5000,
13
- maxRetries: 5,
14
- ...config
15
- };
16
- }
17
- async connect() {
18
- return new Promise((resolve, reject) => {
19
- try {
20
- logger.info(`Connecting to Remote MCP Server: ${this.config.serverUrl}`);
21
- this.ws = new WebSocket(this.config.serverUrl);
22
- this.ws.on('open', () => {
23
- logger.info('Connected to Remote MCP Server');
24
- this.reconnectAttempts = 0;
25
- // Send authentication
26
- this.sendMessage({
27
- id: 'auth-1',
28
- type: 'auth',
29
- payload: { deviceToken: this.config.deviceToken },
30
- timestamp: Date.now()
31
- });
32
- });
33
- this.ws.on('message', (data) => {
34
- try {
35
- const message = JSON.parse(data.toString());
36
- this.handleMessage(message);
37
- // Resolve connection promise on successful auth
38
- if (message.type === 'auth' && message.payload?.success && !this.isAuthenticated) {
39
- this.isAuthenticated = true;
40
- this.deviceId = message.payload.deviceId;
41
- logger.info(`Remote MCP authentication successful! Device ID: ${this.deviceId}`);
42
- resolve();
43
- }
44
- }
45
- catch (error) {
46
- logger.error('Error processing remote message:', error);
47
- }
48
- });
49
- this.ws.on('close', (code, reason) => {
50
- logger.info(`Remote MCP connection closed (${code}): ${reason}`);
51
- this.isAuthenticated = false;
52
- this.deviceId = null;
53
- this.handleReconnect();
54
- });
55
- this.ws.on('error', (error) => {
56
- logger.error('Remote MCP WebSocket error:', error.message);
57
- if (!this.isAuthenticated) {
58
- reject(new Error(`Failed to connect to Remote MCP Server: ${error.message}`));
59
- }
60
- });
61
- // Set up heartbeat
62
- setInterval(() => {
63
- if (this.isConnected()) {
64
- this.sendMessage({
65
- id: `heartbeat-${Date.now()}`,
66
- type: 'heartbeat',
67
- payload: { timestamp: Date.now() },
68
- timestamp: Date.now()
69
- });
70
- }
71
- }, 30000);
72
- }
73
- catch (error) {
74
- reject(error);
75
- }
76
- });
77
- }
78
- handleMessage(message) {
79
- if (message.type === 'mcp_response') {
80
- // Handle MCP response
81
- const request = this.pendingRequests.get(message.payload.id);
82
- if (request) {
83
- clearTimeout(request.timeout);
84
- this.pendingRequests.delete(message.payload.id);
85
- request.resolve(message.payload);
86
- }
87
- }
88
- else if (message.type === 'heartbeat') {
89
- // Respond to heartbeat
90
- this.sendMessage({
91
- id: message.id,
92
- type: 'heartbeat',
93
- payload: { timestamp: Date.now() },
94
- timestamp: Date.now()
95
- });
96
- }
97
- }
98
- handleReconnect() {
99
- if (this.reconnectAttempts >= (this.config.maxRetries || 5)) {
100
- logger.error('Max reconnection attempts reached. Giving up.');
101
- return;
102
- }
103
- this.reconnectAttempts++;
104
- const delay = (this.config.retryInterval || 5000) * this.reconnectAttempts;
105
- logger.info(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
106
- this.reconnectTimer = setTimeout(async () => {
107
- try {
108
- await this.connect();
109
- }
110
- catch (error) {
111
- logger.error('Reconnection failed:', error);
112
- this.handleReconnect();
113
- }
114
- }, delay);
115
- }
116
- sendMessage(message) {
117
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
118
- this.ws.send(JSON.stringify(message));
119
- }
120
- }
121
- async sendMCPRequest(request) {
122
- if (!this.isConnected() || !this.isAuthenticated) {
123
- throw new Error('Remote MCP client not connected or authenticated');
124
- }
125
- return new Promise((resolve, reject) => {
126
- const requestId = `mcp-${Date.now()}-${Math.random()}`;
127
- // Set up timeout
128
- const timeout = setTimeout(() => {
129
- this.pendingRequests.delete(request.id);
130
- reject(new Error('Remote MCP request timeout'));
131
- }, 30000); // 30 second timeout
132
- this.pendingRequests.set(request.id, {
133
- resolve,
134
- reject,
135
- timeout
136
- });
137
- // Send MCP request to remote server
138
- this.sendMessage({
139
- id: requestId,
140
- type: 'mcp_request',
141
- payload: request,
142
- timestamp: Date.now()
143
- });
144
- });
145
- }
146
- isConnected() {
147
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.isAuthenticated;
148
- }
149
- disconnect() {
150
- if (this.reconnectTimer) {
151
- clearTimeout(this.reconnectTimer);
152
- this.reconnectTimer = null;
153
- }
154
- // Clear pending requests
155
- this.pendingRequests.forEach(({ timeout, reject }) => {
156
- clearTimeout(timeout);
157
- reject(new Error('Client disconnecting'));
158
- });
159
- this.pendingRequests.clear();
160
- if (this.ws) {
161
- this.ws.close();
162
- this.ws = null;
163
- }
164
- this.isAuthenticated = false;
165
- this.deviceId = null;
166
- }
167
- getStatus() {
168
- return {
169
- connected: this.ws?.readyState === WebSocket.OPEN || false,
170
- authenticated: this.isAuthenticated,
171
- deviceId: this.deviceId
172
- };
173
- }
174
- }
@@ -1,45 +0,0 @@
1
- export interface RemoteSSEClientConfig {
2
- serverUrl: string;
3
- deviceToken: string;
4
- retryInterval?: number;
5
- maxRetries?: number;
6
- }
7
- export interface MCPRequest {
8
- jsonrpc: string;
9
- id: string | number;
10
- method: string;
11
- params?: any;
12
- }
13
- export interface MCPResponse {
14
- jsonrpc: string;
15
- id: string | number;
16
- result?: any;
17
- error?: {
18
- code: number;
19
- message: string;
20
- data?: any;
21
- };
22
- }
23
- export declare class RemoteSSEClient {
24
- private eventSource;
25
- private config;
26
- private isConnected;
27
- private isAuthenticated;
28
- private deviceId;
29
- private pendingRequests;
30
- private reconnectAttempts;
31
- private reconnectTimer;
32
- constructor(config: RemoteSSEClientConfig);
33
- connect(): Promise<void>;
34
- private connectWithFetch;
35
- private handleSSEMessage;
36
- sendMCPRequest(request: MCPRequest): Promise<MCPResponse>;
37
- private handleReconnect;
38
- isConnectedAndAuthenticated(): boolean;
39
- disconnect(): void;
40
- getStatus(): {
41
- connected: boolean;
42
- authenticated: boolean;
43
- deviceId: string | null;
44
- };
45
- }
@@ -1,243 +0,0 @@
1
- import { logger } from './utils/logger.js';
2
- export class RemoteSSEClient {
3
- constructor(config) {
4
- this.eventSource = null;
5
- this.isConnected = false;
6
- this.isAuthenticated = false;
7
- this.deviceId = null;
8
- this.pendingRequests = new Map();
9
- this.reconnectAttempts = 0;
10
- this.reconnectTimer = null;
11
- this.config = {
12
- retryInterval: 5000,
13
- maxRetries: 5,
14
- ...config
15
- };
16
- }
17
- async connect() {
18
- return new Promise((resolve, reject) => {
19
- try {
20
- const sseUrl = `${this.config.serverUrl}/sse?deviceToken=${encodeURIComponent(this.config.deviceToken)}`;
21
- logger.info(`Connecting to Remote MCP Server via SSE: ${sseUrl}`);
22
- // Check if EventSource is available (Node.js environment needs polyfill)
23
- if (typeof EventSource === 'undefined') {
24
- // For Node.js environment, we'll use a different approach
25
- this.connectWithFetch(sseUrl, resolve, reject);
26
- return;
27
- }
28
- this.eventSource = new EventSource(sseUrl);
29
- this.eventSource.onopen = () => {
30
- logger.info('SSE connection opened');
31
- this.isConnected = true;
32
- this.reconnectAttempts = 0;
33
- };
34
- this.eventSource.onmessage = (event) => {
35
- this.handleSSEMessage('message', event.data);
36
- };
37
- this.eventSource.addEventListener('connected', (event) => {
38
- this.handleSSEMessage('connected', event.data);
39
- this.isAuthenticated = true;
40
- resolve();
41
- });
42
- this.eventSource.addEventListener('mcp_response', (event) => {
43
- this.handleSSEMessage('mcp_response', event.data);
44
- });
45
- this.eventSource.addEventListener('heartbeat', (event) => {
46
- this.handleSSEMessage('heartbeat', event.data);
47
- });
48
- this.eventSource.onerror = (event) => {
49
- logger.error('SSE connection error:', event);
50
- if (!this.isAuthenticated) {
51
- reject(new Error('Failed to connect to Remote MCP Server via SSE'));
52
- }
53
- else {
54
- this.handleReconnect();
55
- }
56
- };
57
- }
58
- catch (error) {
59
- reject(error);
60
- }
61
- });
62
- }
63
- async connectWithFetch(sseUrl, resolve, reject) {
64
- try {
65
- // For Node.js environment, use fetch with streaming
66
- const fetch = (await import('cross-fetch')).default;
67
- const response = await fetch(sseUrl, {
68
- headers: {
69
- 'Accept': 'text/event-stream',
70
- 'Cache-Control': 'no-cache',
71
- },
72
- });
73
- if (!response.ok) {
74
- throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
75
- }
76
- this.isConnected = true;
77
- this.reconnectAttempts = 0;
78
- logger.info('SSE connection established via fetch');
79
- // Handle the readable stream
80
- const reader = response.body?.getReader();
81
- if (!reader) {
82
- throw new Error('No readable stream available');
83
- }
84
- const decoder = new TextDecoder();
85
- let buffer = '';
86
- // Process the stream
87
- const processStream = async () => {
88
- while (this.isConnected) {
89
- try {
90
- const { done, value } = await reader.read();
91
- if (done)
92
- break;
93
- buffer += decoder.decode(value, { stream: true });
94
- // Process complete SSE messages
95
- const lines = buffer.split('\n');
96
- buffer = lines.pop() || ''; // Keep incomplete line in buffer
97
- let eventType = 'message';
98
- let eventData = '';
99
- for (const line of lines) {
100
- if (line.startsWith('event: ')) {
101
- eventType = line.substring(7);
102
- }
103
- else if (line.startsWith('data: ')) {
104
- eventData = line.substring(6);
105
- }
106
- else if (line === '' && eventData) {
107
- // Complete event received
108
- this.handleSSEMessage(eventType, eventData);
109
- // Resolve on first successful connection
110
- if (eventType === 'connected' && !this.isAuthenticated) {
111
- this.isAuthenticated = true;
112
- resolve();
113
- }
114
- eventType = 'message';
115
- eventData = '';
116
- }
117
- }
118
- }
119
- catch (error) {
120
- logger.error('Error reading SSE stream:', error);
121
- break;
122
- }
123
- }
124
- };
125
- processStream().catch(error => {
126
- logger.error('SSE stream processing error:', error);
127
- if (!this.isAuthenticated) {
128
- reject(error);
129
- }
130
- else {
131
- this.handleReconnect();
132
- }
133
- });
134
- }
135
- catch (error) {
136
- reject(error);
137
- }
138
- }
139
- handleSSEMessage(eventType, data) {
140
- try {
141
- const message = JSON.parse(data);
142
- switch (eventType) {
143
- case 'connected':
144
- this.deviceId = message.deviceId;
145
- logger.info(`Remote MCP SSE authentication successful! Device ID: ${this.deviceId}`);
146
- break;
147
- case 'mcp_response':
148
- // This shouldn't happen in our architecture since the local agent sends responses directly
149
- // But we can handle it for completeness
150
- logger.info('Received unexpected mcp_response via SSE');
151
- break;
152
- case 'heartbeat':
153
- // Silent heartbeat handling
154
- break;
155
- default:
156
- logger.info(`Received SSE event: ${eventType}`, message);
157
- }
158
- }
159
- catch (error) {
160
- logger.error('Error processing SSE message:', error);
161
- }
162
- }
163
- async sendMCPRequest(request) {
164
- if (!this.isConnected || !this.isAuthenticated) {
165
- throw new Error('Remote MCP SSE client not connected or authenticated');
166
- }
167
- // In the SSE architecture, we don't send requests directly via SSE
168
- // Instead, we send them via HTTP POST to the server's MCP endpoint
169
- return new Promise(async (resolve, reject) => {
170
- try {
171
- const fetch = (await import('cross-fetch')).default;
172
- const response = await fetch(`${this.config.serverUrl}/api/mcp/execute`, {
173
- method: 'POST',
174
- headers: {
175
- 'Content-Type': 'application/json',
176
- 'Authorization': `Bearer ${this.config.deviceToken}`
177
- },
178
- body: JSON.stringify({
179
- deviceToken: this.config.deviceToken,
180
- request: request
181
- })
182
- });
183
- if (!response.ok) {
184
- throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
185
- }
186
- const result = await response.json();
187
- resolve(result);
188
- }
189
- catch (error) {
190
- reject(error);
191
- }
192
- });
193
- }
194
- handleReconnect() {
195
- if (this.reconnectAttempts >= (this.config.maxRetries || 5)) {
196
- logger.error('Max SSE reconnection attempts reached. Giving up.');
197
- return;
198
- }
199
- this.isConnected = false;
200
- this.isAuthenticated = false;
201
- this.reconnectAttempts++;
202
- const delay = (this.config.retryInterval || 5000) * this.reconnectAttempts;
203
- logger.info(`Attempting to reconnect SSE in ${delay}ms (attempt ${this.reconnectAttempts})`);
204
- this.reconnectTimer = setTimeout(async () => {
205
- try {
206
- await this.connect();
207
- }
208
- catch (error) {
209
- logger.error('SSE reconnection failed:', error);
210
- this.handleReconnect();
211
- }
212
- }, delay);
213
- }
214
- isConnectedAndAuthenticated() {
215
- return this.isConnected && this.isAuthenticated;
216
- }
217
- disconnect() {
218
- if (this.reconnectTimer) {
219
- clearTimeout(this.reconnectTimer);
220
- this.reconnectTimer = null;
221
- }
222
- // Clear pending requests
223
- this.pendingRequests.forEach(({ timeout, reject }) => {
224
- clearTimeout(timeout);
225
- reject(new Error('Client disconnecting'));
226
- });
227
- this.pendingRequests.clear();
228
- if (this.eventSource) {
229
- this.eventSource.close();
230
- this.eventSource = null;
231
- }
232
- this.isConnected = false;
233
- this.isAuthenticated = false;
234
- this.deviceId = null;
235
- }
236
- getStatus() {
237
- return {
238
- connected: this.isConnected,
239
- authenticated: this.isAuthenticated,
240
- deviceId: this.deviceId
241
- };
242
- }
243
- }
@@ -1,20 +0,0 @@
1
- export declare function connectRemoteMCP(serverUrl: string, deviceToken: string): Promise<{
2
- success: boolean;
3
- message: string;
4
- status?: any;
5
- }>;
6
- export declare function disconnectRemoteMCP(): {
7
- success: boolean;
8
- message: string;
9
- };
10
- export declare function getRemoteMCPStatus(): {
11
- connected: boolean;
12
- authenticated: boolean;
13
- deviceId: string | null;
14
- message: string;
15
- };
16
- export declare function executeRemoteMCP(method: string, params?: any): Promise<{
17
- success: boolean;
18
- result?: any;
19
- error?: string;
20
- }>;
@@ -1,149 +0,0 @@
1
- import { RemoteSSEClient } from '../remote-sse-client.js';
2
- import { logger } from '../utils/logger.js';
3
- let remoteClient = null;
4
- export async function connectRemoteMCP(serverUrl, deviceToken) {
5
- try {
6
- // Disconnect existing client if any
7
- if (remoteClient) {
8
- logger.info('Disconnecting existing remote MCP client');
9
- remoteClient.disconnect();
10
- }
11
- // Create new client
12
- const config = {
13
- serverUrl,
14
- deviceToken,
15
- retryInterval: 5000,
16
- maxRetries: 3
17
- };
18
- logger.info(`Connecting to Remote MCP Server via SSE: ${serverUrl}`);
19
- remoteClient = new RemoteSSEClient(config);
20
- await remoteClient.connect();
21
- const status = remoteClient.getStatus();
22
- logger.info('Remote MCP connection successful', status);
23
- return {
24
- success: true,
25
- message: `Connected to Remote MCP Server via SSE successfully. Device ID: ${status.deviceId}`,
26
- status
27
- };
28
- }
29
- catch (error) {
30
- const errorMessage = error instanceof Error ? error.message : String(error);
31
- logger.error('Failed to connect to Remote MCP Server:', errorMessage);
32
- return {
33
- success: false,
34
- message: `Failed to connect to Remote MCP Server: ${errorMessage}`
35
- };
36
- }
37
- }
38
- export function disconnectRemoteMCP() {
39
- try {
40
- if (remoteClient) {
41
- logger.info('Disconnecting Remote MCP client');
42
- remoteClient.disconnect();
43
- remoteClient = null;
44
- return {
45
- success: true,
46
- message: 'Disconnected from Remote MCP Server'
47
- };
48
- }
49
- else {
50
- return {
51
- success: true,
52
- message: 'No active Remote MCP connection to disconnect'
53
- };
54
- }
55
- }
56
- catch (error) {
57
- const errorMessage = error instanceof Error ? error.message : String(error);
58
- logger.error('Error disconnecting Remote MCP:', errorMessage);
59
- return {
60
- success: false,
61
- message: `Error disconnecting Remote MCP: ${errorMessage}`
62
- };
63
- }
64
- }
65
- export function getRemoteMCPStatus() {
66
- if (!remoteClient) {
67
- return {
68
- connected: false,
69
- authenticated: false,
70
- deviceId: null,
71
- message: 'No Remote MCP client initialized'
72
- };
73
- }
74
- const status = remoteClient.getStatus();
75
- let message = 'Remote MCP Status: ';
76
- if (status.connected && status.authenticated) {
77
- message += `Connected and authenticated. Device ID: ${status.deviceId}`;
78
- }
79
- else if (status.connected) {
80
- message += 'Connected but not authenticated';
81
- }
82
- else {
83
- message += 'Not connected';
84
- }
85
- return {
86
- connected: status.connected,
87
- authenticated: status.authenticated,
88
- deviceId: status.deviceId,
89
- message
90
- };
91
- }
92
- export async function executeRemoteMCP(method, params) {
93
- try {
94
- if (!remoteClient) {
95
- throw new Error('Remote MCP client not initialized. Use connect_remote_mcp first.');
96
- }
97
- if (!remoteClient.isConnectedAndAuthenticated()) {
98
- throw new Error('Remote MCP SSE client not connected or authenticated. Check connection status.');
99
- }
100
- logger.info(`Executing remote MCP method: ${method}`, { params });
101
- // Create MCP request
102
- const mcpRequest = {
103
- jsonrpc: '2.0',
104
- id: `remote-${Date.now()}-${Math.random()}`,
105
- method,
106
- params: params || {}
107
- };
108
- // Send request and wait for response
109
- const response = await remoteClient.sendMCPRequest(mcpRequest);
110
- if (response.error) {
111
- logger.error(`Remote MCP method ${method} failed:`, response.error);
112
- return {
113
- success: false,
114
- error: `Remote MCP error: ${response.error.message} (code: ${response.error.code})`
115
- };
116
- }
117
- logger.info(`Remote MCP method ${method} completed successfully`);
118
- return {
119
- success: true,
120
- result: response.result
121
- };
122
- }
123
- catch (error) {
124
- const errorMessage = error instanceof Error ? error.message : String(error);
125
- logger.error(`Failed to execute remote MCP method ${method}:`, errorMessage);
126
- return {
127
- success: false,
128
- error: `Failed to execute remote MCP: ${errorMessage}`
129
- };
130
- }
131
- }
132
- // Cleanup function to disconnect on process exit
133
- process.on('exit', () => {
134
- if (remoteClient) {
135
- remoteClient.disconnect();
136
- }
137
- });
138
- process.on('SIGINT', () => {
139
- if (remoteClient) {
140
- remoteClient.disconnect();
141
- }
142
- process.exit(0);
143
- });
144
- process.on('SIGTERM', () => {
145
- if (remoteClient) {
146
- remoteClient.disconnect();
147
- }
148
- process.exit(0);
149
- });