Skip to content

Commit bc43c96

Browse files
committed
feature/allow index generation from resource docs
1 parent 8a7d9e1 commit bc43c96

6 files changed

Lines changed: 354 additions & 22 deletions

File tree

database/obp_utils.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,47 @@
66
from typing import Dict, Any
77

88

9-
def get_obp_config(endpoint_type="static"):
10-
"""Get OBP configuration from environment variables."""
9+
def get_obp_config(endpoint_type: str = "static", use_resource_docs: bool = True) -> Dict[str, str]:
10+
"""
11+
Get OBP configuration from environment variables.
12+
13+
Args:
14+
endpoint_type: Type of endpoints to fetch ("static", "dynamic", or "all")
15+
use_resource_docs: If True, use resource-docs endpoint (includes roles).
16+
If False, use swagger endpoint.
17+
18+
Returns:
19+
Dictionary with configuration including URLs for data fetching
20+
"""
1121
base_url = os.getenv("OBP_BASE_URL")
1222
api_version = os.getenv("OBP_API_VERSION")
1323

1424
if not all([base_url, api_version]):
1525
raise ValueError("Missing required environment variables: OBP_BASE_URL, OBP_API_VERSION")
1626

17-
return {
27+
config = {
1828
"base_url": base_url,
1929
"api_version": api_version,
2030
"glossary_url": f"{base_url}/obp/{api_version}/api/glossary",
21-
"swagger_url": f"{base_url}/obp/{api_version}/resource-docs/{api_version}/swagger?content={endpoint_type}"
31+
"swagger_url": f"{base_url}/obp/{api_version}/resource-docs/{api_version}/swagger?content={endpoint_type}",
32+
"resource_docs_url": f"{base_url}/obp/{api_version}/resource-docs/{api_version}/obp?content={endpoint_type}",
2233
}
34+
35+
# Set the primary data URL based on preference
36+
if use_resource_docs:
37+
config["data_url"] = config["resource_docs_url"]
38+
config["data_format"] = "resource_docs"
39+
else:
40+
config["data_url"] = config["swagger_url"]
41+
config["data_format"] = "swagger"
42+
43+
return config
2344

2445

2546
def fetch_obp_data(url: str) -> Dict[str, Any]:
2647
"""Fetch data from OBP API endpoint."""
2748
print(f"Fetching data from: {url}")
28-
response = requests.get(url, timeout=30)
49+
response = requests.get(url, timeout=60) # Increased timeout for large responses
2950
try:
3051
response.raise_for_status()
3152
except requests.exceptions.HTTPError as e:

scripts/generate_endpoint_index.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#!/usr/bin/env python3
22
"""
3-
Script to generate the lightweight endpoint index from OBP swagger data.
3+
Script to generate the lightweight endpoint index from OBP resource docs.
44
5-
This script fetches the swagger/OpenAPI specification from the OBP API
6-
and generates a lightweight index for fast endpoint discovery.
5+
This script fetches the resource docs from the OBP API and generates a
6+
lightweight index for fast endpoint discovery. Resource docs provide richer
7+
information than swagger, including roles/entitlements required for each endpoint.
78
"""
89
import os
910
import sys
@@ -23,7 +24,7 @@ def main():
2324

2425
# Parse command line arguments
2526
parser = argparse.ArgumentParser(
26-
description="Generate lightweight endpoint index from OBP swagger data."
27+
description="Generate lightweight endpoint index from OBP resource docs."
2728
)
2829
parser.add_argument(
2930
"--endpoints",
@@ -37,33 +38,48 @@ def main():
3738
default=None,
3839
help="Output file path for the index (default: database/endpoint_index.json)"
3940
)
41+
parser.add_argument(
42+
"--use-swagger",
43+
action="store_true",
44+
help="Use swagger endpoint instead of resource docs (not recommended - lacks role info)"
45+
)
4046
args = parser.parse_args()
4147

48+
use_resource_docs = not args.use_swagger
49+
4250
try:
4351
# Get configuration
44-
config = get_obp_config(args.endpoints)
52+
config = get_obp_config(args.endpoints, use_resource_docs=use_resource_docs)
4553

46-
# Fetch swagger data
54+
# Fetch data
4755
print("\n" + "="*50)
48-
print("FETCHING SWAGGER DATA")
56+
print(f"FETCHING {'RESOURCE DOCS' if use_resource_docs else 'SWAGGER'} DATA")
4957
print("="*50)
50-
swagger_data = fetch_obp_data(config["swagger_url"])
58+
print(f"URL: {config['data_url']}")
59+
60+
data = fetch_obp_data(config["data_url"])
5161

5262
# Build index
5363
print("\n" + "="*50)
5464
print("BUILDING ENDPOINT INDEX")
5565
print("="*50)
5666

5767
index = EndpointIndex(index_file=args.output)
58-
index.build_index_from_swagger(swagger_data)
68+
69+
if use_resource_docs:
70+
index.build_index_from_resource_docs(data)
71+
else:
72+
index.build_index_from_swagger(data)
5973

6074
# Print statistics
6175
print("\n" + "="*50)
6276
print("INDEX GENERATION COMPLETE")
6377
print("="*50)
78+
print(f"Data source: {'Resource Docs' if use_resource_docs else 'Swagger'}")
6479
print(f"Total endpoints indexed: {len(index._index)}")
6580
print(f"Total unique tags: {len(index.get_all_tags())}")
66-
print(f"Output file: {index.index_file}")
81+
print(f"Index file: {index.index_file}")
82+
print(f"Schemas file: {index.schemas_file}")
6783

6884
# Print sample of tags
6985
all_tags = index.get_all_tags()
@@ -72,8 +88,18 @@ def main():
7288
for tag in all_tags[:10]:
7389
print(f" - {tag}")
7490

91+
# If using resource docs, show role statistics
92+
if use_resource_docs:
93+
endpoints_with_roles = sum(
94+
1 for schema in index._schemas.values()
95+
if schema.get("roles")
96+
)
97+
print(f"\nEndpoints with role requirements: {endpoints_with_roles}")
98+
7599
except Exception as e:
76100
print(f"Error during index generation: {e}")
101+
import traceback
102+
traceback.print_exc()
77103
sys.exit(1)
78104

79105

src/mcp_server_obp/elicitation.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ class ApprovalConsent:
99
consent_id: str
1010

1111

12-
def get_roles(endpoint: dict[str, Any]) -> list[str]:
12+
def get_roles_for_endpoint(endpoint_id: str) -> list[str]:
1313
"""
14-
Gets the required entitlements for a given OBP endpoint.
14+
Gets the required entitlements for a given OBP endpoint from its ID.
1515
"""
1616

17-
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.")
21+
22+
return endpoint.roles

src/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
EndpointSchema,
77
EndpointParameter,
88
EndpointResponse,
9+
RoleRequirement,
910
HttpMethod,
1011
get_endpoint_index,
1112
reload_endpoint_index,
@@ -17,6 +18,7 @@
1718
"EndpointSchema",
1819
"EndpointParameter",
1920
"EndpointResponse",
21+
"RoleRequirement",
2022
"HttpMethod",
2123
"get_endpoint_index",
2224
"reload_endpoint_index",

src/tools/endpoint_index.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,45 @@ class SecurityRequirement(BaseModel):
105105
}
106106

