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
24
25
from typing import List , Union
25
26
from uuid import uuid4
26
27
27
- import mcp . types as types
28
+ from mcp import types
28
29
from fastapi .security .utils import get_authorization_scheme_param
29
30
from mcp .server .lowlevel import Server
30
31
from mcp .server .streamable_http import (
37
38
from mcp .server .streamable_http_manager import StreamableHTTPSessionManager
38
39
from mcp .types import JSONRPCMessage
39
40
from starlette .datastructures import Headers
40
- from starlette .middleware .base import BaseHTTPMiddleware
41
- from starlette .requests import Request
42
41
from starlette .responses import JSONResponse
43
42
from starlette .status import HTTP_401_UNAUTHORIZED
44
- from starlette .types import ASGIApp , Receive , Scope , Send
43
+ from starlette .types import Receive , Scope , Send
45
44
46
45
from mcpgateway .config import settings
47
46
from mcpgateway .db import SessionLocal
55
54
tool_service = ToolService ()
56
55
mcp_app = Server ("mcp-streamable-http-stateless" )
57
56
57
+ server_id_var : contextvars .ContextVar [str ] = contextvars .ContextVar ("server_id" , default = None )
58
+
58
59
## ------------------------------ Event store ------------------------------
59
60
60
61
@@ -191,13 +192,24 @@ async def list_tools() -> List[types.Tool]:
191
192
A list of Tool objects containing metadata such as name, description, and input schema.
192
193
Logs and returns an empty list on failure.
193
194
"""
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 []
195
+ server_id = server_id_var .get ()
196
+
197
+ if server_id :
198
+ try :
199
+ async with get_db () as db :
200
+ tools = await tool_service .list_server_tools (db , server_id )
201
+ return [types .Tool (name = tool .name , description = tool .description , inputSchema = tool .input_schema ) for tool in tools ]
202
+ except Exception as e :
203
+ logger .exception (f"Error listing tools:{ e } " )
204
+ return []
205
+ else :
206
+ try :
207
+ async with get_db () as db :
208
+ tools = await tool_service .list_tools (db )
209
+ return [types .Tool (name = tool .name , description = tool .description , inputSchema = tool .input_schema ) for tool in tools ]
210
+ except Exception as e :
211
+ logger .exception (f"Error listing tools:{ e } " )
212
+ return []
201
213
202
214
203
215
class SessionManagerWrapper :
@@ -226,7 +238,7 @@ def __init__(self) -> None:
226
238
)
227
239
self .stack = AsyncExitStack ()
228
240
229
- async def start (self ) -> None :
241
+ async def initialize (self ) -> None :
230
242
"""
231
243
Starts the Streamable HTTP session manager context.
232
244
"""
@@ -250,80 +262,59 @@ async def handle_streamable_http(self, scope: Scope, receive: Receive, send: Sen
250
262
send (Send): ASGI send callable.
251
263
Logs any exceptions that occur during request handling.
252
264
"""
265
+
266
+ path = scope ["modified_path" ]
267
+ match = re .search (r"/servers/(?P<server_id>\d+)/mcp" , path )
268
+
269
+ if match :
270
+ server_id = match .group ("server_id" )
271
+ server_id_var .set (server_id )
272
+
253
273
try :
254
274
await self .session_manager .handle_request (scope , receive , send )
255
275
except Exception as e :
256
- logger .exception ("Error handling streamable HTTP request" )
276
+ logger .exception (f "Error handling streamable HTTP request: { e } " )
257
277
raise
258
278
259
279
260
- ## ------------------------- FastAPI Middleware for Authentication ------------------------------
280
+ ## ------------------------- Authentication for /mcp routes ------------------------------
261
281
262
282
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.
283
+ async def streamable_http_auth (scope , receive , send ):
268
284
"""
285
+ Perform authentication check in middleware context (ASGI scope).
269
286
270
- def __init__ (self , app : ASGIApp ):
271
- """
272
- Initialize the middleware with the given ASGI application.
273
-
274
- Args:
275
- app (ASGIApp): The ASGI application to wrap.
276
- """
277
- super ().__init__ (app )
287
+ If path does not end with "/mcp", just continue (return True).
278
288
279
- async def dispatch (self , request : Request , call_next ):
280
- """
281
- Dispatch the request to the appropriate handler after performing JWT authentication.
282
-
283
- Args:
284
- request (Request): The incoming request.
285
- call_next: The next middleware or route handler in the chain.
289
+ Only check Authorization header for Bearer token.
290
+ If no Bearer token provided, allow (return True).
286
291
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
292
+ If auth_required is True and Bearer token provided, verify it.
293
+ If verification fails, send 401 JSONResponse and return False.
294
+ """
310
295
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
- )
296
+ path = scope .get ("path" , "" )
297
+ if not path .endswith ("/mcp" ) and not path .endswith ("/mcp/" ):
298
+ # No auth needed for other paths in this middleware usage
299
+ return True
318
300
319
- if token :
320
- await verify_credentials ( token )
301
+ headers = Headers ( scope = scope )
302
+ authorization = headers . get ( "authorization" )
321
303
322
- return await call_next (request )
304
+ token = None
305
+ if authorization :
306
+ scheme , credentials = get_authorization_scheme_param (authorization )
307
+ if scheme .lower () == "bearer" and credentials :
308
+ token = credentials
309
+ try :
310
+ await verify_credentials (token )
311
+ except Exception :
312
+ response = JSONResponse (
313
+ {"detail" : "Authentication failed" },
314
+ status_code = HTTP_401_UNAUTHORIZED ,
315
+ headers = {"WWW-Authenticate" : "Bearer" },
316
+ )
317
+ await response (scope , receive , send )
318
+ return False
323
319
324
- except Exception as e :
325
- return JSONResponse (
326
- {"detail" : "Authentication failed" },
327
- status_code = HTTP_401_UNAUTHORIZED ,
328
- headers = {"WWW-Authenticate" : "Bearer" },
329
- )
320
+ return True
0 commit comments