From c021f2145df2d3bedbfbf01179ef01588ca958cc Mon Sep 17 00:00:00 2001 From: Hojin Yang Date: Thu, 5 Mar 2026 23:06:21 +0900 Subject: [PATCH 1/2] feat: add ROUTER_IGNORE_TOOL_IDS env var for tool-level filtering Adds support for blocking specific tools from specific MCP servers without disabling the entire server. Format: ROUTER_IGNORE_TOOL_IDS=serverId:toolName,serverId2:toolName2 Example: ROUTER_IGNORE_TOOL_IDS=grep_app:searchGitHub Also removes reenableOhMyOpencodeMcps postWriteHook that was force-re-enabling built-in MCPs the user had intentionally disabled. Fixes: grep_app searchGitHub routing to unrelated GitHub repos --- mcp-server/mcp_tool_router/hub.py | 8 ++++++-- mcp-server/mcp_tool_router/opencode_gateway_server.py | 2 ++ mcp-server/mcp_tool_router/router.py | 6 +++++- mcp-server/mcp_tool_router/router_mcp_server.py | 8 +++++--- src/index.ts | 4 +++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/mcp-server/mcp_tool_router/hub.py b/mcp-server/mcp_tool_router/hub.py index 1ad0587..c57dc4e 100644 --- a/mcp-server/mcp_tool_router/hub.py +++ b/mcp-server/mcp_tool_router/hub.py @@ -50,6 +50,7 @@ def __init__( self._host_tool_definitions: dict[str, list[dict[str, Any]]] = ( host_tool_definitions or {} ) + self._ignore_tool_ids: set[str] = set() @classmethod def from_yaml( @@ -67,6 +68,7 @@ def from_opencode_config( auto_sync: bool = True, include_disabled: bool = False, ignore_ids: Iterable[str] | None = None, + ignore_tool_ids: Iterable[str] | None = None, ) -> "ToolRouterHub": registry, host_tools, host_tool_definitions = cls.load_opencode_runtime( path, @@ -74,7 +76,7 @@ def from_opencode_config( ignore_ids=ignore_ids, ) router = ToolRouter(routerd_path=routerd_path) - return cls( + hub = cls( registry, router, auto_sync=auto_sync, @@ -82,6 +84,8 @@ def from_opencode_config( host_tools=host_tools, host_tool_definitions=host_tool_definitions, ) + hub._ignore_tool_ids = set(ignore_tool_ids or []) + return hub @classmethod def load_opencode_runtime( @@ -180,7 +184,7 @@ def sync_server(self, server_id: str, *, raise_on_error: bool = True) -> None: raise ValueError(f"Server '{server_id}' is disabled.") try: client = self._ensure_client(server) - self._router.sync_from_mcp(server_id, client) + self._router.sync_from_mcp(server_id, client, ignore_tool_ids=self._ignore_tool_ids) except Exception as exc: if server.transport == "http" and not raise_on_error: print( diff --git a/mcp-server/mcp_tool_router/opencode_gateway_server.py b/mcp-server/mcp_tool_router/opencode_gateway_server.py index 94f5f61..3252156 100644 --- a/mcp-server/mcp_tool_router/opencode_gateway_server.py +++ b/mcp-server/mcp_tool_router/opencode_gateway_server.py @@ -829,6 +829,7 @@ def _load_gateway_hub() -> ToolRouterHub: ignore_ids.add(router_id) if not ignore_ids: ignore_ids.add("router") + ignore_tool_ids = _parse_id_list(os.environ.get("ROUTER_IGNORE_TOOL_IDS")) routerd_cmd = os.environ.get("ROUTERD") return ToolRouterHub.from_opencode_config( config_path, @@ -836,6 +837,7 @@ def _load_gateway_hub() -> ToolRouterHub: auto_sync=True, include_disabled=include_disabled, ignore_ids=sorted(ignore_ids), + ignore_tool_ids=sorted(ignore_tool_ids), ) diff --git a/mcp-server/mcp_tool_router/router.py b/mcp-server/mcp_tool_router/router.py index 1e16f6d..fdb6482 100644 --- a/mcp-server/mcp_tool_router/router.py +++ b/mcp-server/mcp_tool_router/router.py @@ -133,7 +133,7 @@ def _routerd_argv(self) -> list[str]: return shlex.split(self._config.routerd_path) return ["tool-routerd"] - def sync_from_mcp(self, server_id: str, mcp_client: Any) -> None: + def sync_from_mcp(self, server_id: str, mcp_client: Any, ignore_tool_ids: set[str] | None = None) -> None: """Sync MCP tools into the router catalog. Expected flow: @@ -149,6 +149,7 @@ def sync_from_mcp(self, server_id: str, mcp_client: Any) -> None: tools=tools or [], source_type="mcp", source_platform="mcp", + ignore_tool_ids=ignore_tool_ids, ) def sync_from_tool_definitions( @@ -158,6 +159,7 @@ def sync_from_tool_definitions( *, source_type: str = "mcp", source_platform: str = "mcp", + ignore_tool_ids: set[str] | None = None, ) -> None: tool_cards: list[dict[str, Any]] = [] for tool in tools: @@ -172,6 +174,8 @@ def sync_from_tool_definitions( if not card: continue tool_id = card["toolId"] + if ignore_tool_ids and tool_id in ignore_tool_ids: + continue raw_tool = dict(tool) if "name" not in raw_tool: raw_tool["name"] = card["toolName"] diff --git a/mcp-server/mcp_tool_router/router_mcp_server.py b/mcp-server/mcp_tool_router/router_mcp_server.py index d7ad677..25735a5 100644 --- a/mcp-server/mcp_tool_router/router_mcp_server.py +++ b/mcp-server/mcp_tool_router/router_mcp_server.py @@ -359,12 +359,13 @@ def _parse_id_list(value: str | None) -> set[str]: return {item.strip() for item in value.split(",") if item.strip()} -def _load_hub() -> tuple[ToolRouterHub, str, bool, list[str], str]: +def _load_hub() -> tuple[ToolRouterHub, str, bool, list[str], list[str], str]: config_path = os.environ.get("OPENCODE_CONFIG", "~/.config/opencode/opencode.json") include_disabled = os.environ.get( "ROUTER_INCLUDE_DISABLED", "true" ).lower() not in {"0", "false", "no"} ignore_ids = _parse_id_list(os.environ.get("ROUTER_IGNORE_IDS")) + ignore_tool_ids = _parse_id_list(os.environ.get("ROUTER_IGNORE_TOOL_IDS")) router_id = os.environ.get("ROUTER_MCP_ID") if router_id: ignore_ids.add(router_id) @@ -377,8 +378,9 @@ def _load_hub() -> tuple[ToolRouterHub, str, bool, list[str], str]: auto_sync=True, include_disabled=include_disabled, ignore_ids=sorted(ignore_ids), + ignore_tool_ids=sorted(ignore_tool_ids), ) - return hub, config_path, include_disabled, sorted(ignore_ids), routerd_cmd + return hub, config_path, include_disabled, sorted(ignore_ids), sorted(ignore_tool_ids), routerd_cmd class _ConfigWatcher(threading.Thread): @@ -438,7 +440,7 @@ def _reload(self) -> None: def main() -> int: - hub, config_path, include_disabled, ignore_ids, routerd_cmd = _load_hub() + hub, config_path, include_disabled, ignore_ids, ignore_tool_ids, routerd_cmd = _load_hub() interval = _coerce_int(os.environ.get("ROUTER_WATCH_INTERVAL"), 1) watcher = _ConfigWatcher( hub, diff --git a/src/index.ts b/src/index.ts index c61c6c6..2e661a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,9 @@ const INSTALL_PROFILES: Record = { defaultConfigPath: "~/.config/opencode/opencode.json", mcpField: "mcp", wellKnownRemoteMcps: OPENCODE_WELL_KNOWN_REMOTE_MCPS, - postWriteHook: reenableOhMyOpencodeMcps, + // postWriteHook removed: reenableOhMyOpencodeMcps was overriding user + // config by force-re-enabling built-in MCPs the user had disabled. + // User config should always be respected. postInstallHook: ensureOpencodeGatewayShim, }, claude: { From a07feb4befbb73430fc5a29fad2bfbfc9c470897 Mon Sep 17 00:00:00 2001 From: Hojin Yang Date: Thu, 5 Mar 2026 23:09:43 +0900 Subject: [PATCH 2/2] fix: also remove _reenable_oh_my_opencode_mcps from Python opencode_config.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python side had the same bug — force-re-enabling built-in MCPs (context7, grep_app, websearch) that users disabled via oh-my-opencode. --- .gitignore | 5 +++++ mcp-server/mcp_tool_router/opencode_config.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d8680d0..a99cdf5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ coverage/ # Session docs *.md + +bun.lock +iTerm2 +mcpflow-router@* +node diff --git a/mcp-server/mcp_tool_router/opencode_config.py b/mcp-server/mcp_tool_router/opencode_config.py index 4c9b206..7b00d6b 100644 --- a/mcp-server/mcp_tool_router/opencode_config.py +++ b/mcp-server/mcp_tool_router/opencode_config.py @@ -60,7 +60,7 @@ def apply_router_config( entry["enabled"] = True _write_config(path, payload, create_backup=create_backup) - _reenable_oh_my_opencode_mcps(path.parent, create_backup=create_backup) + # _reenable_oh_my_opencode_mcps removed: user config should be respected return payload @@ -135,7 +135,7 @@ def main() -> int: return 0 _write_config(path, payload, create_backup=args.create_backup) - _reenable_oh_my_opencode_mcps(path.parent, create_backup=args.create_backup) + # _reenable_oh_my_opencode_mcps removed: user config should be respected return 0