Skip to main content
Glama
apj72

notes-mcp

by apj72
README.md
# Notes MCP Server

A comprehensive system for creating and managing notes in Apple Notes on macOS. This project provides these main components:

1. **MCP Server** - Direct MCP (Model Context Protocol) server for creating notes via stdio/JSON-RPC
2. **Pull Worker** - Background service that processes note creation jobs from a GitHub Gist queue
3. **Tailscale Ingress API** - HTTP API endpoint (Tailnet-only) for creating notes without copy/paste
4. **Notes Export** - Local tool for exporting Apple Notes metadata/content to SQLite or JSONL

All components share the same security controls: authentication, rate limiting, folder allowlisting, confirmation modes, and audit logging.

## Prerequisites

- **macOS** (required for Apple Notes integration)
- **Python 3.11 or higher**
- **Apple Notes app** installed and configured
- **Automation permissions** granted to Terminal/Python for controlling Notes (see Permissions section)

## Quick install on macOS

On a Mac you can install the worker, Python env, and Tailscale ingress with one command. Secrets are stored in **macOS Keychain** (no plain-text files).

**One command (installs to `~/notes_mcp`):**

```bash
curl -fsSL https://raw.githubusercontent.com/apj72/notes_mcp/main/scripts/install_macos.sh | zsh
```

Or run locally after cloning:

```bash
zsh scripts/install_macos.sh
```

**What to expect:**

1. **Pre-flight:** macOS and Xcode Command Line Tools are checked. If CLI tools are missing, you’ll be prompted to install them; re-run the installer afterward.
2. **Prerequisites:** Homebrew is installed if needed, then `git`, `python@3.11`, and (unless skipped) Tailscale.
3. **Repo:** If you used the curl one-liner, the script clones the repo to `~/notes_mcp` and continues from there.
4. **Secrets:** You’ll be prompted once for:
   - **GitHub PAT** (Personal Access Token with `gist` scope)
   - **Gist ID** (your queue Gist)
   - An MCP token is generated and stored in Keychain; the installer prints it once (store it for your MCP client).
5. **Tailscale:** If Tailscale isn’t logged in, the installer will tell you to open the Tailscale app and log in, then re-run with `--skip-brew` (or finish Tailscale setup and run again).
6. **Services:** The worker and ingress launchd agents are installed and started.

**Options:**

- `--install-dir <path>` — Install to a different directory (default: `~/notes_mcp`).
- `--force` — Overwrite Keychain entries and reinstall.
- `--skip-tailscale` — Install the worker only; skip Tailscale and ingress.
- `--skip-brew` — Assume Homebrew and dependencies are already installed.
- `--non-interactive` — Fail if required secrets are missing (no prompts).

**Verify:**

```bash
# Worker status
launchctl print gui/$(id -u)/com.notes-mcp.worker

# Logs
tail -f /tmp/notes-mcp-worker.out
tail -f /tmp/notes-mcp-worker.err

# Health (local)
curl http://127.0.0.1:8443/health

# Health (tailnet; use your machine’s Tailscale name)
curl http://YOUR_TAILSCALE_NAME:8443/health

# Retrieve MCP token from Keychain
security find-generic-password -s notes_mcp_token -w
```

**Uninstall:**

```bash
zsh scripts/uninstall_macos.sh
```

This unloads the launchd services and optionally removes Keychain entries (you’ll be asked before deleting secrets).

## Installation

1. Clone or download this repository:
```bash
cd /path/to/notes_mcp
```

2. Create a virtual environment (recommended):
```bash
python3 -m venv venv
source venv/bin/activate
```

3. Install dependencies:
```bash
python3 -m pip install -e .
```

**Note**: On macOS, use `python3 -m pip` instead of just `pip` if the `pip` command is not found.

## Configuration

### Initial Setup

1. **Copy the configuration template:**
   ```bash
   cp start_worker.sh.example start_worker.sh
   ```

2. **Edit `start_worker.sh`** and replace all placeholder values with your actual tokens and IDs.

3. **Generate secure tokens:**
   ```bash
   # MCP Authentication Token
   openssl rand -hex 32
   
   # Ingress API Key (for Tailscale Funnel)
   openssl rand -hex 32
   ```

**Important:** `start_worker.sh` is gitignored and contains sensitive secrets. Never commit it to version control.

### Required Environment Variables

- **`NOTES_MCP_TOKEN`**: Shared secret token for authentication
- **`GITHUB_TOKEN`**: GitHub Personal Access Token (for Gist queue, requires 'gist' scope)
- **`NOTES_QUEUE_GIST_ID`**: Your GitHub Gist ID (from the Gist URL)

### Optional Environment Variables

- **`NOTES_MCP_ALLOWED_FOLDERS`**: Comma-separated list of allowed folder names (default: "MCP Inbox")
- **`NOTES_MCP_INGRESS_KEY`**: Ingress API key for Tailscale Funnel public access
- **`NOTES_MCP_REQUIRE_CONFIRM`**: If `"true"`, all create requests must include `confirm: true`
- **`NOTES_QUEUE_POLL_SECONDS`**: Polling interval in seconds (default: 60)
- **`NOTES_MCP_MAX_JOB_AGE_SECONDS`**: Maximum age for jobs in seconds (default: 86400 = 24 hours)

## Quick Start

### Option 1: Direct MCP Server (for Cursor/IDE integration)

The server uses stdio transport (standard input/output) for MCP communication. Run it directly:

```bash
python3 -m notes_mcp.server
```

**Note**: On macOS, use `python3` instead of `python` if the `python` command is not found.

The server will read JSON-RPC requests from stdin and write responses to stdout.

### Option 2: Pull Worker (for async processing via Gist queue)

Set up environment variables (see Pull Worker section below) and run:

```bash
python3 -m notes_mcp.pull_worker
```

Or set up as a background service:

```bash
./setup_service.sh
```

### Option 3: Tailscale Ingress API (for HTTP access)

Start the ingress service:

```bash
./start_ingress.sh
```

Then expose via Tailscale:

```bash
# For Tailnet-only access (private)
sudo /Applications/Tailscale.app/Contents/MacOS/tailscale serve --bg --http=8443 http://127.0.0.1:8443

# For public access via Funnel (requires ingress key)
sudo /Applications/Tailscale.app/Contents/MacOS/tailscale funnel --bg http://127.0.0.1:8443
```

**Note:** For public Funnel access, ensure `NOTES_MCP_INGRESS_KEY` is set in `start_worker.sh` and include it in requests as `X-Notes-MCP-Key` header.

## Cursor MCP Client Configuration

To use this server with Cursor, add the following to your Cursor MCP configuration file (typically `~/.cursor/mcp.json` or similar):

```json
{
  "mcpServers": {
    "notes-mcp": {
      "command": "python3",
      "args": ["-m", "notes_mcp.server"],
      "env": {
        "NOTES_MCP_TOKEN": "your-secret-token-here",
        "NOTES_MCP_ALLOWED_FOLDERS": "MCP Inbox,Work,Personal",
        "NOTES_MCP_REQUIRE_CONFIRM": "false"
      }
    }
  }
}
```

**Important**: Make sure the `NOTES_MCP_TOKEN` in the Cursor config matches the token you set in your environment, or pass it via the `_token` field in tool calls.

### Alternative: Using a Wrapper Script

If you prefer to load environment variables from a file, create a wrapper script:

```bash
#!/bin/bash
# ~/bin/notes-mcp-server.sh
source ~/.notes-mcp-env
exec python -m notes_mcp.server
```

Then reference it in your Cursor config:

```json
{
  "mcpServers": {
    "notes-mcp": {
      "command": "/Users/yourusername/bin/notes-mcp-server.sh"
    }
  }
}
```

**Note**: In the wrapper script, use `python3` instead of `python` if needed.

## macOS Permissions

The server requires Automation permissions to control Apple Notes. When you first run the server, macOS will prompt you to grant these permissions.

### Granting Permissions

1. **Automatic Prompt**: When the server first tries to create a note, macOS will show a permission dialog. Click "OK" to grant access.

2. **Manual Setup**: If you need to grant permissions manually:
   - Open **System Settings** (or System Preferences on older macOS)
   - Go to **Privacy & Security** → **Automation**
   - Find **Terminal** (or **Python**) in the list
   - Enable **Notes** in the allowed apps list

3. **Troubleshooting**: If permissions are denied:
   - Check System Settings → Privacy & Security → Automation
   - Remove and re-add the permission if needed
   - Restart Terminal/Python after granting permissions

## API Reference

### Tool: `notes.create`

Creates a new note in Apple Notes.

**Parameters:**
- `title` (string, required): Note title (max 200 characters)
- `body` (string, required): Note body (max 50,000 characters)
- `folder` (string, optional): Target folder name (default: "MCP Inbox")
- `account` (string, optional): Target account - `"iCloud"` or `"On My Mac"` (default: "iCloud")
- `confirm` (boolean, optional): Confirmation flag (required if `NOTES_MCP_REQUIRE_CONFIRM=true`)
- `_token` (string, **required**): Authentication token (must match `NOTES_MCP_TOKEN` environment variable)

**Returns:**
```json
{
  "ok": true,
  "location": {
    "account": "iCloud",
    "folder": "MCP Inbox"
  },
  "reference": "timestamp - title"
}
```

**Errors:**
- `-32000`: Validation error (invalid input, rate limit, permission denied, etc.)
- `-32601`: Unknown tool or method
- `-32700`: Parse error

## Manual Testing

### Using Python

Create a test script `test_notes_mcp.py`:

```python
import json
import subprocess
import os

# Set token
os.environ["NOTES_MCP_TOKEN"] = "test-token-123"
os.environ["NOTES_MCP_ALLOWED_FOLDERS"] = "MCP Inbox"

# Initialize request
init_request = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {}
}

# Create note request
create_request = {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
        "name": "notes.create",
        "arguments": {
            "title": "Test Note",
            "body": "This is a test note created via MCP",
            "folder": "MCP Inbox",
            "account": "iCloud",
            "_token": "test-token-123"
        }
    }
}

# Run server
proc = subprocess.Popen(
    ["python3", "-m", "notes_mcp.server"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    text=True
)

# Send requests
proc.stdin.write(json.dumps(init_request) + "\n")
proc.stdin.write(json.dumps(create_request) + "\n")
proc.stdin.close()

# Read responses
for line in proc.stdout:
    print(json.loads(line))
```

Run it:
```bash
python test_notes_mcp.py
```

### Using echo and pipe

```bash
export NOTES_MCP_TOKEN="test-token-123"
export NOTES_MCP_ALLOWED_FOLDERS="MCP Inbox"

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python3 -m notes_mcp.server

echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"notes.create","arguments":{"title":"Test","body":"Test body","_token":"test-token-123"}}}' | python3 -m notes_mcp.server
```

## Pull Worker (Queue-Based Processing)

The pull worker allows you to process note creation jobs from a remote GitHub Gist queue. Jobs are signed with HMAC to prevent tampering, and the worker maintains idempotency using SQLite to prevent duplicate execution.

### Overview

- **Queue**: Private GitHub Gist with `queue.jsonl` (newline-delimited JSON jobs)
- **Results**: Same Gist with `results.jsonl` (appended results)
- **Security**: HMAC-SHA256 signatures on all jobs
- **Idempotency**: SQLite database tracks processed job IDs
- **Reuses**: All existing security controls (allowlist, confirmation, rate limiting, logging)

### Job Schema

**Job line (queue.jsonl):**
```json
{
  "job_id": "uuid",
  "created_at": "ISO-8601",
  "tool": "notes.create",
  "args": {
    "title": "...",
    "body": "...",
    "folder": "MCP Inbox",
    "account": "iCloud",
    "confirm": true
  },
  "sig": "base64(hmac_sha256(canonical_json))"
}
```

**Result line (results.jsonl):**
```json
{
  "job_id": "uuid",
  "processed_at": "ISO-8601",
  "status": "ok|denied|error",
  "location": {
    "account": "iCloud",
    "folder": "MCP Inbox"
  },
  "reference": "timestamp - title",
  "error": {
    "code": "...",
    "message": "..."
  }
}
```

### Setup

#### 1. Create a Private GitHub Gist

1. Go to https://gist.github.com
2. Create a new **private** gist
3. Add two files:
   - `queue.jsonl` (initially empty or with a comment)
   - `results.jsonl` (initially empty)
4. Copy the Gist ID from the URL (e.g., `https://gist.github.com/username/abc123def456` → ID is `abc123def456`)

#### 2. Create a GitHub Fine-Grained Token

1. Go to GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens
2. Create a new token with:
   - **Repository access**: Only select gists
   - **Permissions**: `gists` (read and write)
   - **Expiration**: Set as needed
3. Copy the token

#### 3. Configure Environment Variables

Edit `start_worker.sh` (created from `start_worker.sh.example`) and set:

