M72 · An MCP server for Qdrant that is a binary rather than an interpreter - #156
Merged
Conversation
…reter The way a model reaches a tool is MCP, and the only MCP server for Qdrant is written in Python, so every agent that searches a collection does it through an interpreter, a virtual environment and a dependency tree. For a process an agent spawns and kills repeatedly, install footprint and cold start are not incidental properties; they are most of what distinguishes one server from another, and a JVM server would have competed with the Python one on nothing. The gating question this item said to settle first is settled: the Kotlin MCP SDK publishes native artifacts for linuxX64, linuxArm64, macosArm64 and mingwX64, which is every target the release attaches a binary for. macosX64 is the one gap, and it is the only place this module reaches less far than the CLI. Six tools read and are on by default. upsert_points and delete_points exist and are off unless the operator passes --allow-writes, because letting a model write to somebody's index is a different risk class from letting it read one, and shipping writes on by default would be that choice made for the operator rather than by them. A read-only server does not register them at all, so they are absent from tools/list rather than present and refused. Nothing creates or drops a collection: a model that can create collections can fill a disk, and the operations that change what a deployment is belong where a person is typing. Three things were found by running it rather than by compiling it. Stdout is the protocol's channel and the SDK's logging library prints to it, starting with a banner before the first frame, so a client saw a parse error on the handshake and disconnected. The binary reconfigures that at startup and sends its diagnostics to stderr, where MCP expects them. Point ids came out as `Num(value=1)`, which is what the data class prints and not something a model can hand back to retrieve_points. A numeric id is a number now and a uuid is a string, because Qdrant treats 1 and "1" as different points. And the refusal message for a disabled write tool is unreachable over MCP: the SDK rejects an unregistered name before any handler runs. What tells the model why is the instructions field, which says the server is read-only. The guard stays for a caller holding the tools directly, and the KDoc now says where each fires. The hierarchy is wired by hand because it had to be. `read` and `write` take a size_t, 64-bit on Linux and macOS and 32-bit on mingw, and the compiler refuses a declaration in a shared native source set whose signature varies by platform. So the two primitives live one level down and everything that is logic stays shared. Declaring one dependsOn turns the default hierarchy template off entirely, which left nativeMain attached to nothing and the entry point compiled into no target: the link failed with "could not find main", a long way from the cause. prove-mcp.sh is the exit criterion, run on every push and again at every release for each platform: a client initializes, lists the tools, describes a collection, completes a search and checks the top hit, confirms a write is refused read-only and offered with the flag, and asserts that every line on stdout is JSON-RPC.
|
This pull request adds no capability. The code it touches can already touch the filesystem (2 places). What I could not follow39 calls resolve to a name whose type is written nowhere, so what they reach is not established here, and 2 more are in tests and benchmarks, which this report does not judge. Across everything it read that is 41 of 594, 7%. That is too many to list, and where they are is more useful than which they are:
If one of those reaches the network or starts a process, this report does not show it. 17 files read · 9226 ms · against |
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #125.
The question this was gated on
The item said to settle one thing before writing any of it: whether the Kotlin MCP SDK compiles for the
targets
kdrant-cliships for. It does.io.modelcontextprotocol:kotlin-sdk0.15.0 publishes nativeartifacts for
linuxX64,linuxArm64,macosArm64andmingwX64, and so do thecoreandservermodules underneath it.
macosX64is the one gap, and it is the only place this module reaches less farthan the CLI.
So the argument in the description survives: the only MCP server for Qdrant is written in Python, and for a
process an agent spawns and kills repeatedly, install footprint and cold start are most of what
distinguishes one server from another. Measured: 7.9 MB on macOS arm64, 9.1 MB on Windows x64, 16.4 MB on
Linux x64 and 15.3 MB on Linux arm64, the Linux ones larger because they link libcurl and its TLS stack
statically.
What it exposes
Six read tools on by default:
list_collections,describe_collection,search_points,scroll_points,retrieve_points,count_points.upsert_pointsanddelete_pointsare off unless the operator passes--allow-writes, and a read-only server does not register them at all, so they are absent fromtools/listrather than present and refused. Nothing creates or drops a collection: a model that cancreate collections can fill a disk, and the operations that change what a deployment is belong in
kdrant-cli, where a person is typing.Three things found by running it rather than compiling it
Stdout is the protocol's channel, and the SDK's logging library prints to it. It announces itself with
a
kotlin-logging: initializing...banner before the first JSON-RPC frame, so a client sees a parse erroron the handshake and disconnects. That is a server that works in a terminal and is broken everywhere it is
used. The binary reconfigures the logger at startup and sends diagnostics to stderr, where the MCP
convention puts them.
Point ids came back as
Num(value=1). That is what the data class prints, and it is not an id: a modelcannot hand it to
retrieve_points. A numeric id is a number now and a uuid is a string, because Qdranttreats
1and"1"as different points.The refusal message for a disabled write tool is unreachable over MCP. The SDK rejects an unregistered
name before any handler runs, so a model calling
upsert_pointson a read-only server gets "not found",which is the truth. What tells it why is the instructions field at initialize, which says the server is
read-only. The guard in
Tools.callstays for a caller holding it directly, and the KDoc says where eachfires.
Two build notes worth reading before touching the module
readandwritetake asize_t, which is 64-bit on Linux and macOS and 32-bit on mingw, and thecompiler refuses a declaration in a shared native source set whose signature varies by platform. So the two
primitives live in
posixMainandmingwX64Mainand everything that is logic stays innativeMain.Declaring one
dependsOnturns the default hierarchy template off entirely. That leftnativeMainattached to nothing and the entry point compiled into no target at all, and the link failed with "could not
find
main", which is a long way from the cause. The hierarchy is wired by hand now, with a comment sayingwhy.
Verification
prove-mcp.shis the exit criterion, and it ran locally against a realQdrant 1.19.1 before this was committed: a client initializes, lists the tools, describes a collection,
completes a search and checks that the point aligned with the query ranks first, confirms a write is
refused read-only and offered with
--allow-writes, and asserts that every line on stdout is JSON-RPC.It runs on every push and again at every release, for each platform the release attaches a binary for.
The tool surface is tested without a transport in
ToolsTest, because the schemas, the write gating and theargument messages are what a model meets and none of them need a process.