Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9bfb2c6
mcp web
menglinghan Jun 22, 2026
3652e8f
mcp web
menglinghan Jun 23, 2026
62dd475
mcp web
menglinghan Jun 23, 2026
ac20629
mcp web
menglinghan Jun 23, 2026
c3c9824
mcp web
menglinghan Jun 24, 2026
9cd1f85
mcp web
menglinghan Jun 25, 2026
64c9e59
delete version
menglinghan Jun 25, 2026
40c70b8
delete version
menglinghan Jun 25, 2026
bcd346a
local mirror
menglinghan Jun 26, 2026
165b0c6
Merge remote-tracking branch 'origin/develop' into mlh/mcp_repository
menglinghan Jun 26, 2026
c52548b
修复工具验证问题以及工具数量显示问题
menglinghan Jun 29, 2026
1c3488d
增加自定义外部市场mcp名称功能;
menglinghan Jun 29, 2026
88d74e0
增加工具数量更新功能
menglinghan Jun 29, 2026
0842309
Merge remote-tracking branch 'origin/develop' into mlh/mcp_repository
menglinghan Jun 29, 2026
5513511
增加工具数量更新功能
menglinghan Jun 29, 2026
791f35a
修复仓库的工具数量显示问题;
menglinghan Jun 30, 2026
87b5cb8
“我的”mcp“申请上架”逻辑问题
menglinghan Jun 30, 2026
1409605
修复“我的”mcp开发者显示问题;
menglinghan Jun 30, 2026
2d7d9d0
修复删除mcp未初始化mcp启用状态的问题
menglinghan Jun 30, 2026
9c2636c
修复市场下载的mcp作者显示问题
menglinghan Jun 30, 2026
d889d4c
外部市场mcp连通性校验
menglinghan Jun 30, 2026
52bcb01
添加MCP服务按钮修改
menglinghan Jun 30, 2026
7363f21
修复仓库下载的mcp不显示hub的问题
menglinghan Jun 30, 2026
536865a
Merge remote-tracking branch 'origin/develop' into mlh/mcp_repository
menglinghan Jul 3, 2026
c48f03b
删除smithery mcp市场
menglinghan Jul 3, 2026
0bc2a0e
merge
menglinghan Jul 6, 2026
b3980c9
修复通过镜像上传mcp不显示工具数量问题
menglinghan Jul 6, 2026
cbcb529
修复mcp来源显示错误
menglinghan Jul 7, 2026
6d1c14c
修复审核中心标签数字显示问题
menglinghan Jul 7, 2026
cbacd3f
mcp市场“仓库”页面和“我的”页面权限调整:仓库页面为租户内共享,我的页面为用户个人使用;
menglinghan Jul 9, 2026
f02cb85
Update settings.local.json
menglinghan Jul 9, 2026
6a7961a
merge
menglinghan Jul 10, 2026
1130ef5
Merge remote-tracking branch 'fork/mlh/mcp_repository' into mlh/mcp_r…
menglinghan Jul 10, 2026
47ed90b
Delete MCP_MARKET_FRONTEND_BACKEND_MAPPING.md
menglinghan Jul 10, 2026
e94c10c
更新测试用例
menglinghan Jul 10, 2026
c0dcd1a
Merge remote-tracking branch 'fork/mlh/mcp_repository' into mlh/mcp_r…
menglinghan Jul 10, 2026
b737dd3
merge
menglinghan Jul 13, 2026
5046c78
fix test case
menglinghan Jul 13, 2026
a85fa1e
Quality Gate
menglinghan Jul 13, 2026
cc752c2
Quality Gate
menglinghan Jul 13, 2026
cd94c9d
test case
menglinghan Jul 13, 2026
b4bc14f
前端修改
menglinghan Jul 13, 2026
2e0d240
审核机制及表设计按照agent仓库修改
menglinghan Jul 13, 2026
7b1024f
审核机制及表设计按照agent仓库修改
menglinghan Jul 13, 2026
097a778
test cases
menglinghan Jul 13, 2026
f4f2b76
test cases
menglinghan Jul 13, 2026
391bace
test cases
menglinghan Jul 13, 2026
e949dae
test cases
menglinghan Jul 14, 2026
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
417 changes: 325 additions & 92 deletions backend/apps/mcp_management_app.py

Large diffs are not rendered by default.

100 changes: 97 additions & 3 deletions backend/apps/remote_mcp_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import json
from typing import Optional
from typing import Annotated, Optional

from fastapi import APIRouter, Header, HTTPException, UploadFile, File, Form, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
Expand All @@ -15,6 +15,7 @@
McpValidationError,
McpNameConflictError,
McpPortConflictError,
UnauthorizedError,
)
from consts.model import (
MCPConfigRequest,
Expand All @@ -24,6 +25,7 @@
EnableMcpServiceRequest,
DisableMcpServiceRequest,
HealthcheckMcpServiceRequest,
TestMcpConnectionRequest,
ListMcpServicesQuery,
)
from services.remote_mcp_service import (
Expand All @@ -35,12 +37,14 @@
attach_mcp_container_permissions,
get_mcp_record_by_id,
list_mcp_service_tools_by_id,
refresh_mcp_service_tool_count,
add_mcp_service,
add_container_mcp_service,
update_mcp_service,
update_mcp_service_enabled,
delete_mcp_service,
check_mcp_service_health,
test_mcp_connection,
check_container_port_conflict,
suggest_container_port,
)
Expand All @@ -51,14 +55,16 @@
router = APIRouter(prefix="/mcp")
logger = logging.getLogger("remote_mcp_app")

_MCP_SERVICE_ID_DESC = Query(..., description="MCP service ID")


# ---------------------------------------------------------------------------
# Tools Endpoint
# ---------------------------------------------------------------------------

@router.get("/tools")
async def get_tools_from_mcp(
mcp_id: int = Query(..., description="MCP service ID"),
mcp_id: int = _MCP_SERVICE_ID_DESC,
authorization: Optional[str] = Header(None),
http_request: Request = None
):
Expand Down Expand Up @@ -96,6 +102,46 @@
)


# ---------------------------------------------------------------------------
# Tool Count Refresh Endpoint
# ---------------------------------------------------------------------------

@router.post("/refresh-tools")
async def refresh_mcp_tools_endpoint(
mcp_id: int = _MCP_SERVICE_ID_DESC,
authorization: Optional[str] = Header(None),
http_request: Request = None
):
"""Connect to the MCP server, fetch tool names, and persist them to the record."""
try:
user_id, tenant_id, _ = get_current_user_info(authorization, http_request)
await refresh_mcp_service_tool_count(
tenant_id=tenant_id,
user_id=user_id,
mcp_id=mcp_id,
)
return JSONResponse(
status_code=HTTPStatus.OK,
content={"message": "Tool count refreshed", "status": "success"}
)
except McpNotFoundError as e:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))
except McpValidationError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except MCPConnectionError as e:
logger.exception(f"Failed to refresh tool count for mcp_id={mcp_id}")
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail="MCP connection failed"
)
except Exception as e:
logger.error(f"Failed to refresh MCP tool count: {e}")

Check failure on line 138 in backend/apps/remote_mcp_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "logging.exception()" instead.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ9G1NWZZ9F0_slUKesY&open=AZ9G1NWZZ9F0_slUKesY&pullRequest=3396
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to refresh MCP tool count"
)


# ---------------------------------------------------------------------------
# Add Endpoints
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -125,6 +171,8 @@
custom_headers=payload.custom_headers,
container_config=payload.container_config,
registry_json=payload.registry_json,
config_json=payload.config_json,
market_id=payload.market_id,
enabled=payload.enabled if payload.enabled is not None else False,
)

Expand Down Expand Up @@ -171,6 +219,8 @@
tags=payload.tags,
authorization_token=payload.authorization_token,
registry_json=payload.registry_json,
version=payload.version,
market_id=payload.market_id,
port=payload.port,
mcp_config=payload.mcp_config,
)
Expand Down Expand Up @@ -241,7 +291,9 @@
server_url=payload.server_url,
authorization_token=payload.authorization_token,
custom_headers=payload.custom_headers,
config_json=payload.config_json,
tags=payload.tags,
market_id=payload.market_id,
)

return JSONResponse(
Expand Down Expand Up @@ -385,6 +437,11 @@
"status": "success"
}
)
except UnauthorizedError as e:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail=str(e)
)
except Exception as e:
logger.error(f"Failed to get MCP list: {e}")
raise HTTPException(
Expand Down Expand Up @@ -553,7 +610,7 @@

@router.get("/healthcheck")
async def check_mcp_health(
mcp_id: int = Query(..., description="MCP service ID"),
mcp_id: int = _MCP_SERVICE_ID_DESC,
authorization: Optional[str] = Header(None),
http_request: Request = None
):
Expand Down Expand Up @@ -589,6 +646,43 @@
)


# ---------------------------------------------------------------------------
# Test Connection Endpoint
# ---------------------------------------------------------------------------


@router.post("/test-connection")
async def test_mcp_connection_endpoint(
payload: TestMcpConnectionRequest,
authorization: Annotated[Optional[str], Header()] = None,
http_request: Request = None
):
"""Lightweight MCP connectivity test. Performs only the initialize handshake.

This is faster than a full health check (which calls list_tools()) and is
intended for pre-install validation in the Quick Add modal.
"""
try:
get_current_user_info(authorization, http_request)

success = await test_mcp_connection(
server_url=payload.server_url,
authorization_token=payload.authorization_token,
custom_headers=payload.custom_headers,
)

return JSONResponse(
status_code=HTTPStatus.OK,
content={"success": success}
)
except Exception as e:
logger.exception("MCP test connection failed")
return JSONResponse(

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
status_code=HTTPStatus.OK,
content={"success": False, "error": str(e) or "Connection failed"}
)


