Skip to main content
Glama
Luissalet

FunesHoard MCP Server

by Luissalet

Funes's Hoard

Where was I? What was I doing before lunch? How much of this week actually went to Faustus?

Your computer's episodic memory: desktop activity and locally recorded audio, with searchable transcripts and a shared timeline.

Español · Quick start · Connect to Faustus · MCP reference · Portfolio

Day view: yesterday's timeline zoomed to the active hours with away time hatched, one segment pinned, and the Where was I? card with the last work contexts Actual application, three days of synthetic demo data (--demo, no real window titles).

Before anything else, what this does with your activity.

  • Where it lives: one SQLite file, data/funes.sqlite3, on this computer. No account, no telemetry, no cloud copy.

  • What is recorded: the foreground app, its window title, idle and locked time, files opened through Windows Recent Items, and commits in the git folders you add.

  • What is never recorded: keystrokes, screen content, screenshots or the clipboard. Password managers and private/incognito windows are excluded by default, and your own rules can drop or redact any app or window before a sample is written.

  • Pause: 15 min, 1 h or until you resume, from any screen. The assistant can pause too, but can never resume or shorten your pause.

  • Delete: any time range, permanently, from Privacy (with "last 15 min / 30 min / hour / today" shortcuts), plus automatic purge after 180 days by default.

  • What leaves the app: only "Write my day", if you use it, sends a compact day summary (categories, apps, projects, durations; never window titles) to the language model Hoard Link finds on this machine or the address you set in Settings. It can be switched off.

Full detail in Privacy and security.

Why

A local assistant has no record of what was on the user's screen. Asked "where was I?", "how did today go?" or "what was that DuckDB page I had open yesterday?", a language model can only invent an answer or ask the user to reconstruct it. Funes's Hoard records the one thing the model cannot see: the sequence of windows in front of the user, with idle and locked time, the files they opened and the commits they made. It turns that into spans, day summaries, focus blocks and resume-context answers, and gives the assistant focused tools to ask for them.

Related MCP server: DevFlow MCP

What is implemented

Area

Available now

Boundary

Collection

Foreground app, window title, idle and locked state sampled every second on Windows (ctypes + psutil); merged into active/away/locked spans; away time back-dated to when input stopped, except in a meeting or a video (also one in a browser tab), which get a separate, longer threshold (60 min by default) before only the rest counts as away; sleep gaps and excluded or paused moments close the span instead of bridging it; the open span is flushed every 30 s and a crashed session's span is closed on the next start

The Linux probe (xdotool/xprintidle) is a development aid; the Win32 calls are tested with the calls faked (see known limits)

Privacy

Exclusion rules drop a sample before it is stored (password managers, private/incognito windows, EN+ES); redaction rules keep the app and replace the title; invalid rules are rejected, not silently ignored; pause 15 min / 1 h / until resumed, with automatic resume; retention purge; delete a range (overlapping spans and their search rows included), with last 15 min / 30 min / hour / today shortcuts; export spans as CSV or everything as JSON, optionally for a range of days

Rules apply from the moment they are added, not retroactively to stored titles

Classification

Ordered app / title-regex / domain-in-title rules with defaults for common Windows apps; project detection from VS Code (hyphenated folders, remote and Insiders titles), JetBrains and Visual Studio titles, plus the names of discovered git repos; live preview ("would reclassify N spans"); reapply to history as a background job with progress

Browser domains are not read from the address bar; a "domain" rule matches text in the title

Derived knowledge

