@wonderwhy-er/desktop-commander 0.2.29-alpha.1 → 0.2.29-alpha.3
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-client.d.ts +44 -0
- package/dist/remote-client.js +174 -0
- package/dist/remote-device/device-authenticator.js +2 -8
- package/dist/remote-device/templates/auth-success.d.ts +1 -0
- package/dist/remote-device/templates/auth-success.js +30 -0
- package/dist/remote-sse-client.d.ts +45 -0
- package/dist/remote-sse-client.js +243 -0
- package/dist/tools/remote-mcp.d.ts +20 -0
- package/dist/tools/remote-mcp.js +149 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/dist/data/spec-kit-prompts.json +0 -123
- package/dist/handlers/node-handlers.d.ts +0 -6
- package/dist/handlers/node-handlers.js +0 -73
- package/dist/handlers/test-crash-handler.d.ts +0 -11
- package/dist/handlers/test-crash-handler.js +0 -26
- package/dist/http-index.d.ts +0 -45
- package/dist/http-index.js +0 -51
- package/dist/http-server-auto-tunnel.d.ts +0 -1
- package/dist/http-server-auto-tunnel.js +0 -667
- package/dist/http-server-named-tunnel.d.ts +0 -2
- package/dist/http-server-named-tunnel.js +0 -167
- package/dist/http-server-tunnel.d.ts +0 -2
- package/dist/http-server-tunnel.js +0 -111
- package/dist/http-server.d.ts +0 -2
- package/dist/http-server.js +0 -270
- package/dist/index-oauth.d.ts +0 -2
- package/dist/index-oauth.js +0 -201
- package/dist/oauth/auth-middleware.d.ts +0 -20
- package/dist/oauth/auth-middleware.js +0 -62
- package/dist/oauth/index.d.ts +0 -3
- package/dist/oauth/index.js +0 -3
- package/dist/oauth/oauth-manager.d.ts +0 -80
- package/dist/oauth/oauth-manager.js +0 -179
- package/dist/oauth/oauth-routes.d.ts +0 -3
- package/dist/oauth/oauth-routes.js +0 -377
- package/dist/oauth/provider.d.ts +0 -22
- package/dist/oauth/provider.js +0 -124
- package/dist/oauth/server.d.ts +0 -18
- package/dist/oauth/server.js +0 -160
- package/dist/oauth/types.d.ts +0 -54
- package/dist/oauth/types.js +0 -2
- package/dist/setup.log +0 -275
- package/dist/test-setup.js +0 -14
- package/dist/tools/pdf-processor.d.ts +0 -1
- package/dist/tools/pdf-processor.js +0 -3
- package/dist/tools/search.d.ts +0 -32
- package/dist/tools/search.js +0 -202
- package/dist/utils/crash-logger.d.ts +0 -18
- package/dist/utils/crash-logger.js +0 -44
- package/dist/utils/dedent.d.ts +0 -8
- package/dist/utils/dedent.js +0 -38
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
}
|
|
@@ -2,11 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import { createServer } from 'http';
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import readline from 'readline';
|
|
5
|
-
import
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
5
|
+
import { authSuccessHtml } from './templates/auth-success.js';
|
|
10
6
|
function escapeHtml(text) {
|
|
11
7
|
if (text === null || text === undefined)
|
|
12
8
|
return '';
|
|
@@ -62,9 +58,7 @@ export class DeviceAuthenticator {
|
|
|
62
58
|
reject(new Error(`${error}: ${error_description}`));
|
|
63
59
|
}
|
|
64
60
|
else if (token) {
|
|
65
|
-
|
|
66
|
-
const htmlContent = fs.readFileSync(templatePath, 'utf8');
|
|
67
|
-
res.send(htmlContent);
|
|
61
|
+
res.send(authSuccessHtml);
|
|
68
62
|
server.close();
|
|
69
63
|
console.log(' - ✅ Authentication successful, token received');
|
|
70
64
|
resolve({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const authSuccessHtml = "\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authentication Successful</title>\n</head>\n\n<body\n style=\"margin: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; line-height: 1.5; color: #F8FAFC; background-color: #101219; min-height: 100vh; display: flex; align-items: center; justify-content: center;\">\n <div\n style=\"background: rgba(30, 41, 59, 0.4); backdrop-filter: blur(12px); border: 1px solid #2B303B; border-radius: 12px; padding: 40px; width: 100%; max-width: 480px; margin: 20px; text-align: center; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\">\n <h2 style=\"font-size: 24px; font-weight: 700; color: #F8FAFC; margin-top: 0; margin-bottom: 16px;\">\n Authentication Successful!</h2>\n <p style=\"color: #94A3B8; font-size: 15px; line-height: 1.6; margin: 0 0 8px 0;\">Your device is now connected.\n </p>\n <p style=\"color: #94A3B8; font-size: 15px; line-height: 1.6; margin: 0;\">You can close this window.</p>\n </div>\n <script>\n // Clean up URL parameters and hash\n if (window.history && window.history.replaceState) {\n window.history.replaceState(null, '', window.location.pathname);\n }\n </script>\n</body>\n\n</html>\n";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const authSuccessHtml = `
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>Authentication Successful</title>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body
|
|
12
|
+
style="margin: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; line-height: 1.5; color: #F8FAFC; background-color: #101219; min-height: 100vh; display: flex; align-items: center; justify-content: center;">
|
|
13
|
+
<div
|
|
14
|
+
style="background: rgba(30, 41, 59, 0.4); backdrop-filter: blur(12px); border: 1px solid #2B303B; border-radius: 12px; padding: 40px; width: 100%; max-width: 480px; margin: 20px; text-align: center; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);">
|
|
15
|
+
<h2 style="font-size: 24px; font-weight: 700; color: #F8FAFC; margin-top: 0; margin-bottom: 16px;">
|
|
16
|
+
Authentication Successful!</h2>
|
|
17
|
+
<p style="color: #94A3B8; font-size: 15px; line-height: 1.6; margin: 0 0 8px 0;">Your device is now connected.
|
|
18
|
+
</p>
|
|
19
|
+
<p style="color: #94A3B8; font-size: 15px; line-height: 1.6; margin: 0;">You can close this window.</p>
|
|
20
|
+
</div>
|
|
21
|
+
<script>
|
|
22
|
+
// Clean up URL parameters and hash
|
|
23
|
+
if (window.history && window.history.replaceState) {
|
|
24
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
|
|
29
|
+
</html>
|
|
30
|
+
`;
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}>;
|