mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 13:47:31 +01:00
Each of these was one file doing several jobs, so they are split along what they actually do rather than at arbitrary line offsets: - _total_block: the lock file and the status derived from it stay in _total_block; making Steam *not installed* moves to _total_block_purge, making it *unreachable* to _total_block_hosts / _total_block_iptables. The generic pacman verbs (owner, installed, remove) become _pacman, since nothing about them is total-block-specific. Domain and process name tables become data modules. - _playtime: budget state and persistence (_playtime_state), pure transitions (_playtime_budget), cmdline process detection (_playtime_procs), the warn/cutoff/kill ladder (_playtime_cutoff), desktop notifications (_playtime_notify), process-tree walking and killing (_playtime_kill), mountinfo parsing (_playtime_mountinfo) and the shared privileged-command helper (_playtime_run). - _mcp: read-only tools (_mcp_query) split from mutating ones (_mcp_actions, _mcp_gaming_time), so "what can this server change?" is answerable from one file. _mcp keeps the server object and entry point. No re-export shims: callers import from the module that defines the name. That keeps a stale patch target loud (AttributeError) instead of letting it pass while intercepting nothing. `ruff check --select F821,F401` is run after every extraction -- an earlier mechanical attempt moved functions without the module-level names they referenced, which imported fine and only failed when called (349 errors), so it was reverted and redone by hand. The autouse fixtures in conftest are now imported by name and held by __all__; ruff --unsafe-fixes had deleted them as unused, since pytest resolves fixtures by name. 1324 tests pass at 100.00% coverage in the clean venv; `main status` is byte-identical to a worktree at HEAD. 49 -> 39 files over the cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPpmMpTSkp8UziRc5u8B7P
99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
"""Read-only MCP tools: dataset, status, stats and the backlog list.
|
|
|
|
Separated from the mutating tools in
|
|
:mod:`steam_backlog_enforcer._mcp_actions` so that "what can this server
|
|
change?" is answerable by looking at one file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from steam_backlog_enforcer._actions import (
|
|
status_payload,
|
|
)
|
|
from steam_backlog_enforcer._mcp_server import (
|
|
_DEFAULT_LIST_LIMIT,
|
|
_backlog_sort_key,
|
|
mcp,
|
|
)
|
|
from steam_backlog_enforcer._web_dataset import build_web_dataset, dataset_to_payload
|
|
from steam_backlog_enforcer.config import State, load_snapshot
|
|
from steam_backlog_enforcer.steam_api import GameInfo
|
|
|
|
|
|
@mcp.tool()
|
|
def get_dataset() -> dict[str, Any]:
|
|
"""Return the full, secrets-free backlog dataset.
|
|
|
|
Includes every incomplete candidate game with HowLongToBeat times,
|
|
ProtonDB tiers, pace-vs-HLTB calibration, and the default CLI thresholds —
|
|
the same projection the local web UI consumes. Reads on-disk caches only
|
|
(no network, no Steam API key).
|
|
"""
|
|
return dataset_to_payload(build_web_dataset(State.load()))
|
|
|
|
|
|
@mcp.tool()
|
|
def get_status() -> dict[str, Any]:
|
|
"""Return the current enforcer status.
|
|
|
|
Reports the assigned game, finished count, whether the Steam store is
|
|
blocked, installed-game count, any active total block (with days remaining),
|
|
and whether a manual-pick lock is in force.
|
|
"""
|
|
return status_payload(State.load())
|
|
|
|
|
|
@mcp.tool()
|
|
def get_stats() -> dict[str, Any]:
|
|
"""Return backlog completion-time estimates.
|
|
|
|
A focused subset of the dataset: the default qualifying-games summary
|
|
(rush / leisure / worst-case totals) and the player's measured pace versus
|
|
HowLongToBeat.
|
|
"""
|
|
payload = dataset_to_payload(build_web_dataset(State.load()))
|
|
return {
|
|
"default_summary": payload["default_summary"],
|
|
"pace_vs_hltb": payload["pace_vs_hltb"],
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def list_backlog(limit: int = _DEFAULT_LIST_LIMIT) -> dict[str, Any]:
|
|
"""List incomplete games, shortest-completionist-first, capped at ``limit``.
|
|
|
|
Args:
|
|
limit: Maximum number of games to return (non-positive returns none).
|
|
"""
|
|
snapshot = load_snapshot()
|
|
if snapshot is None:
|
|
return {
|
|
"total": 0,
|
|
"returned": 0,
|
|
"games": [],
|
|
"note": "No snapshot found. Run 'scan' first.",
|
|
}
|
|
games = [GameInfo.from_snapshot(entry) for entry in snapshot]
|
|
incomplete = sorted((g for g in games if not g.is_complete), key=_backlog_sort_key)
|
|
capped = incomplete[: max(0, limit)]
|
|
return {
|
|
"total": len(incomplete),
|
|
"returned": len(capped),
|
|
"games": [
|
|
{
|
|
"app_id": g.app_id,
|
|
"name": g.name,
|
|
"completion_pct": round(g.completion_pct, 1),
|
|
"completionist_hours": g.completionist_hours,
|
|
}
|
|
for g in capped
|
|
],
|
|
}
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Gated write tools (preview unless confirm=True; NEVER allowlist these)
|
|
# ──────────────────────────────────────────────────────────────
|