Skip to content

Commit 3d258c5

Browse files
jonpspriclaude
andcommitted
Refactor JSON functions from within config.py
Signed-off-by: Jonathan Springer <[email protected]> Additional linting improvements Signed-off-by: Jonathan Springer <[email protected]> Lots of cleanup of secrets in code Signed-off-by: Jonathan Springer <[email protected]> fix: Correct Flake8 docstring errors in config.py - Remove type annotations from parameter docstrings (DAR103) - Add missing Raises sections for ValueError exceptions (DAR401) - Fix return type descriptions to match TypedDict types (DAR203) Resolves all DAR (docstring argument/return) validation errors. Co-Authored-By: Claude <[email protected]> Signed-off-by: Jonathan Springer <[email protected]>
1 parent 5ef6438 commit 3d258c5

File tree

16 files changed

+394
-333
lines changed

16 files changed

+394
-333
lines changed

mcpgateway/bootstrap_db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def bootstrap_admin_user() -> None:
7878
logger.info(f"Creating platform admin user: {settings.platform_admin_email}")
7979
admin_user = await auth_service.create_user(
8080
email=settings.platform_admin_email,
81-
password=settings.platform_admin_password,
81+
password=settings.platform_admin_password.get_secret_value(),
8282
full_name=settings.platform_admin_full_name,
8383
is_admin=True,
8484
)

mcpgateway/config.py

Lines changed: 108 additions & 194 deletions
Large diffs are not rendered by default.

mcpgateway/main.py

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
4646
from fastapi.staticfiles import StaticFiles
4747
from fastapi.templating import Jinja2Templates
48+
from jsonpath_ng.ext import parse
49+
from jsonpath_ng.jsonpath import JSONPath
4850
from pydantic import ValidationError
4951
from sqlalchemy import text
5052
from sqlalchemy.exc import IntegrityError
@@ -61,7 +63,7 @@
6163
from mcpgateway.auth import get_current_user
6264
from mcpgateway.bootstrap_db import main as bootstrap_db
6365
from mcpgateway.cache import ResourceCache, SessionRegistry
64-
from mcpgateway.config import jsonpath_modifier, settings
66+
from mcpgateway.config import settings
6567
from mcpgateway.db import refresh_slugs_on_startup, SessionLocal
6668
from mcpgateway.db import Tool as DbTool
6769
from mcpgateway.handlers.sampling import SamplingHandler
@@ -102,9 +104,8 @@
102104
from mcpgateway.services.completion_service import CompletionService
103105
from mcpgateway.services.export_service import ExportError, ExportService
104106
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayError, GatewayNameConflictError, GatewayNotFoundError, GatewayService, GatewayUrlConflictError
105-
from mcpgateway.services.import_service import ConflictStrategy, ImportConflictError
107+
from mcpgateway.services.import_service import ConflictStrategy, ImportConflictError, ImportService, ImportValidationError
106108
from mcpgateway.services.import_service import ImportError as ImportServiceError
107-
from mcpgateway.services.import_service import ImportService, ImportValidationError
108109
from mcpgateway.services.logging_service import LoggingService
109110
from mcpgateway.services.prompt_service import PromptError, PromptNameConflictError, PromptNotFoundError, PromptService
110111
from mcpgateway.services.resource_service import ResourceError, ResourceNotFoundError, ResourceService, ResourceURIConflictError
@@ -264,6 +265,75 @@ def get_user_email(user):
264265
resource_cache = ResourceCache(max_size=settings.resource_cache_size, ttl=settings.resource_cache_ttl)
265266

266267

