Kdrant MCP server
2026년 11월 9일 커밋코루틴 우선 API와 타입 안정성이 보장되는 DSL을 제공하는 Qdrant 벡터 데이터베이스용 Kotlin 클라이언트입니다.
다른 이름 NaCode-Studios/Kdrant
- 스타
- 6
- Tools
- —
- 다운로드, 30일
- —
- 최신 버전
- —
- 마지막 커밋
- 2026년 11월 9일
- 라이선스
- Apache-2.0
Kdrant MCP란?
Kdrant은(는) MCP server입니다. 코루틴 우선 API와 타입 안정성이 보장되는 DSL을 제공하는 Qdrant 벡터 데이터베이스용 Kotlin 클라이언트입니다.
프로젝트 README에서 생성된 요약
Kdrant MCP server 정보
벡터 검색 애플리케이션을 구축하는 Kotlin 개발자는 Kdrant를 사용하여 관용적인 코루틴 우선 패턴으로 Qdrant 벡터 데이터베이스와 상호작용할 수 있습니다. 이 클라이언트는 타입 안정성이 보장되는 DSL을 제공하며, 모든 작업에 suspend 함수 또는 Flow를 사용하여 취소 및 타임아웃이 협력적으로 처리되도록 보장합니다. Ktor와 kotlinx-serialization을 통한 기본 REST 엔진을 제공하여 gRPC나 Netty의 무거운 의존성 부담을 피할 수 있도록 가볍게 설계되었습니다.
사용자는 대부분의 워크로드에 적합한 경량 REST 엔진과 높은 처리량이 필요한 경우를 위한 선택 사항인 gRPC 엔진 중 하나를 선택할 수 있습니다. 이 라이브러리는 멀티플랫폼을 지원하여 JVM, iOS, Linux, macOS 등의 타겟을 지원합니다. Kdrant 자체는 임베딩을 계산하지 않지만, 연결된 Qdrant 인스턴스가 추론 제공자(inference provider)로 구성되어 있다면 서버 측 추론을 용이하게 할 수 있습니다. Spring Boot, Spring AI, LangChain4j와 통합할 수 있어 Retrieval-Augmented-Generation 서비스를 구축하는 데 적합합니다.
활용 사례
- 1경량 벡터 검색 클라이언트 구축개발자는 무거운 gRPC 스택 대신 REST 엔진을 사용하여 메모리 점유율이 최소화된 검색 클라이언트를 구현할 수 있습니다.
프로젝트의 README를 바탕으로 작성되었습니다. 모든 활용 사례는 해당 내용의 인용문과 대조하여 확인합니다.
구성
QDRANT_API_KEY
| 이름 | 설명 | 유형 |
|---|---|---|
| QDRANT_API_KEY | — |
자동 검사
2026년 11월 14일에 실행됨전체 보고서 →아직 인증되지 않았습니다. 서버는 80점 이상의 점수, 심각한 결함 없음, 그리고 검사에 충분한 데이터를 갖추어야 합니다. 검사 방식 · 점수를 높이기 위해 수정할 항목
- 소스 저장소 연결 및 접근 가능nacode-studios/kdrant is public and reachablegithub.com/nacode-studios/kdrant ↗통과
- 패키지가 이 저장소를 소스로 명시검사되지 않음
- 빌드 출처(Sigstore 증명)검사되지 않음
- 오픈 소스 라이선스 (OSI)Apache-2.0spdx.org/licenses/Apache-2.0.html ↗통과
- 최근 1년 내 커밋 또는 릴리스Last commit or release 2026-11-09github.com/nacode-studios/kdrant ↗통과
- OpenSSF Scorecard검사되지 않음
- 패키지 이름이 인기 패키지와 유사하지 않음검사되지 않음
- 패키지 및 해당 종속성에 중요한 보안 권고 없음검사되지 않음
- 모델에 대한 숨겨진 지침 없음0 tool descriptions and the README read; no override phrases, credential paths or hidden characters통과
- 파괴적인 도구에 라벨 표시됨검사되지 않음
- 원격 엔드포인트가 MCP 핸드셰이크에 응답검사되지 않음
README
GitHub에서 보기Kdrant
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 질문
Kdrant가 임베딩을 계산하나요?
아니요, Kdrant는 모델이나 추론 라이브러리를 포함하지 않습니다. 사용자가 float 리스트 형태로 임베딩을 직접 제공해야 합니다.
사용 가능한 전송 엔진은 무엇인가요?
Kdrant는 기본적으로 순수 Kotlin 기반의 REST 엔진을 제공하며, 높은 처리량이 필요한 경우를 위해 선택 사항으로 gRPC 엔진을 제공합니다.