Handoff
by jueluddinsf
README.md
<p align="center"><img src="assets/logo.svg" width="96" height="96" alt="Handoff logo"></p>
# Handoff: File Sharing for AI Agents
**Private file storage and controlled share links for AI agents, with no human signup.**
Handoff gives an agent somewhere to put a file and a safe way to hand it to someone else. That someone can be a human or another agent. The agent saves the file privately, then creates a share link with the rules it needs: an expiry, a maximum number of opens, an optional human approval step, and a specific recipient agent. It can check status, revoke, and read the access log at any time.
- **Remote MCP server:** `https://hand-off.space/mcp` (Streamable HTTP)
- **REST API:** `https://hand-off.space/v1` ([OpenAPI](https://hand-off.space/openapi.json), [llms.txt](https://hand-off.space/llms.txt))
- **Auth:** `Authorization: Bearer <api_key>`. An agent gets a key by calling `POST /v1/signup`, with no email, card or human account.
- **Registry name:** `io.github.jueluddinsf/handoff`
This repository holds docs and listing metadata. The service is hosted, so you have nothing to install.
## Use it when an agent needs to…
- **Deliver a report to a human.** The agent saves `report.pdf`, then sends a link that expires in 24 hours and opens at most 3 times.
- **Hand a file to another agent.** The link works only when the named recipient agent presents its own key.
- **Wait for a human before releasing a file.** The link stays locked (`pending_approval`) until the owner approves it.
- **Take something back.** Revoke a link, and see every successful and refused open.
## Quickstart: REST (3 calls)
```bash
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"}'
# → {"account_id":"…","api_key":"hf_…","agent_id":"my-agent","caps":{…}}
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\":\"$(printf '# Q3 report' | base64)\"}"
# → {"id":"FILE_ID","name":"report.md",…}
# 3. Create a share link
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}'
# → {"grant_id":"…","url":"https://hand-off.space/g/…"}
```
Give the `url` to the recipient. It downloads with a plain `GET`.
## Quickstart: MCP (3 tool calls)
1. Connect your client to `https://hand-off.space/mcp` (configs below).
2. Get a key. Call the `signup` MCP tool, or `POST /v1/signup` as shown above. Put the key in your client's `Authorization: Bearer …` header and reconnect.
> The keyless `signup` tool is rolling out. If your client shows only the six tools below, use `POST /v1/signup`.
3. Call `save_file` → `create_share_link`, then hand over the returned `url`.
### Tools
| Tool | What it does |
|---|---|
| `save_file` | Save a file privately: `name`, `mime`, `content_base64`. Returns the file id. No link is created. |
| `list_files` | List your files, newest first. |
| `create_share_link` | `file_id`, `audience` (`anyone_with_link` or `agent`), optional `audience_value` (the recipient's `agent_id`), `expires_in_hours`, `max_opens`, `requires_approval`. Returns `grant_id` and `url`. |
| `get_share_status` | Approval, expiry, revocation and `open_count` for a link, without using up an open. |
| `revoke_share` | Permanently revoke a link. |
| `get_access_log` | Up to the latest 1,000 opens and refusals for a link. |
## Client configuration
In each config below, replace `YOUR_HANDOFF_API_KEY` with your key, or use the environment-variable form shown.
### Claude Code
```bash
claude mcp add --transport http handoff https://hand-off.space/mcp \
--header "Authorization: Bearer YOUR_HANDOFF_API_KEY"
```
You can also commit a project-scoped `.mcp.json`, which is the file in this repo. Claude Code expands `${HANDOFF_API_KEY}` from your environment:
```json
{ "mcpServers": { "handoff": { "type": "http", "url": "https://hand-off.space/mcp",
"headers": { "Authorization": "Bearer ${HANDOFF_API_KEY}" } } } }
```
### Claude Desktop
Claude Desktop custom connectors don't take a static header, so bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) in `claude_desktop_config.json`:
```json
{
"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 (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
```json
{
"mcpServers": {
"handoff": {
"url": "https://hand-off.space/mcp",
"headers": { "Authorization": "Bearer ${env:HANDOFF_API_KEY}" }
}
}
}
```
### VS Code / GitHub Copilot (`.vscode/mcp.json`)
```json
{
"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 (`cline_mcp_settings.json`)
Set `"type": "streamableHttp"` explicitly. If you leave it out, Cline falls back to legacy SSE.
```json
{
"mcpServers": {
"handoff": {
"type": "streamableHttp",
"url": "https://hand-off.space/mcp",
"headers": { "Authorization": "Bearer YOUR_HANDOFF_API_KEY" },
"disabled": false,
"autoApprove": []
}
}
}
```
### Windsurf (`~/.codeium/windsurf/mcp_config.json`)
```json
{
"mcpServers": {
"handoff": {
"serverUrl": "https://hand-off.space/mcp",
"headers": { "Authorization": "Bearer ${env:HANDOFF_API_KEY}" }
}
}
}
```
### OpenAI Agents SDK (Python)
```python
import asyncio, os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main():
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:
agent = Agent(
name="Reporter",
instructions="Write the report, save it with Handoff, and reply with a share link that expires in 24h.",
mcp_servers=[handoff],
)
print((await Runner.run(agent, "Summarize Q3 sales into report.md and share it.")).final_output)
asyncio.run(main())
```
### LangChain / LangGraph (`langchain-mcp-adapters`)
```python
import os
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
client = MultiServerMCPClient({
"handoff": {
"transport": "streamable_http",
"url": "https://hand-off.space/mcp",
"headers": {"Authorization": f"Bearer {os.environ['HANDOFF_API_KEY']}"},
}
})
tools = await client.get_tools()
agent = create_agent("openai:gpt-4.1", tools) # or langgraph.prebuilt.create_react_agent
```
### Hermes Agent (`~/.hermes/config.yaml`)
```yaml
mcp_servers:
handoff:
url: "https://hand-off.space/mcp"
headers:
Authorization: "Bearer YOUR_HANDOFF_API_KEY"
```
### Any other MCP client
| Setting | Value |
|---|---|
| URL | `https://hand-off.space/mcp` |
| Transport | Streamable HTTP (stateless, `POST` only) |
| Header | `Authorization: Bearer <api_key>` |
More step-by-step setup for agents is in [`llms-install.md`](llms-install.md).
### Agent Skill (no MCP needed)
This repo also ships an [Agent Skill](skills/handoff/SKILL.md) that teaches any skills-capable agent (Claude Code, Codex, Copilot, Cursor and others) to use the REST API:
```bash
npx skills add jueluddinsf/handoff-mcp
```
## Use with Grok Bot
Add Handoff as a custom MCP connector:
| Setting | Value |
|---|---|
| URL | `https://hand-off.space/mcp` |
| Header | `Authorization: Bearer <api_key>` |
Get a key with the `signup` MCP tool, or with `POST /v1/signup` if that tool is not listed. The key is shown once. Put it in the header and reconnect.
Or have the bot follow [`skills/handoff/SKILL.md`](skills/handoff/SKILL.md) and call the REST API over plain HTTP.
Example prompts:
- Save this report to Handoff and send me a link that expires in 24h.
- Share this file with my other agent only.
- Show me who opened the link, then revoke it.
## Recipes
### Agent → human report, with an expiry
`save_file(report.pdf)` → `create_share_link(audience="anyone_with_link", expires_in_hours=24, max_opens=3)` → send the `url`. Use `get_access_log` to confirm it was opened.
### Agent → agent handoff
Agent A calls `create_share_link(audience="agent", audience_value="agent-b")`. Agent B downloads with its own key:
```bash
curl -L -H "authorization: Bearer $AGENT_B_KEY" -o out.bin "$URL"
```
Any other caller gets `401 {"error":"unauthorized"}`.
### Human-approval gate
`create_share_link(..., requires_approval=true)`. Until the owner approves, opens return `403 {"error":"pending_approval"}`. To approve or deny:
```bash
curl -X POST -H "authorization: Bearer $HANDOFF_API_KEY" $BASE/v1/grants/GRANT_ID/approve # or /deny
```
## Limits (free, unclaimed accounts)
- 100 MB total, 50 files, 10 MB per file, 200 successful opens per day.
- Allowed file types: txt, md, csv, json, pdf, png, jpg, gif, webp, svg, xml, yaml, log, docx, xlsx, pptx.
- Executables, scripts, HTML and archives are refused.
- Signup is rate-limited per IP.
### Download responses
| Status | Error | Meaning |
|---|---|---|
| `410` | `expired` | The link is past its expiry. |
| `429` | `limit` | The link hit its max opens or the daily cap. |
| `403` | `revoked` / `denied` / `pending_approval` | Revoked, denied, or still waiting for approval. |
| `401` | `unauthorized` | The link is for a different agent. |
## Security model
- Files are private by default. Every share is an explicit grant with its own token.
- Downloads are served as `application/octet-stream` attachments with `Content-Security-Policy: sandbox` and `nosniff`.
- Every attempt, successful or refused, is logged.
- API keys are stored hashed and shown once.
Report security issues privately (see [CONTRIBUTING.md](CONTRIBUTING.md)).
## License
[MIT](LICENSE). This applies to the docs and examples in this repository. The hosted service is operated by Juel Uddin.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues