# One-Time Transfer Links

One-time transfer links let vault files move between the server and a browser or another service without passing the bytes through the LLM context window. The LLM calls a tool to mint a short-lived capability URL; that URL is then used directly by `curl`, a browser, or any HTTP client to download or upload the file.

!!! note "HTTP/SSE transport only"
    Transfer links require a running HTTP or SSE server with `MARKDOWN_VAULT_MCP_BASE_URL` configured. They are not available on stdio transport.

## When to use transfer links

Use transfer links when you need to move a file between the vault and something outside the conversation:

- **Downloading** a large PDF, image, or binary attachment to a local machine without base64-encoding it through the LLM.
- **Uploading** a file produced by an external tool (a script, a browser export, a compiled artifact) into the vault without reading it into context first.
- **Handing off bytes to another service** — a data pipeline, a browser-based viewer, or a CI job — that can speak HTTP but does not have MCP client support.

In every case the file bytes travel directly over HTTP; the LLM receives only the metadata (`url`, `path`, `expires_at`, `expires_in_seconds`).

## Download walkthrough

### Step 1 — mint a download link

Ask the LLM (or call the tool directly):

```
create_download_link(path="reports/q1.pdf", ttl_seconds=600)
```

The tool returns:

```json
{
  "url": "https://mcp.example.com/transfer/AbCdEfGhIjKlMnOpQrStUvWxYz01234567890ab",
  "path": "reports/q1.pdf",
  "expires_at": "2026-06-05T14:10:00+00:00",
  "expires_in_seconds": 600
}
```

The URL is a one-time capability URL. No authentication header is required to fetch it.

### Step 2 — fetch the file

On any machine that can reach the server:

```bash
curl "https://mcp.example.com/transfer/AbCdEfGhIjKlMnOpQrStUvWxYz01234567890ab" \
     -o q1.pdf
```

Or open the URL in a browser — the server sets `Content-Disposition: attachment` so the browser downloads rather than renders.

After a successful download the token is consumed. A second request returns HTTP 404.

!!! tip "Token lifetime"
    If you do not specify `ttl_seconds`, the server uses `MARKDOWN_VAULT_MCP_TRANSFER_TTL_DEFAULT_S` (default 3600 seconds / 1 hour). The maximum is `MARKDOWN_VAULT_MCP_TRANSFER_TTL_MAX_S` (default 86400 seconds / 24 hours). Shorter TTLs reduce the exposure window if the URL is accidentally shared.

## Upload walkthrough

### Step 1 — mint an upload link

```
create_upload_link(path="assets/new-diagram.png")
```

The tool returns:

```json
{
  "url": "https://mcp.example.com/transfer/ZyXwVuTsRqPoNmLkJiHgFeDcBa98765432109zy",
  "path": "assets/new-diagram.png",
  "expires_at": "2026-06-05T15:00:00+00:00",
  "expires_in_seconds": 3600
}
```

The destination path in the vault is fixed at link-creation time; the uploader cannot change it.

### Step 2 — upload the file

Send the raw file bytes as the request body using `POST`:

```bash
curl -X POST \
     --data-binary @new-diagram.png \
     "https://mcp.example.com/transfer/ZyXwVuTsRqPoNmLkJiHgFeDcBa98765432109zy"
```

`PUT` is also accepted as an alias for `POST`. Both behave identically.

After a successful upload the token is consumed and the file is available in the vault. The FTS index is updated and the git-commit callback fires (when git integration is configured).

!!! note "Raw body, not multipart"
    Send the file bytes directly as the request body. Do **not** use `multipart/form-data` — the endpoint reads raw bytes. curl's `--data-binary` flag sends raw bytes and is correct; `--form` sends multipart and is not.

## Security model

### Capability-URL authorization

The `/transfer/{token}` route is mounted outside the server's auth middleware. The token itself is the authorization: a URL-safe random string generated by `secrets.token_urlsafe(32)` (43 characters, 256 bits of entropy). Anyone who holds the URL can perform the transfer — treat the URL with the same care as a short-lived password.

### One-time use (burn-on-success)

A token is consumed on the **first successful** transfer. A failed request — network drop, size limit exceeded, server error — does **not** burn the token, so retry is permitted until the TTL expires. This is the "burn-on-success" pattern: the token survives transient failures but disappears once the bytes have moved.

### Short TTL

Tokens expire after a configurable TTL. The server default is 1 hour; the configurable ceiling is 24 hours. An expired token is rejected the moment it is used, and stale entries are purged from memory the next time a link is minted — no background thread is needed.

### Per-upload size cap

The upload route reads the request body up to `MARKDOWN_VAULT_MCP_TRANSFER_MAX_UPLOAD_BYTES` (default 100 MiB). A body that exceeds this limit is rejected with HTTP 413 and the token is returned to `available` so a smaller upload can retry.

### Fixed destination (upload)

The upload destination path is validated for path traversal and allowed extension at link-creation time. The uploader cannot override the destination. The write path re-validates on write as a defense-in-depth measure.

## Limitations

- **No HTTP range requests.** Each GET reads the entire file into memory and streams it. Partial downloads (`Range:` header) are not supported.
- **Raw body only.** The upload endpoint does not parse `multipart/form-data`. Send raw bytes.
- **HTTP/SSE transport only.** The tools raise an error on stdio transport because there is no HTTP server to service the request.
- **`BASE_URL` required.** Both tools raise an error if `MARKDOWN_VAULT_MCP_BASE_URL` is not configured.
- **In-memory token store.** Tokens are not persisted to disk. A server restart invalidates all outstanding tokens.

## Configuration reference

| Variable | Default | Description |
|----------|---------|-------------|
| `MARKDOWN_VAULT_MCP_BASE_URL` | — | Public base URL used to construct the capability URL. Required for transfer tools |
| `MARKDOWN_VAULT_MCP_TRANSFER_TTL_DEFAULT_S` | `3600` | Default token lifetime in seconds when `ttl_seconds` is omitted |
| `MARKDOWN_VAULT_MCP_TRANSFER_TTL_MAX_S` | `86400` | Maximum permitted TTL; requested values above this are clamped |
| `MARKDOWN_VAULT_MCP_TRANSFER_MAX_UPLOAD_BYTES` | `104857600` (100 MiB) | Per-upload size cap; exceeded bodies are rejected with HTTP 413 |

See [Configuration](../configuration.md#one-time-transfer-links) for the full details.
