Skip to content

Commit 36ed05e

Browse files
authored
Implementing invalidateCredentials (#96)
See modelcontextprotocol/typescript-sdk#570
1 parent f8bfde1 commit 36ed05e

File tree

5 files changed

+130
-99
lines changed

5 files changed

+130
-99
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"open": "^10.1.0"
3434
},
3535
"devDependencies": {
36-
"@modelcontextprotocol/sdk": "^1.12.1",
36+
"@modelcontextprotocol/sdk": "https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508",
3737
"@types/express": "^5.0.0",
3838
"@types/node": "^22.13.10",
3939
"prettier": "^3.5.3",

pnpm-lock.yaml

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/coordination.ts

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export type AuthCoordinator = {
1818
export async function isPidRunning(pid: number): Promise<boolean> {
1919
try {
2020
process.kill(pid, 0) // Doesn't kill the process, just checks if it exists
21-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Process ${pid} is running`)
21+
if (DEBUG) debugLog(`Process ${pid} is running`)
2222
return true
2323
} catch (err) {
24-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Process ${pid} is not running`, err)
24+
if (DEBUG) debugLog(`Process ${pid} is not running`, err)
2525
return false
2626
}
2727
}
@@ -32,14 +32,14 @@ export async function isPidRunning(pid: number): Promise<boolean> {
3232
* @returns True if the lockfile is valid, false otherwise
3333
*/
3434
export async function isLockValid(lockData: LockfileData): Promise<boolean> {
35-
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if lockfile is valid', lockData)
35+
if (DEBUG) debugLog('Checking if lockfile is valid', lockData)
3636

3737
// Check if the lockfile is too old (over 30 minutes)
3838
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
3939
if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
4040
log('Lockfile is too old')
4141
if (DEBUG)
42-
await debugLog(global.currentServerUrlHash!, 'Lockfile is too old', {
42+
debugLog('Lockfile is too old', {
4343
age: Date.now() - lockData.timestamp,
4444
maxAge: MAX_LOCK_AGE,
4545
})
@@ -49,13 +49,13 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
4949
// Check if the process is still running
5050
if (!(await isPidRunning(lockData.pid))) {
5151
log('Process from lockfile is not running')
52-
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 })
5353
return false
5454
}
5555

5656
// Check if the endpoint is accessible
5757
try {
58-
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 })
5959

6060
const controller = new AbortController()
6161
const timeout = setTimeout(() => controller.abort(), 1000)
@@ -67,12 +67,11 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
6767
clearTimeout(timeout)
6868

6969
const isValid = response.status === 200 || response.status === 202
70-
if (DEBUG)
71-
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 })
7271
return isValid
7372
} catch (error) {
7473
log(`Error connecting to auth server: ${(error as Error).message}`)
75-
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Error connecting to auth server', error)
74+
if (DEBUG) debugLog('Error connecting to auth server', error)
7675
return false
7776
}
7877
}
@@ -84,44 +83,41 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
8483
*/
8584
export async function waitForAuthentication(port: number): Promise<boolean> {
8685
log(`Waiting for authentication from the server on port ${port}...`)
87-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Waiting for authentication from server on port ${port}`)
8886

8987
try {
9088
let attempts = 0
9189
while (true) {
9290
attempts++
9391
const url = `http://127.0.0.1:${port}/wait-for-auth`
9492
log(`Querying: ${url}`)
95-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll attempt ${attempts}: ${url}`)
93+
if (DEBUG) debugLog(`Poll attempt ${attempts}`)
9694

9795
try {
9896
const response = await fetch(url)
99-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll response status: ${response.status}`)
97+
if (DEBUG) debugLog(`Poll response status: ${response.status}`)
10098

