Skip to content
Open
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
142 changes: 75 additions & 67 deletions backend/apps/tool_config_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@
list_all_tools,
load_last_tool_config_impl,
validate_tool_impl,
import_openapi_json,
list_outer_api_tools,
get_outer_api_tool,
delete_outer_api_tool,
_refresh_outer_api_tools_in_mcp,
import_openapi_service,
list_openapi_services,
delete_openapi_service,
_refresh_openapi_services_in_mcp,
)
from utils.auth_utils import get_current_user_id

Expand Down Expand Up @@ -142,128 +141,137 @@


# --------------------------------------------------
# Outer API Tools (OpenAPI to MCP Conversion)
# OpenAPI Service Management (using from_openapi)
# --------------------------------------------------

@router.post("/import_openapi")
async def import_openapi_api(
openapi_json: Dict[str, Any] = Body(...),
@router.post("/openapi_service")
async def import_openapi_service_api(
openapi_service_request: Dict[str, Any] = Body(...),

Check failure on line 149 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2MAmA-ANpnOc1_fCIC&open=AZ2MAmA-ANpnOc1_fCIC&pullRequest=2778
authorization: Optional[str] = Header(None)
):
"""
Import OpenAPI JSON and convert tools to MCP format.
This will sync tools with the database (update existing, create new, delete removed).
After import, refreshes the MCP server to register new tools.
Import OpenAPI JSON as an MCP service using FastMCP.from_openapi().

All tools from the same OpenAPI spec will be grouped under the same
mcp_service_name. When refreshing, all tools are registered together.

Request Body:
service_name: MCP service name for grouping tools
server_url: Base URL of the REST API server
openapi_json: Complete OpenAPI JSON specification
service_description: Optional service description
force_update: If True, replace all existing tools for this service
"""
service_name = openapi_service_request.get("service_name")
server_url = openapi_service_request.get("server_url")
openapi_json = openapi_service_request.get("openapi_json")
service_description = openapi_service_request.get("service_description")
force_update = openapi_service_request.get("force_update", False)

if not service_name:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="service_name is required"
)
if not server_url:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="server_url is required"
)
if not openapi_json:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="openapi_json is required"
)
try:
user_id, tenant_id = get_current_user_id(authorization)
result = import_openapi_json(openapi_json, tenant_id, user_id)
result = import_openapi_service(
service_name=service_name,
openapi_json=openapi_json,
server_url=server_url,
tenant_id=tenant_id,
user_id=user_id,
service_description=service_description,
force_update=force_update
)

mcp_result = _refresh_outer_api_tools_in_mcp(tenant_id)
mcp_result = _refresh_openapi_services_in_mcp(tenant_id)
result["mcp_refresh"] = mcp_result

return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "OpenAPI import successful",
"message": "OpenAPI service import successful",
"status": "success",
"data": result
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to import OpenAPI: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to import OpenAPI: {str(e)}"
)


@router.get("/outer_api_tools")
async def list_outer_api_tools_api(
authorization: Optional[str] = Header(None)
):
"""
List all outer API tools for the current tenant.
"""
try:
_, tenant_id = get_current_user_id(authorization)
tools = list_outer_api_tools(tenant_id)
return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "success",
"data": tools
}
)
except Exception as e:
logger.error(f"Failed to list outer API tools: {e}")
logger.error(f"Failed to import OpenAPI service: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to list outer API tools: {str(e)}"
detail=f"Failed to import OpenAPI service: {str(e)}"
)


@router.get("/outer_api_tools/{tool_id}")
async def get_outer_api_tool_api(
tool_id: int,
@router.get("/openapi_services")
async def list_openapi_services_api(
authorization: Optional[str] = Header(None)
):
"""
Get a specific outer API tool by ID.
List all OpenAPI services for the current tenant.
"""
try:
_, tenant_id = get_current_user_id(authorization)
tool = get_outer_api_tool(tool_id, tenant_id)
if tool is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Tool not found"
)
services = list_openapi_services(tenant_id)
return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "success",
"data": tool
"data": services
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get outer API tool: {e}")
logger.error(f"Failed to list OpenAPI services: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to get outer API tool: {str(e)}"
detail=f"Failed to list OpenAPI services: {str(e)}"
)


