Three guides in this series each built one piece: a vector database, something to put in it, and the protocol an agent uses to reach a tool. Each is useful on its own and none of them does the thing you actually wanted, which is an agent that can answer questions from your own notes.

This joins them with 78 lines of code and zero dependencies. The real test is not the wiring; it is what the agent is told when a piece of it goes down.

The shape

The agent never sees a vector. It calls a tool with a plain question; the server does the rest:

agent  --tools/call-->  server  --embed-->   Ollama
                          server  --search-->  Qdrant
agent  <---text-------  server

Two fetches and a bit of formatting. Node's built-in fetch talks to both services, which is why there is still nothing to install.

The tool description is the interface

This is the line that decides whether your work ever gets used:

"description": "Search the team's internal notes and documents for
 anything related to a question, and return the closest matches."

The model reads that and nothing else when deciding whether to call you. Describe the job, not the mechanism. "Query the vector database" is an implementation detail a model cannot reason about; "search the team's internal notes" is a situation it can recognise. A precise, boring sentence here is worth more than any amount of tuning underneath.

Running it

Six short documents were embedded and loaded. Then, through the protocol:

{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"search_docs",
           "arguments":{"query":"we have not been paid","limit":3}}}

What comes back is text, with the score attached so the model can judge confidence rather than trusting rank alone:

payment has not arrived from the client  (score 0.4959)
the invoice is overdue  (score 0.3686)
expenses must be filed within 30 days  (score 0.3154)

The query shares no meaningful word with any of those. That is the whole reason for the machinery, and it was measured in detail in the embeddings guide.

The check worth doing when you join things up

Wiring three working parts together is exactly where a tutorial starts describing what ought to happen. So: the embeddings guide ran this same query against the same model and the same store without MCP in the path, and got 0.4959 and 0.3686 for its top two.

Through the agent, with two extra hops and a protocol in between, the top two are 0.4959 and 0.3686. Identical. The wiring is passing the question through unchanged rather than quietly mangling it.

That is worth doing deliberately every time you put a layer in front of something that already worked. If the numbers had moved, the only useful response would be to find out why before writing another word. Without an earlier measurement to compare against, the layer is unfalsifiable by construction.

What happens when a piece is down

To see what matters in practice, the vector store was stopped mid-run — not simulated — and the same query asked again.

The server returned a result, not a protocol error:

"result": {"isError": true,
           "content": [{"type":"text","text":"Search failed: fetch failed"}]}

That distinction is deliberate and it is the one thing here you should copy. A JSON-RPC error means you, the client, asked wrongly. A dependency being down is not the client's fault — it is a tool failure, and the model should see it, be able to tell the user "the search service is unavailable", and carry on doing something else. Return a protocol error instead and the agent is told the call was malformed, which sends whoever debugs it in precisely the wrong direction.

By contrast, a malformed call — search_docs with no query — does return protocol error -32602, because that one really is the client's mistake.

When the store was started again, the next call succeeded with no intervention: payment has not arrived from the client (score 0.4959). No restart of the MCP server, no reconnection dance. The server holds no state, so recovery is free — which is an argument for writing them that way.

Before you point an agent at real documents

  • The agent inherits your access. This server has no authentication; it is launched by the client and runs as you, so whatever you can read, the agent can now read.
  • Return text the model can use, not identifiers. A result of "doc_4817, score 0.61" is useless to it. Include the passage.
  • Cap the result count. Every returned document is context the model pays for and can be distracted by. Three good matches beat twenty mediocre ones.
  • Embed queries with the model that embedded the documents. Mixing them fails silently — you get results, they are just wrong.
  • Six documents is a demonstration. Retrieval quality on a real corpus is its own problem, and nothing here measures it.

What is measured here

The server is 78 lines of code excluding comments and blank lines, with no dependencies. Everything above was executed on 21 August 2026 against Ollama 0.32.15 and Qdrant 1.19.0, by a script in our repository that drives the server over stdin and records what returns. The failure path was tested, not simulated: the script stopped and restarted the container, and the isError flag, the message and the recovery are all direct observations.

The consistency check compares against figures published in the embeddings guide, from a separate run on the same day. Matching to four decimal places is evidence the pipeline is faithful; it is not evidence that either result is good, which is a question about your corpus rather than about this code.

No real agent was tested. We drove the protocol directly, which proves the server behaves correctly and proves nothing about any particular client's configuration — the same limitation, for the same reason, as the MCP guide this builds on.