Skip to content

Commit 0070b9e

Browse files
authored
✨ Feature: MCP market restructure — Repository, My MCPs, Review Center (#3396)
* mcp web * mcp web * mcp web * mcp web * mcp web * mcp web * delete version * delete version * local mirror * 修复工具验证问题以及工具数量显示问题 * 增加自定义外部市场mcp名称功能; 增加mcp重复命名检查; * 增加工具数量更新功能 * 增加工具数量更新功能 * 修复仓库的工具数量显示问题; 修复mcp删除bug * “我的”mcp“申请上架”逻辑问题 * 修复“我的”mcp开发者显示问题; 修复mcp删除后没有同步到智能体开发mcp列表问题; * 修复删除mcp未初始化mcp启用状态的问题 * 修复市场下载的mcp作者显示问题 * 外部市场mcp连通性校验 * 添加MCP服务按钮修改 * 修复仓库下载的mcp不显示hub的问题 * 删除smithery mcp市场 * 修复通过镜像上传mcp不显示工具数量问题 * 修复mcp来源显示错误 * 修复审核中心标签数字显示问题 * mcp市场“仓库”页面和“我的”页面权限调整:仓库页面为租户内共享,我的页面为用户个人使用; 增加表格迁移; * Update settings.local.json * Delete MCP_MARKET_FRONTEND_BACKEND_MAPPING.md * 更新测试用例 * fix test case * Quality Gate * Quality Gate * test case * 前端修改 * 审核机制及表设计按照agent仓库修改 * 审核机制及表设计按照agent仓库修改 * test cases * test cases * test cases * test cases
1 parent e218281 commit 0070b9e

64 files changed

Lines changed: 9313 additions & 2149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/apps/mcp_management_app.py

Lines changed: 325 additions & 92 deletions
Large diffs are not rendered by default.

backend/apps/remote_mcp_app.py

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import json
3-
from typing import Optional
3+
from typing import Annotated, Optional
44

55
from fastapi import APIRouter, Header, HTTPException, UploadFile, File, Form, Query, Request
66
from fastapi.responses import JSONResponse, StreamingResponse
@@ -15,6 +15,7 @@
1515
McpValidationError,
1616
McpNameConflictError,
1717
McpPortConflictError,
18+
UnauthorizedError,
1819
)
1920
from consts.model import (
2021
MCPConfigRequest,
@@ -24,6 +25,7 @@
2425
EnableMcpServiceRequest,
2526
DisableMcpServiceRequest,
2627
HealthcheckMcpServiceRequest,
28+
TestMcpConnectionRequest,
2729
ListMcpServicesQuery,
2830
)
2931
from services.remote_mcp_service import (
@@ -35,12 +37,14 @@
3537
attach_mcp_container_permissions,
3638
get_mcp_record_by_id,
3739
list_mcp_service_tools_by_id,
40+
refresh_mcp_service_tool_count,
3841
add_mcp_service,
3942
add_container_mcp_service,
4043
update_mcp_service,
4144
update_mcp_service_enabled,
4245
delete_mcp_service,
4346
check_mcp_service_health,
47+
test_mcp_connection,
4448
check_container_port_conflict,
4549
suggest_container_port,
4650
)
@@ -51,14 +55,16 @@
5155
router = APIRouter(prefix="/mcp")
5256
logger = logging.getLogger("remote_mcp_app")
5357

58+
_MCP_SERVICE_ID_DESC = Query(..., description="MCP service ID")
59+
5460

5561
# ---------------------------------------------------------------------------
5662
# Tools Endpoint
5763
# ---------------------------------------------------------------------------
5864

5965
@router.get("/tools")
6066
async def get_tools_from_mcp(
61-
mcp_id: int = Query(..., description="MCP service ID"),
67+
mcp_id: int = _MCP_SERVICE_ID_DESC,
6268
authorization: Optional[str] = Header(None),
6369
http_request: Request = None
6470
):
@@ -96,6 +102,46 @@ async def get_tools_from_mcp(
96102
)
97103

98104

