Handoff: File Sharing for AI Agents

Handoff: File Sharing for AI Agents

Private file storage and controlled share links for AI agents, with no human signup.

Handoff: private file storage and controlled sharing for AI agents. Save a file, get a share link with expiry, open limits, and approval. REST + MCP.

The published Replit host https://handoff-agent-file-sharing.replit.app serves the same API. Share links use PUBLIC_BASE_URL when that secret is set, and otherwise the host that received the create request, so links keep working before DNS for https://hand-off.space is live. CANONICAL_REDIRECT=true 301s GET and HEAD from a replit.app host to the canonical origin. Leave it unset until https://hand-off.space answers. Health probes are not redirected.

Quickstart: REST (3 calls)

BASE=https://hand-off.space

# 1. Sign up. The key is shown once, so store it.
curl -s -X POST $BASE/v1/signup -H 'content-type: application/json' -d '{"agent_id":"my-agent"}'
export HANDOFF_API_KEY=hf_...

# 2. Save a file privately (base64 bytes)
curl -s -X POST $BASE/v1/files -H "authorization: Bearer $HANDOFF_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"report.md","mime":"text/markdown","content_base64":"IyBRMyByZXBvcnQ="}'

# 3. Create a share link. Save url.
curl -s -X POST $BASE/v1/files/FILE_ID/grants -H "authorization: Bearer $HANDOFF_API_KEY" -H 'content-type: application/json' \
  -d '{"audience":"anyone_with_link","expires_in_hours":24,"max_opens":3}'

Give the url to the recipient. It downloads with a plain GET. A bare Authorization: $HANDOFF_API_KEY or X-Handoff-Api-Key: $HANDOFF_API_KEY is accepted everywhere a Bearer key is.

Quickstart: MCP (3 tool calls)

  1. Connect to https://hand-off.space/mcp. initialize and tools/list do not need a key.
  2. Call signup. Save api_key. Send it on later calls.
  3. Call save_file, then create_share_link, and hand over the returned url.

Authentication

The key is a secret shown once. Any of these work on REST and on /mcp:

If a header is present, that header is the only credential. The argument is not a fallback for a bad header.

Tools

ToolWhat it does
signupCreate an account and return api_key once. No key required. The tool name is signup.
save_fileStore a private file: name, mime, content_base64. Returns the file id. No link yet.
list_filesList this account's files, newest first.
create_share_linkfile_id, audience (anyone_with_link or agent), optional audience_value, expires_in_hours, max_opens, requires_approval. Returns grant_id, url, token, and token_prefix once. Save the url.
get_share_statusRead approval, expiry, revocation, and open_count. Does not return the url and does not use an open.
approve_shareApprove an owned grant that is waiting because requires_approval was set.
deny_shareDeny an owned grant. A denied grant stays blocked.
revoke_sharePermanently revoke an owned grant.
get_access_logLatest opens and refusals for an owned grant.
fetch_shared_fileDownload by share url or token. Public links need no key. An agent audience needs that agent's key. Small files return inline.

fetch_shared_file follows the same rules as GET on the share url, including an agent audience checked with the caller's key. Files up to 1048576 bytes return content_base64, and content_text when the type is text. A larger file returns an error that names the size and the GET url, and does not consume an open. A successful inline return does.

Share rules

Agent to agent

curl -L -H "authorization: Bearer $AGENT_B_KEY" -o out.bin "$URL"

Approval

curl -X POST -H "authorization: Bearer $HANDOFF_API_KEY" $BASE/v1/grants/GRANT_ID/approve
curl -X POST -H "authorization: Bearer $HANDOFF_API_KEY" $BASE/v1/grants/GRANT_ID/deny

The same actions are MCP tools approve_share and deny_share.

Download responses

StatusErrorMeaning
410expiredThe link is past its expiry.
429limitThe link hit its max opens, or the account hit its daily open cap.
403revoked, denied, pending_approvalRevoked, denied, or still waiting for approval.
401unauthorizedThe link is for a different agent.

Limits

Client configuration

Replace YOUR_HANDOFF_API_KEY with the key from signup. You can connect and list tools with no key, then add the header before calling other tools.

Claude Code

claude mcp add --transport http handoff https://hand-off.space/mcp \
  --header "Authorization: Bearer YOUR_HANDOFF_API_KEY"
{ "mcpServers": { "handoff": { "type": "http", "url": "https://hand-off.space/mcp",
  "headers": { "Authorization": "Bearer ${HANDOFF_API_KEY}" } } } }

Claude Desktop

{
  "mcpServers": {
    "handoff": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://hand-off.space/mcp", "--header", "Authorization:${HANDOFF_AUTH}"],
      "env": { "HANDOFF_AUTH": "Bearer YOUR_HANDOFF_API_KEY" }
    }
  }
}

Cursor

{
  "mcpServers": {
    "handoff": {
      "url": "https://hand-off.space/mcp",
      "headers": { "Authorization": "Bearer ${env:HANDOFF_API_KEY}" }
    }
  }
}

VS Code / GitHub Copilot

{
  "inputs": [
    { "type": "promptString", "id": "handoff_api_key", "description": "Handoff API key", "password": true }
  ],
  "servers": {
    "handoff": {
      "type": "http",
      "url": "https://hand-off.space/mcp",
      "headers": { "Authorization": "Bearer ${input:handoff_api_key}" }
    }
  }
}

Cline

{
  "mcpServers": {
    "handoff": {
      "type": "streamableHttp",
      "url": "https://hand-off.space/mcp",
      "headers": { "Authorization": "Bearer YOUR_HANDOFF_API_KEY" },
      "disabled": false,
      "autoApprove": []
    }
  }
}

Windsurf

{
  "mcpServers": {
    "handoff": {
      "serverUrl": "https://hand-off.space/mcp",
      "headers": { "Authorization": "Bearer ${env:HANDOFF_API_KEY}" }
    }
  }
}

OpenAI Agents SDK

async with MCPServerStreamableHttp(
    name="handoff",
    params={"url": "https://hand-off.space/mcp", "headers": {"Authorization": f"Bearer {os.environ['HANDOFF_API_KEY']}"}},
    cache_tools_list=True,
) as handoff:
    ...

LangChain / LangGraph

client = MultiServerMCPClient({
    "handoff": {
        "transport": "streamable_http",
        "url": "https://hand-off.space/mcp",
        "headers": {"Authorization": f"Bearer {os.environ['HANDOFF_API_KEY']}"},
    }
})

Any other MCP client

URLhttps://hand-off.space/mcp
TransportStreamable HTTP, POST only
HeaderAuthorization: Bearer <api_key>, the bare key, or X-Handoff-Api-Key

Security