[project] name = "llm-usage-mcp" version = "0.1.3" description = "Local-first, multi-provider tool that captures LLM API spend and exposes it to coding agents via MCP" readme = "README.md" license = "MIT" license-files = ["LICENSE"] authors = [ { name = "Y.Zhao", email = "zhaoyue722@gmail.com" } ] requires-python = ">=3.13" dependencies = [ "alembic>=1.18.4", "fastapi>=0.115", "httpx[socks]>=0.27", # Upper bound is load-bearing: mcp 2.0.0 removed `mcp.server.fastmcp` # (the FastMCP entry point this server is built on) in favor of a # restructured `mcp.server.mcpserver` API. Without the ceiling, a # fresh install resolves to 2.x and every console script fails at # import — the repo's own uv.lock hides it, because it pins 1.27.0. # Lift this only together with the port to the 2.x API. "mcp[cli]>=1.27.0,<2", "pydantic>=2", "pydantic-settings>=2", "sqlalchemy>=2.0", "typer>=0.15", "uvicorn[standard]>=0.32", ] [project.urls] Homepage = "https://github.com/zhaoyue722/llm-usage-mcp" Repository = "https://github.com/zhaoyue722/llm-usage-mcp" Issues = "https://github.com/zhaoyue722/llm-usage-mcp/issues" [project.scripts] # Multi-command CLI: `llm-usage proxy`, `llm-usage compare`, ... llm-usage = "llm_usage.cli:main" # Stdio MCP server — its own script because the boot path differs (no Typer # parsing; stdio is the only "argument"). llm-usage-mcp = "llm_usage:main" # Backward-compat alias for `llm-usage proxy`, documented in the README. # Kept as a thin standalone so existing `uv run llm-usage-proxy` setups don't # break when `llm-usage` becomes the primary entry. llm-usage-proxy = "llm_usage.cli:proxy_main" [build-system] requires = ["uv_build>=0.11.1,<0.12.0"] build-backend = "uv_build" [tool.uv.build-backend] module-name = "llm_usage" [dependency-groups] dev = [ "mypy>=1.20.2", "pytest>=9.0.3", "pytest-cov>=7.1.0", "respx>=0.22", "ruff>=0.15.12", ] [tool.ruff] line-length = 100 target-version = "py313" src = ["src", "tests"] [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade "SIM", # flake8-simplify "RUF", # ruff-specific "N", # pep8-naming "TID", # flake8-tidy-imports ] ignore = [ "E501", # line too long (handled by formatter) ] # `×` (U+00D7, MULTIPLICATION SIGN) is used deliberately as the # "times N" marker in the `compare` renderer's variant column # (e.g., `×4`). Ruff's confusable-character check would otherwise # flag it as visually ambiguous with `x` (U+0078); explicit allow. allowed-confusables = ["×"] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["S101"] # allow assert in tests [tool.ruff.lint.flake8-bugbear] # Typer's idiom is `def cmd(x: int = typer.Option(...))` — the call in the # default position is *the* design pattern, not a smell. Whitelist it (and # `typer.Argument`) so B008 doesn't fight Typer. extend-immutable-calls = ["typer.Option", "typer.Argument"] [tool.ruff.format] quote-style = "double" indent-style = "space" [tool.mypy] python_version = "3.13" strict = true warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = true no_implicit_reexport = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true files = ["src", "tests"] [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false [tool.pytest.ini_options] minversion = "8.0" testpaths = ["tests"] addopts = [ "-ra", "--strict-markers", "--strict-config", ] filterwarnings = ["error"] [tool.coverage.run] source = ["src"] branch = true [tool.coverage.report] show_missing = true skip_covered = false exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "if __name__ == .__main__.:", ]