-
Notifications
You must be signed in to change notification settings - Fork 0
MCP
Expose AsyncSteamClient as Model Context Protocol tools an LLM agent can call.
New in
pysteam-client1.6. MCP SDK is imported lazily — installingsteam.mcpdoesn't force any framework in.
-
steam.mcp.tools— the raw definitions. Framework-agnostic (Pydantic input/output schemas + async handler functions). Use these directly if you're building your own MCP server on a framework the built-in adapter doesn't cover. -
steam.mcp.server— a thin adapter for FastMCP-compatible servers. Works with the officialmcpSDK (mcp.server.fastmcp.FastMCP) and the standalonefastmcppackage — both expose the same@server.tool()decorator.
from mcp.server.fastmcp import FastMCP
from steam.aio import AsyncSteamClient
from steam.mcp import register_steam_tools
server = FastMCP("Steam")
client = AsyncSteamClient()
await client.start()
await client.anonymous_login()
register_steam_tools(server, client)
# server.run() as usual (stdio, http, whatever transport you configured)That's it. Three MCP tools are now available to any LLM connected to server:
| tool | what it does |
|---|---|
steam.status |
Session health snapshot (connected, logged_on, cell_id, reconnect state). No network. |
steam.get_product_info |
Fetch Steam metadata for one or more app IDs / package IDs. |
steam.send_um |
Arbitrary Unified Messages RPC — for anything beyond product info (auctions, workshop, chat, inventory). |
Every tool has a Pydantic input and output model. The LLM gets a strongly-typed contract; you get validation for free.
class SteamStatusInput(BaseModel):
# no parameters
pass
class SteamStatusOutput(BaseModel):
healthy: bool # connected AND logged_on
connected: bool
logged_on: bool
username: str | None
cell_id: int
reconnect_state: str # "idle" | "reconnecting" | "failed"
reconnect_attempts: int
last_activity_at: float | None
uptime_seconds: float | NoneCheap — reads local state only.
class GetProductInfoInput(BaseModel):
apps: list[int] = []
packages: list[int] = []
meta_data_only: bool = False
timeout_seconds: float = 15.0 # bounded [0.5, 60]
class GetProductInfoOutput(BaseModel):
apps: dict[int, dict]
packages: dict[int, dict]Rejects empty input (ValueError) — no burning a CM call for nothing.
class SendUmInput(BaseModel):
method_name: str # e.g. "Player.GetGameBadgeLevels#1"
params: dict[str, Any] = {}
timeout_seconds: float = 10.0
class SendUmOutput(BaseModel):
ok: bool
body: dict[str, Any]Tool handlers raise typed errors that map to stable MCP error codes:
AsyncSteamError subclass |
MCP error code |
|---|---|
SteamNotStartedError |
steam_not_started |
SteamClosedError |
steam_closed |
SteamLoginError |
steam_login |
SteamReconnectError |
steam_reconnect |
SteamRPCTimeoutError |
steam_rpctimeout |
| Anything else | internal_error |
The MCP client sees {"error": {"code": "steam_login", "message": "..."}} — stable enough to pattern-match on without regex-parsing the message.
register_steam_tools accepts an optional bindings= iterable for filtering / extending:
from steam.mcp import build_steam_tool_bindings, register_steam_tools
# Drop the low-level send_um for a locked-down deployment
safe = [b for b in build_steam_tool_bindings() if b.name != "steam.send_um"]
register_steam_tools(server, client, bindings=safe)If one MCP server exposes multiple Steam accounts, prefix the tools so they don't collide:
register_steam_tools(server, alice, prefix="alice.")
register_steam_tools(server, bob, prefix="bob.")
# Tools registered: alice.steam.status, alice.steam.get_product_info,
# alice.steam.send_um, bob.steam.status, ...The raw handlers are exported for reuse. If you want a steam.family_sharing tool the built-in set doesn't have:
from pydantic import BaseModel, Field
from steam.mcp.tools import SteamToolBinding, register_steam_tools
from steam.aio import AsyncSteamClient
class FamilyInput(BaseModel):
steam_id: int = Field(description="64-bit Steam ID.")
class FamilyOutput(BaseModel):
apps: list[int]
async def _handler(client: AsyncSteamClient, inp: FamilyInput) -> FamilyOutput:
resp = await client.send_um_and_wait(
"FamilyGroups.GetFamilyGroupForUser#1",
{"steamid": inp.steam_id},
)
return FamilyOutput(apps=[a["appid"] for a in resp.body.get("apps", [])])
custom = [
SteamToolBinding(
name="steam.family_sharing",
description="List apps a Steam user shares with their family group.",
input_model=FamilyInput,
output_model=FamilyOutput,
handler=_handler,
),
]
register_steam_tools(server, client, bindings=custom)Or mix built-ins with custom:
from steam.mcp import build_steam_tool_bindings
register_steam_tools(server, client, bindings=[*build_steam_tool_bindings(), *custom])from contextlib import asynccontextmanager
from fastapi import FastAPI
from mcp.server.fastmcp import FastMCP
from steam.aio import AsyncSteamClient
from steam.aio.integrations.fastapi import steam_client_lifespan
from steam.mcp import register_steam_tools
client = AsyncSteamClient()
mcp_server = FastMCP("Steam")
async def _login(c):
await c.anonymous_login()
@asynccontextmanager
async def lifespan(app: FastAPI):
async with steam_client_lifespan(app, client, on_start=_login):
register_steam_tools(mcp_server, client)
yield
app = FastAPI(lifespan=lifespan)
# Mount MCP on the FastAPI app however your MCP SDK version prefers
# (streamable_http_manager, sse, or as a separate stdio process).- AsyncSteamClient — the client the tools call
- FastAPI-Integration — mount MCP alongside FastAPI
- TaskIQ-Integration — same client can back both MCP tools and background jobs
H47R15/steam — maintained fork of ValvePython/steam. MIT licensed.