@@ -38,6 +38,7 @@ export interface McpWebSocketServerOptions {
3838export interface McpConnectionInfo {
3939 id : string ;
4040 connectedAt : number ;
41+ authenticated : boolean ;
4142 clientInfo ?: {
4243 name ?: string ;
4344 version ?: string ;
@@ -69,6 +70,14 @@ interface WsWebSocketServer {
6970// WebSocket ready states
7071const WS_OPEN = 1 ;
7172
73+ // Authentication timeout (5 seconds)
74+ const AUTH_TIMEOUT_MS = 5000 ;
75+
76+ // Custom close codes for authentication
77+ const WS_CLOSE_AUTH_TIMEOUT = 4001 ;
78+ const WS_CLOSE_AUTH_REQUIRED = 4002 ;
79+ const WS_CLOSE_AUTH_INVALID = 4003 ;
80+
7281// ============================================================================
7382// MCP WebSocket Server
7483// ============================================================================
@@ -77,12 +86,15 @@ export class McpWebSocketServer extends Disposable {
7786 private httpServer : HttpServer | null = null ;
7887 private wss : WsWebSocketServer | null = null ;
7988 private readonly router : McpRequestRouter ;
80- private readonly connections = new Map < string , { ws : WsWebSocket ; info : McpConnectionInfo } > ( ) ;
89+ private readonly connections = new Map < string , { ws : WsWebSocket ; info : McpConnectionInfo ; authTimeout ?: ReturnType < typeof setTimeout > } > ( ) ;
8190 private connectionCounter = 0 ;
8291
8392 private _port : number | undefined ;
8493 private _host : string = 'localhost' ;
8594
95+ // Authentication token (only processes in Roopik's tree have this via env var)
96+ private readonly authToken : string ;
97+
8698 // Events
8799 private readonly _onServerStarted = this . _register ( new Emitter < { port : number ; host : string } > ( ) ) ;
88100 readonly onServerStarted : Event < { port : number ; host : string } > = this . _onServerStarted . event ;
@@ -96,9 +108,10 @@ export class McpWebSocketServer extends Disposable {
96108 private readonly _onClientDisconnected = this . _register ( new Emitter < string > ( ) ) ;
97109 readonly onClientDisconnected : Event < string > = this . _onClientDisconnected . event ;
98110
99- constructor ( toolExecutor : ToolExecutor ) {
111+ constructor ( toolExecutor : ToolExecutor , authToken : string ) {
100112 super ( ) ;
101113 this . router = new McpRequestRouter ( toolExecutor ) ;
114+ this . authToken = authToken ;
102115 }
103116
104117 /**
@@ -221,12 +234,21 @@ export class McpWebSocketServer extends Disposable {
221234
222235 const connectionInfo : McpConnectionInfo = {
223236 id : connectionId ,
224- connectedAt : Date . now ( )
237+ connectedAt : Date . now ( ) ,
238+ authenticated : false
225239 } ;
226240
227- this . connections . set ( connectionId , { ws, info : connectionInfo } ) ;
228- console . log ( `[MCP WebSocket] Client connected: ${ connectionId } ` ) ;
229- this . _onClientConnected . fire ( connectionInfo ) ;
241+ // Set authentication timeout - client must auth within 5 seconds
242+ const authTimeout = setTimeout ( ( ) => {
243+ const conn = this . connections . get ( connectionId ) ;
244+ if ( conn && ! conn . info . authenticated ) {
245+ console . log ( `[MCP WebSocket] Auth timeout for ${ connectionId } , closing` ) ;
246+ ws . close ( WS_CLOSE_AUTH_TIMEOUT , 'Authentication timeout' ) ;
247+ }
248+ } , AUTH_TIMEOUT_MS ) ;
249+
250+ this . connections . set ( connectionId , { ws, info : connectionInfo , authTimeout } ) ;
251+ console . log ( `[MCP WebSocket] Client connected: ${ connectionId } (awaiting auth)` ) ;
230252
231253 // Handle incoming messages
232254 ws . on ( 'message' , async ( data : unknown ) => {
@@ -236,6 +258,10 @@ export class McpWebSocketServer extends Disposable {
236258 // Handle close
237259 ws . on ( 'close' , ( code : number , _reason : Buffer ) => {
238260 console . log ( `[MCP WebSocket] Client disconnected: ${ connectionId } (code: ${ code } )` ) ;
261+ const conn = this . connections . get ( connectionId ) ;
262+ if ( conn ?. authTimeout ) {
263+ clearTimeout ( conn . authTimeout ) ;
264+ }
239265 this . connections . delete ( connectionId ) ;
240266 this . _onClientDisconnected . fire ( connectionId ) ;
241267 } ) ;
@@ -247,20 +273,63 @@ export class McpWebSocketServer extends Disposable {
247273 }
248274
249275 private async handleMessage ( connectionId : string , ws : WsWebSocket , data : unknown ) : Promise < void > {
250- let request : McpRequest ;
276+ const conn = this . connections . get ( connectionId ) ;
277+ if ( ! conn ) {
278+ return ;
279+ }
280+
281+ let message : { type ?: string ; token ?: string ; jsonrpc ?: string ; method ?: string ; id ?: string | number ; params ?: unknown } ;
251282
252283 try {
253284 // Parse message
254285 const messageStr = data instanceof Buffer ? data . toString ( 'utf-8' ) : String ( data ) ;
255- request = JSON . parse ( messageStr ) as McpRequest ;
286+ message = JSON . parse ( messageStr ) ;
287+ } catch {
288+ this . sendError ( ws , null , MCP_ERROR_CODES . PARSE_ERROR , 'Failed to parse JSON' ) ;
289+ return ;
290+ }
256291
257- // Validate JSON-RPC format
258- if ( request . jsonrpc !== '2.0' || ! request . method ) {
259- this . sendError ( ws , request ?. id ?? null , MCP_ERROR_CODES . INVALID_REQUEST , 'Invalid JSON-RPC request' ) ;
292+ // ============================================================
293+ // Authentication Flow
294+ // ============================================================
295+
296+ // If not authenticated, first message MUST be auth
297+ if ( ! conn . info . authenticated ) {
298+ if ( message . type === 'auth' ) {
299+ // Validate token
300+ if ( message . token === this . authToken ) {
301+ conn . info . authenticated = true ;
302+ // Clear auth timeout
303+ if ( conn . authTimeout ) {
304+ clearTimeout ( conn . authTimeout ) ;
305+ conn . authTimeout = undefined ;
306+ }
307+ console . log ( `[MCP WebSocket] Client authenticated: ${ connectionId } ` ) ;
308+ this . _onClientConnected . fire ( conn . info ) ;
309+ // Send success response
310+ this . send ( ws , { jsonrpc : '2.0' , id : 0 , result : { type : 'auth_success' } } ) ;
311+ } else {
312+ console . log ( `[MCP WebSocket] Invalid token from ${ connectionId } , rejecting` ) ;
313+ ws . close ( WS_CLOSE_AUTH_INVALID , 'Invalid token' ) ;
314+ }
315+ return ;
316+ } else {
317+ // Non-auth message before authentication
318+ console . log ( `[MCP WebSocket] Non-auth message from unauthenticated client ${ connectionId } ` ) ;
319+ ws . close ( WS_CLOSE_AUTH_REQUIRED , 'Authentication required' ) ;
260320 return ;
261321 }
262- } catch {
263- this . sendError ( ws , null , MCP_ERROR_CODES . PARSE_ERROR , 'Failed to parse JSON' ) ;
322+ }
323+
324+ // ============================================================
325+ // Normal MCP Request Processing (authenticated clients only)
326+ // ============================================================
327+
328+ const request = message as McpRequest ;
329+
330+ // Validate JSON-RPC format
331+ if ( request . jsonrpc !== '2.0' || ! request . method ) {
332+ this . sendError ( ws , request ?. id ?? null , MCP_ERROR_CODES . INVALID_REQUEST , 'Invalid JSON-RPC request' ) ;
264333 return ;
265334 }
266335
@@ -269,13 +338,10 @@ export class McpWebSocketServer extends Disposable {
269338
270339 // Update client info if this was an initialize request
271340 if ( request . method === 'initialize' && request . params ) {
272- const conn = this . connections . get ( connectionId ) ;
273- if ( conn ) {
274- conn . info . clientInfo = {
275- name : ( request . params as { clientInfo ?: { name ?: string } } ) . clientInfo ?. name ,
276- version : ( request . params as { clientInfo ?: { version ?: string } } ) . clientInfo ?. version
277- } ;
278- }
341+ conn . info . clientInfo = {
342+ name : ( request . params as { clientInfo ?: { name ?: string } } ) . clientInfo ?. name ,
343+ version : ( request . params as { clientInfo ?: { version ?: string } } ) . clientInfo ?. version
344+ } ;
279345 }
280346
281347 // Send response
0 commit comments