Zum Inhalt springen
An assistant node reaching a search index through a guarded gate — as a glowing low-poly illustration.
All posts
Series: NextSearchPart 4 of 4
·Self-HostingNextcloudAI

When a filter isn't enough

Some matters can't be clicked together — somebody has to read the hits. NextSearch now has an MCP server that lets an assistant do exactly that: search, read, tag, with its user's permissions.

Search finds words. Sometimes the question is whether a document is meant — and no filter holds that.

Part three was about tags and the API behind them, and the routine was clear: click the filters together in the interface until the result list is right, then pour that same search into a tag with one bulk call. It works as long as you can name the criteria. It falls apart the moment you can't.

The MCP server in detail

docs/mcp.md covers the tools, the permissions, the browser flow and the places where a connection typically fails to come up.


The problem: "Which contracts expire this year?"

The question is mundane, and search still doesn't answer it. A term isn't a metadata field on the document, it sits in the middle of the text — as "36 months from commissioning" in one, as "renews automatically for twelve months unless cancelled three months before expiry" in another, as a date in a table in a third. No single word catches all three, and the derived facets know nothing about it.

Searching for "cancellation period" returns forty hits. Roughly eight of them are contracts that genuinely expire this year. The other thirty-two mention the term in passing, belong to relationships that ended long ago, or are the covering letter rather than the contract. Finding the eight means somebody reads the snippets and decides. A script cannot do that — it can only do what you hand it as a filter.

That is the gap the MCP server fills: it gives an assistant the same four moves a person makes in the interface, and the judgement about which hit is meant happens between the calls.


Four tools, in that order

The endpoint sits at /api/mcp, on the same address as the interface. What a client gets there is deliberately narrow.

list_tags comes first, and not out of politeness. Tags are addressed by slug, and an assistant has no way of guessing that the installation says vertrag and not contracts. The tool returns the vocabulary this user may work with, with a flag per tag saying whether they can assign it at all.

search_documents is the full-text search with every facet the interface offers — instance, folder, directory, file type, year, size, whether OCR was involved. Back come the hits with metadata, a snippet and their tags, plus which facet values occur in the result set. That last part is the important one: it is how an assistant narrows the next search instead of guessing.

A tag that does not exist fails the call. Dropping it silently would return more documents than were asked for, and nothing in the answer would say so.

get_document fetches one document by its uuid: path, size, type, dates, origin, tags and the links to open it. No file contents — the matching text arrives as a snippet from the search, and the file itself is opened in NextSearch.

tag_documents sets and removes tags, up to 500 documents per call. It runs through the same services as the interface and /api/v1, so the same rules hold: documents in folders that were never shared are reported as skipped, and a tag this user may not assign fails the call rather than being quietly left out.

For the contract question that reads: fetch the vocabulary, search broadly, work through the forty snippets, send a get_document after the unclear ones, and tag the eight that survive with expiry-2026 in a single call. After that the question is no longer a research job but a filter in the sidebar.


Access nobody has to pass around

Two ways in, and they differ in who does the work.

Through the browser. The client registers itself, sends you to a page on your own installation, and there you decide what it may do. After that it holds a token that belongs to it alone. Claude Desktop works this way: add a custom connector, enter the URL, the rest happens on screen.

With a personal key, for clients that cannot run a browser flow. The same nxs_… key as the API in part three, as a bearer token:

claude mcp add --transport http nextsearch https://search.example.org/api/mcp \
  --header "Authorization: Bearer nxs_…"

What is connected is listed under User settings › MCP access, and one click cuts it.


The core: a connection inherits, it gains nothing

A tool that hands an assistant access to other people's documents stands or falls on what that access covers. The answer is deliberately unspectacular in two places.

A connection carries its user's permissions and no others. The same folder grants, the same tags, the same administrator rights or lack of them. What a person cannot find in the interface, their assistant cannot find either. There is no service account that may do more than a human — that would be convenient and would spend the very promise everything else rests on.

Reading and writing are separate. The consent screen always grants documents:read, since without it there is nothing to connect to. tags:write is a switch next to it that can be left off — then the assistant searches and touches nothing. A personal key carries both, because it already holds its owner's full permissions and pretending otherwise would be a promise the interface does not make either.

Then there is what the server deliberately cannot do: no file contents, no downloads, no index administration, no user management, no creating tags. The first because a document leaving the installation should be a visible act; the rest because their consequences belong where they can be seen.


The one place it breaks quietly

If a connection won't come up, look at APP_URL first. A client asks the installation where the authorization server lives and compares the answer with the address it arrived on. If those differ it stops — usually with nothing more helpful than a failed login.

So APP_URL has to be the address people actually type, including https://. An installation behind a reverse proxy whose APP_URL still says http://localhost:3000 serves the interface perfectly well and fails every MCP connection. Two paths also have to reach the backend through the proxy, and they are easy to miss when only /api is passed through:

/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server

The remaining failure modes are in docs/mcp.md. One rule of thumb saves the most time: the tools use the same services as the interface. Anything they cannot find, the search page cannot find either — which separates a permissions problem from an indexing problem in one step.


Conclusion

The MCP server does not make NextSearch cleverer. It gives an assistant the same four moves a person has in the interface and lets the judgement happen in between — for questions that cannot be phrased as a filter. For anything you can click together, the interface stays faster and the API from part three stays cheaper.

The permissions stay where they were. That is the part that took the most work to build and draws the least attention in operation.

It ships in NextSearch 0.4.0.


Call to action

NextSearch is open source and lives on GitHub: https://github.com/McGo/NextSearch

Hooking it up to an assistant needs a reachable installation, a correct APP_URL and two minutes on the consent screen. MCP_ENABLED=false turns the endpoint off as if it had never been there — for anyone who would rather not, for now.

That closes the series for the time being: from full-text search through running it on your own hardware and the tags to here. If you set NextSearch up for a team and get stuck somewhere, get in touch.