From c8a6b0cf77a9fd4a9aebe95aed1cdfeed38d9e72 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 3 Sep 2026 11:25:48 +0200 Subject: [PATCH] fix(mcp): let a provider that states nothing admit a declared client utils/roles/applications/config.py turns a missing key with a None default into False, so the provider-side probe in attachments.py read an omitted mcp_consumer flag as an explicit refusal. No MCP client ever joined a provider's shared_net: open-webui rendered no gitlab network, http://gitlabmcp:8080/mcp did not resolve from its namespace, and Open WebUI's verify call answered 400 in both GitLab#1 jobs of run 33698844864, clearnet and tor alike. Measured on that run's own inventory: without the change open-webui does not join the gitlab overlay, with it it does, and a sentinel probe shows the provider carries no mcp_consumer key at all. utils/roles/applications/mcp.py:295 already states the rule this restores, that an omitted flag inherits the client's own declaration and only an explicit false excludes. The stub in the render tests answered an unset path with the caller's default, which is why the regression passed them; it now coerces the way the accessor does, and the new case fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) --- .../unit/python/utils/networks/test_render.py | 30 ++++++++++++++++++- utils/networks/attachments.py | 6 ++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/unit/python/utils/networks/test_render.py b/tests/unit/python/utils/networks/test_render.py index 174dcbece1..0050b62e43 100644 --- a/tests/unit/python/utils/networks/test_render.py +++ b/tests/unit/python/utils/networks/test_render.py @@ -43,13 +43,18 @@ def _role_lookup_config(per_role, **values): Args: per_role: ``{role: {path: value}}`` consulted before the shared map. values: paths every role answers identically. + + An unset path is answered the way utils.roles.applications.config.get does + it, which turns a ``None`` default into ``False``. """ def _lookup(app, path, default): scoped = per_role.get(app) or {} if path in scoped: return scoped[path] - return values.get(path, default) + if path in values: + return values[path] + return default if default is not None else False return _lookup @@ -152,6 +157,29 @@ class TestMcpClientConsumer(unittest.TestCase): "being a client is not an admission; the self-declaration decides", ) + def test_a_provider_that_states_nothing_admits_a_declared_client(self): + """Only an explicit false refuses; an unset flag inherits the client's + own declaration, and the accessor answers an unset path with false.""" + entry = { + "role": "web-app-gitlab", + "overlay": {"consumer": {"kind": "mcp_client"}}, + } + self.assertTrue( + _is_consumer( + entry, + "web-app-openwebui", + _role_lookup_config( + {"web-app-openwebui": {"services.openwebui.mcp_consumer": True}}, + **{ + "mcp.enabled": True, + "mcp.shared": True, + "mcp.direction": "client", + }, + ), + _const_lookup_database(), + ) + ) + def test_a_provider_override_refuses_a_declared_client(self): entry = { "role": "web-app-homeassistant", diff --git a/utils/networks/attachments.py b/utils/networks/attachments.py index 591ed7c00e..2801d81d8c 100644 --- a/utils/networks/attachments.py +++ b/utils/networks/attachments.py @@ -73,10 +73,10 @@ def _matches_kind( is not True ): return False - override = lookup_config( - entry.get("role") or "", f"services.{consumer_key}.mcp_consumer", None + refusal = lookup_config( + entry.get("role") or "", f"services.{consumer_key}.mcp_consumer", True ) - return override is not False + return refusal is not False if kind == "onion_sso": provider = consumer.get("provider") if not provider: