Metadata-Version: 2.5
Name: modern-collections
Version: 0.1.0
Summary: Official client, CLI, and MCP server for the Modern Collections external REST API
Project-URL: Homepage, https://moderncollections.io
Project-URL: Documentation, https://docs.moderncollections.io
Author: Modern Collections
License: Proprietary
Keywords: accounts-receivable,api-client,cli,collections,mcp
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: mcp<2,>=1.2
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# modern-collections — Modern Collections client, CLI, and MCP server

One hardened HTTP client over the Modern Collections **external REST API** (the
creditor-API-key surface), with two front-ends:

- **CLI** — `modern-collections`
- **MCP server** — `modern-collections-mcp` (stdio, or streamable HTTP on the API at `/mcp`)

The client depends only on the public HTTP contract, so it ships independently of the
backend application.

## Connect an agent

No install. Point an MCP client at the hosted server:

- URL: `https://api.moderncollections.io/mcp` (streamable HTTP)
- Header: `Authorization: Bearer <creditor API key>`
- Partner keys also send `X-MC-Creditor: <creditor uuid or external ref>`

`GET /v1/public/mcp` returns that card for whatever host you are calling.
To file an invoice: `placement_create` (confirm first — this starts real
outreach), then `document_upload` with `document_type` `invoice`, then
`document_process`. `placement_recall` stops a placement that should not
have been opened.

## Install

The install below is only for the local CLI and the stdio server. The hosted
URL does not need it.

```bash
uv tool install modern-collections   # CLI `modern-collections` and server `modern-collections-mcp`
# or: pip install modern-collections
```

Requires Python 3.11 or later. The PyPI project `mc-api` is unrelated; this package is
`modern-collections`.

## Configure

Pick an environment (no production default — by design) and supply a creditor API key.
The key is read from `MC_API_KEY` and is **never printed or logged**.

> **Getting a key.** Dashboard → **Settings → API Key**. Only the bcrypt hash is stored,
> so an existing key can never be re-displayed — Settings shows the key id
> (`ca_<prefix>`) and a rotate action that reveals a fresh secret exactly once. A lost
> key is rotated, not recovered. Setup snippets for the CLI, the MCP server, and the
> agent skills live under **Settings → Developer access**.

| Var | Values |
| --- | --- |
| `MC_API_ENV` | `local` \| `staging` \| `demo` \| `prod` |
| `MC_API_BASE_URL` | explicit base URL (overrides `MC_API_ENV`) |
| `MC_API_KEY` | `ca_<prefix>_<secret>` (creditor) or `pa_<prefix>_<secret>` (partner) |
| `MC_API_CREDITOR` | creditor UUID or external reference — only with a partner key |

Environment → base URL map:

| env | base URL |
| --- | --- |
| `local` | `http://localhost:8000` |
| `staging` | `https://web-staging-c8aa.up.railway.app` (Modern Collections internal) |
| `demo` | `https://app.demo.moderncollections.io` |
| `prod` | `https://api.moderncollections.io` |

### Partner keys and the creditor scope

A **partner** key (`pa_<prefix>_<secret>`) acts on behalf of several linked
creditors instead of just one, so every request has to say which creditor it is
for. Pass `--creditor`, set `MC_API_CREDITOR`, or construct `MCClient(creditor=...)`
with a creditor UUID or its `external_creditor_ref`; the client sends it as the
`X-MC-Creditor` header on every request. An ordinary creditor key (`ca_...`) is
already scoped to one creditor and ignores the header — `creditor` is simply a
no-op for it, so day-to-day creditor scripts and skills need no changes.

```bash
export MC_API_ENV=prod MC_API_KEY=pa_... MC_API_CREDITOR=<creditor-uuid>
modern-collections placements list                          # scoped to that one creditor

# or override per invocation
modern-collections --creditor <other-creditor-uuid-or-ref> placements list
```

Omitting `--creditor`/`MC_API_CREDITOR` with a partner key gets a `400` from the
API (`X-MC-Creditor header required`) — the client does not guess.

## CLI

```bash
export MC_API_ENV=prod MC_API_KEY=ca_...

modern-collections health                                   # reachability
modern-collections placements create --company "Acme Freight" --amount 7500.00 --email ap@acme.co \
  --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01
modern-collections placements list --status in_outreach
modern-collections placements get <placement_id>
modern-collections payments record <placement_id> --amount 500.00 --idempotency-key <bank-ref>
modern-collections -o json analytics summary
modern-collections disputes ack <placement_id> --note "spoke with AP"
modern-collections improve                                  # placements whose file needs attention
modern-collections improve <placement_id>                   # per-placement plan (missing documents)
modern-collections documents upload invoice-4411.pdf --placement <placement_id> --type invoice
modern-collections documents list --placement <placement_id>
modern-collections documents process <document_id>          # classify/extract/match now
```

### File an invoice, read it back, then correct it

The whole loop is unified on the placement id — submit the invoice and any
supporting PDFs against it, read back what was extracted, then amend:

```bash
P=$(modern-collections -o json placements create --company "Acme Freight" --amount 7500.00 \
      --invoice-number INV-4411 --invoice-date 2026-01-01 --due-date 2026-02-01 \
      | jq -r .placement_id)

modern-collections documents upload invoice-4411.pdf  --placement $P --type invoice
modern-collections documents upload bol-4411.pdf      --placement $P --type bol
modern-collections documents process <document_id>          # or let the worker pick it up

modern-collections placements context $P                    # what the platform actually read
modern-collections placements documents $P                  # everything on file

# fix what the extraction got wrong — append-only and audited
modern-collections placements amend $P --po-number PO-8871 --service-date 2026-01-20 \
      --reason "read off the signed BOL"
modern-collections placements amend $P --amount 7250.00 --reason "credit memo applied"
modern-collections placements debtor $P --email ap@acmefreight.com
```

Amendments are accepted **mid-collection**: the placement planner re-reads the
row on its next tick, so a correction reaches the next call or email without any
further action. Every edit writes an append-only history row plus an audit event.

One amendment kind per invocation — `--amount`, `--notes`/`--clear-notes`, or any
combination of the contract-metadata flags (`--invoice-number`, `--invoice-date`,
`--due-date`, `--po-number`, `--service-date`, `--currency`,
`--written-contract`). A placement's *status* is deliberately not amendable here;
use `payments record`, `disputes resolve`, or `placements recall`, which own the
side effects.

### Everything else

```bash
modern-collections intake csv-preview aging.csv             # bulk placement from an AR aging file
modern-collections intake csv-commit <preview_id> --mappings '{"amount":"Balance"}'
modern-collections evidence get <placement_id>              # how well-documented the file is
modern-collections disputes list --status open
modern-collections remittances list                         # money owed back to you
modern-collections statements list && modern-collections exports create placements
modern-collections qbo status                               # QuickBooks link
modern-collections agreements current
```

`-o json` emits machine-readable output; the default is a human table. It is a
**global** option, so it goes *before* the subcommand (`modern-collections -o json analytics
summary`, not `modern-collections analytics summary -o json`) — the same is true of `--env`,
`--base-url`, `--api-key`, `--creditor`, and `--timeout`. Exit codes: `0` success,
`1` API/runtime error, `2` configuration/usage error.

## MCP server

Hosted, on the API, for any MCP client that can take a URL. Send the creditor
key on the request. The process does not keep a key of its own, and a request
without `Authorization: Bearer` cannot fall through to one.

```json
{
  "mcpServers": {
    "modern-collections": {
      "url": "https://api.moderncollections.io/mcp",
      "headers": {
        "Authorization": "Bearer ca_..."
      }
    }
  }
}
```

A partner key also sends `X-MC-Creditor: <creditor uuid or external ref>`.

Local stdio (the same tools, key from the environment):

```bash
MC_API_ENV=local MC_API_KEY=ca_... modern-collections-mcp
```

`modern-collections-mcp --http` serves the same streamable-HTTP endpoint on
`http://127.0.0.1:8765/mcp` for a machine that should not use the hosted URL.
Set `MC_API_ENV` or `MC_API_BASE_URL` in that process. The key still comes from
the client's `Authorization` header, not from `MC_API_KEY`.

The MCP server exposes the **same** surface as the CLI — one tool per client
method, grouped as: placements (`placement_create`, `placement_status`,
`placement_list`, `placement_recall`, `placement_set_posture`, `placement_calls`,
`placement_emails`, `placement_payments`, `placement_compliance_decision`,
`placement_refresh_enrichment`), editing (`placement_amend`,
`placement_update_debtor`), documents (`document_upload`, `document_list`,
`document_status`, `document_process`, `document_match_decide`, `document_batch`,
`placement_documents`, `placement_context`, `placement_context_rebuild`), improve
and evidence (`placement_improve`, `improve_overview`, `evidence_*`), disputes
(`dispute_*`), money (`payment_*`, `remittance_*`, `statement_*`, `export_*`),
reporting (`analytics_summary`, `analytics_series`, `audit_query`), settings
(`settings_*`, `notification_settings_*`), integrations (`qbo_*`), agreements
(`agreement_*`), and intake (`intake_*`).

> **Tools that start real outreach:** `placement_create`, `intake_csv_commit`,
> `intake_file_confirm`, `intake_draft_resolve` (approve), `intake_structured_submit`.
> There is no sandbox on this surface. `placement_recall` is the stop button and is
> exposed as a tool for exactly that reason.
>
> **Binding or destructive:** `agreement_accept` (legally binding), `dispute_resolve`,
> `qbo_disconnect`, `intake_webhook_rotate_secret` (invalidates the previous secret),
> `notification_settings_replace` (replaces the whole grid).
>
> **Hosted `/mcp` differences:** `agreement_accept` is refused there, because the
> acceptance record would carry the server's loopback address instead of the
> signer's; accept the MSA in the dashboard or call `POST /v1/agreements/accept`
> from your own system. The binary tools (`settings_agent_skills`,
> `agreement_certificate`) write a file only over stdio; over HTTP they return
> the bytes as `content_base64` with a suggested `filename`.