10199
if (response.status === 200) {
102100
// Auth completed, but we don't return the code anymore
103101
log(`Authentication completed by other instance`)
104-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication completed by other instance`)
105102
return true
106103
} else if (response.status === 202) {
107104
// Continue polling
108105
log(`Authentication still in progress`)
109-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication still in progress, will retry in 1s`)
106+
if (DEBUG) debugLog(`Will retry in 1s`)
110107
await new Promise((resolve) => setTimeout(resolve, 1000))
111108
} else {
112109
log(`Unexpected response status: ${response.status}`)
113-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Unexpected response status`, { status: response.status })
114110
return false
115111
}
116112
} catch (fetchError) {
117-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Fetch error during poll`, fetchError)
113+
if (DEBUG) debugLog(`Fetch error during poll`, fetchError)
118114
// If we can't connect, we'll try again after a delay
119115
await new Promise((resolve) => setTimeout(resolve, 2000))
120116
}
121117
}
122118
} catch (error) {
123119
log(`Error waiting for authentication: ${(error as Error).message}`)
124-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Error waiting for authentication`, error)
120+
if (DEBUG) debugLog(`Error waiting for authentication`, error)
125121
return false
126122
}
127123
}
@@ -140,16 +136,16 @@ export function createLazyAuthCoordinator(serverUrlHash: string, callbackPort: n
140136
initializeAuth: async () => {
141137
// If auth has already been initialized, return the existing state
142138
if (authState) {
143-
if (DEBUG) await debugLog(serverUrlHash, 'Auth already initialized, reusing existing state')
139+
if (DEBUG) debugLog('Auth already initialized, reusing existing state')
144140
return authState
145141
}
146142

147143
log('Initializing auth coordination on-demand')
148-
if (DEBUG) await debugLog(serverUrlHash, 'Initializing auth coordination on-demand', { serverUrlHash, callbackPort })
144+
if (DEBUG) debugLog('Initializing auth coordination on-demand', { serverUrlHash, callbackPort })
149145

150146
// Initialize auth using the existing coordinateAuth logic
151147
authState = await coordinateAuth(serverUrlHash, callbackPort, events)
152-
if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth })
148+
if (DEBUG) debugLog('Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth })
153149
return authState
154150
},
155151
}
@@ -167,42 +163,39 @@ export async function coordinateAuth(
167163
callbackPort: number,
168164
events: EventEmitter,
169165
): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> {
170-
if (DEBUG) await debugLog(serverUrlHash, 'Coordinating authentication', { serverUrlHash, callbackPort })
166+
if (DEBUG) debugLog('Coordinating authentication', { serverUrlHash, callbackPort })
171167

172168
// Check for a lockfile (disabled on Windows for the time being)
173169
const lockData = process.platform === 'win32' ? null : await checkLockfile(serverUrlHash)
174170

175171
if (DEBUG) {
176172
if (process.platform === 'win32') {
177-
await debugLog(serverUrlHash, 'Skipping lockfile check on Windows')
173+
debugLog('Skipping lockfile check on Windows')
178174
} else {
179-
await debugLog(serverUrlHash, 'Lockfile check result', { found: !!lockData, lockData })
175+
debugLog('Lockfile check result', { found: !!lockData, lockData })
180176
}
181177
}
182178

183179
// If there's a valid lockfile, try to use the existing auth process
184180
if (lockData && (await isLockValid(lockData))) {
185-
log(`Another instance is handling authentication on port ${lockData.port}`)
186-
if (DEBUG) await debugLog(serverUrlHash, 'Another instance is handling authentication', { port: lockData.port, pid: lockData.pid })
181+
log(`Another instance is handling authentication on port ${lockData.port} (pid: ${lockData.pid})`)
187182

188183
try {
189184
// Try to wait for the authentication to complete
190-
if (DEBUG) await debugLog(serverUrlHash, 'Waiting for authentication from other instance')
185+
if (DEBUG) debugLog('Waiting for authentication from other instance')
191186
const authCompleted = await waitForAuthentication(lockData.port)
192187

193188
if (authCompleted) {
194-
log('Authentication completed by another instance')
195-
if (DEBUG) await debugLog(serverUrlHash, 'Authentication completed by another instance, will use tokens from disk')
189+
log('Authentication completed by another instance. Using tokens from disk')
196190

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

202196
// This shouldn't actually be called in normal operation, but provide it for API compatibility
203197
const dummyWaitForAuthCode = () => {
204198
log('WARNING: waitForAuthCode called in secondary instance - this is unexpected')
205-
if (DEBUG) debugLog(serverUrlHash, 'WARNING: waitForAuthCode called in secondary instance - this is unexpected').catch(() => {})
206199
// Return a promise that never resolves - the client should use the tokens from disk instead
207200
return new Promise<string>(() => {})
208201
}
@@ -214,25 +207,23 @@ export async function coordinateAuth(
214207
}
215208
} else {
216209
log('Taking over authentication process...')
217-
if (DEBUG) await debugLog(serverUrlHash, 'Taking over authentication process')
218210
}
219211
} catch (error) {
220212
log(`Error waiting for authentication: ${error}`)
221-
if (DEBUG) await debugLog(serverUrlHash, 'Error waiting for authentication', error)
213+
if (DEBUG) debugLog('Error waiting for authentication', error)
222214
}
223215

224216
// If we get here, the other process didn't complete auth successfully
225-
if (DEBUG) await debugLog(serverUrlHash, 'Other instance did not complete auth successfully, deleting lockfile')
217+
if (DEBUG) debugLog('Other instance did not complete auth successfully, deleting lockfile')
226218
await deleteLockfile(serverUrlHash)
227219
} else if (lockData) {
228220
// Invalid lockfile, delete it
229221
log('Found invalid lockfile, deleting it')
230-
if (DEBUG) await debugLog(serverUrlHash, 'Found invalid lockfile, deleting it')
231222
await deleteLockfile(serverUrlHash)
232223
}
233224

234225
// Create our own lockfile
235-
if (DEBUG) await debugLog(serverUrlHash, 'Setting up OAuth callback server', { port: callbackPort })
226+
if (DEBUG) debugLog('Setting up OAuth callback server', { port: callbackPort })
236227
const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({
237228
port: callbackPort,
238229
path: '/oauth/callback',
@@ -242,21 +233,19 @@ export async function coordinateAuth(
242233
// Get the actual port the server is running on
243234
const address = server.address() as AddressInfo
244235
const actualPort = address.port
245-
if (DEBUG) await debugLog(serverUrlHash, 'OAuth callback server running', { port: actualPort })
236+
if (DEBUG) debugLog('OAuth callback server running', { port: actualPort })
246237

247238
log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`)
248-
if (DEBUG) await debugLog(serverUrlHash, 'Creating lockfile', { serverUrlHash, pid: process.pid, port: actualPort })
249239
await createLockfile(serverUrlHash, process.pid, actualPort)
250240

