Skip to content

Commit 5ed06b5

Browse files
committed
consent elicitation WIP
1 parent bc43c96 commit 5ed06b5

4 files changed

Lines changed: 121 additions & 16 deletions

File tree

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ AUTH_PROVIDER=obp-oidc
2828
# For bearer-only: Set this to accept tokens from OBP-OIDC
2929
OBP_OIDC_ISSUER_URL=http://localhost:9000/obp-oidc
3030

31+
# This setting controls how we authorize API requests to OBP after authentication.
32+
# Options:
33+
# - "oauth": Use OAuth the oauth access token. Best for when using an MCP client that cannot perform the
34+
# consent flow itself (e.g. VS Code extension, Claude Desktop).
35+
# - "consent": Elicit user consent before each API call. Best for MCP clients like Opey that will support
36+
# user consent flows.
37+
# - "none": No authorization method is used. OBP-API calls are made but with limited access (only public endpoints).
38+
OBP_AUTHORIZATION_VIA="none" # Options: "oauth", "consent", "none"
39+
3140
# Multi-issuer bearer-only mode:
3241
# Set BOTH KEYCLOAK_REALM_URL and OBP_OIDC_ISSUER_URL with AUTH_PROVIDER=bearer-only
3342
# to accept tokens from either identity provider. The server will route validation

src/mcp_server_obp/elicitation.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
1+
import json
2+
13
from fastmcp import Context
4+
from fastmcp.server.elicitation import (
5+
AcceptedElicitation,
6+
DeclinedElicitation,
7+
CancelledElicitation
8+
)
29
from dataclasses import dataclass
3-
from typing import Any
10+
from typing import Any, List
411

512
from src.tools.endpoint_index import get_endpoint_index
13+
from src.tools import EndpointSchema, RoleRequirement
614

715
@dataclass
816
class ApprovalConsent:
9-
consent_id: str
10-
11-
12-
def get_roles_for_endpoint(endpoint_id: str) -> list[str]:
17+
consent_jwt: str
18+
19+
20+
async def elicit_consent(ctx: Context, endpoint: EndpointSchema) -> AcceptedElicitation[ApprovalConsent] | DeclinedElicitation | CancelledElicitation:
1321
"""
14-
Gets the required entitlements for a given OBP endpoint from its ID.
22+
Elicit user consent for accessing a given OBP endpoint.
1523
"""
1624

17-
index = get_endpoint_index()
18-
endpoint = index.get_endpoint_schema(endpoint_id)
19-
if not endpoint:
20-
raise ValueError(f"Endpoint with ID '{endpoint_id}' not found in index.")
25+
prompt = f"Endpoint with operation ID '{endpoint.operation_id}' needs your permission to run."
26+
27+
# This message should be json-serializable so we can extract information at the frontend
28+
message_json = {
29+
"operation_id": endpoint.operation_id,
30+
"message_prompt": prompt,
31+
"required_roles": [role.model_dump() for role in endpoint.roles] # we will use these to construct our narrowly-scoped consent
32+
}
33+
34+
response = await ctx.elicit(
35+
message=json.dumps(message_json),
36+
response_type=ApprovalConsent,
37+
)
38+
39+
return response
40+
2141

22-
return endpoint.roles
42+

src/mcp_server_obp/headers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
from fastmcp import Context
3+
from fastmcp.server.dependencies import get_access_token
4+
from dotenv import load_dotenv
5+
from mcp_server_obp.elicitation import elicit_consent
6+
from src.tools import EndpointSchema
7+
8+
load_dotenv()
9+
10+
async def construct_obp_headers(ctx: Context, headers: dict, endpoint: EndpointSchema) -> dict:
11+
"""
12+
Construct headers for OBP API requests, eliciting user consent if necessary.
13+
"""
14+
authorization_method = os.getenv("OBP_AUTHORIZATION_VIA", "none").lower()
15+
if not authorization_method in ["oauth", "consent", "none"]:
16+
raise ValueError(f"Invalid OBP_AUTHORIZATION_VIA value: {authorization_method}")
17+
18+
if authorization_method == "oauth":
19+
# Use OAuth access token from context
20+
access_token = get_access_token()
21+
if not access_token:
22+
raise ValueError("No access token available in context for OAuth authorization.")
23+
24+
obp_headers = {
25+
**headers,
26+
"Authorization": f"Bearer {access_token}"
27+
}
28+
return obp_headers
29+
elif authorization_method == "consent":
30+
# Elicit user consent for the endpoint
31+
consent_response = await elicit_consent(ctx, endpo)