105+
# ---------------------------------------------------------------------------
106+
# Tool Count Refresh Endpoint
107+
# ---------------------------------------------------------------------------
108+
109+
@router.post("/refresh-tools")
110+
async def refresh_mcp_tools_endpoint(
111+
mcp_id: int = _MCP_SERVICE_ID_DESC,
112+
authorization: Optional[str] = Header(None),
113+
http_request: Request = None
114+
):
115+
"""Connect to the MCP server, fetch tool names, and persist them to the record."""
116+
try:
117+
user_id, tenant_id, _ = get_current_user_info(authorization, http_request)
118+
await refresh_mcp_service_tool_count(
119+
tenant_id=tenant_id,
120+
user_id=user_id,
121+
mcp_id=mcp_id,
122+
)
123+
return JSONResponse(
124+
status_code=HTTPStatus.OK,
125+
content={"message": "Tool count refreshed", "status": "success"}
126+
)
127+
except McpNotFoundError as e:
128+
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))
129+
except McpValidationError as e:
130+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
131+
except MCPConnectionError as e:
132+
logger.exception(f"Failed to refresh tool count for mcp_id={mcp_id}")
133+
raise HTTPException(
134+
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
135+
detail="MCP connection failed"
136+
)
137+
except Exception as e:
138+
logger.error(f"Failed to refresh MCP tool count: {e}")
139+
raise HTTPException(
140+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
141+
detail="Failed to refresh MCP tool count"
142+
)
143+
144+
99145
# ---------------------------------------------------------------------------
100146
# Add Endpoints
101147
# ---------------------------------------------------------------------------
@@ -125,6 +171,8 @@ async def add_mcp_service_endpoint(
125171
custom_headers=payload.custom_headers,
126172
container_config=payload.container_config,
127173
registry_json=payload.registry_json,
174+
config_json=payload.config_json,
175+
market_id=payload.market_id,
128176
enabled=payload.enabled if payload.enabled is not None else False,
129177
)
130178

@@ -171,6 +219,8 @@ async def add_container_mcp_service_endpoint(
171219
tags=payload.tags,
172220
authorization_token=payload.authorization_token,
173221
registry_json=payload.registry_json,
222+
version=payload.version,
223+
market_id=payload.market_id,
174224
port=payload.port,
175225
mcp_config=payload.mcp_config,
176226
)
@@ -241,7 +291,9 @@ async def update_mcp_service_endpoint(
241291
server_url=payload.server_url,
242292
authorization_token=payload.authorization_token,
243293
custom_headers=payload.custom_headers,
294+
config_json=payload.config_json,
244295
tags=payload.tags,
296+
market_id=payload.market_id,
245297
)
246298

