---
name: malaf
description: >
  Publish plans, upload files, and deploy static sites, then hand the user a
  short shareable link. Use when asked to "share this plan", "publish this",
  "upload this file", "host this", "give me a link for this PDF/image/CSV",
  "deploy this site", "put this online", or when a result is easier to open in
  a browser than to read in chat. No account is needed; an API key keeps links
  for 30 days, renewable.
---

# Malaf agent skill

Malaf is free storage for everything an agent makes. One HTTP call returns a
short, public URL. Anonymous results last 24 hours. Results made with an API
key live 30 days from their last update (republishing or renewing in the
dashboard resets the clock) and appear in the user's dashboard.

**Base URL:** `https://malaf.dev` (this document is served with the real origin filled in).
**Auth (optional):** `Authorization: Bearer <api key>` from `MALAF_API_KEY`.
**Contract:** `https://malaf.dev/api/openapi.json`
**Limits and identity:** `GET https://malaf.dev/api/v1/whoami`

Always return the `url` from the response to the user. Never invent a URL.
Every command below is copy-ready: `https://malaf.dev` is already the base URL.

## Decide what to publish

| The user wants… | Use |
| --- | --- |
| A document you wrote (plan, spec, report, notes, Markdown, HTML) | **Plan** → `POST /api/v1/plans` |
| A binary or data file (PDF, image, CSV, ZIP, audio, video, code) | **File** → `POST /api/v1/upload` |
| A multi-file website or app build | **Site** → `POST /api/v1/sites/deploy` |

## 1. Publish a plan (Markdown, HTML, or text)

```bash
curl -sS -X POST "https://malaf.dev/api/v1/plans" \
  -H "Authorization: Bearer $MALAF_API_KEY" \
  -H "Content-Type: text/markdown" \
  --data-binary @plan.md
```

With a raw body, options go in the query string:
`"https://malaf.dev/api/v1/plans?title=Launch%20plan&slug=launch-plan"`.
JSON also works: `{"markdown":"# Title\n…","title":"Optional","slug":"optional-slug","password":"optional"}`
(`html`, `text`, or `content` + `format` are accepted too.) Plans are capped at 4 MiB.

Response (201):

```json
{ "url": "https://quiet-harbor-142.malaf.dev", "slug": "quiet-harbor-142", "kind": "plan",
  "title": "Launch plan", "version": 1, "expiresAt": null, "claimToken": null }
```

Re-using a `slug` publishes a new version of the same link. Markdown is
rendered into a clean reader page with light/dark support; raw HTML is served
as-is.

## 2. Upload a file

```bash
curl -sS -X POST "https://malaf.dev/api/v1/upload" \
  -H "Authorization: Bearer $MALAF_API_KEY" \
  -F "file=@./report.pdf"
```

Options: `-F private=true` (workspace only), `-F tags=a,b` (up to 20 tags of
40 characters), header `Idempotency-Key: <8 to 200 chars, [A-Za-z0-9_-]>` for
safe retries. A raw body with `X-Filename: name.ext` (or `?filename=`) also
works. Bodies above 4 MiB must use the presigned flow below.

Response (201):

```json
{ "id": "…", "shortId": "k3x9a2mq7w",
  "url": "https://malaf.dev/f/k3x9a2mq7w/report.pdf",
  "shortUrl": "https://malaf.dev/f/k3x9a2mq7w",
  "previewUrl": "https://malaf.dev/f/k3x9a2mq7w/preview",
  "downloadUrl": "https://malaf.dev/f/k3x9a2mq7w/download",
  "name": "report.pdf", "type": "application/pdf", "size": 48213,
  "expiresAt": "2026-09-06T00:00:00.000Z", "claimToken": "…" }
```

Share `url` (inline) or `previewUrl` (a page with a rich preview). Use
`downloadUrl` when the user should save the file.

### Large files (over ~4 MB)

Use the presigned flow so bytes go straight to storage:

1. `POST /api/v1/upload/presign` with `{"filename","size","type"}` → `{uploadId, uploadUrl, headers}` (the PUT URL stays valid for 30 minutes)
2. `PUT` the raw bytes to `uploadUrl` with the returned `headers`
3. `POST /api/v1/upload/complete` with `{"uploadId"}` → the file record above
   (`DELETE /api/v1/upload/complete` with the same body cancels)

The CLI does this automatically: `npx -y malaf upload ./big.mp4`.

## 3. Deploy a static site

```bash
curl -sS -X POST "https://malaf.dev/api/v1/sites/deploy" \
  -H "Authorization: Bearer $MALAF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"my-demo","files":{"index.html":"<h1>Hello</h1>","app.js":"console.log(1)"}}'
```

Also accepts multipart (each field name is a path, or `file` = a ZIP) and a raw
ZIP body. Put `index.html` at the root; when every file sits under one folder
that folder becomes the root. Slugs are 3 to 48 lowercase letters, digits, and
hyphens; a few names are reserved. Up to 2,000 files per deploy. Add
`"password":"…"` for a protected site (workspace only). Sites over ~4 MB should
use `POST /api/v1/sites/deploy/begin` → PUT each file → `POST /api/v1/sites/deploy/commit`
(`DELETE` the same commit URL with `{"deploymentId"}` to abandon one),
or simply `npx -y malaf deploy ./dist`.

Response (201): `{ "url", "slug", "version", "files": [...], "totalSize", "expiresAt", "claimToken" }`

Manage: `GET /api/v1/sites`, `GET /api/v1/sites/{slug}`,
`POST /api/v1/sites/{slug}/rollback` (`{"version": 2}` optional),
`PATCH /api/v1/sites/{slug}` (`{"password": "…" | null}`),
`DELETE /api/v1/sites/{slug}`.

## CLI (no install)

```bash
npx -y malaf plan ./plan.md            # Markdown/HTML/text → page
npx -y malaf upload ./report.pdf       # any file → short link
npx -y malaf deploy ./dist             # folder with index.html → live site
npx -y malaf login                     # store a key (prompts, input hidden)
npx -y malaf ls | sites | plans | rm <id|slug> | claim <token…> | whoami
```

Each command prints the URL on stdout. Add `--json` for the full response
(errors are JSON on stdout too). Flags: `--slug`, `--title`, `--password`,
`--format`, `--private`, `--tag`, `--dry-run` (deploy). Hidden files, `.env*`,
and `node_modules` are never deployed.

## MCP

Streamable HTTP endpoint: `https://malaf.dev/api/mcp` (send the Bearer header if you
have a key). Tools: `publish_plan`, `upload_file`, `upload_from_url`,
`deploy_site` (text files as strings, binaries as `{content, encoding: "base64"}`),
`set_site_password`, `list_files`, `get_file`, `delete_file`, `list_sites`,
`get_site`, `delete_site`, `rollback_site`, `whoami`, `claim_links`.

```bash
claude mcp add malaf --transport http https://malaf.dev/api/mcp --header "Authorization: Bearer $MALAF_API_KEY"
```

Config file form (Claude Code expands `${MALAF_API_KEY}`; other clients may
need the key pasted in):

```json
{ "mcpServers": { "malaf": { "url": "https://malaf.dev/api/mcp",
  "headers": { "Authorization": "Bearer ${MALAF_API_KEY}" } } } }
```

## Keep anonymous links

Every anonymous response includes a `claimToken`. Tell the user: "This link
expires in 24 hours. Sign in to Malaf and paste the claim token, or run
`npx -y malaf claim <token>` with an API key, to keep it for 30 days and renew
any time." With a key, `POST /api/v1/claim` `{"tokens":["…"]}` (or the MCP
tool `claim_links`) moves them into the workspace.

## Limits

| | Anonymous | Workspace (free) |
| --- | --- | --- |
| Per file | 25 MiB | 50 MiB |
| Storage | 100 MiB per IP per day, 24h TTL | 200 MiB, 30-day lifetime |
| Per site | 10 MiB | 25 MiB |
| Sites and plans | unlimited, temporary | 50 |
| Versions kept | 2 | 5 |
| API requests | 60 / hour / IP | 2,000 / hour |
| API keys / webhooks | none | 5 / 5 |
| Private files, passwords, custom domains, webhooks | no | included |

Also: plans up to 4 MiB, 2,000 files per site, inline request bodies up to
4 MiB (use the presigned flows above that), slugs 3 to 48 characters.
There is no paid plan. The exact values are in `GET /api/v1/whoami`.

## Errors

```json
{ "error": { "code": "RATE_LIMITED", "message": "…", "details": {} } }
```

Codes: `VALIDATION_ERROR` (400), `UNAUTHORIZED` (401), `FORBIDDEN` /
`QUOTA_EXCEEDED` (403), `NOT_FOUND` (404), `DUPLICATE_SLUG` (409),
`PAYLOAD_TOO_LARGE` (413), `RATE_LIMITED` (429, honour `Retry-After`),
`STORAGE_UNAVAILABLE` (503, retry), `INTERNAL` (500).
Rate-limit state: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.

## What to tell the user

- Give them the `url`. For files mention the preview page if helpful.
- Anonymous: say it expires in 24 hours and how to keep it (claim token or sign in).
- Workspace: say it lives 30 days from the last update and can be renewed from the dashboard.
- On 429: wait `Retry-After` seconds, or suggest a free API key for a much higher allowance.
- Never paste API keys, gateway secrets, or storage keys into chat or files.
