Skip to content

Commit 5c68fcc

Browse files
authored
feat: allow configuring qdrant connection (#27)
1 parent 0e1e1b1 commit 5c68fcc

File tree

5 files changed

+171
-6
lines changed

5 files changed

+171
-6
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ The included `docker-compose.yml` launches both Qdrant and the MCP server.
7070
The server will connect to the `qdrant` service at `http://qdrant:6333` and
7171
expose an SSE endpoint at `http://localhost:8000/mcp`.
7272

73+
### Qdrant Configuration
74+
75+
Connection settings can be provided via environment variables:
76+
77+
- `QDRANT_URL` – full URL or SQLite path.
78+
- `QDRANT_HOST`/`QDRANT_PORT` – HTTP host and port.
79+
- `QDRANT_GRPC_PORT` – gRPC port.
80+
- `QDRANT_HTTPS` – set to `1` to enable HTTPS.
81+
- `QDRANT_PREFER_GRPC` – set to `1` to prefer gRPC.
82+
7383
## Development
7484
Run linting and tests through `uv`:
7585
```bash

mcp_plex/loader.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ async def run(
285285
sample_dir: Optional[Path],
286286
qdrant_url: Optional[str],
287287
qdrant_api_key: Optional[str],
288+
qdrant_host: Optional[str] = None,
289+
qdrant_port: int = 6333,
290+
qdrant_grpc_port: int = 6334,
291+
qdrant_https: bool = False,
292+
qdrant_prefer_grpc: bool = False,
288293
) -> None:
289294
"""Core execution logic for the CLI."""
290295

@@ -323,7 +328,17 @@ async def run(
323328
dense_vectors = list(dense_model.embed(texts))
324329
sparse_vectors = list(sparse_model.passage_embed(texts))
325330

326-
client = AsyncQdrantClient(qdrant_url or ":memory:", api_key=qdrant_api_key)
331+
if qdrant_url is None and qdrant_host is None:
332+
qdrant_url = ":memory:"
333+
client = AsyncQdrantClient(
334+
location=qdrant_url,
335+
api_key=qdrant_api_key,
336+
host=qdrant_host,
337+
port=qdrant_port,
338+
grpc_port=qdrant_grpc_port,
339+
https=qdrant_https,
340+
prefer_grpc=qdrant_prefer_grpc,
341+
)
327342
collection_name = "media-items"
328343
vectors_config = {
329344
"dense": models.VectorParams(
@@ -456,6 +471,47 @@ async def run(
456471
required=False,
457472
help="Qdrant API key",
458473
)
474+
@click.option(
475+
"--qdrant-host",
476+
envvar="QDRANT_HOST",
477+
show_envvar=True,
478+
required=False,
479+
help="Qdrant host",
480+
)
481+
@click.option(
482+
"--qdrant-port",
483+
envvar="QDRANT_PORT",
484+
show_envvar=True,
485+
type=int,
486+
default=6333,
487+
show_default=True,
488+
required=False,
489+
help="Qdrant HTTP port",
490+
)
491+
@click.option(
492+
"--qdrant-grpc-port",
493+
envvar="QDRANT_GRPC_PORT",
494+
show_envvar=True,
495+
type=int,
496+
default=6334,
497+
show_default=True,
498+
required=False,
499+
help="Qdrant gRPC port",
500+
)
501+
@click.option(
502+
"--qdrant-https/--no-qdrant-https",
503+
envvar="QDRANT_HTTPS",
504+
show_envvar=True,
505+
default=False,
506+
help="Use HTTPS when connecting to Qdrant",
507+
)
508+
@click.option(
509+
"--qdrant-prefer-grpc/--no-qdrant-prefer-grpc",
510+
envvar="QDRANT_PREFER_GRPC",
511+
show_envvar=True,
512+
default=False,
513+
help="Prefer gRPC when connecting to Qdrant",
514+
)
459515
@click.option(
460516
"--continuous",
461517
is_flag=True,
@@ -479,6 +535,11 @@ def main(
479535
sample_dir: Optional[Path],
480536
qdrant_url: Optional[str],
481537
qdrant_api_key: Optional[str],
538+
qdrant_host: Optional[str],
539+
qdrant_port: int,
540+
qdrant_grpc_port: int,
541+
qdrant_https: bool,
542+
qdrant_prefer_grpc: bool,
482543
continuous: bool,
483544
delay: float,
484545
) -> None:
@@ -492,6 +553,11 @@ def main(
492553
sample_dir,
493554
qdrant_url,
494555
qdrant_api_key,
556+
qdrant_host,
557+
qdrant_port,
558+
qdrant_grpc_port,
559+
qdrant_https,
560+
qdrant_prefer_grpc,
495561
continuous,
496562
delay,
497563
)
@@ -505,6 +571,11 @@ async def load_media(
505571
sample_dir: Optional[Path],
506572
qdrant_url: Optional[str],
507573
qdrant_api_key: Optional[str],
574+
qdrant_host: Optional[str],
575+
qdrant_port: int,
576+
qdrant_grpc_port: int,
577+
qdrant_https: bool,
578+
qdrant_prefer_grpc: bool,
508579
continuous: bool,
509580
delay: float,
510581
) -> None:
@@ -518,6 +589,11 @@ async def load_media(
518589
sample_dir,
519590
qdrant_url,
520591
qdrant_api_key,
592+
qdrant_host,
593+
qdrant_port,
594+
qdrant_grpc_port,
595+
qdrant_https,
596+
qdrant_prefer_grpc,
521597
)
522598
if not continuous:
523599
break

mcp_plex/server.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,28 @@
2525
CrossEncoder = None
2626

2727
# Environment configuration for Qdrant
28-
_QDRANT_URL = os.getenv("QDRANT_URL", ":memory:")
28+
_QDRANT_URL = os.getenv("QDRANT_URL")
2929
_QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
30+
_QDRANT_HOST = os.getenv("QDRANT_HOST")
31+
_QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
32+
_QDRANT_GRPC_PORT = int(os.getenv("QDRANT_GRPC_PORT", "6334"))
33+
_QDRANT_PREFER_GRPC = os.getenv("QDRANT_PREFER_GRPC", "0") == "1"
34+
_https_env = os.getenv("QDRANT_HTTPS")
35+
_QDRANT_HTTPS = None if _https_env is None else _https_env == "1"
36+
37+
if _QDRANT_URL is None and _QDRANT_HOST is None:
38+
_QDRANT_URL = ":memory:"
3039

3140
# Instantiate global client and embedding models
32-
_client = AsyncQdrantClient(_QDRANT_URL, api_key=_QDRANT_API_KEY)
41+
_client = AsyncQdrantClient(
42+
location=_QDRANT_URL,
43+
api_key=_QDRANT_API_KEY,
44+
host=_QDRANT_HOST,
45+
port=_QDRANT_PORT,
46+
grpc_port=_QDRANT_GRPC_PORT,
47+
prefer_grpc=_QDRANT_PREFER_GRPC,
48+
https=_QDRANT_HTTPS,
49+
)
3350
_dense_model = TextEmbedding("BAAI/bge-small-en-v1.5")
3451
_sparse_model = SparseTextEmbedding("Qdrant/bm42-all-minilm-l6-v2-attentions")
3552

tests/test_loader_integration.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ def passage_embed(self, texts):
3939
class DummyQdrantClient:
4040
instance = None
4141

42-
def __init__(self, url: str, api_key: str | None = None):
42+
def __init__(self, url: str | None = None, api_key: str | None = None, **kwargs):
4343
self.collections = {}
4444
self.upserted = []
45+
self.kwargs = kwargs
4546
DummyQdrantClient.instance = self
4647

4748
async def collection_exists(self, name: str) -> bool:
@@ -68,8 +69,8 @@ async def upsert(self, collection_name: str, points):
6869
class TrackingQdrantClient(DummyQdrantClient):
6970
"""Qdrant client that starts with a mismatched collection size."""
7071

71-
def __init__(self, url: str, api_key: str | None = None):
72-
super().__init__(url, api_key)
72+
def __init__(self, url: str | None = None, api_key: str | None = None, **kwargs):
73+
super().__init__(url, api_key, **kwargs)
7374
# Pre-create a collection with the wrong vector size to force recreation
7475
wrong_params = SimpleNamespace(
7576
vectors={
@@ -117,3 +118,38 @@ def test_run_recreates_mismatched_collection(monkeypatch):
117118
client.collections["media-items"].config.params.vectors["dense"].size
118119
== 3
119120
)
121+
122+
123+
def test_run_uses_connection_options(monkeypatch):
124+
monkeypatch.setattr(loader, "TextEmbedding", DummyTextEmbedding)
125+
monkeypatch.setattr(loader, "SparseTextEmbedding", DummySparseEmbedding)
126+
127+
captured = {}
128+
129+
class CaptureClient(DummyQdrantClient):
130+
def __init__(self, url: str | None = None, api_key: str | None = None, **kwargs):
131+
super().__init__(url, api_key, **kwargs)
132+
captured.update(kwargs)
133+
134+
monkeypatch.setattr(loader, "AsyncQdrantClient", CaptureClient)
135+
sample_dir = Path(__file__).resolve().parents[1] / "sample-data"
136+
asyncio.run(
137+
loader.run(
138+
None,
139+
None,
140+
None,
141+
sample_dir,
142+
None,
143+
None,
144+
qdrant_host="example",
145+
qdrant_port=1111,
146+
qdrant_grpc_port=2222,
147+
qdrant_https=True,
148+
qdrant_prefer_grpc=True,
149+
)
150+
)
151+
assert captured["host"] == "example"
152+
assert captured["port"] == 1111
153+
assert captured["grpc_port"] == 2222
154+
assert captured["https"] is True
155+
assert captured["prefer_grpc"] is True

tests/test_server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,32 @@ async def _setup_db(tmp_path: Path) -> str:
144144
return "dummy"
145145

146146

147+
def test_qdrant_env_config(monkeypatch):
148+
from qdrant_client import async_qdrant_client
149+
150+
captured = {}
151+
152+
class CaptureClient:
153+
def __init__(self, *args, **kwargs):
154+
captured.update(kwargs)
155+
156+
monkeypatch.setattr(async_qdrant_client, "AsyncQdrantClient", CaptureClient)
157+
monkeypatch.setenv("QDRANT_HOST", "example.com")
158+
monkeypatch.setenv("QDRANT_PORT", "1234")
159+
monkeypatch.setenv("QDRANT_GRPC_PORT", "5678")
160+
monkeypatch.setenv("QDRANT_PREFER_GRPC", "1")
161+
monkeypatch.setenv("QDRANT_HTTPS", "1")
162+
import importlib
163+
import mcp_plex.server as server
164+
importlib.reload(server)
165+
166+
assert captured["host"] == "example.com"
167+
assert captured["port"] == 1234
168+
assert captured["grpc_port"] == 5678
169+
assert captured["prefer_grpc"] is True
170+
assert captured["https"] is True
171+
172+
147173
def test_server_tools(tmp_path, monkeypatch):
148174
# Patch embeddings and Qdrant client to use dummy implementations
149175
monkeypatch.setattr(loader, "TextEmbedding", DummyTextEmbedding)

0 commit comments

Comments
 (0)