@@ -208,6 +208,35 @@ def get_endpoint_schema(endpoint_id: str) -> str:
208208 return json .dumps ({"error" : str (e )}, indent = 2 )
209209
210210
211+ # Genuinely public OBP endpoints — no user identity required. Everything else goes
212+ # through the consent flow in consent mode. Conservative on purpose: only list paths
213+ # that are certainly public, since a wrong exclusion just costs an extra prompt while
214+ # a wrong inclusion would call an identity-bound endpoint with no auth.
215+ _PUBLIC_PATH_HEADS = {"root" , "glossary" , "resource-docs" , "api" }
216+
217+
218+ def _is_public_endpoint (path : str , method : str ) -> bool :
219+ """True for OBP endpoints that need no user identity (only GET can be public)."""
220+ if (method or "" ).upper () != "GET" :
221+ return False
222+ parts = [p for p in (path or "" ).split ("/" ) if p ]
223+ # Drop the leading "obp" and version segment: ["obp","v7.0.0","banks"] -> ["banks"]
224+ if len (parts ) >= 2 and parts [0 ] == "obp" and parts [1 ].startswith ("v" ):
225+ rest = parts [2 :]
226+ else :
227+ rest = parts
228+ if not rest :
229+ return True # /obp/vX.Y.Z
230+ head = rest [0 ]
231+ if head in _PUBLIC_PATH_HEADS :
232+ return True
233+ # Bank directory: the bank list and single-bank info are public; anything deeper
234+ # (accounts, transactions, …) is not.
235+ if head == "banks" and len (rest ) <= 2 :
236+ return True
237+ return False
238+
239+
211240@mcp .tool ()
212241async def call_obp_api (
213242 ctx : Context ,
@@ -308,26 +337,21 @@ async def call_obp_api(
308337 if body_role not in required_roles :
309338 required_roles .append (body_role )
310339
311- # Detect endpoints that need consent even without explicit role
312- # requirements:
313- # * account/view-scoped: path contains ACCOUNT_ID or VIEW_ID — gated by
314- # account-access-to-a-view in OBP.
315- # * user-scoped: path contains the `/my/` segment — identity-bound, the
316- # response depends on which user is calling so we still need a
317- # Consent-JWT for the user.
340+ # Scoping metadata for the consent_required payload (used by the
341+ # frontend to build the consent and label the consent card).
318342 path_segments = (endpoint .path or "" ).split ("/" )
319343 requires_view_access = "ACCOUNT_ID" in path_segments or "VIEW_ID" in path_segments
320344 is_user_scoped = "my" in path_segments
321345 account_id = (path_params or {}).get ("ACCOUNT_ID" ) or (path_params or {}).get ("account_id" )
322346 view_id = (path_params or {}).get ("VIEW_ID" ) or (path_params or {}).get ("view_id" )
323347
324- # Endpoints with no role requirements AND no account/view/user scoping are
325- # treated as public — skip the consent prompt (e.g. GET /root). OBP will
326- # reject the call if it actually needs auth, which is a better UX than a
327- # meaningless "grant consent" dialog .
328- if not required_roles and not requires_view_access and not is_user_scoped :
348+ # Consent-by-default: in consent mode every OBP call needs a Consent-JWT
349+ # for the user's identity. Skip ONLY a small allowlist of genuinely public
350+ # endpoints. This fails safe — a wrongly-excluded endpoint just shows an
351+ # extra prompt, whereas guessing "needs auth" wrong fails silently .
352+ if _is_public_endpoint ( endpoint . path , endpoint . method ) :
329353 logger .info (
330- f"Skipping consent for { endpoint .operation_id } : no required roles, not account/view-scoped, not user-scoped "
354+ f"Skipping consent for { endpoint .operation_id } : public endpoint "
331355 )
332356 else :
333357 return json .dumps ({
0 commit comments