Skip to content

Commit de89206

Browse files
committed
AMP added debug mode
1 parent 2aeb697 commit de89206

File tree

6 files changed

+290
-35
lines changed

6 files changed

+290
-35
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ To bypass authentication, or to emit custom headers on all requests to your remo
125125
]
126126
```
127127

128+
* To enable detailed debugging logs, add the `--debug` flag. This will write verbose logs to `~/.mcp-auth/{server_hash}_debug.log` with timestamps and detailed information about the auth process, connections, and token refreshing.
129+
130+
```json
131+
"args": [
132+
"mcp-remote",
133+
"https://remote.mcp.server/sse",
134+
"--debug"
135+
]
136+
```
137+
128138
### Transport Strategies
129139

130140
MCP Remote supports different transport strategies when connecting to an MCP server. This allows you to control whether it uses Server-Sent Events (SSE) or HTTP transport, and in what order it tries them.
@@ -239,6 +249,22 @@ this might look like:
239249

240250
## Debugging
241251

252+
### Debug Logs
253+
254+
For troubleshooting complex issues, especially with token refreshing or authentication problems, use the `--debug` flag:
255+
256+
```json
257+
"args": [
258+
"mcp-remote",
259+
"https://remote.mcp.server/sse",
260+
"--debug"
261+
]
262+
```
263+
264+
This creates detailed logs in `~/.mcp-auth/{server_hash}_debug.log` with timestamps and complete information about every step of the connection and authentication process. When you find issues with token refreshing, laptop sleep/resume issues, or auth problems, provide these logs when seeking support.
265+
266+
### Authentication Errors
267+
242268
If you encounter the following error, returned by the `/callback` URL:
243269

244270
```

src/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ async function runClient(
153153
}
154154

155155
// Parse command-line arguments and run the client
156-
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
157-
.then(({ serverUrl, callbackPort, headers, transportStrategy, host }) => {
156+
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port] [--debug]')
157+
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, debug }) => {
158158
return runClient(serverUrl, callbackPort, headers, transportStrategy, host)
159159
})
160160
.catch((error) => {

src/lib/coordination.ts

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventEmitter } from 'events'
33
import { Server } from 'http'
44
import express from 'express'
55
import { AddressInfo } from 'net'
6-
import { log, setupOAuthCallbackServerWithLongPoll } from './utils'
6+
import { log, debugLog, DEBUG, setupOAuthCallbackServerWithLongPoll } from './utils'
77

88
export type AuthCoordinator = {
99
initializeAuth: () => Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }>
@@ -17,8 +17,10 @@ export type AuthCoordinator = {
1717
export async function isPidRunning(pid: number): Promise<boolean> {
1818
try {
1919
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`)
2021
return true
21-
} catch {
22+
} catch (err) {
23+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Process ${pid} is not running`, err)
2224
return false
2325
}
2426
}
@@ -29,21 +31,30 @@ export async function isPidRunning(pid: number): Promise<boolean> {
2931
* @returns True if the lockfile is valid, false otherwise
3032
*/
3133
export async function isLockValid(lockData: LockfileData): Promise<boolean> {
34+
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if lockfile is valid', lockData)
35+
3236
// Check if the lockfile is too old (over 30 minutes)
3337
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
3438
if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
3539
log('Lockfile is too old')
40+
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Lockfile is too old', {
41+
age: Date.now() - lockData.timestamp,
42+
maxAge: MAX_LOCK_AGE
43+
})
3644
return false
3745
}
3846

3947
// Check if the process is still running
4048
if (!(await isPidRunning(lockData.pid))) {
4149
log('Process from lockfile is not running')
50+
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Process from lockfile is not running', { pid: lockData.pid })
4251
return false
4352
}
4453

4554
// Check if the endpoint is accessible
4655
try {
56+
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if endpoint is accessible', { port: lockData.port })
57+
4758
const controller = new AbortController()
4859
const timeout = setTimeout(() => controller.abort(), 1000)
4960

@@ -52,9 +63,13 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
5263
})
5364

