fix(tools-iam): the MCP IAM tool listed users over a verb being deleted

_users() called /v1/iam/get-users and then did

    users = data if isinstance(data, list) else []

The verb answered a bare JSON list. The native /v1/iam/users answers
{"users": [...]}. So swapping only the path would have left that isinstance check
False and reported ZERO users for a healthy org — an empty result, not an error,
surfaced through an MCP tool an agent then reasons over. Both halves change here.

The dict branch also accepts "data" so a server still on the legacy envelope
keeps working during rollout; it goes when the compat surface does.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-07-28 15:41:54 -07:00
co-authored by Hanzo Dev
parent f46bf7fec4
commit ecb1a3d8e8
2 changed files with 12 additions and 4 deletions
@@ -253,8 +253,16 @@ class IAMTool(BaseTool):
async def _users(self, owner: str | None) -> str:
owner = owner or "hanzo"
data = await _iam_get("get-users", params={"owner": owner})
users = data if isinstance(data, list) else []
# Native noun route, not the legacy get-users verb — the server is
# removing those. It answers {"users": [...]}, where the verb answered a
# bare list, so BOTH halves change together: swapping only the path would
# leave isinstance(data, list) False and report zero users for a healthy
# org, which reads as "no users" rather than as an error.
data = await _iam_get("users", params={"owner": owner})
if isinstance(data, dict):
users = data.get("users") or data.get("data") or []
else:
users = data if isinstance(data, list) else []
result = []
for u in users:
result.append({
+2 -2
View File
@@ -15,8 +15,8 @@ from hanzo_tools.iam.iam_tool import IAMTool, _iam_url, IAM_BASE_URL
def test_iam_url_uses_v1_iam_not_api():
url = _iam_url("get-users")
assert url == f"{IAM_BASE_URL}/v1/iam/get-users"
url = _iam_url("users")
assert url == f"{IAM_BASE_URL}/v1/iam/users"
assert "/api/" not in url