Fresh Jots MCP Server
FreshJotsDotCom MCP 伺服器
文件
Bearer-token REST. Plain notes only. Available on the Dev and Team tiers. For the systemic pitch — every script gets its own notebook, append-by-filename for cron jobs and AI-session digests, real-world workflows — see /for/developers. Base URL: https://freshjots.com/api/v1. Machine-readable: the same API as an OpenAPI 3.1 document at /openapi.json, for client generators, Postman and agent tooling.
Quick reference — clients
Linux & macOS
Windows & JS
Ruby gem
Quick API use examples. Ready-to-copy curl for the common workflows: create, append-only logs, append by filename, list.
Working with folders? Create folders, drop notes into them on create, bulk-upload into a folder.
Heartbeat for programmers. One ping URL per source, checked every minute: cron, systemd, Docker, Kubernetes, Ruby, Python, Node, Go, PowerShell, and the JSON report. Guides for Docker, your own code, job workers and machines behind NAT.
On Windows, or prefer no install? Write to your account from PowerShell with zero install, or the npm / pip client — no WSL needed.
Authentication
Generate a personal token in /settings/api_tokens. Tokens are shown once on creation and stored as a one-way hash — copy to your password manager or shell profile immediately.
Authorization: Bearer mn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Missing or invalid token returns 401 unauthenticated. Token holder lacks API access (Free / Personal tier) returns 403 forbidden.
Scopes. Each token is minted with a permission level — read_write (full access, the default), read_only (GET / HEAD only), or write_only (POST / PATCH / PUT / DELETE only). A token can also be locked to a single note, so it can read or append to exactly that one stream and nothing else — handy for a per-script credential. A fourth scope, append, backs the browser extension: minted under Settings → Browser extension (open to every active subscriber, Personal included), it is POST-only and always locked to one note, so the extension can clip pages into that note and do nothing else. A request that falls outside the token's scope returns 403 forbidden with a message naming the restriction. Pick the scope when you create the token.
Team tokens
Workspaces on the Team tier mint tokens at /team/api_tokens (owner / admin only). Same bearer-token wire format, same endpoints, same error envelope — but every read and write resolves to the team's notes, folders, and storage pool rather than the actor's personal pool.
GET /noteslists only the team's notes; the actor's personal notes never appear.POST /notescreates withteam_idset; the row'suser_idrecords the actor (who wrote it) for the audit log at /team/audit_events./folders/*,/notes/bulk, and/notes/:id/moveare scope-aware — a team token only sees the team's folders, never personal ones, and refuses cross-pool placement with404 not_found.- Storage, rate limits, and per-tier caps come from the team's subscription, not the actor's. Bulk is enabled on every team token — the team subscription is the entitlement.
A team's token-active cap is 30 active tokens at once. Revoking lives at the same panel — revoked tokens stay listed for audit but stop authenticating immediately.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /notes | List notes (summary). Search ?q=<words> (the same full-text search as the app: title, filename and body, best match first unless ?sort is given). Filter ?format=plain|rich, ?folder_id=N (or none for un-foldered). Sort ?sort=created|updated|appended (default updated). Paginate ?limit=N&offset=N (max 200/page). |
| GET | /notes/:id | Full note (plain_body + byte_size). To follow a log: ?tail=50 returns the 50 most recent lines, ?since=<bytes> only what was appended after the body was that long (store the returned byte_size as the next since); slice in the response describes the cut. |
| POST | /notes | Create plain note. Body: {note: {title, plain_body, folder_id?, append_only?, client_encrypted?}}. client_encrypted: true stores a note you encrypted locally with your own key — kept verbatim, never read or indexed (personal accounts only; immutable after creation). |
| PATCH | /notes/:id | Update title / plain_body / settings. Settings include append_deadline_hours, alert_email, match_pattern and the webhook fields. Format is immutable. On append-only notes, content fields are refused but settings (folder_id, append_deadline_hours, alert_email, webhook_url, webhook_secret) are accepted. |
| DELETE | /notes/:id | Delete note. |
| POST | /notes/:id/append | Atomic append to plain_body. Body: {text}. |
| POST | /notes/:id/move | Move to folder. Body: {folder_id} or null. |
| GET | /notes/by-filename/:filename | Find by filename instead of id. Takes the same ?tail and ?since as GET /notes/:id. |
| PATCH | /notes/by-filename/:filename | Same body shape as PATCH /notes/:id, addressed by stream name. Useful for reconfiguring a script's note (deadline, webhook URL) without first looking up the id. |
| POST | /notes/by-filename/:filename/append | Stream addressing — append by filename. Creates the note if missing (append-only by default). On first touch, client_encrypted: true opens it as a client-encrypted stream — send one self-contained ciphertext line per append (personal accounts only). Ignored once the note exists. |
| POST | /notes/bulk | Up to 50 creates per call. Body: {notes: [...]}. |
| GET | /folders | List folders. |
| GET | /folders/:id | Show a single folder. |
| POST | /folders | Create folder. Body: {folder: {name}}. |
| PATCH | /folders/:id | Rename folder. |
| DELETE | /folders/:id | Delete folder. Notes inside are preserved (moved to the built-in default folder). |
Rate limits
- Dev: 600 reads / 60 writes / 300 appends per minute, per token. 3 active tokens. 15 GB storage. Bulk endpoint enabled.
- Team: 2,000 reads / 200 writes / 1,000 appends per minute, per token. 30 active tokens per team. 50 GB workspace storage. Bulk endpoint enabled. 50,000 plain notes / 1,000 rich notes team-wide.
Throttled requests return 429 rate_limited with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
The full per-tier breakdown — including per-note size caps, browser-surface throttles, pagination ceilings, token-expiry policy, and the export window — is on the Service limits page.
Idempotent retries
Every write endpoint (POST, PATCH, PUT, DELETE) accepts an Idempotency-Key request header. A repeated request with the same key + same body fingerprint replays the original response instead of running the action twice — safe for "did the previous request succeed before my timeout?" retries from cron jobs and CI scripts.
- Header:
Idempotency-Key: <your-key> - Key format: 8..255 characters. UUIDs work; any string in that range does.
- Replay window: 24 hours. After that, the same key is accepted as a fresh request.
- Replays carry
Idempotent-Replay: trueon the response so your client can tell.
A repeated key with a different body fingerprint returns 409 idempotency_key_conflict — the server refuses to silently overwrite a previous request's response. Either retry with the original body or generate a new key.
curl -X POST https://freshjots.com/api/v1/notes \
-H "Authorization: Bearer $FRESH_JOTS_TOKEN" \
-H "Idempotency-Key: cron-2026-05-06-evening-digest" \
-H "Content-Type: application/json" \
-d '{"note":{"title":"Evening digest","plain_body":"...","format":"plain"}}'
Error envelope
All error responses share the same shape:
{ "error": { "code": "validation_failed", "message": "Title can't be blank", "details": ["Title can't be blank"] } }
Stable error codes:
unauthenticated— missing / invalid token (401)forbidden— token lacks API access, is out of scope, or account unconfirmed (403)note_locked— content edit attempted on an append-only note; appends and deletion are allowed, edits are not (403)not_found— record absent or owned by another user (404)validation_failed— bad input or schema violation (422)cap_exceeded— note count over your tier's cap (422)storage_cap_exceeded— total bytes would exceed your storage cap (422)content_too_large— single note over per-format byte cap (413). An append-only note rolls over instead of refusing (see append-only notes), so on one this means the text alone exceeds the cap or the roll-over was blocked; the message says which.content_type_mismatch— attempted rich-note write via API (422)rate_limited— throttle window exceeded (429)idempotency_key_conflict— replay key reused with a different body (409). See Idempotent retries above.service_unavailable— backend temporarily unreachable; safe to retry with backoff (503)
Watchdog & webhooks
Two per-note knobs for turning a stream into a monitoring substrate. Both are configured via PATCH /notes/:id (or PATCH /notes/by-filename/:filename) and can also be set from the per-note Settings page in the web UI. Both require the note to be append-only; the underlying flag is set at create time ({"append_only": true}) or via the browser-side lock toggle.
Dead-man's-switch alerts
Set append_deadline_hours on an append-only note (1–720) and Fresh Jots emails you when no append has landed in that window. The next append clears the alert; if the script recovers and then fails again, you get a fresh email — no manual reset. Optional alert_email overrides the destination (defaults to your account email).
curl -X PATCH https://freshjots.com/api/v1/notes/by-filename/cron-jobs-prod \
-H "Authorization: Bearer $FRESHJOTS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":{"append_deadline_hours":2,"alert_email":"oncall@example.com"}}'
Content-match alerts
The dead-man's switch reports silence; a content-match rule reports a line. Set match_pattern on an append-only note (case-insensitive text, up to 200 characters) and Fresh Jots emails you when an append contains it, with the matching line in the email. A burst of matching lines sends one alert, then at most one more every 10 minutes while they keep coming; the note itself has every line. Destination is the same as the dead-man's switch: alert_email or your account email for a personal note, the workspace alert destination for a team note (a signed team_alert.content_match event when that destination is a webhook). The response field match_alerted_at shows the last alert.
curl -X PATCH https://freshjots.com/api/v1/notes/by-filename/cron-jobs-prod \
-H "Authorization: Bearer $FRESHJOTS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":{"match_pattern":"Traceback"}}'
Outbound webhooks
One URL for everything? Set a default alert webhook under Settings: a note or heartbeat slot with no URL of its own posts there (personal notes only; team notes use the workspace destination). Set webhook_url on an append-only note and every successful append POSTs the new content to your endpoint. By default the body is a signed JSON envelope — HMAC-SHA256 in the X-FreshJots-Signature header (format: sha256=<hex>) using the webhook_secret you configure; a blank secret signs with the empty string, and the secret is never read back via the API. Set webhook_format to a chat format — slack, discord, google_chat, mattermost, or rocket_chat — to send a native chat message instead (see "Payload formats" below).
Ten consecutive non-2xx responses (or transport failures) auto-disables the webhook so a dead receiver doesn't drain the job queue. Re-arm it by saving the note again — either with a new URL, or, once auto-disabled, with the URL unchanged (the deliberate save is the acknowledgement). webhook_failure_count and webhook_disabled_at are exposed on GET /notes/:id for monitoring.
curl -X PATCH https://freshjots.com/api/v1/notes/by-filename/payments-prod \
-H "Authorization: Bearer $FRESHJOTS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":{"webhook_url":"https://hooks.example.com/x","webhook_secret":"sk_..."}}'
Payload shape:
{
"event": "note.appended",
"delivered_at": "2026-05-02T12:34:56Z",
"delivery_id": "<uuid>",
"note": {
"id": 123, "filename": "payments-prod", "title": "payments-prod",
"byte_size": 4096, "last_appended_at": "2026-05-02T12:34:55Z"
},
"appended_text": "...new chunk (capped at 8 KB)...",
"appended_bytes": 42,
"appended_truncated": false
}
Payload formats
webhook_format controls the payload shape and accepts six values. generic (default — the signed note.appended envelope shown above, for your own server or an automation hub like Zapier / Make / n8n) is the only signed format. The other five are native chat messages — paste that platform's incoming-webhook URL and you're done, no adapter to write: slack (https://hooks.slack.com/services/...), discord (https://discord.com/api/webhooks/...), google_chat (https://chat.googleapis.com/v1/spaces/...), mattermost (a <your-host>/hooks/<token> URL), and rocket_chat (a <your-host>/hooks/<id>/<token> URL). Set it from the Settings page drop-down or from the same PATCH endpoint that takes webhook_url / webhook_secret; the current value rides back on every GET /notes/:id. An unset note defaults to generic.
Chat-format messages are clipped to fit each platform: Slack to 3 500 characters, Discord to 1 900 (under Discord's hard 2 000-character ceiling on the content field), and Google Chat, Mattermost, and Rocket.Chat to 4 000 each. The Generic appended_text preview is unchanged (capped at 8 KB; the full body is always reachable through GET /notes/:id).
Chat-format deliveries are not signed. None of the chat platforms' incoming webhooks can verify a signature, so the X-FreshJots-Signature header is omitted entirely for the slack, discord, google_chat, mattermost, and rocket_chat formats. For those the unguessable hook URL is the credential; treat it like a password. The signing rules in "Verifying deliveries" below apply to the Generic format only.
curl -X PATCH https://freshjots.com/api/v1/notes/by-filename/cron-jobs-prod \
-H "Authorization: Bearer $FRESHJOTS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":{"webhook_url":"https://hooks.slack.com/services/T.../B.../...","webhook_format":"slack"}}'
Verifying deliveries (Generic only)
Your endpoint is a public URL, so verify every delivery before trusting it. Set the same webhook_secret on your receiving server, recompute the HMAC over the raw, unparsed request body, and compare it to the X-FreshJots-Signature header with a constant-time comparison. Reject on mismatch before parsing JSON. (If you configured a blank secret, the key is the empty string.)
require "openssl"
require "active_support/security_utils"
SECRET = ENV.fetch("FRESHJOTS_WEBHOOK_SECRET") # the same value you set on the note
def verified?(raw_body, header)
expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", SECRET, raw_body)}"
header.to_s.bytesize == expected.bytesize &&
ActiveSupport::SecurityUtils.secure_compare(header.to_s, expected)
end
# Rails: verify request.raw_post (NOT params) against
# request.headers["X-FreshJots-Signature"], then head(:unauthorized) on false.
Live browser updates
Heads-up: when you have a note open in the web UI and an API write hits it, only append-only notes update in place — the browser subscribes to a per-user stream and replaces the body as new content lands, no refresh needed. Try it: open an append-only note in one tab, then curl an append from another window.
Editable (CRUD) notes updated through the API (PATCH /api/v1/notes/:id, POST /notes/:id/append on a non-locked note) are not pushed to an open browser tab — refresh the page to pick up the new content. This is by design: pushing mid-edit content swaps would clash with the editor's in-flight autosave.
Questions? Contact me directly.