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
13 changes: 13 additions & 0 deletions backend/apps/mcp_management_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async def list_community_mcp_services_api(
user_id, tenant_id, _ = get_current_user_info(authorization, http_request)
data = await list_community_mcp_services(
tenant_id=tenant_id,
user_id=user_id,
search=query.search,
tag=query.tag,
transport_type=query.transport_type,
Expand Down Expand Up @@ -214,6 +215,9 @@ async def publish_community_mcp_service_api(
tags=payload.tags,
mcp_server=payload.mcp_server,
config_json=payload.config_json,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)
return JSONResponse(
status_code=HTTPStatus.OK,
Expand Down Expand Up @@ -257,6 +261,9 @@ async def update_community_mcp_service_legacy_api(
mcp_server=payload.mcp_server,
config_json=payload.config_json,
transport_type=payload.transport_type,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)
return JSONResponse(status_code=HTTPStatus.OK, content={"status": "success"})
except McpNotFoundError as exc:
Expand Down Expand Up @@ -386,6 +393,9 @@ async def create_community_mcp_service_api(
tags=payload.tags,
mcp_server=payload.mcp_server,
config_json=payload.config_json,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)
return JSONResponse(
status_code=HTTPStatus.OK,
Expand Down Expand Up @@ -430,6 +440,9 @@ async def update_community_mcp_service_api(
mcp_server=payload.mcp_server,
config_json=payload.config_json,
transport_type=payload.transport_type,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)
return JSONResponse(status_code=HTTPStatus.OK, content={"status": "success"})
except McpNotFoundError as exc:
Expand Down
9 changes: 9 additions & 0 deletions backend/apps/remote_mcp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ async def add_mcp_service_endpoint(
config_json=payload.config_json,
market_id=payload.market_id,
enabled=payload.enabled if payload.enabled is not None else False,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)

return JSONResponse(
Expand Down Expand Up @@ -225,6 +228,9 @@ async def add_container_mcp_service_endpoint(
market_id=payload.market_id,
port=payload.port,
mcp_config=payload.mcp_config,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)

return JSONResponse(
Expand Down Expand Up @@ -296,6 +302,9 @@ async def update_mcp_service_endpoint(
config_json=payload.config_json,
tags=payload.tags,
market_id=payload.market_id,
group_ids=payload.group_ids,
ingroup_permission=payload.ingroup_permission,
shared_fields=payload.shared_fields,
)

return JSONResponse(
Expand Down
15 changes: 15 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,9 @@
config_json: Optional[Dict[str, Any]] = Field(None, description="MCP configuration JSON (e.g. OpenAPI spec for API-type MCP)")
market_id: Optional[int] = Field(None, gt=0, description="Linked market record ID")
enabled: Optional[bool] = Field(default=False, description="Whether the MCP is enabled after creation")
group_ids: Optional[str] = Field(None, description="Comma-separated group IDs that can access this MCP")

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Comma-separated group IDs that can access this MCP" 3 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ-CyftxSF-_auD-m7kn&open=AZ-CyftxSF-_auD-m7kn&pullRequest=3470
ingroup_permission: Optional[str] = Field(None, description="Permission level: EDIT, READ_ONLY, PRIVATE")

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Permission level: EDIT, READ_ONLY, PRIVATE" 5 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ-CyftxSF-_auD-m7kl&open=AZ-CyftxSF-_auD-m7kl&pullRequest=3470
shared_fields: Optional[Dict[str, Any]] = Field(None, description="JSON object of field-level sharing flags")

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "JSON object of field-level sharing flags" 5 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ-CyftxSF-_auD-m7km&open=AZ-CyftxSF-_auD-m7km&pullRequest=3470

@field_validator("name", "server_url", "description", "authorization_token", mode="before")
@classmethod
Expand All @@ -1401,6 +1404,9 @@
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")
group_ids: Optional[str] = Field(None, description="Comma-separated group IDs that can access this MCP")
ingroup_permission: Optional[str] = Field(None, description="Permission level: EDIT, READ_ONLY, PRIVATE")
shared_fields: Optional[Dict[str, Any]] = Field(None, description="JSON object of field-level sharing flags")

@field_validator("name", "description", "authorization_token", mode="before")
@classmethod
Expand All @@ -1422,6 +1428,9 @@
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")
group_ids: Optional[str] = Field(None, description="Comma-separated group IDs that can access this MCP")
ingroup_permission: Optional[str] = Field(None, description="Permission level: EDIT, READ_ONLY, PRIVATE")
shared_fields: Optional[Dict[str, Any]] = Field(None, description="JSON object of field-level sharing flags")

@field_validator("name", "server_url", "description", "authorization_token", "version", mode="before")
@classmethod
Expand Down Expand Up @@ -1538,6 +1547,9 @@
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")
group_ids: Optional[List[int]] = Field(None, description="Group IDs that can access this MCP")
ingroup_permission: Optional[str] = Field(None, description="Permission level: EDIT, READ_ONLY, PRIVATE")
shared_fields: Optional[Dict[str, Any]] = Field(None, description="JSON object of field-level sharing flags")

@field_validator("name", "description", "mcp_server", mode="before")
@classmethod
Expand All @@ -1561,6 +1573,9 @@
None,
description="Container MCP configuration JSON (omit to leave unchanged)",
)
group_ids: Optional[List[int]] = Field(None, description="Group IDs that can access this MCP")
ingroup_permission: Optional[str] = Field(None, description="Permission level: EDIT, READ_ONLY, PRIVATE")
shared_fields: Optional[Dict[str, Any]] = Field(None, description="JSON object of field-level sharing flags")

@field_validator("name", "description", "mcp_server", "transport_type", mode="before")
@classmethod
Expand Down
10 changes: 10 additions & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@
group_ids = Column(String, doc="Agent group IDs list")
is_new = Column(Boolean, default=False, doc="Whether this agent is marked as new for the user")
current_version_no = Column(Integer, nullable=True, doc="Current published version number. NULL means no version published yet")
ingroup_permission = Column(String(30), doc="In-group permission: EDIT, READ_ONLY, PRIVATE")

Check failure on line 614 in backend/database/db_models.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "In-group permission: EDIT, READ_ONLY, PRIVATE" 4 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ-CyfsuSF-_auD-m7kj&open=AZ-CyfsuSF-_auD-m7kj&pullRequest=3470
requested_output_tokens = Column(
Integer,
doc=(
Expand Down Expand Up @@ -799,6 +799,11 @@
enabled = Column(Boolean, default=True, doc="Enabled")
tags = Column(ARRAY(Text), doc="Tags")
description = Column(Text, doc="Description")
group_ids = Column(String, doc="Comma-separated group IDs that can access this MCP")
ingroup_permission = Column(String(30), default="READ_ONLY",
doc="In-group permission: EDIT, READ_ONLY, PRIVATE")
shared_fields = Column(JSON, default=None,
doc="JSON object of field-level sharing flags (e.g. {\"serverUrl\": true, \"authorizationToken\": false})")


class McpCommunityRecord(TableBase):
Expand Down Expand Up @@ -859,6 +864,11 @@
doc="Listing status: not_shared / pending_review / rejected / shared")
submitted_by = Column(String(100), doc="Submitter email when listing enters pending_review")
source_mcp_id = Column(Integer, doc="Local MCP record ID that created this market record")
group_ids = Column(String, doc="Comma-separated group IDs that can access this MCP")
ingroup_permission = Column(String(30), default="READ_ONLY",
doc="In-group permission: EDIT, READ_ONLY, PRIVATE")
shared_fields = Column(JSON, default=None,
doc="Snapshot of shared_fields at submission time")


class UserTenant(TableBase):
Expand Down
41 changes: 39 additions & 2 deletions backend/database/market_mcp_db.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import logging
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from sqlalchemy import func, or_
from sqlalchemy import func, or_, text as sa_text

from database.client import as_dict, filter_property, get_db_session
from database.db_models import McpMarketRecord

logger = logging.getLogger("market_mcp_db")


def _apply_group_permission_filter(query, user_id: str, user_group_ids: List[int]):
"""Apply group-based visibility filter to a market record query.

Users see MCPs where:
- They are the creator, OR
- The MCP has no group restriction (group_ids IS NULL/empty), OR
- They belong to at least one of the MCP's allowed groups
"""
conditions = [
McpMarketRecord.user_id == user_id,
McpMarketRecord.group_ids.is_(None),
McpMarketRecord.group_ids == "",
]
if user_group_ids:
group_ids_str = ",".join(str(g) for g in user_group_ids)
conditions.append(
sa_text(
f"string_to_array(group_ids, ',') && ARRAY[{group_ids_str}]::text[]"
)
)
return query.filter(or_(*conditions))


def get_mcp_market_records(
*,
tenant_id: str | None = None,
Expand All @@ -17,6 +40,8 @@ def get_mcp_market_records(
transport_type: str | None = None,
cursor: str | None = None,
limit: int = 30,
user_id: str | None = None,
user_group_ids: Optional[List[int]] = None,
) -> Dict[str, Any]:
"""Cursor-paginated listing of shared (approved) market records scoped to a tenant."""
with get_db_session() as session:
Expand Down Expand Up @@ -54,6 +79,9 @@ def get_mcp_market_records(
if cursor_id is not None:
query = query.filter(McpMarketRecord.market_id < cursor_id)

if user_id is not None and user_group_ids is not None:
query = _apply_group_permission_filter(query, user_id, user_group_ids)

rows: List[McpMarketRecord] = (
query.order_by(McpMarketRecord.market_id.desc())
.limit(limit + 1)
Expand Down Expand Up @@ -148,6 +176,9 @@ def update_mcp_market_record(
mcp_server: str | None = None,
config_json: Dict[str, Any] | None = None,
transport_type: str | None = None,
group_ids: str | None = None,
ingroup_permission: str | None = None,
shared_fields: dict | None = None,
) -> None:
"""Update editable fields on a market record (does not change status)."""
update_fields: Dict[str, Any] = {"updated_by": user_id}
Expand All @@ -165,6 +196,12 @@ def update_mcp_market_record(
update_fields["config_json"] = config_json
if transport_type is not None:
update_fields["transport_type"] = transport_type
if group_ids is not None:
update_fields["group_ids"] = group_ids
if ingroup_permission is not None:
update_fields["ingroup_permission"] = ingroup_permission
if shared_fields is not None:
update_fields["shared_fields"] = shared_fields

with get_db_session() as session:
session.query(McpMarketRecord).filter(
Expand Down
39 changes: 24 additions & 15 deletions backend/database/remote_mcp_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def create_mcp_record(mcp_data: Dict[str, Any], tenant_id: str, user_id: str):
allowed_fields = {
'mcp_name', 'mcp_server', 'status', 'container_id', 'container_port',
'authorization_token', 'custom_headers', 'source', 'market_id',
'registry_json', 'config_json', 'enabled', 'tags', 'description'
'registry_json', 'config_json', 'enabled', 'tags', 'description',
'group_ids', 'ingroup_permission', 'shared_fields'
}

filtered_data = {k: v for k, v in mcp_data.items() if k in allowed_fields and v is not None}
Expand Down Expand Up @@ -139,26 +140,34 @@ def update_mcp_record_manage_fields_by_id(
custom_headers: Dict[str, Any] | None,
config_json: Dict[str, Any] | None,
market_id: int | None,
group_ids: str | None = None,
ingroup_permission: str | None = None,
shared_fields: dict | None = None,
) -> None:
with get_db_session() as session:
update_data = {
"mcp_name": name,
"mcp_server": server_url,
"description": description,
"tags": tags or [],
"source": source,
"authorization_token": authorization_token,
"custom_headers": custom_headers,
"config_json": config_json,
"market_id": market_id,
"updated_by": user_id,
}
if group_ids is not None:
update_data["group_ids"] = group_ids
if ingroup_permission is not None:
update_data["ingroup_permission"] = ingroup_permission
if shared_fields is not None:
update_data["shared_fields"] = shared_fields
session.query(McpRecord).filter(
McpRecord.mcp_id == mcp_id,
McpRecord.tenant_id == tenant_id,
McpRecord.delete_flag != 'Y'
).update(
{
"mcp_name": name,
"mcp_server": server_url,
"description": description,
"tags": tags or [],
"source": source,
"authorization_token": authorization_token,
"custom_headers": custom_headers,
"config_json": config_json,
"market_id": market_id,
"updated_by": user_id,
}
)
).update(update_data)


def update_mcp_record_market_id_by_id(
Expand Down
Loading
Loading