1
+ import fs from 'fs'
1
2
import { checkLockfile , createLockfile , deleteLockfile , getConfigFilePath , LockfileData } from './mcp-auth-config'
2
3
import { EventEmitter } from 'events'
3
4
import { Server } from 'http'
@@ -17,10 +18,10 @@ export type AuthCoordinator = {
17
18
export async function isPidRunning ( pid : number ) : Promise < boolean > {
18
19
try {
19
20
process . kill ( pid , 0 ) // Doesn't kill the process, just checks if it exists
20
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Process ${ pid } is running` )
21
+ if ( DEBUG ) debugLog ( `Process ${ pid } is running` )
21
22
return true
22
23
} catch ( err ) {
23
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Process ${ pid } is not running` , err )
24
+ if ( DEBUG ) debugLog ( `Process ${ pid } is not running` , err )
24
25
return false
25
26
}
26
27
}
@@ -31,14 +32,14 @@ export async function isPidRunning(pid: number): Promise<boolean> {
31
32
* @returns True if the lockfile is valid, false otherwise
32
33
*/
33
34
export async function isLockValid ( lockData : LockfileData ) : Promise < boolean > {
34
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Checking if lockfile is valid' , lockData )
35
+ if ( DEBUG ) debugLog ( 'Checking if lockfile is valid' , lockData )
35
36
36
37
// Check if the lockfile is too old (over 30 minutes)
37
38
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
38
39
if ( Date . now ( ) - lockData . timestamp > MAX_LOCK_AGE ) {
39
40
log ( 'Lockfile is too old' )
40
41
if ( DEBUG )
41
- await debugLog ( global . currentServerUrlHash ! , 'Lockfile is too old' , {
42
+ debugLog ( 'Lockfile is too old' , {
42
43
age : Date . now ( ) - lockData . timestamp ,
43
44
maxAge : MAX_LOCK_AGE ,
44
45
} )
@@ -48,13 +49,13 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
48
49
// Check if the process is still running
49
50
if ( ! ( await isPidRunning ( lockData . pid ) ) ) {
50
51
log ( 'Process from lockfile is not running' )
51
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Process from lockfile is not running' , { pid : lockData . pid } )
52
+ if ( DEBUG ) debugLog ( 'Process from lockfile is not running' , { pid : lockData . pid } )
52
53
return false
53
54
}
54
55
55
56
// Check if the endpoint is accessible
56
57
try {
57
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Checking if endpoint is accessible' , { port : lockData . port } )
58
+ if ( DEBUG ) debugLog ( 'Checking if endpoint is accessible' , { port : lockData . port } )
58
59
59
60
const controller = new AbortController ( )
60
61
const timeout = setTimeout ( ( ) => controller . abort ( ) , 1000 )
@@ -66,12 +67,11 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
66
67
clearTimeout ( timeout )
67
68
68
69
const isValid = response . status === 200 || response . status === 202
69
- if ( DEBUG )
70
- await debugLog ( global . currentServerUrlHash ! , `Endpoint check result: ${ isValid ? 'valid' : 'invalid' } ` , { status : response . status } )
70
+ if ( DEBUG ) debugLog ( `Endpoint check result: ${ isValid ? 'valid' : 'invalid' } ` , { status : response . status } )
71
71
return isValid
72
72
} catch ( error ) {
73
73
log ( `Error connecting to auth server: ${ ( error as Error ) . message } ` )
74
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Error connecting to auth server' , error )
74
+ if ( DEBUG ) debugLog ( 'Error connecting to auth server' , error )
75
75
return false
76
76
}
77
77
}
@@ -83,44 +83,44 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
83
83
*/
84
84
export async function waitForAuthentication ( port : number ) : Promise < boolean > {
85
85
log ( `Waiting for authentication from the server on port ${ port } ...` )
86
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Waiting for authentication from server on port ${ port } ` )
86
+ if ( DEBUG ) debugLog ( `Waiting for authentication from server on port ${ port } ` )
87
87
88
88
try {
89
89
let attempts = 0
90
90
while ( true ) {
91
91
attempts ++
92
92
const url = `http://127.0.0.1:${ port } /wait-for-auth`
93
93
log ( `Querying: ${ url } ` )
94
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Poll attempt ${ attempts } : ${ url } ` )
94
+ if ( DEBUG ) debugLog ( `Poll attempt ${ attempts } : ${ url } ` )
95
95
96
96
try {
97
97
const response = await fetch ( url )
98
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Poll response status: ${ response . status } ` )
98
+ if ( DEBUG ) debugLog ( `Poll response status: ${ response . status } ` )
99
99
100
100
if ( response . status === 200 ) {
101
101
// Auth completed, but we don't return the code anymore
102
102
log ( `Authentication completed by other instance` )
103
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Authentication completed by other instance` )
103
+ if ( DEBUG ) debugLog ( `Authentication completed by other instance` )
104
104
return true
105
105
} else if ( response . status === 202 ) {
106
106
// Continue polling
107
107
log ( `Authentication still in progress` )
108
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Authentication still in progress, will retry in 1s` )
108
+ if ( DEBUG ) debugLog ( `Authentication still in progress, will retry in 1s` )
109
109
await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) )
110
110
} else {
111
111
log ( `Unexpected response status: ${ response . status } ` )
112
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Unexpected response status` , { status : response . status } )
112
+ if ( DEBUG ) debugLog ( `Unexpected response status` , { status : response . status } )
113
113
return false
114
114
}
115
115
} catch ( fetchError ) {
116
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Fetch error during poll` , fetchError )
116
+ if ( DEBUG ) debugLog ( `Fetch error during poll` , fetchError )
117
117
// If we can't connect, we'll try again after a delay
118
118
await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) )
119
119
}
120
120
}
121
121
} catch ( error ) {
122
122
log ( `Error waiting for authentication: ${ ( error as Error ) . message } ` )
123
- if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Error waiting for authentication` , error )
123
+ if ( DEBUG ) debugLog ( `Error waiting for authentication` , error )
124
124
return false
125
125
}
126
126
}
@@ -139,16 +139,16 @@ export function createLazyAuthCoordinator(serverUrlHash: string, callbackPort: n
139
139
initializeAuth : async ( ) => {
140
140
// If auth has already been initialized, return the existing state
141
141
if ( authState ) {
142
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth already initialized, reusing existing state' )
142
+ if ( DEBUG ) debugLog ( 'Auth already initialized, reusing existing state' )
143
143
return authState
144
144
}
145
145
146
146
log ( 'Initializing auth coordination on-demand' )
147
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Initializing auth coordination on-demand' , { serverUrlHash, callbackPort } )
147
+ if ( DEBUG ) debugLog ( 'Initializing auth coordination on-demand' , { serverUrlHash, callbackPort } )
148
148
149
149
// Initialize auth using the existing coordinateAuth logic
150
150
authState = await coordinateAuth ( serverUrlHash , callbackPort , events )
151
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth coordination completed' , { skipBrowserAuth : authState . skipBrowserAuth } )
151
+ if ( DEBUG ) debugLog ( 'Auth coordination completed' , { skipBrowserAuth : authState . skipBrowserAuth } )
152
152
return authState
153
153
} ,
154
154
}
@@ -166,42 +166,42 @@ export async function coordinateAuth(
166
166
callbackPort : number ,
167
167
events : EventEmitter ,
168
168
) : Promise < { server : Server ; waitForAuthCode : ( ) => Promise < string > ; skipBrowserAuth : boolean } > {
169
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Coordinating authentication' , { serverUrlHash, callbackPort } )
169
+ if ( DEBUG ) debugLog ( 'Coordinating authentication' , { serverUrlHash, callbackPort } )
170
170
171
171
// Check for a lockfile (disabled on Windows for the time being)
172
172
const lockData = process . platform === 'win32' ? null : await checkLockfile ( serverUrlHash )
173
173
174
174
if ( DEBUG ) {
175
175
if ( process . platform === 'win32' ) {
176
- await debugLog ( serverUrlHash , 'Skipping lockfile check on Windows' )
176
+ debugLog ( 'Skipping lockfile check on Windows' )
177
177
} else {
178
- await debugLog ( serverUrlHash , 'Lockfile check result' , { found : ! ! lockData , lockData } )
178
+ debugLog ( 'Lockfile check result' , { found : ! ! lockData , lockData } )
179
179
}
180
180
}
181
181
182
182
// If there's a valid lockfile, try to use the existing auth process
183
183
if ( lockData && ( await isLockValid ( lockData ) ) ) {
184
184
log ( `Another instance is handling authentication on port ${ lockData . port } ` )
185
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Another instance is handling authentication' , { port : lockData . port , pid : lockData . pid } )
185
+ if ( DEBUG ) debugLog ( 'Another instance is handling authentication' , { port : lockData . port , pid : lockData . pid } )
186
186
187
187
try {
188
188
// Try to wait for the authentication to complete
189
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Waiting for authentication from other instance' )
189
+ if ( DEBUG ) debugLog ( 'Waiting for authentication from other instance' )
190
190
const authCompleted = await waitForAuthentication ( lockData . port )
191
191
192
192
if ( authCompleted ) {
193
193
log ( 'Authentication completed by another instance' )
194
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Authentication completed by another instance, will use tokens from disk' )
194
+ if ( DEBUG ) debugLog ( 'Authentication completed by another instance, will use tokens from disk' )
195
195
196
196
// Setup a dummy server - the client will use tokens directly from disk
197
197
const dummyServer = express ( ) . listen ( 0 ) // Listen on any available port
198
198
const dummyPort = ( dummyServer . address ( ) as AddressInfo ) . port
199
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Started dummy server' , { port : dummyPort } )
199
+ if ( DEBUG ) debugLog ( 'Started dummy server' , { port : dummyPort } )
200
200
201
201
// This shouldn't actually be called in normal operation, but provide it for API compatibility
202
202
const dummyWaitForAuthCode = ( ) => {
203
203
log ( 'WARNING: waitForAuthCode called in secondary instance - this is unexpected' )
204
- if ( DEBUG ) debugLog ( serverUrlHash , 'WARNING: waitForAuthCode called in secondary instance - this is unexpected' ) . catch ( ( ) => { } )
204
+ if ( DEBUG ) debugLog ( 'WARNING: waitForAuthCode called in secondary instance - this is unexpected' )
205
205
// Return a promise that never resolves - the client should use the tokens from disk instead
206
206
return new Promise < string > ( ( ) => { } )
207
207
}
@@ -213,25 +213,25 @@ export async function coordinateAuth(
213
213
}
214
214
} else {
215
215
log ( 'Taking over authentication process...' )
216
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Taking over authentication process' )
216
+ if ( DEBUG ) debugLog ( 'Taking over authentication process' )
217
217
}
218
218
} catch ( error ) {
219
219
log ( `Error waiting for authentication: ${ error } ` )
220
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Error waiting for authentication' , error )
220
+ if ( DEBUG ) debugLog ( 'Error waiting for authentication' , error )
221
221
}
222
222
223
223
// If we get here, the other process didn't complete auth successfully
224
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Other instance did not complete auth successfully, deleting lockfile' )
224
+ if ( DEBUG ) debugLog ( 'Other instance did not complete auth successfully, deleting lockfile' )
225
225
await deleteLockfile ( serverUrlHash )
226
226
} else if ( lockData ) {
227
227
// Invalid lockfile, delete it
228
228
log ( 'Found invalid lockfile, deleting it' )
229
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Found invalid lockfile, deleting it' )
229
+ if ( DEBUG ) debugLog ( 'Found invalid lockfile, deleting it' )
230
230
await deleteLockfile ( serverUrlHash )
231
231
}
232
232
233
233
// Create our own lockfile
234
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Setting up OAuth callback server' , { port : callbackPort } )
234
+ if ( DEBUG ) debugLog ( 'Setting up OAuth callback server' , { port : callbackPort } )
235
235
const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll ( {
236
236
port : callbackPort ,
237
237
path : '/oauth/callback' ,
@@ -241,29 +241,29 @@ export async function coordinateAuth(
241
241
// Get the actual port the server is running on
242
242
const address = server . address ( ) as AddressInfo
243
243
const actualPort = address . port
244
- if ( DEBUG ) await debugLog ( serverUrlHash , 'OAuth callback server running' , { port : actualPort } )
244
+ if ( DEBUG ) debugLog ( 'OAuth callback server running' , { port : actualPort } )
245
245
246
246
log ( `Creating lockfile for server ${ serverUrlHash } with process ${ process . pid } on port ${ actualPort } ` )
247
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Creating lockfile' , { serverUrlHash, pid : process . pid , port : actualPort } )
247
+ if ( DEBUG ) debugLog ( 'Creating lockfile' , { serverUrlHash, pid : process . pid , port : actualPort } )
248
248
await createLockfile ( serverUrlHash , process . pid , actualPort )
249
249
250
250
// Make sure lockfile is deleted on process exit
251
251
const cleanupHandler = async ( ) => {
252
252
try {
253
253
log ( `Cleaning up lockfile for server ${ serverUrlHash } ` )
254
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Cleaning up lockfile' )
254
+ if ( DEBUG ) debugLog ( 'Cleaning up lockfile' )
255
255
await deleteLockfile ( serverUrlHash )
256
256
} catch ( error ) {
257
257
log ( `Error cleaning up lockfile: ${ error } ` )
258
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Error cleaning up lockfile' , error )
258
+ if ( DEBUG ) debugLog ( 'Error cleaning up lockfile' , error )
259
259
}
260
260
}
261
261
262
262
process . once ( 'exit' , ( ) => {
263
263
try {
264
264
// Synchronous version for 'exit' event since we can't use async here
265
265
const configPath = getConfigFilePath ( serverUrlHash , 'lock.json' )
266
- require ( 'fs' ) . unlinkSync ( configPath )
266
+ fs . unlinkSync ( configPath )
267
267
if ( DEBUG ) console . error ( `[DEBUG] Removed lockfile on exit: ${ configPath } ` )
268
268
} catch ( error ) {
269
269
if ( DEBUG ) console . error ( `[DEBUG] Error removing lockfile on exit:` , error )
@@ -272,11 +272,11 @@ export async function coordinateAuth(
272
272
273
273
// Also handle SIGINT separately
274
274
process . once ( 'SIGINT' , async ( ) => {
275
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Received SIGINT signal, cleaning up' )
275
+ if ( DEBUG ) debugLog ( 'Received SIGINT signal, cleaning up' )
276
276
await cleanupHandler ( )
277
277
} )
278
278
279
- if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth coordination complete, returning primary instance handlers' )
279
+ if ( DEBUG ) debugLog ( 'Auth coordination complete, returning primary instance handlers' )
280
280
return {
281
281
server,
282
282
waitForAuthCode,
0 commit comments