268+
def jsonpath_modifier(data: Any, jsonpath: str = "$[*]", mappings: Optional[Dict[str, str]] = None) -> Union[List, Dict]:
269+
"""
270+
Applies the given JSONPath expression and mappings to the data.
271+
Only return data that is required by the user dynamically.
272+
273+
Args:
274+
data: The JSON data to query.
275+
jsonpath: The JSONPath expression to apply.
276+
mappings: Optional dictionary of mappings where keys are new field names
277+
and values are JSONPath expressions.
278+
279+
Returns:
280+
Union[List, Dict]: A list (or mapped list) or a Dict of extracted data.
281+
282+
Raises:
283+
HTTPException: If there's an error parsing or executing the JSONPath expressions.
284+
285+
Examples:
286+
>>> jsonpath_modifier({'a': 1, 'b': 2}, '$.a')
287+
[1]
288+
>>> jsonpath_modifier([{'a': 1}, {'a': 2}], '$[*].a')
289+
[1, 2]
290+
>>> jsonpath_modifier({'a': {'b': 2}}, '$.a.b')
291+
[2]
292+
>>> jsonpath_modifier({'a': 1}, '$.b')
293+
[]
294+
"""
295+
if not jsonpath:
296+
jsonpath = "$[*]"
297+
298+
try:
299+
main_expr: JSONPath = parse(jsonpath)
300+
except Exception as e:
301+
raise HTTPException(status_code=400, detail=f"Invalid main JSONPath expression: {e}")
302+
303+
try:
304+
main_matches = main_expr.find(data)
305+
except Exception as e:
306+
raise HTTPException(status_code=400, detail=f"Error executing main JSONPath: {e}")
307+
308+
results = [match.value for match in main_matches]
309+
310+
if mappings:
311+
mapped_results = []
312+
for item in results:
313+
mapped_item = {}
314+
for new_key, mapping_expr_str in mappings.items():
315+
try:
316+
mapping_expr = parse(mapping_expr_str)
317+
except Exception as e:
318+
raise HTTPException(status_code=400, detail=f"Invalid mapping JSONPath for key '{new_key}': {e}")
319+
try:
320+
mapping_matches = mapping_expr.find(item)
321+
except Exception as e:
322+
raise HTTPException(status_code=400, detail=f"Error executing mapping JSONPath for key '{new_key}': {e}")
323+
if not mapping_matches:
324+
mapped_item[new_key] = None
325+
elif len(mapping_matches) == 1:
326+
mapped_item[new_key] = mapping_matches[0].value
327+
else:
328+
mapped_item[new_key] = [m.value for m in mapping_matches]
329+
mapped_results.append(mapped_item)
330+
results = mapped_results
331+
332+
if len(results) == 1 and isinstance(results[0], dict):
333+
return results[0]
334+
return results
335+
336+
267337
####################
268338
# Startup/Shutdown #
269339
####################
@@ -432,7 +502,7 @@ async def validate_security_configuration():
432502
if settings.jwt_secret_key == "my-test-key" and not settings.dev_mode: # nosec B105 - checking for default value
433503
critical_issues.append("Using default JWT secret in non-dev mode. Set JWT_SECRET_KEY environment variable!")
434504

435-
if settings.basic_auth_password == "changeme" and settings.mcpgateway_ui_enabled: # nosec B105 - checking for default value
505+
if settings.basic_auth_password.get_secret_value() == "changeme" and settings.mcpgateway_ui_enabled: # nosec B105 - checking for default value
436506
critical_issues.append("Admin UI enabled with default password. Set BASIC_AUTH_PASSWORD environment variable!")
437507

438508
if not settings.auth_required and settings.federation_enabled and not settings.dev_mode:
@@ -469,7 +539,7 @@ async def validate_security_configuration():
469539
logger.info(" • Generate a strong JWT secret:")
470540
logger.info(" python3 -c 'import secrets; print(secrets.token_urlsafe(32))'")
471541

472-
if settings.basic_auth_password == "changeme": # nosec B105 - checking for default value
542+
if settings.basic_auth_password.get_secret_value() == "changeme": # nosec B105 - checking for default value
473543
logger.info(" • Set a strong admin password in BASIC_AUTH_PASSWORD")
474544