5465
clearTimeout(timeout)
55-
return response.status === 200 || response.status === 202
66+
67+
const isValid = response.status === 200 || response.status === 202
68+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status })
69+
return isValid
5670
} catch (error) {
5771
log(`Error connecting to auth server: ${(error as Error).message}`)
72+
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Error connecting to auth server', error)
5873
return false
5974
}
6075
}
@@ -66,28 +81,44 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
6681
*/
6782
export async function waitForAuthentication(port: number): Promise<boolean> {
6883
log(`Waiting for authentication from the server on port ${port}...`)
84+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Waiting for authentication from server on port ${port}`)
6985

7086
try {
87+
let attempts = 0;
7188
while (true) {
89+
attempts++;
7290
const url = `http://127.0.0.1:${port}/wait-for-auth`
7391
log(`Querying: ${url}`)
74-
const response = await fetch(url)
75-
76-
if (response.status === 200) {
77-
// Auth completed, but we don't return the code anymore
78-
log(`Authentication completed by other instance`)
79-
return true
80-
} else if (response.status === 202) {
81-
// Continue polling
82-
log(`Authentication still in progress`)
83-
await new Promise((resolve) => setTimeout(resolve, 1000))
84-
} else {
85-
log(`Unexpected response status: ${response.status}`)
86-
return false
92+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll attempt ${attempts}: ${url}`)
93+
94+
try {
95+
const response = await fetch(url)
96+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll response status: ${response.status}`)
97+
98+
if (response.status === 200) {
99+
// Auth completed, but we don't return the code anymore
100+
log(`Authentication completed by other instance`)
101+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication completed by other instance`)
102+
return true
103+
} else if (response.status === 202) {
104+
// Continue polling
105+
log(`Authentication still in progress`)
106+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication still in progress, will retry in 1s`)
107+
await new Promise((resolve) => setTimeout(resolve, 1000))
108+
} else {
109+
log(`Unexpected response status: ${response.status}`)
110+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Unexpected response status`, { status: response.status })
111+
return false
112+
}
113+
} catch (fetchError) {
114+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Fetch error during poll`, fetchError)
115+
// If we can't connect, we'll try again after a delay
116+
await new Promise((resolve) => setTimeout(resolve, 2000))
87117
}
88118
}
89119
} catch (error) {
90120
log(`Error waiting for authentication: ${(error as Error).message}`)
121+
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Error waiting for authentication`, error)
91122
return false
92123
}
93124
}
@@ -110,13 +141,16 @@ export function createLazyAuthCoordinator(
110141
initializeAuth: async () => {
111142
// If auth has already been initialized, return the existing state
112143
if (authState) {
144+
if (DEBUG) await debugLog(serverUrlHash, 'Auth already initialized, reusing existing state')
113145
return authState
114146
}
115147

116148
log('Initializing auth coordination on-demand')
149+
if (DEBUG) await debugLog(serverUrlHash, 'Initializing auth coordination on-demand', { serverUrlHash, callbackPort })
117150

118151
// Initialize auth using the existing coordinateAuth logic
119152
authState = await coordinateAuth(serverUrlHash, callbackPort, events)
153+
if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth })
120154
return authState
121155
}
122156
}
@@ -134,25 +168,42 @@ export async function coordinateAuth(
134168
callbackPort: number,
135169
events: EventEmitter,
136170
): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> {
171+
if (DEBUG) await debugLog(serverUrlHash, 'Coordinating authentication', { serverUrlHash, callbackPort })
172+
137173
// Check for a lockfile (disabled on Windows for the time being)
138174
const lockData = process.platform === 'win32' ? null : await checkLockfile(serverUrlHash)
175+
176+
if (DEBUG) {
177+
if (process.platform === 'win32') {
178+
await debugLog(serverUrlHash, 'Skipping lockfile check on Windows')
179+
} else {
180+
await debugLog(serverUrlHash, 'Lockfile check result', { found: !!lockData, lockData })
181+
}
182+
}
139183

140184
// If there's a valid lockfile, try to use the existing auth process
141185
if (lockData && (await isLockValid(lockData))) {
142186
log(`Another instance is handling authentication on port ${lockData.port}`)
187+
if (DEBUG) await debugLog(serverUrlHash, 'Another instance is handling authentication', { port: lockData.port, pid: lockData.pid })
143188

144189
try {
145190
// Try to wait for the authentication to complete
191+
if (DEBUG) await debugLog(serverUrlHash, 'Waiting for authentication from other instance')
146192
const authCompleted = await waitForAuthentication(lockData.port)
193+
147194
if (authCompleted) {
148195
log('Authentication completed by another instance')
196+
if (DEBUG) await debugLog(serverUrlHash, 'Authentication completed by another instance, will use tokens from disk')
149197

150198
// Setup a dummy server - the client will use tokens directly from disk
151199
const dummyServer = express().listen(0) // Listen on any available port
200+
const dummyPort = (dummyServer.address() as AddressInfo).port
201+
if (DEBUG) await debugLog(serverUrlHash, 'Started dummy server', { port: dummyPort })
152202

153203
// This shouldn't actually be called in normal operation, but provide it for API compatibility
154204
const dummyWaitForAuthCode = () => {
155205
log('WARNING: waitForAuthCode called in secondary instance - this is unexpected')
206+
if (DEBUG) debugLog(serverUrlHash, 'WARNING: waitForAuthCode called in secondary instance - this is unexpected').catch(() => {})
156207
// Return a promise that never resolves - the client should use the tokens from disk instead
157208
return new Promise<string>(() => {})
158209
}
@@ -164,20 +215,25 @@ export async function coordinateAuth(
164215
}
165216
} else {
166217
log('Taking over authentication process...')
218+
if (DEBUG) await debugLog(serverUrlHash, 'Taking over authentication process')
167219
}
168220
} catch (error) {
169221
log(`Error waiting for authentication: ${error}`)
222+
if (DEBUG) await debugLog(serverUrlHash, 'Error waiting for authentication', error)
170223
}
171224

172225
// If we get here, the other process didn't complete auth successfully
226+
if (DEBUG) await debugLog(serverUrlHash, 'Other instance did not complete auth successfully, deleting lockfile')
173227
await deleteLockfile(serverUrlHash)
174228
} else if (lockData) {
175-
// Invalid lockfile, delete its
229+
// Invalid lockfile, delete it
176230
log('Found invalid lockfile, deleting it')
231+
if (DEBUG) await debugLog(serverUrlHash, 'Found invalid lockfile, deleting it')
177232
await deleteLockfile(serverUrlHash)
178233
}
179234

180235
// Create our own lockfile
236+
if (DEBUG) await debugLog(serverUrlHash, 'Setting up OAuth callback server', { port: callbackPort })
181237
const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({
182238
port: callbackPort,
183239
path: '/oauth/callback',
@@ -187,17 +243,21 @@ export async function coordinateAuth(
187243
// Get the actual port the server is running on
188244
const address = server.address() as AddressInfo
189245
const actualPort = address.port
246+
if (DEBUG) await debugLog(serverUrlHash, 'OAuth callback server running', { port: actualPort })
190247

191248
log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`)
249+
if (DEBUG) await debugLog(serverUrlHash, 'Creating lockfile', { serverUrlHash, pid: process.pid, port: actualPort })
192250
await createLockfile(serverUrlHash, process.pid, actualPort)
193251

