npm package: @squirrelnotes.app/mcp
Description: MCP stdio CLI for Squirrel Notes — bridges AI agents to the encrypted note-taking API
Latest version: 1.28.0

# Squirrel Notes MCP Server

An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that lets AI agents (Claude Desktop, Cursor, etc.) read and write your Squirrel Notes, with full end-to-end decryption.

---

## ⚠️ Security Caveat — Read This First

The MCP server necessarily decrypts your notes — that is its job. The security implications depend on which transport you use:

### stdio (local process) — recommended

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.

| Risk | Mitigation |
|------|------------|
| Passphrase in config file | Stored on disk in a local config file; protect it with OS user permissions and full-disk encryption |
| Passphrase in process memory | Env var; cleared when process exits |
| Decrypted content visible to AI | This is the intended behaviour — AI agents need to read your notes |

### Remote HTTP (Lambda)

Your passphrase is sent over HTTPS to a Lambda function on every call.

| Risk | Mitigation |
|------|------------|
| Passphrase sent over the network | HTTPS-only (API Gateway always TLS) |
| Passphrase in server RAM during invocation | Stack-scoped variable; never assigned to module state; GC'd after invocation |
| Passphrase in CloudWatch logs | Lambda code never logs headers |
| Lambda container reuse retaining secrets | Passphrase is NOT module-scope — reused containers hold no residual secret |

**You must trust:** AWS Lambda, the self-hosted MCP server code, and anyone with access to your CloudWatch logs.

If this trade-off is not acceptable, use stdio transport instead.

---

## Tools

### Notes

| Tool | Description |
|------|-------------|
| `list_notes` | Returns all notes with decrypted titles, tags, and metadata. Supports optional `collectionId`, `tagId`, `pinned` filters. |
| `get_recent_notes` | Returns the N most-recently updated notes (default 10). Fast — titles only. |
| `get_note` | Returns a single note with decrypted title + full content. |
| `create_note` | Creates a note with encrypted title + content. |
| `update_note` | Updates title, content, or pinnedAt on a note. |
| `delete_note` | Soft-deletes a note (recoverable). |
| `pin_note` | Pins or unpins a note (appears at top of sidebar). |
| `append_to_note` | Appends markdown content to an existing note. Avoids race conditions vs get → update. |
| `set_note_tags` | Replaces all tags on a note. |
| `search_notes` | Fast title-only search across all notes. |
| `search_note_content` | Full-content search (slow — decrypts all notes). Use only when title search fails. |
| `move_note_to_collection` | Moves a note to a collection (or to scratchpad if collectionId is null). |

### Saved Searches (Pro)

| Tool | Description |
|------|-------------|
| `list_saved_searches` | Returns saved searches with decrypted names and smart-query strings. |
| `create_saved_search` | Creates a saved search from plaintext name + query (encrypted before API call). |
| `update_saved_search` | Updates saved search name and/or query. |
| `delete_saved_search` | Deletes a saved search (soft-delete server-side). |

### Attachments

| Tool | Transport | Description |
|------|-----------|-------------|
| `list_attachments` | All | Lists all attachments for a note with decrypted filenames and metadata. |
| `get_attachment_download_url` | All | Returns a presigned S3 download URL (expires in 5 minutes). |
| `delete_attachment` | All | Permanently deletes an attachment from S3 and the database. |
| `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. |

---

## Filesystem MCP Companion (STDIO only)

Combining the Squirrel Notes MCP server with the [MCP Filesystem server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) enables a two-step pipeline for attaching files generated by Claude — without putting file bytes in the context window:

```
Claude generates content
        │
        ▼  write_file (Filesystem MCP) ──► file lands on disk
                                                  │
                                                  ▼  upload_attachment_from_path (Squirrel MCP)
                                                     reads bytes, encrypts, uploads to S3
                                                     (only a path string in context)
```

### Claude Desktop configuration

Add both servers to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "squirrel-notes": {
      "command": "npx",
      "args": ["-y", "@squirrelnotes.app/mcp"],
      "env": {
        "SQUIRREL_API_BASE_URL": "https://api.squirrelnotes.app",
        "SQUIRREL_API_KEY": "sqn_your_api_key",
        "SQUIRREL_PASSPHRASE": "your-passphrase",
        "SQUIRREL_UPLOAD_ROOT": "/tmp"
      }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "env": {}
    }
  }
}
```