107107

108+
class RoleRequirement(BaseModel):
109+
"""
110+
OBP Role requirement for an endpoint.
111+
112+
Represents a required role/entitlement to access the endpoint.
113+
"""
114+
role: str = Field(..., description="The role/entitlement name")
115+
requires_bank_id: bool = Field(default=False, description="Whether this role requires a bank_id context")
116+
117+
model_config = {
118+
"extra": "ignore",
119+
}
120+
121+
108122
class EndpointSchema(BaseModel):
109123
"""
110-
Full OpenAPI schema for an endpoint.
124+
Full schema for an endpoint.
111125
112126
Contains complete specification including parameters, request body,
113-
responses, and security requirements.
127+
responses, security requirements, and roles from OBP resource docs.
114128
"""
115129
path: str = Field(..., description="API path template")
116130
method: HttpMethod = Field(..., description="HTTP method")
117131
operation_id: str = Field(default="", description="OpenAPI operationId")
118132
summary: str = Field(default="", description="Brief description")
119133
description: str = Field(default="", description="Detailed description (may contain HTML)")
134+
description_markdown: str = Field(default="", description="Description in markdown format")
120135
tags: List[str] = Field(default_factory=list, description="Tags for categorization")
121136
parameters: List[EndpointParameter] = Field(default_factory=list, description="Endpoint parameters")
122137
requestBody: Optional[Dict[str, Any]] = Field(default=None, description="Request body specification")
123138
responses: Dict[str, EndpointResponse] = Field(default_factory=dict, description="Response specifications by status code")
139+
success_response_body: Optional[Dict[str, Any]] = Field(default=None, description="Example success response body")
140+
error_response_bodies: List[str] = Field(default_factory=list, description="Possible error response messages")
141+
typed_success_response_body: Optional[Dict[str, Any]] = Field(default=None, description="JSON Schema for success response")
124142
security: List[Dict[str, Any]] = Field(default_factory=list, description="Security requirements")
125-
roles: List[str] = Field(default_factory=list, description="Required roles")
143+
roles: List[RoleRequirement] = Field(default_factory=list, description="Required roles/entitlements")
144+
is_featured: bool = Field(default=False, description="Whether this endpoint is featured")
145+
special_instructions: str = Field(default="", description="Special instructions for using this endpoint")
146+
connector_methods: List[str] = Field(default_factory=list, description="Related connector methods")
126147

