jottacloud-mcp
# jottacloud-mcp
**v0.1.0** — `@henrikogard/jottacloud-mcp` on npm.
Stdio [MCP](https://modelcontextprotocol.io) server so **Cursor**, **Grok Bot**, **Claude**, and **Codex** can read (and optionally write) [Jottacloud](https://www.jottacloud.com) files.
It is a local process: Cursor/Claude/Codex spawn it on your machine; Grok Bot runs the same `npx` command on its cloud VM. There is **no public HTTP endpoint**.
Jottacloud has no third-party OAuth app. Login is the same **personal login token** as `jotta-cli` / rclone.
**Author:** Henrik Øgård · **License:** [MIT](./LICENSE)
## Architecture
The server is a thin MCP adapter in front of Jottacloud’s unofficial JFS (XML) and `files/v1` (JSON) APIs. Hosts never talk to Jottacloud directly.
```mermaid
flowchart LR
subgraph hosts [MCP hosts]
Cursor
GrokBot["Grok Bot VM"]
Claude
Codex
end
subgraph mcp [jottacloud-mcp stdio]
Tools["Tools"]
Resources["jotta:/// resources"]
Client["JottaClient"]
Store["tokens.json 0600"]
end
subgraph jotta [Jottacloud]
OIDC["id.jottacloud.com OIDC"]
JFS["jfs.jottacloud.com XML"]
API["api.jottacloud.com JSON"]
end
Cursor -->|stdin / stdout| Tools
GrokBot --> Tools
Claude --> Tools
Codex --> Tools
Tools --> Client
Resources --> Client
Client --> Store
Client --> OIDC
Client --> JFS
Client --> API
```
### Auth
The personal login token from [jottacloud.com/web/secure](https://www.jottacloud.com/web/secure) is a one-shot base64url JSON blob (`username`, `auth_token`, `well_known_link`). The server exchanges it for access + refresh tokens and stores only those.
```mermaid
sequenceDiagram
participant User
participant Host as MCP host
participant MCP as jottacloud-mcp
participant OIDC as Jottacloud OIDC
participant JFS as JFS / files v1
User->>User: Create token at /web/secure
Note over User,Host: Prefer elicitation or env — not chat
User->>Host: login tool / JOTTACLOUD_LOGIN_TOKEN
Host->>MCP: stdio tools/call login
MCP->>OIDC: password grant client_id=jottacli
OIDC-->>MCP: access_token + refresh_token
MCP->>MCP: write ~/.config/jottacloud-mcp/tokens.json
MCP->>JFS: Bearer access_token
JFS-->>MCP: XML / JSON
MCP-->>Host: tool / resource result
```
Use the `login` tool (host elicitation) or `JOTTACLOUD_LOGIN_TOKEN`. Do not paste the token into chat transcripts.
### Path sandbox
Agent paths are mount-relative. They never include your Jottacloud username.
```mermaid
flowchart TD
In["/Archive/docs/note.md"] --> Norm["Normalize: reject .. and relatives"]
Norm --> Roots["Must sit under JOTTACLOUD_ROOTS<br/>default /Archive"]
Roots --> Map["JFS: /{user}/Jotta/Archive/docs/note.md<br/>allocate: /jfs/Jotta/Archive/docs/note.md"]
Map --> Caps["Size caps + read-only default"]
```
| Agent path | JFS URL |
| --- | --- |
| `/Archive/docs/note.md` | `https://jfs.jottacloud.com/jfs/{user}/Jotta/Archive/docs/note.md` |
| `jotta:///Archive/docs/note.md` | Same file, via MCP resources |
`/Trash` is **not** a default root. `list_trash` / `restore_file` use it explicitly so search and listing do not dump trash into ordinary results. Add Sync with `JOTTACLOUD_ROOTS=/Archive,/Sync`.
## Setup
```bash
npm install
npm run build
```
Node 20+.
1. Create a personal login token at [https://www.jottacloud.com/web/secure](https://www.jottacloud.com/web/secure).
2. Either leave it in `JOTTACLOUD_LOGIN_TOKEN` for the first MCP start, call the `login` tool, or exchange it once:
```bash
JOTTACLOUD_LOGIN_TOKEN='…' node dist/index.js --login
```
Refresh tokens are stored at `~/.config/jottacloud-mcp/tokens.json` (`0600`). Do not share that file. **One MCP process per store** — refresh-token rotation will kill the session if two processes race.
The personal login token is **one-shot**. After a successful exchange, the store is enough.
Default is **read-only**. Set `JOTTACLOUD_READ_ONLY=0` to enable write/mkdir/copy/move/delete/share.
## Cursor
Desktop Cursor: `~/.cursor/mcp.json` (or project `.cursor/mcp.json`):
```json
{
"mcpServers": {
"jottacloud": {
"command": "npx",
"args": ["-y", "@henrikogard/jottacloud-mcp"],
"env": {
"JOTTACLOUD_LOGIN_TOKEN": "paste-from-web-secure",
"JOTTACLOUD_ROOTS": "/Archive",
"JOTTACLOUD_READ_ONLY": "1"
}
}
}
}
```
After `--login` you can omit `JOTTACLOUD_LOGIN_TOKEN`.
**Cursor Cloud Agents** do not read `~/.cursor/mcp.json`. Add the server in the MCP dropdown at [cursor.com/agents](https://cursor.com/agents).
## Grok Bot
Grok Bot cannot use your laptop’s `mcp.json`. Ask the bot in chat (say **custom server**):
> Add a custom MCP server called jottacloud that runs: npx -y @henrikogard/jottacloud-mcp
> Set env JOTTACLOUD_LOGIN_TOKEN to my personal login token, JOTTACLOUD_READ_ONLY=1, and JOTTACLOUD_ROOTS=/Archive.
Until publish, use a built path the bot can run, or `npx` from npm once published. Put the token on the **server env**, not in later chat.
On Grok’s cloud computer the token store is that VM’s home dir. For the durable workspace:
`JOTTACLOUD_TOKEN_STORE=/workspace/jottacloud-mcp-tokens.json`
Attach with `@jottacloud` in chat.
This is **not** grok.com → Connectors (that path needs a public HTTPS MCP).
## Claude and Codex
Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` (same `mcpServers` JSON as Cursor). Fully quit Claude after edits.
Claude Code: `.mcp.json` or `~/.claude.json`.
Codex (`~/.codex/config.toml`):
```toml
[mcp_servers.jottacloud]
command = "npx"
args = ["-y", "@henrikogard/jottacloud-mcp"]
[mcp_servers.jottacloud.env]
JOTTACLOUD_LOGIN_TOKEN = "paste-from-web-secure"
JOTTACLOUD_ROOTS = "/Archive"
JOTTACLOUD_READ_ONLY = "1"
```
## Environment
| Variable | Role |
| --- | --- |
| `JOTTACLOUD_LOGIN_TOKEN` | One-shot personal login token (bootstrap) |
| `JOTTACLOUD_TOKEN_STORE` | Override token file path |
| `JOTTACLOUD_ROOTS` | Allowed path prefixes (default `/Archive`) |
| `JOTTACLOUD_READ_ONLY` | `1` (default) omits write tools; `0` enables them |
| `JOTTACLOUD_DEVICE` / `JOTTACLOUD_MOUNT` | Defaults `Jotta` / `Archive` |
| `JOTTACLOUD_LIVE_TEST` | `1` enables live API tests (not used in CI) |
## Tools and resources
Read (always):
| Tool | Role |
| --- | --- |
| `login` | Elicit / exchange a personal login token |
| `list_roots` | Configured roots + discovered mounts |
| `list_directory` | Folder listing with cursor pagination |
| `get_file_info` | Metadata only |
| `search_files` | `?mode=liststream` + local name filter; walk fallback |
| `read_file` | Capped text / base64; `head` / `tail` / `max_bytes` |
| `list_trash` | List `/Trash` without making it a general root |
Write (if `JOTTACLOUD_READ_ONLY=0`):
| Tool | Role |
| --- | --- |
| `write_file` | Create/overwrite (capped) |
| `create_directory` | `mkDir`, parents as needed |
| `copy_file` | `cp` / `cpDir` |
| `move_file` | `mv` / `mvDir` |
| `restore_file` | Move out of `/Trash` into an allowed root |
| `delete_file` | Soft-delete to trash; requires `confirm=true` |
| `create_share_link` / `revoke_share_link` | `enableShare` / `disableShare` |
**Resources:** `jotta:///{+path}` (for example `jotta:///Archive/docs/note.md`). Same size caps as `read_file`. The template is advertised; the whole account is not enumerated in `resources/list`.
Paths look like `/Archive/docs/note.md`.
## Development
```bash
npm test
npm run typecheck
npm run build
```
Live Jottacloud tests stay off unless you opt in (needs a real token / store; skipped in CI):
```bash
JOTTACLOUD_LIVE_TEST=1 npm test
```
Protocol smoke (initialize + `tools/list`) runs in CI as a stand-in for MCP Inspector. For the GUI:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
## Security
- Default root is `/Archive` only; `..` and paths outside the allow-list are rejected.
- Read-only by default; delete is trash-only and needs `confirm=true`.
- Tokens live in env or a `0600` file, never in tool arguments by design. Elicitation is the in-app login path.
- One process per token store (refresh rotation).
- Treat cloud file names and contents as untrusted (prompt injection).
## License
[MIT](./LICENSE) © 2026 Henrik Øgård
TDQS
Scored across 7 tools
Each tool has a clearly distinct purpose: authentication, listing roots, listing directory contents, metadata retrieval, searching, reading file content, and trash listing. No overlap or ambiguity between tools.
All tool names follow a consistent verb_noun pattern in snake_case (list_roots, get_file_info, search_files, etc.). Naming is predictable and uniform across the set.
Seven tools is well-scoped for a cloud storage MCP. Each tool serves a distinct function without redundancy or missing essentials, fitting comfortably in the ideal range.
The read-focused surface covers listing, searching, metadata, and file reading, plus trash handling. Write operations (upload, delete, move) are absent, but if the server is intended for read-only access, only minor gaps remain (e.g., no way to download complete files without streaming limits).