Skip to main content
Glama
rudrakshkarpe

privacy-first-search-mcp

Privacy First Search Lab

Code and protocol checks

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:

  1. Parse documents locally and preserve file, page, and chunk provenance.

  2. Explain how chunking and embeddings affect semantic retrieval.

  3. Choose semantic search, exact metadata filters, or structured table access for a question.

  4. Swap vector implementations behind a shared interface.

  5. Expose bounded, read-only retrieval tools through Model Context Protocol (MCP).

  6. Trace how OpenCode asks a local model to select tools and use their results.

  7. Separate local processing, application permissions, and enforced network isolation.

  8. 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

Complete workflow for the privacy-first search lab, including Docker sbx, OpenCode, MCP, Ollama, Qdrant, SQLite, ingestion, retrieval, policy, and evidence flows

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:

  1. Verify the PDF corpus and mount it read-only into the ingestion container.

  2. Extract pages, create chunks with stable citation IDs, and validate metadata and repayment schedules.

  3. Call host Ollama to embed passage text with nomic-embed-text:v1.5.

  4. 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:

  1. Send the question to OpenCode; the small local model selects a tool and OpenCode issues the MCP call.

  2. 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.

  3. Return passages with document IDs and page numbers, or structured records, through MCP to OpenCode. Repeat the tool loop as needed.

  4. 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

./workshop prepare --local

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

./workshop prepare

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, uv, and Ollama

Local mode

Node/npm; preparation installs OpenCode 1.18.31 under .local/

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

Gemma 4 E4B IT

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-lab

Fast local dry run

./workshop prepare --local
./workshop doctor --local
./workshop demo --local

Preparation 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 login

Then 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 127.0.0.1:11435

Dedicated workshop Ollama with OLLAMA_NO_CLOUD=1

127.0.0.1:8765/mcp

MCP endpoint on the host in local mode, or inside the microVM in sbx mode

127.0.0.1:8765/health

MCP service health in the same environment

qdrant:6333

Compose-internal database; no database port published to the host

data/source/

Downloaded PDFs, excluded from Git and Docker build contexts

data/index/

Local-mode catalog and vector index

.local/

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/source

Expected 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 status

MCP 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

search_documents

Similar clauses, optionally restricted by document ID

Query up to 2,000 characters; 1–10 hits

get_passage

Re-open a chunk returned by search

One chunk UUID; no arbitrary file path

list_documents

Exact category, frequency, and principal filters

1–50 documents per page

get_repayment_schedule

Validated structured table rows

1–24 rows per page

corpus_status

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 jurisdiction

The 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 rehearsal

Press 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 --auto

A 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 ./sandbox

The 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 jurisdiction

Both 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

Not authenticated to Docker

Run ./workshop login; Docker Desktop sign-in alone may not authenticate sbx

Compose cannot find /var/run/docker.sock

Remove an old workshop sandbox and rerun preparation; current kits use the pinned opencode-docker template

Docker Hub layer returns 403

Rerun ./workshop prepare; it temporarily permits the observed CloudFront registry redirect and removes the rule afterward

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 data/source/, then rerun preparation

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 PYTHONPATH=src commands; see the editable-package note in the operations guide

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 checks

Guide

Read it when you want to…

Quickstart

Prepare a machine and choose a mode

Architecture

Follow data ownership and trust boundaries

Docker sbx

Understand the microVM, host endpoint, and network policy

Ingestion

Inspect parsing, chunk identity, table validation, and index lifecycle

MCP tools

Understand schemas, bounds, pagination, and provenance

Exercises

Work through attendee experiments

Presenter runbook

Deliver the 60-minute session and recover from failures

Operations

Inspect services, validate standalone containers, reset, or troubleshoot

Verification

Check exactly what ran and the limits of that evidence

Design decision

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.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Provides read-only MCP tools for hybrid semantic and keyword search over locally indexed PDF documentation, with citations and context retrieval for LLM agents.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    1
    MIT