Skip to content

Commit f671053

Browse files
crivetimihairakduttajonpspriclaude
authored andcommitted
feat(api): auto-populate REST tool schemas from OpenAPI specs (#3167)
* input_schema Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * toolupdate change Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * lint Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * test case fix Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * missing testcases Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * missing coverage fix Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * remove Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * docstring Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * edittool Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * review Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * lint Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * add button Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * outshcem fix in admin ui Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * output_schema Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * openai endpoint fix Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * docstring Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * test case fix Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * refactor Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * refactor Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * doctest Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * pylint Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * test coverage Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * pre-commit Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * remove doc file Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * lint fix Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> * fix: harden OpenAPI schema endpoint — SSRF, size limits, dead code removal Security: - Disable HTTP redirects (follow_redirects=False) to prevent SSRF bypass via attacker-controlled 302 redirects to internal addresses - Add 10 MiB response size cap to prevent memory exhaustion from malicious servers - Remove broken frontend buttons that called undefined JS functions - Require 'url' parameter (not just openapi_url) to prevent empty URL parsing producing invalid base_url/path Complexity reduction (-1,900 lines): - Remove unused extract_all_schemas_from_openapi and fetch_and_extract_all_schemas (zero production callers) - Extract _extract_rest_url_components helper to deduplicate URL parsing in ToolCreate and ToolUpdate validators - Remove excessive doctests that duplicated unit test coverage - Collapse phantom tests mocking requests.get (never called by validators) into focused tests of actual validator behaviour - Consolidate admin endpoint error-mapping tests via parametrize - Remove unused _db dependency from endpoint signature - Remove debug logging from ToolCreate validator Closes #2784 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Jonathan Springer <jps@s390x.com> --------- Signed-off-by: Rakhi Dutta <rakhibiswas@yahoo.com> Signed-off-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Rakhi Dutta <rakhibiswas@yahoo.com> Co-authored-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5be0dbc commit f671053

9 files changed

Lines changed: 1265 additions & 66 deletions

File tree

.secrets.baseline

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline|package-lock.json|Cargo.lock|scripts/sign_image.sh|scripts/zap|sonar-project.properties|uv.lock|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-04-13T06:27:05Z",
6+
"generated_at": "2026-04-13T06:40:07Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -5086,31 +5086,31 @@
50865086
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
50875087
"is_secret": false,
50885088
"is_verified": false,
5089-
"line_number": 4239,
5089+
"line_number": 4240,
50905090
"type": "Secret Keyword",
50915091
"verified_result": null
50925092
},
50935093
{
50945094
"hashed_secret": "559b05f1b2863e725b76e216ac3dadecbf92e244",
50955095
"is_secret": false,
50965096
"is_verified": false,
5097-
"line_number": 4840,
5097+
"line_number": 4841,
50985098
"type": "Secret Keyword",
50995099
"verified_result": null
51005100
},
51015101
{
51025102
"hashed_secret": "a8af4759392d4f7496d613174f33afe2074a4b8d",
51035103
"is_secret": false,
51045104
"is_verified": false,
5105-
"line_number": 4842,
5105+
"line_number": 4843,
51065106
"type": "Secret Keyword",
51075107
"verified_result": null
51085108
},
51095109
{
51105110
"hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7",
51115111
"is_secret": false,
51125112
"is_verified": false,
5113-
"line_number": 15050,
5113+
"line_number": 15169,
51145114
"type": "Secret Keyword",
51155115
"verified_result": null
51165116
}
@@ -5962,47 +5962,47 @@
59625962
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
59635963
"is_secret": false,
59645964
"is_verified": false,
5965-
"line_number": 4197,
5965+
"line_number": 4220,
59665966
"type": "Hex High Entropy String",
59675967
"verified_result": null
59685968
},
59695969
{
59705970
"hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee",
59715971
"is_secret": false,
59725972
"is_verified": false,
5973-
"line_number": 5310,
5973+
"line_number": 5333,
59745974
"type": "Secret Keyword",
59755975
"verified_result": null
59765976
},
59775977
{
59785978
"hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511",
59795979
"is_secret": false,
59805980
"is_verified": false,
5981-
"line_number": 5474,
5981+
"line_number": 5497,
59825982
"type": "Secret Keyword",
59835983
"verified_result": null
59845984
},
59855985
{
59865986
"hashed_secret": "f42a3fabe1e9bed059d727f47eb752e3aa61b977",
59875987
"is_secret": false,
59885988
"is_verified": false,
5989-
"line_number": 5531,
5989+
"line_number": 5554,
59905990
"type": "Secret Keyword",
59915991
"verified_result": null
59925992
},
59935993
{
59945994
"hashed_secret": "b85788b459aa4d67e1070930dae6d0827756aadb",
59955995
"is_secret": false,
59965996
"is_verified": false,
5997-
"line_number": 5569,
5997+
"line_number": 5592,
59985998
"type": "Secret Keyword",
59995999
"verified_result": null
60006000
},
60016001
{
60026002
"hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685",
60036003
"is_secret": false,
60046004
"is_verified": false,
6005-
"line_number": 5570,
6005+
"line_number": 5593,
60066006
"type": "Secret Keyword",
60076007
"verified_result": null
60086008
}
@@ -9958,31 +9958,31 @@
99589958
"hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf",
99599959
"is_secret": false,
99609960
"is_verified": false,
9961-
"line_number": 14402,
9961+
"line_number": 14411,
99629962
"type": "Secret Keyword",
99639963
"verified_result": null
99649964
},
99659965
{
99669966
"hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511",
99679967
"is_secret": false,
99689968
"is_verified": false,
9969-
"line_number": 17159,
9969+
"line_number": 17168,
99709970
"type": "Secret Keyword",
99719971
"verified_result": null
99729972
},
99739973
{
99749974
"hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c",
99759975
"is_secret": false,
99769976
"is_verified": false,
9977-
"line_number": 17178,
9977+
"line_number": 17187,
99789978
"type": "Secret Keyword",
99799979
"verified_result": null
99809980
},
99819981
{
99829982
"hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71",
99839983
"is_secret": false,
99849984
"is_verified": false,
9985-
"line_number": 20836,
9985+
"line_number": 20845,
99869986
"type": "Secret Keyword",
99879987
"verified_result": null
99889988
}

