stickafile-mcp
# stickafile-mcp
An MCP server that lets a coding agent upload a file from disk to
[Stickafile](https://stickafile.com) and hand back a shareable link. The
bytes go from disk to storage and never enter the model's context.
Two tools:
- `push(path, portal?, note?)` → `{ url, name, size }`
- `list_portals()` → the portals the token can push to
A push makes a standalone link by default. Pass a portal token when the file
belongs in a project's portal instead.
## Install
1. Sign in at [stickafile.com](https://stickafile.com), open the gear menu,
choose **api tokens**, and create one.
2. Register the server with Claude Code, pasting the token:
```
claude mcp add stickafile --scope user -e STICKAFILE_TOKEN=sf_... -- npx -y stickafile-mcp
```
3. In a project, ask for a link to a file:
> push dist/report.pdf and give me the link
The server checks the token once at startup. A revoked, expired, or
wrong-server token makes it exit with a message in the client's MCP log
instead of failing on the first push. A network failure at startup does not
block it; the first call reports the problem instead.
Any MCP client works; the equivalent `.mcp.json` entry is:
```json
{
"mcpServers": {
"stickafile": {
"type": "stdio",
"command": "npx",
"args": ["-y", "stickafile-mcp"],
"env": { "STICKAFILE_TOKEN": "sf_..." }
}
}
}
```
## Configuration
| Variable | Required | Meaning |
|---|---|---|
| `STICKAFILE_TOKEN` | yes | An `sf_` API token. Scopes: list portals, push to portals, create links. |
| `STICKAFILE_ALLOW` | no | Directories `push` may read from, separated by `:` (`;` on Windows). Default: the directory the server was started in, which Claude Code sets to the project. |
| `STICKAFILE_PORTAL` | no | Default portal, by name or 8-character token. When set, a `push` without `portal` goes there instead of making a link. |
| `STICKAFILE_URL` | no | Base URL. Default `https://stickafile.com`. Point at a dev deploy to test. |
Set them with more `-e` flags on `claude mcp add`, or in the `env` block of
`.mcp.json`.
## Agent skill
[`skills/stickafile/`](skills/stickafile/SKILL.md) is an agent skill that
teaches an agent when to push a file, when to pass a portal, and what to do
when a push fails or is refused. It is also listed on
[Agensi](https://agensi.io). To use it directly, copy the folder into your
agent's skills directory (for Claude Code, `~/.claude/skills/stickafile/`).
## Links and portals
`push` without `portal` makes a **link**: a standalone file owned by your
account, listed under Links on the dashboard, with no portal involved. This is
the quick-send path and needs nothing configured beyond the token. The usual
limits apply: 2 GB and 24 hours on the free plan, 10 GB and 30 days with pack
credit.
`push` with `portal` puts the file in that **portal** instead, so it sits with
a project's other files and the portal's own settings (gate, password, shared
view) apply. The argument takes an 8-character portal token (e.g. `a1b2c3d4`),
never a name. A name is guessable and an agent can invent a plausible one; a
token has to come from `list_portals`.
`STICKAFILE_PORTAL` changes the default: when it is set, a push without
`portal` goes to that portal rather than making a link. An explicit `portal`
token still wins. If the configured portal is missing or deactivated, the push
fails rather than silently falling back to a link.
`note` is optional plain text, up to 500 characters, shown on the download
page under the file name. It is dropped on portals that have notes turned
off.
## Safety
`push` reads files, so it is the tool a prompt injection would aim at. The
controls below run in the server and do not depend on the model behaving.
- **`STICKAFILE_ALLOW` is the real boundary.** `path` must be absolute,
symlinks are resolved first, and the resolved path must sit inside one of
the allowed roots. The agent cannot widen the roots.
- **Deny list inside the roots.** Dotfiles and dot-directories (`.env`,
`.git`, `.ssh`, `.aws`, ...) and credential-shaped names are refused, with
no override. The name check strips backup and double extensions
(`id_rsa.bak`, `server.key.gz`) and matches credential words as whole tokens
(`aws_credentials`, `prod-secrets.yaml`), so a suffix or affix does not slip
a key or secret past it. As a side effect, a file whose name contains the
whole word `secret(s)` or `credential(s)` is refused even when it is not
sensitive (e.g. `secret_plans.txt`). If that is a false positive, you can
rename it or copy it to a neutral name yourself, by hand, and push that.
This is for the user to do; the agent is told never to rename or copy a
file to get it past a refusal.
- **Regular files only, and only what was vetted.** `vetPath` opens the file
once and the uploader reads from that descriptor, so the bytes uploaded are
the bytes that were checked — a symlink or file swapped in after the check
cannot redirect the upload. The descriptor's device and inode (on Windows,
volume serial and NTFS file ID) must match the entry that passed the name
checks, or the push is refused. Files with
more than one hard link are refused, because a hardlinked name can point at
an inode whose home is outside the workspace. If you do want to send one,
make a copy yourself by hand and push the copy; the agent is told to ask
you rather than make it. The API token never appears in tool output.
- **Portal choice.** A push to the wrong *shared* portal exposes the file to
everyone holding that portal's link. The default is therefore a link, which
only you and the people you send it to can open; the server never picks a
portal on its own. Requiring a token rather than a name for `portal` raises
the bar, but an agent that has called `list_portals` can still pass any
token it saw, and there is no setting that forbids the argument.
Claude Code's own permission prompt is the human confirmation. The path shown
there is the path that gets read.
- **Don't allowlist or auto-approve `push`.** Leave it out of
`permissions.allow` and don't answer its prompt with "don't ask again".
The checks above stop keys, secrets and files outside the workspace, but
they cannot tell an ordinary project file you meant to share from one a
prompt injection wants sent. Approving each upload yourself is the main
defence against an injected instruction uploading a project file.
### Known gap: a parent directory swapped mid-check
The open-once check pins the *file*: if the vetted entry is swapped for a
symlink, junction, or other file before the open, the identity check refuses
it. It does not pin the *directories above it*. A process that can write
inside an allowed root could replace a parent directory with a link to
somewhere else between the path being resolved and the file being opened,
then put it back; the open and the identity check would then agree on the
outside file. This applies on every platform. Closing it needs the
operating system's path for the open handle (`GetFinalPathNameByHandle`,
`F_GETPATH`, `/proc/self/fd`), which Node does not expose without a native
module. It matters only if something other than you can write into an
allowed folder and race the server, for example an agent whose shell is
sandboxed without network access while this server has it.
### Windows
Supported on Windows 10/11 and tested in CI on `windows-latest` (Node 20
and 22) alongside macOS and Linux. Separate `STICKAFILE_ALLOW` entries with
`;`, e.g. `C:\Users\me\work;D:\exports`; forward slashes and any drive-letter
case work. Windows-specific behaviour:
- NTFS alternate data streams (`notes.txt:payload`, `report.pdf::$DATA`) are
refused; only a file's main contents are uploaded.
- Reserved device names (`CON`, `NUL`, `COM1`, `con.txt`, ...) are refused,
and so are `\\.\` device paths and `\\?\GLOBALROOT` paths. `\\?\C:\...`
long paths work.
- 8.3 short names (`IDED~1`, `SSHDIR~1`) are resolved to the long name before
the name checks, so they cannot hide a denied name.
- Names ending in dots or spaces (`id_rsa.`, `server.pem `) are checked as if
the trailing characters were not there.
- Junctions are treated like symlinks: resolved first, then held to the root.
NTFS hard links are refused like any other multi-link file.
- Windows has no `O_NOFOLLOW`. A swap of the file between the check and the
open is caught by the file-ID check instead.
## No resume
A failed push restarts from the beginning; the abandoned server-side session
is cleaned up automatically. Because a retry re-sends the whole file, `push`
tells the agent not to retry on its own and to ask the user first.
## Development
```
npm run test:unit # path and deny-list tests; no network
STICKR_REPO=../stickr npm test # plus the full e2e against a local stickr checkout
```
The e2e boots the Stickafile backend from a checkout of the private `stickr`
repo, so outside contributors cannot run it; `npm run test:unit` is the suite
that runs anywhere.
`src/upload.js` is a port of `public/upload-core.js` in the stickr repo. The
retry, chunking, and presign-continuation logic must stay in step between the
two; see the header comment in each.
TDQS
Scored across 2 tools
push uploads a file and returns a link, while list_portals enumerates available portals; these are clearly distinct resource+action pairs with no overlap. The descriptions even clarify the dependency relationship between them without creating ambiguity.
Both names use lowercase snake_case, but 'push' is a bare verb while 'list_portals' follows the verb_noun convention. The deviation is minor and both remain readable and predictable in style.
Two tools is thin even for a focused file-sharing server; the primary action (push) plus one helper leaves no room for adjacent operations. It is defensible for a narrowly scoped uploader, but sits at the borderline of feeling under-provisioned.
The core upload-to-link workflow and portal discovery are covered, but there is no way to list files already in a portal, check upload status, delete/revoke a previously shared file, or manage portals beyond listing them. These are notable gaps for a file-sharing lifecycle.