{"repo":"NousResearch/hermes-agent","public_id":"36264","display_label":"MCP server connectivity and reconnection issues","summary":"A cluster of bug reports and fixes around MCP servers appearing connected but failing tool calls, covering stale/parked server entries, session expiry, reconnection resilience, tool registration on revival, and SSE transport handling.","subsystem":"mcp","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":59331},"resolution":{"state":"partially_resolved","by":"pr-59331"},"freshness":{"state":"verified","last_verified_at":"2026-07-06T04:48:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.24799999999999997},"fix_edges":[{"issue_number":37768,"pr_number":59331,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":50170,"pr_number":59331,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":36264,"title":"Bug: Exa MCP Server - hermes mcp test passes but tool calls fail","url":null,"state":"closed"},{"kind":"issue","number":36554,"title":"MCP tool calls fail with 'server not connected' despite active SSE session","url":null,"state":"closed"},{"kind":"issue","number":37768,"title":"MCP tool call returns 'not connected' while hermes mcp test passes","url":null,"state":"closed"},{"kind":"pr","number":37772,"title":"fix(mcp): reconnect stale server entries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#37899","number":37899},{"kind":"pr","value":"#37899","number":37899},{"kind":"pr","value":"#37899","number":37899}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":37899,"title":"fix(mcp): reconnect stale server entries in register_mcp_servers (#37768)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59222","number":59222},{"kind":"pr","value":"#59331","number":59331},{"kind":"pr","value":"#37772","number":37772}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":59331,"title":"fix(mcp): session-expired retry waits for a fresh session; wake parked servers at session startup (salvage #27052)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/release.py","tests/tools/test_mcp_circuit_breaker.py","tests/tools/test_mcp_register_wakes_stale.py","tests/tools/test_mcp_tool_session_expired.py","tools/mcp_tool.py"],"fix_hunks":[{"file":"scripts/release.py","hunks":["@@ -1211,6 +1211,7 @@\n     \"[redacted:email]\": \"BlackishGreen33\",\n     \"[redacted:email]\": \"gnodet\",  # PR #37598 salvage (MCP preflight POST probe fallback)\n     \"[redacted:email]\": \"kaishi00\",  # PR #55203 salvage (skip_preflight opt-out)\n+    \"[redacted:email]\": \"setclock\",  # PR #27052 salvage (MCP session-expired retry waits for a distinct fresh session)\n     \"[redacted:email]\": \"sberan\",  # PR #54494 salvage (--connect-timeout flag on hermes mcp add)\n     \"[redacted:email]\": \"labsobsidian\",  # PR #56699 salvage (MCP OAuth login connect_timeout floor)\n     \"[redacted:email]\": \"SaulJWu\","]},{"file":"tests/tools/test_mcp_circuit_breaker.py","hunks":["@@ -31,14 +31,49 @@ def _install_stub_server(mcp_tool_module, name: str, call_tool_impl):\n     ``call_tool_impl`` is an async function stored at ``session.call_tool``\n     (it's what the tool handler invokes).\n     \"\"\"\n+    import threading\n+\n     server = MagicMock()\n     server.name = name\n     session = MagicMock()\n     session.call_tool = call_tool_impl\n     server.session = session\n-    server._reconnect_event = MagicMock()\n-    server._ready = MagicMock()\n-    server._ready.is_set.return_value = True\n+\n+    ready_flag = threading.Event()\n+    ready_flag.set()\n+\n+    class _ReadyAdapter:\n+        def is_set(self):\n+            return ready_flag.is_set()\n+\n+        def clear(self):\n+            ready_flag.clear()\n+\n+        def set(self):\n+            ready_flag.set()\n+\n+    class _ReconnectAdapter:\n+        def __init__(self):\n+            self.set_calls = 0\n+\n+        def set(self):\n+            self.set_calls += 1\n+            old_session = server.session\n+            new_session = MagicMock()\n+            if old_session is not None:\n+                new_session.call_tool = old_session.call_tool\n+            elif call_tool_impl is not None:\n+                new_session.call_tool = call_tool_impl\n+            server.session = new_session\n+            ready_flag.set()\n+\n+        # MagicMock-compat shim: the dead-session half-open test asserts the\n+        # reconnect signal was delivered exactly once.\n+        def assert_called_once(self):\n+            assert self.set_calls == 1, f\"set() called {self.set_calls} times\"\n+\n+    server._reconnect_event = _ReconnectAdapter()\n+    server._ready = _ReadyAdapter()\n \n     mcp_tool_module._servers[name] = server\n     mcp_tool_module._server_error_counts.pop(name, None)","@@ -220,7 +255,7 @@ def _fake_monotonic():\n \n         # Clean \"reconnecting\" error, and a reconnect was actually signalled.\n         assert \"reconnect\" in parsed.get(\"error\", \"\").lower(), parsed\n-        server._reconnect_event.set.assert_called_once()\n+        server._reconnect_event.assert_called_once()\n     finally:\n         _cleanup(mcp_tool, \"srv\")\n "]},{"file":"tests/tools/test_mcp_register_wakes_stale.py","hunks":["@@ -0,0 +1,62 @@\n+\"\"\"New sessions must wake parked/stale cached MCP servers immediately.\n+\n+Regression for #50170: after a keepalive failure parks a server, its tools\n+are deregistered — so a NEW agent session starting up saw the tools silently\n+absent and had no way to trigger recovery until the next timed self-probe\n+(up to _PARKED_RETRY_INTERVAL later). register_mcp_servers now nudges any\n+cached entry whose session is None via _signal_reconnect.\n+\"\"\"\n+\n+import pytest\n+\n+\n[redacted:email]_isolate\n+def test_register_wakes_stale_cached_server(monkeypatch, tmp_path):\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+\n+    from tools import mcp_tool\n+\n+    woken: list[str] = []\n+\n+    class _Event:\n+        def __init__(self, name):\n+            self._name = name\n+\n+        def set(self):\n+            woken.append(self._name)\n+\n+    class _Stale:\n+        session = None\n+\n+        def __init__(self, name):\n+            self.name = name\n+            self._reconnect_event = _Event(name)\n+            self._registered_tool_names: list[str] = []\n+\n+    class _Alive:\n+        session = object()\n+\n+        def __init__(self, name):\n+            self.name = name\n+            self._reconnect_event = _Event(name)\n+            self._registered_tool_names = [f\"{name}__tool\"]\n+\n+    monkeypatch.setattr(mcp_tool, \"_MCP_AVAILABLE\", True)\n+    stale = _Stale(\"parked-srv\")\n+    alive = _Alive(\"healthy-srv\")\n+    monkeypatch.setitem(mcp_tool._servers, \"parked-srv\", stale)\n+    monkeypatch.setitem(mcp_tool._servers, \"healthy-srv\", alive)\n+\n+    try:\n+        result = mcp_tool.register_mcp_servers({\n+            \"parked-srv\": {\"url\": \"http://127.0.0.1:9/mcp\"},\n+            \"healthy-srv\": {\"url\": \"http://127.0.0.1:9/mcp\"},\n+        })\n+        # Both cached → no new connections attempted; existing names returned.\n+        assert \"healthy-srv__tool\" in result\n+        # The parked (session=None) entry got a reconnect nudge; the healthy\n+        # one was left alone.\n+        assert woken == [\"parked-srv\"]\n+    finally:\n+        mcp_tool._servers.pop(\"parked-srv\", None)\n+        mcp_tool._servers.pop(\"healthy-srv\", None)"]},{"file":"tests/tools/test_mcp_tool_session_expired.py","hunks":["@@ -112,23 +112,47 @@ def _install_stub_server(name: str = \"wpcom\"):\n \n     server = MagicMock()\n     server.name = name\n+\n+    ready_flag = threading.Event()\n+    ready_flag.set()\n+\n+    class _ReadyAdapter:\n+        def is_set(self):\n+            return ready_flag.is_set()\n+\n+        def clear(self):\n+            ready_flag.clear()\n+\n+        def set(self):\n+            ready_flag.set()\n+\n+    server._ready = _ReadyAdapter()\n+\n     # _reconnect_event is called via loop.call_soon_threadsafe(…set); use\n-    # a threading-safe substitute.\n+    # a threading-safe substitute.  The production reconnect path must not\n+    # treat the old stale session as fresh, so this test double swaps in a\n+    # distinct session object when reconnect is requested.\n     reconnect_flag = threading.Event()\n \n     class _EventAdapter:\n         def set(self):\n             reconnect_flag.set()\n+            old_session = server.session\n+            new_session = MagicMock()\n+            for method_name in (\n+                \"call_tool\",\n+                \"list_resources\",\n+                \"read_resource\",\n+                \"list_prompts\",\n+                \"get_prompt\",\n+            ):\n+                if hasattr(old_session, method_name):\n+                    setattr(new_session, method_name, getattr(old_session, method_name))\n+            server.session = new_session\n+            ready_flag.set()\n \n     server._reconnect_event = _EventAdapter()\n \n-    # Immediately \"ready\" — simulates a fast reconnect (_ready.is_set()\n-    # is polled by _handle_session_expired_and_retry until the timeout).\n-    ready_flag = threading.Event()\n-    ready_flag.set()\n-    server._ready = MagicMock()\n-    server._ready.is_set = ready_flag.is_set\n-\n     # session attr must be truthy for the handler's initial check\n     # (``if not server or not server.session``) and for the post-\n     # reconnect readiness probe (``srv.session is not None``).","@@ -188,6 +212,74 @@ async def _call_sequence(*a, **kw):\n         mcp_tool._server_error_counts.pop(\"wpcom\", None)\n \n \n+def test_session_expired_retry_waits_for_new_session(monkeypatch, tmp_path):\n+    \"\"\"Regression for long-lived HTTP/stream MCP sessions.\n+\n+    If the reconnect helper only checks ``_ready.is_set()`` and\n+    ``session is not None``, it can return immediately while ``session`` still\n+    points at the stale transport. The retry then hits the same dead session\n+    and the circuit breaker eventually reports the server as unreachable. The\n+    handler must wait for a distinct session object before retrying.\n+    \"\"\"\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+\n+    from tools import mcp_tool\n+    from tools.mcp_tool import _make_tool_handler\n+\n+    mcp_tool._ensure_mcp_loop()\n+    server = MagicMock()\n+    server.name = \"hindsight\"\n+    ready_flag = threading.Event()\n+    ready_flag.set()\n+\n+    class _ReadyAdapter:\n+        def is_set(self):\n+            return ready_flag.is_set()\n+\n+        def clear(self):\n+            ready_flag.clear()\n+\n+        def set(self):\n+            ready_flag.set()\n+\n+    old_session = MagicMock()\n+\n+    async def _old_call(*a, **kw):\n+        raise RuntimeError(\"Session terminated\")\n+\n+    old_session.call_tool = _old_call\n+    new_session = MagicMock()\n+\n+    async def _new_call(*a, **kw):\n+        result = MagicMock()\n+        result.isError = False\n+        result.content = [MagicMock(type=\"text\", text=\"bank ok\")]\n+        result.structuredContent = None\n+        return result\n+\n+    new_session.call_tool = _new_call\n+    server.session = old_session\n+    server._ready = _ReadyAdapter()\n+\n+    class _ReconnectAdapter:\n+        def set(self):\n+            server.session = new_session\n+            ready_flag.set()\n+\n+    server._reconnect_event = _ReconnectAdapter()\n+    mcp_tool._servers[\"hindsight\"] = server\n+    mcp_tool._server_error_counts.pop(\"hindsight\", None)\n+\n+    try:\n+        handler = _make_tool_handler(\"hindsight\", \"get_bank\", 10.0)\n+        parsed = json.loads(handler({}))\n+        assert parsed.get(\"result\") == \"bank ok\", parsed\n+        assert mcp_tool._server_error_counts.get(\"hindsight\", 0) == 0\n+    finally:\n+        mcp_tool._servers.pop(\"hindsight\", None)\n+        mcp_tool._server_error_counts.pop(\"hindsight\", None)\n+\n+\n def test_call_tool_handler_non_session_expired_error_falls_through(\n     monkeypatch, tmp_path\n ):"]},{"file":"tools/mcp_tool.py","hunks":["@@ -2539,12 +2539,12 @@ async def run(self, config: dict):\n                 # permanently kill an otherwise-healthy server.\n                 self._reconnect_retries = 0\n                 backoff = 1.0\n-                # Reset the session reference; _run_http/_run_stdio will\n-                # repopulate it on successful re-entry.\n+                # Reset the session reference and readiness; _run_http/_run_stdio\n+                # will repopulate both on successful re-entry.  Leaving\n+                # _ready set here lets handler-side recovery mistake the stale\n+                # pre-reconnect session for a fresh one and retry too early.\n+                self._ready.clear()\n                 self.session = None\n-                # Keep _ready set across reconnects so tool handlers can\n-                # still detect a transient in-flight state — it'll be\n-                # re-set after the fresh session initializes.\n                 continue\n             except asyncio.CancelledError:\n                 # Task was cancelled (shutdown, gateway restart, explicit","@@ -2816,6 +2816,89 @@ def _signal_reconnect(server: Any) -> bool:\n     return True\n \n \n+def _wait_for_server_session_ready(\n+    srv: \"MCPServerTask\",\n+    *,\n+    old_session: Any = None,\n+    timeout: float = 15.0,\n+) -> bool:\n+    \"\"\"Wait for an MCP server to expose a usable session.\n+\n+    Tool handlers run in normal worker threads while the MCP transport lives on\n+    the module's background asyncio loop. During a reconnect there is a short\n+    window where ``srv.session`` is ``None`` (or still points at the stale\n+    session until the lifecycle coroutine has left the transport context). A\n+    handler that blindly retries in that window can burn circuit-breaker strikes\n+    and return ``not connected`` even though the reconnect is already in\n+    progress.\n+\n+    When ``old_session`` is supplied, require the observed session object to be\n+    different so callers do not mistake the pre-reconnect, stale session for a\n+    fresh one.\n+    \"\"\"\n+    # Iteration-bounded rather than deadline-bounded: several tests (and the\n+    # circuit-breaker cooldown logic) monkeypatch time.monotonic to a frozen\n+    # clock, which would make a monotonic-deadline loop spin forever.\n+    poll_interval = 0.25\n+    iterations = max(1, int(max(float(timeout), 0.0) / poll_interval))\n+    for i in range(iterations):\n+        session = getattr(srv, \"session\", None)\n+        ready = getattr(srv, \"_ready\", None)\n+        is_ready = True\n+        if ready is not None and hasattr(ready, \"is_set\"):\n+            try:\n+                is_ready = bool(ready.is_set())\n+            except Exception:\n+                is_ready = True\n+        if session is not None and session is not old_session and is_ready:\n+            return True\n+        if i < iterations - 1:\n+            time.sleep(poll_interval)\n+    return False\n+\n+\n+def _signal_reconnect_and_wait(\n+    server_name: str,\n+    srv: \"MCPServerTask\",\n+    *,\n+    op_description: str,\n+    timeout: float = 15.0,\n+) -> bool:\n+    \"\"\"Ask a live MCP server task to rebuild its transport session.\n+\n+    The important detail is clearing ``_ready`` on the MCP event loop before\n+    setting ``_reconnect_event``. Older code left ``_ready`` set across\n+    reconnects, so the caller's readiness poll could return immediately and\n+    retry against the same dead HTTP/stream session. That was observed as\n+    repeated ``Session terminated`` / ``not connected`` / circuit-breaker\n+    failures in long-lived gateway sessions even though a fresh CLI process\n+    could connect successfully.\n+    \"\"\"\n+    loop = _mcp_loop\n+    if loop is None or not loop.is_running():\n+        return False\n+\n+    old_session = getattr(srv, \"session\", None)\n+\n+    def _request_reconnect() -> None:\n+        ready = getattr(srv, \"_ready\", None)\n+        if ready is not None and hasattr(ready, \"clear\"):\n+            ready.clear()\n+        reconnect_event = getattr(srv, \"_reconnect_event\", None)\n+        if reconnect_event is not None and hasattr(reconnect_event, \"set\"):\n+            reconnect_event.set()\n+\n+    logger.info(\n+        \"MCP server '%s': %s requesting transport reconnect\",\n+        server_name, op_description,\n+    )\n+    loop.call_soon_threadsafe(_request_reconnect)\n+    return _wait_for_server_session_ready(\n+        srv,\n+        old_session=old_session,\n+        timeout=timeout,\n+    )\n+\n # ---------------------------------------------------------------------------\n # Auth-failure detection helpers (Task 6 of MCP OAuth consolidation)\n # ---------------------------------------------------------------------------","@@ -2941,41 +3024,24 @@ async def _recover():\n     if recovered:\n         with _lock:\n             srv = _servers.get(server_name)\n+        reconnected = False\n         if srv is not None and hasattr(srv, \"_reconnect_event\"):\n-            loop = _mcp_loop\n-            if loop is not None and loop.is_running():\n-                loop.call_soon_threadsafe(srv._reconnect_event.set)\n-\n-                # Wait briefly for the session to come back ready. Bounded\n-                # so that a stuck reconnect falls through to the error\n-                # path rather than hanging the caller.  The async helper\n-                # runs on the MCP event loop via _run_on_mcp_loop so it\n-                # does NOT block the event loop during the poll interval.\n-                async def _await_ready() -> bool:\n-                    deadline = time.monotonic() + 15\n-                    while time.monotonic() < deadline:\n-                        if srv.session is not None and srv._ready.is_set():\n-                            return True\n-                        await asyncio.sleep(0.25)\n-                    return False\n-\n-                try:\n-                    _run_on_mcp_loop(_await_ready(), timeout=15)\n-                except Exception as exc:\n-                    logger.warning(\n-                        \"MCP OAuth '%s': ready poll failed: %s\",\n-                        server_name, exc,\n-                    )\n+            reconnected = _signal_reconnect_and_wait(\n+                server_name,\n+                srv,\n+                op_description=f\"{op_description} after OAuth recovery\",\n+                timeout=15,\n+            )\n \n-        # A successful OAuth recovery is independent evidence that the\n-        # server is viable again, so close the circuit breaker here —\n-        # not only on retry success. Without this, a reconnect\n-        # followed by a failing retry would leave the breaker pinned\n-        # above threshold forever (the retry-exception branch below\n-        # bumps the count again).  The post-reset retry still goes\n-        # through _bump_server_error on failure, so a genuinely broken\n-        # server will re-trip the breaker as normal.\n-        _reset_server_error(server_name)\n+        # A successful OAuth recovery + transport reconnect is independent\n+        # evidence that the server is viable again, so close the circuit\n+        # breaker here — not only on retry success. Without this, a reconnect\n+        # followed by a failing retry would leave the breaker pinned above\n+        # threshold forever. The post-reset retry still goes through\n+        # _bump_server_error on failure, so a genuinely broken server will\n+        # re-trip the breaker as normal.\n+        if reconnected:\n+            _reset_server_error(server_name)\n \n         try:\n             result = retry_call()","@@ -3104,15 +3170,12 @@ def _handle_session_expired_and_retry(\n \n     # Trigger the same reconnect mechanism the OAuth recovery path\n     # uses, then wait briefly for the new session to come back ready.\n-    loop.call_soon_threadsafe(srv._reconnect_event.set)\n-    deadline = time.monotonic() + 15\n-    ready = False\n-    while time.monotonic() < deadline:\n-        if srv.session is not None and srv._ready.is_set():\n-            ready = True\n-            break\n-        time.sleep(0.25)\n-    if not ready:\n+    if not _signal_reconnect_and_wait(\n+        server_name,\n+        srv,\n+        op_description=op_description,\n+        timeout=15,\n+    ):\n         logger.warning(\n             \"MCP server '%s': reconnect did not ready within 15s after \"\n             \"session-expired error; falling through to error response.\",","@@ -3561,27 +3624,38 @@ def _handler(args: dict, **kwargs) -> str:\n             }, ensure_ascii=False)\n \n         if not server.session:\n-            # No live session — the server task is reconnecting, or it has\n-            # exhausted its retry budget and parked (e.g. a dead stdio\n-            # subprocess). Probing here would write into a dead/absent\n-            # transport and re-arm the breaker forever (#16788). Instead,\n-            # ask the (always-present) server task to rebuild the transport\n-            # — which respawns a dead stdio subprocess — and return a clean\n-            # \"reconnecting\" error so the model backs off without burning\n-            # iterations. The breaker resets once the fresh session\n-            # initializes (_run_stdio/_run_http call _reset_server_error).\n-            _bump_server_error(server_name)\n-            if _signal_reconnect(server):\n+            # No live session. A reconnect may already be completing (the\n+            # transport swaps in a fresh session object asynchronously) —\n+            # wait briefly before treating this as a failure, so a\n+            # transient reconnect window doesn't burn a circuit-breaker\n+            # strike (#26892).\n+            if _wait_for_server_session_ready(\n+                server, timeout=min(5.0, float(tool_timeout or 5.0)),\n+            ):\n+                pass  # Fresh session arrived; proceed below.\n+            else:\n+                # Still down — the server task is reconnecting, or it has\n+                # exhausted its retry budget and parked (e.g. a dead stdio\n+                # subprocess). Probing here would write into a dead/absent\n+                # transport and re-arm the breaker forever (#16788). Instead,\n+                # ask the (always-present) server task to rebuild the\n+                # transport — which respawns a dead stdio subprocess — and\n+                # return a clean \"reconnecting\" error so the model backs off\n+                # without burning iterations. The breaker resets once the\n+                # fresh session initializes (_run_stdio/_run_http call\n+                # _reset_server_error).\n+                _bump_server_error(server_name)\n+                if _signal_reconnect(server):\n+                    return json.dumps({\n+                        \"error\": (\n+                            f\"MCP server '{server_name}' transport is down; \"\n+                            f\"reconnect requested. Do NOT retry this tool \"\n+                            f\"immediately — give it a few seconds to come back.\"\n+                        )\n+                    }, ensure_ascii=False)\n                 return json.dumps({\n-                    \"error\": (\n-                        f\"MCP server '{server_name}' transport is down; \"\n-                        f\"reconnect requested. Do NOT retry this tool \"\n-                        f\"immediately — give it a few seconds to come back.\"\n-                    )\n+                    \"error\": f\"MCP server '{server_name}' is not connected\"\n                 }, ensure_ascii=False)\n-            return json.dumps({\n-                \"error\": f\"MCP server '{server_name}' is not connected\"\n-            }, ensure_ascii=False)\n \n         async def _call():\n             async with server._rpc_lock:","@@ -4529,6 +4603,16 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:\n             for k, v in servers.items()\n             if k not in _servers and _parse_boolish(v.get(\"enabled\", True), default=True)\n         }\n+        # Cached entries with no live session are parked or mid-reconnect.\n+        # Their tools are deregistered, so nothing else can reach\n+        # _signal_reconnect — without this nudge a new session silently\n+        # waits up to _PARKED_RETRY_INTERVAL for the next self-probe\n+        # (#50170). Wake them now so their tools come back promptly.\n+        stale_cached = [\n+            _servers[k]\n+            for k in servers\n+            if k in _servers and getattr(_servers[k], \"session\", None) is None\n+        ]\n         _server_connecting.update(new_servers)\n         for srv_name in new_servers:\n             _server_connect_errors.pop(srv_name, None)","@@ -4539,6 +4623,9 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:\n             else:\n                 _parallel_safe_servers.discard(sanitize_mcp_name_component(srv_name))\n \n+    for srv in stale_cached:\n+        _signal_reconnect(srv)\n+\n     if not new_servers:\n         return _existing_tool_names()\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}