PACER-native MCP server (stacked on the spend-governance core) - #9
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional PACER-native MCP (Model Context Protocol) stdio server that exposes a governed, billable tool surface backed by the existing spend-governance layer (from stacked PR #6). This PR aims to provide an agent-friendly interface while preserving the same preventive cap enforcement and audit-ledger accounting as the CLI.
Changes:
- Introduces
pacer_cli.mcp_serverimplementing MCP tools (search_cases,search_parties,get_docket,get_document) plus a read-onlyspend_status. - Adds MCP-focused tests, including a “fail-closed network tripwire” invariant suite for the MCP surface.
- Adds an optional
[mcp]extra and apacer-mcpconsole entry point; documents MCP usage in the README.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/pacer_cli/mcp_server.py |
Implements the MCP adapter and tool wiring over the governed client. |
tests/test_mcp_server.py |
Unit tests for MCP adapter behavior without requiring the MCP SDK or network. |
tests/test_failclosed_invariant_mcp.py |
Invariant tests ensuring refusals prevent any network client construction. |
README.md |
Documents MCP installation, registration, and tool list. |
pyproject.toml |
Adds the optional mcp extra and the pacer-mcp entry point. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def spend_status() -> dict[str, Any]: | ||
| """Today's spend vs. the active caps - a read-only budget view (no billing).""" | ||
| cfg = _load_config() | ||
| spent = spend_today() | ||
| return { | ||
| "spent_today": spent, | ||
| "per_op_cap": cfg.per_op_cap_usd, | ||
| "daily_cap": cfg.daily_cap_usd, | ||
| "remaining_today": round(max(0.0, cfg.daily_cap_usd - spent), 2), | ||
| "require_client_code": cfg.require_client_code, | ||
| } |
| cost = float(result.cost or 0.0) | ||
| _audit(cfg, f"docket {court_normalized}/{case_number}", cost) | ||
| return {"path": str(result.filepath), "pages": int(result.pages or 0), "cost": cost} |
| cost = float(result.cost or 0.0) | ||
| _audit(cfg, f"document {doc_number}", cost) | ||
| return {"path": str(result.filepath), "pages": int(result.pages or 0), "cost": cost} |
|
@copilot full bug test, check for race conditions |
Full bug test run: 242 passed, 3 skipped (0 failures). Two genuine race conditions fixed in commit
A third condition — the check-then-act gap between New |
74024df to
eec0ae8
Compare
Optional MCP stdio server exposing the governed PACER client: search_cases, search_parties, get_docket, get_document (all billable, all routed through the shared check_spend() gate + audit ledger) plus a read-only spend_status. A cap breach maps to a structured MCP error mirroring the CLI's JSON. Credentials are human-provisioned; the server never prompts, auto-unlocks the vault, or writes policy.csv/courts.csv. Ships as an optional [mcp] extra + pacer-mcp entry point, so the core CLI keeps its dependency set. Includes the MCP fail-closed invariant suite. 238 passed.
…parity - spend_status() is read-only, so it must stay available when policy.csv is fat-fingered (only billable ops fail closed). Load the base config, apply the overlay best-effort, and on PolicyError report conservative built-in caps plus a policy_error field instead of raising. (Copilot review, mcp_server.py:94) - get_docket()/get_document() now record via log_download (with page count), matching the CLI's download ledger format and always emitting the cost=$ token even at $0.00 — one consistent trail across surfaces. (Copilot, lines 175/198) - Tests for read-only-survives-bad-policy added.
…add concurrency tests
Copilot's pass fixed the audit-logger thread races but only documented the check-then-act TOCTOU on the cap: two concurrent processes/agents could each read the same 'spent today', both pass the cap, and both bill. Fix it at the source with security.spend_lock() - a reentrant in-process RLock + an flock on ~/.pacer/logs/.spend.lock - held across the whole read-check-bill-record section so concurrent billable ops serialize and the second sees the first's spend. Acquire is bounded (fail-closed SpendLockTimeout on timeout); the OS releases the flock if a process dies mid-op. Wired in one place each: MCP tools wrap their critical section in spend_lock(); CLI billable commands via an @under_spend_lock decorator (one op per process). Trade-off: concurrent billable ops serialize - correct for a hard shared cap. Tests: reentrancy, thread serialization, and a 2-thread race proving exactly one op bills while the other is refused. 244 passed.
5109645 to
17e474c
Compare
Follow-up PR A from the plan on #6. Extracts the optional MCP server out of #6 so it reviews on its own. Stacked on #6 (
base: claude/pacer-spend-governance-50JNs) — review/merge #6 first; the diff here is just the MCP layer.What this adds
src/pacer_cli/mcp_server.py— a thin adapter over the governed client, not new business logic.search_cases,search_parties,get_docket,get_document(all billable) + a read-onlyspend_status.[mcp]extra +pacer-mcpstdio entry point, so the core CLI keeps its dependency set.tests/test_mcp_server.py+tests/test_failclosed_invariant_mcp.py(the network-tripwire invariant for the MCP surface).Why it's safe
Every billable tool routes through the same
security.check_spend()gate and writes the same audit line as the CLI — one cap, one ledger, for both surfaces. A breach maps to a structured MCP error mirroring the CLI's JSON (BUDGET_EXCEEDED/MATTER_REQUIRED/MATTER_INVALID/SCOPE_EMPTY/POLICY_INVALID). Credentials stay human-provisioned (env /config.env); the server never prompts, never auto-unlocks the vault, and never writespolicy.csv/courts.csv. ThemcpSDK is imported lazily, so the module (and tests) import without the extra installed.Verification
pyflakes+tools/audit.pyclean.Depends on #6 (the shared
check_spend()/spend_today()live there).https://claude.ai/code/session_01NNvhsYRVWhjfcdgaSmU5bt
Generated by Claude Code