194252
// Make sure lockfile is deleted on process exit
195253
const cleanupHandler = async () => {
196254
try {
197255
log(`Cleaning up lockfile for server ${serverUrlHash}`)
256+
if (DEBUG) await debugLog(serverUrlHash, 'Cleaning up lockfile')
198257
await deleteLockfile(serverUrlHash)
199258
} catch (error) {
200259
log(`Error cleaning up lockfile: ${error}`)
260+
if (DEBUG) await debugLog(serverUrlHash, 'Error cleaning up lockfile', error)
201261
}
202262
}
203263

@@ -206,14 +266,19 @@ export async function coordinateAuth(
206266
// Synchronous version for 'exit' event since we can't use async here
207267
const configPath = getConfigFilePath(serverUrlHash, 'lock.json')
208268
require('fs').unlinkSync(configPath)
209-
} catch {}
269+
if (DEBUG) console.error(`[DEBUG] Removed lockfile on exit: ${configPath}`)
270+
} catch (error) {
271+
if (DEBUG) console.error(`[DEBUG] Error removing lockfile on exit:`, error)
272+
}
210273
})
211274

212275
// Also handle SIGINT separately
213276
process.once('SIGINT', async () => {
277+
if (DEBUG) await debugLog(serverUrlHash, 'Received SIGINT signal, cleaning up')
214278
await cleanupHandler()
215279
})
216280

281+
if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination complete, returning primary instance handlers')
217282
return {
218283
server,
219284
waitForAuthCode,

0 commit comments

Comments
 (0)