Skip to content

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. Clients such as curl, a browser, or any HTTP client then use that URL directly to download or upload the file.

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.

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

Ask the LLM (or call the tool directly):

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

The tool returns:

{
  "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:

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 link is grace-settled: its remaining lifetime shrinks to MARKDOWN_VAULT_MCP_TRANSFER_GRACE_TTL_S (default 60 seconds) so a stalled transfer can retry. Once that window passes, a further request returns HTTP 404.

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

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

The tool returns:

{
  "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.

By default, the destination must be a new file. Link creation rejects existing notes and attachments because upload links have no if_match option. Choose a new path, or have the operator set MARKDOWN_VAULT_MCP_WRITE_PROTECT_EXISTING=false to allow blind overwrites. With that opt-out, uploading replaces an existing file.

Step 2: upload the file

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

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 file is available in the vault, and the link is grace-settled in the same way as a download. The FTS index is updated and the git-commit callback fires (when git integration is configured).

The write guard checks again when the bytes arrive. If another writer creates the destination after link creation, the upload returns HTTP 409 Conflict and preserves that file. With protection enabled, retrying after a successful upload also returns 409 because the destination now exists, even while the token remains valid. A conflict releases the token reservation without extending its expiry; repeating the request keeps returning 409 while the file exists.

Raw body, not multipart

Send the file bytes directly as the request body. Avoid multipart/form-data; the endpoint reads raw bytes. curl's --data-binary flag sends raw bytes and is correct; --form sends multipart and will be rejected.

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, so treat the URL with the same care as a short-lived password.

One-time use (grace-settled)

A valid token grants exactly one operation on one vault path. On success the link is grace-settled rather than burned outright: its remaining lifetime shrinks to MARKDOWN_VAULT_MCP_TRANSFER_GRACE_TTL_S (default 60 seconds). A transfer that was served but stalled part-way can still reclaim the link. Uploads remain subject to overwrite protection, which refuses a retry if the first upload already created the file.

A transient failure (network drop, size limit exceeded, server error) releases the reservation with the full remaining TTL, so the transfer can be retried until expiry. If a handler crashes mid-transfer, its in-flight reservation frees itself after MARKDOWN_VAULT_MCP_TRANSFER_LEASE_S (default 60 seconds) and the token becomes claimable again.

Short TTL

Tokens expire after a configurable TTL, and that TTL is the security-relevant bound. The server default is 1 hour; the configurable ceiling is 24 hours. An expired token is rejected the moment it is used.

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 route and both tools are registered only on an HTTP or SSE transport, because the /transfer/{token} route needs an HTTP server. On stdio they are absent from the tool listing rather than present and failing.
  • BASE_URL required. The same registration also requires MARKDOWN_VAULT_MCP_BASE_URL, which supplies the public origin for the capability URL. Without it the tools do not appear.

Persistence

The token store is KV-backed (MARKDOWN_VAULT_MCP_KV_STORE_URL, on-disk state by default), so links minted before a restart remain valid afterwards for the rest of their TTL.

Configuration reference

Variable Default Description
MARKDOWN_VAULT_MCP_BASE_URL (none) 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_GRACE_TTL_S 60 Grace window after a successful transfer. The token's remaining lifetime shrinks to this, so a stalled or retried transfer can reclaim the link
MARKDOWN_VAULT_MCP_TRANSFER_LEASE_S 60 Reclaim window for an in-flight reservation. A crashed handler's token becomes claimable again once this lease lapses
MARKDOWN_VAULT_MCP_TRANSFER_MAX_UPLOAD_BYTES 104857600 (100 MiB) Per-upload size cap; exceeded bodies are rejected with HTTP 413

See Configuration for the full details.