nibble
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@nibblesearch my memory for the article on local LLMs"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Nibble
A portable, background-running desktop app for macOS, Windows and Linux that keeps a local memory of what you capture, indexes it on device, and exposes it to any LLM over MCP, plus reminders that fire as real system notifications.
One Electron codebase, three real builds: a .dmg, a portable .exe and an
.AppImage. No account, no server, no telemetry.
Written in TypeScript, strict mode, with no bundler: plain tsc to out/.
npm install
npm start # compile, then run
npm run watch # tsc --watch while developing
npm run typecheck # no emit
npm test # store + search tests
npm run build:mac # .dmg + .zip (arm64 + x64)
npm run build:win # portable .exe + setup + .zip
npm run build:linux # .AppImage + .deb + .tar.gzrootDir is src and outDir is out, so every file compiles to the same
depth it was written at: src/main/index.ts becomes out/main/index.js. The
main process resolves the preload, the renderer, the embedding worker and the
MCP relay through __dirname, and this keeps all of those paths correct
without a single change between running from source and running packaged.
Each target must be built on its own OS. .github/workflows/build.yml runs the
three natively and attaches the results to a GitHub release when you push a tag.
What it does
Memory | Captures text, chunks it, embeds it on device, and searches it by keyword and meaning |
Related | Any result opens its nearest neighbours in meaning, with no query and no model call |
Quick capture | One global hotkey stores the clipboard on purpose, with nothing watching in between |
Now playing | Remembers what you listen to and watch, so a day is searchable by what was on |
MCP connector | Claude, ChatGPT or any MCP client can query that memory as a tool |
Reminders | Once, hourly, daily, weekdays, weekly, or a custom interval |
Portable | Data lives beside the executable, not in your profile |
Background | Closing the window hides it; the tray icon keeps everything running |
Offline | The default embedding model runs locally. Nothing is uploaded |
Related MCP server: Enigma MCP Server
The memory
Capture and indexing are decoupled. Text is written and keyword-searchable immediately, with its vector slot zeroed and filled in behind it, so a model that is still downloading degrades search quality instead of dropping data.
Search fuses BM25 and cosine similarity with Reciprocal Rank Fusion, so the two scoring scales never have to be reconciled, and something both methods like outranks something only one of them loves. A cosine floor keeps unrelated chunks out, so "no results" is an answer the search can actually give.
Storage
Two flat files, no database:
memory/chunks.jsonl one JSON record per line, appended, never rewritten
memory/vectors.bin fixed-width Float32 rows; row N belongs to line N
memory/meta.json embedding width and model idA native SQLite build would mean a compiled module per platform and architecture. This behaves identically everywhere, and the keyword index is rebuilt at load in a few milliseconds.
Whether a row is embedded is derived from the vector file rather than stored, so the log and the vectors cannot drift apart if the app is killed mid-batch. Deletions are tombstones; Compact reclaims the space.
Embeddings
Backend | Model | Width | Notes |
|
| 384 | ~23 MB, downloaded once, then fully offline |
| Gemini or Voyage | 768 / 1024 | Better recall, needs an API key, text leaves the device |
The local model runs in a utilityProcess. Embedding a batch is tens of
milliseconds of CPU, which in the main process would stall the tray, the window
and the reminder timers. Switching backends invalidates the stored vectors and
re-embeds in the background, because vectors from different models are not
comparable even at the same width.
Related
related() answers a different question from search: not "what matches this
query", but "what else is near this". The stored chunk's own vector is the
query, so it costs no embedding call, works offline, and is instant even with
a cloud backend configured.
Two details make the results worth reading. The floor is 0.25 rather than the search floor of 0.1, because both sides are full passages here rather than a short question and so sit higher on the cosine scale. And chunks split from the same original capture are dropped: they are the rest of the same paragraph, they always score highest, and they would fill the list with text you are already looking at.
Capture sources
Source | Status | What it does |
Clipboard | live | Remembers what you copy. Drops anything matching a credential shape or a high-entropy blob before storing it |
Folders | live | Reads text and Markdown from folders you choose, and notices changes |
Quick capture | live | A global hotkey ( |
Now playing | live | What you play, with the title, artist and link. MPRIS on Linux, including browser tabs; Spotify and Music on macOS |
Meetings & audio | phase 2 | Permission handling is real; capture is not wired up yet |
Screen | phase 3 | Same. The most invasive source, so it ships last and with the strictest treatment |
Sources that are not built yet still register and report their real permission state. They say they are not capturing rather than implying that they are.
There is a global pause that keeps sources running but stores nothing, and a forget on every result and every source.
Now playing
The question this answers is the one nothing else can: what was that track, or that video, that was on while I was working on the thing I now want to find again. Media is a good index into a day precisely because it is incidental -- you never wrote it down, and you would never have thought to.
On Linux it reads MPRIS over D-Bus, which every serious player speaks,
browsers included, so a YouTube tab arrives with the video title and the
channel as the artist. One gdbus call per player returns the playback state
and the metadata together, so a poll is one short-lived process and no
dependency: parsing GVariant by hand is cheaper than a native D-Bus binding
per platform and architecture, which is the same trade the memory store made.
On macOS it asks Spotify and Music over AppleScript. There is no supported way to read what a browser is playing -- system-wide Now Playing lives behind a private framework, and reading browser tabs instead means asking for automation access to the browser, which is a much bigger ask than this is worth. Windows has a real API for it (GlobalSystemMediaTransportControls) and reaching it needs a native module or a WinRT round trip through PowerShell, so it is not wired up and says so.
Nothing is stored until something has been playing for thirty seconds, so skipping through a playlist leaves no trace, and paused is not playing: leaving something paused all afternoon does not put it in your memory.
What is playing shows in the notch panel straight away, though, which is a different question from whether it is worth remembering: the strip grows sideways and says what it is, and the open panel has a line for it. A capture always wins that space while it is on screen, because it is news and it is gone in two seconds; the track is underneath, so the strip returns to it rather than to nothing.
Quick capture
The clipboard source is ambient: while it runs, everything you copy is stored. The hotkey is the opposite trade -- nothing is watched, and one key press stores one thing. Both can be on at once.
It refuses in four cases, and says which: nothing useful on the clipboard (it opens the window instead), capture is paused, the text looks like a credential, or you saved the same thing within the last six hours. Registering a global shortcut can also simply fail, because whichever app asks first owns the combination, so Settings shows not registered rather than a switch that claims to be on.
Connecting an LLM
Turn on the connector in the Memory tab. It binds to 127.0.0.1 only, and every
request needs a bearer token, because anything that can run on your machine can reach
loopback, and browsers will happily POST there, so the Origin header is checked
too.
Claude Code / Claude Desktop: spawn the stdio relay.
claude mcp add nibble -- node /path/to/nibble/out/mcp/stdio.jsThe app shows the exact path. The relay is dependency-free on purpose: a client
spawns it with a plain node, which cannot require out of a packaged asar.
Anything that takes a URL: http://127.0.0.1:8787/mcp, with the token as a
Bearer header.
Tools it exposes
Tool | |
| Keyword + semantic search, with source and date filters. Prints each chunk's id |
| The nearest neighbours of a chunk, by meaning, from an id |
| Newest captures first |
| Store something from the conversation |
| Counts, model, index state |
| Scheduled reminders, soonest first |
| Schedule a notification |
The notch panel (macOS)
A panel that hangs off the MacBook notch: hover it for a search box, the next reminder and a pause toggle, or drop text or a file on it to remember it. Off by default; the switch is in Settings.
There is no API for any of this. macOS exposes the notch only through
NSScreen.safeAreaInsets, which Electron does not surface, so the panel is a
borderless transparent NSPanel pinned to the top centre of the internal
display. Three details make it behave like part of the system:
the window level is
screen-saver, the only level above the menu barit is visible on every space and over fullscreen apps, or it vanishes the moment you switch desktops
collapsed, it ignores mouse events and forwards them, so the menu bar underneath stays clickable, and hover is detected by polling the cursor, because a window that ignores mouse events cannot receive them
At rest it paints nothing at all. Drawing a collapsed lip only looks right if it exactly covers the physical notch, and nothing can tell us how wide that is, so any guess would show as black wings on one machine and a gap on another.
How it moves
The panel is one silhouette clipped to a notch-shaped path: two concave flares where it meets the top of the screen, two convex corners at the bottom. It grows out of the notch and shrinks back into it, rather than sliding into view from somewhere.
Three shapes, built from the same sequence of path commands with different numbers in them. That is the whole trick: a browser interpolates one path into another only when their commands line up, so the morph costs a CSS transition instead of a frame loop.
closed the size of the notch, and invisible island the notch plus a wing either side, when something was captured open the full panel
The content inside is laid out at full size the whole time and is revealed by the shape, so opening never has to lay anything out: the only work per frame is a clip and a transform. Opening and closing are deliberately not the same motion -- 520ms with the barest overshoot on the way out, 400ms with none on the way back, because a panel that bounces as it leaves reads as a mistake.
Opening and closing are also intentions rather than events. The cursor has to rest on the notch briefly before anything happens, and stray briefly before it closes, so crossing the notch on the way to the menu bar does not fling the panel out and a wobble on the way to a button does not lose it.
Notch detection is a guess too, and the obvious signal does not work: a 14" M3 reports a 29pt menu bar at one scaled resolution while an external 1080p display reports 30pt. It reads the model identifier instead, and the panel still works on a Mac without a notch, it just hangs from the top of the screen.
--notch-open opens the panel on launch during development, since hovering
cannot be scripted.
Where it stores things
Portable mode turns on when any of these is true, and everything lives in a
nibble-data folder next to the executable:
launched with
--portablethe Windows
portablebuild (setsPORTABLE_EXECUTABLE_DIR)running as an AppImage (sets
APPIMAGE)a file named
portablesits next to the executable
Otherwise it uses the normal per-user location: ~/Library/Application Support,
%APPDATA%, or ~/.config. The tray menu's Show data folder opens whichever
is in use.
Layout
src/types.ts every shape that crosses a boundary
src/main/index.ts app wiring, windows, IPC
src/main/scheduler.ts the reminder clock (chunked timers, DST-safe repeats)
src/main/store.ts settings and reminders
src/main/paths.ts portable-vs-installed data directory
src/main/autostart.ts login items: LaunchServices / Run key / XDG autostart
src/main/tray.ts menu bar icon and menu
src/main/notifier.ts native notifications
src/main/notch.ts the macOS notch panel
src/main/quickcapture.ts the global hotkey
src/main/memory/ chunker, store, hybrid search, embedder
src/main/memory/embed-worker.ts the model, in its own process
src/main/capture/ capture sources behind one interface
src/main/mcp/server.ts loopback MCP server
src/mcp/stdio.ts dependency-free relay for stdio clients
src/preload.ts the only bridge into the renderer
src/renderer/ the window UI (no framework)
src/renderer/tokens.css the design tokens, shared by both windows
src/renderer/notch.* the notch panel's page, styles and script
src/renderer/env.d.ts ambient types; the renderer stays a script, not a module
src/test/ store and search tests
scripts/make-icons.js generates the mark from code, no image deps
scripts/adhoc-sign.js ad-hoc signs unsigned macOS builds so they will launch
scripts/copy-assets.js the renderer's html/css, which tsc does not emit
assets/fonts/ Satoshi, bundled so the app never fetches a font
web/ the website (Next.js, static export)
site/index.html the same story in one file, for a no-build previewRendererApi in src/types.ts is implemented by src/preload.ts and declared
as window.api for the renderer, so adding a channel on one side without the
other is a compile error rather than a runtime undefined.
Development
--screenshot=<file> renders the window, captures it and exits. Handy for
checking a change without a screen grab picking up whatever else is in front:
npx electron . --dev --screenshot=out.png --tab=memory --query="release process"npm run shots does the same job without launching anything. The renderer is
plain HTML, CSS and one compiled script, and the only thing it needs from
Electron is window.api; stub that and the pages run in any Chromium. It
writes both windows, in every state worth looking at, into out/shots:
out/shots/window-reminders.png window-memory.png window-settings.png
out/shots/notch-collapsed.png notch-island.png notch-expanded.pngTwo reasons to prefer it. It draws the UI against interesting data rather than the empty state a fresh profile gives you, so a full reminder list, a search with its nearest neighbours open, and every capture source running are all one command away. And it works on a machine where Electron cannot open a window at all, which is worth having the day that happens.
Signing
Builds are unsigned by default, so macOS shows an "unidentified developer" warning on first launch (right-click → Open, or allow it in System Settings → Privacy & Security).
To sign, add these repository secrets and drop the
CSC_IDENTITY_AUTO_DISCOVERY: false line from the workflow:
macOS:
CSC_LINK(base64.p12),CSC_KEY_PASSWORD, plusAPPLE_ID,APPLE_APP_SPECIFIC_PASSWORDandAPPLE_TEAM_IDfor notarizationWindows:
CSC_LINKandCSC_KEY_PASSWORD
Renaming it
The product name appears in package.json (productName), electron-builder.yml
(productName, appId), src/main/index.ts (setAppUserModelId),
src/mcp/stdio.ts (APP_DIR_NAME, which locates the config) and the copy in
web/ and site/index.html. The data folder name is the FOLDER constant in
src/main/paths.ts.
The website
The site lives in web/: a Next.js app exported to static files, because
documentation for a desktop app has nothing to run on a server.
cd web
npm install
npm run dev # http://localhost:3000
npm run build # static export into web/out.github/workflows/pages.yml builds and deploys it, and only runs when you
trigger it, because deploying makes the page public. Turn Pages on under
Settings → Pages → Source: GitHub Actions, then run the pages workflow.
It sets NEXT_PUBLIC_BASE_PATH from the repository name, since a project site
is served from /<repo> and every asset would otherwise 404.
site/index.html is the same story in one self-contained file, with no build
step, for opening straight off disk.
The design
The window, the notch panel and the site all draw from one vocabulary: the ENS design system (Thorin) in strict black and white, with the edges left hard. Satoshi as the typeface, Thorin's component shapes -- pill buttons, tag pills, switches, segmented tabs -- and no colour anywhere: every state is a fill or a stroke, so it survives a monochrome screen and a colour-blind reader alike.
The tokens are defined twice, once in src/renderer/tokens.css for the app and
once in web/app/globals.css for the site, because the two have no build step
in common. Satoshi is bundled in assets/fonts/ rather than fetched from a CDN:
an app that promises nothing leaves the device should not open a connection to
someone else's server to draw its own text.
What is left
TODO.md: the unbuilt capture sources, the settings that exist as an API but not as UI, and what has to happen before a release.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
- KogniteOAuthdev.kognite
Hosted agent memory: store, search, and recall facts across sessions from any MCP client.
Cross-tool persistent memory and context for AI assistants over MCP.
Persistent personal memory for AI assistants — save, search, and recall across every MCP client.
Your memory, everywhere AI goes. Build knowledge once, access it via MCP anywhere.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides persistent, local-first AI memory across sessions via MCP tools for storing, searching, and retrieving context from past interactions.1MIT

Enigma MCP Serverofficial
AlicenseNot gradedqualityAmaintenanceOffers MCP tools for managing a local AI memory vault, including remembering, searching, context packing, deletion, and verification of receipts.38 npm1Apache 2.0- FlicenseAqualityCmaintenanceEnables LLM agents to persist, search, update, forget, summarize, and manage user cognitive memories through MCP tools.7-
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to store and semantically retrieve durable memories across sessions via MCP or REST, with tools for remembering, recalling, asking, updating, and forgetting memories.12 npmMIT