src/mcp_server_obp/server.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,25 @@
2424
if str(_project_root) not in sys.path:
2525
sys.path.insert(0, str(_project_root))
2626

27-
from fastmcp import FastMCP
27+
from fastmcp import FastMCP, Context
2828
from fastmcp.server.dependencies import get_access_token
29+
from fastmcp.server.elicitation import AcceptedElicitation, DeclinedElicitation, CancelledElicitation
2930

3031
from src.tools.endpoint_index import get_endpoint_index
3132
from src.tools.glossary_index import get_glossary_index
3233

3334
from mcp_server_obp.lifespan import lifespan
3435
from mcp_server_obp.auth import get_auth_provider
36+
from mcp_server_obp.elicitation import elicit_consent, ApprovalConsent
3537

3638
logger = logging.getLogger(__name__)
3739

3840
# Initialize authentication provider based on environment configuration
3941
# Supports: keycloak, obp-oidc, or none (disabled)
4042
# See auth.py for configuration details
4143
auth = get_auth_provider()
42-
44+
45+
4346

4447
mcp = FastMCP(
4548
"Open Bank Project",
@@ -142,12 +145,13 @@ def get_endpoint_schema(endpoint_id: str) -> str:
142145

143146

144147
@mcp.tool()
145-
def call_obp_api(
148+
async def call_obp_api(
149+
ctx: Context,
146150
endpoint_id: str,
147151
path_params: dict = None,
148152
query_params: dict = None,
149153
body: dict = None,
150-
headers: dict = None
154+
headers: dict = None,
151155
) -> str:
152156
"""
153157
Execute an OBP API request to a specific endpoint.
@@ -173,7 +177,7 @@ def call_obp_api(
173177
index = get_endpoint_index()
174178

175179
# Get endpoint info from lightweight index
176-
endpoint = index.get_endpoint_by_id(endpoint_id)
180+
endpoint = index.get_endpoint_schema(endpoint_id)
177181
if not endpoint:
178182
return json.dumps({
179183
"error": f"Endpoint '{endpoint_id}' not found",
@@ -205,6 +209,47 @@ def call_obp_api(
205209
method = endpoint.method.value # HttpMethod enum value
206210
request_headers = headers or {}
207211

212+
auth_method = os.getenv("OBP_AUTHORIZATION_VIA", "none").lower()
213+
match auth_method:
214+
case "oauth":
215+
logger.info("Using OAuth authorization method for OBP API call")
216+
access_token = get_access_token()
217+
if not access_token:
218+
logger.error("No access token available in context for OAuth authorization.")
219+
else:
220+
# TODO: Elicit a simple approval here to confirm tool use?
221+
request_headers["Authorization"] = f"Bearer {access_token}"
222+
case "consent":
223+
# Elicit user consent
224+
logger.info("Eliciting user consent for OBP API call")
225+
from mcp_server_obp.elicitation import elicit_consent
226+
227+
response = await elicit_consent(ctx, endpoint)
228+
match response:
229+
case AcceptedElicitation(data=consent):
230+
# For now just assume the consent is valid, but we might want to decode the JWT in the future
231+
logger.info("User consent accepted for OBP API call")
232+
request_headers["Consent-JWT"] = consent.consent_jwt
233+
case DeclinedElicitation():
234+
logger.info("User declined consent for OBP API call")
235+
return json.dumps({
236+
"error": "User declined consent for this endpoint"
237+
}, indent=2)
238+
case CancelledElicitation():
239+
logger.info("User cancelled consent elicitation for OBP API call")
240+
return json.dumps({
241+
"error": "User cancelled operation."
242+
}, indent=2)
243+
case "none":
244+
# No authorization
245+
logger.info("No authorization method used for OBP API call")
246+
pass
247+
case _:
248+
logger.error(f"Invalid OBP_AUTHORIZATION_VIA value: {auth_method}")
249+
return json.dumps({
250+
"error": f"Internal server error."
251+
}, indent=2)
252+
208253
# Make the request
209254
logger.info(f"Calling OBP API: {method} {url}")
210255

0 commit comments

Comments
 (0)