Kdrant MCP server
Commit 9. Nov. 2026Ein idiomatischer Kotlin-Client für die Vektordatenbank Qdrant, der Coroutine-first APIs und ein type-sicheres DSL bietet.
Auch bekannt als NaCode-Studios/Kdrant
- Stars
- 6
- Tools
- —
- Downloads, 30 Tage
- —
- Neueste Version
- —
- Letzter Commit
- 9. Nov. 2026
- Lizenz
- Apache-2.0
Was ist Kdrant MCP?
Kdrant ist ein MCP server. Ein idiomatischer Kotlin-Client für die Vektordatenbank Qdrant, der Coroutine-first APIs und ein type-sicheres DSL bietet.
Zusammenfassung generiert aus dem README des Projekts
Über Kdrant MCP server
Kotlin-Entwickler, die Vektorsuch-Anwendungen bauen, können Kdrant nutzen, um mit einer Qdrant-Vektordatenbank über idiomatische, Coroutine-first Patterns zu interagieren. Der Client bietet ein type-sicheres DSL und nutzt suspend Funktionen oder Flows für jede Operation, wodurch sichergestellt wird, dass Abbrüche und Timeouts kooperativ gehandhabt werden. Er ist auf Leichtgewichtigkeit ausgelegt und bietet eine Standard-REST-Engine via Ktor und kotlinx-serialization, die den schweren Dependency-Footprint von gRPC oder Netty vermeidet.
Nutzer können zwischen der leichtgewichtigen REST-Engine für die meisten Workloads oder einer optionalen gRPC-Engine für hohe Durchsatzraten wählen. Die Library ist Multiplatform und unterstützt Targets wie JVM, iOS, Linux und macOS. Obwohl Kdrant selbst keine Embeddings berechnet, kann es Server-seitige Inference ermöglichen, sofern die verbundene Qdrant-Instanz mit einem Inference-Provider konfiguriert ist. Integrationen für Spring Boot, Spring AI und LangChain4j sind verfügbar, was die Library ideal für den Bau von Retrieval-Augmented-Generation Services macht.
Was du damit machen kannst
- 1Leichtgewichtige Vektorsuch-Clients bauenEin Entwickler kann einen Search-Client mit minimalem Memory-Footprint implementieren, indem er die REST-Engine anstelle eines schweren gRPC-Stacks verwendet.
Geschrieben aus dem README des Projekts. Jeder Use Case wurde gegen ein Zitat daraus geprüft.
Konfiguration
QDRANT_API_KEY
| Name | Beschreibung | Typ |
|---|---|---|
| QDRANT_API_KEY | — |
Automatisierte Checks
Geluften am 14. Nov. 2026Vollständiger Bericht →Noch nicht verifiziert. Ein Server benötigt einen Score von 80, keine kritischen Befunde und genug Daten für unsere Prüfung. Wie die Checks funktionieren · Was du für mehr Punkte verbessern kannst
- Source Repository verknüpft und erreichbarnacode-studios/kdrant is public and reachablegithub.com/nacode-studios/kdrant ↗Bestanden
- Paket nennt dieses Repository als QuelleNicht geprüft
- Build-Provenienz (Sigstore-Attestierung)Nicht geprüft
- Open Source Lizenz (OSI)Apache-2.0spdx.org/licenses/Apache-2.0.html ↗Bestanden
- Commit oder Release im letzten JahrLast commit or release 2026-11-09github.com/nacode-studios/kdrant ↗Bestanden
- OpenSSF ScorecardNicht geprüft
- Package-Name ähnelt keinem populären PackageNicht geprüft
- Keine kritischen Advisories im Paket oder seinen AbhängigkeitenNicht geprüft
- Keine versteckten Anweisungen an das Modell0 tool descriptions and the README read; no override phrases, credential paths or hidden charactersBestanden
- Destruktive Tools sind markiertNicht geprüft
- Remote-Endpoint beantwortet den MCP-HandshakeNicht geprüft
README
Auf GitHub ansehenKdrant
An idiomatic, coroutine-first Kotlin client for the Qdrant vector database.
Qdrant's official JVM client is built for Java: every call returns a ListenableFuture, requests are
assembled with protobuf builders, and the wire is decided for you: gRPC, with a shaded Netty on your
classpath whether you needed the throughput or not. Kdrant is the client you would want to write Kotlin
against: suspend functions, a type-safe DSL, kotlinx-serialization models, and a wire you pick. The
default engine is pure-Kotlin REST and pulls in no gRPC, no protobuf and no Netty; the gRPC engine is
one dependency away when throughput is the bottleneck. Both are the same QdrantClient and are held to
the same behavioural test suite against a real Qdrant.
val qdrant = Kdrant(host = "localhost", port = 6333) {
apiKey = System.getenv("QDRANT_API_KEY")
requestTimeout = 5.seconds
}
qdrant.use { client ->
client.createCollection("articles") {
vector { size = 1_536; distance = Distance.COSINE }
}
client.upsert("articles", wait = true) {
point(id = 1) {
vector(embedding)
payload("title" to "Introduction", "lang" to "en", "year" to 2026)
}
}
}
Kdrant computes no embeddings. It bundles no model and depends on no inference library, so
embedding above is a List<Float> you produced. Where your Qdrant has an inference provider
configured, Kdrant can send the text and the model name and let the server produce the vector: see
server-side inference.
See it end to end.
example-ragis a small runnable Retrieval-Augmented-Generation service (ingest, embed, store, retrieve) built on Kdrant, with adocker-composefor Qdrant.
Status —
2.3, stable. Both engines cover Qdrant's API through 1.19: collections, points, the modern/points/querysearch including hybrid fusion, sparse and multi-vectors, prefix matching, relevance feedback and sliced scrolls, memory tiers and 4-bit storage, scroll, payload and vector management, aliases, snapshots, quotas, cluster and sharding. Which Qdrant that is, is one fact the build checks rather than a claim. The public API is stable under SemVer and tracked per target; see STABILITY.md. The plan is on the board.
Why Kdrant
Every operation is a suspend function or a Flow. Cancellation and timeouts are cooperative and
CancellationException always propagates, so a request abandoned by a caller is a request that stops.
The default engine costs no gRPC stack. REST on Ktor and kotlinx-serialization adds roughly 3 to 5 MB;
protobuf, Netty and grpc-java reach your classpath only if you ask for kdrant-transport-grpc by name,
and a build that does not ask is checked on every commit rather than trusted.
Failures are a sealed hierarchy you can handle exhaustively, identically over either engine, and
every one of them says whether the same request could succeed later. A degraded cluster reports itself
as ReadOnly, ShardUnavailable or PartiallyApplied rather than as a generic failure.
The wire sits behind one seam. That is not a claim about layering: it is why a second engine exists
without a line changing in kdrant-core, why tracing and metrics are one decorator each rather than one
per engine, and why the same code sends a request from iOS, Linux and Windows as well as from the JVM.
Compared with the official client
Dependency stacks verified against io.qdrant:client:1.18.3.
Kdrant (kdrant-transport-rest) |
Official io.qdrant:client |
|
|---|---|---|
| Wire protocol | REST/HTTP over Ktor CIO | gRPC (HTTP/2) |
| Heavy dependencies | none, pure Kotlin (Ktor + kotlinx) | grpc-netty-shaded (bundled Netty), grpc-protobuf/grpc-stub, protobuf-java, Guava, slf4j |
| Approximate added footprint | 3 to 5 MB | 15 to 20 MB of transitive jars, shaded Netty about 9 MB alone |
| API style | suspend functions and Flow, type-safe DSL |
ListenableFuture<T>, protobuf builders |
| Models | kotlinx-serialization data classes |
generated protobuf messages |
| GraalVM native image | a cold process answers a search in single-digit milliseconds from a 42 MB static binary, measured and bounded on every push | needs gRPC, Netty and protobuf native configuration you write and maintain |
That last row is a CI job rather than an adjective: native-image compiles
example-native-image with --no-fallback and makes it search a real Qdrant
on every change, so the day a dependency starts reflecting, the build fails instead of the sentence
quietly becoming false. The same job fails if that search crosses a ceiling, which
is what keeps the figure beside it from drifting. The ceiling is loose because the runner is shared, and
the span is narrow on purpose: the collection is seeded before the binary starts, so the clock covers
process start, client construction and one round trip rather than a collection being built. That
narrowing is also why the figure fell. It used to read 37 ms, and most of that was the server creating a
collection and making a write durable inside a span this table was calling a cold start. Nothing is required of you:
kdrant-transport-rest ships the one reflection registration kotlinx-serialization needs, generated
from its own classes rather than written by hand.
That table is about footprint. The speed question has an answer too, and it is measured rather than
argued: benchmarks/README.md runs both clients and both of Kdrant's
engines against the same server in the same JVM.
Kdrant over REST is slower than the official client on every operation, by 2.5x on a single search and
9.3x on a 500-point upsert. Over Kdrant's own gRPC engine that gap collapses to between 8% and 30%. So
what a comparison of the two defaults measures is HTTP and JSON against protobuf, not a Kotlin client
against a Java one, and the suspending functions and typed DSL cost nothing detectable. If a hot path is
latency-sensitive, kdrant-transport-grpc is the same QdrantClient behind the same API and the choice
between libraries stops being about speed. For typical RAG and embedding-search workloads, REST trades
those milliseconds for a fraction of the footprint.
Installation
Requires JDK 17+. Artifacts are on Maven Central under io.github.nacode-studios.
dependencies {
implementation("io.github.nacode-studios:kdrant-transport-rest:2.2.0")
}
kdrant-transport-rest brings kdrant-core with it; it is the only dependency you add. You also need
a running Qdrant:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
6333 is the REST port and 6334 the gRPC one; map both and either engine connects.
The modules
Everything below is optional and additive.
| Artifact | What you get |
|---|---|
kdrant-transport-rest |
The one to start with. The REST engine and the Kdrant(...) factory. Multiplatform. Brings kdrant-core with it. |
kdrant-transport-grpc |
The opt-in gRPC engine and the KdrantGrpc(...) factory, for when throughput is the bottleneck. JVM only, because it is grpc-java. |
kdrant-core |
The public API, models, DSLs and the QdrantTransport seam. Multiplatform. You rarely depend on it directly. |
kdrant-spring-boot-starter |
Spring Boot auto-configuration: kdrant.* properties and a ready QdrantClient bean. |
kdrant-spring-ai |
A Spring AI VectorStore backed by Kdrant, metadata filters included. |
kdrant-langchain4j |
A LangChain4j EmbeddingStore backed by Kdrant, metadata filters included. |
kdrant-koog |
A Koog document storage where Qdrant runs the search instead of the agent scoring in memory. |
kdrant-micrometer |
A timer per operation, over either engine, tagged by the operation rather than by a URL. |
kdrant-otel |
One OpenTelemetry client span per operation, over either engine, carrying no payload or vector data. |
kdrant-migrate |
Re-embed a collection into a new one, resume after an interruption, verify, and move the alias only once the verification passed. |
kdrant-cli |
A single static binary for the operations that are not requests. Not on Maven Central; the binaries are attached to each release. |
kdrant-bom |
A platform that keeps the versions above aligned. Import it and drop the versions. |
dependencies {
implementation(platform("io.github.nacode-studios:kdrant-bom:2.2.0"))
implementation("io.github.nacode-studios:kdrant-transport-rest")
implementation("io.github.nacode-studios:kdrant-spring-ai")
}
Choosing an engine
REST is the default and the right answer for most applications: a smaller dependency set, no native configuration under GraalVM, and every operation available.
val qdrant: QdrantClient =
if (useGrpc) KdrantGrpc(host = "localhost") // gRPC, port 6334
else Kdrant(host = "localhost") // REST, port 6333
Every example below reads the same either way. Two differences matter before you switch. The port is 6334 rather than 6333, and nothing rewrites it for you. And Qdrant serves sixteen operations over HTTP only: telemetry, Prometheus metrics, the two issues calls, the two quota calls, snapshot recovery, the snapshot and storage-snapshot transfers, and the six shard-scope snapshot operations. The gRPC engine refuses each of them by name rather than degrading quietly.
Platforms
kdrant-core, kdrant-transport-rest and kdrant-migrate publish one artifact per target: the JVM,
iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, linuxArm64, linuxX64 and
mingwX64. An iOS or Linux application depends on the same coordinate a JVM one does, and a Gradle
build resolves the right variant with no change. A Maven build names the artifact directly and wants
the -jvm one: kdrant-core-jvm, kdrant-transport-rest-jvm.
Two consequences belong here rather than in a stack trace. On Apple platforms the engine is
NSURLSession, so App Transport Security applies and a plaintext http:// Qdrant is refused by the
platform before Kdrant sees the request. On Linux the engine is Curl, which links against the system
libcurl, present on every mainstream distribution and worth checking in a slim container image.
One more thing this list invites a reader to conclude, and it is worth stating rather than leaving to
be worked out. Kdrant is a client. It talks to a Qdrant over a network, and it never holds an index
itself. The three facts above point the other way: linuxArm64 is a published target, the GraalVM
image answers its first search in tens of milliseconds in 42 MB, and kdrant-cli is a static binary of
5.7 MB on macOS arm64, 6.8 on Windows and 14.1 on Linux x64. Together they say this runs well on small
hardware, which is true, and they can be read as saying it runs there as the vector database, which it
does not.
The line worth drawing is between the gateway and the device. An ARM box, a container or a small appliance that queries a Qdrant running elsewhere is exactly what those three facts are for. A robot, a kiosk or a handset that has to answer with no network is a different architecture, and the answer there is Qdrant Edge, which runs the engine in-process and offline. Reach for that, not for this.
There is no Kotlin/JS target, and that is a decision rather than a gap. A browser cannot reach a Qdrant without CORS on the server, a Qdrant reachable from a browser is reachable from anyone who opens the developer tools, and an API key shipped to a browser is a published key. The answer changes if Qdrant grows a browser-facing authorisation model that keeps the credential off the page, or if someone wants Kdrant on Node.
Qdrant versions
The contract both engines are held to runs against the four most recent Qdrant minors, and the result is published whichever way it goes. It does not all go one way.
| Qdrant | The shared client contract |
|---|---|
v1.19.1 |
46/46 pass |
v1.18.3 |
42/46 pass — a prefix filter is correct with and without the index that serves it fails; four sliced scrolls read every point exactly once, repeatably fails; 4-bit storage and a memory tier per component round-trip fails; a collection's replication and payload placement can change after it exists fails |
v1.17.1 |
41/46 pass — a prefix filter is correct with and without the index that serves it fails; four sliced scrolls read every point exactly once, repeatably fails; 4-bit storage and a memory tier per component round-trip fails; a collection's replication and payload placement can change after it exists fails; the quantization families a collection can be created with are accepted fails |
v1.16.3 |
40/46 pass — a prefix filter is correct with and without the index that serves it fails; relevance feedback reranks the query it was given fails; four sliced scrolls read every point exactly once, repeatably fails; 4-bit storage and a memory tier per component round-trip fails; a collection's replication and payload placement can change after it exists fails; the quantization families a collection can be created with are accepted fails |
Written by QdrantVersionMatrixIntegrationTest from
run 34693421161. Re-measure it by
dispatching the CI workflow, or locally with Docker and
KDRANT_UPDATE_COMPAT=1 ./gradlew :kdrant-transport-rest:jvmTest --rerun --tests '*QdrantVersionMatrix*'.
What the failing cells say. Each one names a feature the server does not have, which is the table doing its job rather than a defect. Four need Qdrant 1.19: prefix matching, sliced scrolls, memory tiers with 4-bit storage, and changing a collection's placement after it exists. The quantization case needs 1.18, because TurboQuant arrived there; product quantization is much older and is not what fails. Relevance feedback needs 1.17.
Everything else passes on all four: the whole of collections, points, search including hybrid fusion, sparse and multi-vectors, scroll, payload and vector management, aliases, snapshots, cluster and sharding. So the answer to "we are on 1.17, can we use this" is yes, without the features 1.17 does not serve.
Usage
Connecting
val qdrant: QdrantClient = Kdrant(host = "localhost", port = 6333) {
apiKey = "..." // sent as the api-key header; omit for a local, unauthenticated node
useTls = true // required when sending a credential over a network
requestTimeout = 10.seconds
}
QdrantClient is AutoCloseable; use it with use { } or close it explicitly.
useTls decides whether to speak HTTPS; trustAnchors decides which certificate to accept. The
default is the platform's own store, which is right for Qdrant Cloud. A private CA, a self-signed
staging node, or certificate pinning are named instead:
val qdrant = Kdrant(host = "qdrant.internal", port = 6333) {
useTls = true
trustAnchors = TrustAnchors.Pem(File("company-ca.pem").readText())
}
Every target honours TrustAnchors.System. The JVM honours all three; Linux honours a PEM bundle;
on iOS, macOS and Windows the trust store belongs to the platform, so a private CA goes into the
keychain or the machine store and Kdrant refuses the configuration rather than falling back to system
trust and looking like it complied. TrustAnchors names the store each engine reads, and says why each
empty cell is empty: Windows has no per-handle root override and never will, Linux cannot pin until
Ktor's Curl engine exposes libcurl's pinning option, and Darwin could take a bundle through a challenge
handler but will not until there is a test proving it rejects a chain the bundle does not anchor.
Collections
qdrant.createCollection("articles") {
vector { size = 1_536; distance = Distance.COSINE }
onDiskPayload = true
}
qdrant.createCollection("multimodal") {
namedVector("text") { size = 768; distance = Distance.COSINE }
namedVector("image") { size = 512; distance = Distance.DOT }
}
Create-if-absent is race-tolerant, and there is a shorthand for the common case and a non-throwing read:
qdrant.createCollectionIfNotExists("articles") { vector { size = 1_536; distance = Distance.COSINE } }
qdrant.createCollection("quickstart", size = 1_536) // single vector, COSINE
val info = qdrant.getCollectionOrNull("articles") // null instead of throwing if absent
Payload indexes
Filtering on an unindexed payload field is a full scan. An index is one call, and the parameters that index type accepts are part of it:
qdrant.createPayloadIndex("articles", "lang") { keyword { isTenant = true; onDisk = true } }
qdrant.createPayloadIndex("articles", "body") { text { tokenizer = Tokenizer.WORD; phraseMatching = true } }
Two of those decide whether a query works rather than how fast it is. A matchPhrase filter matches
nothing unless the text index was built with phraseMatching = true, and a multi-tenant collection is
only laid out per tenant if its tenant field says isTenant = true. onDisk is the difference between
an index that has to fit in RAM and one that does not.
Upserting points
Point ids are unsigned integers or UUID strings. Payloads accept heterogeneous JSON values.
qdrant.upsert("articles", wait = true) {
point(id = 1) {
vector(0.12f, 0.87f, 0.03f /* ... */)
payload("title" to "Intro", "tags" to listOf("nlp", "kotlin"))
}
point(id = "550e8400-e29b-41d4-a716-446655440000") {
vector("text" to textEmbedding, "image" to imageEmbedding)
payload {
put("title", "Cover")
put("score", 0.91)
}
}
}
Large batches are split automatically to stay under Qdrant's request-size limit.
Ingesting more than fits in memory
upsert sends what it is given. ingest owns the parts you would otherwise write yourself: batching
by count and by size, a bound on requests in flight, retry of a batch rather than of the stream, and a
token that says where to carry on from.
var token = loadCheckpoint() // null on a first run
val report = qdrant.ingest("articles", pointsFromDisk, concurrency = 4, resumeFrom = token) {
saveCheckpoint(it) // called as the acknowledged prefix grows
}
The token counts only an unbroken prefix, and it is a lower bound on what the collection holds rather than an equality: a batch cancelled in flight may already have been applied. Resuming therefore re-sends a few points that are already there, which upsert makes free, and can never skip a point the server did not write.
Server-side inference
Where the deployment has an inference provider configured, a point can carry the text and the name of a model instead of a vector, and a query can do the same. The embedding happens in Qdrant.
qdrant.upsert("articles", wait = true) {
point(1) { document("the text to embed", model = "jinaai/jina-embeddings-v2-base-en") }
}
val hits = qdrant.search("articles") {
queryDocument("what to look for", model = "jinaai/jina-embeddings-v2-base-en")
limit = 5
}
A plain Qdrant container has no provider and refuses these requests.
Filters
The filter DSL mirrors Qdrant's filtering model and powers both search and delete-by-filter:
val query = filter {
must {
"lang" eq "en"
"year" gte 2024
"price" between 10.0..99.0
}
should {
matchAny("tag", "featured", "promo")
geoRadius("location", GeoPoint(lon = 13.40, lat = 52.52), radius = 5_000.0)
}
mustNot { "archived" eq true }
}
Supported conditions include exact, any, except and full-text match, numeric and datetime ranges,
values_count, geo bounding-box, radius and polygon, is_empty and is_null, has_id, has_vector,
per-element nested filters, and recursive filter { } sub-groups.
Searching
val hits: List<ScoredPoint> = qdrant.search("articles") {
query(queryVector)
limit = 5
scoreThreshold = 0.75
withPayload = WithPayload.include("title")
filter { must { "lang" eq "en" } }
}
Decode each hit's payload straight into your own type with searchAs, or payloadAs on a single hit:
@Serializable data class Article(val title: String, val lang: String)
val articles: List<Hit<Article>> = qdrant.searchAs<Article>("articles") {
query(queryVector); limit = 5
}
Hybrid search fuses several prefetch sources with Reciprocal Rank Fusion or DBSF:
val hits = qdrant.search("articles") {
prefetch { query(denseVector); using = "dense"; limit = 50 }
prefetch { querySparse(indices, values); using = "keywords"; limit = 50 }
rrf() // Reciprocal Rank Fusion; or dbsf()
limit = 10
}
Declare the two with namedVector(...) and sparseVector("keywords") { modifier = Modifier.IDF } and
that is dense plus keyword hybrid search. You can also query by a stored point's vector
(query(PointId.num(1))), orderBy("field"), sample(), or a multi-vector (queryMulti(...)).
Scrolling, deleting, counting
qdrant.scroll("articles", pageSize = 256) {
filter { must { "lang" eq "en" } }
}.collect { record -> /* ... */ } // a cold Flow, paged transparently
qdrant.delete("articles", ids = listOf(PointId.num(1)))
qdrant.delete("articles") { must { "lang" eq "en" } } // by filter
val total = qdrant.count("articles")
val points = qdrant.retrieve("articles", ids = listOf(PointId.num(1), PointId.num(2)))
Kdrant MCP Fragen
Berechnet Kdrant Embeddings?
Nein, Kdrant enthält kein Modell oder eine Inference-Library; es erwartet, dass du die Embeddings als Liste von Floats bereitstellst.
Welche Transport-Engines sind verfügbar?
Kdrant bietet standardmäßig eine reine Kotlin REST-Engine und optional eine gRPC-Engine für Anforderungen mit hohem Durchsatz.