Day, week and range totals by category/app/project, clipped at the window edges; first/last activity; context switches (>= 10 s dwell); focus blocks (>= 25 min, each interruption <= 2 min); "where was I" with distinct contexts (work first; music, chat and games left out unless asked), their recent titles, files (the project's own first) and commits

Focus is measured from window time only; it says nothing about attention

Other sources

Recent files from a Shell Link (.lnk) parser written from the spec (Unicode paths, path suffixes, truncated files rejected); git commits from configured roots, filtered by configured authors or, by default, each repo's own git identity

Files opened without passing through Windows Recent Items are not seen

Search

SQLite FTS5 over titles, file paths and commit subjects, accent-insensitive, prefix words, safe for any input; falls back to "any word" when all words find nothing; a window hit says how long it was open and, in the interface, opens its day at that moment

If the platform's sqlite3 lacks FTS5 the app uses LIKE search (checked at startup)

Agent API

Twelve activity tools (two audio tools more for other apps, see below), read-only except a pause that can only extend; local ISO times and human strings in every result; small limits with has_more/next_offset; ids and times chain from one call into the next (activity_timeline(around=<a hit's ts>)); project-specific resume reaches back up to 180 days; every call audited, including rejected ones

The agent cannot resume, change rules, delete or export, by design

Audio memory

Record microphone and system audio, transcribe locally, import audio, search sessions, edit notes/tags and export TXT/SRT/Markdown. Open Audio y transcripción in Funes or install its /audio/ PWA. Audio lives in data/audio, with its own retention and local model cache

Recording requires an audio device; imported files work without one

Meeting minutes

An acta for a meeting or interview session: summary, decisions, open questions, participants and action items (who, what, for whom, by when), each with the evidence it came from (time, speaker, a quote that is literally in the transcript; time and speaker are taken from the transcript, never from the model). Long meetings are read in slices and merged. Deadlines such as "el martes" or "in two weeks" are worked out from the meeting's own date by plain rules, not by the model; when they name no single day they stay as words. Written by itself when a meeting or interview finishes transcribing (setting Escribir actas automáticamente, on by default), or on demand: Generar / Regenerar in the session view, timestamps that seek the audio, Copiar como Markdown, and the minutes are included in the Markdown export. Announces funes.minutes.ready {minutes_id, title, date, attendees} on the Hoard Link bus (the minutes id is the session id; the older keys session_id, action_items and started_at stay), and minutes_get hands the minutes to other apps in a compact shape

Needs a language model through Hoard Link; with none the answer is no_model and nothing is invented. Minutes are replaced when a session is transcribed again

Import by path

scribe_import_file(path, wait_s) imports a local audio or video file by its path without moving or deleting the original, and with wait_s returns the transcript (capped, with a hint to continue)

The path must be absolute and on this computer; the app never reads outside a file you name

Recall

recall/recall_search merge desktop episodes, Funes audio, Argus screen memory and Echo clipboard history into one citable timeline ([scribe:seg 17 16:04] keeps the existing audio citation id) (docs/RECALL.md)

Argus and Echo need their sibling apps; audio runs inside Funes

Contradictions

recall/recall_search drop memories that contradict a better-supported or more recent one (different time/value, negation, opposite state) and report contradictions: [{kept, dropped, reason}]; include_dropped keeps them, marked, and the Recall view shows a note (docs/RECALL.md)

Deterministic rules only, no model; works on sentences it can parse, es/en

Shared models

"Write my day": a cached, regenerable short narrative of a day ("You spent the morning on..."), from the same compact data activity_summary returns (never raw or redacted titles); Settings shows the resolved model, provider and a plain-English reason when none is available, with a Re-check button and manual overrides

UI-only, not an MCP tool; needs a language model reachable through Hoard Link (Faustus, or a shared Ollama, llama.cpp or other OpenAI-compatible server); a day with nothing recorded is refused without calling the model

Interface

Today (zoomable timeline with away and locked time drawn, legend, pinned details, keyboard-focusable segments, Where was I?, Write my day), Week (navigable), Search (date filter), Projects (range picker), Files & commits, Rules, Privacy, Settings (Models), Assistant activity; every day and moment has its own address (reload and Back work); English/Spanish; light/dark

Desktop layout; not designed for phones

More screens: Search · Privacy · Settings · Assistant activity (real calls made through the agent API by scripts/screenshots.py, including a rejected one).

Use cases

Eight concrete scenarios for a fictional developer, each walked end to end as a person in the browser and as a local model over MCP (docs/USE_CASES.md, findings and fixes in docs/USABILITY_REPORT.md):

  • Monday morning, "where was I?": the Today screen opens with the last three work contexts before now (project, last titles, files, commits), so Friday's last file is on screen before you pick a day; one click pins that moment in its day's timeline.

  • "Faustus, ¿qué estaba haciendo a las 16:00?": one recall call merges Funes's own window history with what was on screen (Argus), what was copied (Echo) and what was said in a call (Funes audio) around that moment, each item with a short citation to quote back; a source that is not running just shows up as unavailable, the rest of the answer still comes.

  • "Faustus, ¿dónde lo dejé ayer?": one activity_where_was_i call, about 500 tokens, answers with the project, the window and the editor title that names the file, skipping the music player and the chat.

  • A weekly note for a job search: activity_summary gives hours per project as ready-to-read strings, and a search for the job boards or a novel's title says how long those windows were open; Faustus writes the note with its own notes tool.

  • "That FTS5 page from Tuesday": search, click the hit and land on that day with the segment pinned (the address keeps it, so reload and Back work); an agent passes the hit's ts to activity_timeline(around=...).

  • Privacy before an interview or the bank: pause for an hour, private windows (also Spanish "incógnito") and bank pages never become searchable, and "Last 30 min" fills the delete-range pickers for the half hour you forgot.

  • Hours per project for a chart: export spans as CSV (local dates and times, minutes, category, project) for a spreadsheet or a data-analysis tool.

Quick start

git clone https://github.com/Luissalet/FunesHoard.git
cd FunesHoard

Windows

Double-click Iniciar Funes's Hoard.cmd. It runs scripts\start.ps1, which on first run creates .venv (Python 3.11+, preferring C:\Python313), installs requirements-lock.txt and builds the interface if frontend\dist is missing (Node 22); later runs reinstall only when the lock changed. It then starts the app hidden, with the repo root as working directory, waits for /api/health and opens http://127.0.0.1:8813. If the app is already running it just opens it. Detener Funes's Hoard.cmd stops it. The same from PowerShell: scripts\start.ps1 [-Port 8813] [-Demo] [-NoBrowser] and scripts\stop.ps1.

Manual steps:

python -m venv .venv
.venv\Scripts\pip install -r requirements-lock.txt
cd frontend; npm ci; npm run build; cd ..
cd audio_ui; npm ci; npm run build; cd ..
.venv\Scripts\python -m funes_hoard

Linux / macOS

Recording is Windows-first: on Linux the probe is best effort (X11 with xdotool and xprintidle), and macOS has no probe. The interface, the API and the MCP tools all run on synthetic data with --demo. Python 3.11 or newer and Node 22:

python3 -m venv .venv
.venv/bin/pip install -r requirements-lock.txt
(cd frontend && npm ci && npm run build)
(cd audio_ui && npm ci && npm run build)
.venv/bin/python -m funes_hoard --demo

The app answers on http://127.0.0.1:8813 (curl http://127.0.0.1:8813/api/health).

Flags: --demo (synthetic data in data-demo/, nothing recorded from the desktop), --data-dir (or FUNES_DATA_DIR), --port, --no-browser. Your own data lives in data/. The app only listens on loopback; --host refuses anything else.

Connect it to Faustus

Funes's Hoard is a plugin for Faustus and declares itself with faustus-plugin.json at the repo root. Start the app, then in Faustus open Connectors -> Nearby apps -> Add: Faustus finds it on port 8813, checks that /api/health answers as funes-hoard, starts the MCP adapter and loads the where-was-i skill, which tells a local model which tool to pick and the traps. The adapter is a stdio script started by path, with the app's URL in FUNES_URL:

$env:FUNES_URL = "http://127.0.0.1:8813"
.venv\Scripts\python.exe funes_hoard\mcp_server.py

MCP tools

Tool

What it does

Read-only?

activity_now

Current app, title, project, idle seconds, recording or paused (and until when)

yes

activity_where_was_i

Resume context: last distinct contexts before a moment, with title, files and commits

yes

activity_project_resume

Resume one named project's recent windows, opened files and commits, up to 180 days back

yes

activity_timeline

Spans for a day or range, paginated with offset

yes

activity_summary

Totals grouped by category/app/project, focus blocks, context switches

yes

activity_search

When a title, file or commit containing some words appeared

yes

activity_recent_files

Recently opened files

yes

activity_projects

Time per project, last touched, commits

yes

activity_pause

Pause recording; never shortens an existing pause

no (the only write)

recall

"What was I doing at time X" merged across Funes activity/audio, Argus and Echo, with a short citation per item

yes

recall_search

The same merge, but a text search across a range instead of a moment; both accept include_dropped and return contradictions

yes

sources_status

Health of every federated source (running, reachable, token accepted)

yes

scribe_minutes

Minutes of a recorded session (summary, decisions, action items with evidence); written with the local model if needed, no_model when there is none; regenerate rewrites them

no (stores the minutes)

minutes_get

The same minutes in the compact shape other apps ask for: {title, date, attendees, action_items: [{text, owner, due}], summary}; minutes_id is the session id from funes.minutes.ready; generate=false only reads what is stored

no (stores the minutes unless generate=false)

scribe_import_file

Import a local audio/video file by path; with wait_s returns the transcript so a caller can use it at once

no

The audio session tools (scribe_status, scribe_sessions, scribe_transcript, scribe_search, scribe_start, scribe_stop, scribe_note, scribe_tag, scribe_export, scribe_delete) are served by the same MCP adapter. Other apps reach Funes through the Hoard Link hub, which sees exactly the twelve activity tools plus scribe_minutes, minutes_get and scribe_import_file: starting, stopping and deleting recordings stay out of reach of other apps.

It works with any MCP client over stdio; docs/MCP.md has the output of every tool, the error codes and a config snippet, and docs/RECALL.md explains the federated recall tools and citation format in full.

Every tool description stays under 750 characters (about 250 tokens), so a host pays little for offering all of them; argument details live in each parameter's own description, and tests/test_tool_descriptions.py keeps it so.

Faustus assistant

Once the shared Hoard Link launcher is installed, the app shows the family's Faustus panel (a floating button) in both the activity interface and the audio pages. The conversation, the model and the tools live in Faustus; this app only tells the panel what is open.

  • Context per view. Only ids, titles and short previews leave the app: no window-title history, tokens or whole documents. In the activity interface the panel receives the page (today, timeline, week, search, projects, files, rules, privacy, assistant, settings), the open day and the pinned span (kind, from, to, minutes, category). In the audio pages it receives the open audio_session (title, up to 10 tags), its minutes, and a session that is being recorded (status recording, elapsed seconds). Creation views give no object.

  • Unsaved fields. The panel asks for them with the funes:context-request window event. Activity forms: search-form, sources-form, commit-sources-form, recall-form, classify-rule-form, privacy-rule-form, privacy-retention-form, privacy-delete-range-form, settings-form. Audio forms: search-form, recording-form, session-form, sessions-filter-form.

  • Refresh. When the assistant finishes a turn the open view reloads in place (hoard:copilot-turn-end / window.hoardCopilot.onTurnEnd); forms you are editing keep what you typed.

  • Saving. Forms send only the fields that changed. PATCH /api/sessions/{id} touches only the fields sent and has new add_tags and remove_tags; the new change_tags reads and writes under one lock, and the tag tool uses it, so a tag added elsewhere is never overwritten. PUT /api/backend/config merges per key, an empty string removes one override, and the token is sent only when typed.

  • Tests. node --test frontend/copilot-context.test.mjs, node --test audio_ui/copilot-context.test.mjs and python -m pytest tests/test_partial_updates.py.

The features that need a language model, "Write my day" and the meeting minutes, never load one of their own: it uses HoardLink (vendored in funes_hoard/hoard_link/), the resolver every Faustus plugin app shares, in this order -- explicit override in Settings, then Faustus's own model registry, then a shared llama.cpp, Ollama or other OpenAI-compatible server already running on this machine. The rest of the app works fully without any model at all, and Settings says exactly why when one is not available.

Architecture

flowchart LR
  Probe["Windows probe, 1 sample/s"] --> Privacy["privacy rules: drop or redact"]
  Privacy --> Spans["span builder + classification"]
  Spans --> DB[("SQLite in WAL mode")]
  Pollers["recent files, git log, retention"] --> DB
  UI["React interface"] -->|"/api/*"| API["FastAPI on 127.0.0.1:8813"]
  Model["Faustus or any MCP client"] -->|stdio| MCP["mcp_server.py"]
  MCP -->|"/api/agent/*"| API
  API --> DB
  API -->|"Write my day"| Link["HoardLink: shared language model"]
  API -->|"recall: /api/agent/call"| Argus["Argus's Hoard (screen)"]
  API -->|"recall: /api/agent/call"| Echo["Echo's Hoard (clipboard)"]
  API -->|"recall: /audio/api/agent/call"| Audio["Funes audio memory"]

FastAPI, one collector thread sampling once a second, one scheduler thread for recent files, git and retention, and SQLite in WAL mode. The span, privacy, classification and summary logic is pure Python with no FastAPI or sqlite imports. The MCP adapter is a separate script that only speaks HTTP to the app. See docs/ARCHITECTURE.md.

Privacy and security

  • Everything stays in data/funes.sqlite3 on this computer. No telemetry and no network use; git log runs locally. The one exception is "Write my day", described above: it sends only the compact activity_summary data (categories, apps, projects, durations, focus blocks) to the language model -- never raw or redacted window titles -- and can be turned off in Settings; deleting a day's history also deletes its cached narrative.

  • Exclusion rules run before a sample is written; a test asserts the row never exists. Redacted titles are never put in the search index.

  • The server binds loopback only. A middleware rejects DNS rebinding (a Host header that is not a loopback name with this app's port) and cross-site writes (a foreign Origin or Sec-Fetch-Site: cross-site); there is no CORS.

  • The assistant sees only what its tools return and can do exactly one thing: pause (or lengthen a pause). Every call, including rejected ones, is stored in the agent_calls audit table (tool, argument summary, duration, ok/error) and listed on "Assistant activity".

  • Results are capped (default 5-20 items, maximum 100) and long titles are truncated, because the intended consumer is a local model with a finite context window; the server instructions tell the model to treat window titles as data, never as instructions.

  • Retention and delete-range also remove file_events and commits in the same window, not only spans, so deleting history means all of it.

Development

.venv/bin/python -m pytest tests/ -q    # 300+ passed, offline, about 15-30 s
cd frontend && npm ci && npm run build  # 0 TypeScript errors

On Windows, .venv\Scripts\python -m pytest tests/ -q. CI runs the same two jobs on Ubuntu with Python 3.12 and Node 22.

The suite covers span merging, away/locked back-dating, sleep gaps, excluded and paused interludes, crash flushing and stale open spans; privacy rules (asserting the excluded row never exists) and rule validation; pause expiry and the agent's extend-only pause; retention and delete-range including the search index and the "Write my day" cache; the .lnk parser on fixtures built in the tests (ANSI, Unicode, suffix, truncated); project detection from editor titles; focus blocks, switches, clipping and where-was-i on scripted days; date words in both languages and their range edges; search on both the FTS5 and LIKE paths with hostile input; the browser guard (host and origin ports, null origin), the SPA path-traversal fix and the exact agent route list; the shared model backend (backend.json persistence and merging, the token never echoed back, resolved/unavailable Settings states, the Re-check swap, and "Write my day" caching/regeneration/disabling and the empty-day and empty-answer refusals against a fake Link, plus one pass through the real resolver over a mocked HTTP transport -- never a real network call); the Windows probe logic with the Win32 calls faked (tick wraparound, access-denied executables); git scanning with UTF-8 subjects, author filters, hidden consoles and timezones ahead of UTC; the CLI; the manifest check; and an MCP protocol test that spawns mcp_server.py over stdio against a live app and checks keywords, annotations, error passthrough and the audit log.

scripts/uxtest_data.py, scripts/ui_walkthrough.py and scripts/agent_walkthrough.py rebuild the realistic data set and replay the use cases in a browser and over MCP; scripts/screenshots.py retakes the images in docs/media/.

Roadmap / known limits

  • Windows-only paths are tested with fakes. The suite and CI run on Linux; the target is Windows with Python 3.13. The Win32 calls in WindowsProbe (GetForegroundWindow, GetLastInputInfo, OpenInputDesktop) have declared signatures and the logic around them is tested with the calls faked. Lock detection relies on OpenInputDesktop failing on the secure desktop, so a UAC prompt also counts as "locked".

  • FTS5 in the python.org 3.13 sqlite3 is expected but checked at startup; without it search falls back to LIKE.

  • The PowerShell launchers were run under PowerShell 7 on Linux (without -WindowStyle Hidden, which only exists on Windows); stop.ps1's Get-CimInstance and Get-NetTCPConnection calls have not been exercised.

  • No browser address-bar reader: a title rule decides streaming vs. browsing, so a video on a site the rule does not know stays Browsing with the ordinary idle threshold.

  • Minutes are only as good as the transcript and the model. Quotes are checked against the transcript and an action item without a verifiable quote is dropped, so a smaller model gives shorter minutes, not invented ones. Owners are "yo" (the person who recorded), a name that appears in the transcript, or empty. They are read from the words of the quote (a first-person promise on a single track is the user's; "Marta me tiene que devolver…" belongs to Marta), never copied from a speaker label such as otros or S1.

  • "Where was I?" looks three days back; after a longer absence it says there is nothing to pick up.

  • The Models panel shows the backend's diagnostic reason in English in both languages, and "Write my day" usually answers in English.

See docs/USABILITY_REPORT.md for how each item was found.

License

MIT - see LICENSE.

File transcription is a stateless family service: callers receive text and timed segments without creating recording sessions. The shared Whisper engine uses CPU for subsequent automatic-device jobs after detecting missing CUDA libraries.

The vendored copy is maintained by HoardLink’s sync script. Windows validation and the family service contract are documented in HoardLink’s docs/commons/windows-validation.md and docs/commons/services.md.

The day view also has an Agents lane, available through the read-only activity_agents_day MCP tool: local commits, currently closed Agora tasks and explicit Faustus turn receipts with source and coverage. It stays visible without desktop activity and does not infer agent working hours. Faustus's saved-turn receipt producer is integrated; only receipts actually sent to the Hub appear, with no reconstruction of older turns. See source limits.

With --demo, Windows Recent Items are not polled; the file list retains its synthetic examples. Use a separate data directory to keep existing activity out of a demonstration. See demo behaviour.

Speech-to-text status waits for the job's completion signal instead of polling at 250 ms intervals. wait_s still caps the caller's wait, and jobs retain the same result, error, cancellation and retention behaviour. A failed engine factory now completes the job as an error rather than leaving it running. The offline test suite uses isolated demo copies and fake Link clients for routes that do not exercise a backend; dedicated HTTP/MCP protocol tests keep their real local transports. On the recorded Windows run, 466 tests passed in 75.03 s (the repository target is under 90 s). This is test timing, not a speech-model throughput measurement.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides persistent local memory functionality for AI assistants, enabling them to store, retrieve, and search contextual information across conversations with SQLite-based full-text search. All data stays private on your machine while dramatically improving context retention and personalized assistance.
    3
    -
  • A
    license
    A
    quality
    D
    maintenance
    Connects AI assistants like Cursor and Claude to your local Daylens timeline so they can answer questions about your past work sessions, no cloud or API keys needed.
    5
    6 npm
    MIT