privacy-first-search-mcp
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., "@privacy-first-search-mcpwhat did I write about Euclidean distance in my research notes?"
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.
Privacy First Search Lab
Build a local document-search pipeline, expose it through MCP, and watch a small local model use it from OpenCode.
A hands-on workshop for Nerdearla Argentina 2026, accompanying Building Privacy-First Vector Search Pipelines With Local LLMs.
Lab by Rudraksh Karpe, Hrittik Roy, and Shivay Lamba.
Follow a document from PDF extraction to embeddings, vector search, cited evidence, and an agent's answer. The main architecture runs OpenCode and the search services inside Docker sbx, with a dedicated local Ollama process providing inference on the host. A lighter local mode lets you rehearse the same retrieval and MCP workflow without creating a sandbox.
Contents
Related MCP server: rag-mcp
What you will learn
This workshop is for developers, platform engineers, and practitioners exploring local retrieval-augmented generation (RAG). Basic command-line, Python, and Docker familiarity helps; you do not need to have built an MCP server before.
By the end, you should be able to:
Parse documents locally and preserve file, page, and chunk provenance.
Explain how chunking and embeddings affect semantic retrieval.
Choose semantic search, exact metadata filters, or structured table access for a question.
Swap vector implementations behind a shared interface.
Expose bounded, read-only retrieval tools through Model Context Protocol (MCP).
Trace how OpenCode asks a local model to select tools and use their results.
Separate local processing, application permissions, and enforced network isolation.
Rehearse a demo with cached dependencies and inspect failures when they happen.
The implementation is deliberately inspectable: a Python parser and service, two vector adapters, five MCP tools, and a presentation script. It is a workshop reference with a single-user trust boundary, not a multi-tenant compliance platform.
Architecture and the role of Docker sbx
The diagram separates the runtime map from two left-to-right workflows. Open the full-size diagram for presentation.
The blue indexing lane prepares the corpus:
Verify the PDF corpus and mount it read-only into the ingestion container.
Extract pages, create chunks with stable citation IDs, and validate metadata and repayment schedules.
Call host Ollama to embed passage text with
nomic-embed-text:v1.5.Persist vectors and passages in Qdrant; store exact fields, schedules, source hashes, and ingestion state in SQLite. Structured fields bypass embedding.
The purple answering lane follows a live audience question:
Send the question to OpenCode; the small local model selects a tool and OpenCode issues the MCP call.
Validate the request and retrieve evidence through the five read-only MCP tools. Semantic search uses local query embeddings and Qdrant; exact filters and repayment schedules use SQLite.
Return passages with document IDs and page numbers, or structured records, through MCP to OpenCode. Repeat the tool loop as needed.
OpenCode sends the evidence to the local model and displays its cited answer.
The runtime map above the lanes shows the sbx microVM boundary. Its private Docker daemon owns the Compose containers, networks, images, and volumes. Host Ollama remains outside that border for local hardware acceleration. Location labels on the workflow cards identify where each step executes; repeated tool names refer to the same running services. The bottom panels show preparation and presentation policy separately from the data flow.
Two data flows
Ingestion: PDF → page text → validated metadata and repayment rows → page-local chunks → local embeddings → vector store. The SQLite catalog stores exact fields, source hashes, schedules, and ingestion state alongside the vector index.
Answering: question → OpenCode → local model → MCP tool request → retrieval service → evidence with citations → local model → answer. Ollama runs the model; OpenCode coordinates the tool calls. Ollama does not connect to MCP itself.
What sbx adds
Docker sbx is the intended microVM isolation boundary for the agent and search stack. Docker Compose runs Qdrant, the MCP service, and the ingestion job using the sandbox's Docker daemon. The kit adds a scoped local inference destination and contains no cloud-provider credentials. Preparation temporarily permits package and image downloads; the script then attempts to remove those preparation rules. The effective policy must pass the audit before presentation.
Ollama stays on the host to use Apple Silicon acceleration. Its local endpoint is an explicit boundary crossing. The sandbox does not control the host Ollama process's own outbound traffic, so that process separately disables cloud support. Only the workshop checkout is shared with sbx; the ingestion container receives the corpus through a read-only mount. The shared checkout itself is not immutable.
Why keep an exact catalog beside vectors?
“Where does this agreement discuss early repayment?” is a semantic retrieval task. “Which quarterly loans exceed $10 million?” requires exact filtering over the catalog. A few similar passages cannot establish an exhaustive portfolio count. Repayment schedules also need validated columns and arithmetic, rather than an LLM guessing relationships from flattened PDF text.
Read the architecture, sbx guide, and design decision for the full rationale.
Choose a run mode
Mode | Search storage | What it exercises | Current evidence |
| SQLite catalog and SQLite cosine scan | Fast ingestion, retrieval, MCP, and OpenCode rehearsal on the host | Verified with the full corpus and small model |
Standalone Docker Compose | Qdrant plus SQLite catalog in service volumes | Container packaging, Qdrant, and HTTP MCP on Docker Desktop | Verified; see commands |
| Qdrant plus SQLite catalog inside sbx | Intended workshop architecture with microVM and egress policy | Verified with sbx 0.43.0, the full corpus, MCP, and small model |
All modes share the same parser, retrieval service, and MCP tool contract. Local mode has no sandbox containment. Running Compose on Docker Desktop also does not establish that the sbx network policy works.
Prerequisites and model choices
Requirement | Details |
Host | Initial validation used an Apple Silicon Mac with 24 GiB unified memory; this is an observed environment, not a tested minimum |
Common tools | Git, Python 3.11–3.13, |
Local mode | Node/npm; preparation installs OpenCode 1.18.31 under |
Sandbox mode | Docker Sandboxes CLI and Docker account sign-in; verified with sbx 0.43.0 |
Standalone containers | Docker Desktop with Compose |
Preparation | Internet access and disk space for PDFs, packages, images, and model files |
Linux and Windows/WSL have not been rehearsed here. Prepare downloads before the session; first-run setup time depends on your connection and cache state.
Model role | Reference / setting | Reason |
Generation and tool selection | Workshop reference for local generation and tool selection | |
Existing demo context | 16,384 tokens, up to 2,048 output tokens | A runtime setting, not the reference model's maximum context |
Embeddings | nomic-embed-text:v1.5 | Same local model for document and query vectors |
The documentation and diagram use Gemma 4 E4B IT as the generation reference. The executable setup remains on Llama 3.2 3B; this reference update does not install or rehearse Gemma. See model references for the distinction.
The Modelfile sets the generation context and temperature zero. OpenCode configuration selects the local provider and workshop agent. There is no large-model or hosted-provider fallback. The model can still omit citations or invent prose; the two prepared answer steps include narrow quotation checks. Temperature zero is not a guarantee of factuality or identical output.
Packages are locked in uv.lock, and service/template images are pinned by digest.
Model tags can change: the index records the embedding model identity and digest
and rejects an incompatible query model. Changing the embedding model or parser
requires a fresh index.
Setup from a fresh clone
Run the commands below from the repository root after installing the prerequisites.
git clone https://github.com/rudrakshkarpe/Nerdearla-BA-privacy-first-search-lab.git
cd Nerdearla-BA-privacy-first-search-labFast local dry run
./workshop prepare --local
./workshop doctor --local
./workshop demo --localPreparation starts the dedicated Ollama endpoint, downloads missing model files and the corpus, installs the locked Python dependencies, ingests the PDFs, installs the pinned local OpenCode harness, and starts the MCP server. The preflight checks cloud-disabled inference, corpus readiness, source hashes, and the MCP protocol.
Main Docker sbx path
On macOS, install sbx if needed and complete its browser sign-in:
brew tap docker/tap
brew install docker/tap/sbx
./workshop loginThen prepare and verify the sandbox:
./workshop prepare
./workshop doctor
./workshop privacy-check
./workshop demo./workshop login starts Docker's browser device flow when needed, verifies the
result with sbx ls, and runs sbx diagnose. prepare performs the same login
check and reuses an existing named sandbox when present. It creates
privacy-search-lab with 3 GiB memory and four CPUs, pins OpenCode 1.18.31 inside
the sandbox, builds the service image, starts Compose, and ingests the corpus.
It does not reset an existing global sandbox policy. privacy-check verifies the
effective rules, local inference access, blocked external requests from both the
agent and service container, retrieval after the denial, and the policy log.
Endpoints and local state
Endpoint or location | Purpose |
Host | Dedicated workshop Ollama with |
| MCP endpoint on the host in local mode, or inside the microVM in sbx mode |
| MCP service health in the same environment |
| Compose-internal database; no database port published to the host |
| Downloaded PDFs, excluded from Git and Docker build contexts |
| Local-mode catalog and vector index |
| Process IDs, logs, harness installation, conversations, and rehearsal reports |
Compose volumes | Container-mode catalog and Qdrant storage |
The dedicated Ollama process shares already downloaded model files but does not change the regular Ollama service at port 11434. If port 11435 is occupied by an Ollama process with cloud support enabled, preparation refuses to use it.
Corpus and ingestion
The supplied Drive folder contains 40 fictional loan agreements spanning five illustrative borrower groups: household, microbusiness, small business, midsize, and corporation. The corpus index declares the identities and agreements synthetic. These specimens support document-review exercises; they are not regulatory authorities or a basis for legal compliance determinations.
Extracted material | Verified count |
Agreements | 40 |
PDF pages | 178 |
Page-local chunks | 481 |
Validated repayment rows | 1,744 |
The parser extracts text locally, checks the specimen layout, and validates repayment arithmetic using integer cents. It creates chunks of up to 180 words with 30-word overlap, keeping each chunk within one page. Every chunk carries a stable UUID, document ID, filename, source SHA-256, page, and ordinal.
Unchanged ready documents are skipped on re-ingestion. Partial or failed replacements are excluded from retrieval, and source hashes prevent old vector payloads from being returned as the current document. Deleting a source file does not silently delete its indexed copy; use explicit removal or rebuild the index.
The importer is tailored to these specimens. Unsupported layouts, encrypted PDFs, scanned/empty pages, oversized files, and invalid schedules fail visibly. OCR and arbitrary-contract extraction are extensions, not shipped features.
PDFs are downloaded during preparation and kept outside Git. Public folder access
does not grant redistribution rights; the repository ships a
provenance manifest and independently authored test fixtures
instead of the source PDFs. If Drive download fails, extract the folder manually
under data/source/. See data handling and
ingestion internals.
Try the retrieval pipeline
After ./workshop prepare --local, these commands exercise each retrieval path
without asking the language model to produce an answer. They use the default
local SQLite index and dedicated Ollama endpoint.
# Inspect extraction and index state
PYTHONPATH=src uv run privacy-lab inspect data/source
PYTHONPATH=src uv run privacy-lab status
# Semantic clause retrieval within one agreement
PYTHONPATH=src uv run privacy-lab search \
"fees and voluntary prepayment" --document-id DEMO-LA-2026-001 --limit 3
# Exact portfolio filter: quarterly loans strictly above $10 million
PYTHONPATH=src uv run privacy-lab list \
--frequency quarterly --min-principal-cents 1000000001
# Validated repayment rows, with source provenance
PYTHONPATH=src uv run privacy-lab schedule DEMO-LA-2026-001
# Check source files against the index
PYTHONPATH=src uv run privacy-lab verify-sources data/sourceExpected observations for the supplied corpus:
The prepayment search retrieves relevant page-2 evidence.
The exact filter returns a total of 10 matching loans.
Agreement 001 has 12 repayment rows, ending at a zero closing balance.
The minimum-principal filter is inclusive and takes integer USD cents.
1000000001 means strictly above $10 million; 1000000000 includes exactly
$10 million. Follow next_offset before making exhaustive claims about larger
result sets. Semantic scores measure similarity, not confidence or legal certainty.
For the sandbox, execute the same CLI inside its MCP container, for example:
sbx exec -w "$PWD" privacy-search-lab \
docker compose exec -T mcp privacy-lab statusMCP and the OpenCode tool loop
The application exposes retrieval through five read-only MCP tools. This is a bounded service in front of the vector database and catalog, rather than direct agent access to database administration.
Tool | Use it for | Bound |
| Similar clauses, optionally restricted by document ID | Query up to 2,000 characters; 1–10 hits |
| Re-open a chunk returned by search | One chunk UUID; no arbitrary file path |
| Exact category, frequency, and principal filters | 1–50 documents per page |
| Validated structured table rows | 1–24 rows per page |
| Counts and embedding identity | No document bodies |
For example, a search tool request carries these arguments:
{
"query": "fees and voluntary prepayment",
"document_id": "DEMO-LA-2026-001",
"limit": 3
}The result includes passage text and provenance. OpenCode exposes the tool with
its MCP server prefix, such as loans_search_documents, sends the result back to
the local model, and displays the answer. The workshop agent's permissions allow
only the loans_* tools. Ingestion, deletion, shell execution, arbitrary file
access, and web access are not available to it.
# Discover tools and exercise validation over the real HTTP MCP connection
PYTHONPATH=src uv run privacy-lab mcp-smoke
# Open the interactive harness with the workshop configuration
./workshop opencode --local
# Observe actual tool calls and verify the two prepared source quotations
python3 scripts/rehearse.py --local
python3 scripts/rehearse.py --local --case jurisdictionThe rehearsal saves real traces and reports under .local/; it does not supply
canned model responses. A successful process exit alone is insufficient: each
prepared case requires a successful search call, the expected source sentence,
and a document/page citation. These checks cover specific quotations, not every
possible unsupported statement. See the MCP contract.
The Enter-to-advance demo
./workshop demo # Main sandbox path
./workshop demo --local # Host-only functional rehearsalPress Enter to run a step, s to skip it, or q to exit. Each command is shown before it runs. A failed step stops the walkthrough and prints a resume command. The two generated-answer steps also stop if their evidence checks fail.
Step | Demonstration | What to explain |
1 | Preflight | Model configuration, corpus readiness, source identity, MCP connectivity |
2 | Corpus state | Which data and metadata stay in the local index |
3 | Repeat ingestion | 40 unchanged documents; idempotence avoids duplicate indexing |
4 | Semantic search | Similarity ranking and page-level evidence |
5 | Exact filter | Exhaustive catalog selection instead of top-k approximation |
6 | Repayment schedule | Structured rows and deterministic arithmetic |
7 | MCP discovery | Tool schemas, bounded arguments, and actual protocol calls |
8 | Prepayment answer | Observe the real tool call, source passage, and checked quotation |
9 | Unspecified jurisdiction | Quote what the specimen actually establishes |
10 | Privacy boundary | Audit and probe sbx policy; local mode explicitly skips isolation claims |
# Resume at semantic search; keep --local when resuming a local run
./workshop demo --local --from-step 4
# Run without pauses for a rehearsal
./workshop demo --local --autoA suggested 60-minute session allocates 10 minutes to architecture and corpus, 20 to ingestion/search/tables, 10 to MCP and OpenCode, 10 to privacy and model limitations, and 10 to exercises and questions. The detailed presenter runbook includes teaching points and failure recovery; the audience exercises extend each stage.
Privacy boundaries and limitations
Concern | Implemented control or boundary |
Hosted inference | Dedicated cloud-disabled Ollama process and local model/provider configuration |
Outbound access | Verified sbx allowlist audit, local-access probes, external denials from the agent and service container, and policy-log evidence |
Database exposure | Qdrant has no published host port; MCP is published to loopback |
Agent capabilities | Five retrieval tools; administrative changes remain explicit CLI operations |
Instructions inside PDFs | Treat source text and tool results as untrusted evidence; their contents cannot grant tool permissions |
Source traceability | File hashes, chunk IDs, page citations, and per-document readiness state |
Public repository hygiene | PDFs, indexes, logs, conversations, reports, and environment files excluded from Git |
Embeddings and vector payloads remain sensitive derived data; they are not anonymization. Storage is local but not automatically encrypted. The MCP endpoint has no user authentication and is intended for a single-user loopback deployment. Do not expose it as a public or multi-tenant service without adding authentication, authorization, transport protection, and operational controls.
Preparation needs external access for authentication and downloads. Presentation is intended to use cached artifacts after policy verification. The workshop does not implement selective web retrieval, local OCR, multi-user access control, or automatic compliance decisions. See security and scope.
Verification and development
The verification record separates executed checks from their limits. The recorded baseline includes 24 automated tests, all 40 PDFs, six corpus acceptance checks on both backends, real MCP handshakes, actual sbx policy probes, and two successful small-model quotation rehearsals in the sandbox.
Run code and protocol checks without downloading the corpus or models:
uv sync --frozen --group dev
PYTHONPATH=src uv run pytest -q
uv run ruff check src scripts tests
uv run ruff format --check src scripts tests
docker compose config --quiet
sbx kit validate ./sandboxThe last two commands require Docker Compose and sbx respectively. GitHub Actions runs the Python checks, Compose validation, and a guard against tracked generated data; it does not execute the real-model or sbx isolation rehearsal.
After local preparation, run the corpus and model checks:
./workshop doctor --local
OLLAMA_URL=http://127.0.0.1:11435 PYTHONPATH=src uv run python scripts/evaluate.py
python3 scripts/rehearse.py --local
python3 scripts/rehearse.py --local --case jurisdictionBoth vector adapters share contract tests for search, filtering, replacement, and removal. The SQLite adapter is an educational exact cosine scan with O(number of chunks × vector dimensions) query cost. Qdrant supplies the dedicated vector-store path. Parser, backend, and embedding changes need compatible index state; use separate indexes when experimenting.
Read the commit history to follow architecture, parsing, retrieval, MCP, containers, sandbox configuration, small-model tuning, regression fixes, and rehearsal as focused changes.
Troubleshooting and cleanup
Symptom | First action |
| Run |
Compose cannot find | Remove an old workshop sandbox and rerun preparation; current kits use the pinned |
Docker Hub layer returns | Rerun |
OpenCode prints a tool call instead of invoking it | Rerun preparation so the sandbox installs the verified OpenCode 1.18.31 build |
Drive download fails | Download and extract the corpus under |
Partial/custom corpus detected | Inspect the source folder; the conductor expects the supplied 40 PDFs |
Port 11435 has cloud support enabled | Inspect the process occupying it; the workshop intentionally refuses that endpoint |
Model is slow or memory is pressured | Keep the configured demo context bounded, warm the model first, and stop unrelated heavy inference jobs |
Answer fails its quotation check | Inspect the actual tool evidence and trace; a tool call does not prove the answer is grounded |
Python cannot import the package on macOS | Use the shown |
Index identity differs from the model | Create a fresh index rather than mixing embedding identities |
Privacy audit fails or external access succeeds | Inspect effective sbx rules and deny logs before making an isolation claim |
./workshop stop stops the workshop sandbox. After reviewing the
retention and reset instructions, ./workshop reset --yes
removes the sandbox's Compose service volumes, including indexed data; source PDFs
and host model files are retained. Local processes are identified in .local/*.pid
and require inspection before stopping. Local cleanup is documented separately;
stop --local and reset --local do not automate it.
Repository map and further reading
Nerdearla-BA-privacy-first-search-lab/
├── workshop # Executable entry point for setup and presentation
├── scripts/
│ ├── workshop.py # Preparation, preflight, and guided demo
│ ├── evaluate.py # Real-corpus retrieval acceptance checks
│ ├── rehearse.py # Actual OpenCode tool-call and quotation checks
│ └── privacy_audit.py # Conservative sandbox policy audit
├── src/privacy_lab/
│ ├── documents.py # PDF extraction, metadata, chunks, repayment validation
│ ├── embeddings.py # Local embedding requests and model identity
│ ├── catalog.py # Exact fields, schedules, and ingestion state
│ ├── vectors.py # Qdrant and SQLite vector adapters
│ ├── service.py # Ingestion, retrieval, provenance, removal
│ ├── mcp_server.py # Bounded read-only tools and service health
│ └── cli.py # Administrative and inspection commands
├── sandbox/spec.yaml # Docker sbx kit and scoped inference destination
├── compose.yaml # Qdrant, MCP, ingestion job, networks, volumes
├── Dockerfile # Search service image
├── opencode.json # Local provider, MCP connection, agent permissions
├── models/Modelfile # Small generation model and context settings
├── data/manifest.json # Source provenance without publishing PDF bodies
├── docs/ # Attendee, presenter, design, and operations guides
├── tests/ # Parser, retrieval, protocol, and failure-path checks
└── .github/workflows/ # Automated repository checksGuide | Read it when you want to… |
Prepare a machine and choose a mode | |
Follow data ownership and trust boundaries | |
Understand the microVM, host endpoint, and network policy | |
Inspect parsing, chunk identity, table validation, and index lifecycle | |
Understand schemas, bounds, pagination, and provenance | |
Work through attendee experiments | |
Deliver the 60-minute session and recover from failures | |
Inspect services, validate standalone containers, reset, or troubleshoot | |
Check exactly what ran and the limits of that evidence | |
Understand the component choices and tradeoffs |
Contributing and license
See CONTRIBUTING.md for the development workflow and SECURITY.md for security scope. Keep behavioral changes focused, exercise meaningful failure paths, and update the verification record with actual execution evidence. Do not commit source documents, generated indexes, credentials, or harness conversations.
Repository code and documentation are MIT licensed. Third-party documents, models, and dependencies retain their own terms. The repository license does not grant redistribution rights to the supplied PDFs.
This server cannot be deployed
Maintenance
Related MCP Connectors
Agentic search over your Dewey document collections from any MCP-compatible client.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
Read-only MCP tools for AI agent discovery, structured resources, and NIULAI information.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceProvides read-only, citation-backed semantic search and retrieval-augmented generation over enterprise documents via standardized MCP tools, with local embeddings for privacy.-
- FlicenseNot gradedqualityCmaintenanceProvides read-only MCP tools for hybrid semantic and keyword search over locally indexed PDF documentation, with citations and context retrieval for LLM agents.-
- AlicenseNot gradedqualityBmaintenanceEnables AI tools to securely search and retrieve relevant, source-attributed chunks from private local documents via MCP, without sending document content to third-party services.1MIT
- FlicenseNot gradedqualityBmaintenanceEnables local full-text and semantic search over saved content, providing search and retrieval capabilities via MCP for AI assistants.-