475545
if not settings.auth_required:
@@ -1011,9 +1081,10 @@ def require_api_key(api_key: str) -> None:
10111081
10121082
Examples:
10131083
>>> from mcpgateway.config import settings
1084+
>>> from pydantic import SecretStr
10141085
>>> settings.auth_required = True
10151086
>>> settings.basic_auth_user = "admin"
1016-
>>> settings.basic_auth_password = "secret"
1087+
>>> settings.basic_auth_password = SecretStr("secret")
10171088
>>>
10181089
>>> # Valid API key
10191090
>>> require_api_key("admin:secret") # Should not raise
@@ -1026,7 +1097,7 @@ def require_api_key(api_key: str) -> None:
10261097
401
10271098
"""
10281099
if settings.auth_required:
1029-
expected = f"{settings.basic_auth_user}:{settings.basic_auth_password}"
1100+
expected = f"{settings.basic_auth_user}:{settings.basic_auth_password.get_secret_value()}"
10301101
if api_key != expected:
10311102
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
10321103

@@ -2623,8 +2694,10 @@ async def read_resource(resource_id: str, request: Request, db: Session = Depend
26232694
# Ensure a plain JSON-serializable structure
26242695
try:
26252696
# First-Party
2626-
from mcpgateway.models import ResourceContent # pylint: disable=import-outside-toplevel
2627-
from mcpgateway.models import TextContent # pylint: disable=import-outside-toplevel
2697+
from mcpgateway.models import (
2698+
ResourceContent, # pylint: disable=import-outside-toplevel
2699+
TextContent, # pylint: disable=import-outside-toplevel
2700+
)
26282701

26292702
# If already a ResourceContent, serialize directly
26302703
if isinstance(content, ResourceContent):

mcpgateway/scripts/validate_env.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ def get_security_warnings(settings: Settings) -> list[str]:
5353
warnings.append(f"PORT: Out of allowed range (1-65535). Got: {settings.port}")
5454

5555
# --- PLATFORM_ADMIN_PASSWORD ---
56-
pw = settings.platform_admin_password
56+
pw = settings.platform_admin_password.get_secret_value() if isinstance(settings.platform_admin_password, SecretStr) else settings.platform_admin_password
57+
5758
if not pw or pw.lower() in ("changeme", "admin", "password"):
5859
warnings.append("Default admin password detected! Please change PLATFORM_ADMIN_PASSWORD immediately.")
5960
min_length = settings.password_min_length
@@ -64,7 +65,7 @@ def get_security_warnings(settings: Settings) -> list[str]:
6465
warnings.append("Admin password has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters")
6566

6667
# --- BASIC_AUTH_PASSWORD ---
67-
basic_pw = settings.basic_auth_password
68+
basic_pw = settings.basic_auth_password.get_secret_value() if isinstance(settings.basic_auth_password, SecretStr) else settings.basic_auth_password
6869
if not basic_pw or basic_pw.lower() in ("changeme", "password"):
6970
warnings.append("Default BASIC_AUTH_PASSWORD detected! Please change it immediately.")
7071
min_length = settings.password_min_length

mcpgateway/services/tool_service.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
# Third-Party
3030
import httpx
31+
import jq
3132
from mcp import ClientSession
3233
from mcp.client.sse import sse_client
3334
from mcp.client.streamable_http import streamablehttp_client
@@ -62,14 +63,61 @@
6263
from mcpgateway.utils.services_auth import decode_auth
6364
from mcpgateway.utils.sqlalchemy_modifier import json_contains_expr
6465

65-
# Local
66-
from ..config import extract_using_jq
67-
6866
# Initialize logging service first
6967
logging_service = LoggingService()
7068
logger = logging_service.get_logger(__name__)
7169

7270

71+
def extract_using_jq(data, jq_filter=""):
72+
"""
73+
Extracts data from a given input (string, dict, or list) using a jq filter string.
74+
75+
Args:
76+
data (str, dict, list): The input JSON data. Can be a string, dict, or list.
77+
jq_filter (str): The jq filter string to extract the desired data.
78+
79+
Returns:
80+
The result of applying the jq filter to the input data.
81+
82+
Examples:
83+
>>> extract_using_jq('{"a": 1, "b": 2}', '.a')
84+
[1]
85+
>>> extract_using_jq({'a': 1, 'b': 2}, '.b')
86+
[2]
87+
>>> extract_using_jq('[{"a": 1}, {"a": 2}]', '.[].a')
88+
[1, 2]
89+
>>> extract_using_jq('not a json', '.a')
90+
['Invalid JSON string provided.']
91+
>>> extract_using_jq({'a': 1}, '')
92+
{'a': 1}
93+
"""
94+
if jq_filter == "":
95+
return data
96+
if isinstance(data, str):
97+
# If the input is a string, parse it as JSON
98+
try:
99+
data = json.loads(data)
100+
except json.JSONDecodeError:
101+
return ["Invalid JSON string provided."]
102+
103+
elif not isinstance(data, (dict, list)):
104+
# If the input is not a string, dict, or list, raise an error
105+
return ["Input data must be a JSON string, dictionary, or list."]
106+
107+
# Apply the jq filter to the data
108+
try:
109+
# Pylint can't introspect C-extension modules, so it doesn't know that jq really does export an all() function.
110+
# pylint: disable=c-extension-no-member
111+
result = jq.all(jq_filter, data) # Use `jq.all` to get all matches (returns a list)
112+
if result == [None]:
113+
result = "Error applying jsonpath filter"
114+
except Exception as e:
115+
message = "Error applying jsonpath filter: " + str(e)
116+
return message
117+
118+
return result
119+
120+
73121
class ToolError(Exception):
74122
"""Base class for tool-related errors.
75123

