@squirrelnotes.app/mcp 2.0.2 → 2.1.1-test.341
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 +112 -113
- package/dist/stdio-handler.js +1620 -852
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -6,32 +6,17 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
|
|
|
6
6
|
|
|
7
7
|
## ⚠️ Security Caveat — Read This First
|
|
8
8
|
|
|
9
|
-
The MCP server necessarily decrypts your notes — that is its job.
|
|
9
|
+
The MCP server necessarily decrypts your notes — that is its job. It runs only as a local process on your machine (the stdio transport); there is no hosted MCP server.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
The passphrase lives in `claude_desktop_config.json` on your machine and is passed as an env var to the local process. It never leaves your machine, and decryption happens locally — similar to the standard Squirrel Notes app, where the passphrase is entered locally in the browser and not sent to the server. Compared with the Remote HTTP/Lambda transport below, stdio avoids sending your passphrase over the network, at the cost of storing it in a local config file.
|
|
11
|
+
The passphrase lives in `claude_desktop_config.json` on your machine and is passed as an env var to the local process. It never leaves your machine, and decryption happens locally — similar to the standard Squirrel Notes app, where the passphrase is entered locally in the browser and not sent to the server. The cost is that the passphrase is stored in a local config file.
|
|
14
12
|
|
|
15
13
|
| Risk | Mitigation |
|
|
16
14
|
|------|------------|
|
|
17
|
-
| Passphrase in config file | Stored on disk in a local config file; protect it with OS user permissions and full-disk encryption |
|
|
18
|
-
| 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. |
|
|
19
17
|
| Decrypted content visible to AI | This is the intended behaviour — AI agents need to read your notes |
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Your passphrase is sent over HTTPS to a Lambda function on every call.
|
|
24
|
-
|
|
25
|
-
| Risk | Mitigation |
|
|
26
|
-
|------|------------|
|
|
27
|
-
| Passphrase sent over the network | HTTPS-only (API Gateway always TLS) |
|
|
28
|
-
| Passphrase in server RAM during invocation | Stack-scoped variable; never assigned to module state; GC'd after invocation |
|
|
29
|
-
| Passphrase in CloudWatch logs | Lambda code never logs headers |
|
|
30
|
-
| Lambda container reuse retaining secrets | Passphrase is NOT module-scope — reused containers hold no residual secret |
|
|
31
|
-
|
|
32
|
-
**You must trust:** AWS Lambda, the self-hosted MCP server code, and anyone with access to your CloudWatch logs.
|
|
33
|
-
|
|
34
|
-
If this trade-off is not acceptable, use stdio transport instead.
|
|
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.
|
|
35
20
|
|
|
36
21
|
---
|
|
37
22
|
|
|
@@ -48,7 +33,7 @@ If this trade-off is not acceptable, use stdio transport instead.
|
|
|
48
33
|
| `update_note` | Updates title, content, or pinnedAt on a note. |
|
|
49
34
|
| `delete_note` | Soft-deletes a note (recoverable). |
|
|
50
35
|
| `pin_note` | Pins or unpins a note (appears at top of sidebar). |
|
|
51
|
-
| `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. |
|
|
52
37
|
| `set_note_tags` | Replaces all tags on a note. |
|
|
53
38
|
| `search_notes` | Fast title-only search across all notes. |
|
|
54
39
|
| `search_note_content` | Full-content search (slow — decrypts all notes). Use only when title search fails. |
|
|
@@ -65,12 +50,90 @@ If this trade-off is not acceptable, use stdio transport instead.
|
|
|
65
50
|
|
|
66
51
|
### Attachments
|
|
67
52
|
|
|
68
|
-
| Tool |
|
|
69
|
-
|
|
70
|
-
| `list_attachments` |
|
|
71
|
-
| `get_attachment_download_url` |
|
|
72
|
-
| `delete_attachment` |
|
|
73
|
-
| `upload_attachment_from_path` |
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `list_attachments` | Lists all attachments for a note with decrypted filenames and metadata. |
|
|
56
|
+
| `get_attachment_download_url` | Returns a presigned S3 download URL (expires in 5 minutes). |
|
|
57
|
+
| `delete_attachment` | Permanently deletes an attachment from S3 and the database. |
|
|
58
|
+
| `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. |
|
|
59
|
+
|
|
60
|
+
### Memory (Pro, feature 0031)
|
|
61
|
+
|
|
62
|
+
Short, atomic memories about you that an agent can recall in later sessions.
|
|
63
|
+
Each memory is encrypted on your machine under your data key, bound to its id
|
|
64
|
+
and revision (AES-GCM with AAD `sqn-memory-v2:<id>:<revision>`), so the server
|
|
65
|
+
stores only ciphertext. Someone holding only your API key cannot put an older
|
|
66
|
+
version back, or another memory's ciphertext in its place: it does not open.
|
|
67
|
+
**What this does not stop:** the server, or whoever operates it, returning a
|
|
68
|
+
whole older row (its ciphertext *and* its older revision). That opens, and
|
|
69
|
+
undoes later changes, because this MCP keeps no record of the revisions it has
|
|
70
|
+
seen. The app (PR 1b) will notice a revision lower than the one it cached; the
|
|
71
|
+
MCP does not (R-038).
|
|
72
|
+
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `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. 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. A record that fails to decrypt or verify is skipped and counted (`unverifiable`), never shown. |
|
|
76
|
+
| `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. |
|
|
77
|
+
| `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. |
|
|
78
|
+
| `invalidate_memory` | Marks an active memory as wrong. The text is kept; the memory is written unpinned. |
|
|
79
|
+
| `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. 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`. |
|
|
80
|
+
| `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. `dryRun` defaults to `true` (returns candidates only); with `dryRun: false` it writes up to 100. The note is never changed. |
|
|
81
|
+
|
|
82
|
+
- **Every agent write is live and flagged.** It sets `needsReview: true`,
|
|
83
|
+
`author: 'agent'` and `flaggedAt` inside the ciphertext. `flaggedAt` (ISO
|
|
84
|
+
8601 UTC) is when the memory was flagged: the app's review badge counts 24
|
|
85
|
+
hours from it (ADR-0419 §4) and clears it to `null` when you review. A new
|
|
86
|
+
memory gets now. Superseding or invalidating a memory that is already
|
|
87
|
+
flagged keeps its time, and a supersede's new memory inherits it, so an
|
|
88
|
+
agent cannot restart the clock. It is optional in the payload (`v` stays
|
|
89
|
+
1), so older memories without it still open. A flagged memory is still searched
|
|
90
|
+
and used: the flag is for you, not a gate (ADR-0418 §5). The flag is set by
|
|
91
|
+
the MCP, so a modified client could leave it off (R-038).
|
|
92
|
+
- **Agents cannot delete or pin memories.** No tool sends a `DELETE`, and
|
|
93
|
+
`/public/memory` has no delete route. No agent write is pinned, so a pinned
|
|
94
|
+
memory that an agent supersedes or invalidates leaves your profile until
|
|
95
|
+
you pin it again (product owner, 2026-10-12). You delete and pin them in
|
|
96
|
+
the app.
|
|
97
|
+
- **Search matches ASCII letters and digits only**, as the app's search does:
|
|
98
|
+
accented and non-Latin letters are ignored, so text written only in them
|
|
99
|
+
cannot be found, and near-duplicate detection cannot see it. A query with no
|
|
100
|
+
searchable characters says so.
|
|
101
|
+
- **Up to 60 s stale.** Search and the profile reuse the decrypted list for up
|
|
102
|
+
to 60 s, so a change made in the app, or a revoked key, can take up to 60 s
|
|
103
|
+
to show in a running session (product owner, 2026-10-12; R-036). Writes
|
|
104
|
+
from this process, and a rejected key, clear it at once.
|
|
105
|
+
- **Memory text is data, not instructions.** The server instructions and every
|
|
106
|
+
memory tool description tell the agent so, and to treat a memory as
|
|
107
|
+
unverified while `needsReview` is true. Results carry `author`,
|
|
108
|
+
`provenance` (`via`, the client's self-asserted name, capped at 100
|
|
109
|
+
characters, and the tool) and `reviewedAt`.
|
|
110
|
+
- **Limits.** An account holds up to 5,000 memories and 4 MiB of memory
|
|
111
|
+
ciphertext; past either, writes return a clear "limit reached" error. Writes
|
|
112
|
+
are limited to 120 a minute per user, per route, per Lambda container
|
|
113
|
+
(ADR-0418 §9b, R-042), so not an exact account-wide limit.
|
|
114
|
+
- **Errors and logs never contain memory text.** Log lines carry the tool name,
|
|
115
|
+
ids, counts and HTTP status only.
|
|
116
|
+
- **stdio only.** The memory tools exist only in this stdio package
|
|
117
|
+
(ADR-0421 removed the hosted HTTP server).
|
|
118
|
+
|
|
119
|
+
#### Semantic memory search (opt-in)
|
|
120
|
+
|
|
121
|
+
This package's `search_memory` is lexical. The separate package
|
|
122
|
+
[`@squirrelnotes.app/mcp-embeddings`](https://www.npmjs.com/package/@squirrelnotes.app/mcp-embeddings) is this same
|
|
123
|
+
server (built from the same source and published from the same commit) with a
|
|
124
|
+
local embedding model, so `search_memory` also finds memories by meaning. To
|
|
125
|
+
opt in, change `args` to `["-y", "@squirrelnotes.app/mcp-embeddings"]` and add
|
|
126
|
+
`"ONNXRUNTIME_NODE_INSTALL": "skip"` to `env`.
|
|
127
|
+
|
|
128
|
+
- The model runs in the MCP process on your machine. Memory text is not sent
|
|
129
|
+
anywhere to be embedded. Search results still go to your agent.
|
|
130
|
+
- Vectors are kept only in an AES-256-GCM file in your OS cache directory,
|
|
131
|
+
mode `0600`. They are never sent, logged or returned.
|
|
132
|
+
- Tools, inputs and output keys are identical. If the model is refused or
|
|
133
|
+
cannot load, search is this package's lexical search.
|
|
134
|
+
|
|
135
|
+
This package has no ML dependency: `onnxruntime-node` is installed only by the
|
|
136
|
+
companion (ADR-0442).
|
|
74
137
|
|
|
75
138
|
---
|
|
76
139
|
|
|
@@ -142,28 +205,15 @@ Add both servers to `claude_desktop_config.json`:
|
|
|
142
205
|
1. Claude calls `get_note(id)` (Squirrel MCP) to read the note
|
|
143
206
|
2. Claude calls `write_file("/tmp/roadmap.md", content)` (Filesystem MCP)
|
|
144
207
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
Authorization: Bearer sqn_<api_key>
|
|
149
|
-
X-Passphrase: <your_passphrase>
|
|
150
|
-
```
|
|
208
|
+
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.
|
|
151
209
|
|
|
152
210
|
Generate an API key in Squirrel Notes → Settings → API Key (requires Pro tier).
|
|
153
211
|
|
|
154
|
-
Neither header is ever stored, cached, or logged by the server.
|
|
155
|
-
|
|
156
212
|
---
|
|
157
213
|
|
|
158
214
|
## Setup
|
|
159
215
|
|
|
160
|
-
|
|
161
|
-
> - **Claude Desktop** → use **stdio (recommended)** — no OAuth, passphrase stays on your machine
|
|
162
|
-
> - **API automation / Cursor / other agents** → use **Remote HTTP (Lambda)**
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
### Setup — Claude Desktop (stdio) ✅ Recommended
|
|
216
|
+
### Setup — Claude Desktop, Cursor and other MCP clients (stdio)
|
|
167
217
|
|
|
168
218
|
The stdio transport runs the MCP server as a local process on your machine. No OAuth, no network headers, no AWS account needed. Your passphrase is set as an env var in the config file and **never leaves your machine**.
|
|
169
219
|
|
|
@@ -232,47 +282,6 @@ Then reference the built file directly:
|
|
|
232
282
|
|
|
233
283
|
---
|
|
234
284
|
|
|
235
|
-
### Setup — Remote HTTP (Lambda)
|
|
236
|
-
|
|
237
|
-
Use this if you want to connect agents or tools that support remote HTTP MCP endpoints (Cursor, custom automations, etc.). This requires deploying to AWS and sending the passphrase over HTTPS on every call.
|
|
238
|
-
|
|
239
|
-
#### 1. Deploy with CDK
|
|
240
|
-
|
|
241
|
-
```bash
|
|
242
|
-
cd infra
|
|
243
|
-
npx cdk deploy SquirrelMcp
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
This creates:
|
|
247
|
-
- A Lambda function running the MCP server Docker image
|
|
248
|
-
- An HTTP API Gateway at `https://<id>.execute-api.<region>.amazonaws.com/mcp`
|
|
249
|
-
|
|
250
|
-
The deployed URL is printed as a CDK output (`McpApiUrl`).
|
|
251
|
-
|
|
252
|
-
#### 2. Configure your client
|
|
253
|
-
|
|
254
|
-
```json
|
|
255
|
-
{
|
|
256
|
-
"mcpServers": {
|
|
257
|
-
"squirrel-notes": {
|
|
258
|
-
"url": "https://<your-mcp-api-url>/mcp",
|
|
259
|
-
"headers": {
|
|
260
|
-
"Authorization": "Bearer sqn_<your_api_key>",
|
|
261
|
-
"X-Passphrase": "<your_passphrase>"
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
> **Note:** Newer versions of Claude Desktop enforce OAuth 2.0 for remote HTTP servers and will reject this config. Use the stdio setup above for Claude Desktop.
|
|
269
|
-
|
|
270
|
-
#### 3. Verify
|
|
271
|
-
|
|
272
|
-
Ask Claude: *"List my Squirrel Notes"* — you should see decrypted note titles.
|
|
273
|
-
|
|
274
|
-
---
|
|
275
|
-
|
|
276
285
|
## Common Workflows
|
|
277
286
|
|
|
278
287
|
Once configured, you can speak to Claude naturally. Some examples:
|
|
@@ -296,28 +305,15 @@ Once configured, you can speak to Claude naturally. Some examples:
|
|
|
296
305
|
## Local Development
|
|
297
306
|
|
|
298
307
|
```bash
|
|
299
|
-
|
|
300
|
-
npm
|
|
301
|
-
npm
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
This starts an HTTP MCP server at `http://localhost:3002` pointing at the local backend (`http://localhost:3001`). No build step needed — `tsx` runs TypeScript directly.
|
|
305
|
-
|
|
306
|
-
Smoke test with curl:
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
curl -s http://localhost:3002 -X POST \
|
|
310
|
-
-H "Authorization: Bearer sqn_<your_api_key>" \
|
|
311
|
-
-H "X-Passphrase: <your_passphrase>" \
|
|
312
|
-
-H "Content-Type: application/json" \
|
|
313
|
-
-H "Accept: application/json, text/event-stream" \
|
|
314
|
-
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
|
|
308
|
+
npm --prefix mcp install
|
|
309
|
+
npm --prefix mcp run build # builds dist/stdio-handler.js
|
|
310
|
+
npm --prefix mcp test
|
|
315
311
|
```
|
|
316
312
|
|
|
317
|
-
Or
|
|
313
|
+
Point a client at `mcp/dist/stdio-handler.js` with `SQUIRREL_API_BASE_URL=http://localhost:3001` (the local backend), as in Option B above. Or open the MCP Inspector on it — set the three env vars in the Inspector's UI:
|
|
318
314
|
|
|
319
315
|
```bash
|
|
320
|
-
|
|
316
|
+
make mcp-inspect
|
|
321
317
|
```
|
|
322
318
|
|
|
323
319
|
---
|
|
@@ -377,26 +373,29 @@ tail -f /tmp/squirrel-mcp.log
|
|
|
377
373
|
|
|
378
374
|
#### What's logged
|
|
379
375
|
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
Each line goes to stderr and, when `SQUIRREL_LOG_FILE` is set, is also appended to that file.
|
|
377
|
+
|
|
378
|
+
- `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.
|
|
379
|
+
- 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.
|
|
380
|
+
- With the embeddings companion only: one start-up line saying whether local
|
|
381
|
+
embeddings are ready or were not loaded (with a reason and, at most, a file
|
|
382
|
+
name), and, once, a runtime or vector-file failure. Never a vector or a query.
|
|
383
|
+
- Fatal startup errors: missing `SQUIRREL_API_KEY`, `SQUIRREL_PASSPHRASE` or `SQUIRREL_API_BASE_URL`, or a failure to connect the stdio transport.
|
|
384
|
+
|
|
385
|
+
Not logged:
|
|
386
|
+
|
|
387
|
+
- 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.
|
|
388
|
+
- 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.
|
|
389
|
+
- The passphrase, the API key, the data key and any decrypted content.
|
|
382
390
|
|
|
383
391
|
---
|
|
384
392
|
|
|
385
393
|
## Environment Variables
|
|
386
394
|
|
|
387
|
-
### stdio transport
|
|
388
|
-
|
|
389
395
|
| Variable | Required | Description |
|
|
390
396
|
|----------|----------|-------------|
|
|
391
397
|
| `SQUIRREL_API_BASE_URL` | ✅ | Base URL of the Squirrel Notes backend API (e.g. `https://api.squirrelnotes.app`) |
|
|
392
398
|
| `SQUIRREL_API_KEY` | ✅ | Your API key from Settings → API Key (requires Pro tier) |
|
|
393
399
|
| `SQUIRREL_PASSPHRASE` | ✅ | Your encryption passphrase — never leaves your machine |
|
|
394
400
|
| `SQUIRREL_LOG_FILE` | — | Absolute path to a log file. When set, all stderr output is also appended to this file. |
|
|
395
|
-
|
|
396
|
-
### Lambda (HTTP) transport
|
|
397
|
-
|
|
398
|
-
| Variable | Description |
|
|
399
|
-
|----------|-------------|
|
|
400
|
-
| `SQUIRREL_API_BASE_URL` | Base URL of the Squirrel Notes backend API |
|
|
401
|
-
|
|
402
|
-
For Lambda, the API key and passphrase are passed per-request via `Authorization` and `X-Passphrase` headers — **not** environment variables.
|
|
401
|
+
| `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. |
|