Post-Mortem: Building a Local MCP Server for Codebase Memory using Ollama and ChromaDB A developer built a local MCP server for codebase memory using Ollama and ChromaDB, enabling AI-assisted coding without sending proprietary code to cloud APIs. The project, zerikai_memory, supports local, cloud, and hybrid modes, with local mode running entirely on-device via Ollama. Benchmarks show mistral:7b runs faster on 8GB VRAM than ornith:9b, which offers better agentic coding performance but requires more memory. Developers are pushing back against cloud API billing and the privacy risks of sending proprietary codebases to third-party endpoints. A Hacker News thread from this year put it plainly: the problem isn't the price per token, it's the unpredictability of usage-based billing when AI agents are continuously polling APIs. On Reddit, the privacy concern is starker -- for enterprise and defense work, sending company IP to OpenAI or Anthropic is a hard no regardless of cost. zerikai memory https://github.com/KikeVen/zerikai memory has a local mode for exactly this: everything runs through Ollama, nothing leaves your machine. We shipped mistral:7b as the default local model. ornith:9b dropped in June 2026, trained specifically for agentic coding tasks, so we tested both. Here is what we found. zerikai memory https://github.com/KikeVen/zerikai memory runs in three modes: cloud DeepSeek , local Ollama , and hybrid . Routing between them is handled by should use cloud at main.py:986 , a 4-step priority chain: explicit override, keyword match, word count threshold, then MEMORY MODE env var fallback. In local mode that function always returns false -- everything stays on-device. In local mode, every synthesis call hits query ollama at main.py:1555 . The model receives a project brief from load project context plus structured ChromaDB entity payloads: function signatures, file paths, line ranges, docstrings. It returns an answer with inline file:line citations. One call, no streaming, no tool loop. The retrieval layer ChromaDB + L2 distance + lexical rerank is model-agnostic. Both models received identical context in every query test. The only variable was synthesis. 8GB dedicated VRAM is the hard ceiling. Windows offers shared system memory as overflow, but inference on shared RAM through PCIe is slow enough to matter in practice. mistral:7b v0.3 -- 4.4GB, 32K context. Function calling via Ollama raw mode only: you construct AVAILABLE TOOLS prompts by hand and parse TOOL CALLS responses yourself. Last updated May 2024. ornith:9b-dense -- roughly 5.5GB estimated, built on Gemma 4 and Qwen 3.5. RL-trained for agentic coding tasks with native tool calling. 69.4 on SWE-Bench Verified, 43.1 on Terminal-Bench 2.1, matching or beating models 3x its parameter count. A standalone Python script hit Ollama directly with static ChromaDB payload samples constructed from real zerikai memory https://github.com/KikeVen/zerikai memory workspace entities. 3 queries, 3 samples each per model, raw latency measured at the HTTP layer. This test measures inference speed on the hardware, not synthesis quality against a live codebase. | Model | Mean | StdDev | Min | Max | |---|---|---|---|---| | mistral:7b | 6.14s | 3.58s | 2.92s | 14.57s | | ornith:9b | 13.39s | 5.76s | 8.77s | 25.67s | ornith's cold start on the first query hit 25.67s. It spilled into shared memory before Ollama pinned the model. Warmed samples settled at 9-17s. mistral fits clean in 8GB and runs at 3-7s warm. The benchmark script is in the repo in the blog/ directory . Run it against your own hardware before committing to a model swap. Both models were tested live through the zerikai memory universal-brain MCP layer against the real zerikai memory https://github.com/KikeVen/zerikai memory workspace index. Same 5 queries, models switched manually between runs, Ollama restarted between each switch. This test measures what a developer actually experiences in Cursor or VS Code -- not synthetic payloads, real ChromaDB retrieval. Same query, same ChromaDB context, same system prompt. Here is what came back. ornith:9b: Based on the retrieved codebase memory, here's how query routing works: Routing Logic: 1. Explicit Override: The should use cloud function first checks for an explicit use cloud parameter that can force cloud mode DeepSeek regardless of other conditions main.py:986-1005. 2. Keyword Detection: If the user query contains keywords like "architecture", "explain", "how does", or "overview", it routes to DeepSeek's cloud mode config.py:43-44. 3. Word Count Threshold: Queries exceeding 40 words automatically route to cloud/DeepSeek via should use cloud 's word count check main.py:992-994, with this threshold defined in config as CLOUD ESCALATION WORD COUNT = 40 config.py:37. 4. Default Mode: Falls back to whatever is set as DEFAULT MEMORY MODE , which defaults to "hybrid" unless explicitly changed via the MEMORY MODE environment variable config.py:31-32, with this function returning based on whether that mode equals "cloud" main.py:996-998. Important Note: The retrieved context does not contain explicit information about Ollama's role in the routing decision or how local model queries are constructed when cloud mode is NOT selected. mistral:7b: The query routing in zerikai memory decides between using Ollama or DeepSeek based on a 4-step priority chain. The priorities are as follows: 1. Explicit use cloud override. 2. CLOUD ESCALATION KEYWORDS keyword match. 3. CLOUD ESCALATION WORD COUNT length threshold. 4. DEFAULT MEMORY MODE fallback recommended . This decision-making process aims to balance privacy, speed, and API costs while providing maximum context nuance for the user. For more detailed information on modes, configurations, and usage, you can refer to the README.md file in the project repository. Sources: main.py:986, README.md:320, main.py:1495, main.py:1311, README.md:105 ornith:9b: The .brain directory path is defined and resolved through two key locations: Definition: In config.py , line 1-2, there's a constant that defines the base name for the brain directory config.py:1 : DB PATH = '.brain/' BRAIN DIR = Path DB PATH Resolution: In main.py , lines 120-145, within the init workspace function, this path is resolved by joining it with the workspace's absolute path main.py:130 : path = Path workspace path .resolve db path = path / DB PATH db path.mkdir parents=True, exist ok=True The resolution process: 1. Takes the provided workspace path parameter 2. Resolves it to an absolute path using .resolve 3. Joins it with the .brain/ directory constant 4. Creates the full directory structure if it doesn't exist mistral:7b: The DB PATH constant, which is used for defining the storage root for all workspace data, is defined in config.py : DB PATH = Path file .parent / ".brain" It resolves to zerikai memory/.brain/ . This path is platform-agnostic as the Path object in Python handles different operating systems' file paths. Sources: config.py:67, main.py:1034, main.py:818, README.md:375, main.py:1796 ornith:9b: Background brief synthesis avoids MCP timeouts by running as a fire-and-forget task launched with asyncio.create task , meaning background brief synthesis is called without awaiting or returning its result. This decouples the slow LLM-based brief generation from the MCP request lifecycle, so the IDE agent receives its response immediately rather than waiting for synthesis to complete. Sources: main.py:784 - 1.96 rerank main.py:1827 - 0.99 rerank main.py:393 - 0.98 rerank main.py:2094 - 0.94 rerank mistral:7b: Background brief synthesis avoids MCP timeouts by using asynchronous task creation with asyncio.create task in the background brief synthesis function. This approach allows the function to run independently without blocking the main event loop, ensuring timely responses and avoiding MCP timeouts. The synthesized brief is saved as a markdown file in .brain/contexts/