> **`SQUIRREL_UPLOAD_ROOT`** restricts which directories `upload_attachment_from_path` may read from. Defaults to `os.tmpdir()` — `/tmp` on macOS/Linux, `%TEMP%` equivalent on Windows. Set to a comma-separated list for multiple allowed roots (e.g. `"/tmp,/Users/you/Downloads"`). This is a security guardrail — without it, a prompt-injection attack could trick Claude into uploading files from anywhere on disk (e.g. `~/.ssh/id_rsa`). Note: symlinks inside an allowed root that point outside it are not followed during the check — use OS-level controls if that is a concern.

### Example workflows

**Attach a Claude-generated CSV report to a note:**
> "Create a monthly expense summary as a CSV and attach it to my Finances note."

1. Claude generates the CSV content → `write_file("/tmp/expenses.csv", content)` (Filesystem MCP)
2. Claude calls `upload_attachment_from_path` with `localPath="/tmp/expenses.csv"` (Squirrel MCP)

**Attach an xlsx file generated by a script:**
> "Write a Python script to generate an xlsx report and attach it to my Analytics note."

1. Claude writes the script to `/tmp/generate_report.py` → executes it → `report.xlsx` is written to `/tmp/`
2. Claude calls `upload_attachment_from_path` with `localPath="/tmp/report.xlsx"`

**Bulk-import Markdown files from disk:**
> "Import all the markdown files from /tmp/obsidian-export/ as notes in my Archive collection."

1. Claude calls `list_directory("/tmp/obsidian-export/")` (Filesystem MCP) to enumerate files
2. For each `.md` file: `read_file(path)` → `create_note` with the content (Squirrel MCP)

**Export a note to disk:**
> "Export my project roadmap note as a Markdown file to /tmp/roadmap.md."

1. Claude calls `get_note(id)` (Squirrel MCP) to read the note
2. Claude calls `write_file("/tmp/roadmap.md", content)` (Filesystem MCP)

Every request requires two headers:

```
Authorization: Bearer sqn_<api_key>
X-Passphrase: <your_passphrase>
```

Generate an API key in Squirrel Notes → Settings → API Key (requires Pro tier).

Neither header is ever stored, cached, or logged by the server.

---

## Setup

> **Which setup should I use?**
> - **Claude Desktop** → use **stdio (recommended)** — no OAuth, passphrase stays on your machine
> - **API automation / Cursor / other agents** → use **Remote HTTP (Lambda)**

---

### Setup — Claude Desktop (stdio) ✅ Recommended

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**.

#### Option A — npx (zero install, recommended)

No download or build step required. `npx` fetches the latest published version automatically on first run and caches it locally.

**1. Configure Claude Desktop**

Open `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) and add:

```json
{
  "mcpServers": {
    "squirrel-notes": {
      "command": "npx",
      "args": ["-y", "@squirrelnotes.app/mcp"],
      "env": {
        "SQUIRREL_API_BASE_URL": "https://api.squirrelnotes.app",
        "SQUIRREL_API_KEY": "sqn_<your_api_key>",
        "SQUIRREL_PASSPHRASE": "<your_passphrase>"
      }
    }
  }
}
```

**2. Restart Claude Desktop**

Quit and reopen Claude Desktop. The Squirrel Notes MCP server will start automatically.

**3. Verify**

Ask Claude: *"List my Squirrel Notes"* — you should see your note titles.

#### Option B — build from source (local development / pinned version)

Use this if you want to run a specific local build or contribute to the server code.

```bash
cd mcp
npm install
npm run build:stdio
# Produces dist/stdio-handler.js
```

Then reference the built file directly:

```json
{
  "mcpServers": {
    "squirrel-notes": {
      "command": "node",
      "args": ["/absolute/path/to/squirrel-notes/mcp/dist/stdio-handler.js"],
      "env": {
        "SQUIRREL_API_BASE_URL": "https://api.squirrelnotes.app",
        "SQUIRREL_API_KEY": "sqn_<your_api_key>",
        "SQUIRREL_PASSPHRASE": "<your_passphrase>"
      }
    }
  }
}
```

> **Windows:** Replace the path with `C:\\path\\to\\mcp\\dist\\stdio-handler.js` (double backslashes).

---

### Setup — Remote HTTP (Lambda)

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.

#### 1. Deploy with CDK

```bash
cd infra
npx cdk deploy SquirrelMcp
```

This creates:
- A Lambda function running the MCP server Docker image
- An HTTP API Gateway at `https://<id>.execute-api.<region>.amazonaws.com/mcp`

The deployed URL is printed as a CDK output (`McpApiUrl`).

#### 2. Configure your client

```json
{
  "mcpServers": {
    "squirrel-notes": {
      "url": "https://<your-mcp-api-url>/mcp",
      "headers": {
        "Authorization": "Bearer sqn_<your_api_key>",
        "X-Passphrase": "<your_passphrase>"
      }
    }
  }
}
```