251241
// Make sure lockfile is deleted on process exit
252242
const cleanupHandler = async () => {
253243
try {
254244
log(`Cleaning up lockfile for server ${serverUrlHash}`)
255-
if (DEBUG) await debugLog(serverUrlHash, 'Cleaning up lockfile')
256245
await deleteLockfile(serverUrlHash)
257246
} catch (error) {
258247
log(`Error cleaning up lockfile: ${error}`)
259-
if (DEBUG) await debugLog(serverUrlHash, 'Error cleaning up lockfile', error)
248+
if (DEBUG) debugLog('Error cleaning up lockfile', error)
260249
}
261250
}
262251

@@ -273,11 +262,11 @@ export async function coordinateAuth(
273262

274263
// Also handle SIGINT separately
275264
process.once('SIGINT', async () => {
276-
if (DEBUG) await debugLog(serverUrlHash, 'Received SIGINT signal, cleaning up')
265+
if (DEBUG) debugLog('Received SIGINT signal, cleaning up')
277266
await cleanupHandler()
278267
})
279268

280-
if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination complete, returning primary instance handlers')
269+
if (DEBUG) debugLog('Auth coordination complete, returning primary instance handlers')
281270
return {
282271
server,
283272
waitForAuthCode,

0 commit comments

Comments
 (0)