247299
return JSONResponse(
@@ -385,6 +437,11 @@ async def get_mcp_list(
385437
"status": "success"
386438
}
387439
)
440+
except UnauthorizedError as e:
441+
raise HTTPException(
442+
status_code=HTTPStatus.UNAUTHORIZED,
443+
detail=str(e)
444+
)
388445
except Exception as e:
389446
logger.error(f"Failed to get MCP list: {e}")
390447
raise HTTPException(
@@ -553,7 +610,7 @@ async def generate_log_stream():
553610

554611
@router.get("/healthcheck")
555612
async def check_mcp_health(
556-
mcp_id: int = Query(..., description="MCP service ID"),
613+
mcp_id: int = _MCP_SERVICE_ID_DESC,
557614
authorization: Optional[str] = Header(None),
558615
http_request: Request = None
559616
):
@@ -589,6 +646,43 @@ async def check_mcp_health(
589646
)
590647

591648

649+
# ---------------------------------------------------------------------------
650+
# Test Connection Endpoint
651+
# ---------------------------------------------------------------------------
652+
653+
654+
@router.post("/test-connection")
655+
async def test_mcp_connection_endpoint(
656+
payload: TestMcpConnectionRequest,
657+
authorization: Annotated[Optional[str], Header()] = None,
658+
http_request: Request = None
659+
):
660+
"""Lightweight MCP connectivity test. Performs only the initialize handshake.
661+
662+
This is faster than a full health check (which calls list_tools()) and is
663+
intended for pre-install validation in the Quick Add modal.
664+
"""
665+
try:
666+
get_current_user_info(authorization, http_request)
667+
668+
success = await test_mcp_connection(
669+
server_url=payload.server_url,
670+
authorization_token=payload.authorization_token,
671+
custom_headers=payload.custom_headers,
672+
)
673+
674+
return JSONResponse(
675+
status_code=HTTPStatus.OK,
676+
content={"success": success}
677+
)
678+
except Exception as e:
679+
logger.exception("MCP test connection failed")
680+
return JSONResponse(
681+
status_code=HTTPStatus.OK,
682+
content={"success": False, "error": str(e) or "Connection failed"}
683+
)
684+
685+
592686
# ---------------------------------------------------------------------------
593687
# Port Management Endpoints
594688
# ---------------------------------------------------------------------------

backend/consts/mcp_market.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Domain constants for MCP marketplace listing statuses."""
2+
3+
# Listing status: not_shared (未共享), pending_review (待审核),
4+
# rejected (审核驳回), shared (已共享)
5+
STATUS_NOT_SHARED = "not_shared"
6+
STATUS_PENDING_REVIEW = "pending_review"
7+
STATUS_REJECTED = "rejected"
8+
STATUS_SHARED = "shared"
9+
10+
VALID_MARKET_STATUSES = frozenset({
11+
STATUS_NOT_SHARED,
12+
STATUS_PENDING_REVIEW,
13+
STATUS_REJECTED,
14+
STATUS_SHARED,
15+
})

backend/consts/model.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,9 +1356,12 @@ class AddMcpServiceRequest(BaseModel):
13561356
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")
13571357
container_config: Optional[Dict[str, Any]] = Field(None, description="Container configuration")
13581358
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
1359+
config_json: Optional[Dict[str, Any]] = Field(None, description="MCP configuration JSON (e.g. OpenAPI spec for API-type MCP)")
1360+
version: Optional[str] = Field(None, description="MCP version")
1361+
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")
13591362
enabled: Optional[bool] = Field(default=False, description="Whether the MCP is enabled after creation")
13601363

1361-
@field_validator("name", "server_url", "description", "authorization_token", mode="before")
1364+
@field_validator("name", "server_url", "description", "authorization_token", "version", mode="before")
13621365
@classmethod
13631366
def _strip_text(cls, value: Any):
13641367
if isinstance(value, str):
@@ -1374,10 +1377,12 @@ class AddContainerMcpServiceRequest(BaseModel):
13741377
tags: List[str] = Field(default_factory=list, description="MCP tags")
13751378
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
13761379
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
1380+
version: Optional[str] = Field(None, description="MCP version")
1381+
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")
13771382
port: int = Field(..., ge=1, le=65535, description="Host port for the container")
13781383
mcp_config: MCPConfigRequest = Field(..., description="MCP server configuration")
13791384

1380-
@field_validator("name", "description", "authorization_token", mode="before")
1385+
@field_validator("name", "description", "authorization_token", "version", mode="before")
13811386
@classmethod
13821387
def _strip_text(cls, value: Any):
13831388
if isinstance(value, str):
@@ -1394,8 +1399,11 @@ class UpdateMcpServiceRequest(BaseModel):
13941399
tags: List[str] = Field(default_factory=list, description="MCP tags")
13951400
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
13961401
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")
1402+
config_json: Optional[Dict[str, Any]] = Field(None, description="MCP configuration JSON")
1403+
version: Optional[str] = Field(None, description="MCP version")
1404+
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")
13971405

1398-
@field_validator("name", "server_url", "description", "authorization_token", mode="before")
1406+
@field_validator("name", "server_url", "description", "authorization_token", "version", mode="before")
13991407
@classmethod
14001408
def _strip_text(cls, value: Any):
14011409
if isinstance(value, str):
@@ -1418,6 +1426,13 @@ class HealthcheckMcpServiceRequest(BaseModel):
14181426
mcp_id: int = Field(..., gt=0, description="MCP record ID to health check")
14191427

14201428

1429+
class TestMcpConnectionRequest(BaseModel):
1430+
"""Request model for testing MCP server connectivity (lightweight handshake)"""
1431+
server_url: str = Field(..., min_length=1, description="MCP server URL to test")
1432+
authorization_token: Optional[str] = Field(None, description="Authorization token for MCP server")
1433+
custom_headers: Optional[Dict[str, Any]] = Field(None, description="Custom HTTP headers as JSON object")
1434+
1435+
14211436
class ListMcpToolsRequest(BaseModel):
14221437
"""Request model for listing MCP service tools"""
14231438
mcp_id: int = Field(..., gt=0, description="MCP record ID")
@@ -1476,18 +1491,35 @@ def _strip_text(cls, value: Any):
14761491
return value
14771492

14781493

1494+
class CommunityReviewListRequest(CommunityListRequest):
1495+
"""Request model for listing MCP community review submissions"""
1496+
status: Optional[str] = Field(None, description="Review status filter")
1497+
1498+
@field_validator("status", mode="before")
1499+
@classmethod
1500+
def _strip_status(cls, value: Any):
1501+
if isinstance(value, str):
1502+
stripped = value.strip()
1503+
return stripped or None
1504+
return value
1505+
1506+
1507+
class CommunityReviewActionRequest(BaseModel):
1508+
"""Request model for approving or rejecting an MCP community submission"""
1509+
review_id: int = Field(..., gt=0, description="Review record ID")
1510+
1511+
14791512
class CommunityPublishRequest(BaseModel):
14801513
"""Publish a local MCP to the community; optional fields override the snapshot."""
14811514

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

1490-
@field_validator("name", "description", "version", "mcp_server", mode="before")
1522+
@field_validator("name", "description", "mcp_server", mode="before")
14911523
@classmethod
14921524
def _strip_publish_optional_text(cls, value: Any):
14931525
if isinstance(value, str):
@@ -1498,18 +1530,19 @@ def _strip_publish_optional_text(cls, value: Any):
14981530

14991531
class CommunityUpdateRequest(BaseModel):
15001532
"""Request model for updating community MCP service"""
1501-
community_id: int = Field(..., gt=0, description="Community record ID")
1533+
market_id: int = Field(..., gt=0, description="Market record ID")
15021534
name: Optional[str] = Field(default=None, min_length=1, description="New MCP service name")
15031535
description: Optional[str] = Field(None, description="MCP service description")
15041536
tags: List[str] = Field(default_factory=list, description="MCP tags")
1505-
version: Optional[str] = Field(None, description="MCP version")
15061537
registry_json: Optional[Dict[str, Any]] = Field(None, description="Registry metadata JSON")
1538+
mcp_server: Optional[str] = Field(None, max_length=500, description="MCP server URL")
1539+
transport_type: Optional[str] = Field(None, description="Transport type")
15071540
config_json: Optional[Dict[str, Any]] = Field(
15081541
None,
15091542
description="Container MCP configuration JSON (omit to leave unchanged)",
15101543
)
15111544

1512-
@field_validator("name", "description", "version", mode="before")
1545+
@field_validator("name", "description", "mcp_server", "transport_type", mode="before")
15131546
@classmethod
15141547
def _strip_text(cls, value: Any):
15151548
if isinstance(value, str):
@@ -1518,6 +1551,11 @@ def _strip_text(cls, value: Any):
15181551
return value
15191552

15201553

1554+
class CommunityStatusUpdateRequest(BaseModel):
1555+
"""Request model for changing MCP market listing status (PATCH)."""
1556+
status: str = Field(..., description="New status: shared / rejected / not_shared / pending_review")
1557+
1558+
15211559
class DeleteMcpServiceRequest(BaseModel):
15221560
"""Request model for deleting an MCP service"""
15231561
mcp_id: int = Field(..., gt=0, description="MCP record ID to delete")

0 commit comments

Comments
 (0)