@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.
Files changed (3) hide show
  1. package/README.md +112 -113
  2. package/dist/stdio-handler.js +1620 -852
  3. 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. The security implications depend on which transport you use:
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
- ### stdio (local process) — recommended
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 | Env var; cleared when process exits |
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
- ### Remote HTTP (Lambda)
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. Avoids race conditions vs get → update. |
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 | Transport | Description |
69
- |------|-----------|-------------|
70
- | `list_attachments` | All | Lists all attachments for a note with decrypted filenames and metadata. |
71
- | `get_attachment_download_url` | All | Returns a presigned S3 download URL (expires in 5 minutes). |
72
- | `delete_attachment` | All | Permanently deletes an attachment from S3 and the database. |
73
- | `upload_attachment_from_path` | STDIO only | 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. |
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
- Every request requires two headers:
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
- > **Which setup should I use?**
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
- cd mcp
300
- npm install
301
- npm run dev
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 use the MCP Inspector for a browser UI:
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
- npx @modelcontextprotocol/inspector http://localhost:3002
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
- - S3 PUT failures (HTTP status + response body from S3)
381
- - Fatal startup errors (missing env vars, wrong passphrase)
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. |