# sharekit — file share + MCP server stack

Two services running as the `hermes` system user, exposed publicly via nginx + Let's Encrypt.

| Service | Port | URL | Purpose |
|---|---|---|---|
| `sharekit-fileserver` | 127.0.0.1:8789 | https://files.spannerjun.top | Public file share (browse/upload/download) |
| `sharekit-mcpserver` | 127.0.0.1:8790 | https://mcp.spannerjun.top/mcp | MCP server (12 tools) |

## Files

```
/home/hermes/
├── sharekit/
│   ├── file_server.py        # public file share (FastAPI)
│   ├── mcp_server.py         # MCP server (FastMCP streamable-http, 12 tools)
│   ├── sharekit-bootstrap.sh # install/configure systemd services
│   ├── skill-install.sh      # publish a single skill into sharekit
│   ├── .env                  # secrets (chmod 600, hermes:hermes)
│   └── static/...            # (reserved for future UI)
├── files/                    # share root
│   ├── readme.txt
│   └── skills/               # publicly mirrored hermes skill library
│       ├── smail-mail-SKILL.md
│       └── hermes-skills-YYYYMMDD-HHMM.tar.gz
└── venv/                     # Python 3.12 venv (fastapi + mcp SDK)
/etc/systemd/system/
├── sharekit-fileserver.service
└── sharekit-mcpserver.service
```

## systemd commands

```bash
systemctl status sharekit-fileserver
systemctl status sharekit-mcpserver
systemctl restart sharekit-fileserver
journalctl -u sharekit-fileserver -f   # tail logs
```

Both services run as user `hermes`, auto-restart on failure, and start on boot.

## One-shot scripts

### `sharekit-bootstrap.sh`

Install systemd units, enable, start, then bundle `~/.hermes/skills/` into the share. Idempotent.

```bash
sudo /home/hermes/sharekit/sharekit-bootstrap.sh
```

### `skill-install.sh`

Publish a single SKILL.md (or a directory of them) to the share's `/skills/` folder, and optionally register with local hermes so the running agent can use it.

```bash
# publish only
/home/hermes/sharekit/skill-install.sh /path/to/SKILL.md

# publish + register locally
/home/hermes/sharekit/skill-install.sh /path/to/SKILL.md --register

# publish a directory of skills
/home/hermes/sharekit/skill-install.sh --bundle /path/to/skills/ --register
```

The script prints the public URL on https://files.spannerjun.top/ for each installed skill.

## Auth

| Endpoint | Auth |
|---|---|
| `GET /browse`, `GET /raw` | public (no auth) |
| `POST /upload`, `POST /delete`, `POST /mkdir` | public by default; set `SHARE_ROOT_TOKEN` in `.env` to require `Authorization: Bearer *** 头 |
| `POST /mcp` initialize | public (no auth); Host header must match one of `SHARE_MCP_ALLOWED_HOSTS` |

To enable write gating on the file server:

```bash
echo "SHARE_ROOT_TOKEN=$(openssl rand -hex 24)" >> /home/hermes/sharekit/.env
systemctl restart sharekit-fileserver
```

To enable the litellm_* tools on the MCP server (currently off because we don't ship the master key in a unit file):

```bash
echo "LITELLM_MASTER_KEY=sk-lit..." >> /home/hermes/sharekit/.env
systemctl restart sharekit-mcpserver
```

## MCP tools (12)

```
list_dir, read_file, file_info, mkdir, upload_text, delete, search, share_link,
MiniMax_usage, opencode_quota, litellm_models, litellm_chat
```

### Connect from another agent

```python
import httpx, json

# 1. initialize
r = httpx.post("https://mcp.spannerjun.top/mcp",
    json={"jsonrpc":"2.0","id":1,"method":"initialize",
          "params":{"protocolVersion":"2025-03-26","capabilities":{},
                   "clientInfo":{"name":"client","version":"0"}}},
    headers={"Content-Type":"application/json","Accept":"application/json, text/event-stream"})
sid = r.headers["mcp-session-id"]

# 2. call a tool
r = httpx.post("https://mcp.spannerjun.top/mcp",
    headers={"mcp-session-id":sid, "Content-Type":"application/json",
             "Accept":"application/json, text/event-stream"},
    json={"jsonrpc":"2.0","id":2,"method":"tools/call",
          "params":{"name":"share_link","arguments":{"path":"readme.txt"}}})
print(r.text)
```

Sessions are stateful across requests (carry the `mcp-session-id` header).

## Persistence

- `sharekit-fileserver` restart policy: `on-failure`, 3s backoff. State is on the filesystem (every upload is permanent unless deleted).
- `sharekit-mcpserver` same. Sessions are in-memory; clients reconnect after a restart.
- `files/` survives across container / VM restarts because it's just `/home/hermes/files` on the host.
- venv at `/home/hermes/venv/` is a one-time setup; restore via `python3 -m venv /home/hermes/venv && /home/hermes/venv/bin/pip install fastapi httpx 'uvicorn[standard]' pydantic mcp` if re-deployed.

## Gotchas discovered

- FastMCP enforces DNS rebinding protection by default — must pass `transport_security=TransportSecuritySettings(allowed_hosts=[...])` or clients get 421 "Invalid Host header".
- FastMCP/uvicorn binds to 127.0.0.1 — nginx reverse-proxy MUST hardcode `proxy_set_header Host 127.0.0.1` to avoid 421 "Misdirected Request".
- venv must be readable by the `hermes` user: `chmod -R go+rX /home/hermes/venv` (or install fresh venv).
