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
15 changes: 13 additions & 2 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,14 +834,15 @@
"remote_mcp_server_name": "outer-apis",
"remote_mcp_server": default_mcp_url,
"status": True,
"authorization_token": None
"authorization_token": None,
"custom_headers": None
})
remote_mcp_dict = {record["remote_mcp_server_name"]: record for record in remote_mcp_list if record["status"]}

# Filter MCP servers and tools, and build mcp_host with authorization
used_mcp_urls = filter_mcp_servers_and_tools(agent_config, remote_mcp_dict)

# Build mcp_host list with authorization tokens
# Build mcp_host list with authorization tokens and custom headers
mcp_host = []
for url in used_mcp_urls:
# Find the MCP record for this URL
Expand All @@ -860,6 +861,16 @@
auth_token = mcp_record.get("authorization_token")
if auth_token:
mcp_config["authorization"] = auth_token
# Add custom headers if present
custom_headers = mcp_record.get("custom_headers")
if custom_headers:
try:
import json
headers_dict = json.loads(custom_headers)
if isinstance(headers_dict, dict):
mcp_config["headers"] = headers_dict
except (json.JSONDecodeError, ValueError):

Check warning on line 872 in backend/agents/create_agent_info.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant Exception class; it derives from another which is already caught.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ5il3gkb9sO6VmId6sj&open=AZ5il3gkb9sO6VmId6sj&pullRequest=3014
logger.warning(f"Failed to parse custom_headers for {url}, ignoring")
mcp_host.append(mcp_config)
else:
# Fallback to string format if record not found
Expand Down
6 changes: 5 additions & 1 deletion backend/apps/remote_mcp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
service_name: str,
authorization_token: Optional[str] = Query(
None, description="Authorization token for MCP server authentication (e.g., Bearer token)"),
custom_headers: Optional[str] = Query(
None, description="Custom headers in JSON format for MCP server requests"),

Check warning on line 70 in backend/apps/remote_mcp_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=AZ5il3VXb9sO6VmId6sd&open=AZ5il3VXb9sO6VmId6sd&pullRequest=3014
tenant_id: Optional[str] = Query(
None, description="Tenant ID for filtering (uses auth if not provided)"),
authorization: Optional[str] = Header(None),
Expand All @@ -82,7 +84,8 @@
remote_mcp_server=mcp_url,
remote_mcp_server_name=service_name,
container_id=None,
authorization_token=authorization_token)
authorization_token=authorization_token,
custom_headers=custom_headers)
return JSONResponse(
status_code=HTTPStatus.OK,
content={"message": "Successfully added remote MCP proxy",
Expand Down Expand Up @@ -233,6 +236,7 @@
"mcp_name": mcp_record.get("mcp_name"),
"mcp_server": mcp_record.get("mcp_server"),
"authorization_token": mcp_record.get("authorization_token"),
"custom_headers": mcp_record.get("custom_headers"),
"status": "success"
}
)
Expand Down
2 changes: 2 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ class MCPUpdateRequest(BaseModel):
new_mcp_url: str = Field(..., description="New MCP server URL")
new_authorization_token: Optional[str] = Field(
None, description="New authorization token for MCP server authentication (e.g., Bearer token)")
new_custom_headers: Optional[str] = Field(
None, description="Custom headers in JSON format for MCP server requests")


# Tenant Management Data Models
Expand Down
5 changes: 5 additions & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@ class McpRecord(TableBase):
doc="Authorization token for MCP server authentication (e.g., Bearer token)",
default=None,
)
custom_headers = Column(
String(5000),
doc="Custom headers in JSON format for MCP server requests",
default=None,
)


