@squirrelnotes.app/mcp 2.1.1 → 2.2.0-test.349
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -8
- package/dist/stdio-handler.js +1837 -167
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,10 +12,12 @@ The passphrase lives in `claude_desktop_config.json` on your machine and is pass
|
|
|
12
12
|
|
|
13
13
|
| Risk | Mitigation |
|
|
14
14
|
|------|------------|
|
|
15
|
-
| Passphrase in config file | Stored on disk in a local config file; protect it with OS user permissions and full-disk encryption |
|
|
16
|
-
| Passphrase in process memory |
|
|
15
|
+
| Passphrase in config file | Stored on disk in a local config file; protect it with OS user permissions and full-disk encryption. Anyone who can read this file has your passphrase. |
|
|
16
|
+
| Passphrase and key in process memory | The process derives your data key once, on its first tool call, and holds it in memory until the process exits, even while it is idle between requests. The MCP server never writes it to a file or a log. The operating system can still put it on disk, through swap or a crash dump, and a debugger running as your user can read it, as all of these could already read the passphrase in the process's environment. |
|
|
17
17
|
| Decrypted content visible to AI | This is the intended behaviour — AI agents need to read your notes |
|
|
18
18
|
|
|
19
|
+
**Ending a running session.** Because the stdio process holds your data key for its whole life, **changing your passphrase in the app does not stop a session that is already running.** A passphrase change only re-wraps the key; it does not change it. To end a running session, **revoke or rotate the API key** (Settings → API & Data). An idle process keeps the key until its next request that reaches the API-key guard. On that request the server rejects the key and the process drops it (the reference is released, not zeroed). A request that fails before the guard (a 400 or 413 from the body parser, an API Gateway 429, or a 5xx) does not drop the key, but serves no data. The process drops the key when the server answers 401 "Invalid API key" (a missing, unknown, revoked or rotated key), or 403 "API access requires a Pro subscription" (the key is valid but the account is no longer Pro). **After a revoked or rotated key**, every later call fails until the process is restarted with a valid key. **After a Pro lapse** the key is still valid, so every call fails only while the account is not Pro: once you subscribe again, the next call derives your data key afresh and the session works again, without a restart. A 403 that a route raises for another reason, such as a shared note you do not own, leaves the key held. Quitting the MCP client also ends the process.
|
|
20
|
+
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Tools
|
|
@@ -29,14 +31,53 @@ The passphrase lives in `claude_desktop_config.json` on your machine and is pass
|
|
|
29
31
|
| `get_note` | Returns a single note with decrypted title + full content. |
|
|
30
32
|
| `create_note` | Creates a note with encrypted title + content. |
|
|
31
33
|
| `update_note` | Updates title, content, or pinnedAt on a note. |
|
|
32
|
-
| `delete_note` |
|
|
34
|
+
| `delete_note` | Moves a note to trash (recoverable for 30 days). Its public share link is revoked at once. |
|
|
33
35
|
| `pin_note` | Pins or unpins a note (appears at top of sidebar). |
|
|
34
|
-
| `append_to_note` | Appends markdown content to an existing note.
|
|
36
|
+
| `append_to_note` | Appends markdown content to an existing note. Saves the agent a get → update, but it is **not** race-free: it reads the note and writes it back whole, and the server has no version check, so an edit made elsewhere in between is overwritten. |
|
|
35
37
|
| `set_note_tags` | Replaces all tags on a note. |
|
|
36
38
|
| `search_notes` | Fast title-only search across all notes. |
|
|
37
39
|
| `search_note_content` | Full-content search (slow — decrypts all notes). Use only when title search fails. |
|
|
38
40
|
| `move_note_to_collection` | Moves a note to a collection (or to scratchpad if collectionId is null). |
|
|
39
41
|
|
|
42
|
+
### Trash
|
|
43
|
+
|
|
44
|
+
| Tool | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `list_trash` | Lists trashed notes with decrypted titles and when each was trashed. Trashed notes are hard-deleted automatically 30 days after trashing. |
|
|
47
|
+
| `restore_note` | Restores a trashed note to its last collection. Public share links are not recreated. |
|
|
48
|
+
| `permanently_delete_note` | Permanently deletes one trashed note, its attachments and its encrypted content. Irreversible; the note must already be in trash. |
|
|
49
|
+
| `empty_trash` | Permanently deletes every trashed note and its attachments. Irreversible; requires `confirm: true`. |
|
|
50
|
+
|
|
51
|
+
### Collections
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `list_collections` | Lists regular collections (folders) with decrypted names and `parentId` for nesting. Smart collections are left out: their notes are computed, so nothing can be placed in them. |
|
|
56
|
+
| `get_collection` | Returns one collection with its decrypted name. |
|
|
57
|
+
| `create_collection` | Creates a collection from a plaintext `name` (encrypted before the API call), optionally nested under `parentId`, with an optional `iconColor` hex code. |
|
|
58
|
+
| `update_collection` | Changes a collection's name, parent or icon colour. Its kind cannot be changed. |
|
|
59
|
+
| `delete_collection` | Deletes a collection. Its notes move to the scratchpad; they are not deleted. |
|
|
60
|
+
|
|
61
|
+
### Tags
|
|
62
|
+
|
|
63
|
+
| Tool | Description |
|
|
64
|
+
|------|-------------|
|
|
65
|
+
| `list_tags` | Lists tags with decrypted names and colours. Use it to find tag IDs for `set_note_tags`. |
|
|
66
|
+
| `get_tag` | Returns one tag with its decrypted name. |
|
|
67
|
+
| `create_tag` | Creates a tag from a plaintext `name` (encrypted before the API call) and an optional `color` hex code. |
|
|
68
|
+
| `update_tag` | Changes a tag's name or colour. |
|
|
69
|
+
| `delete_tag` | Deletes a tag and removes it from every note that had it. |
|
|
70
|
+
|
|
71
|
+
### Sharing
|
|
72
|
+
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `share_note` | Publishes a note as a public link protected by a share passphrase, with optional `expiry` and `allowDownload` (default off). The note is decrypted here and re-encrypted under a key derived from the share passphrase, so the server never sees plaintext. Returns the share URL and token. The share passphrase is a tool argument, so it passes through your agent and its model provider; send it to recipients separately from the link. |
|
|
76
|
+
| `get_share_status` | Returns a note's share URL, view count, expiry and settings, or `null` if it is not shared. |
|
|
77
|
+
| `republish_share` | Re-encrypts an existing share with the note's latest content. Needs the share passphrase (the same one or a new one). |
|
|
78
|
+
| `update_share_settings` | Changes a share's `expiry` and/or `allowDownload` without changing its content. |
|
|
79
|
+
| `revoke_share` | Revokes a share link permanently; it stops working at once. The note is not affected. |
|
|
80
|
+
|
|
40
81
|
### Saved Searches (Pro)
|
|
41
82
|
|
|
42
83
|
| Tool | Description |
|
|
@@ -55,6 +96,86 @@ The passphrase lives in `claude_desktop_config.json` on your machine and is pass
|
|
|
55
96
|
| `delete_attachment` | Permanently deletes an attachment from S3 and the database. |
|
|
56
97
|
| `upload_attachment_from_path` | Uploads a file from a local path — **no file bytes in context**. Use after `write_file` (Filesystem MCP) to attach Claude-generated files. See [Filesystem MCP companion](#filesystem-mcp-companion-stdio-only) below. |
|
|
57
98
|
|
|
99
|
+
### Memory (Pro, feature 0031)
|
|
100
|
+
|
|
101
|
+
Short, atomic memories about you that an agent can recall in later sessions.
|
|
102
|
+
Each memory is encrypted on your machine under your data key, bound to its id
|
|
103
|
+
and revision (AES-GCM with AAD `sqn-memory-v2:<id>:<revision>`), so the server
|
|
104
|
+
stores only ciphertext. Someone holding only your API key cannot put an older
|
|
105
|
+
version back, or another memory's ciphertext in its place: it does not open.
|
|
106
|
+
**What this does not stop:** the server, or whoever operates it, returning a
|
|
107
|
+
whole older row (its ciphertext *and* its older revision). That opens, and
|
|
108
|
+
undoes later changes, because this MCP keeps no record of the revisions it has
|
|
109
|
+
seen. The app (PR 1b) will notice a revision lower than the one it cached; the
|
|
110
|
+
MCP does not (R-038).
|
|
111
|
+
|
|
112
|
+
| Tool | Description |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `search_memory` | Finds memories by the words in their text, entity and tags: BM25 over the decrypted set, times a recency decay per category (half-life 30 days for `project_state`, a year for preferences and mental models, two years for entity facts), with the app's trigram matching. Common English words and contraction fragments (a built-in list of 144: "what", "does", "he", "is", the "s" of "what's"…) are dropped from the query unless it has nothing else. The response names its `ranking` (`lexical` here; `hybrid` with the embeddings package; `none` for a listing) and `total` (matches before `limit`). Each hit has `score`, which orders that response only, and two scores that compare across searches: `lexicalScore` (0–1, the share of the query's content words the memory holds, a prefix counting half) and `semanticScore` (cosine similarity; always `null` in this package). Hits come in rank order, and the tool suggests no cut-off: a low score means lower confidence, not a wrong answer, and no score is a threshold (as an example only, correct hits on real memories have had a `semanticScore` of 0.42), so an agent is told to check low-scoring hits against the question before dismissing them (ADR-0444, amended 2026-10-13). `lexicalScore` is delivered rounded to two places. `query` is optional when `category` or `entity` is given: the matching memories are then listed, newest first, `limit` at a time; pass `nextCursor` back as `cursor` for the next page (`null` on the last, and always `null` for a search). A memory edited during a walk can be missed, so compare the count against `total` or start again. With neither a query nor a filter, the call is refused. The decrypted list is reused for up to 60 s, and re-read after any write from this process. Reads every page of `GET /public/memory` and searches the decrypted set on your machine; it never asks the server for one memory, so the server cannot tell which ones were consulted. Leaves out superseded and invalidated memories unless asked; `searched` counts the memories that state filter let through (active, plus superseded or invalidated when asked for), before any `category` or `entity` filter. A record that fails to decrypt or verify is skipped and counted (`unverifiable`), never shown. |
|
|
115
|
+
| `remember_memory` | Saves one memory (`text`, `category`, `entity`, optional `tags`, `sourceNoteId`). It is never pinned: only you pin, in the app. Returns near-duplicates so the agent can supersede instead of adding a copy. |
|
|
116
|
+
| `supersede_memory` | Replaces an active memory: the old one is kept, marked superseded and linked to the new one. Guarded by the memory's revision, so a concurrent change is refused (409), not overwritten. Both records are written unpinned, even if the old one was pinned. |
|
|
117
|
+
| `invalidate_memory` | Marks an active memory as wrong. The text is kept; the memory is written unpinned. |
|
|
118
|
+
| `get_memory_profile` | No inputs. The memories you have pinned (payload field `pinned`; only you pin, in the app), active ones only, most recently updated first (ties by id), cut at whole memories once text plus entity passes about 300 tokens (characters ÷ 4); the first is always included. Each item carries `needsReview`, `reviewedAt` and `provenance`. With nothing pinned it says that only you pin, in the app, and to list your preferences with `search_memory` (`category: "preference"`, no query) instead; the tool description and the server's instructions say the same, since a new store has nothing pinned. Also the resource `memory://profile`, with the same content, and named in the server's instructions so an agent loads it at the start of a session. Reads only the list, like `search_memory`. |
|
|
119
|
+
| `import_memories_from_note` | Turns a note (`noteId`) into candidates, one per list item and one per paragraph, with the heading above as the entity and `category` (default `entity_fact`) on each. Left out: struck-through text (`~~x~~` or `~x~`; a candidate struck through entirely is dropped), checked tasks (`[x]`) with everything nested under them, thematic breaks, and everything under a heading for undecided content until the next heading (ATX or setext) of the same or a higher level. Those headings start with "Open questions", "Questions", "Not decided", "Undecided", "Discussed but not decided", "Ideas", "Todo", "To do", "Parking lot" or "Scratch", case-insensitive, singular or plural, with or without a leading number ("3. Open question", "TODOs"); a line that is only bold text ("**Open questions**") counts as such a heading until the next heading or bold-only line. The match is a deliberately broad prefix match that errs on the side of skipping: "Ideas for the launch" and "Questions answered" are skipped too; only a heading that does not start with one of these words is kept ("Scratchpad", "Decided ideas"). `dryRun` defaults to `true` (returns candidates only); with `dryRun: false` it writes up to 100. The note is never changed. |
|
|
120
|
+
|
|
121
|
+
- **Every agent write is live and flagged.** It sets `needsReview: true`,
|
|
122
|
+
`author: 'agent'` and `flaggedAt` inside the ciphertext. `flaggedAt` (ISO
|
|
123
|
+
8601 UTC) is when the memory was flagged: the app's review badge counts 24
|
|
124
|
+
hours from it (ADR-0419 §4) and clears it to `null` when you review. A new
|
|
125
|
+
memory gets now. Superseding or invalidating a memory that is already
|
|
126
|
+
flagged keeps its time, and a supersede's new memory inherits it, so an
|
|
127
|
+
agent cannot restart the clock. It is optional in the payload (`v` stays
|
|
128
|
+
1), so older memories without it still open. A flagged memory is still searched
|
|
129
|
+
and used: the flag is for you, not a gate (ADR-0418 §5). The flag is set by
|
|
130
|
+
the MCP, so a modified client could leave it off (R-038).
|
|
131
|
+
- **Agents cannot delete or pin memories.** No tool sends a `DELETE`, and
|
|
132
|
+
`/public/memory` has no delete route. No agent write is pinned, so a pinned
|
|
133
|
+
memory that an agent supersedes or invalidates leaves your profile until
|
|
134
|
+
you pin it again (product owner, 2026-10-12). You delete and pin them in
|
|
135
|
+
the app.
|
|
136
|
+
- **Search matches ASCII letters and digits only**, as the app's search does:
|
|
137
|
+
accented and non-Latin letters are ignored, so text written only in them
|
|
138
|
+
cannot be found, and near-duplicate detection cannot see it. A query with no
|
|
139
|
+
searchable characters says so.
|
|
140
|
+
- **Up to 60 s stale.** Search and the profile reuse the decrypted list for up
|
|
141
|
+
to 60 s, so a change made in the app, or a revoked key, can take up to 60 s
|
|
142
|
+
to show in a running session (product owner, 2026-10-12; R-036). Writes
|
|
143
|
+
from this process, and a rejected key, clear it at once.
|
|
144
|
+
- **Memory text is data, not instructions.** The server instructions and every
|
|
145
|
+
memory tool description tell the agent so, and to treat a memory as
|
|
146
|
+
unverified while `needsReview` is true. Results carry `author`,
|
|
147
|
+
`provenance` (`via`, the client's self-asserted name, capped at 100
|
|
148
|
+
characters, and the tool) and `reviewedAt`.
|
|
149
|
+
- **Limits.** An account holds up to 5,000 memories and 4 MiB of memory
|
|
150
|
+
ciphertext; past either, writes return a clear "limit reached" error. Writes
|
|
151
|
+
are limited to 120 a minute per user, per route, per Lambda container
|
|
152
|
+
(ADR-0418 §9b, R-042), so not an exact account-wide limit.
|
|
153
|
+
- **Errors and logs never contain memory text.** Log lines carry the tool name,
|
|
154
|
+
ids, counts and HTTP status only.
|
|
155
|
+
- **stdio only.** The memory tools exist only in this stdio package
|
|
156
|
+
(ADR-0421 removed the hosted HTTP server).
|
|
157
|
+
|
|
158
|
+
#### Semantic memory search (opt-in)
|
|
159
|
+
|
|
160
|
+
This package's `search_memory` is lexical. The separate package
|
|
161
|
+
[`@squirrelnotes.app/mcp-embeddings`](https://www.npmjs.com/package/@squirrelnotes.app/mcp-embeddings) is this same
|
|
162
|
+
server (built from the same source and published from the same commit) with a
|
|
163
|
+
local embedding model, so `search_memory` also finds memories by meaning. To
|
|
164
|
+
opt in, change `args` to `["-y", "@squirrelnotes.app/mcp-embeddings"]` and add
|
|
165
|
+
`"ONNXRUNTIME_NODE_INSTALL": "skip"` to `env`.
|
|
166
|
+
|
|
167
|
+
- The model runs in the MCP process on your machine. Memory text is not sent
|
|
168
|
+
anywhere to be embedded. Search results still go to your agent.
|
|
169
|
+
- Vectors are kept only in an AES-256-GCM file in your OS cache directory,
|
|
170
|
+
mode `0600`. They are never sent, logged or returned; each hit carries only its
|
|
171
|
+
similarity, one rounded number, as `semanticScore`.
|
|
172
|
+
- Tools, inputs and output keys are identical (`ranking` says `hybrid` when
|
|
173
|
+
meaning was used). If the model is refused or
|
|
174
|
+
cannot load, search is this package's lexical search.
|
|
175
|
+
|
|
176
|
+
This package has no ML dependency: `onnxruntime-node` is installed only by the
|
|
177
|
+
companion (ADR-0442).
|
|
178
|
+
|
|
58
179
|
---
|
|
59
180
|
|
|
60
181
|
## Filesystem MCP Companion (STDIO only)
|
|
@@ -127,7 +248,7 @@ Add both servers to `claude_desktop_config.json`:
|
|
|
127
248
|
|
|
128
249
|
The server needs an API key and your passphrase, both set as env vars in your client's config (see Setup). The API key is sent to the Squirrel Notes API in the `Authorization` header; the passphrase is never sent anywhere.
|
|
129
250
|
|
|
130
|
-
Generate an API key in Squirrel Notes → Settings → API
|
|
251
|
+
Generate an API key in Squirrel Notes → Settings → API & Data (requires Pro tier).
|
|
131
252
|
|
|
132
253
|
---
|
|
133
254
|
|
|
@@ -293,8 +414,20 @@ tail -f /tmp/squirrel-mcp.log
|
|
|
293
414
|
|
|
294
415
|
#### What's logged
|
|
295
416
|
|
|
296
|
-
|
|
297
|
-
|
|
417
|
+
Each line goes to stderr and, when `SQUIRREL_LOG_FILE` is set, is also appended to that file.
|
|
418
|
+
|
|
419
|
+
- `upload_attachment_from_path` progress: **the local file path it reads**, which can contain a plaintext filename; the file's size and MIME type; the new attachment id; the S3 PUT status code; and the confirmed attachment id. Leave `SQUIRREL_LOG_FILE` unset if those paths should not be kept on disk.
|
|
420
|
+
- Memory tools: the tool name, memory ids, counts (memories searched, results, unverifiable records, imports created, profile items and omitted) and, on failure, the HTTP status or `network`. Never memory text, entities, tags or reasons.
|
|
421
|
+
- With the embeddings companion only: one start-up line saying whether local
|
|
422
|
+
embeddings are ready or were not loaded (with a reason and, at most, a file
|
|
423
|
+
name), and, once, a runtime or vector-file failure. Never a vector or a query.
|
|
424
|
+
- Fatal startup errors: missing `SQUIRREL_API_KEY`, `SQUIRREL_PASSPHRASE` or `SQUIRREL_API_BASE_URL`, or a failure to connect the stdio transport.
|
|
425
|
+
|
|
426
|
+
Not logged:
|
|
427
|
+
|
|
428
|
+
- An S3 PUT failure. The tool throws it, with S3's status and response body, and the MCP client shows it as the tool's error. The log has only the status line above.
|
|
429
|
+
- A wrong passphrase. It is no longer caught at startup: the key is derived on the first tool call, and a wrong passphrase fails that call (and every later one) with an error returned to the client.
|
|
430
|
+
- The passphrase, the API key, the data key and any decrypted content.
|
|
298
431
|
|
|
299
432
|
---
|
|
300
433
|
|
|
@@ -303,6 +436,7 @@ tail -f /tmp/squirrel-mcp.log
|
|
|
303
436
|
| Variable | Required | Description |
|
|
304
437
|
|----------|----------|-------------|
|
|
305
438
|
| `SQUIRREL_API_BASE_URL` | ✅ | Base URL of the Squirrel Notes backend API (e.g. `https://api.squirrelnotes.app`) |
|
|
306
|
-
| `SQUIRREL_API_KEY` | ✅ | Your API key from Settings → API
|
|
439
|
+
| `SQUIRREL_API_KEY` | ✅ | Your API key from Settings → API & Data (requires Pro tier) |
|
|
307
440
|
| `SQUIRREL_PASSPHRASE` | ✅ | Your encryption passphrase — never leaves your machine |
|
|
308
441
|
| `SQUIRREL_LOG_FILE` | — | Absolute path to a log file. When set, all stderr output is also appended to this file. |
|
|
442
|
+
| `SQUIRREL_MEMORY_EMBEDDINGS`, `SQUIRREL_MEMORY_MODEL_DIR`, `SQUIRREL_MEMORY_CACHE_DIR` | — | Read only by the opt-in embeddings companion (`@squirrelnotes.app/mcp-embeddings`); see its README. |
|