Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Steam Cloud Integration** (`cloud_saves.py`)
- `list_cloud_files` - List cloud save files for a specific game
- Shows file names, sizes, and modification timestamps
- Works with 'me'/'my' Steam ID shortcuts
- `get_cloud_quota` - Get cloud storage usage and limits
- Shows total quota, used space, available space
- Visual usage bar representation

### Changed
- Tool count increased from 36 to 38

## [v0.9.0] - 2025-12-12

### Added
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 36 tools covering player profiles, game libraries, achievements, stats, reviews, wishlists, news, community guides, trading/market data, family sharing, and Steam Workshop.
The server includes 38 tools covering player profiles, game libraries, achievements, stats, reviews, wishlists, news, community guides, trading/market data, family sharing, Steam Workshop, and cloud saves.

---

Expand Down Expand Up @@ -179,7 +179,7 @@ No registration needed - just drop in the file and restart.

---

## Available Tools (36 total)
## Available Tools (38 total)

### Player Profiles (ISteamUser) - 6 tools

Expand Down Expand Up @@ -269,6 +269,13 @@ No registration needed - just drop in the file and restart.
| `get_workshop_item_details` | Get full details on a Workshop item (description, subscribers, dependencies) |
| `get_workshop_collection` | Get items from a Workshop collection |

### Cloud Saves (ISteamRemoteStorage) - 2 tools

| Tool | What it does |
|------|--------------|
| `list_cloud_files` | List cloud save files for a game (names, sizes, timestamps) |
| `get_cloud_quota` | Get cloud storage usage and limits |

---

## Configuration Reference
Expand Down
184 changes: 184 additions & 0 deletions src/steam_mcp/endpoints/cloud_saves.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""ISteamRemoteStorage and ICloudService API endpoints.

This module provides MCP tools for Steam Cloud save management,
including listing cloud files and checking storage quotas.

Reference: https://partner.steamgames.com/doc/webapi/ISteamRemoteStorage
"""

from steam_mcp.endpoints.base import BaseEndpoint, endpoint


class ISteamRemoteStorage(BaseEndpoint):
"""Steam Cloud save management endpoints."""

@endpoint(
name="list_cloud_files",
description=(
"List cloud save files for a specific game. "
"Returns file names, sizes, and timestamps. "
"Note: Requires user authentication context."
),
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,
},
"app_id": {
"type": "integer",
"description": "Game app ID to list cloud files for",
"required": True,
},
},
)
async def list_cloud_files(self, steam_id: str, app_id: int) -> str:
"""List cloud save files for a game."""
normalized_id = await self._resolve_steam_id(steam_id)
if normalized_id.startswith("Error"):
return normalized_id

try:
result = await self.client.get(
"ISteamRemoteStorage",
"EnumerateUserFiles",
version=1,
params={
"steamid": normalized_id,
"appid": app_id,
},
)
except Exception as e:
error_str = str(e).lower()
if "401" in error_str or "403" in error_str or "unauthorized" in error_str:
return (
f"Could not access cloud files for Steam ID {normalized_id}.\n"
"This may require owner authentication or the profile is private."
)
return f"Error fetching cloud files: {e}"

response = result.get("response", {})
files = response.get("files", [])
total_count = response.get("totalcount", len(files))

if not files:
return (
f"No cloud files found for app {app_id} (Steam ID: {normalized_id}).\n"
"This game may not use Steam Cloud, or no saves exist yet."
)

# Calculate total size
total_bytes = sum(f.get("file_size", 0) for f in files)

output = [
f"Cloud Files for App {app_id}",
f"Steam ID: {normalized_id}",
f"Total Files: {total_count}",
f"Total Size: {self._format_bytes(total_bytes)}",
"",
]

for f in files:
filename = f.get("filename", "Unknown")
size = f.get("file_size", 0)
timestamp = f.get("timestamp", 0)

# Format timestamp
if timestamp:
from datetime import datetime
dt = datetime.fromtimestamp(timestamp)
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
else:
time_str = "Unknown"

output.append(
f" {filename}\n"
f" Size: {self._format_bytes(size)} | Modified: {time_str}"
)

return "\n".join(output)

@endpoint(
name="get_cloud_quota",
description=(
"Get Steam Cloud storage usage and limits for a user. "
"Shows total quota, used space, and available space."
),
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_cloud_quota(self, steam_id: str) -> str:
"""Get cloud storage quota for a user."""
normalized_id = await self._resolve_steam_id(steam_id)
if normalized_id.startswith("Error"):
return normalized_id

try:
result = await self.client.get(
"ICloudService",
"GetUploadServerInfo",
version=1,
params={"steamid": normalized_id},
)
except Exception as e:
error_str = str(e).lower()
if "401" in error_str or "403" in error_str or "unauthorized" in error_str:
return (
f"Could not access cloud quota for Steam ID {normalized_id}.\n"
"This may require owner authentication or the profile is private."
)
return f"Error fetching cloud quota: {e}"

response = result.get("response", {})

# Extract quota information
total_bytes = response.get("quota_bytes", 0)
used_bytes = response.get("used_bytes", 0)

if total_bytes == 0 and used_bytes == 0:
return (
f"No cloud quota information available for Steam ID {normalized_id}.\n"
"This may indicate restricted access or no cloud usage."
)

available_bytes = total_bytes - used_bytes
usage_percent = (used_bytes / total_bytes * 100) if total_bytes > 0 else 0

output = [
f"Steam Cloud Storage for {normalized_id}",
"",
f"Total Quota: {self._format_bytes(total_bytes)}",
f"Used Space: {self._format_bytes(used_bytes)} ({usage_percent:.1f}%)",
f"Available: {self._format_bytes(available_bytes)}",
]

# Add usage bar visualization
bar_length = 20
filled = int(bar_length * usage_percent / 100)
bar = "[" + "=" * filled + "-" * (bar_length - filled) + "]"
output.append(f"\n{bar} {usage_percent:.1f}% used")

return "\n".join(output)

@staticmethod
def _format_bytes(size: int) -> str:
"""Format bytes into human-readable string."""
if size < 1024:
return f"{size} B"
elif size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
elif size < 1024 * 1024 * 1024:
return f"{size / (1024 * 1024):.1f} MB"
else:
return f"{size / (1024 * 1024 * 1024):.2f} GB"
Loading