class UserTenant(TableBase):
Expand Down
24 changes: 24 additions & 0 deletions backend/database/remote_mcp_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ def get_mcp_authorization_token_by_name_and_url(mcp_name: str, mcp_server: str,
return mcp_record.authorization_token if mcp_record else None


def get_mcp_custom_headers_by_name_and_url(mcp_name: str, mcp_server: str, tenant_id: str) -> str | None:
"""
Get MCP custom headers by name, URL and tenant ID

:param mcp_name: MCP name
:param mcp_server: MCP server URL
:param tenant_id: Tenant ID
:return: Custom headers JSON string, None if not found
"""
with get_db_session() as session:
mcp_record = session.query(McpRecord).filter(
McpRecord.mcp_name == mcp_name,
McpRecord.mcp_server == mcp_server,
McpRecord.tenant_id == tenant_id,
McpRecord.delete_flag != 'Y'
).first()

return mcp_record.custom_headers if mcp_record else None


def update_mcp_record_by_name_and_url(
update_data,
tenant_id: str,
Expand Down Expand Up @@ -161,6 +181,10 @@ def update_mcp_record_by_name_and_url(
if hasattr(update_data, 'new_authorization_token'):
update_fields["authorization_token"] = update_data.new_authorization_token

# Update custom_headers if provided
if hasattr(update_data, 'new_custom_headers'):
update_fields["custom_headers"] = update_data.new_custom_headers

with get_db_session() as session:
session.query(McpRecord).filter(
McpRecord.mcp_name == update_data.current_service_name,
Expand Down
74 changes: 67 additions & 7 deletions backend/services/remote_mcp_service.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
import os
import tempfile
import httpx

from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport, SSETransport
from httpx import AsyncClient

from consts.const import CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ
from consts.exceptions import MCPConnectionError, MCPNameIllegal
Expand All @@ -16,35 +18,58 @@
update_mcp_status_by_name_and_url,
update_mcp_record_by_name_and_url,
get_mcp_authorization_token_by_name_and_url,
get_mcp_custom_headers_by_name_and_url,
get_mcp_record_by_id_and_tenant,
)
from database.user_tenant_db import get_user_tenant_by_user_id
from services.mcp_container_service import MCPContainerManager

logger = logging.getLogger("remote_mcp_service")

def create_httpx_client(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> AsyncClient:
return AsyncClient(
headers=headers,
timeout=timeout,
auth=auth,
trust_env=False,
Comment on lines +34 to +38
verify=False,

Check failure on line 39 in backend/services/remote_mcp_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Enable server certificate validation on this SSL/TLS connection.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ5KTL-zCXIsTxTT8WFc&open=AZ5KTL-zCXIsTxTT8WFc&pullRequest=3014
)

async def mcp_server_health(remote_mcp_server: str, authorization_token: str | None = None) -> bool:
async def mcp_server_health(
remote_mcp_server: str,
authorization_token: str | None = None,
custom_headers: dict[str, str] | None = None
) -> bool:
try:
# Select transport based on URL ending
url_stripped = remote_mcp_server.strip()
headers = {"Authorization": authorization_token} if authorization_token else {}
# Merge custom headers
if custom_headers:
headers.update(custom_headers)

if url_stripped.endswith("/sse"):
transport = SSETransport(
url=url_stripped,
headers=headers
headers=headers,
httpx_client_factory=create_httpx_client
)
elif url_stripped.endswith("/mcp"):
transport = StreamableHttpTransport(
url=url_stripped,
headers=headers
headers=headers,
httpx_client_factory=create_httpx_client
)
else:
# Default to StreamableHttpTransport for unrecognized formats
transport = StreamableHttpTransport(
url=url_stripped,
headers=headers
headers=headers,
httpx_client_factory=create_httpx_client
)

client = Client(transport=transport)
Expand All @@ -65,16 +90,29 @@
remote_mcp_server_name: str,
container_id: str | None = None,
authorization_token: str | None = None,
custom_headers: str | None = None,
):

# Parse custom headers if provided
parsed_custom_headers = None
if custom_headers:
try:
import json
parsed_custom_headers = json.loads(custom_headers)
if not isinstance(parsed_custom_headers, dict):
logger.warning("custom_headers is not a valid JSON object, ignoring")
parsed_custom_headers = None
except (json.JSONDecodeError, ValueError) as e:

Check warning on line 105 in backend/services/remote_mcp_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant Exception class; it derives from another which is already caught.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ5il3eob9sO6VmId6se&open=AZ5il3eob9sO6VmId6se&pullRequest=3014
logger.warning(f"Failed to parse custom_headers: {e}, ignoring")

# check if MCP name already exists
if check_mcp_name_exists(mcp_name=remote_mcp_server_name, tenant_id=tenant_id):
logger.error(
f"MCP name already exists, tenant_id: {tenant_id}, remote_mcp_server_name: {remote_mcp_server_name}")
raise MCPNameIllegal("MCP name already exists")

# check if the address is available
if not await mcp_server_health(remote_mcp_server=remote_mcp_server, authorization_token=authorization_token):
if not await mcp_server_health(remote_mcp_server=remote_mcp_server, authorization_token=authorization_token, custom_headers=parsed_custom_headers):
raise MCPConnectionError("MCP connection failed")

# update the PG database record
Expand All @@ -84,6 +122,7 @@
"status": True,
"container_id": container_id,
"authorization_token": authorization_token,
"custom_headers": custom_headers,
}
create_mcp_record(mcp_data=insert_mcp_data,
tenant_id=tenant_id, user_id=user_id)
Expand Down Expand Up @@ -182,6 +221,7 @@
}
if is_need_auth:
record_dict["authorization_token"] = record.get("authorization_token")
record_dict["custom_headers"] = record.get("custom_headers")
mcp_records_list.append(record_dict)
return mcp_records_list

Expand Down Expand Up @@ -245,11 +285,30 @@
tenant_id=tenant_id
)

# Get custom headers from database
custom_headers_json = get_mcp_custom_headers_by_name_and_url(
mcp_name=service_name,
mcp_server=mcp_url,
tenant_id=tenant_id
)

# Parse custom headers
parsed_custom_headers = None
if custom_headers_json:
try:
import json
parsed_custom_headers = json.loads(custom_headers_json)
if not isinstance(parsed_custom_headers, dict):
parsed_custom_headers = None
except (json.JSONDecodeError, ValueError):

Check warning on line 303 in backend/services/remote_mcp_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant Exception class; it derives from another which is already caught.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ5il3eob9sO6VmId6sf&open=AZ5il3eob9sO6VmId6sf&pullRequest=3014
parsed_custom_headers = None

# check the health of the MCP server
try:
status = await mcp_server_health(
remote_mcp_server=mcp_url,
authorization_token=authorization_token
authorization_token=authorization_token,
custom_headers=parsed_custom_headers
)
except BaseException:
status = False
Expand Down Expand Up @@ -287,7 +346,7 @@
tenant_id: Tenant ID

Returns:
Dictionary containing mcp_name, mcp_server, and authorization_token, or None if not found
Dictionary containing mcp_name, mcp_server, authorization_token, and custom_headers, or None if not found
"""
mcp_record = get_mcp_record_by_id_and_tenant(mcp_id=mcp_id, tenant_id=tenant_id)
if not mcp_record:
Expand All @@ -297,6 +356,7 @@
"mcp_name": mcp_record.get("mcp_name"),
"mcp_server": mcp_record.get("mcp_server"),
"authorization_token": mcp_record.get("authorization_token"),
"custom_headers": mcp_record.get("custom_headers"),
}


Expand Down
Loading