9
9
10
10
Key components include:
11
11
- SessionManagerWrapper: Manages the lifecycle of streamable HTTP sessions
12
- - JWTAuthMiddlewareStreamableHttp: Middleware for JWT authentication
13
12
- Configuration options for:
14
13
1. stateful/stateless operation
15
14
2. JSON response mode or SSE streams
16
15
- InMemoryEventStore: A simple in-memory event storage system for maintaining session state
17
16
18
17
"""
19
18
19
+ import contextvars
20
20
import logging
21
+ import re
21
22
from collections import deque
22
23
from contextlib import AsyncExitStack , asynccontextmanager
23
24
from dataclasses import dataclass
38
39
from mcp .types import JSONRPCMessage
39
40
from starlette .datastructures import Headers
40
41
from starlette .middleware .base import BaseHTTPMiddleware
41
- from starlette .requests import Request
42
42
from starlette .responses import JSONResponse
43
43
from starlette .status import HTTP_401_UNAUTHORIZED
44
- from starlette .types import ASGIApp , Receive , Scope , Send
44
+ from starlette .types import Receive , Scope , Send
45
45
46
46
from mcpgateway .config import settings
47
47
from mcpgateway .db import SessionLocal
55
55
tool_service = ToolService ()
56
56
mcp_app = Server ("mcp-streamable-http-stateless" )
57
57
58
+ server_id_var : contextvars .ContextVar [str ] = contextvars .ContextVar ("server_id" , default = None )
59
+
58
60
## ------------------------------ Event store ------------------------------
59
61
60
62
@@ -191,13 +193,24 @@ async def list_tools() -> List[types.Tool]:
191
193
A list of Tool objects containing metadata such as name, description, and input schema.
192
194
Logs and returns an empty list on failure.
193
195
"""
194
- try :
195
- async with get_db () as db :
196
- tools = await tool_service .list_tools (db )
197
- return [types .Tool (name = tool .name , description = tool .description , inputSchema = tool .input_schema ) for tool in tools ]
198
- except Exception as e :
199
- logger .exception ("Error listing tools" )
200
- return []
196
+ server_id = server_id_var .get ()
197
+
198
+ if server_id :
199
+ try :
200
+ async with get_db () as db :
201
+ tools = await tool_service .list_server_tools (db , server_id )
202
+ return [types .Tool (name = tool .name , description = tool .description , inputSchema = tool .input_schema ) for tool in tools ]
203
+ except Exception as e :
204
+ logger .exception ("Error listing tools" )
205
+ return []
206
+ else :
207
+ try :
208
+ async with get_db () as db :
209
+ tools = await tool_service .list_tools (db )
210
+ return [types .Tool (name = tool .name , description = tool .description , inputSchema = tool .input_schema ) for tool in tools ]
211
+ except Exception as e :
212
+ logger .exception ("Error listing tools" )
213
+ return []
201
214
202
215
203
216
class SessionManagerWrapper :
@@ -226,7 +239,7 @@ def __init__(self) -> None:
226
239
)
227
240
self .stack = AsyncExitStack ()
228
241
229
- async def start (self ) -> None :
242
+ async def initialize (self ) -> None :
230
243
"""
231
244
Starts the Streamable HTTP session manager context.
232
245
"""
@@ -250,80 +263,122 @@ async def handle_streamable_http(self, scope: Scope, receive: Receive, send: Sen
250
263
send (Send): ASGI send callable.
251
264
Logs any exceptions that occur during request handling.
252
265
"""
266
+
267
+ path = scope ["modified_path" ]
268
+ match = re .search (r"/servers/(?P<server_id>\d+)/mcp" , path )
269
+
270
+ if match :
271
+ server_id = match .group ("server_id" )
272
+ server_id_var .set (server_id )
273
+
253
274
try :
254
275
await self .session_manager .handle_request (scope , receive , send )
255
276
except Exception as e :
256
277
logger .exception ("Error handling streamable HTTP request" )
257
278
raise
258
279
259
280
260
- ## ------------------------- FastAPI Middleware for Authentication ------------------------------
281
+ ## ------------------------- Authentication for /mcp routes ------------------------------
261
282
283
+ # async def streamable_http_auth(scope, receive, send):
284
+ # """
285
+ # Perform authentication check in middleware context (ASGI scope).
262
286
263
- class JWTAuthMiddlewareStreamableHttp (BaseHTTPMiddleware ):
264
- """
265
- Middleware for handling JWT authentication in an ASGI application.
266
- This middleware checks for JWT tokens in the authorization header or cookies
267
- and verifies the credentials before allowing access to protected routes.
287
+ # If path does not end with "/mcp", just continue (return True).
288
+
289
+ # If auth fails, sends 401 JSONResponse and returns False.
290
+
291
+ # If auth succeeds or not required, returns True.
292
+ # """
293
+
294
+ # path = scope.get("path", "")
295
+ # if not path.endswith("/mcp"):
296
+ # # No auth needed for other paths in this middleware usage
297
+ # return True
298
+
299
+ # headers = Headers(scope=scope)
300
+ # authorization = headers.get("authorization")
301
+ # cookie_header = headers.get("cookie", "")
302
+
303
+ # token = None
304
+ # if authorization:
305
+ # scheme, credentials = get_authorization_scheme_param(authorization)
306
+ # if scheme.lower() == "bearer" and credentials:
307
+ # token = credentials
308
+
309
+ # if not token:
310
+ # # parse cookie header manually
311
+ # for cookie in cookie_header.split(";"):
312
+ # if cookie.strip().startswith("jwt_token="):
313
+ # token = cookie.strip().split("=", 1)[1]
314
+ # break
315
+
316
+ # if settings.auth_required and not token:
317
+ # response = JSONResponse(
318
+ # {"detail": "Not authenticated"},
319
+ # status_code=HTTP_401_UNAUTHORIZED,
320
+ # headers={"WWW-Authenticate": "Bearer"},
321
+ # )
322
+ # await response(scope, receive, send)
323
+ # return False
324
+
325
+ # if token:
326
+ # try:
327
+ # await verify_credentials(token)
328
+ # except Exception:
329
+ # response = JSONResponse(
330
+ # {"detail": "Authentication failed"},
331
+ # status_code=HTTP_401_UNAUTHORIZED,
332
+ # headers={"WWW-Authenticate": "Bearer"},
333
+ # )
334
+ # await response(scope, receive, send)
335
+ # return False
336
+
337
+ # return True
338
+
339
+
340
+ async def streamable_http_auth (scope , receive , send ):
268
341
"""
342
+ Perform authentication check in middleware context (ASGI scope).
269
343
270
- def __init__ (self , app : ASGIApp ):
271
- """
272
- Initialize the middleware with the given ASGI application.
344
+ If path does not end with "/mcp", just continue (return True).
273
345
274
- Args:
275
- app (ASGIApp): The ASGI application to wrap.
276
- """
277
- super ().__init__ (app )
346
+ Only check Authorization header for Bearer token.
347
+ If no Bearer token provided, allow (return True).
278
348
279
- async def dispatch ( self , request : Request , call_next ):
280
- """
281
- Dispatch the request to the appropriate handler after performing JWT authentication.
349
+ If auth_required is True and Bearer token provided, verify it.
350
+ If verification fails, send 401 JSONResponse and return False.
351
+ """
282
352
283
- Args:
284
- request (Request): The incoming request.
285
- call_next: The next middleware or route handler in the chain.
353
+ path = scope .get ("path" , "" )
354
+ if not path .endswith ("/mcp" ) and not path .endswith ("/mcp/" ):
355
+ # No auth needed for other paths in this middleware usage
356
+ return True
286
357
287
- Returns:
288
- JSONResponse: A response indicating authentication failure if the token is invalid or missing.
289
- Response: The response from the next middleware or route handler if authentication is successful.
290
- """
291
- # Only apply auth to /mcp path
292
- if not request .url .path .startswith ("/mcp" ):
293
- return await call_next (request )
294
-
295
- headers = Headers (scope = request .scope )
296
- authorization = headers .get ("authorization" )
297
- cookie_header = headers .get ("cookie" , "" )
298
-
299
- token = None
300
- if authorization :
301
- scheme , credentials = get_authorization_scheme_param (authorization )
302
- if scheme .lower () == "bearer" and credentials :
303
- token = credentials
304
-
305
- if not token :
306
- for cookie in cookie_header .split (";" ):
307
- if cookie .strip ().startswith ("jwt_token=" ):
308
- token = cookie .strip ().split ("=" , 1 )[1 ]
309
- break
358
+ headers = Headers (scope = scope )
359
+ authorization = headers .get ("authorization" )
310
360
311
- try :
312
- if settings .auth_required and not token :
313
- return JSONResponse (
314
- {"detail" : "Not authenticated" },
315
- status_code = HTTP_401_UNAUTHORIZED ,
316
- headers = {"WWW-Authenticate" : "Bearer" },
317
- )
361
+ token = None
362
+ if authorization :
363
+ scheme , credentials = get_authorization_scheme_param (authorization )
364
+ if scheme .lower () == "bearer" and credentials :
365
+ token = credentials
318
366
319
- if token :
320
- await verify_credentials (token )
367
+ # # If no Bearer token in Authorization header, just allow (no auth)
368
+ # if not token:
369
+ # return True
321
370
322
- return await call_next (request )
371
+ # If token is present, verify it
372
+ print ("TOKEN::::" , token )
373
+ try :
374
+ await verify_credentials (token )
375
+ except Exception :
376
+ response = JSONResponse (
377
+ {"detail" : "Authentication failed" },
378
+ status_code = HTTP_401_UNAUTHORIZED ,
379
+ headers = {"WWW-Authenticate" : "Bearer" },
380
+ )
381
+ await response (scope , receive , send )
382
+ return False
323
383
324
- except Exception as e :
325
- return JSONResponse (
326
- {"detail" : "Authentication failed" },
327
- status_code = HTTP_401_UNAUTHORIZED ,
328
- headers = {"WWW-Authenticate" : "Bearer" },
329
- )
384
+ return True
0 commit comments