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.
- modern_collections-0.1.0/.gitignore +12 -0
- modern_collections-0.1.0/PKG-INFO +345 -0
- modern_collections-0.1.0/README.md +322 -0
- modern_collections-0.1.0/mc_api/__init__.py +34 -0
- modern_collections-0.1.0/mc_api/_windows_download.py +193 -0
- modern_collections-0.1.0/mc_api/cli.py +1634 -0
- modern_collections-0.1.0/mc_api/client.py +1204 -0
- modern_collections-0.1.0/mc_api/config.py +87 -0
- modern_collections-0.1.0/mc_api/errors.py +57 -0
- modern_collections-0.1.0/mc_api/mcp_server.py +2062 -0
- modern_collections-0.1.0/pyproject.toml +57 -0
|
@@ -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
|
+
```
|