mcpgateway/utils/sso_bootstrap.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def get_predefined_sso_providers() -> List[Dict]:
117117
"display_name": "GitHub",
118118
"provider_type": "oauth2",
119119
"client_id": settings.sso_github_client_id,
120-
"client_secret": settings.sso_github_client_secret or "",
120+
"client_secret": settings.sso_github_client_secret.get_secret_value() if settings.sso_github_client_secret else "",
121121
"authorization_url": "https://github.com/login/oauth/authorize",
122122
"token_url": "https://github.com/login/oauth/access_token",
123123
"userinfo_url": "https://api.github.com/user",
@@ -137,7 +137,7 @@ def get_predefined_sso_providers() -> List[Dict]:
137137
"display_name": "Google",
138138
"provider_type": "oidc",
139139
"client_id": settings.sso_google_client_id,
140-
"client_secret": settings.sso_google_client_secret or "",
140+
"client_secret": settings.sso_google_client_secret.get_secret_value() if settings.sso_google_client_secret else "",
141141
"authorization_url": "https://accounts.google.com/o/oauth2/auth",
142142
"token_url": "https://oauth2.googleapis.com/token",
143143
"userinfo_url": "https://openidconnect.googleapis.com/v1/userinfo",
@@ -159,7 +159,7 @@ def get_predefined_sso_providers() -> List[Dict]:
159159
"display_name": "IBM Security Verify",
160160
"provider_type": "oidc",
161161
"client_id": settings.sso_ibm_verify_client_id,
162-
"client_secret": settings.sso_ibm_verify_client_secret or "",
162+
"client_secret": settings.sso_ibm_verify_client_secret.get_secret_value() if settings.sso_ibm_verify_client_secret else "",
163163
"authorization_url": f"{base_url}/oidc/endpoint/default/authorize",
164164
"token_url": f"{base_url}/oidc/endpoint/default/token",
165165
"userinfo_url": f"{base_url}/oidc/endpoint/default/userinfo",
@@ -181,7 +181,7 @@ def get_predefined_sso_providers() -> List[Dict]:
181181
"display_name": "Okta",
182182
"provider_type": "oidc",
183183
"client_id": settings.sso_okta_client_id,
184-
"client_secret": settings.sso_okta_client_secret or "",
184+
"client_secret": settings.sso_okta_client_secret.get_secret_value() if settings.sso_okta_client_secret else "",
185185
"authorization_url": f"{base_url}/oauth2/default/v1/authorize",
186186
"token_url": f"{base_url}/oauth2/default/v1/token",
187187
"userinfo_url": f"{base_url}/oauth2/default/v1/userinfo",
@@ -204,7 +204,7 @@ def get_predefined_sso_providers() -> List[Dict]:
204204
"display_name": "Microsoft Entra ID",
205205
"provider_type": "oidc",
206206
"client_id": settings.sso_entra_client_id,
207-
"client_secret": settings.sso_entra_client_secret or "",
207+
"client_secret": settings.sso_entra_client_secret.get_secret_value() if settings.sso_entra_client_secret else "",
208208
"authorization_url": f"{base_url}/oauth2/v2.0/authorize",
209209
"token_url": f"{base_url}/oauth2/v2.0/token",
210210
"userinfo_url": "https://graph.microsoft.com/oidc/userinfo",
@@ -232,7 +232,7 @@ def get_predefined_sso_providers() -> List[Dict]:
232232
"display_name": f"Keycloak ({settings.sso_keycloak_realm})",
233233
"provider_type": "oidc",
234234
"client_id": settings.sso_keycloak_client_id,
235-
"client_secret": settings.sso_keycloak_client_secret or "",
235+
"client_secret": settings.sso_keycloak_client_secret.get_secret_value() if settings.sso_keycloak_client_secret else "",
236236
"authorization_url": endpoints["authorization_url"],
237237
"token_url": endpoints["token_url"],
238238
"userinfo_url": endpoints["userinfo_url"],
@@ -270,7 +270,7 @@ def get_predefined_sso_providers() -> List[Dict]:
270270
"display_name": display_name,
271271
"provider_type": "oidc",
272272
"client_id": settings.sso_generic_client_id,
273-
"client_secret": settings.sso_generic_client_secret or "",
273+
"client_secret": settings.sso_generic_client_secret.get_secret_value() if settings.sso_generic_client_secret else "",
274274
"authorization_url": settings.sso_generic_authorization_url,
275275
"token_url": settings.sso_generic_token_url,
276276
"userinfo_url": settings.sso_generic_userinfo_url,

0 commit comments

Comments
 (0)