Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions mcp_plex/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

Comment on lines +1 to +5

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Declare pydantic-settings dependency

The new configuration module now imports pydantic_settings.BaseSettings to define Settings, but pyproject.toml still omits pydantic-settings from the project dependencies (only the version number was bumped). In a fresh installation the library will raise ModuleNotFoundError as soon as mcp_plex.config is imported. Add pydantic-settings to the dependency list so the package can start successfully.

Useful? React with 👍 / 👎.


class Settings(BaseSettings):
"""Application configuration settings."""

qdrant_url: str | None = Field(default=None, env="QDRANT_URL")
qdrant_api_key: str | None = Field(default=None, env="QDRANT_API_KEY")
qdrant_host: str | None = Field(default=None, env="QDRANT_HOST")
qdrant_port: int = Field(default=6333, env="QDRANT_PORT")
qdrant_grpc_port: int = Field(default=6334, env="QDRANT_GRPC_PORT")
qdrant_prefer_grpc: bool = Field(default=False, env="QDRANT_PREFER_GRPC")
qdrant_https: bool | None = Field(default=None, env="QDRANT_HTTPS")
dense_model: str = Field(
default="BAAI/bge-small-en-v1.5", env="DENSE_MODEL"
)
sparse_model: str = Field(
default="Qdrant/bm42-all-minilm-l6-v2-attentions", env="SPARSE_MODEL"
)
cache_size: int = Field(default=128, env="CACHE_SIZE")
use_reranker: bool = Field(default=True, env="USE_RERANKER")

model_config = SettingsConfigDict(case_sensitive=False)
86 changes: 43 additions & 43 deletions mcp_plex/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import asyncio
import inspect
import json
import os
from typing import Annotated, Any, Callable

from fastapi import FastAPI
Expand All @@ -21,56 +20,60 @@
from starlette.responses import JSONResponse, PlainTextResponse, Response

from .cache import MediaCache
from .config import Settings

try:
from sentence_transformers import CrossEncoder
except Exception:
CrossEncoder = None

# Environment configuration for Qdrant
_QDRANT_URL = os.getenv("QDRANT_URL")
_QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
_QDRANT_HOST = os.getenv("QDRANT_HOST")
_QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
_QDRANT_GRPC_PORT = int(os.getenv("QDRANT_GRPC_PORT", "6334"))
_QDRANT_PREFER_GRPC = os.getenv("QDRANT_PREFER_GRPC", "0") == "1"
_https_env = os.getenv("QDRANT_HTTPS")
_QDRANT_HTTPS = None if _https_env is None else _https_env == "1"

# Embedding model configuration
_DENSE_MODEL_NAME = os.getenv("DENSE_MODEL", "BAAI/bge-small-en-v1.5")
_SPARSE_MODEL_NAME = os.getenv(
"SPARSE_MODEL", "Qdrant/bm42-all-minilm-l6-v2-attentions"
)

if _QDRANT_URL is None and _QDRANT_HOST is None:
_QDRANT_URL = ":memory:"


_CACHE_SIZE = 128
settings = Settings()


class PlexServer(FastMCP):
"""FastMCP server with an attached Qdrant client."""

def __init__(self) -> None: # noqa: D401 - short description inherited
super().__init__()
self.qdrant_client = AsyncQdrantClient(
location=_QDRANT_URL,
api_key=_QDRANT_API_KEY,
host=_QDRANT_HOST,
port=_QDRANT_PORT,
grpc_port=_QDRANT_GRPC_PORT,
prefer_grpc=_QDRANT_PREFER_GRPC,
https=_QDRANT_HTTPS,
def __init__(
self,
*,
settings: Settings | None = None,
qdrant_client: AsyncQdrantClient | None = None,
) -> None: # noqa: D401 - short description inherited
self._settings = settings or Settings()
location = self.settings.qdrant_url
host = self.settings.qdrant_host
if location is None and host is None:
location = ":memory:"
self.qdrant_client = qdrant_client or AsyncQdrantClient(
location=location,
api_key=self.settings.qdrant_api_key,
host=host,
port=self.settings.qdrant_port,
grpc_port=self.settings.qdrant_grpc_port,
prefer_grpc=self.settings.qdrant_prefer_grpc,
https=self.settings.qdrant_https,
)

async def _lifespan(app: FastMCP): # noqa: ARG001
yield
await self.close()

super().__init__(lifespan=_lifespan)
self._reranker: CrossEncoder | None = None
self._reranker_loaded = False
self.cache = MediaCache(_CACHE_SIZE)
self.cache = MediaCache(self.settings.cache_size)

async def close(self) -> None:
await self.qdrant_client.close()

@property
def settings(self) -> Settings: # type: ignore[override]
return self._settings

@property
def reranker(self) -> CrossEncoder | None:
if not _USE_RERANKER or CrossEncoder is None:
if not self.settings.use_reranker or CrossEncoder is None:
return None
if not self._reranker_loaded:
try:
Expand All @@ -83,9 +86,7 @@ def reranker(self) -> CrossEncoder | None:
return self._reranker


_USE_RERANKER = os.getenv("USE_RERANKER", "1") == "1"

server = PlexServer()
server = PlexServer(settings=settings)


async def _find_records(identifier: str, limit: int = 5) -> list[models.Record]:
Expand Down Expand Up @@ -181,8 +182,8 @@ async def search_media(
] = 5,
) -> list[dict[str, Any]]:
"""Hybrid similarity search across media items using dense and sparse vectors."""
dense_doc = models.Document(text=query, model=_DENSE_MODEL_NAME)
sparse_doc = models.Document(text=query, model=_SPARSE_MODEL_NAME)
dense_doc = models.Document(text=query, model=server.settings.dense_model)
sparse_doc = models.Document(text=query, model=server.settings.sparse_model)
reranker = server.reranker
candidate_limit = limit * 3 if reranker is not None else limit
prefetch = [
Expand Down Expand Up @@ -618,7 +619,6 @@ async def _rest_resource(request: Request, _uri_template=uri, _resource=resource

def main(argv: list[str] | None = None) -> None:
"""CLI entrypoint for running the MCP server."""
global _DENSE_MODEL_NAME, _SPARSE_MODEL_NAME
parser = argparse.ArgumentParser(description="Run the MCP server")
parser.add_argument("--bind", help="Host address to bind to")
parser.add_argument("--port", type=int, help="Port to listen on")
Expand All @@ -631,12 +631,12 @@ def main(argv: list[str] | None = None) -> None:
parser.add_argument("--mount", help="Mount path for HTTP transports")
parser.add_argument(
"--dense-model",
default=_DENSE_MODEL_NAME,
default=server.settings.dense_model,
help="Dense embedding model name (env: DENSE_MODEL)",
)
parser.add_argument(
"--sparse-model",
default=_SPARSE_MODEL_NAME,
default=server.settings.sparse_model,
help="Sparse embedding model name (env: SPARSE_MODEL)",
)
args = parser.parse_args(argv)
Expand All @@ -653,8 +653,8 @@ def main(argv: list[str] | None = None) -> None:
if args.mount:
run_kwargs["path"] = args.mount

_DENSE_MODEL_NAME = args.dense_model
_SPARSE_MODEL_NAME = args.sparse_model
server.settings.dense_model = args.dense_model
server.settings.sparse_model = args.sparse_model

server.run(transport=args.transport, **run_kwargs)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mcp-plex"
version = "0.26.11"
version = "0.26.12"

description = "Plex-Oriented Model Context Protocol Server"
requires-python = ">=3.11,<3.13"
Expand Down
Loading