6464# First-Party
6565from mcpgateway .cache .global_config_cache import global_config_cache
6666from mcpgateway .common .models import LogLevel
67- from mcpgateway .common .validators import validate_meta_data as _validate_meta_data
6867from mcpgateway .config import settings
6968from mcpgateway .db import SessionLocal
7069from mcpgateway .middleware .rbac import _ACCESS_DENIED_MSG
@@ -1158,22 +1157,6 @@ async def _validate_streamable_session_access(
11581157 return False , HTTP_403_FORBIDDEN , "Session owner metadata unavailable"
11591158
11601159
1161- def _build_paginated_params (meta : Optional [Any ]) -> Optional [PaginatedRequestParams ]:
1162- """Build a ``PaginatedRequestParams`` carrying ``_meta`` when provided.
1163-
1164- Args:
1165- meta: Request metadata (_meta) from the original MCP request, or ``None``.
1166-
1167- Returns:
1168- A ``PaginatedRequestParams`` instance with ``_meta`` set, or ``None`` when *meta* is falsy.
1169- """
1170- if not meta :
1171- return None
1172- # CWE-532: log only key names, never values which may carry PII/tokens
1173- logger .debug ("Forwarding _meta to remote gateway (keys: %s)" , sorted (meta .keys ()) if isinstance (meta , dict ) else type (meta ).__name__ )
1174- return PaginatedRequestParams (_meta = meta )
1175-
1176-
11771160async def _proxy_list_tools_to_gateway (gateway : Any , request_headers : dict , user_context : dict , meta : Optional [Any ] = None ) -> List [types .Tool ]: # pylint: disable=unused-argument
11781161 """Proxy tools/list request directly to remote MCP gateway using MCP SDK.
11791162
@@ -1211,8 +1194,14 @@ async def _proxy_list_tools_to_gateway(gateway: Any, request_headers: dict, user
12111194 async with ClientSession (read_stream , write_stream ) as session :
12121195 await session .initialize ()
12131196
1197+ # Prepare params with _meta if provided
1198+ params = None
1199+ if meta :
1200+ params = PaginatedRequestParams (_meta = meta )
1201+ logger .debug ("Forwarding _meta to remote gateway: %s" , meta )
1202+
12141203 # List tools with _meta forwarded
1215- result = await session .list_tools (params = _build_paginated_params ( meta ) )
1204+ result = await session .list_tools (params = params )
12161205 return result .tools
12171206
12181207 except Exception as e :
@@ -1254,16 +1243,21 @@ async def _proxy_list_resources_to_gateway(gateway: Any, request_headers: dict,
12541243
12551244 logger .info ("Proxying resources/list to gateway %s at %s" , gateway .id , gateway .url )
12561245 if meta :
1257- # CWE-532: log only key names, never values which may carry PII/tokens
1258- logger .debug ("Forwarding _meta to remote gateway (keys: %s)" , sorted (meta .keys ()) if isinstance (meta , dict ) else type (meta ).__name__ )
1246+ logger .debug ("Forwarding _meta to remote gateway: %s" , meta )
12591247
12601248 # Use MCP SDK to connect and list resources
12611249 async with streamablehttp_client (url = gateway .url , headers = headers , timeout = settings .mcpgateway_direct_proxy_timeout ) as (read_stream , write_stream , _get_session_id ):
12621250 async with ClientSession (read_stream , write_stream ) as session :
12631251 await session .initialize ()
12641252
1253+ # Prepare params with _meta if provided
1254+ params = None
1255+ if meta :
1256+ params = PaginatedRequestParams (_meta = meta )
1257+ logger .debug ("Forwarding _meta to remote gateway: %s" , meta )
1258+
12651259 # List resources with _meta forwarded
1266- result = await session .list_resources (params = _build_paginated_params ( meta ) )
1260+ result = await session .list_resources (params = params )
12671261
12681262 logger .info ("Received %s resources from gateway %s" , len (result .resources ), gateway .id )
12691263 return result .resources
@@ -1315,8 +1309,7 @@ async def _proxy_read_resource_to_gateway(gateway: Any, resource_uri: str, user_
13151309
13161310 logger .info ("Proxying resources/read for %s to gateway %s at %s" , resource_uri , gateway .id , gateway .url )
13171311 if meta :
1318- # CWE-532: log only key names, never values which may carry PII/tokens
1319- logger .debug ("Forwarding _meta to remote gateway (keys: %s)" , sorted (meta .keys ()) if isinstance (meta , dict ) else type (meta ).__name__ )
1312+ logger .debug ("Forwarding _meta to remote gateway: %s" , meta )
13201313
13211314 # Use MCP SDK to connect and read resource
13221315 async with streamablehttp_client (url = gateway .url , headers = headers , timeout = settings .mcpgateway_direct_proxy_timeout ) as (read_stream , write_stream , _get_session_id ):
@@ -1326,10 +1319,8 @@ async def _proxy_read_resource_to_gateway(gateway: Any, resource_uri: str, user_
13261319 # Prepare request params with _meta if provided
13271320 if meta :
13281321 # Create params and inject _meta
1329- # by_alias=True ensures the alias "_meta" key is written so
1330- # model_validate resolves it correctly (fixes CWE-20 silent drop)
13311322 request_params = ReadResourceRequestParams (uri = resource_uri )
1332- request_params_dict = request_params .model_dump (by_alias = True )
1323+ request_params_dict = request_params .model_dump ()
13331324 request_params_dict ["_meta" ] = meta
13341325
13351326 # Send request with _meta
@@ -2353,20 +2344,23 @@ async def read_resource(resource_uri: str) -> Union[str, bytes]:
23532344 return ""
23542345
23552346 # Direct proxy mode: forward request to remote MCP server
2356- # SECURITY: CWE-532 protection - Log only meta_data key names, NEVER values
2357- # Metadata may contain PII, authentication tokens, or sensitive context that
2358- # MUST NOT be written to logs. This is a critical security control.
2359- logger .debug (
2360- "Using direct_proxy mode for resources/read %s, server %s, gateway %s (from %s header), forwarding _meta keys: %s" ,
2361- resource_uri ,
2362- server_id ,
2363- gateway .id ,
2364- GATEWAY_ID_HEADER ,
2365- sorted (meta_data .keys ()) if meta_data else None ,
2366- )
2367- # CWE-400: validate _meta limits before network I/O (bypassed in direct-proxy branch)
2368- _validate_meta_data (meta_data )
2369- contents = await _proxy_read_resource_to_gateway (gateway , str (resource_uri ), user_context , meta_data )
2347+ # Get _meta from request context if available
2348+ meta = None
2349+ try :
2350+ request_ctx = mcp_app .request_context
2351+ meta = request_ctx .meta
2352+ logger .info (
2353+ "Using direct_proxy mode for resources/read %s, server %s, gateway %s (from %s header), forwarding _meta: %s" ,
2354+ resource_uri ,
2355+ server_id ,
2356+ gateway .id ,
2357+ GATEWAY_ID_HEADER ,
2358+ meta ,
2359+ )
2360+ except (LookupError , AttributeError ) as e :
2361+ logger .debug ("No request context available for _meta extraction: %s" , e )
2362+
2363+ contents = await _proxy_read_resource_to_gateway (gateway , str (resource_uri ), user_context , meta )
23702364 if contents :
23712365 # Return first content (text or blob)
23722366 first_content = contents [0 ]
@@ -3487,15 +3481,14 @@ async def _auth_no_token(self, *, path: str, bearer_header_supplied: bool) -> bo
34873481 Returns:
34883482 True if the request is allowed with public-only access, False if rejected.
34893483 """
3490- # If client supplied a Bearer header but with empty credentials, fail closed
3491- if bearer_header_supplied :
3492- return await self ._send_error (detail = "Invalid authentication credentials" , headers = {"WWW-Authenticate" : "Bearer" })
3493-
3484+ # Build the WWW-Authenticate header, enriching it with RFC 9728
3485+ # resource_metadata when the target server has OAuth enabled.
34943486 # Per-server OAuth enforcement MUST run before the global auth check so that
34953487 # oauth_enabled servers always return 401 with resource_metadata URL (RFC 9728).
34963488 # Without this, strict mode (mcp_require_auth=True) returns a generic
34973489 # WWW-Authenticate: Bearer with no resource_metadata, and MCP clients cannot
3498- # discover the OAuth server to authenticate. (Fixes #3752)
3490+ # discover the OAuth server to authenticate.
3491+ www_auth = "Bearer"
34993492 match = _SERVER_ID_RE .search (path )
35003493 if match :
35013494 per_server_id = match .group ("server_id" )
@@ -3509,9 +3502,13 @@ async def _auth_no_token(self, *, path: str, bearer_header_supplied: bool) -> bo
35093502 logger .exception ("OAuth enforcement check failed for server %s" , per_server_id )
35103503 return await self ._send_error (detail = "Service unavailable — unable to verify server authentication requirements" , status_code = 503 )
35113504
3512- # Strict mode: require authentication (non-OAuth servers get generic 401)
3505+ # If client supplied a Bearer header but with empty credentials, fail closed
3506+ if bearer_header_supplied :
3507+ return await self ._send_error (detail = "Invalid authentication credentials" , headers = {"WWW-Authenticate" : www_auth })
3508+
3509+ # Strict mode: require authentication
35133510 if settings .mcp_require_auth :
3514- return await self ._send_error (detail = "Authentication required for MCP endpoints" , headers = {"WWW-Authenticate" : "Bearer" })
3511+ return await self ._send_error (detail = "Authentication required for MCP endpoints" , headers = {"WWW-Authenticate" : www_auth })
35153512
35163513 # Permissive mode: allow unauthenticated access with public-only scope
35173514 # Set context indicating unauthenticated user with public-only access (teams=[])
0 commit comments