mcpgateway/admin.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
from mcpgateway.services.logging_service import LoggingService
137137
from mcpgateway.services.mcp_session_pool import get_mcp_session_pool
138138
from mcpgateway.services.oauth_manager import OAuthManager
139+
from mcpgateway.services.openapi_service import fetch_and_extract_schemas
139140
from mcpgateway.services.performance_service import get_performance_service
140141
from mcpgateway.services.permission_service import PermissionService
141142
from mcpgateway.services.plugin_service import get_plugin_service
@@ -11772,6 +11773,124 @@ async def admin_edit_tool(
1177211773
return ORJSONResponse(content={"message": str(ex), "success": False}, status_code=500)
1177311774

1177411775

11776+
@admin_router.post("/tools/generate-schemas-from-openapi")
11777+
# tools.create — this endpoint makes outbound HTTP requests to user-supplied
11778+
# URLs to fetch OpenAPI specs. tools.read would let viewers probe internal
11779+
# services; tools.create scopes it to users who can already register tools.
11780+
@require_permission("tools.create", allow_admin_bypass=False)
11781+
async def generate_schemas_from_openapi(
11782+
request: Request,
11783+
_user=Depends(get_current_user_with_permissions),
11784+
) -> JSONResponse:
11785+
"""
11786+
Generate input_schema and output_schema from OpenAPI specification URL.
11787+
11788+
Expects JSON body with:
11789+
- url: The tool URL (e.g., http://localhost:8100/calculate)
11790+
- request_type: HTTP method (GET, POST, etc.)
11791+
- openapi_url: (optional) Direct OpenAPI spec URL
11792+
11793+
Args:
11794+
request: FastAPI Request object containing JSON body
11795+
11796+
Returns:
11797+
JSONResponse with generated schemas or error message.
11798+
"""
11799+
try:
11800+
body = await _read_request_json(request)
11801+
except Exception:
11802+
return ORJSONResponse(
11803+
content={"message": "Invalid JSON in request body", "success": False},
11804+
status_code=400,
11805+
)
11806+
11807+
if not isinstance(body, dict):
11808+
return ORJSONResponse(
11809+
content={"message": "Request body must be a JSON object", "success": False},
11810+
status_code=400,
11811+
)
11812+
11813+
tool_url = body.get("url", "")
11814+
request_type = body.get("request_type", "GET")
11815+
openapi_url = body.get("openapi_url", "")
11816+
11817+
if not isinstance(tool_url, str) or not isinstance(request_type, str) or not isinstance(openapi_url, str):
11818+
return ORJSONResponse(
11819+
content={"message": "'url', 'request_type', and 'openapi_url' must be strings", "success": False},
11820+
status_code=400,
11821+
)
11822+
11823+
tool_url = tool_url.strip()
11824+
request_type = request_type.strip()
11825+
openapi_url = openapi_url.strip()
11826+
11827+
if not tool_url:
11828+
return ORJSONResponse(
11829+
content={"message": "'url' is required to identify the API path and base URL", "success": False},
11830+
status_code=400,
11831+
)
11832+
11833+
try:
11834+
SecurityValidator.validate_url(tool_url, "Tool URL")
11835+
except ValueError as e:
11836+
return ORJSONResponse(
11837+
content={"message": str(e), "success": False},
11838+
status_code=400,
11839+
)
11840+
11841+
parsed = urllib.parse.urlparse(tool_url)
11842+
base_url = f"{parsed.scheme}://{parsed.netloc}"
11843+
tool_path = parsed.path
11844+
11845+
try:
11846+
input_schema, output_schema, spec_url = await fetch_and_extract_schemas(
11847+
base_url=base_url,
11848+
path=tool_path,
11849+
method=request_type,
11850+
openapi_url=openapi_url,
11851+
timeout=10.0,
11852+
)
11853+
except ValueError as e:
11854+
return ORJSONResponse(
11855+
content={"message": f"Security validation failed: {str(e)}", "success": False},
11856+
status_code=400,
11857+
)
11858+
except KeyError as e:
11859+
return ORJSONResponse(
11860+
content={"message": str(e), "success": False},
11861+
status_code=404,
11862+
)
11863+
except httpx.HTTPStatusError as e:
11864+
LOGGER.warning("OpenAPI spec server returned HTTP %s", e.response.status_code, exc_info=True)
11865+
return ORJSONResponse(
11866+
content={"message": f"OpenAPI spec server returned HTTP {e.response.status_code}", "success": False},
11867+
status_code=502,
11868+
)
11869+
except httpx.HTTPError:
11870+
LOGGER.warning("Failed to fetch OpenAPI spec", exc_info=True)
11871+
return ORJSONResponse(
11872+
content={"message": "Failed to fetch OpenAPI spec from the provided URL", "success": False},
11873+
status_code=502,
11874+
)
11875+
except Exception:
11876+
LOGGER.error("Error fetching OpenAPI spec", exc_info=True)
11877+
return ORJSONResponse(
11878+
content={"message": "An unexpected error occurred while processing the OpenAPI spec", "success": False},
11879+
status_code=500,
11880+
)
11881+
11882+
return ORJSONResponse(
11883+
content={
11884+
"message": "Schemas generated successfully from OpenAPI spec",
11885+
"success": True,
11886+
"input_schema": input_schema,
11887+
"output_schema": output_schema,
11888+
"spec_url": spec_url,
11889+
},
11890+
status_code=200,
11891+
)
11892+
11893+
1177511894
@admin_router.post("/tools/{tool_id}/delete")
1177611895
@require_permission("tools.delete", allow_admin_bypass=False)
1177711896
async def admin_delete_tool(tool_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> RedirectResponse:

0 commit comments

Comments
 (0)