Every tool also takes a trailing `creditor` argument (creditor UUID or external
reference) — required to reach any data at all when `MC_API_KEY` is a partner key
(`pa_...`); harmless and unnecessary with an ordinary creditor key. It falls back
to `MC_API_CREDITOR` when omitted, so a partner-scoped MCP server can be
registered once per creditor without repeating it on every tool call.

The document loop closes from an agent session: `document_upload` with a
`placement_id`, then `document_process`, then `placement_context` to read back the
extracted fields, then `placement_amend` to correct anything wrong — including
`po_number` and `service_date`. Amendments apply mid-collection and are audited.

The document/improve tools close the evidence loop from an agent session:
`improve_overview` ranks the placements whose collection file needs attention,
`placement_improve` returns one placement's plan (missing documents with
where-to-look and search hints), and `document_upload` sends a found file —
either `content_base64` or `file_path` read from a directory explicitly allowed
with `MC_API_MCP_UPLOAD_ROOT`. Encoding is handled **client-side in the MCP
server**; the API itself takes the raw bytes
(`POST /v1/documents` with `Content-Type` + `X-Filename`), so there is no
base64 endpoint and no inflated payload on the wire.

`mcp.example.json` is the stdio config. Destructive tools set MCP
`destructiveHint` (outreach starters, `placement_recall`, `placement_amend`,
`agreement_accept`, `dispute_resolve`, `qbo_disconnect`, intake secret rotation,
notification replace, intake address delete). For Claude Code stdio:

```bash
claude mcp add modern-collections -- modern-collections-mcp
# then set MC_API_ENV / MC_API_KEY in the server's environment
# with a partner key (pa_...), also set MC_API_CREDITOR to the linked creditor
# optional: set MC_API_MCP_UPLOAD_ROOT=/path/to/approved-documents to enable file_path uploads
```

## Agent skills

`skills/` holds Claude Code / Claude Desktop skills that drive this same API —
`modern-collections` (core operations) and `collections-ar-triage` (AR aging export →
reviewed placement batch). Partners download them from **Settings → Developer access**
(`GET /v1/settings/agent-skills.zip`).

## Client library

```python
from mc_api import MCClient

with MCClient(env="prod", api_key="ca_...") as api:
    placement = api.create_placement(
        {
            "invoice_amount": "7500.00",
            "invoice_number": "INV-4411",
            "invoice_date": "2026-01-01",
            "due_date": "2026-02-01",
            "debtor": {"company_name": "Acme Freight", "primary_email": "ap@acme.co"},
        }
    )
    print(api.get_placement(placement["placement_id"]))
```

With a partner key, add `creditor=` (or set `MC_API_CREDITOR`) to scope every call
made through that client instance to one linked creditor:

```python
with MCClient(env="prod", api_key="pa_...", creditor="<creditor uuid>") as api:
    print(api.list_placements())
```

## Surface parity

The client, the CLI, and the MCP server cover the same creditor-API-key surface,
1:1. `tests/test_parity.py` holds the mapping table and fails the build if a client
method gains no CLI command and no MCP tool (or vice versa), so the three front-ends
cannot silently drift apart.

One endpoint is deliberately **not** reachable from any of them:
`POST /v1/settings/api-key/rotate` is gated on a dashboard session and rejects
`ca_*` bearers by design, so a leaked key cannot mint its own replacement. Rotate
from **Dashboard → Settings → API Key**. `MCClient.rotate_api_key()` exists only to
raise with that explanation rather than let a caller guess.

## Safety properties

- The API key lives only in the request `Authorization` header — excluded from
  `repr()`, exceptions, and logs. Same treatment for the creditor scope (`creditor=`
  / `MC_API_CREDITOR`): never in `repr()`, and both it and the API key are stripped
  from the one unauthenticated request the client makes (`health()`).
- Explicit connect/read/write timeouts.
- Only idempotent `GET`s are retried (429/5xx, exponential backoff); `POST`/`PATCH`/
  `DELETE` are never auto-retried, so a create or payment can't be duplicated.
- No production default environment.
- Client-side validation for the amendment allow-list and enum-ish arguments, so a
  typo fails locally instead of burning a round-trip.
- `POST /v1/intake/structured` is HMAC-signed over the exact bytes sent — the body is
  serialised once and posted as raw content so the signature can't desync from it.

## Test

```bash
uv run --with 'mcp<2' --with typer --with pytest --with pytest-asyncio \
  pytest clients/mc_api/tests -q
```