- `NOTES_QUEUE_GIST_ID` - Your GitHub Gist ID
- `GITHUB_TOKEN` - Your GitHub Personal Access Token
- `NOTES_MCP_TOKEN` - Your MCP authentication token
- `NOTES_MCP_ALLOWED_FOLDERS` - Comma-separated list of allowed folders
- `NOTES_MCP_INGRESS_KEY` - (Optional) Ingress API key for Tailscale Funnel

**Important**: 
- `start_worker.sh` is gitignored and contains sensitive secrets. Never commit it.
- The HMAC secret defaults to `NOTES_MCP_TOKEN` if `NOTES_QUEUE_HMAC_SECRET` is not set.

#### 4. Generate a Signed Job

Use the helper script to generate a signed job line:

```bash
python3 -m notes_mcp.sign_job \
  --title "My Note Title" \
  --body "Note content here" \
  --folder "MCP Inbox" \
  --account "iCloud" \
  --confirm
```

This outputs a JSON line that you can paste into `queue.jsonl` in your Gist.

#### 5. Run the Worker

**Foreground (for testing):**
```bash
python3 -m notes_mcp.pull_worker
```

**Background with launchd (macOS):**

```bash
# Automated setup
./setup_service.sh

# Or manually
cp com.notes-mcp.worker.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.notes-mcp.worker.plist
```

**Managing the service:**
- Start: `launchctl kickstart -k gui/$(id -u)/com.notes-mcp.worker`
- Stop: `launchctl bootout gui/$(id -u)/com.notes-mcp.worker`
- Check status: `launchctl list | grep notes-mcp`
- View logs: Check `~/Library/Logs/notes-mcp/` directory

### Security Notes

1. **HMAC Signatures**: Jobs are signed with HMAC-SHA256 over a canonical JSON representation. The signature covers all fields except `sig` itself. This prevents anyone with gist access from injecting malicious jobs.

2. **Canonicalization**: Jobs are canonicalized by:
   - Sorting keys alphabetically
   - Removing the `sig` field
   - Using compact JSON (no spaces)

3. **Least Privilege**: 
   - GitHub token should only have gists read/write permissions
   - Worker only reads `queue.jsonl` and appends to `results.jsonl` (never deletes or modifies existing lines)
   - All existing security controls (allowlist, confirmation, rate limiting) apply

4. **Idempotency**: SQLite database at `~/.notes-mcp-queue/worker.sqlite3` tracks processed job IDs to prevent replay attacks.

5. **Risks**:
   - If your GitHub token is compromised, an attacker could read/write the gist but cannot create valid jobs without the HMAC secret
   - If your HMAC secret is compromised, an attacker could create valid jobs (but still subject to allowlist, confirmation, rate limits)
   - Keep both secrets secure

### Troubleshooting

**GitHub 401/403 errors:**
- Verify `GITHUB_TOKEN` is set and valid
- Check token has gists read/write permissions
- Verify token hasn't expired

**Gist file missing:**
- Ensure `queue.jsonl` and `results.jsonl` exist in the gist
- Check `NOTES_QUEUE_GIST_ID` is correct

**Invalid signature:**
- Verify `NOTES_QUEUE_HMAC_SECRET` (or `NOTES_MCP_TOKEN`) matches what was used to sign the job
- Check job wasn't modified after signing
- Ensure canonicalization matches (use `sign_job.py` helper)

**Already processed job:**
- This is expected - jobs are only processed once
- Check SQLite database: `sqlite3 ~/.notes-mcp-queue/worker.sqlite3 "SELECT * FROM processed_jobs;"`

**Notes automation permissions:**
- Same as MCP server - grant in System Settings → Privacy & Security → Automation

## Security Features

1. **Authentication**: All requests require a valid token via `NOTES_MCP_TOKEN`
2. **Rate Limiting**: Maximum 10 create calls per minute per token
3. **Folder Allowlisting**: Only specified folders can be used (configured via `NOTES_MCP_ALLOWED_FOLDERS`)
4. **Confirmation Mode**: Optional requirement for explicit confirmation on write operations
5. **Input Validation**: Title max 200 chars, body max 50,000 chars, no null bytes
6. **AppleScript Injection Defense**: User input is passed as arguments, never concatenated into script strings
7. **Audit Logging**: All actions logged to `~/Library/Logs/notes-mcp/notes-mcp.log`

## Audit Logging

All actions are logged to `~/Library/Logs/notes-mcp/notes-mcp.log` in JSON format. Each log entry includes:

- `timestamp`: UTC timestamp
- `action`: Action performed (e.g., "create")
- `title_length`: Length of note title
- `body_length`: Length of note body
- `account`: Target account
- `folder`: Target folder
- `outcome`: "allowed", "denied", or "error"
- `error`: Error message (if outcome is "error" or "denied")

**Note**: The log does NOT include the full body content for privacy.

## Troubleshooting

### Common Issues

1. **"AppleScript error: Notes got an error: Can't get account"**
   - Ensure Notes app is installed and configured
   - Check that you have an iCloud account or "On My Mac" account set up in Notes
   - Grant Automation permissions (see Permissions section)

2. **"Invalid or missing authentication token"**
   - Ensure `NOTES_MCP_TOKEN` environment variable is set
   - Verify the token matches between server and client

3. **"Folder 'X' is not in the allowlist"**
   - Add the folder to `NOTES_MCP_ALLOWED_FOLDERS` environment variable
   - Or use "MCP Inbox" which is always allowed

4. **"Confirmation required but not provided"**
   - Set `confirm: true` in the tool call arguments
   - Or set `NOTES_MCP_REQUIRE_CONFIRM="false"` to disable confirmation mode

5. **"Rate limit exceeded"**
   - Wait 60 seconds before making another request
   - Maximum 10 calls per minute per token

6. **Permission Denied**
   - Check System Settings → Privacy & Security → Automation
   - Ensure Terminal/Python has permission to control Notes
   - Restart Terminal after granting permissions

### Pull Worker Issues

7. **GitHub 401/403 errors**
   - Verify `GITHUB_TOKEN` is set and valid
   - Check token has gists read/write permissions
   - Verify token hasn't expired
   - **Rate limit exceeded**:
     - Wait 5-10 minutes and try again
     - Check your token's rate limit: `curl -H "Authorization: token YOUR_TOKEN" https://api.github.com/rate_limit`
     - Increase poll interval: `export NOTES_QUEUE_POLL_SECONDS="30"`

8. **Gist file missing**
   - Ensure `queue.jsonl` and `results.jsonl` exist in the gist
   - Check `NOTES_QUEUE_GIST_ID` is correct

9. **Invalid signature**
   - Verify `NOTES_QUEUE_HMAC_SECRET` (or `NOTES_MCP_TOKEN`) matches what was used to sign the job
   - Check job wasn't modified after signing
   - Use `sign_job.py` helper to generate correctly signed jobs

10. **Already processed job**
    - This is expected - jobs are only processed once (idempotency)
    - Check SQLite database: `sqlite3 ~/.notes-mcp-queue/worker.sqlite3 "SELECT * FROM processed_jobs;"`

### Viewing Logs

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

## Tailscale Ingress API

The Tailscale Ingress API provides a secure HTTP endpoint for creating notes without requiring copy/paste of commands. It's accessible only over your Tailnet (not the public internet).

### Features

- **Tailnet-only access** - Only accessible via Tailscale VPN
- **HTTP API** - Simple POST requests to create notes
- **No copy/paste** - Direct API calls from iOS Shortcuts, webhooks, etc.
- **Optional authentication** - Additional header-based auth (beyond Tailscale ACLs)
- **Rate limiting** - 30 requests per minute per IP
- **Reuses all security controls** - Folder allowlist, confirmation, validation

### Quick Start

1. **Install dependencies:**
   ```bash
   python3 -m pip install fastapi uvicorn pydantic
   ```

2. **Start the ingress service:**
   ```bash
   ./start_ingress.sh
   ```

3. **Expose via Tailscale:**
   ```bash
   sudo /Applications/Tailscale.app/Contents/MacOS/tailscale serve --bg --http=8443 http://127.0.0.1:8443
   ```

4. **Test it:**
   ```bash
   curl -X POST http://yourmachine.tailnet.ts.net:8443/notes \
     -H "Content-Type: application/json" \
     -d '{"title": "Test", "body": "Test content", "folder": "MCP Inbox"}'
   ```

**API Endpoints:**
- `GET /health` - Health check
- `POST /notes` - Create a note (requires `X-Notes-MCP-Key` header if `NOTES_MCP_INGRESS_KEY` is set)

**Request format:**
```json
{
  "title": "Note Title",
  "body": "Note content",
  "folder": "MCP Inbox",
  "account": "iCloud"
}
```