# ---------------------------------------------------------------------------
# Port Management Endpoints
# ---------------------------------------------------------------------------
Expand Down
15 changes: 15 additions & 0 deletions backend/consts/mcp_market.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Domain constants for MCP marketplace listing statuses."""

# Listing status: not_shared (未共享), pending_review (待审核),
# rejected (审核驳回), shared (已共享)
STATUS_NOT_SHARED = "not_shared"
STATUS_PENDING_REVIEW = "pending_review"
STATUS_REJECTED = "rejected"
STATUS_SHARED = "shared"

VALID_MARKET_STATUSES = frozenset({
STATUS_NOT_SHARED,
STATUS_PENDING_REVIEW,
STATUS_REJECTED,
STATUS_SHARED,
})
54 changes: 46 additions & 8 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,9 +1356,12 @@
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")
container_config: Optional[Dict[str, Any]] = Field(None, description="Container configuration")
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
config_json: Optional[Dict[str, Any]] = Field(None, description="MCP configuration JSON (e.g. OpenAPI spec for API-type MCP)")
version: Optional[str] = Field(None, description="MCP version")

Check failure on line 1360 in backend/consts/model.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "MCP version" 3 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ9G1NYKZ9F0_slUKesr&open=AZ9G1NYKZ9F0_slUKesr&pullRequest=3396
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")

Check failure on line 1361 in backend/consts/model.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Linked market record ID" 3 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ9G1NYKZ9F0_slUKesq&open=AZ9G1NYKZ9F0_slUKesq&pullRequest=3396
enabled: Optional[bool] = Field(default=False, description="Whether the MCP is enabled after creation")

@field_validator("name", "server_url", "description", "authorization_token", mode="before")
@field_validator("name", "server_url", "description", "authorization_token", "version", mode="before")
@classmethod
def _strip_text(cls, value: Any):
if isinstance(value, str):
Expand All @@ -1374,10 +1377,12 @@
tags: List[str] = Field(default_factory=list, description="MCP tags")
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
version: Optional[str] = Field(None, description="MCP version")
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")
port: int = Field(..., ge=1, le=65535, description="Host port for the container")
mcp_config: MCPConfigRequest = Field(..., description="MCP server configuration")

@field_validator("name", "description", "authorization_token", mode="before")
@field_validator("name", "description", "authorization_token", "version", mode="before")
@classmethod
def _strip_text(cls, value: Any):
if isinstance(value, str):
Expand All @@ -1394,8 +1399,11 @@
tags: List[str] = Field(default_factory=list, description="MCP tags")
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")
config_json: Optional[Dict[str, Any]] = Field(None, description="MCP configuration JSON")
version: Optional[str] = Field(None, description="MCP version")
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")

@field_validator("name", "server_url", "description", "authorization_token", mode="before")
@field_validator("name", "server_url", "description", "authorization_token", "version", mode="before")
@classmethod
def _strip_text(cls, value: Any):
if isinstance(value, str):
Expand All @@ -1418,6 +1426,13 @@
mcp_id: int = Field(..., gt=0, description="MCP record ID to health check")


class TestMcpConnectionRequest(BaseModel):
"""Request model for testing MCP server connectivity (lightweight handshake)"""
server_url: str = Field(..., min_length=1, description="MCP server URL to test")
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")


class ListMcpToolsRequest(BaseModel):
"""Request model for listing MCP service tools"""
mcp_id: int = Field(..., gt=0, description="MCP record ID")
Expand Down Expand Up @@ -1476,18 +1491,35 @@
return value


class CommunityReviewListRequest(CommunityListRequest):
"""Request model for listing MCP community review submissions"""
status: Optional[str] = Field(None, description="Review status filter")

@field_validator("status", mode="before")
@classmethod
def _strip_status(cls, value: Any):
if isinstance(value, str):
stripped = value.strip()
return stripped or None
return value


class CommunityReviewActionRequest(BaseModel):
"""Request model for approving or rejecting an MCP community submission"""
review_id: int = Field(..., gt=0, description="Review record ID")


class CommunityPublishRequest(BaseModel):
"""Publish a local MCP to the community; optional fields override the snapshot."""

mcp_id: int = Field(..., gt=0, description="MCP record ID to publish")
name: Optional[str] = Field(None, description="Community display name override")
description: Optional[str] = Field(None, description="Description override")
version: Optional[str] = Field(None, description="Version override")
tags: Optional[List[str]] = Field(None, description="Tags override")
mcp_server: Optional[str] = Field(None, max_length=500, description="Remote MCP server URL override (URL / HTTP / SSE transports)")
config_json: Optional[Dict[str, Any]] = Field(None, description="Container MCP configuration JSON override")

@field_validator("name", "description", "version", "mcp_server", mode="before")
@field_validator("name", "description", "mcp_server", mode="before")
@classmethod
def _strip_publish_optional_text(cls, value: Any):
if isinstance(value, str):
Expand All @@ -1498,18 +1530,19 @@

class CommunityUpdateRequest(BaseModel):
"""Request model for updating community MCP service"""
community_id: int = Field(..., gt=0, description="Community record ID")
market_id: int = Field(..., gt=0, description="Market record ID")
name: Optional[str] = Field(default=None, min_length=1, description="New MCP service name")
description: Optional[str] = Field(None, description="MCP service description")
tags: List[str] = Field(default_factory=list, description="MCP tags")
version: Optional[str] = Field(None, description="MCP version")
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
mcp_server: Optional[str] = Field(None, max_length=500, description="MCP server URL")
transport_type: Optional[str] = Field(None, description="Transport type")
config_json: Optional[Dict[str, Any]] = Field(
None,
description="Container MCP configuration JSON (omit to leave unchanged)",
)

@field_validator("name", "description", "version", mode="before")
@field_validator("name", "description", "mcp_server", "transport_type", mode="before")
@classmethod
def _strip_text(cls, value: Any):
if isinstance(value, str):
Expand All @@ -1518,6 +1551,11 @@
return value


class CommunityStatusUpdateRequest(BaseModel):
"""Request model for changing MCP market listing status (PATCH)."""
status: str = Field(..., description="New status: shared / rejected / not_shared / pending_review")


class DeleteMcpServiceRequest(BaseModel):
"""Request model for deleting an MCP service"""
mcp_id: int = Field(..., gt=0, description="MCP record ID to delete")
Loading
Loading