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
41 changes: 41 additions & 0 deletions mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12153,6 +12153,24 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
else:
passthrough_headers = None

# Handle tools_include filter
tools_include_str = str(form.get("tools_include", ""))
tools_include: Optional[list[str]] = None
if tools_include_str and tools_include_str.strip():
try:
tools_include = orjson.loads(tools_include_str)
except (orjson.JSONDecodeError, ValueError):
tools_include = [p.strip() for p in tools_include_str.split(",") if p.strip()]

# Handle tools_exclude filter
tools_exclude_str = str(form.get("tools_exclude", ""))
tools_exclude: Optional[list[str]] = None
if tools_exclude_str and tools_exclude_str.strip():
try:
tools_exclude = orjson.loads(tools_exclude_str)
except (orjson.JSONDecodeError, ValueError):
tools_exclude = [p.strip() for p in tools_exclude_str.split(",") if p.strip()]

# Auto-detect OAuth: if oauth_config is present and auth_type not explicitly set, use "oauth"
auth_type_from_form = str(form.get("auth_type", ""))
LOGGER.info(f"DEBUG: auth_type from form: '{auth_type_from_form}', oauth_config present: {oauth_config is not None}")
Expand Down Expand Up @@ -12203,6 +12221,8 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
oauth_config=oauth_config,
one_time_auth=form.get("one_time_auth", False),
passthrough_headers=passthrough_headers,
tools_include=tools_include,
tools_exclude=tools_exclude,
visibility=visibility,
ca_certificate=ca_certificate,
ca_certificate_sig=sig if sig else None,
Expand Down Expand Up @@ -12349,6 +12369,25 @@ async def admin_edit_gateway(
else:
passthrough_headers = None

# Handle tools_include filter
# Use [] (empty list) to signal "user cleared the filter" vs None (not provided)
tools_include_str = str(form.get("tools_include", ""))
tools_include: Optional[List[str]] = []
if tools_include_str and tools_include_str.strip():
try:
tools_include = orjson.loads(tools_include_str)
except (orjson.JSONDecodeError, ValueError):
tools_include = [p.strip() for p in tools_include_str.split(",") if p.strip()]

# Handle tools_exclude filter
tools_exclude_str = str(form.get("tools_exclude", ""))
tools_exclude: Optional[List[str]] = []
if tools_exclude_str and tools_exclude_str.strip():
try:
tools_exclude = orjson.loads(tools_exclude_str)
except (orjson.JSONDecodeError, ValueError):
tools_exclude = [p.strip() for p in tools_exclude_str.split(",") if p.strip()]

# Parse OAuth configuration - support both JSON string and individual form fields
oauth_config_json = str(form.get("oauth_config"))
oauth_config: Optional[dict[str, Any]] = None
Expand Down Expand Up @@ -12454,6 +12493,8 @@ async def admin_edit_gateway(
auth_query_param_value=str(form.get("auth_query_param_value", "")) or None,
one_time_auth=form.get("one_time_auth", False),
passthrough_headers=passthrough_headers,
tools_include=tools_include,
tools_exclude=tools_exclude,
oauth_config=oauth_config,
visibility=visibility,
owner_email=user_email,
Expand Down
44 changes: 44 additions & 0 deletions mcpgateway/admin_ui/formSubmitHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,28 @@ export const handleEditGatewayFormSubmit = async function (e) {

formData.append("passthrough_headers", JSON.stringify(passthroughHeaders));

// Process tools_include - convert comma-separated string to JSON array
const toolsIncludeString = formData.get("tools_include") || "";
const toolsInclude = toolsIncludeString
.split(",")
.map((pattern) => pattern.trim())
.filter((pattern) => pattern.length > 0);
if (toolsInclude.length > 0) {
formData.delete("tools_include");
formData.append("tools_include", JSON.stringify(toolsInclude));
}

// Process tools_exclude - convert comma-separated string to JSON array
const toolsExcludeString = formData.get("tools_exclude") || "";
const toolsExclude = toolsExcludeString
.split(",")
.map((pattern) => pattern.trim())
.filter((pattern) => pattern.length > 0);
if (toolsExclude.length > 0) {
formData.delete("tools_exclude");
formData.append("tools_exclude", JSON.stringify(toolsExclude));
}

// Handle OAuth configuration
// NOTE: OAuth config assembly is now handled by the backend (mcpgateway/admin.py)
// The backend assembles individual form fields into oauth_config with proper field names
Expand Down Expand Up @@ -884,6 +906,28 @@ export const handleEditA2AAgentFormSubmit = async function (e) {

formData.append("passthrough_headers", JSON.stringify(passthroughHeaders));

// Process tools_include - convert comma-separated string to JSON array
const editToolsIncludeString = formData.get("tools_include") || "";
const editToolsInclude = editToolsIncludeString
.split(",")
.map((pattern) => pattern.trim())
.filter((pattern) => pattern.length > 0);
if (editToolsInclude.length > 0) {
formData.delete("tools_include");
formData.append("tools_include", JSON.stringify(editToolsInclude));
}

// Process tools_exclude - convert comma-separated string to JSON array
const editToolsExcludeString = formData.get("tools_exclude") || "";
const editToolsExclude = editToolsExcludeString
.split(",")
.map((pattern) => pattern.trim())
.filter((pattern) => pattern.length > 0);
if (editToolsExclude.length > 0) {
formData.delete("tools_exclude");
formData.append("tools_exclude", JSON.stringify(editToolsExclude));
}

// Handle auth_headers JSON field
const authHeadersJson = formData.get("auth_headers");
if (authHeadersJson) {
Expand Down
20 changes: 20 additions & 0 deletions mcpgateway/admin_ui/gateways.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,26 @@ export const editGateway = async function (gatewayId) {
}
}

// Handle tools_include
const toolsIncludeField = safeGetElement("edit-gateway-tools-include");
if (toolsIncludeField) {
if (gateway.toolsInclude && Array.isArray(gateway.toolsInclude)) {
toolsIncludeField.value = gateway.toolsInclude.join(", ");
} else {
toolsIncludeField.value = "";
}
}

// Handle tools_exclude
const toolsExcludeField = safeGetElement("edit-gateway-tools-exclude");
if (toolsExcludeField) {
if (gateway.toolsExclude && Array.isArray(gateway.toolsExclude)) {
toolsExcludeField.value = gateway.toolsExclude.join(", ");
} else {
toolsExcludeField.value = "";
}
}

openModal("gateway-edit-modal");
applyVisibilityRestrictions(["edit-gateway-visibility"]); // Disable public radio if restricted, preserve checked state
console.log("βœ“ Gateway edit modal loaded successfully");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add tools_include and tools_exclude columns to gateways table.

Revision ID: b3c4d5e6f7a8
Revises: a7f3c9e1b2d4
Create Date: 2026-04-03 10:00:00.000000

"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect

# revision identifiers, used by Alembic.
revision = "b3c4d5e6f7a8"
down_revision = "a7f3c9e1b2d4"
branch_labels = None
depends_on = None


def upgrade():
"""Add tool filter columns to gateways."""
conn = op.get_bind()
inspector = inspect(conn)
existing_columns = {col["name"] for col in inspector.get_columns("gateways")}

if "tools_include" not in existing_columns:
op.add_column("gateways", sa.Column("tools_include", sa.JSON(), nullable=True, comment="Glob patterns to include tools (whitelist)"))
if "tools_exclude" not in existing_columns:
op.add_column("gateways", sa.Column("tools_exclude", sa.JSON(), nullable=True, comment="Glob patterns to exclude tools (blacklist)"))


def downgrade():
"""Remove tool filter columns from gateways."""
with op.batch_alter_table("gateways", schema=None) as batch_op:
batch_op.drop_column("tools_exclude")
batch_op.drop_column("tools_include")
7 changes: 7 additions & 0 deletions mcpgateway/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4668,6 +4668,13 @@ def team(self) -> Optional[str]:
# - 'direct_proxy': All RPC calls are proxied directly to remote MCP server with no database caching
gateway_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="cache", comment="Gateway mode: 'cache' (database caching) or 'direct_proxy' (pass-through mode)")

# Tool filtering: include/exclude patterns (fnmatch glob syntax)
# - tools_include: only tools matching at least one pattern are imported (whitelist)
# - tools_exclude: tools matching any pattern are excluded (blacklist)
# - If both are set, include is applied first, then exclude
tools_include: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, default=None, comment="Glob patterns to include tools (whitelist)")
tools_exclude: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, default=None, comment="Glob patterns to exclude tools (blacklist)")

# Relationship with OAuth tokens
oauth_tokens: Mapped[List["OAuthToken"]] = relationship("OAuthToken", back_populates="gateway", cascade="all, delete-orphan")

Expand Down
12 changes: 12 additions & 0 deletions mcpgateway/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2815,6 +2815,10 @@ class GatewayCreate(BaseModelWithConfigDict):
# Gateway mode configuration
gateway_mode: str = Field(default="cache", description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)", pattern="^(cache|direct_proxy)$")

# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")

@field_validator("gateway_mode", mode="before")
@classmethod
def default_gateway_mode(cls, v: Optional[str]) -> str:
Expand Down Expand Up @@ -3151,6 +3155,10 @@ class GatewayUpdate(BaseModelWithConfigDict):
# Gateway mode configuration
gateway_mode: Optional[str] = Field(None, description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)", pattern="^(cache|direct_proxy)$")

# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")

# CA certificate configuration for custom TLS trust
ca_certificate: Optional[str] = Field(None, description="Custom CA certificate for TLS verification")
ca_certificate_sig: Optional[str] = Field(None, description="Signature of the custom CA certificate")
Expand Down Expand Up @@ -3520,6 +3528,10 @@ class GatewayRead(BaseModelWithConfigDict):
# Gateway mode configuration
gateway_mode: str = Field(default="cache", description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)")

# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")

_normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))

# Tool count (populated from the tools relationship; 0 when not loaded)
Expand Down
12 changes: 12 additions & 0 deletions mcpgateway/services/export_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,12 @@ async def _export_gateways(self, db: Session, tags: Optional[List[str]], include
"passthrough_headers": gateway.passthrough_headers or [],
}

# Tool filtering patterns
if gateway.tools_include:
gateway_data["tools_include"] = gateway.tools_include
if gateway.tools_exclude:
gateway_data["tools_exclude"] = gateway.tools_exclude

# Handle authentication data securely - use batch-fetched values
if gateway.auth_type and gateway.auth_value:
if gateway.auth_value == settings.masked_auth_value:
Expand Down Expand Up @@ -938,6 +944,12 @@ async def _export_selected_gateways(self, db: Session, gateway_ids: List[str], u
"passthrough_headers": db_gateway.passthrough_headers or [],
}

# Tool filtering patterns
if db_gateway.tools_include:
gateway_data["tools_include"] = db_gateway.tools_include
if db_gateway.tools_exclude:
gateway_data["tools_exclude"] = db_gateway.tools_exclude

# Include auth data directly from DB (already have raw values)
if db_gateway.auth_type:
gateway_data["auth_type"] = db_gateway.auth_type
Expand Down
Loading