Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$",
"lines": null
},
"generated_at": "2026-06-26T14:48:00Z",
"generated_at": "2026-06-26T15:26:45Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -274,63 +274,63 @@
"hashed_secret": "b4673e578b9b30fe8bba1b555b7b59883444c697",
"is_secret": false,
"is_verified": false,
"line_number": 1590,
"line_number": 1594,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "4a0a2df96d4c9a13a282268cab33ac4b8cbb2c72",
"is_secret": false,
"is_verified": false,
"line_number": 1678,
"line_number": 1682,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_secret": false,
"is_verified": false,
"line_number": 2028,
"line_number": 2032,
"type": "Basic Auth Credentials",
"verified_result": null
},
{
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
"is_secret": false,
"is_verified": false,
"line_number": 3394,
"line_number": 3398,
"type": "Basic Auth Credentials",
"verified_result": null
},
{
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
"is_secret": false,
"is_verified": false,
"line_number": 3485,
"line_number": 3489,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "ac371b6dcce28a86c90d12bc57d946a800eebf17",
"is_secret": false,
"is_verified": false,
"line_number": 3528,
"line_number": 3532,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "0b6ec68df700dec4dcd64babd0eda1edccddace1",
"is_secret": false,
"is_verified": false,
"line_number": 3533,
"line_number": 3537,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "4ad6f0082ee224001beb3ca5c3e81c8ceea5ed86",
"is_secret": false,
"is_verified": false,
"line_number": 3538,
"line_number": 3542,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down Expand Up @@ -4215,24 +4215,6 @@
"verified_result": null
}
],
"mcpgateway/alembic/versions/e28cd485ad3c_remove_global_unique_constraints_for_.py": [
{
"hashed_secret": "64589d1d5776faad3fc219e30c877e7ab8abb551",
"is_secret": false,
"is_verified": false,
"line_number": 33,
"type": "Hex High Entropy String",
"verified_result": null
},
{
"hashed_secret": "5dbedebc3423bc4336c49a36bd75fe7d370c5176",
"is_secret": false,
"is_verified": false,
"line_number": 34,
"type": "Hex High Entropy String",
"verified_result": null
}
],
"mcpgateway/common/validators.py": [
{
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
Expand Down Expand Up @@ -4274,7 +4256,7 @@
"hashed_secret": "d3ecb0d890368d7659ee54010045b835dacb8efe",
"is_secret": false,
"is_verified": false,
"line_number": 738,
"line_number": 739,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## [Unreleased]

### Added

- New REST API endpoint `POST /v1/tools/generate-schemas-from-openapi` for generating MCP tool schemas from OpenAPI specifications without admin UI dependencies (#5142)

### Deprecation Notice

- Rust MCP runtime sidecar, Rust A2A runtime sidecar, and ValidationMiddleware are deprecated as of 2026-06-11 and will sunset on 2026-07-07. Use the Python MCP transport path, the Python A2A invocation path, and endpoint-level Pydantic or protocol-specific validation instead. See [Deprecations](docs/docs/deprecations.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "e28cd485ad3c"
down_revision: Union[str, Sequence[str], None] = "0a089912b5f0"
revision: str = "e28cd485ad3c" # pragma: allowlist secret
down_revision: Union[str, Sequence[str], None] = "0a089912b5f0" # pragma: allowlist secret
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand Down
2 changes: 2 additions & 0 deletions mcpgateway/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
stop_plugin_invalidation_listener,
)
from mcpgateway.plugins.violation_codes import PLUGIN_VIOLATION_CODE_MAPPING, PluginViolationCode, VALID_HTTP_STATUS_CODES
from mcpgateway.routers.openapi_schema_router import router as openapi_schema_router
from mcpgateway.routers.server_well_known import router as server_well_known_router
from mcpgateway.routers.well_known import router as well_known_router
from mcpgateway.schemas import (
Expand Down Expand Up @@ -12101,6 +12102,7 @@ async def cleanup_import_statuses(max_age_hours: int = 24, user=Depends(get_curr
app.include_router(metrics_router)
app.include_router(tag_router)
app.include_router(export_import_router)
app.include_router(openapi_schema_router)

# Compliance report router (admin API)
if settings.mcpgateway_admin_api_enabled:
Expand Down
1 change: 1 addition & 0 deletions mcpgateway/routers/oauth_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ async def initiate_oauth_flow(gateway_id: str, request: Request, current_user: E
request: The FastAPI request object.
current_user: The authenticated user initiating the OAuth flow.
db: The database session dependency.
popup: Indicates if the OAuth flow is initiated in a popup window.

Returns:
A redirect response to the OAuth provider's authorization URL.
Expand Down
124 changes: 124 additions & 0 deletions mcpgateway/routers/openapi_schema_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/routers/openapi_schema_router.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0

OpenAPI Schema Generation Router.

This module provides a versioned REST API endpoint for generating MCP tool
input/output schemas from OpenAPI specifications. It mirrors the functionality
of the admin endpoint but without CSRF protection, making it suitable for
API consumers and integrations.
"""

# Standard
import logging
import urllib.parse

# Third-Party
from fastapi import APIRouter, Body, Depends
from fastapi.responses import JSONResponse
import httpx
from pydantic import BaseModel, Field

# First-Party
from mcpgateway.common.validators import SecurityValidator
from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission
from mcpgateway.services.openapi_service import fetch_and_extract_schemas
from mcpgateway.utils.orjson_response import ORJSONResponse

# Initialize router
router = APIRouter(prefix="/v1/tools", tags=["Tools"])
logger = logging.getLogger(__name__)


class GenerateSchemaRequest(BaseModel):
"""Request body for generating schemas from OpenAPI specification."""

url: str = Field(..., description="The tool URL (e.g., http://localhost:8100/calculate)")
request_type: str = Field("GET", description="HTTP method (GET, POST, etc.)")
openapi_url: str = Field("", description="Direct OpenAPI spec URL (optional, auto-discovered if empty)")


@router.post("/generate-schemas-from-openapi")
@require_permission("tools.create", allow_admin_bypass=False)
async def generate_schemas_from_openapi(
body: GenerateSchemaRequest = Body(...),
_user=Depends(get_current_user_with_permissions),
) -> JSONResponse:
"""
Generate input_schema and output_schema from OpenAPI specification.

This endpoint is part of the versioned REST API and does not require
admin UI access. It delegates to the same service logic as the admin
endpoint but without CSRF protection.

Args:
body: Request body with url, request_type, and openapi_url
_user: Authenticated user from RBAC dependency

Returns:
JSONResponse with generated schemas or error message.
"""
# Security validation
try:
SecurityValidator.validate_url(body.url, "Tool URL")
except ValueError as e:
return ORJSONResponse(
content={"message": str(e), "success": False},
status_code=400,
)

# Parse URL to extract base and path
parsed = urllib.parse.urlparse(body.url)
base_url = f"{parsed.scheme}://{parsed.netloc}"
tool_path = parsed.path

# Fetch and extract schemas
try:
input_schema, output_schema, spec_url = await fetch_and_extract_schemas(
base_url=base_url,
path=tool_path,
method=body.request_type,
openapi_url=body.openapi_url or "",
timeout=10.0,
)
except ValueError as e:
return ORJSONResponse(
content={"message": f"Security validation failed: {str(e)}", "success": False},
status_code=400,
)
except KeyError as e:
return ORJSONResponse(
content={"message": str(e), "success": False},
status_code=404,
)
except httpx.HTTPStatusError as e:
logger.warning("OpenAPI spec server returned HTTP %s", e.response.status_code, exc_info=True)
return ORJSONResponse(
content={"message": f"OpenAPI spec server returned HTTP {e.response.status_code}", "success": False},
status_code=502,
)
except httpx.HTTPError:
logger.warning("Failed to fetch OpenAPI spec", exc_info=True)
return ORJSONResponse(
content={"message": "Failed to fetch OpenAPI spec from the provided URL", "success": False},
status_code=502,
)
except Exception:
logger.error("Error fetching OpenAPI spec", exc_info=True)
return ORJSONResponse(
content={"message": "An unexpected error occurred while processing the OpenAPI spec", "success": False},
status_code=500,
)

return ORJSONResponse(
content={
"message": "Schemas generated successfully from OpenAPI spec",
"success": True,
"input_schema": input_schema,
"output_schema": output_schema,
"spec_url": spec_url,
},
status_code=200,
)
Loading
Loading