## Notes Export

The export tool allows you to read and export Apple Notes content locally for search, summarization, or backup purposes.

### Features

- **Local-only** - All data stays on your Mac
- **Privacy-first** - Metadata-only export by default (bodies excluded unless explicitly requested)
- **Multiple formats** - Export to SQLite or JSONL
- **Filtering** - Filter by date, limit count, include/exclude bodies
- **Tag extraction** - Automatically extracts tags from title prefixes (e.g., `[WORK]`)
- **All folders** - Export all notes regardless of folder allowlist
- **Duplicate detection** - Identify duplicate notes based on title and body similarity

### Usage

```bash
# Export metadata only from allowed folders (default)
python3 -m notes_mcp.export_notes

# Export ALL notes from ALL folders (for organization/cleanup)
python3 -m notes_mcp.export_notes --all-folders --max-notes 0 --include-body

# Export with duplicate detection
python3 -m notes_mcp.export_notes --all-folders --find-duplicates --include-body

# Export to SQLite for analysis
python3 -m notes_mcp.export_notes --all-folders --format sqlite --include-body --find-duplicates
```

### Organizing and Cleaning Notes

To organize all your notes and find duplicates:

```bash
# Export all notes with bodies and duplicate detection
python3 -m notes_mcp.export_notes --all-folders --max-notes 0 --include-body --find-duplicates --format sqlite

# Then query the SQLite database
sqlite3 .data/notes_export.db "SELECT * FROM notes_export WHERE is_duplicate = 1 ORDER BY duplicate_group, modified_at"
```

Output files are saved to `.data/` directory (gitignored).

## Development

### Container Deployment

Notes MCP can be run in Podman or Docker containers using a hybrid architecture:
- **Container**: Runs ingress API and/or pull worker (handles HTTP, GitHub Gist, queue management)
- **Host Bridge Service**: Runs on macOS host (handles Apple Notes via AppleScript)

For deployment, see **[DEPLOYMENT.md](DEPLOYMENT.md)** for the minimum artifacts in this repo.

## Project Structure

```
notes_mcp/
├── pyproject.toml                    # Project configuration
├── README.md                         # This file
├── DEPLOYMENT.md                     # What's needed to deploy (minimum artifacts)
├── start_worker.sh.example           # Configuration template (copy to start_worker.sh)
├── start_ingress.sh.example          # Ingress template (copy to start_ingress.sh)
├── setup_service.sh                  # Worker service setup (launchd)
├── gpt_action.json.example           # Custom GPT schema (copy to gpt_action.json)
├── openapi-schema.json               # OpenAPI schema
├── CUSTOM_GPT_CONFIG.md              # Custom GPT setup guide
├── NOTE_TAGS.md                      # Tags (hashtags) feature
├── APPLE_NOTES_FORMATTING_SUPPORT.md # HTML formatting in Notes
├── docs/
│   └── launchd/                      # Launchd plists (ingress, export)
└── src/
    └── notes_mcp/
        ├── __init__.py
        ├── server.py                 # MCP server (stdio)
        ├── pull_worker.py             # Queue-based worker
        ├── ingress.py                 # Tailscale ingress API
        ├── applescript.py             # AppleScript integration
        ├── bridge_client.py           # Bridge client (container)
        ├── bridge_server.py          # Bridge server (host)
        ├── export_notes.py            # Notes export tool
        ├── security.py
        ├── logging.py
        ├── formatting.py
        ├── sign_job.py
        └── enqueue_job.py
```

Config files with secrets (`start_worker.sh`, `start_ingress.sh`, `gpt_action.json`, plists) are gitignored. Helper scripts, container files, and experimental docs live in `_unsynced/` (gitignored).

### Current Status

**✅ Implemented:**
- MCP server with stdio transport
- Pull worker with GitHub Gist queue
- Tailscale Ingress API (HTTP endpoint)
- Notes export tool (local read access)
- Comprehensive security controls
- Launchd service setup for worker and ingress
- Queue clearing and idempotency
- Text formatting normalization
- Rate limit handling improvements

**📚 Documentation:**
- Complete setup guides for all components
- API reference documentation
- Security review
- Troubleshooting guides

**🔒 Security:**
- HMAC-signed jobs
- Folder allowlisting
- Rate limiting
- Optional confirmation modes
- Audit logging (no secrets)
- Input validation
- AppleScript injection defense

## License

This project is provided as-is for local use.