# Publishing the MCCP app

The contract for the build agent: how a finished build and its changelog get onto
dasmaffin.com. The app is the overlay half of MCCP, the Moddable Console Capture Platform. This is
the whole of it - nothing here needs the site's source, an account, or the mods API.

**These are machine endpoints.** No account, no sign-in, no token refresh. Every call carries one
header:

```
X-Api-Key: <the machine key>
```

The key comes from whoever runs the site, out of band. It is not in this document and must not be
in your repository.

A `401` carries a reason your program can branch on:

| `error` | What happened | What to do |
| --- | --- | --- |
| `no_api_key` | No `X-Api-Key` header arrived at all | Your client is not sending it. A shell eating the header looks exactly like this |
| `unknown_api_key` | A key arrived and is not one of ours | Stop and ask a person. Retrying will not help |

Whether a refused key is a typo, an old key or a guess is deliberately not said, and neither is
whether the server has any keys configured. Which of the two above it is says nothing about the key
list - only whether your own request carried one - and that is exactly the bit worth knowing, since
it is the difference between *my client is broken* and *my credential is no longer good*.

**To test a key without publishing anything:**

```
GET /api/mccp/key
X-Api-Key: <the machine key>
```

`200` and `{"ok":true}`, or the same `401` as above. It changes nothing.

Base address: `https://dasmaffin.com`.

**The wiki is documented elsewhere.** Pushing developer documentation to `/Mccp/Wiki` uses the same
key and is written up in [the wiki contract](/docs/wiki/publishing),
which `GET /api/mccp/doc/wiki` will also hand you.

---

## 1. Upload a build

```
POST /api/mccp/app/{os}?name=mccp-setup.exe&version=1.2.0
X-Api-Key: <machine key>
Content-Type: application/octet-stream

<the file, raw - not multipart, not base64>
```

- `{os}` is a short lowercase name for what the build runs on: `windows`, `linux`, `macos`.
  Anything short, lowercase, letters-digits-hyphens works; the site shows friendly labels for the
  usual ones and the raw name for the rest.
- `name` is the filename a person's download arrives as. Optional; defaults to `mccp-{os}.zip`.
- `version` is a label shown beside the download button. Optional, never parsed - write whatever
  your release is called.

`201`:

```json
{ "os": "windows", "version": "1.2.0", "bytes": 48211233,
  "url": "/Mccp/app/windows/17", "newest": "/Mccp/app/windows" }
```

**Every build is kept.** Uploading adds; nothing is deleted and nothing is overwritten. A release
that breaks somebody's capture card leaves them with nothing to go back to if the version that
worked an hour ago was deleted the moment the new one arrived - so the page leads with the newest
and offers the rest behind an arrow beside the button.

Two addresses come back and they mean different things:

- `newest` - `/Mccp/app/{os}` - follows the newest build for that OS. This is the one to publish,
  to link, and to put in the app's updater; it goes on meaning "the current build" forever.
- `url` - `/Mccp/app/{os}/{id}` - is **this** build, and keeps meaning this build however many
  releases come after it. Worth putting in your release notes or a support reply.

Old builds are removed by hand when they are not worth their space. Nothing on the server prunes
them.

| Code | Meaning |
| --- | --- |
| `400` | `{os}` is not a plausible platform name |
| `401` | The key - see the table above for which kind. Do not retry in a loop |
| `413` | Over 64 MB. Nothing was kept |
| `502` | Storage refused it; worth one retry |
| `503` | File storage is not configured on the server; retrying will not help |

**Sizes are counted as the body arrives**, so a `413` can turn up after you finished sending.
Nothing is stored when it does.

---

## 2. Upload the changelog

```
POST /api/mccp/app/changelog
X-Api-Key: <machine key>
Content-Type: text/plain

1.2.0
- The overlay no longer eats the first keypress.
- Mods reload without a restart.

1.1.0
- Added the mod list.
```

**Send the section for the release you just built, not the whole file.** Each upload is added on
top of the ones before it, so the page shows every release with the newest first and the history
is already here. Posting the whole file every time would print every release again under the new
one.

Plain text, UTF-8, up to 256 KB per section. It is shown **as text** - markdown and HTML are not
rendered, so write for a reader, not a parser. `201` with `characters` (this section) and `whole`
(the changelog it is now part of); `400` for an empty body.

It read the other way round until 17 August 2026: an upload replaced what the page showed. That is
why the 0.3.0 upload - correctly sending only its own notes - took 0.2.0, 0.1.1 and 0.1.0 off the
page. Nothing was lost and they are back.

### Taking one back

```
DELETE /api/mccp/app/changelog
X-Api-Key: <machine key>
```

Removes the newest section from the changelog people read. Posting the right text over a mistake
stopped working when uploading started adding, and an agent that sends the wrong file at three in
the morning needs a way out that is not SQL.

`200` with what is left; `404` when there is nothing to take back. The section is hidden rather than
deleted - if it turns out the upload was right after all, the words are still in the database.

Send it after the builds, not before: the changelog describes what is downloadable, and for the
minute between the two it would describe builds that are not there yet.

---

## 3. Check what is live (public, no key)

```
GET /api/mccp/app
```

```json
{
  "builds": [
    { "os": "windows", "version": "1.2.0", "name": "mccp-setup.exe",
      "bytes": 48211233, "uploadedAt": "2026-08-13T12:00:00Z", "url": "/Mccp/app/windows",
      "versions": [
        { "version": "1.2.0", "name": "mccp-setup.exe", "bytes": 48211233,
          "uploadedAt": "2026-08-13T12:00:00Z", "url": "/Mccp/app/windows/17" },
        { "version": "1.1.0", "name": "mccp-setup.exe", "bytes": 47882101,
          "uploadedAt": "2026-08-02T09:14:00Z", "url": "/Mccp/app/windows/11" }
      ] }
  ],
  "changelog": "1.2.0\n- ...\n\n1.1.0\n- ..."
}
```

Use it to verify a publish landed, and ship it in the app as its update check - it needs no
account, so the app can ask before anybody signs in.

**Still one entry per OS**, still the newest build, still the same fields at the same names: an
updater reading `builds[]` is untouched by builds being kept. `versions` is the new part - every
build of that OS still on the server, newest first, each at its own permanent address.

The human-facing page is `https://dasmaffin.com/Mccp/App`; a person's download is
`GET /Mccp/app/{os}` for the newest, or `GET /Mccp/app/{os}/{id}` for one in particular. All of
them work signed out.

---

## A whole release, as commands

```
curl -f -X POST -H "X-Api-Key: $MCCP_PUBLISH_KEY" \
     -H "Content-Type: application/octet-stream" \
     --data-binary @dist/mccp-setup.exe \
     "https://dasmaffin.com/api/mccp/app/windows?name=mccp-setup.exe&version=1.2.0"

curl -f -X POST -H "X-Api-Key: $MCCP_PUBLISH_KEY" \
     -H "Content-Type: text/plain" \
     --data-binary @dist/notes-1.2.0.txt \
     "https://dasmaffin.com/api/mccp/app/changelog"

curl -fs https://dasmaffin.com/api/mccp/app
```

**The changelog file here is this release's section, not the whole changelog** - the releases before
it are already on the site. Sending the whole file would repeat every one of them.

`--data-binary`, never `-d`: `-d` strips newlines from the changelog and corrupts the build.