@router.delete("/outer_api_tools/{tool_id}")
async def delete_outer_api_tool_api(
tool_id: int,
@router.delete("/openapi_service/{service_name}")
async def delete_openapi_service_api(
service_name: str,
authorization: Optional[str] = Header(None)
):
"""
Delete an outer API tool.
Delete an OpenAPI service (all tools belonging to it).
"""
try:
user_id, tenant_id = get_current_user_id(authorization)
deleted = delete_outer_api_tool(tool_id, tenant_id, user_id)
deleted = delete_openapi_service(service_name, tenant_id, user_id)
if not deleted:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Tool not found"
detail="Service not found"
)
# Refresh MCP service to reflect the deletion
mcp_result = _refresh_openapi_services_in_mcp(tenant_id)
return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "Tool deleted successfully",
"status": "success"
"message": "Service deleted successfully",
"status": "success",
"mcp_refresh": mcp_result
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete outer API tool: {e}")
logger.error(f"Failed to delete OpenAPI service: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to delete outer API tool: {str(e)}"
detail=f"Failed to delete OpenAPI service: {str(e)}"
)
32 changes: 17 additions & 15 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,25 +578,27 @@ class SkillInstance(TableBase):
version_no = Column(Integer, default=0, primary_key=True, nullable=False, doc="Version number. 0 = draft/editing state, >=1 = published snapshot")


class OuterApiTool(TableBase):
class OuterApiService(TableBase):
"""
Outer API tools table - stores converted OpenAPI tools as MCP tools.
OpenAPI service table - stores MCP service information converted from OpenAPI specs.
Each record represents one MCP service with its OpenAPI specification.
"""
__tablename__ = "ag_outer_api_tools"
__tablename__ = "ag_outer_api_services"
__table_args__ = {"schema": SCHEMA}

id = Column(BigInteger, Sequence("ag_outer_api_tools_id_seq", schema=SCHEMA),
primary_key=True, nullable=False, doc="Tool ID, unique primary key")
name = Column(String(100), nullable=False, doc="Tool name (unique identifier)")
description = Column(Text, doc="Tool description")
method = Column(String(10), doc="HTTP method: GET/POST/PUT/DELETE")
url = Column(Text, nullable=False, doc="API endpoint URL")
headers_template = Column(JSONB, doc="Headers template as JSON")
query_template = Column(JSONB, doc="Query parameters template as JSON")
body_template = Column(JSONB, doc="Request body template as JSON")
input_schema = Column(JSONB, doc="MCP input schema as JSON")
tenant_id = Column(String(100), doc="Tenant ID for multi-tenancy")
is_available = Column(Boolean, default=True, doc="Whether the tool is available")
id = Column(BigInteger, Sequence("ag_outer_api_services_id_seq", schema=SCHEMA),
primary_key=True, nullable=False, doc="Service ID, unique primary key")
mcp_service_name = Column(String(100), nullable=False, doc="MCP service name (unique identifier per tenant)")
description = Column(Text, doc="Service description from OpenAPI info")
openapi_json = Column(JSONB, doc="Complete OpenAPI JSON specification")
server_url = Column(String(500), doc="Base URL of the REST API server")
headers_template = Column(JSONB, doc="Default headers template as JSON")
tenant_id = Column(String(100), nullable=False, doc="Tenant ID for multi-tenancy")
is_available = Column(Boolean, default=True, doc="Whether the service is available")


# Alias for backward compatibility
OuterApiTool = OuterApiService


class A2ANacosConfig(TableBase):
Expand Down
Loading
Loading