modern-collections 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ # Never commit secrets or local env files
2
+ .env
3
+ *.env
4
+ !.env.example
5
+
6
+ # Build / cache
7
+ __pycache__/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ dist/
11
+ build/
12
+ .venv/
@@ -0,0 +1,345 @@
1
+ Metadata-Version: 2.5
2
+ Name: modern-collections
3
+ Version: 0.1.0
4
+ Summary: Official client, CLI, and MCP server for the Modern Collections external REST API
5
+ Project-URL: Homepage, https://moderncollections.io
6
+ Project-URL: Documentation, https://docs.moderncollections.io
7
+ Author: Modern Collections
8
+ License: Proprietary
9
+ Keywords: accounts-receivable,api-client,cli,collections,mcp
10
+ Classifier: License :: Other/Proprietary License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: mcp<2,>=1.2
18
+ Requires-Dist: typer>=0.12
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # modern-collections — Modern Collections client, CLI, and MCP server
25
+
26
+ One hardened HTTP client over the Modern Collections **external REST API** (the
27
+ creditor-API-key surface), with two front-ends:
28
+
29
+ - **CLI** — `modern-collections`
30
+ - **MCP server** — `modern-collections-mcp` (stdio, or streamable HTTP on the API at `/mcp`)
31
+
32
+ The client depends only on the public HTTP contract, so it ships independently of the
33
+ backend application.
34
+
35
+ ## Connect an agent
36
+
37
+ No install. Point an MCP client at the hosted server:
38
+
39
+ - URL: `https://api.moderncollections.io/mcp` (streamable HTTP)
40
+ - Header: `Authorization: Bearer <creditor API key>`
41
+ - Partner keys also send `X-MC-Creditor: <creditor uuid or external ref>`
42
+
43
+ `GET /v1/public/mcp` returns that card for whatever host you are calling.
44
+ To file an invoice: `placement_create` (confirm first — this starts real
45
+ outreach), then `document_upload` with `document_type` `invoice`, then
46
+ `document_process`. `placement_recall` stops a placement that should not
47
+ have been opened.
48
+
49
+ ## Install
50
+
51
+ The install below is only for the local CLI and the stdio server. The hosted
52
+ URL does not need it.
53
+
54
+ ```bash
55
+ uv tool install modern-collections # CLI `modern-collections` and server `modern-collections-mcp`
56
+ # or: pip install modern-collections
57
+ ```
58
+
59
+ Requires Python 3.11 or later. The PyPI project `mc-api` is unrelated; this package is
60
+ `modern-collections`.
61
+
62
+ ## Configure
63
+
64
+ Pick an environment (no production default — by design) and supply a creditor API key.
65
+ The key is read from `MC_API_KEY` and is **never printed or logged**.
66
+
67
+ > **Getting a key.** Dashboard → **Settings → API Key**. Only the bcrypt hash is stored,
68
+ > so an existing key can never be re-displayed — Settings shows the key id
69
+ > (`ca_<prefix>`) and a rotate action that reveals a fresh secret exactly once. A lost
70
+ > key is rotated, not recovered. Setup snippets for the CLI, the MCP server, and the
71
+ > agent skills live under **Settings → Developer access**.
72
+
73
+ | Var | Values |
74
+ | --- | --- |
75
+ | `MC_API_ENV` | `local` \| `staging` \| `demo` \| `prod` |
76
+ | `MC_API_BASE_URL` | explicit base URL (overrides `MC_API_ENV`) |
77
+ | `MC_API_KEY` | `ca_<prefix>_<secret>` (creditor) or `pa_<prefix>_<secret>` (partner) |
78
+ | `MC_API_CREDITOR` | creditor UUID or external reference — only with a partner key |
79
+
80
+ Environment → base URL map:
81
+
82
+ | env | base URL |
83
+ | --- | --- |
84
+ | `local` | `http://localhost:8000` |
85
+ | `staging` | `https://web-staging-c8aa.up.railway.app` (Modern Collections internal) |
86
+ | `demo` | `https://app.demo.moderncollections.io` |
87
+ | `prod` | `https://api.moderncollections.io` |
88
+
89
+ ### Partner keys and the creditor scope
90
+
91
+ A **partner** key (`pa_<prefix>_<secret>`) acts on behalf of several linked
92
+ creditors instead of just one, so every request has to say which creditor it is
93
+ for. Pass `--creditor`, set `MC_API_CREDITOR`, or construct `MCClient(creditor=...)`
94
+ with a creditor UUID or its `external_creditor_ref`; the client sends it as the
95
+ `X-MC-Creditor` header on every request. An ordinary creditor key (`ca_...`) is
96
+ already scoped to one creditor and ignores the header — `creditor` is simply a
97
+ no-op for it, so day-to-day creditor scripts and skills need no changes.
98
+
99
+ ```bash
100
+ export MC_API_ENV=prod MC_API_KEY=pa_... MC_API_CREDITOR=<creditor-uuid>
101
+ modern-collections placements list # scoped to that one creditor
102
+
103
+ # or override per invocation
104
+ modern-collections --creditor <other-creditor-uuid-or-ref> placements list
105
+ ```
106
+
107
+ Omitting `--creditor`/`MC_API_CREDITOR` with a partner key gets a `400` from the
108
+ API (`X-MC-Creditor header required`) — the client does not guess.
109
+
110
+ ## CLI
111
+
112
+ ```bash
113
+ export MC_API_ENV=prod MC_API_KEY=ca_...
114
+
115
+ modern-collections health # reachability
116
+ modern-collections placements create --company "Acme Freight" --amount 7500.00 --email ap@acme.co \
117
+ --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01
118
+ modern-collections placements list --status in_outreach
119
+ modern-collections placements get <placement_id>
120
+ modern-collections payments record <placement_id> --amount 500.00 --idempotency-key <bank-ref>
121
+ modern-collections -o json analytics summary
122
+ modern-collections disputes ack <placement_id> --note "spoke with AP"
123
+ modern-collections improve # placements whose file needs attention
124
+ modern-collections improve <placement_id> # per-placement plan (missing documents)
125
+ modern-collections documents upload invoice-4411.pdf --placement <placement_id> --type invoice
126
+ modern-collections documents list --placement <placement_id>
127
+ modern-collections documents process <document_id> # classify/extract/match now
128
+ ```
129
+
130
+ ### File an invoice, read it back, then correct it
131
+
132
+ The whole loop is unified on the placement id — submit the invoice and any
133
+ supporting PDFs against it, read back what was extracted, then amend:
134
+
135
+ ```bash
136
+ P=$(modern-collections -o json placements create --company "Acme Freight" --amount 7500.00 \
137
+ --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01 \
138
+ | jq -r .placement_id)
139
+
140
+ modern-collections documents upload invoice-4411.pdf --placement $P --type invoice
141
+ modern-collections documents upload bol-4411.pdf --placement $P --type bol
142
+ modern-collections documents process <document_id> # or let the worker pick it up
143
+
144
+ modern-collections placements context $P # what the platform actually read
145
+ modern-collections placements documents $P # everything on file
146
+
147
+ # fix what the extraction got wrong — append-only and audited
148
+ modern-collections placements amend $P --po-number PO-8871 --service-date 2026-01-20 \
149
+ --reason "read off the signed BOL"
150
+ modern-collections placements amend $P --amount 7250.00 --reason "credit memo applied"
151
+ modern-collections placements debtor $P --email ap@acmefreight.com
152
+ ```
153
+
154
+ Amendments are accepted **mid-collection**: the placement planner re-reads the
155
+ row on its next tick, so a correction reaches the next call or email without any
156
+ further action. Every edit writes an append-only history row plus an audit event.
157
+
158
+ One amendment kind per invocation — `--amount`, `--notes`/`--clear-notes`, or any
159
+ combination of the contract-metadata flags (`--invoice-number`, `--invoice-date`,
160
+ `--due-date`, `--po-number`, `--service-date`, `--currency`,
161
+ `--written-contract`). A placement's *status* is deliberately not amendable here;
162
+ use `payments record`, `disputes resolve`, or `placements recall`, which own the
163
+ side effects.
164
+
165
+ ### Everything else
166
+
167
+ ```bash
168
+ modern-collections intake csv-preview aging.csv # bulk placement from an AR aging file
169
+ modern-collections intake csv-commit <preview_id> --mappings '{"amount":"Balance"}'
170
+ modern-collections evidence get <placement_id> # how well-documented the file is
171
+ modern-collections disputes list --status open
172
+ modern-collections remittances list # money owed back to you
173
+ modern-collections statements list && modern-collections exports create placements
174
+ modern-collections qbo status # QuickBooks link
175
+ modern-collections agreements current
176
+ ```
177
+
178
+ `-o json` emits machine-readable output; the default is a human table. It is a
179
+ **global** option, so it goes *before* the subcommand (`modern-collections -o json analytics
180
+ summary`, not `modern-collections analytics summary -o json`) — the same is true of `--env`,
181
+ `--base-url`, `--api-key`, `--creditor`, and `--timeout`. Exit codes: `0` success,
182
+ `1` API/runtime error, `2` configuration/usage error.
183
+
184
+ ## MCP server
185
+
186
+ Hosted, on the API, for any MCP client that can take a URL. Send the creditor
187
+ key on the request. The process does not keep a key of its own, and a request
188
+ without `Authorization: Bearer` cannot fall through to one.
189
+
190
+ ```json
191
+ {
192
+ "mcpServers": {
193
+ "modern-collections": {
194
+ "url": "https://api.moderncollections.io/mcp",
195
+ "headers": {
196
+ "Authorization": "Bearer ca_..."
197
+ }
198
+ }
199
+ }
200
+ }
201
+ ```
202
+
203
+ A partner key also sends `X-MC-Creditor: <creditor uuid or external ref>`.
204
+
205
+ Local stdio (the same tools, key from the environment):
206
+
207
+ ```bash
208
+ MC_API_ENV=local MC_API_KEY=ca_... modern-collections-mcp
209
+ ```
210
+
211
+ `modern-collections-mcp --http` serves the same streamable-HTTP endpoint on
212
+ `http://127.0.0.1:8765/mcp` for a machine that should not use the hosted URL.
213
+ Set `MC_API_ENV` or `MC_API_BASE_URL` in that process. The key still comes from
214
+ the client's `Authorization` header, not from `MC_API_KEY`.
215
+
216
+ The MCP server exposes the **same** surface as the CLI — one tool per client
217
+ method, grouped as: placements (`placement_create`, `placement_status`,
218
+ `placement_list`, `placement_recall`, `placement_set_posture`, `placement_calls`,
219
+ `placement_emails`, `placement_payments`, `placement_compliance_decision`,
220
+ `placement_refresh_enrichment`), editing (`placement_amend`,
221
+ `placement_update_debtor`), documents (`document_upload`, `document_list`,
222
+ `document_status`, `document_process`, `document_match_decide`, `document_batch`,
223
+ `placement_documents`, `placement_context`, `placement_context_rebuild`), improve
224
+ and evidence (`placement_improve`, `improve_overview`, `evidence_*`), disputes
225
+ (`dispute_*`), money (`payment_*`, `remittance_*`, `statement_*`, `export_*`),
226
+ reporting (`analytics_summary`, `analytics_series`, `audit_query`), settings
227
+ (`settings_*`, `notification_settings_*`), integrations (`qbo_*`), agreements
228
+ (`agreement_*`), and intake (`intake_*`).
229
+
230
+ > **Tools that start real outreach:** `placement_create`, `intake_csv_commit`,
231
+ > `intake_file_confirm`, `intake_draft_resolve` (approve), `intake_structured_submit`.
232
+ > There is no sandbox on this surface. `placement_recall` is the stop button and is
233
+ > exposed as a tool for exactly that reason.
234
+ >
235
+ > **Binding or destructive:** `agreement_accept` (legally binding), `dispute_resolve`,
236
+ > `qbo_disconnect`, `intake_webhook_rotate_secret` (invalidates the previous secret),
237
+ > `notification_settings_replace` (replaces the whole grid).
238
+ >
239
+ > **Hosted `/mcp` differences:** `agreement_accept` is refused there, because the
240
+ > acceptance record would carry the server's loopback address instead of the
241
+ > signer's; accept the MSA in the dashboard or call `POST /v1/agreements/accept`
242
+ > from your own system. The binary tools (`settings_agent_skills`,
243
+ > `agreement_certificate`) write a file only over stdio; over HTTP they return
244
+ > the bytes as `content_base64` with a suggested `filename`.
245
+
246
+ Every tool also takes a trailing `creditor` argument (creditor UUID or external
247
+ reference) — required to reach any data at all when `MC_API_KEY` is a partner key
248
+ (`pa_...`); harmless and unnecessary with an ordinary creditor key. It falls back
249
+ to `MC_API_CREDITOR` when omitted, so a partner-scoped MCP server can be
250
+ registered once per creditor without repeating it on every tool call.
251
+
252
+ The document loop closes from an agent session: `document_upload` with a
253
+ `placement_id`, then `document_process`, then `placement_context` to read back the
254
+ extracted fields, then `placement_amend` to correct anything wrong — including
255
+ `po_number` and `service_date`. Amendments apply mid-collection and are audited.
256
+
257
+ The document/improve tools close the evidence loop from an agent session:
258
+ `improve_overview` ranks the placements whose collection file needs attention,
259
+ `placement_improve` returns one placement's plan (missing documents with
260
+ where-to-look and search hints), and `document_upload` sends a found file —
261
+ either `content_base64` or `file_path` read from a directory explicitly allowed
262
+ with `MC_API_MCP_UPLOAD_ROOT`. Encoding is handled **client-side in the MCP
263
+ server**; the API itself takes the raw bytes
264
+ (`POST /v1/documents` with `Content-Type` + `X-Filename`), so there is no
265
+ base64 endpoint and no inflated payload on the wire.
266
+
267
+ `mcp.example.json` is the stdio config. Destructive tools set MCP
268
+ `destructiveHint` (outreach starters, `placement_recall`, `placement_amend`,
269
+ `agreement_accept`, `dispute_resolve`, `qbo_disconnect`, intake secret rotation,
270
+ notification replace, intake address delete). For Claude Code stdio:
271
+
272
+ ```bash
273
+ claude mcp add modern-collections -- modern-collections-mcp
274
+ # then set MC_API_ENV / MC_API_KEY in the server's environment
275
+ # with a partner key (pa_...), also set MC_API_CREDITOR to the linked creditor
276
+ # optional: set MC_API_MCP_UPLOAD_ROOT=/path/to/approved-documents to enable file_path uploads
277
+ ```
278
+
279
+ ## Agent skills
280
+
281
+ `skills/` holds Claude Code / Claude Desktop skills that drive this same API —
282
+ `modern-collections` (core operations) and `collections-ar-triage` (AR aging export →
283
+ reviewed placement batch). Partners download them from **Settings → Developer access**
284
+ (`GET /v1/settings/agent-skills.zip`).
285
+
286
+ ## Client library
287
+
288
+ ```python
289
+ from mc_api import MCClient
290
+
291
+ with MCClient(env="prod", api_key="ca_...") as api:
292
+ placement = api.create_placement(
293
+ {
294
+ "invoice_amount": "7500.00",
295
+ "invoice_number": "INV-4411",
296
+ "invoice_date": "2026-01-01",
297
+ "due_date": "2026-02-01",
298
+ "debtor": {"company_name": "Acme Freight", "primary_email": "ap@acme.co"},
299
+ }
300
+ )
301
+ print(api.get_placement(placement["placement_id"]))
302
+ ```
303
+
304
+ With a partner key, add `creditor=` (or set `MC_API_CREDITOR`) to scope every call
305
+ made through that client instance to one linked creditor:
306
+
307
+ ```python
308
+ with MCClient(env="prod", api_key="pa_...", creditor="<creditor uuid>") as api:
309
+ print(api.list_placements())
310
+ ```
311
+
312
+ ## Surface parity
313
+
314
+ The client, the CLI, and the MCP server cover the same creditor-API-key surface,
315
+ 1:1. `tests/test_parity.py` holds the mapping table and fails the build if a client
316
+ method gains no CLI command and no MCP tool (or vice versa), so the three front-ends
317
+ cannot silently drift apart.
318
+
319
+ One endpoint is deliberately **not** reachable from any of them:
320
+ `POST /v1/settings/api-key/rotate` is gated on a dashboard session and rejects
321
+ `ca_*` bearers by design, so a leaked key cannot mint its own replacement. Rotate
322
+ from **Dashboard → Settings → API Key**. `MCClient.rotate_api_key()` exists only to
323
+ raise with that explanation rather than let a caller guess.
324
+
325
+ ## Safety properties
326
+
327
+ - The API key lives only in the request `Authorization` header — excluded from
328
+ `repr()`, exceptions, and logs. Same treatment for the creditor scope (`creditor=`
329
+ / `MC_API_CREDITOR`): never in `repr()`, and both it and the API key are stripped
330
+ from the one unauthenticated request the client makes (`health()`).
331
+ - Explicit connect/read/write timeouts.
332
+ - Only idempotent `GET`s are retried (429/5xx, exponential backoff); `POST`/`PATCH`/
333
+ `DELETE` are never auto-retried, so a create or payment can't be duplicated.
334
+ - No production default environment.
335
+ - Client-side validation for the amendment allow-list and enum-ish arguments, so a
336
+ typo fails locally instead of burning a round-trip.
337
+ - `POST /v1/intake/structured` is HMAC-signed over the exact bytes sent — the body is
338
+ serialised once and posted as raw content so the signature can't desync from it.
339
+
340
+ ## Test
341
+
342
+ ```bash
343
+ uv run --with 'mcp<2' --with typer --with pytest --with pytest-asyncio \
344
+ pytest clients/mc_api/tests -q
345
+ ```
@@ -0,0 +1,322 @@
1
+ # modern-collections — Modern Collections client, CLI, and MCP server
2
+
3
+ One hardened HTTP client over the Modern Collections **external REST API** (the
4
+ creditor-API-key surface), with two front-ends:
5
+
6
+ - **CLI** — `modern-collections`
7
+ - **MCP server** — `modern-collections-mcp` (stdio, or streamable HTTP on the API at `/mcp`)
8
+
9
+ The client depends only on the public HTTP contract, so it ships independently of the
10
+ backend application.
11
+
12
+ ## Connect an agent
13
+
14
+ No install. Point an MCP client at the hosted server:
15
+
16
+ - URL: `https://api.moderncollections.io/mcp` (streamable HTTP)
17
+ - Header: `Authorization: Bearer <creditor API key>`
18
+ - Partner keys also send `X-MC-Creditor: <creditor uuid or external ref>`
19
+
20
+ `GET /v1/public/mcp` returns that card for whatever host you are calling.
21
+ To file an invoice: `placement_create` (confirm first — this starts real
22
+ outreach), then `document_upload` with `document_type` `invoice`, then
23
+ `document_process`. `placement_recall` stops a placement that should not
24
+ have been opened.
25
+
26
+ ## Install
27
+
28
+ The install below is only for the local CLI and the stdio server. The hosted
29
+ URL does not need it.
30
+
31
+ ```bash
32
+ uv tool install modern-collections # CLI `modern-collections` and server `modern-collections-mcp`
33
+ # or: pip install modern-collections
34
+ ```
35
+
36
+ Requires Python 3.11 or later. The PyPI project `mc-api` is unrelated; this package is
37
+ `modern-collections`.
38
+
39
+ ## Configure
40
+
41
+ Pick an environment (no production default — by design) and supply a creditor API key.
42
+ The key is read from `MC_API_KEY` and is **never printed or logged**.
43
+
44
+ > **Getting a key.** Dashboard → **Settings → API Key**. Only the bcrypt hash is stored,
45
+ > so an existing key can never be re-displayed — Settings shows the key id
46
+ > (`ca_<prefix>`) and a rotate action that reveals a fresh secret exactly once. A lost
47
+ > key is rotated, not recovered. Setup snippets for the CLI, the MCP server, and the
48
+ > agent skills live under **Settings → Developer access**.
49
+
50
+ | Var | Values |
51
+ | --- | --- |
52
+ | `MC_API_ENV` | `local` \| `staging` \| `demo` \| `prod` |
53
+ | `MC_API_BASE_URL` | explicit base URL (overrides `MC_API_ENV`) |
54
+ | `MC_API_KEY` | `ca_<prefix>_<secret>` (creditor) or `pa_<prefix>_<secret>` (partner) |
55
+ | `MC_API_CREDITOR` | creditor UUID or external reference — only with a partner key |
56
+
57
+ Environment → base URL map:
58
+
59
+ | env | base URL |
60
+ | --- | --- |
61
+ | `local` | `http://localhost:8000` |
62
+ | `staging` | `https://web-staging-c8aa.up.railway.app` (Modern Collections internal) |
63
+ | `demo` | `https://app.demo.moderncollections.io` |
64
+ | `prod` | `https://api.moderncollections.io` |
65
+
66
+ ### Partner keys and the creditor scope
67
+
68
+ A **partner** key (`pa_<prefix>_<secret>`) acts on behalf of several linked
69
+ creditors instead of just one, so every request has to say which creditor it is
70
+ for. Pass `--creditor`, set `MC_API_CREDITOR`, or construct `MCClient(creditor=...)`
71
+ with a creditor UUID or its `external_creditor_ref`; the client sends it as the
72
+ `X-MC-Creditor` header on every request. An ordinary creditor key (`ca_...`) is
73
+ already scoped to one creditor and ignores the header — `creditor` is simply a
74
+ no-op for it, so day-to-day creditor scripts and skills need no changes.
75
+
76
+ ```bash
77
+ export MC_API_ENV=prod MC_API_KEY=pa_... MC_API_CREDITOR=<creditor-uuid>
78
+ modern-collections placements list # scoped to that one creditor
79
+
80
+ # or override per invocation
81
+ modern-collections --creditor <other-creditor-uuid-or-ref> placements list
82
+ ```
83
+
84
+ Omitting `--creditor`/`MC_API_CREDITOR` with a partner key gets a `400` from the
85
+ API (`X-MC-Creditor header required`) — the client does not guess.
86
+
87
+ ## CLI
88
+
89
+ ```bash
90
+ export MC_API_ENV=prod MC_API_KEY=ca_...
91
+
92
+ modern-collections health # reachability
93
+ modern-collections placements create --company "Acme Freight" --amount 7500.00 --email ap@acme.co \
94
+ --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01
95
+ modern-collections placements list --status in_outreach
96
+ modern-collections placements get <placement_id>
97
+ modern-collections payments record <placement_id> --amount 500.00 --idempotency-key <bank-ref>
98
+ modern-collections -o json analytics summary
99
+ modern-collections disputes ack <placement_id> --note "spoke with AP"
100
+ modern-collections improve # placements whose file needs attention
101
+ modern-collections improve <placement_id> # per-placement plan (missing documents)
102
+ modern-collections documents upload invoice-4411.pdf --placement <placement_id> --type invoice
103
+ modern-collections documents list --placement <placement_id>
104
+ modern-collections documents process <document_id> # classify/extract/match now
105
+ ```
106
+
107
+ ### File an invoice, read it back, then correct it
108
+
109
+ The whole loop is unified on the placement id — submit the invoice and any
110
+ supporting PDFs against it, read back what was extracted, then amend:
111
+
112
+ ```bash
113
+ P=$(modern-collections -o json placements create --company "Acme Freight" --amount 7500.00 \
114
+ --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01 \
115
+ | jq -r .placement_id)
116
+
117
+ modern-collections documents upload invoice-4411.pdf --placement $P --type invoice
118
+ modern-collections documents upload bol-4411.pdf --placement $P --type bol
119
+ modern-collections documents process <document_id> # or let the worker pick it up
120
+
121
+ modern-collections placements context $P # what the platform actually read
122
+ modern-collections placements documents $P # everything on file
123
+
124
+ # fix what the extraction got wrong — append-only and audited
125
+ modern-collections placements amend $P --po-number PO-8871 --service-date 2026-01-20 \
126
+ --reason "read off the signed BOL"
127
+ modern-collections placements amend $P --amount 7250.00 --reason "credit memo applied"
128
+ modern-collections placements debtor $P --email ap@acmefreight.com
129
+ ```
130
+
131
+ Amendments are accepted **mid-collection**: the placement planner re-reads the
132
+ row on its next tick, so a correction reaches the next call or email without any
133
+ further action. Every edit writes an append-only history row plus an audit event.
134
+
135
+ One amendment kind per invocation — `--amount`, `--notes`/`--clear-notes`, or any
136
+ combination of the contract-metadata flags (`--invoice-number`, `--invoice-date`,
137
+ `--due-date`, `--po-number`, `--service-date`, `--currency`,
138
+ `--written-contract`). A placement's *status* is deliberately not amendable here;
139
+ use `payments record`, `disputes resolve`, or `placements recall`, which own the
140
+ side effects.
141
+
142
+ ### Everything else
143
+
144
+ ```bash
145
+ modern-collections intake csv-preview aging.csv # bulk placement from an AR aging file
146
+ modern-collections intake csv-commit <preview_id> --mappings '{"amount":"Balance"}'
147
+ modern-collections evidence get <placement_id> # how well-documented the file is
148
+ modern-collections disputes list --status open
149
+ modern-collections remittances list # money owed back to you
150
+ modern-collections statements list && modern-collections exports create placements
151
+ modern-collections qbo status # QuickBooks link
152
+ modern-collections agreements current
153
+ ```
154
+
155
+ `-o json` emits machine-readable output; the default is a human table. It is a
156
+ **global** option, so it goes *before* the subcommand (`modern-collections -o json analytics
157
+ summary`, not `modern-collections analytics summary -o json`) — the same is true of `--env`,
158
+ `--base-url`, `--api-key`, `--creditor`, and `--timeout`. Exit codes: `0` success,
159
+ `1` API/runtime error, `2` configuration/usage error.
160
+
161
+ ## MCP server
162
+
163
+ Hosted, on the API, for any MCP client that can take a URL. Send the creditor
164
+ key on the request. The process does not keep a key of its own, and a request
165
+ without `Authorization: Bearer` cannot fall through to one.
166
+
167
+ ```json
168
+ {
169
+ "mcpServers": {
170
+ "modern-collections": {
171
+ "url": "https://api.moderncollections.io/mcp",
172
+ "headers": {
173
+ "Authorization": "Bearer ca_..."
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ A partner key also sends `X-MC-Creditor: <creditor uuid or external ref>`.
181
+
182
+ Local stdio (the same tools, key from the environment):
183
+
184
+ ```bash
185
+ MC_API_ENV=local MC_API_KEY=ca_... modern-collections-mcp
186
+ ```
187
+
188
+ `modern-collections-mcp --http` serves the same streamable-HTTP endpoint on
189
+ `http://127.0.0.1:8765/mcp` for a machine that should not use the hosted URL.
190
+ Set `MC_API_ENV` or `MC_API_BASE_URL` in that process. The key still comes from
191
+ the client's `Authorization` header, not from `MC_API_KEY`.
192
+
193
+ The MCP server exposes the **same** surface as the CLI — one tool per client
194
+ method, grouped as: placements (`placement_create`, `placement_status`,
195
+ `placement_list`, `placement_recall`, `placement_set_posture`, `placement_calls`,
196
+ `placement_emails`, `placement_payments`, `placement_compliance_decision`,
197
+ `placement_refresh_enrichment`), editing (`placement_amend`,
198
+ `placement_update_debtor`), documents (`document_upload`, `document_list`,
199
+ `document_status`, `document_process`, `document_match_decide`, `document_batch`,
200
+ `placement_documents`, `placement_context`, `placement_context_rebuild`), improve
201
+ and evidence (`placement_improve`, `improve_overview`, `evidence_*`), disputes
202
+ (`dispute_*`), money (`payment_*`, `remittance_*`, `statement_*`, `export_*`),
203
+ reporting (`analytics_summary`, `analytics_series`, `audit_query`), settings
204
+ (`settings_*`, `notification_settings_*`), integrations (`qbo_*`), agreements
205
+ (`agreement_*`), and intake (`intake_*`).
206
+
207
+ > **Tools that start real outreach:** `placement_create`, `intake_csv_commit`,
208
+ > `intake_file_confirm`, `intake_draft_resolve` (approve), `intake_structured_submit`.
209
+ > There is no sandbox on this surface. `placement_recall` is the stop button and is
210
+ > exposed as a tool for exactly that reason.
211
+ >
212
+ > **Binding or destructive:** `agreement_accept` (legally binding), `dispute_resolve`,
213
+ > `qbo_disconnect`, `intake_webhook_rotate_secret` (invalidates the previous secret),
214
+ > `notification_settings_replace` (replaces the whole grid).
215
+ >
216
+ > **Hosted `/mcp` differences:** `agreement_accept` is refused there, because the
217
+ > acceptance record would carry the server's loopback address instead of the
218
+ > signer's; accept the MSA in the dashboard or call `POST /v1/agreements/accept`
219
+ > from your own system. The binary tools (`settings_agent_skills`,
220
+ > `agreement_certificate`) write a file only over stdio; over HTTP they return
221
+ > the bytes as `content_base64` with a suggested `filename`.
222
+
223
+ Every tool also takes a trailing `creditor` argument (creditor UUID or external
224
+ reference) — required to reach any data at all when `MC_API_KEY` is a partner key
225
+ (`pa_...`); harmless and unnecessary with an ordinary creditor key. It falls back
226
+ to `MC_API_CREDITOR` when omitted, so a partner-scoped MCP server can be
227
+ registered once per creditor without repeating it on every tool call.
228
+
229
+ The document loop closes from an agent session: `document_upload` with a
230
+ `placement_id`, then `document_process`, then `placement_context` to read back the
231
+ extracted fields, then `placement_amend` to correct anything wrong — including
232
+ `po_number` and `service_date`. Amendments apply mid-collection and are audited.
233
+
234
+ The document/improve tools close the evidence loop from an agent session:
235
+ `improve_overview` ranks the placements whose collection file needs attention,
236
+ `placement_improve` returns one placement's plan (missing documents with
237
+ where-to-look and search hints), and `document_upload` sends a found file —
238
+ either `content_base64` or `file_path` read from a directory explicitly allowed
239
+ with `MC_API_MCP_UPLOAD_ROOT`. Encoding is handled **client-side in the MCP
240
+ server**; the API itself takes the raw bytes
241
+ (`POST /v1/documents` with `Content-Type` + `X-Filename`), so there is no
242
+ base64 endpoint and no inflated payload on the wire.
243
+
244
+ `mcp.example.json` is the stdio config. Destructive tools set MCP
245
+ `destructiveHint` (outreach starters, `placement_recall`, `placement_amend`,
246
+ `agreement_accept`, `dispute_resolve`, `qbo_disconnect`, intake secret rotation,
247
+ notification replace, intake address delete). For Claude Code stdio:
248
+
249
+ ```bash
250
+ claude mcp add modern-collections -- modern-collections-mcp
251
+ # then set MC_API_ENV / MC_API_KEY in the server's environment
252
+ # with a partner key (pa_...), also set MC_API_CREDITOR to the linked creditor
253
+ # optional: set MC_API_MCP_UPLOAD_ROOT=/path/to/approved-documents to enable file_path uploads
254
+ ```
255
+
256
+ ## Agent skills
257
+
258
+ `skills/` holds Claude Code / Claude Desktop skills that drive this same API —
259
+ `modern-collections` (core operations) and `collections-ar-triage` (AR aging export →
260
+ reviewed placement batch). Partners download them from **Settings → Developer access**
261
+ (`GET /v1/settings/agent-skills.zip`).
262
+
263
+ ## Client library
264
+
265
+ ```python
266
+ from mc_api import MCClient
267
+
268
+ with MCClient(env="prod", api_key="ca_...") as api:
269
+ placement = api.create_placement(
270
+ {
271
+ "invoice_amount": "7500.00",
272
+ "invoice_number": "INV-4411",
273
+ "invoice_date": "2026-01-01",
274
+ "due_date": "2026-02-01",
275
+ "debtor": {"company_name": "Acme Freight", "primary_email": "ap@acme.co"},
276
+ }
277
+ )
278
+ print(api.get_placement(placement["placement_id"]))
279
+ ```
280
+
281
+ With a partner key, add `creditor=` (or set `MC_API_CREDITOR`) to scope every call
282
+ made through that client instance to one linked creditor:
283
+
284
+ ```python
285
+ with MCClient(env="prod", api_key="pa_...", creditor="<creditor uuid>") as api:
286
+ print(api.list_placements())
287
+ ```
288
+
289
+ ## Surface parity
290
+
291
+ The client, the CLI, and the MCP server cover the same creditor-API-key surface,
292
+ 1:1. `tests/test_parity.py` holds the mapping table and fails the build if a client
293
+ method gains no CLI command and no MCP tool (or vice versa), so the three front-ends
294
+ cannot silently drift apart.
295
+
296
+ One endpoint is deliberately **not** reachable from any of them:
297
+ `POST /v1/settings/api-key/rotate` is gated on a dashboard session and rejects
298
+ `ca_*` bearers by design, so a leaked key cannot mint its own replacement. Rotate
299
+ from **Dashboard → Settings → API Key**. `MCClient.rotate_api_key()` exists only to
300
+ raise with that explanation rather than let a caller guess.
301
+
302
+ ## Safety properties
303
+
304
+ - The API key lives only in the request `Authorization` header — excluded from
305
+ `repr()`, exceptions, and logs. Same treatment for the creditor scope (`creditor=`
306
+ / `MC_API_CREDITOR`): never in `repr()`, and both it and the API key are stripped
307
+ from the one unauthenticated request the client makes (`health()`).
308
+ - Explicit connect/read/write timeouts.
309
+ - Only idempotent `GET`s are retried (429/5xx, exponential backoff); `POST`/`PATCH`/
310
+ `DELETE` are never auto-retried, so a create or payment can't be duplicated.
311
+ - No production default environment.
312
+ - Client-side validation for the amendment allow-list and enum-ish arguments, so a
313
+ typo fails locally instead of burning a round-trip.
314
+ - `POST /v1/intake/structured` is HMAC-signed over the exact bytes sent — the body is
315
+ serialised once and posted as raw content so the signature can't desync from it.
316
+
317
+ ## Test
318
+
319
+ ```bash
320
+ uv run --with 'mcp<2' --with typer --with pytest --with pytest-asyncio \
321
+ pytest clients/mc_api/tests -q
322
+ ```