@@ -56,177 +56,177 @@ interface PendingAuth {
5656 timeout : ReturnType < typeof setTimeout >
5757}
5858
59- export namespace McpOAuthCallback {
60- let server : ReturnType < typeof createServer > | undefined
61- const pendingAuths = new Map < string , PendingAuth > ( )
62- // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
63- // find the right entry in pendingAuths (which is keyed by oauthState).
64- const mcpNameToState = new Map < string , string > ( )
65-
66- const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
67-
68- function cleanupStateIndex ( oauthState : string ) {
69- for ( const [ name , state ] of mcpNameToState ) {
70- if ( state === oauthState ) {
71- mcpNameToState . delete ( name )
72- break
73- }
59+ let server : ReturnType < typeof createServer > | undefined
60+ const pendingAuths = new Map < string , PendingAuth > ( )
61+ // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
62+ // find the right entry in pendingAuths (which is keyed by oauthState).
63+ const mcpNameToState = new Map < string , string > ( )
64+
65+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
66+
67+ function cleanupStateIndex ( oauthState : string ) {
68+ for ( const [ name , state ] of mcpNameToState ) {
69+ if ( state === oauthState ) {
70+ mcpNameToState . delete ( name )
71+ break
7472 }
7573 }
74+ }
7675
77- function handleRequest ( req : import ( "http" ) . IncomingMessage , res : import ( "http" ) . ServerResponse ) {
78- const url = new URL ( req . url || "/" , `http://localhost:${ currentPort } ` )
76+ function handleRequest ( req : import ( "http" ) . IncomingMessage , res : import ( "http" ) . ServerResponse ) {
77+ const url = new URL ( req . url || "/" , `http://localhost:${ currentPort } ` )
7978
80- if ( url . pathname !== currentPath ) {
81- res . writeHead ( 404 )
82- res . end ( "Not found" )
83- return
84- }
79+ if ( url . pathname !== currentPath ) {
80+ res . writeHead ( 404 )
81+ res . end ( "Not found" )
82+ return
83+ }
8584
86- const code = url . searchParams . get ( "code" )
87- const state = url . searchParams . get ( "state" )
88- const error = url . searchParams . get ( "error" )
89- const errorDescription = url . searchParams . get ( "error_description" )
85+ const code = url . searchParams . get ( "code" )
86+ const state = url . searchParams . get ( "state" )
87+ const error = url . searchParams . get ( "error" )
88+ const errorDescription = url . searchParams . get ( "error_description" )
9089
91- log . info ( "received oauth callback" , { hasCode : ! ! code , state, error } )
90+ log . info ( "received oauth callback" , { hasCode : ! ! code , state, error } )
9291
93- // Enforce state parameter presence
94- if ( ! state ) {
95- const errorMsg = "Missing required state parameter - potential CSRF attack"
96- log . error ( "oauth callback missing state parameter" , { url : url . toString ( ) } )
97- res . writeHead ( 400 , { "Content-Type" : "text/html" } )
98- res . end ( HTML_ERROR ( errorMsg ) )
99- return
100- }
92+ // Enforce state parameter presence
93+ if ( ! state ) {
94+ const errorMsg = "Missing required state parameter - potential CSRF attack"
95+ log . error ( "oauth callback missing state parameter" , { url : url . toString ( ) } )
96+ res . writeHead ( 400 , { "Content-Type" : "text/html" } )
97+ res . end ( HTML_ERROR ( errorMsg ) )
98+ return
99+ }
101100
102- if ( error ) {
103- const errorMsg = errorDescription || error
104- if ( pendingAuths . has ( state ) ) {
105- const pending = pendingAuths . get ( state ) !
106- clearTimeout ( pending . timeout )
107- pendingAuths . delete ( state )
108- cleanupStateIndex ( state )
109- pending . reject ( new Error ( errorMsg ) )
110- }
111- res . writeHead ( 200 , { "Content-Type" : "text/html" } )
112- res . end ( HTML_ERROR ( errorMsg ) )
113- return
101+ if ( error ) {
102+ const errorMsg = errorDescription || error
103+ if ( pendingAuths . has ( state ) ) {
104+ const pending = pendingAuths . get ( state ) !
105+ clearTimeout ( pending . timeout )
106+ pendingAuths . delete ( state )
107+ cleanupStateIndex ( state )
108+ pending . reject ( new Error ( errorMsg ) )
114109 }
110+ res . writeHead ( 200 , { "Content-Type" : "text/html" } )
111+ res . end ( HTML_ERROR ( errorMsg ) )
112+ return
113+ }
115114
116- if ( ! code ) {
117- res . writeHead ( 400 , { "Content-Type" : "text/html" } )
118- res . end ( HTML_ERROR ( "No authorization code provided" ) )
119- return
120- }
115+ if ( ! code ) {
116+ res . writeHead ( 400 , { "Content-Type" : "text/html" } )
117+ res . end ( HTML_ERROR ( "No authorization code provided" ) )
118+ return
119+ }
121120
122- // Validate state parameter
123- if ( ! pendingAuths . has ( state ) ) {
124- const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
125- log . error ( "oauth callback with invalid state" , { state, pendingStates : Array . from ( pendingAuths . keys ( ) ) } )
126- res . writeHead ( 400 , { "Content-Type" : "text/html" } )
127- res . end ( HTML_ERROR ( errorMsg ) )
128- return
129- }
121+ // Validate state parameter
122+ if ( ! pendingAuths . has ( state ) ) {
123+ const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
124+ log . error ( "oauth callback with invalid state" , { state, pendingStates : Array . from ( pendingAuths . keys ( ) ) } )
125+ res . writeHead ( 400 , { "Content-Type" : "text/html" } )
126+ res . end ( HTML_ERROR ( errorMsg ) )
127+ return
128+ }
130129
131- const pending = pendingAuths . get ( state ) !
130+ const pending = pendingAuths . get ( state ) !
132131
133- clearTimeout ( pending . timeout )
134- pendingAuths . delete ( state )
135- cleanupStateIndex ( state )
136- pending . resolve ( code )
132+ clearTimeout ( pending . timeout )
133+ pendingAuths . delete ( state )
134+ cleanupStateIndex ( state )
135+ pending . resolve ( code )
137136
138- res . writeHead ( 200 , { "Content-Type" : "text/html" } )
139- res . end ( HTML_SUCCESS )
140- }
137+ res . writeHead ( 200 , { "Content-Type" : "text/html" } )
138+ res . end ( HTML_SUCCESS )
139+ }
141140
142- export async function ensureRunning ( redirectUri ?: string ) : Promise < void > {
143- // Parse the redirect URI to get port and path (uses defaults if not provided)
144- const { port, path } = parseRedirectUri ( redirectUri )
141+ export async function ensureRunning ( redirectUri ?: string ) : Promise < void > {
142+ // Parse the redirect URI to get port and path (uses defaults if not provided)
143+ const { port, path } = parseRedirectUri ( redirectUri )
145144
146- // If server is running on a different port/path, stop it first
147- if ( server && ( currentPort !== port || currentPath !== path ) ) {
148- log . info ( "stopping oauth callback server to reconfigure" , { oldPort : currentPort , newPort : port } )
149- await stop ( )
150- }
145+ // If server is running on a different port/path, stop it first
146+ if ( server && ( currentPort !== port || currentPath !== path ) ) {
147+ log . info ( "stopping oauth callback server to reconfigure" , { oldPort : currentPort , newPort : port } )
148+ await stop ( )
149+ }
151150
152- if ( server ) return
151+ if ( server ) return
153152
154- const running = await isPortInUse ( port )
155- if ( running ) {
156- log . info ( "oauth callback server already running on another instance" , { port } )
157- return
158- }
153+ const running = await isPortInUse ( port )
154+ if ( running ) {
155+ log . info ( "oauth callback server already running on another instance" , { port } )
156+ return
157+ }
159158
160- currentPort = port
161- currentPath = path
159+ currentPort = port
160+ currentPath = path
162161
163- server = createServer ( handleRequest )
164- await new Promise < void > ( ( resolve , reject ) => {
165- server ! . listen ( currentPort , ( ) => {
166- log . info ( "oauth callback server started" , { port : currentPort , path : currentPath } )
167- resolve ( )
168- } )
169- server ! . on ( "error" , reject )
162+ server = createServer ( handleRequest )
163+ await new Promise < void > ( ( resolve , reject ) => {
164+ server ! . listen ( currentPort , ( ) => {
165+ log . info ( "oauth callback server started" , { port : currentPort , path : currentPath } )
166+ resolve ( )
170167 } )
171- }
168+ server ! . on ( "error" , reject )
169+ } )
170+ }
172171
173- export function waitForCallback ( oauthState : string , mcpName ?: string ) : Promise < string > {
174- if ( mcpName ) mcpNameToState . set ( mcpName , oauthState )
175- return new Promise ( ( resolve , reject ) => {
176- const timeout = setTimeout ( ( ) => {
177- if ( pendingAuths . has ( oauthState ) ) {
178- pendingAuths . delete ( oauthState )
179- if ( mcpName ) mcpNameToState . delete ( mcpName )
180- reject ( new Error ( "OAuth callback timeout - authorization took too long" ) )
181- }
182- } , CALLBACK_TIMEOUT_MS )
183-
184- pendingAuths . set ( oauthState , { resolve, reject, timeout } )
185- } )
186- }
172+ export function waitForCallback ( oauthState : string , mcpName ?: string ) : Promise < string > {
173+ if ( mcpName ) mcpNameToState . set ( mcpName , oauthState )
174+ return new Promise ( ( resolve , reject ) => {
175+ const timeout = setTimeout ( ( ) => {
176+ if ( pendingAuths . has ( oauthState ) ) {
177+ pendingAuths . delete ( oauthState )
178+ if ( mcpName ) mcpNameToState . delete ( mcpName )
179+ reject ( new Error ( "OAuth callback timeout - authorization took too long" ) )
180+ }
181+ } , CALLBACK_TIMEOUT_MS )
187182
188- export function cancelPending ( mcpName : string ) : void {
189- // Look up the oauthState for this mcpName via the reverse index
190- const oauthState = mcpNameToState . get ( mcpName )
191- const key = oauthState ?? mcpName
192- const pending = pendingAuths . get ( key )
193- if ( pending ) {
194- clearTimeout ( pending . timeout )
195- pendingAuths . delete ( key )
196- mcpNameToState . delete ( mcpName )
197- pending . reject ( new Error ( "Authorization cancelled" ) )
198- }
199- }
183+ pendingAuths . set ( oauthState , { resolve, reject, timeout } )
184+ } )
185+ }
200186
201- export async function isPortInUse ( port : number = OAUTH_CALLBACK_PORT ) : Promise < boolean > {
202- return new Promise ( ( resolve ) => {
203- const socket = createConnection ( port , "127.0.0.1" )
204- socket . on ( "connect" , ( ) => {
205- socket . destroy ( )
206- resolve ( true )
207- } )
208- socket . on ( "error" , ( ) => {
209- resolve ( false )
210- } )
211- } )
187+ export function cancelPending ( mcpName : string ) : void {
188+ // Look up the oauthState for this mcpName via the reverse index
189+ const oauthState = mcpNameToState . get ( mcpName )
190+ const key = oauthState ?? mcpName
191+ const pending = pendingAuths . get ( key )
192+ if ( pending ) {
193+ clearTimeout ( pending . timeout )
194+ pendingAuths . delete ( key )
195+ mcpNameToState . delete ( mcpName )
196+ pending . reject ( new Error ( "Authorization cancelled" ) )
212197 }
198+ }
213199
214- export async function stop ( ) : Promise < void > {
215- if ( server ) {
216- await new Promise < void > ( ( resolve ) => server ! . close ( ( ) => resolve ( ) ) )
217- server = undefined
218- log . info ( "oauth callback server stopped" )
219- }
200+ export async function isPortInUse ( port : number = OAUTH_CALLBACK_PORT ) : Promise < boolean > {
201+ return new Promise ( ( resolve ) => {
202+ const socket = createConnection ( port , "127.0.0.1" )
203+ socket . on ( "connect" , ( ) => {
204+ socket . destroy ( )
205+ resolve ( true )
206+ } )
207+ socket . on ( "error" , ( ) => {
208+ resolve ( false )
209+ } )
210+ } )
211+ }
220212
221- for ( const [ _name , pending ] of pendingAuths ) {
222- clearTimeout ( pending . timeout )
223- pending . reject ( new Error ( "OAuth callback server stopped" ) )
224- }
225- pendingAuths . clear ( )
226- mcpNameToState . clear ( )
213+ export async function stop ( ) : Promise < void > {
214+ if ( server ) {
215+ await new Promise < void > ( ( resolve ) => server ! . close ( ( ) => resolve ( ) ) )
216+ server = undefined
217+ log . info ( "oauth callback server stopped" )
227218 }
228219
229- export function isRunning ( ) : boolean {
230- return server !== undefined
220+ for ( const [ _name , pending ] of pendingAuths ) {
221+ clearTimeout ( pending . timeout )
222+ pending . reject ( new Error ( "OAuth callback server stopped" ) )
231223 }
224+ pendingAuths . clear ( )
225+ mcpNameToState . clear ( )
232226}
227+
228+ export function isRunning ( ) : boolean {
229+ return server !== undefined
230+ }
231+
232+ export * as McpOAuthCallback from "./oauth-callback"
0 commit comments