127148
model_config = {
128149
"extra": "ignore",
@@ -150,18 +171,33 @@ def from_raw(cls, data: Dict[str, Any]) -> "EndpointSchema":
150171
if isinstance(param_data, dict):
151172
parameters.append(EndpointParameter(**param_data))
152173

174+
# Convert roles to RoleRequirement models
175+
roles = []
176+
for role_data in data.get("roles", []):
177+
if isinstance(role_data, dict):
178+
roles.append(RoleRequirement(**role_data))
179+
elif isinstance(role_data, str):
180+
roles.append(RoleRequirement(role=role_data))
181+
153182
return cls(
154183
path=data.get("path", ""),
155184
method=HttpMethod(data.get("method", "GET").upper()),
156185
operation_id=data.get("operation_id", ""),
157186
summary=data.get("summary", ""),
158187
description=data.get("description", ""),
188+
description_markdown=data.get("description_markdown", ""),
159189
tags=data.get("tags", []),
160190
parameters=parameters,
161191
requestBody=data.get("requestBody"),
162192
responses=responses,
193+
success_response_body=data.get("success_response_body"),
194+
error_response_bodies=data.get("error_response_bodies", []),
195+
typed_success_response_body=data.get("typed_success_response_body"),
163196
security=data.get("security", []),
164-
roles=data.get("roles", []),
197+
roles=roles,
198+
is_featured=data.get("is_featured", False),
199+
special_instructions=data.get("special_instructions", ""),
200+
connector_methods=data.get("connector_methods", []),
165201
)
166202

167203

@@ -330,6 +366,88 @@ def build_index_from_swagger(self, swagger_data: Dict[str, Any]) -> None:
330366
self._save_schemas()
331367
self._schemas_loaded = True
332368

369+
def build_index_from_resource_docs(self, resource_docs_data: Dict[str, Any]) -> None:
370+
"""
371+
Build the lightweight endpoint index and full schemas from OBP resource docs.
372+
373+
Resource docs provide richer information than swagger, including:
374+
- Roles/entitlements required for each endpoint
375+
- Markdown descriptions
376+
- Example success/error response bodies
377+
- Typed response schemas
378+
- Connector methods
379+
380+
Args:
381+
resource_docs_data: Resource docs response from OBP API
382+
"""
383+
self._index = {}
384+
self._schemas = {}
385+
self._index_models = {}
386+
self._schema_models = {}
387+
388+
resource_docs = resource_docs_data.get("resource_docs", [])
389+
390+
logger.info(f"Building index from {len(resource_docs)} resource docs...")
391+
392+
for doc in resource_docs:
393+
if not isinstance(doc, dict):
394+
continue
395+
396+
# Extract fields from resource doc format
397+
operation_id = doc.get("operation_id", "")
398+
method = doc.get("request_verb", "GET").upper()
399+
path = doc.get("request_url", "")
400+
summary = doc.get("summary", "")
401+
tags = doc.get("tags", [])
402+
403+
# Use operation_id as the endpoint ID (fallback to generated ID if not present)
404+
if operation_id:
405+
endpoint_id = operation_id
406+
else:
407+
endpoint_id = self._generate_endpoint_id(method, path)
408+
logger.warning(f"No operation_id for {method} {path}, using generated ID: {endpoint_id}")
409+
410+
# Store lightweight index entry
411+
self._index[endpoint_id] = {
412+
"id": endpoint_id,
413+
"method": method,
414+
"path": path,
415+
"operation_id": operation_id,
416+
"summary": summary,
417+
"tags": tags
418+
}
419+
420+
# Extract roles from resource docs format
421+
roles = doc.get("roles", [])
422+
423+
# Store full schema separately (keyed by endpoint_id)
424+
self._schemas[endpoint_id] = {
425+
"path": path,
426+
"method": method,
427+
"operation_id": operation_id,
428+
"summary": summary,
429+
"description": doc.get("description", ""),
430+
"description_markdown": doc.get("description_markdown", ""),
431+
"tags": tags,
432+
"parameters": [], # Resource docs don't provide parameters in the same way
433+
"requestBody": None,
434+
"responses": {},
435+
"success_response_body": doc.get("success_response_body"),
436+
"error_response_bodies": doc.get("error_response_bodies", []),
437+
"typed_success_response_body": doc.get("typed_success_response_body"),
438+
"security": [],
439+
"roles": roles,
440+
"is_featured": doc.get("is_featured", False),
441+
"special_instructions": doc.get("special_instructions", ""),
442+
"connector_methods": doc.get("connector_methods", []),
443+
}
444+
445+
logger.info(f"Built index with {len(self._index)} endpoints")
446+
logger.info(f"Built schemas for {len(self._schemas)} endpoints")
447+
self._save_index()
448+
self._save_schemas()
449+
self._schemas_loaded = True
450+
333451
def _generate_endpoint_id(self, method: str, path: str) -> str:
334452
"""Generate a unique ID for an endpoint."""
335453
# Clean the path for use in ID

0 commit comments

Comments
 (0)