@wonderwhy-er/desktop-commander 0.2.29-alpha.0 → 0.2.29-alpha.10
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/index.js +10 -0
- package/dist/npm-scripts/remote.d.ts +1 -0
- package/dist/npm-scripts/remote.js +20 -0
- package/dist/remote-device/desktop-commander-integration.d.ts +143 -0
- package/dist/remote-device/desktop-commander-integration.js +147 -0
- package/dist/remote-device/device-authenticator.d.ts +16 -0
- package/dist/remote-device/device-authenticator.js +120 -0
- package/dist/remote-device/device.d.ts +25 -0
- package/dist/remote-device/device.js +308 -0
- package/dist/remote-device/remote-channel.d.ts +51 -0
- package/dist/remote-device/remote-channel.js +255 -0
- package/dist/remote-device/scripts/blocking-offline-update.js +64 -0
- package/dist/remote-device/templates/auth-success.d.ts +1 -0
- package/dist/remote-device/templates/auth-success.js +30 -0
- package/dist/server.js +39 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -3
- 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
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runSetup } from './npm-scripts/setup.js';
|
|
|
7
7
|
import { runUninstall } from './npm-scripts/uninstall.js';
|
|
8
8
|
import { capture } from './utils/capture.js';
|
|
9
9
|
import { logToStderr, logger } from './utils/logger.js';
|
|
10
|
+
import { runRemote } from './npm-scripts/remote.js';
|
|
10
11
|
// Store messages to defer until after initialization
|
|
11
12
|
const deferredMessages = [];
|
|
12
13
|
function deferLog(level, message) {
|
|
@@ -24,6 +25,15 @@ async function runServer() {
|
|
|
24
25
|
await runUninstall();
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
28
|
+
if (process.argv[2] === 'remote') {
|
|
29
|
+
await runRemote();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Check if first argument is "remote"
|
|
33
|
+
if (process.argv[2] === 'remote') {
|
|
34
|
+
await runRemote();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
27
37
|
// Parse command line arguments for onboarding control
|
|
28
38
|
const DISABLE_ONBOARDING = process.argv.includes('--no-onboarding');
|
|
29
39
|
if (DISABLE_ONBOARDING) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRemote(): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MCPDevice } from '../remote-device/device.js';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import caffeinate from 'caffeinate';
|
|
4
|
+
export async function runRemote() {
|
|
5
|
+
const persistSession = process.argv.includes('--persist-session');
|
|
6
|
+
const disableNoSleep = process.argv.includes('--disable-no-sleep');
|
|
7
|
+
// Start caffeinate on macOS (unless disabled)
|
|
8
|
+
// Caffeinate will monitor this process and automatically exit when it terminates
|
|
9
|
+
if (!disableNoSleep && os.platform() === 'darwin') {
|
|
10
|
+
try {
|
|
11
|
+
await caffeinate({ pid: process.pid });
|
|
12
|
+
console.log('☕ Caffeinate started (preventing system sleep)');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.warn('⚠️ Failed to start caffeinate:', error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const device = new MCPDevice({ persistSession });
|
|
19
|
+
await device.start();
|
|
20
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
interface McpConfig {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare class DesktopCommanderIntegration {
|
|
8
|
+
private mcpClient;
|
|
9
|
+
private mcpTransport;
|
|
10
|
+
private isReady;
|
|
11
|
+
initialize(): Promise<void>;
|
|
12
|
+
resolveMcpConfig(): Promise<McpConfig | null>;
|
|
13
|
+
callClientTool(toolName: string, args: any, metadata?: any): Promise<{
|
|
14
|
+
[x: string]: unknown;
|
|
15
|
+
content: ({
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
annotations?: {
|
|
19
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
20
|
+
priority?: number | undefined;
|
|
21
|
+
lastModified?: string | undefined;
|
|
22
|
+
} | undefined;
|
|
23
|
+
_meta?: Record<string, unknown> | undefined;
|
|
24
|
+
} | {
|
|
25
|
+
type: "image";
|
|
26
|
+
data: string;
|
|
27
|
+
mimeType: string;
|
|
28
|
+
annotations?: {
|
|
29
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
30
|
+
priority?: number | undefined;
|
|
31
|
+
lastModified?: string | undefined;
|
|
32
|
+
} | undefined;
|
|
33
|
+
_meta?: Record<string, unknown> | undefined;
|
|
34
|
+
} | {
|
|
35
|
+
type: "audio";
|
|
36
|
+
data: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
annotations?: {
|
|
39
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
40
|
+
priority?: number | undefined;
|
|
41
|
+
lastModified?: string | undefined;
|
|
42
|
+
} | undefined;
|
|
43
|
+
_meta?: Record<string, unknown> | undefined;
|
|
44
|
+
} | {
|
|
45
|
+
type: "resource";
|
|
46
|
+
resource: {
|
|
47
|
+
uri: string;
|
|
48
|
+
text: string;
|
|
49
|
+
mimeType?: string | undefined;
|
|
50
|
+
_meta?: Record<string, unknown> | undefined;
|
|
51
|
+
} | {
|
|
52
|
+
uri: string;
|
|
53
|
+
blob: string;
|
|
54
|
+
mimeType?: string | undefined;
|
|
55
|
+
_meta?: Record<string, unknown> | undefined;
|
|
56
|
+
};
|
|
57
|
+
annotations?: {
|
|
58
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
59
|
+
priority?: number | undefined;
|
|
60
|
+
lastModified?: string | undefined;
|
|
61
|
+
} | undefined;
|
|
62
|
+
_meta?: Record<string, unknown> | undefined;
|
|
63
|
+
} | {
|
|
64
|
+
uri: string;
|
|
65
|
+
name: string;
|
|
66
|
+
type: "resource_link";
|
|
67
|
+
description?: string | undefined;
|
|
68
|
+
mimeType?: string | undefined;
|
|
69
|
+
annotations?: {
|
|
70
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
71
|
+
priority?: number | undefined;
|
|
72
|
+
lastModified?: string | undefined;
|
|
73
|
+
} | undefined;
|
|
74
|
+
_meta?: {
|
|
75
|
+
[x: string]: unknown;
|
|
76
|
+
} | undefined;
|
|
77
|
+
icons?: {
|
|
78
|
+
src: string;
|
|
79
|
+
mimeType?: string | undefined;
|
|
80
|
+
sizes?: string[] | undefined;
|
|
81
|
+
theme?: "light" | "dark" | undefined;
|
|
82
|
+
}[] | undefined;
|
|
83
|
+
title?: string | undefined;
|
|
84
|
+
})[];
|
|
85
|
+
_meta?: {
|
|
86
|
+
[x: string]: unknown;
|
|
87
|
+
progressToken?: string | number | undefined;
|
|
88
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
89
|
+
taskId: string;
|
|
90
|
+
} | undefined;
|
|
91
|
+
} | undefined;
|
|
92
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
93
|
+
isError?: boolean | undefined;
|
|
94
|
+
} | {
|
|
95
|
+
[x: string]: unknown;
|
|
96
|
+
toolResult: unknown;
|
|
97
|
+
_meta?: {
|
|
98
|
+
[x: string]: unknown;
|
|
99
|
+
progressToken?: string | number | undefined;
|
|
100
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
101
|
+
taskId: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
} | undefined;
|
|
104
|
+
}>;
|
|
105
|
+
listClientTools(): Promise<{
|
|
106
|
+
tools: {
|
|
107
|
+
inputSchema: {
|
|
108
|
+
[x: string]: unknown;
|
|
109
|
+
type: "object";
|
|
110
|
+
properties?: Record<string, object> | undefined;
|
|
111
|
+
required?: string[] | undefined;
|
|
112
|
+
};
|
|
113
|
+
name: string;
|
|
114
|
+
description?: string | undefined;
|
|
115
|
+
outputSchema?: {
|
|
116
|
+
[x: string]: unknown;
|
|
117
|
+
type: "object";
|
|
118
|
+
properties?: Record<string, object> | undefined;
|
|
119
|
+
required?: string[] | undefined;
|
|
120
|
+
} | undefined;
|
|
121
|
+
annotations?: {
|
|
122
|
+
title?: string | undefined;
|
|
123
|
+
readOnlyHint?: boolean | undefined;
|
|
124
|
+
destructiveHint?: boolean | undefined;
|
|
125
|
+
idempotentHint?: boolean | undefined;
|
|
126
|
+
openWorldHint?: boolean | undefined;
|
|
127
|
+
} | undefined;
|
|
128
|
+
execution?: {
|
|
129
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
130
|
+
} | undefined;
|
|
131
|
+
_meta?: Record<string, unknown> | undefined;
|
|
132
|
+
icons?: {
|
|
133
|
+
src: string;
|
|
134
|
+
mimeType?: string | undefined;
|
|
135
|
+
sizes?: string[] | undefined;
|
|
136
|
+
theme?: "light" | "dark" | undefined;
|
|
137
|
+
}[] | undefined;
|
|
138
|
+
title?: string | undefined;
|
|
139
|
+
}[];
|
|
140
|
+
}>;
|
|
141
|
+
shutdown(): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
export class DesktopCommanderIntegration {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.mcpClient = null;
|
|
12
|
+
this.mcpTransport = null;
|
|
13
|
+
this.isReady = false;
|
|
14
|
+
}
|
|
15
|
+
async initialize() {
|
|
16
|
+
const config = await this.resolveMcpConfig();
|
|
17
|
+
if (!config) {
|
|
18
|
+
throw new Error('Desktop Commander MCP not found. Please install it globally via `npm install -g @wonderwhy-er/desktop-commander` or build the local project.');
|
|
19
|
+
}
|
|
20
|
+
console.log(` - ⏳ Connecting to Local Desktop Commander MCP using: ${config.command} ${config.args.join(' ')}`);
|
|
21
|
+
try {
|
|
22
|
+
this.mcpTransport = new StdioClientTransport(config);
|
|
23
|
+
// Create MCP client
|
|
24
|
+
this.mcpClient = new Client({
|
|
25
|
+
name: "desktop-commander-client",
|
|
26
|
+
version: "1.0.0"
|
|
27
|
+
}, {
|
|
28
|
+
capabilities: {}
|
|
29
|
+
});
|
|
30
|
+
// Connect to Desktop Commander
|
|
31
|
+
await this.mcpClient.connect(this.mcpTransport);
|
|
32
|
+
this.isReady = true;
|
|
33
|
+
console.log(' - 🔌 Connected to Desktop Commander MCP');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(' - ❌ Failed to connect to Desktop Commander MCP:', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async resolveMcpConfig() {
|
|
41
|
+
// Option 1: Development/Local Build
|
|
42
|
+
// Adjusting path resolution since we are now in src/remote-device and dist is in root/dist
|
|
43
|
+
// Original: path.resolve(__dirname, '../../dist/index.js')
|
|
44
|
+
const devPath = path.resolve(__dirname, '../../dist/index.js');
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(devPath);
|
|
47
|
+
console.debug(' - 🔍 Found local MCP server at:', devPath);
|
|
48
|
+
return {
|
|
49
|
+
command: process.execPath, // Use the current node executable
|
|
50
|
+
args: [devPath],
|
|
51
|
+
cwd: path.dirname(devPath)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Local file not found, continue...
|
|
56
|
+
}
|
|
57
|
+
// Option 2: Global Installation
|
|
58
|
+
const commandName = 'desktop-commander';
|
|
59
|
+
try {
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
// Use 'which' to check if the command exists in PATH
|
|
62
|
+
// We can't run it directly as it's an stdio MCP server that waits for input
|
|
63
|
+
const check = spawn('which', [commandName]);
|
|
64
|
+
check.on('error', reject);
|
|
65
|
+
check.on('close', (code) => code === 0 ? resolve() : reject(new Error('Command not found')));
|
|
66
|
+
});
|
|
67
|
+
console.debug(' - Found global desktop-commander CLI');
|
|
68
|
+
return {
|
|
69
|
+
command: commandName,
|
|
70
|
+
args: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Global command not found
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
async callClientTool(toolName, args, metadata) {
|
|
79
|
+
if (!this.isReady || !this.mcpClient) {
|
|
80
|
+
throw new Error('DesktopIntegration not initialized');
|
|
81
|
+
}
|
|
82
|
+
// Proxy other tools to MCP server
|
|
83
|
+
try {
|
|
84
|
+
console.log(`Forwarding tool call ${toolName} to MCP server`, metadata);
|
|
85
|
+
const result = await this.mcpClient.callTool({
|
|
86
|
+
name: toolName,
|
|
87
|
+
arguments: args,
|
|
88
|
+
_meta: { remote: true, ...metadata || {} }
|
|
89
|
+
});
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async listClientTools() {
|
|
98
|
+
if (!this.mcpClient)
|
|
99
|
+
return { tools: [] };
|
|
100
|
+
try {
|
|
101
|
+
// List tools from MCP server
|
|
102
|
+
const mcpTools = await this.mcpClient.listTools();
|
|
103
|
+
// Merge tools
|
|
104
|
+
return {
|
|
105
|
+
tools: mcpTools.tools || []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('Error fetching capabilities:', error);
|
|
110
|
+
// Fallback to local tools
|
|
111
|
+
return {
|
|
112
|
+
tools: []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async shutdown() {
|
|
117
|
+
const closeWithTimeout = async (operation, name, timeoutMs = 3000) => {
|
|
118
|
+
return Promise.race([
|
|
119
|
+
operation(),
|
|
120
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
121
|
+
]);
|
|
122
|
+
};
|
|
123
|
+
if (this.mcpClient) {
|
|
124
|
+
try {
|
|
125
|
+
console.log(' → Closing MCP client...');
|
|
126
|
+
await closeWithTimeout(() => this.mcpClient.close(), 'MCP client close');
|
|
127
|
+
console.log(' ✓ MCP client closed');
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.warn(' ⚠️ MCP client close timeout or error:', e.message);
|
|
131
|
+
}
|
|
132
|
+
this.mcpClient = null;
|
|
133
|
+
}
|
|
134
|
+
if (this.mcpTransport) {
|
|
135
|
+
try {
|
|
136
|
+
console.log(' → Closing MCP transport...');
|
|
137
|
+
await closeWithTimeout(() => this.mcpTransport.close(), 'MCP transport close');
|
|
138
|
+
console.log(' ✓ MCP transport closed');
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.warn(' ⚠️ MCP transport close timeout or error:', e.message);
|
|
142
|
+
}
|
|
143
|
+
this.mcpTransport = null;
|
|
144
|
+
}
|
|
145
|
+
this.isReady = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface AuthSession {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string | null;
|
|
4
|
+
device_id?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class DeviceAuthenticator {
|
|
7
|
+
private baseServerUrl;
|
|
8
|
+
constructor(baseServerUrl: string);
|
|
9
|
+
authenticate(deviceId?: string): Promise<AuthSession>;
|
|
10
|
+
private generatePKCE;
|
|
11
|
+
private requestDeviceCode;
|
|
12
|
+
private displayUserInstructions;
|
|
13
|
+
private pollForAuthorization;
|
|
14
|
+
private sleep;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
const CLIENT_ID = 'mcp-device';
|
|
5
|
+
export class DeviceAuthenticator {
|
|
6
|
+
constructor(baseServerUrl) {
|
|
7
|
+
this.baseServerUrl = baseServerUrl;
|
|
8
|
+
}
|
|
9
|
+
async authenticate(deviceId) {
|
|
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, deviceId);
|
|
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;
|
|
21
|
+
}
|
|
22
|
+
generatePKCE() {
|
|
23
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
24
|
+
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
25
|
+
return { verifier, challenge };
|
|
26
|
+
}
|
|
27
|
+
async requestDeviceCode(codeChallenge, deviceId) {
|
|
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
|
+
device_id: deviceId,
|
|
38
|
+
code_challenge: codeChallenge,
|
|
39
|
+
code_challenge_method: 'S256',
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
44
|
+
throw new Error(error.error_description || 'Failed to start device flow');
|
|
45
|
+
}
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
console.log(' - ✅ Device code received\n');
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
displayUserInstructions(deviceAuth) {
|
|
51
|
+
console.log('📋 Please complete authentication:\n');
|
|
52
|
+
console.log(' 1. Open this URL in your browser:');
|
|
53
|
+
console.log(` ${deviceAuth.verification_uri}\n`);
|
|
54
|
+
console.log(' 2. Enter this code when prompted:');
|
|
55
|
+
console.log(` ${deviceAuth.user_code}\n`);
|
|
56
|
+
console.log(` Code expires in ${Math.floor(deviceAuth.expires_in / 60)} minutes.\n`);
|
|
57
|
+
// Try to open browser automatically
|
|
58
|
+
open(deviceAuth.verification_uri_complete).catch(() => {
|
|
59
|
+
console.log(' - Could not open browser automatically.');
|
|
60
|
+
console.log(` - Please visit: ${deviceAuth.verification_uri}\n`);
|
|
61
|
+
});
|
|
62
|
+
console.log(' - ⏳ Waiting for authorization...\n');
|
|
63
|
+
}
|
|
64
|
+
async pollForAuthorization(deviceAuth, codeVerifier) {
|
|
65
|
+
const interval = (deviceAuth.interval || 5) * 1000;
|
|
66
|
+
const maxAttempts = Math.floor(deviceAuth.expires_in / (deviceAuth.interval || 5));
|
|
67
|
+
let attempt = 0;
|
|
68
|
+
while (attempt < maxAttempts) {
|
|
69
|
+
attempt++;
|
|
70
|
+
// Wait before polling
|
|
71
|
+
await this.sleep(interval);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`${this.baseServerUrl}/device/poll`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
device_code: deviceAuth.device_code,
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
code_verifier: codeVerifier,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
const tokens = await response.json();
|
|
84
|
+
if (tokens.access_token) {
|
|
85
|
+
return {
|
|
86
|
+
device_id: tokens.device_id,
|
|
87
|
+
access_token: tokens.access_token,
|
|
88
|
+
refresh_token: tokens.refresh_token || null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const error = await response.json().catch(() => ({ error: 'unknown' }));
|
|
93
|
+
// Check error type
|
|
94
|
+
if (error.error === 'authorization_pending') {
|
|
95
|
+
// Still waiting - continue polling
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (error.error === 'slow_down') {
|
|
99
|
+
// Server requested slower polling
|
|
100
|
+
await this.sleep(interval);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Terminal error
|
|
104
|
+
throw new Error(error.error_description || error.error || 'Authorization failed');
|
|
105
|
+
}
|
|
106
|
+
catch (fetchError) {
|
|
107
|
+
// Network error - retry unless we're out of attempts
|
|
108
|
+
if (attempt >= maxAttempts) {
|
|
109
|
+
throw fetchError;
|
|
110
|
+
}
|
|
111
|
+
// Continue polling on network errors
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Authorization timeout - user did not authorize within the time limit');
|
|
116
|
+
}
|
|
117
|
+
sleep(ms) {
|
|
118
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface MCPDeviceOptions {
|
|
3
|
+
persistSession?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare class MCPDevice {
|
|
6
|
+
private baseServerUrl;
|
|
7
|
+
private remoteChannel;
|
|
8
|
+
private deviceId?;
|
|
9
|
+
private user;
|
|
10
|
+
private isShuttingDown;
|
|
11
|
+
private configPath;
|
|
12
|
+
private persistSession;
|
|
13
|
+
private desktop;
|
|
14
|
+
constructor(options?: MCPDeviceOptions);
|
|
15
|
+
private setupShutdownHandlers;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
loadPersistedConfig(): Promise<any>;
|
|
18
|
+
savePersistedConfig(session: any): Promise<void>;
|
|
19
|
+
fetchSupabaseConfig(): Promise<{
|
|
20
|
+
supabaseUrl: any;
|
|
21
|
+
anonKey: any;
|
|
22
|
+
}>;
|
|
23
|
+
handleNewToolCall(payload: any): Promise<void>;
|
|
24
|
+
shutdown(): Promise<void>;
|
|
25
|
+
}
|