@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.
- package/dist/remote-device/desktop-commander-integration.d.ts +2 -2
- package/dist/remote-device/desktop-commander-integration.js +5 -4
- package/dist/remote-device/device-authenticator.d.ts +5 -3
- package/dist/remote-device/device-authenticator.js +100 -136
- package/dist/remote-device/device.js +28 -13
- package/dist/remote-device/remote-channel.d.ts +1 -0
- package/dist/remote-device/remote-channel.js +16 -11
- package/dist/server.js +39 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -5
- package/dist/remote-client.d.ts +0 -44
- package/dist/remote-client.js +0 -174
- package/dist/remote-sse-client.d.ts +0 -45
- package/dist/remote-sse-client.js +0 -243
- package/dist/tools/remote-mcp.d.ts +0 -20
- package/dist/tools/remote-mcp.js +0 -149
|
@@ -10,7 +10,7 @@ export declare class DesktopCommanderIntegration {
|
|
|
10
10
|
private isReady;
|
|
11
11
|
initialize(): Promise<void>;
|
|
12
12
|
resolveMcpConfig(): Promise<McpConfig | null>;
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
10
|
-
private
|
|
11
|
-
private
|
|
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
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
if (text === null || text === undefined)
|
|
8
|
-
return '';
|
|
9
|
-
return String(text)
|
|
10
|
-
.replace(/&/g, "&")
|
|
11
|
-
.replace(/</g, "<")
|
|
12
|
-
.replace(/>/g, ">")
|
|
13
|
-
.replace(/"/g, """)
|
|
14
|
-
.replace(/'/g, "'");
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
107
|
-
console.log('
|
|
108
|
-
console.log('
|
|
109
|
-
console.log(`
|
|
110
|
-
console.log('2.
|
|
111
|
-
console.log(
|
|
112
|
-
console.log(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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 (
|
|
143
|
-
|
|
96
|
+
if (error.error === 'slow_down') {
|
|
97
|
+
// Server requested slower polling
|
|
98
|
+
await this.sleep(interval);
|
|
99
|
+
continue;
|
|
144
100
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
},
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
129
|
-
reject(err || new Error('Failed to initialize call
|
|
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
|
|
133
|
-
reject(new Error('
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/dist/remote-client.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/remote-client.js
DELETED
|
@@ -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
|
-
}>;
|
package/dist/tools/remote-mcp.js
DELETED
|
@@ -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
|
-
});
|