Skip to content
Muhammet Şafak
tr
Tools & Technologies 6 min read

The third MCP server was reasonable. The rest were not

MCP was the right answer for giving an agent a project's documentation. As the count grew, the servers themselves became the thing needing maintenance.

Cover — a dark server rack with only the third unit lit and labelled MCP-SERV-03, identical shelves receding row after row into darkness below it

Standing up a separate MCP server for every project looked reasonable for the first three. Then the servers started living apart: which one indexed which directory, which had last synced, which carried its own chunking code.

This is the story of how four requirements turned into four design decisions while I was opening the documentation of dozens of projects to my agents. The short answer: the right unit for giving an agent context is not one server per project but one endpoint per project. What built those four is Contextator.

The problem was not the token bill

My first diagnosis was wrong. I read the agent rediscovering the project every session as a line item: it walks the files, reads them, derives the same structure over and over, and that costs money.

The bill is real, but it is not the point. Where discovery ends, the model does not stop. When it cannot find what it is looking for, it keeps producing an answer and fills the gap with an assumption. That happens quietly: there is no line in the output saying “I could not find this file”, there is a plausible answer with nothing under it. You see the token bill at the end of the month; you see the assumption only if somebody reads that code.

The right answer is giving the model the place it should be searching — which is MCP. The protocol is not the hard part.

The hard part is doing the same thing dozens of times

Writing an MCP server for one project is a few hours the first time. The second is copy and paste. The third is still reasonable.

By the fifth you notice what it actually is: every new project means writing the same chunker, the same embedding loop and the same incremental indexer once more — and maintaining all of them separately afterwards. One supports a new file type, another does not. One drops deleted files from its index, another leaves them. Seeing which one synced when means looking at each in turn.

There is a second problem on top: a project’s documentation does not live in one place. Part of it is in the repository, part in the handbook in Notion, part in an Obsidian vault, part in an archive on a shared drive. A tool that indexes one folder waits for a human to consolidate first — which never happens.

Contextator collapses that repetition into one installation. A project is the sum of its sources: a mounted folder, a git repository (or a single subdirectory of it), an uploaded archive, an Obsidian vault, a Notion workspace. They meet at one endpoint, and every source is mounted under its own name, so a document reads as handbook/install.md.

First decision: isolation is not a setting, it is the address

Each project gets its own URL: /mcp/<project>. A client connected to /mcp/billing never sees /mcp/mobile.

The alternative was one endpoint with a project argument. In that design isolation would depend on the agent passing the right one — and confidentiality that rests on a model’s choice of argument is not confidentiality.

The cost is plain and accepted up front: there is no cross-project search. You cannot ask “which project did I use this pattern in?” in a single question. That is not an omission but the decision itself: if you could, every search would silently span every project.

Second decision: not needing a key in order to work

Embeddings are generated on the machine’s own CPU by default. The reason comes before cost: when a tool built to be queried constantly asks for an API key in order to work at all, it breaks the “runs on your own server” promise at the outset. A few environment variables switch it to OpenAI embeddings, but the default path never leaves the host.

The cost is paid at first start: the model is downloaded and the container takes a few minutes to come up. Later starts take seconds. To keep installation to one command, PostgreSQL and the application sit in the same image — a deliberate departure from one-container-one-process orthodoxy, and that is its price.

Third decision: a search that asks two indexes

A sentence model has no representation of HALYARD_DISPATCH_TIMEOUT. It has a representation of the words around it. And a good share of what people actually search documentation for is exactly that: a constant name, an error code, a flag.

So every chunk sits in a full-text index as well as a vector one, and a search takes both lists and fuses their rankings. Ranks rather than scores — because any weighting of scores would have to be re-learnt every time the embedding model changed, while ranks survive a model swap untouched.

The visible cost: the similarity score on a result no longer explains why that result is where it is.

Fourth decision: saying when nothing was found

When nothing clears the relevance floor, the search does not return its least bad hit; it says there is no good match and points to the tool that lists the documents instead.

The same discipline runs through indexing: a scanned PDF with no text layer is refused rather than indexed. Because the failure nobody notices is the one that produces a document: the empty string gets indexed, and the project now holds a document that exists, is listed, matches nothing, and reads as blank when opened.

A limit that was measured and then left alone

To be honest about it, one thing does not work: cross-lingual search. A question asked in Turkish whose answer lives on an English page mostly goes unanswered.

That is not a to-do but a closed decision. Two remedies were tried. Hybrid search first: it recovered identifiers and not one natural-language question. Then a multilingual reranking model: it raised cross-lingual recall but took a search from milliseconds to seconds and cost a number of ordinary questions their rank-one answer. Both were measured, and the measurements are in the repository.

The real fix would be a second translation-trained encoder: four times the download, a second vector column, and a full re-index on every installation. For a documentation server that price was not paid. So the finding sits in the documentation rather than on a roadmap — and if your corpus is genuinely bilingual and that kind of search is essential, it is a reason to pick a different tool.

What this costs in total

What I gained: opening a new project’s documentation to an agent is no longer writing a server, it is adding a source in a dashboard. When sources change, only the changed file is re-embedded and a deleted file’s documents are dropped.

What I pay is just as plain: no cross-project search, a first start that waits on a model download, one container that is not one process, and no searching across two languages. All four came out of a decision, and none of them is reversible — knowing what a tool does not do is as much part of the installation decision as knowing what it does.

The route I took and the reasoning behind the decisions are in the Labs entry; the product itself is in the portfolio. The tool is open source and runs on your own server.

Experiments on this topic

A multi-tenant documentation server that gives every project its own MCP endpoint and keeps the documents on your own machine.

What it does today

Makes a project's folder, git repository, Obsidian vault or Notion workspace searchable behind a single MCP endpoint, so an agent reaches the documentation without the data going to a third party. Anyone who can run Docker can host it and manage it from the dashboard.

Open source Web app TypeScript Node.js Fastify +6 more
August 2026 — September 2026

A self-hosted portal that puts ad-hoc production SQL behind approval, masking and an immutable trail; it became the QueryProxy product.

What it does today

Runs a developer's SQL against production through an approval step rather than directly; results are masked as they are written to disk and every request lands in an immutable record. Teams where production access sits with one person can run it today.

Open source Web app PHP Laravel Livewire +5 more
September 2026 — September 2026
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind