Skip to content

Commit 9e98adc

Browse files
Olivier Gintrandgcgoncalves
authored andcommitted
feat(gateway): gateway-level tools_include/tools_exclude filters
Signed-off-by: Olivier Gintrand <olivier.gintrand@forterro.com>
1 parent 0fc878c commit 9e98adc

File tree

10 files changed

+319
-0
lines changed

10 files changed

+319
-0
lines changed

mcpgateway/admin.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12153,6 +12153,24 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
1215312153
else:
1215412154
passthrough_headers = None
1215512155

12156+
# Handle tools_include filter
12157+
tools_include_str = str(form.get("tools_include", ""))
12158+
tools_include: Optional[list[str]] = None
12159+
if tools_include_str and tools_include_str.strip():
12160+
try:
12161+
tools_include = orjson.loads(tools_include_str)
12162+
except (orjson.JSONDecodeError, ValueError):
12163+
tools_include = [p.strip() for p in tools_include_str.split(",") if p.strip()]
12164+
12165+
# Handle tools_exclude filter
12166+
tools_exclude_str = str(form.get("tools_exclude", ""))
12167+
tools_exclude: Optional[list[str]] = None
12168+
if tools_exclude_str and tools_exclude_str.strip():
12169+
try:
12170+
tools_exclude = orjson.loads(tools_exclude_str)
12171+
except (orjson.JSONDecodeError, ValueError):
12172+
tools_exclude = [p.strip() for p in tools_exclude_str.split(",") if p.strip()]
12173+
1215612174
# Auto-detect OAuth: if oauth_config is present and auth_type not explicitly set, use "oauth"
1215712175
auth_type_from_form = str(form.get("auth_type", ""))
1215812176
LOGGER.info(f"DEBUG: auth_type from form: '{auth_type_from_form}', oauth_config present: {oauth_config is not None}")
@@ -12203,6 +12221,8 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
1220312221
oauth_config=oauth_config,
1220412222
one_time_auth=form.get("one_time_auth", False),
1220512223
passthrough_headers=passthrough_headers,
12224+
tools_include=tools_include,
12225+
tools_exclude=tools_exclude,
1220612226
visibility=visibility,
1220712227
ca_certificate=ca_certificate,
1220812228
ca_certificate_sig=sig if sig else None,
@@ -12349,6 +12369,25 @@ async def admin_edit_gateway(
1234912369
else:
1235012370
passthrough_headers = None
1235112371

12372+
# Handle tools_include filter
12373+
# Use [] (empty list) to signal "user cleared the filter" vs None (not provided)
12374+
tools_include_str = str(form.get("tools_include", ""))
12375+
tools_include: Optional[List[str]] = []
12376+
if tools_include_str and tools_include_str.strip():
12377+
try:
12378+
tools_include = orjson.loads(tools_include_str)
12379+
except (orjson.JSONDecodeError, ValueError):
12380+
tools_include = [p.strip() for p in tools_include_str.split(",") if p.strip()]
12381+
12382+
# Handle tools_exclude filter
12383+
tools_exclude_str = str(form.get("tools_exclude", ""))
12384+
tools_exclude: Optional[List[str]] = []
12385+
if tools_exclude_str and tools_exclude_str.strip():
12386+
try:
12387+
tools_exclude = orjson.loads(tools_exclude_str)
12388+
except (orjson.JSONDecodeError, ValueError):
12389+
tools_exclude = [p.strip() for p in tools_exclude_str.split(",") if p.strip()]
12390+
1235212391
# Parse OAuth configuration - support both JSON string and individual form fields
1235312392
oauth_config_json = str(form.get("oauth_config"))
1235412393
oauth_config: Optional[dict[str, Any]] = None
@@ -12454,6 +12493,8 @@ async def admin_edit_gateway(
1245412493
auth_query_param_value=str(form.get("auth_query_param_value", "")) or None,
1245512494
one_time_auth=form.get("one_time_auth", False),
1245612495
passthrough_headers=passthrough_headers,
12496+
tools_include=tools_include,
12497+
tools_exclude=tools_exclude,
1245712498
oauth_config=oauth_config,
1245812499
visibility=visibility,
1245912500
owner_email=user_email,

mcpgateway/admin_ui/formSubmitHandlers.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,28 @@ export const handleEditGatewayFormSubmit = async function (e) {
790790

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

793+
// Process tools_include - convert comma-separated string to JSON array
794+
const toolsIncludeString = formData.get("tools_include") || "";
795+
const toolsInclude = toolsIncludeString
796+
.split(",")
797+
.map((pattern) => pattern.trim())
798+
.filter((pattern) => pattern.length > 0);
799+
if (toolsInclude.length > 0) {
800+
formData.delete("tools_include");
801+
formData.append("tools_include", JSON.stringify(toolsInclude));
802+
}
803+
804+
// Process tools_exclude - convert comma-separated string to JSON array
805+
const toolsExcludeString = formData.get("tools_exclude") || "";
806+
const toolsExclude = toolsExcludeString
807+
.split(",")
808+
.map((pattern) => pattern.trim())
809+
.filter((pattern) => pattern.length > 0);
810+
if (toolsExclude.length > 0) {
811+
formData.delete("tools_exclude");
812+
formData.append("tools_exclude", JSON.stringify(toolsExclude));
813+
}
814+
793815
// Handle OAuth configuration
794816
// NOTE: OAuth config assembly is now handled by the backend (mcpgateway/admin.py)
795817
// The backend assembles individual form fields into oauth_config with proper field names
@@ -884,6 +906,28 @@ export const handleEditA2AAgentFormSubmit = async function (e) {
884906

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

909+
// Process tools_include - convert comma-separated string to JSON array
910+
const editToolsIncludeString = formData.get("tools_include") || "";
911+
const editToolsInclude = editToolsIncludeString
912+
.split(",")
913+
.map((pattern) => pattern.trim())
914+
.filter((pattern) => pattern.length > 0);
915+
if (editToolsInclude.length > 0) {
916+
formData.delete("tools_include");
917+
formData.append("tools_include", JSON.stringify(editToolsInclude));
918+
}
919+
920+
// Process tools_exclude - convert comma-separated string to JSON array
921+
const editToolsExcludeString = formData.get("tools_exclude") || "";
922+
const editToolsExclude = editToolsExcludeString
923+
.split(",")
924+
.map((pattern) => pattern.trim())
925+
.filter((pattern) => pattern.length > 0);
926+
if (editToolsExclude.length > 0) {
927+
formData.delete("tools_exclude");
928+
formData.append("tools_exclude", JSON.stringify(editToolsExclude));
929+
}
930+
887931
// Handle auth_headers JSON field
888932
const authHeadersJson = formData.get("auth_headers");
889933
if (authHeadersJson) {

mcpgateway/admin_ui/gateways.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,26 @@ export const editGateway = async function (gatewayId) {
576576
}
577577
}
578578

579+
// Handle tools_include
580+
const toolsIncludeField = safeGetElement("edit-gateway-tools-include");
581+
if (toolsIncludeField) {
582+
if (gateway.toolsInclude && Array.isArray(gateway.toolsInclude)) {
583+
toolsIncludeField.value = gateway.toolsInclude.join(", ");
584+
} else {
585+
toolsIncludeField.value = "";
586+
}
587+
}
588+
589+
// Handle tools_exclude
590+
const toolsExcludeField = safeGetElement("edit-gateway-tools-exclude");
591+
if (toolsExcludeField) {
592+
if (gateway.toolsExclude && Array.isArray(gateway.toolsExclude)) {
593+
toolsExcludeField.value = gateway.toolsExclude.join(", ");
594+
} else {
595+
toolsExcludeField.value = "";
596+
}
597+
}
598+
579599
openModal("gateway-edit-modal");
580600
applyVisibilityRestrictions(["edit-gateway-visibility"]); // Disable public radio if restricted, preserve checked state
581601
console.log("✓ Gateway edit modal loaded successfully");
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Add tools_include and tools_exclude columns to gateways table.
2+
3+
Revision ID: b3c4d5e6f7a8
4+
Revises: a7f3c9e1b2d4
5+
Create Date: 2026-04-03 10:00:00.000000
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
from sqlalchemy import inspect
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "b3c4d5e6f7a8"
15+
down_revision = "a7f3c9e1b2d4"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
"""Add tool filter columns to gateways."""
22+
conn = op.get_bind()
23+
inspector = inspect(conn)
24+
existing_columns = {col["name"] for col in inspector.get_columns("gateways")}
25+
26+
if "tools_include" not in existing_columns:
27+
op.add_column("gateways", sa.Column("tools_include", sa.JSON(), nullable=True, comment="Glob patterns to include tools (whitelist)"))
28+
if "tools_exclude" not in existing_columns:
29+
op.add_column("gateways", sa.Column("tools_exclude", sa.JSON(), nullable=True, comment="Glob patterns to exclude tools (blacklist)"))
30+
31+
32+
def downgrade():
33+
"""Remove tool filter columns from gateways."""
34+
with op.batch_alter_table("gateways", schema=None) as batch_op:
35+
batch_op.drop_column("tools_exclude")
36+
batch_op.drop_column("tools_include")

mcpgateway/db.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4668,6 +4668,13 @@ def team(self) -> Optional[str]:
46684668
# - 'direct_proxy': All RPC calls are proxied directly to remote MCP server with no database caching
46694669
gateway_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="cache", comment="Gateway mode: 'cache' (database caching) or 'direct_proxy' (pass-through mode)")
46704670

4671+
# Tool filtering: include/exclude patterns (fnmatch glob syntax)
4672+
# - tools_include: only tools matching at least one pattern are imported (whitelist)
4673+
# - tools_exclude: tools matching any pattern are excluded (blacklist)
4674+
# - If both are set, include is applied first, then exclude
4675+
tools_include: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, default=None, comment="Glob patterns to include tools (whitelist)")
4676+
tools_exclude: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, default=None, comment="Glob patterns to exclude tools (blacklist)")
4677+
46714678
# Relationship with OAuth tokens
46724679
oauth_tokens: Mapped[List["OAuthToken"]] = relationship("OAuthToken", back_populates="gateway", cascade="all, delete-orphan")
46734680

mcpgateway/schemas.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,6 +2815,10 @@ class GatewayCreate(BaseModelWithConfigDict):
28152815
# Gateway mode configuration
28162816
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)$")
28172817

2818+
# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
2819+
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
2820+
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")
2821+
28182822
@field_validator("gateway_mode", mode="before")
28192823
@classmethod
28202824
def default_gateway_mode(cls, v: Optional[str]) -> str:
@@ -3151,6 +3155,10 @@ class GatewayUpdate(BaseModelWithConfigDict):
31513155
# Gateway mode configuration
31523156
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)$")
31533157

3158+
# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
3159+
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
3160+
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")
3161+
31543162
# CA certificate configuration for custom TLS trust
31553163
ca_certificate: Optional[str] = Field(None, description="Custom CA certificate for TLS verification")
31563164
ca_certificate_sig: Optional[str] = Field(None, description="Signature of the custom CA certificate")
@@ -3520,6 +3528,10 @@ class GatewayRead(BaseModelWithConfigDict):
35203528
# Gateway mode configuration
35213529
gateway_mode: str = Field(default="cache", description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)")
35223530

3531+
# Tool filtering: fnmatch glob patterns to include/exclude tools during refresh
3532+
tools_include: Optional[List[str]] = Field(None, description="Glob patterns to whitelist tools (only matching tools are imported)")
3533+
tools_exclude: Optional[List[str]] = Field(None, description="Glob patterns to blacklist tools (matching tools are excluded)")
3534+
35233535
_normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))
35243536

35253537
# Tool count (populated from the tools relationship; 0 when not loaded)

mcpgateway/services/export_service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,12 @@ async def _export_gateways(self, db: Session, tags: Optional[List[str]], include
512512
"passthrough_headers": gateway.passthrough_headers or [],
513513
}
514514

515+
# Tool filtering patterns
516+
if gateway.tools_include:
517+
gateway_data["tools_include"] = gateway.tools_include
518+
if gateway.tools_exclude:
519+
gateway_data["tools_exclude"] = gateway.tools_exclude
520+
515521
# Handle authentication data securely - use batch-fetched values
516522
if gateway.auth_type and gateway.auth_value:
517523
if gateway.auth_value == settings.masked_auth_value:
@@ -938,6 +944,12 @@ async def _export_selected_gateways(self, db: Session, gateway_ids: List[str], u
938944
"passthrough_headers": db_gateway.passthrough_headers or [],
939945
}
940946

947+
# Tool filtering patterns
948+
if db_gateway.tools_include:
949+
gateway_data["tools_include"] = db_gateway.tools_include
950+
if db_gateway.tools_exclude:
951+
gateway_data["tools_exclude"] = db_gateway.tools_exclude
952+
941953
# Include auth data directly from DB (already have raw values)
942954
if db_gateway.auth_type:
943955
gateway_data["auth_type"] = db_gateway.auth_type

0 commit comments

Comments
 (0)