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.
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:
{
"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¶
Step 1: mint an upload link¶
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.
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).
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 instead of being stranded by a spent one.
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_URLrequired. The same registration also requiresMARKDOWN_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.