> **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.

#### 3. Verify

Ask Claude: *"List my Squirrel Notes"* — you should see decrypted note titles.

---

## Common Workflows

Once configured, you can speak to Claude naturally. Some examples:

| What you say | What Claude does |
|---|---|
| *"What have I been working on lately?"* | `get_recent_notes` → lists last 10 updated notes |
| *"Show me everything in my Work collection"* | `list_collections` → `list_notes(collectionId: ...)` |
| *"Search my notes for 'project roadmap'"* | `search_notes` (title match) or `search_note_content` |
| *"Create a note called 'Meeting Notes' in my Work collection"* | `list_collections` → `create_note(collectionId: ...)` |
| *"Add today's standup to my Daily Log note"* | `search_notes("Daily Log")` → `append_to_note(id, ...)` |
| *"Pin my TODO note"* | `search_notes("TODO")` → `pin_note(id, true)` |
| *"Tag my 'Project Roadmap' note as urgent"* | `list_tags` → `search_notes` → `set_note_tags` |
| *"Show me all my pinned notes"* | `list_notes(pinned: true)` |
| *"Show me all notes tagged 'urgent'"* | `list_tags` → `list_notes(tagId: ...)` |

> **Tip for Claude Desktop:** Add a system prompt in your Claude Project describing your note structure (e.g. "I use a collection called 'Work' for work notes and 'Personal' for personal notes, and tags like 'urgent' and 'later' for priority"). This reduces the number of lookup calls Claude needs to make.

---

## Local Development

```bash
cd mcp
npm install
npm run dev
```

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.

Smoke test with curl:

```bash
curl -s http://localhost:3002 -X POST \
  -H "Authorization: Bearer sqn_<your_api_key>" \
  -H "X-Passphrase: <your_passphrase>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
```

Or use the MCP Inspector for a browser UI:

```bash
npx @modelcontextprotocol/inspector http://localhost:3002
```

---

## Debugging / Getting Logs

### stdio transport (Claude Desktop)

The MCP protocol uses **stdout** for JSON-RPC messages, so all diagnostic output goes to **stderr** instead. The MCP server writes errors and key upload steps to stderr automatically.

#### Where to find the logs

**macOS / Linux**
Claude Desktop captures stderr from each MCP server in a dedicated log file:

```
~/Library/Logs/Claude/mcp-server-squirrel-notes.log   # macOS
```

Open this file in any text editor or tail it in a terminal:

```bash
tail -f ~/Library/Logs/Claude/mcp-server-squirrel-notes.log
```

**Windows**
```
%APPDATA%\Claude\logs\mcp-server-squirrel-notes.log
```

#### Write logs to a custom file

Set `SQUIRREL_LOG_FILE` to an absolute path in your `claude_desktop_config.json` to capture all stderr output to a file of your choice:

```json
{
  "mcpServers": {
    "squirrel-notes": {
      "command": "npx",
      "args": ["-y", "@squirrelnotes.app/mcp"],
      "env": {
        "SQUIRREL_API_BASE_URL": "https://api.squirrelnotes.app",
        "SQUIRREL_API_KEY": "sqn_<your_api_key>",
        "SQUIRREL_PASSPHRASE": "<your_passphrase>",
        "SQUIRREL_LOG_FILE": "/tmp/squirrel-mcp.log"
      }
    }
  }
}
```

Then in a terminal:

```bash
tail -f /tmp/squirrel-mcp.log
```

#### What's logged

- S3 PUT failures (HTTP status + response body from S3)
- Fatal startup errors (missing env vars, wrong passphrase)

---

## Environment Variables

### stdio transport

| Variable | Required | Description |
|----------|----------|-------------|
| `SQUIRREL_API_BASE_URL` | ✅ | Base URL of the Squirrel Notes backend API (e.g. `https://api.squirrelnotes.app`) |
| `SQUIRREL_API_KEY` | ✅ | Your API key from Settings → API Key (requires Pro tier) |
| `SQUIRREL_PASSPHRASE` | ✅ | Your encryption passphrase — never leaves your machine |
| `SQUIRREL_LOG_FILE` | — | Absolute path to a log file. When set, all stderr output is also appended to this file. |

### Lambda (HTTP) transport

| Variable | Description |
|----------|-------------|
| `SQUIRREL_API_BASE_URL` | Base URL of the Squirrel Notes backend API |

For Lambda, the API key and passphrase are passed per-request via `Authorization` and `X-Passphrase` headers — **not** environment variables.