Skip to content

Commit d6bfee4

Browse files
committed
Public endpoints
1 parent 1d2358f commit d6bfee4

1 file changed

Lines changed: 37 additions & 13 deletions

File tree

src/mcp_server_obp/server.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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()
212241
async 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

Comments
 (0)