From b7adc9fc5953f6119d1c3e2d700d7925f8957e05 Mon Sep 17 00:00:00 2001 From: CodeKeanu <85296798+CodeKeanu@users.noreply.github.com> Date: Fri, 12 Dec 2025 06:03:53 +0000 Subject: [PATCH 1/2] feat: add family sharing integration (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IFamilyGroupsService endpoint module with 2 tools: - get_family_group: Get family group membership, members, and roles - get_shared_library_apps: Get games available through family sharing - Include 14 unit tests with mocked responses - Update README (30 -> 32 tools) and CHANGELOG 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 13 + README.md | 11 +- src/steam_mcp/endpoints/family_groups.py | 253 ++++++++++++++++++ tests/test_family_groups.py | 314 +++++++++++++++++++++++ 4 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 src/steam_mcp/endpoints/family_groups.py create mode 100644 tests/test_family_groups.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bc94862..3cf3b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Family Sharing Integration** (`family_groups.py`) + - `get_family_group` - Get family group membership information + - Returns family members, roles (Adult/Child/Member), and cooldown status + - Shows available slots in the family group + - `get_shared_library_apps` - Get games available through family sharing + - Shows shared games grouped by owner + - Indicates exclusion reasons (not shareable, already owned, etc.) + - Optional `include_own` parameter to include owned apps + +### Changed +- Tool count increased from 30 to 32 + ## [v0.8.0] - 2025-12-11 ### Added diff --git a/README.md b/README.md index 787407f..1bbaab0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Once set up, you can ask Claude things like: - "Show my pending trade offers" - "What's the current price for an AK-47 Redline?" -The server includes 30 tools covering player profiles, game libraries, achievements, stats, reviews, wishlists, news, community guides, and trading/market data. +The server includes 32 tools covering player profiles, game libraries, achievements, stats, reviews, wishlists, news, community guides, trading/market data, and family sharing. --- @@ -179,7 +179,7 @@ No registration needed - just drop in the file and restart. --- -## Available Tools (30 total) +## Available Tools (32 total) ### Player Profiles (ISteamUser) - 6 tools @@ -253,6 +253,13 @@ No registration needed - just drop in the file and restart. | `get_market_listings` | Check current market prices for items | | `check_market_eligibility` | Check if a user can use the Steam Market | +### Family Sharing (IFamilyGroupsService) - 2 tools + +| Tool | What it does | +|------|--------------| +| `get_family_group` | Get family group membership, members, and roles | +| `get_shared_library_apps` | Get games available through family sharing | + --- ## Configuration Reference diff --git a/src/steam_mcp/endpoints/family_groups.py b/src/steam_mcp/endpoints/family_groups.py new file mode 100644 index 0000000..057e20d --- /dev/null +++ b/src/steam_mcp/endpoints/family_groups.py @@ -0,0 +1,253 @@ +"""IFamilyGroupsService API endpoints. + +This module provides MCP tools for the IFamilyGroupsService Steam API interface, +which handles family group membership and shared library information. + +Reference: https://partner.steamgames.com/doc/webapi/IFamilyGroupsService +""" + +from typing import Any + +from steam_mcp.endpoints.base import BaseEndpoint, endpoint +from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError + + +class IFamilyGroupsService(BaseEndpoint): + """IFamilyGroupsService API endpoints for family sharing features.""" + + async def _resolve_steam_id(self, steam_id: str) -> str: + """ + Resolve steam_id, handling 'me'/'my' shortcuts. + + Returns: + Normalized SteamID64 or error message starting with "Error" + """ + steam_id_lower = steam_id.strip().lower() + if steam_id_lower in ("me", "my", "myself", "mine"): + if not self.client.owner_steam_id: + return ( + "Error: No owner Steam ID configured. " + "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts." + ) + return self.client.owner_steam_id + + try: + return await normalize_steam_id(steam_id, self.client) + except SteamIDError as e: + return f"Error resolving Steam ID: {e}" + + @endpoint( + name="get_family_group", + description=( + "Get family group membership information for a Steam user. " + "Returns family members, their roles, and shared library status. " + "Note: Only works for users who are part of a Steam family group." + ), + params={ + "steam_id": { + "type": "string", + "description": ( + "Steam ID in any format. Use 'me' or 'my' to query your own profile " + "(requires STEAM_USER_ID to be configured)." + ), + "required": True, + }, + }, + ) + async def get_family_group(self, steam_id: str) -> str: + """Get family group information for a Steam user.""" + normalized_id = await self._resolve_steam_id(steam_id) + if normalized_id.startswith("Error"): + return normalized_id + + try: + result = await self.client.get( + "IFamilyGroupsService", + "GetFamilyGroup", + version=1, + params={"steamid": normalized_id}, + ) + except Exception as e: + error_msg = str(e).lower() + if "401" in error_msg or "forbidden" in error_msg or "403" in error_msg: + return ( + f"Could not access family group for Steam ID {normalized_id}.\n" + "This user may not be in a family group, or their profile is private." + ) + raise + + response = result.get("response", {}) + family_group = response.get("family_group", {}) + + if not family_group: + return ( + f"No family group found for Steam ID {normalized_id}.\n" + "This user is not a member of any Steam family group." + ) + + # Extract family information + family_groupid = family_group.get("family_groupid", "Unknown") + name = family_group.get("name", "Unnamed Family") + members = family_group.get("members", []) + + output = [ + f"Family Group: {name}", + f"Family Group ID: {family_groupid}", + f"Total Members: {len(members)}", + "", + "Members:", + ] + + for member in members: + member_steamid = member.get("steamid", "Unknown") + role = member.get("role", 0) + + # Role mapping based on Steam's family sharing roles + role_name = { + 0: "Member", + 1: "Adult", + 2: "Child", + }.get(role, f"Unknown ({role})") + + # Check cooldown status + cooldown_seconds = member.get("cooldown_seconds_remaining", 0) + cooldown_str = "" + if cooldown_seconds > 0: + hours = cooldown_seconds // 3600 + minutes = (cooldown_seconds % 3600) // 60 + cooldown_str = f" (Cooldown: {hours}h {minutes}m remaining)" + + output.append(f" - {member_steamid}: {role_name}{cooldown_str}") + + # Include slot information if available + free_spots = family_group.get("free_spots", None) + if free_spots is not None: + output.append("") + output.append(f"Available Slots: {free_spots}") + + return "\n".join(output) + + @endpoint( + name="get_shared_library_apps", + description=( + "Get games available through family sharing for a Steam user. " + "Returns shared games with owner information and availability status. " + "Note: Only works for users who are part of a Steam family group." + ), + params={ + "steam_id": { + "type": "string", + "description": ( + "Steam ID in any format. Use 'me' or 'my' to query your own profile " + "(requires STEAM_USER_ID to be configured)." + ), + "required": True, + }, + "include_own": { + "type": "boolean", + "description": "Include apps the user owns directly (not just shared). Default is false.", + "required": False, + "default": False, + }, + }, + ) + async def get_shared_library_apps( + self, + steam_id: str, + include_own: bool = False, + ) -> str: + """Get shared library apps for a Steam user.""" + normalized_id = await self._resolve_steam_id(steam_id) + if normalized_id.startswith("Error"): + return normalized_id + + try: + result = await self.client.get( + "IFamilyGroupsService", + "GetSharedLibraryApps", + version=1, + params={ + "steamid": normalized_id, + "include_own": include_own, + }, + ) + except Exception as e: + error_msg = str(e).lower() + if "401" in error_msg or "forbidden" in error_msg or "403" in error_msg: + return ( + f"Could not access shared library for Steam ID {normalized_id}.\n" + "This user may not be in a family group, or their profile is private." + ) + raise + + response = result.get("response", {}) + apps = response.get("apps", []) + + if not apps: + suffix = " (including owned apps)" if include_own else "" + return ( + f"No shared library apps found for Steam ID {normalized_id}{suffix}.\n" + "This user may not be in a family group, or no games are shared." + ) + + # Group apps by owner + apps_by_owner: dict[str, list[dict[str, Any]]] = {} + for app in apps: + owner_steamids = app.get("owner_steamids", []) + app_info = { + "appid": app.get("appid"), + "name": app.get("name", f"App {app.get('appid', 'Unknown')}"), + "rt_time_acquired": app.get("rt_time_acquired", 0), + "exclude_reason": app.get("exclude_reason", 0), + } + + for owner_id in owner_steamids: + if owner_id not in apps_by_owner: + apps_by_owner[owner_id] = [] + apps_by_owner[owner_id].append(app_info) + + total_apps = len(apps) + output = [ + f"Shared Library for {normalized_id}", + f"Total Shared Apps: {total_apps}", + "", + ] + + # Sort owners by number of apps shared + sorted_owners = sorted( + apps_by_owner.items(), + key=lambda x: len(x[1]), + reverse=True, + ) + + for owner_id, owner_apps in sorted_owners: + output.append(f"From {owner_id} ({len(owner_apps)} apps):") + + # Sort apps by name + owner_apps.sort(key=lambda a: a.get("name", "").lower()) + + # Show first 10 apps per owner + for app in owner_apps[:10]: + appid = app.get("appid", "?") + name = app.get("name", "Unknown") + + # Check exclusion status + exclude_reason = app.get("exclude_reason", 0) + status = "" + if exclude_reason > 0: + # Common exclusion reasons + reason_map = { + 1: " [Excluded: Not shareable]", + 2: " [Excluded: Free game]", + 3: " [Excluded: Region lock]", + 4: " [Excluded: Already owned]", + } + status = reason_map.get(exclude_reason, f" [Excluded: {exclude_reason}]") + + output.append(f" [{appid}] {name}{status}") + + if len(owner_apps) > 10: + output.append(f" ... and {len(owner_apps) - 10} more") + output.append("") + + return "\n".join(output).rstrip() diff --git a/tests/test_family_groups.py b/tests/test_family_groups.py new file mode 100644 index 0000000..9523fa0 --- /dev/null +++ b/tests/test_family_groups.py @@ -0,0 +1,314 @@ +"""Tests for IFamilyGroupsService endpoint - family sharing features.""" + +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +from steam_mcp.endpoints.family_groups import IFamilyGroupsService +from steam_mcp.utils.steam_id import SteamIDError + + +@pytest.fixture +def mock_client(): + """Create mock Steam client.""" + client = MagicMock() + client.owner_steam_id = None + client.get = AsyncMock() + return client + + +@pytest.fixture +def family_service(mock_client): + """Create IFamilyGroupsService instance with mock client.""" + return IFamilyGroupsService(mock_client) + + +class TestGetFamilyGroup: + """Tests for get_family_group endpoint.""" + + @pytest.mark.asyncio + async def test_returns_family_group_info(self, family_service, mock_client): + """Should return formatted family group information.""" + mock_client.get.return_value = { + "response": { + "family_group": { + "family_groupid": "12345", + "name": "Test Family", + "members": [ + {"steamid": "76561198000000001", "role": 1}, + {"steamid": "76561198000000002", "role": 2}, + ], + "free_spots": 3, + } + } + } + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_family_group(steam_id="76561198000000001") + + assert "Test Family" in result + assert "12345" in result + assert "Total Members: 2" in result + assert "76561198000000001" in result + assert "Adult" in result + assert "Child" in result + assert "Available Slots: 3" in result + + @pytest.mark.asyncio + async def test_no_family_group_returns_message(self, family_service, mock_client): + """Should return message when user has no family group.""" + mock_client.get.return_value = {"response": {}} + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_family_group(steam_id="76561198000000001") + + assert "No family group found" in result + assert "not a member" in result + + @pytest.mark.asyncio + async def test_handles_private_profile(self, family_service, mock_client): + """Should handle 401/403 errors gracefully.""" + mock_client.get.side_effect = Exception("401 Unauthorized") + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_family_group(steam_id="76561198000000001") + + assert "Could not access" in result + assert "private" in result.lower() + + @pytest.mark.asyncio + async def test_invalid_steam_id_returns_error(self, family_service): + """Invalid Steam ID should return error.""" + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + side_effect=SteamIDError("Invalid Steam ID"), + ): + result = await family_service.get_family_group(steam_id="invalid_id") + + assert "Error" in result + + @pytest.mark.asyncio + async def test_me_shortcut_without_config_returns_error(self, family_service, mock_client): + """Using 'me' without STEAM_USER_ID configured should return error.""" + mock_client.owner_steam_id = None + + result = await family_service.get_family_group(steam_id="me") + + assert "Error" in result + assert "STEAM_USER_ID" in result + + @pytest.mark.asyncio + async def test_me_shortcut_with_config_works(self, family_service, mock_client): + """Using 'me' with STEAM_USER_ID configured should work.""" + mock_client.owner_steam_id = "76561198000000001" + mock_client.get.return_value = { + "response": { + "family_group": { + "family_groupid": "12345", + "name": "My Family", + "members": [{"steamid": "76561198000000001", "role": 1}], + } + } + } + + result = await family_service.get_family_group(steam_id="my") + + assert "My Family" in result + + @pytest.mark.asyncio + async def test_cooldown_displayed(self, family_service, mock_client): + """Should display cooldown time for members.""" + mock_client.get.return_value = { + "response": { + "family_group": { + "family_groupid": "12345", + "name": "Test Family", + "members": [ + { + "steamid": "76561198000000001", + "role": 1, + "cooldown_seconds_remaining": 7200, # 2 hours + }, + ], + } + } + } + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_family_group(steam_id="76561198000000001") + + assert "Cooldown: 2h 0m" in result + + +class TestGetSharedLibraryApps: + """Tests for get_shared_library_apps endpoint.""" + + @pytest.mark.asyncio + async def test_returns_shared_apps(self, family_service, mock_client): + """Should return formatted shared library apps.""" + mock_client.get.return_value = { + "response": { + "apps": [ + { + "appid": 440, + "name": "Team Fortress 2", + "owner_steamids": ["76561198000000002"], + "exclude_reason": 0, + }, + { + "appid": 570, + "name": "Dota 2", + "owner_steamids": ["76561198000000002"], + "exclude_reason": 0, + }, + ] + } + } + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_shared_library_apps(steam_id="76561198000000001") + + assert "Shared Library" in result + assert "Total Shared Apps: 2" in result + assert "Team Fortress 2" in result + assert "Dota 2" in result + assert "76561198000000002" in result + + @pytest.mark.asyncio + async def test_no_shared_apps_returns_message(self, family_service, mock_client): + """Should return message when no shared apps.""" + mock_client.get.return_value = {"response": {"apps": []}} + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_shared_library_apps(steam_id="76561198000000001") + + assert "No shared library apps found" in result + + @pytest.mark.asyncio + async def test_handles_private_profile(self, family_service, mock_client): + """Should handle 401/403 errors gracefully.""" + mock_client.get.side_effect = Exception("403 Forbidden") + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_shared_library_apps(steam_id="76561198000000001") + + assert "Could not access" in result + + @pytest.mark.asyncio + async def test_exclusion_reason_displayed(self, family_service, mock_client): + """Should display exclusion reason for apps.""" + mock_client.get.return_value = { + "response": { + "apps": [ + { + "appid": 440, + "name": "Some Game", + "owner_steamids": ["76561198000000002"], + "exclude_reason": 4, # Already owned + }, + ] + } + } + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_shared_library_apps(steam_id="76561198000000001") + + assert "Already owned" in result + + @pytest.mark.asyncio + async def test_include_own_parameter(self, family_service, mock_client): + """Should pass include_own parameter to API.""" + mock_client.get.return_value = {"response": {"apps": []}} + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + await family_service.get_shared_library_apps( + steam_id="76561198000000001", + include_own=True, + ) + + mock_client.get.assert_called_once() + call_args = mock_client.get.call_args + assert call_args.kwargs["params"]["include_own"] is True + + @pytest.mark.asyncio + async def test_multiple_owners_grouped(self, family_service, mock_client): + """Should group apps by owner.""" + mock_client.get.return_value = { + "response": { + "apps": [ + { + "appid": 440, + "name": "Game A", + "owner_steamids": ["76561198000000002"], + }, + { + "appid": 570, + "name": "Game B", + "owner_steamids": ["76561198000000003"], + }, + { + "appid": 730, + "name": "Game C", + "owner_steamids": ["76561198000000002"], + }, + ] + } + } + + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + return_value="76561198000000001", + ): + result = await family_service.get_shared_library_apps(steam_id="76561198000000001") + + # Owner with 2 apps should appear first + assert result.index("76561198000000002") < result.index("76561198000000003") + + @pytest.mark.asyncio + async def test_invalid_steam_id_returns_error(self, family_service): + """Invalid Steam ID should return error.""" + with patch( + "steam_mcp.endpoints.family_groups.normalize_steam_id", + new_callable=AsyncMock, + side_effect=SteamIDError("Invalid Steam ID"), + ): + result = await family_service.get_shared_library_apps(steam_id="invalid_id") + + assert "Error" in result From a552f026991473cab711af8b56653e8e5ff14fa6 Mon Sep 17 00:00:00 2001 From: CodeKeanu <85296798+CodeKeanu@users.noreply.github.com> Date: Fri, 12 Dec 2025 09:02:05 +0000 Subject: [PATCH 2/2] refactor: extract _resolve_steam_id to BaseEndpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the shared _resolve_steam_id method from individual endpoint classes to BaseEndpoint, reducing code duplication across 5 files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/steam_mcp/endpoints/base.py | 29 ++++++++++++++++++++++++ src/steam_mcp/endpoints/family_groups.py | 22 ------------------ tests/test_family_groups.py | 26 ++++++++++----------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/steam_mcp/endpoints/base.py b/src/steam_mcp/endpoints/base.py index c0b53e9..439a983 100644 --- a/src/steam_mcp/endpoints/base.py +++ b/src/steam_mcp/endpoints/base.py @@ -41,6 +41,7 @@ async def get_player_summary(self, steam_id: str) -> str: from mcp.types import Tool, TextContent from steam_mcp.client import SteamClient +from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError logger = logging.getLogger(__name__) @@ -347,6 +348,34 @@ def __init__(self, client: SteamClient) -> None: """ self.client = client + async def _resolve_steam_id(self, steam_id: str) -> str: + """ + Resolve steam_id, handling 'me'/'my' shortcuts. + + This is a shared utility method for all endpoints that work with Steam IDs. + It handles the common pattern of accepting 'me'/'my' shortcuts and normalizing + various Steam ID formats. + + Args: + steam_id: Steam ID in any format, or 'me'/'my' for owner's profile + + Returns: + Normalized SteamID64, or error message starting with "Error" + """ + steam_id_lower = steam_id.strip().lower() + if steam_id_lower in ("me", "my", "myself", "mine"): + if not self.client.owner_steam_id: + return ( + "Error: No owner Steam ID configured. " + "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts." + ) + return self.client.owner_steam_id + + try: + return await normalize_steam_id(steam_id, self.client) + except SteamIDError as e: + return f"Error resolving Steam ID: {e}" + @classmethod def get_tools(cls) -> list[Tool]: """Get MCP Tool definitions for this endpoint class.""" diff --git a/src/steam_mcp/endpoints/family_groups.py b/src/steam_mcp/endpoints/family_groups.py index 057e20d..23a3938 100644 --- a/src/steam_mcp/endpoints/family_groups.py +++ b/src/steam_mcp/endpoints/family_groups.py @@ -9,33 +9,11 @@ from typing import Any from steam_mcp.endpoints.base import BaseEndpoint, endpoint -from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError class IFamilyGroupsService(BaseEndpoint): """IFamilyGroupsService API endpoints for family sharing features.""" - async def _resolve_steam_id(self, steam_id: str) -> str: - """ - Resolve steam_id, handling 'me'/'my' shortcuts. - - Returns: - Normalized SteamID64 or error message starting with "Error" - """ - steam_id_lower = steam_id.strip().lower() - if steam_id_lower in ("me", "my", "myself", "mine"): - if not self.client.owner_steam_id: - return ( - "Error: No owner Steam ID configured. " - "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts." - ) - return self.client.owner_steam_id - - try: - return await normalize_steam_id(steam_id, self.client) - except SteamIDError as e: - return f"Error resolving Steam ID: {e}" - @endpoint( name="get_family_group", description=( diff --git a/tests/test_family_groups.py b/tests/test_family_groups.py index 9523fa0..8f7aa42 100644 --- a/tests/test_family_groups.py +++ b/tests/test_family_groups.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from steam_mcp.endpoints.family_groups import IFamilyGroupsService -from steam_mcp.utils.steam_id import SteamIDError +from steam_mcp.endpoints.base import SteamIDError @pytest.fixture @@ -43,7 +43,7 @@ async def test_returns_family_group_info(self, family_service, mock_client): } with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -63,7 +63,7 @@ async def test_no_family_group_returns_message(self, family_service, mock_client mock_client.get.return_value = {"response": {}} with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -78,7 +78,7 @@ async def test_handles_private_profile(self, family_service, mock_client): mock_client.get.side_effect = Exception("401 Unauthorized") with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -91,7 +91,7 @@ async def test_handles_private_profile(self, family_service, mock_client): async def test_invalid_steam_id_returns_error(self, family_service): """Invalid Steam ID should return error.""" with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, side_effect=SteamIDError("Invalid Steam ID"), ): @@ -147,7 +147,7 @@ async def test_cooldown_displayed(self, family_service, mock_client): } with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -182,7 +182,7 @@ async def test_returns_shared_apps(self, family_service, mock_client): } with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -200,7 +200,7 @@ async def test_no_shared_apps_returns_message(self, family_service, mock_client) mock_client.get.return_value = {"response": {"apps": []}} with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -214,7 +214,7 @@ async def test_handles_private_profile(self, family_service, mock_client): mock_client.get.side_effect = Exception("403 Forbidden") with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -239,7 +239,7 @@ async def test_exclusion_reason_displayed(self, family_service, mock_client): } with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -253,7 +253,7 @@ async def test_include_own_parameter(self, family_service, mock_client): mock_client.get.return_value = {"response": {"apps": []}} with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -292,7 +292,7 @@ async def test_multiple_owners_grouped(self, family_service, mock_client): } with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, return_value="76561198000000001", ): @@ -305,7 +305,7 @@ async def test_multiple_owners_grouped(self, family_service, mock_client): async def test_invalid_steam_id_returns_error(self, family_service): """Invalid Steam ID should return error.""" with patch( - "steam_mcp.endpoints.family_groups.normalize_steam_id", + "steam_mcp.endpoints.base.normalize_steam_id", new_callable=AsyncMock, side_effect=SteamIDError("Invalid Steam ID"), ):