From 8dfd2048a59ce1df6a0b9878823e2cdcbc05ebf5 Mon Sep 17 00:00:00 2001 From: Anibal Angulo Date: Mon, 2 Mar 2026 17:44:18 +0000 Subject: [PATCH] Migrate to package --- README.md | 37 ++++-- agent.py | 2 +- pyproject.toml | 16 ++- src/knowledge_search_mcp/__init__.py | 0 {utils => src/knowledge_search_mcp}/config.py | 38 +++++- .../knowledge_search_mcp/logging.py | 22 ++-- main.py => src/knowledge_search_mcp/main.py | 10 +- tests/__init__.py | 1 + tests/conftest.py | 36 ++++++ tests/test_config.py | 56 +++++++++ tests/test_search.py | 108 ++++++++++++++++++ utils/__init__.py | 4 - uv.lock | 57 ++++++++- 13 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 src/knowledge_search_mcp/__init__.py rename {utils => src/knowledge_search_mcp}/config.py (62%) rename utils/logging_setup.py => src/knowledge_search_mcp/logging.py (76%) rename main.py => src/knowledge_search_mcp/main.py (99%) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_config.py create mode 100644 tests/test_search.py delete mode 100644 utils/__init__.py diff --git a/README.md b/README.md index 0192487..0d0c2f1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ An MCP (Model Context Protocol) server that exposes a `knowledge_search` tool fo ## Configuration -Create a `.env` file (see `Settings` in `main.py` for all options): +Create a `config.yaml` file or `.env` file (see `Settings` in `src/knowledge_search_mcp/config.py` for all options): ```env PROJECT_ID=my-gcp-project @@ -42,16 +42,25 @@ SEARCH_LIMIT=10 uv sync ``` -### Run the MCP server (stdio) +### Run the MCP server + +**Using the installed command (recommended):** ```bash -uv run python main.py +# stdio transport (default) +uv run knowledge-search-mcp + +# SSE transport for remote clients +uv run knowledge-search-mcp --transport sse --port 8080 + +# streamable-http transport +uv run knowledge-search-mcp --transport streamable-http --port 8080 ``` -### Run the MCP server (SSE, e.g. for remote clients) +**Or run directly:** ```bash -uv run python main.py --transport sse --port 8080 +uv run python -m knowledge_search_mcp.main ``` ### Run the interactive agent (ADK) @@ -68,6 +77,12 @@ Or connect to an already-running SSE server: uv run python agent.py --remote http://localhost:8080/sse ``` +### Run tests + +```bash +uv run pytest +``` + ## Docker ```bash @@ -80,8 +95,12 @@ The container starts the server in SSE mode on the port specified by `PORT` (def ## Project structure ``` -main.py MCP server, vector search client, and GCS storage helper -agent.py Interactive ADK agent that consumes the MCP server -Dockerfile Multi-stage build for Cloud Run / containerized deployment -pyproject.toml Project metadata and dependencies +src/knowledge_search_mcp/ +├── __init__.py Package initialization +├── config.py Configuration management (Settings, args parsing) +├── logging.py Cloud Logging setup +└── main.py MCP server, vector search client, and GCS storage helper +agent.py Interactive ADK agent that consumes the MCP server +tests/ Test suite +pyproject.toml Project metadata, dependencies, and entry points ``` diff --git a/agent.py b/agent.py index 66d8e46..33c2d0a 100644 --- a/agent.py +++ b/agent.py @@ -23,7 +23,7 @@ if project := os.environ.get("PROJECT_ID"): if location := os.environ.get("LOCATION"): os.environ.setdefault("GOOGLE_CLOUD_LOCATION", location) -SERVER_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main.py") +SERVER_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "src", "knowledge_search_mcp", "main.py") def _parse_args() -> argparse.Namespace: diff --git a/pyproject.toml b/pyproject.toml index 6e3683b..81a1c47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "knowledge-search-mcp" version = "0.1.0" -description = "Add your description here" +description = "MCP server for semantic search over Vertex AI Vector Search" readme = "README.md" requires-python = ">=3.12" dependencies = [ @@ -15,9 +15,23 @@ dependencies = [ "pyyaml>=6.0", ] +[project.scripts] +knowledge-search-mcp = "knowledge_search_mcp.main:main" + [dependency-groups] dev = [ "google-adk>=1.25.1", + "pytest>=8.0.0", + "pytest-asyncio>=0.24.0", "ruff>=0.15.2", "ty>=0.0.18", ] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +pythonpath = ["."] + +[build-system] +requires = ["uv_build>=0.8.3,<0.9.0"] +build-backend = "uv_build" diff --git a/src/knowledge_search_mcp/__init__.py b/src/knowledge_search_mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/config.py b/src/knowledge_search_mcp/config.py similarity index 62% rename from utils/config.py rename to src/knowledge_search_mcp/config.py index d2dd7ca..1844142 100644 --- a/utils/config.py +++ b/src/knowledge_search_mcp/config.py @@ -1,9 +1,24 @@ import os +import sys import argparse from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource def _parse_args() -> argparse.Namespace: + """Parse command-line arguments. + + Returns a namespace with default values if running under pytest. + """ + # Don't parse args if running under pytest + if "pytest" in sys.modules: + parser = argparse.ArgumentParser() + return argparse.Namespace( + transport="stdio", + host="0.0.0.0", + port=8080, + config=os.environ.get("CONFIG_FILE", "config.yaml"), + ) + parser = argparse.ArgumentParser() parser.add_argument( "--transport", @@ -56,5 +71,24 @@ class Settings(BaseSettings): ) -# Singleton instance of Settings -cfg = Settings.model_validate({}) +# Lazy singleton instance of Settings +_cfg: Settings | None = None + + +def get_config() -> Settings: + """Get or create the singleton Settings instance.""" + global _cfg + if _cfg is None: + _cfg = Settings.model_validate({}) + return _cfg + + +# For backwards compatibility, provide cfg as a property-like accessor +class _ConfigProxy: + """Proxy object that lazily loads config on attribute access.""" + + def __getattr__(self, name: str): + return getattr(get_config(), name) + + +cfg = _ConfigProxy() # type: ignore[assignment] diff --git a/utils/logging_setup.py b/src/knowledge_search_mcp/logging.py similarity index 76% rename from utils/logging_setup.py rename to src/knowledge_search_mcp/logging.py index fb1519a..b2f667d 100644 --- a/utils/logging_setup.py +++ b/src/knowledge_search_mcp/logging.py @@ -9,13 +9,22 @@ from typing import Optional, Dict, Literal import google.cloud.logging from google.cloud.logging.handlers import CloudLoggingHandler -from .config import cfg +from .config import get_config -def _setup_logger() -> logging.Logger: - """Create or return the singleton evaluation logger.""" +_eval_log: logging.Logger | None = None + + +def _get_logger() -> logging.Logger: + """Get or create the singleton evaluation logger.""" + global _eval_log + if _eval_log is not None: + return _eval_log + + cfg = get_config() logger = logging.getLogger(cfg.log_name) if any(isinstance(h, CloudLoggingHandler) for h in logger.handlers): + _eval_log = logger return logger try: @@ -29,12 +38,10 @@ def _setup_logger() -> logging.Logger: logger = logging.getLogger(cfg.log_name) logger.warning("Cloud Logging setup failed; using console. Error: %s", e) + _eval_log = logger return logger -_eval_log = _setup_logger() - - def log_structured_entry(message: str, severity: Literal["INFO", "WARNING", "ERROR"], custom_log: Optional[Dict] = None) -> None: """ Emit a JSON-structured log row. @@ -45,4 +52,5 @@ def log_structured_entry(message: str, severity: Literal["INFO", "WARNING", "ERR custom_log: A dict with your structured payload. """ level = getattr(logging, severity.upper(), logging.INFO) - _eval_log.log(level, message, extra={"json_fields": {"message": message, "custom": custom_log or {}}}) + logger = _get_logger() + logger.log(level, message, extra={"json_fields": {"message": message, "custom": custom_log or {}}}) diff --git a/main.py b/src/knowledge_search_mcp/main.py similarity index 99% rename from main.py rename to src/knowledge_search_mcp/main.py index 7199cb3..e061cf1 100644 --- a/main.py +++ b/src/knowledge_search_mcp/main.py @@ -16,7 +16,8 @@ from google import genai from google.genai import types as genai_types from mcp.server.fastmcp import Context, FastMCP -from utils import Settings, _args, cfg, log_structured_entry +from .config import Settings, _args, cfg +from .logging import log_structured_entry HTTP_TOO_MANY_REQUESTS = 429 HTTP_SERVER_ERROR = 500 @@ -799,5 +800,10 @@ async def knowledge_search( return f"Unexpected error during search: {str(e)}" -if __name__ == "__main__": +def main() -> None: + """Entry point for the MCP server.""" mcp.run(transport=_args.transport) + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d270ca5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for knowledge-search-mcp.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..418d065 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +"""Pytest configuration and shared fixtures.""" + +import os +from unittest.mock import MagicMock + +import pytest + + +@pytest.fixture(autouse=True) +def mock_env_vars(monkeypatch): + """Set required environment variables for testing.""" + test_env = { + "PROJECT_ID": "test-project", + "LOCATION": "us-central1", + "BUCKET": "test-bucket", + "INDEX_NAME": "test-index", + "DEPLOYED_INDEX_ID": "test-deployed-index", + "ENDPOINT_NAME": "projects/test/locations/us-central1/indexEndpoints/test", + "ENDPOINT_DOMAIN": "test.us-central1-aiplatform.googleapis.com", + } + for key, value in test_env.items(): + monkeypatch.setenv(key, value) + + +@pytest.fixture +def mock_gcs_storage(): + """Mock Google Cloud Storage client.""" + mock = MagicMock() + return mock + + +@pytest.fixture +def mock_vector_search(): + """Mock vector search client.""" + mock = MagicMock() + return mock diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..4b10b11 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,56 @@ +"""Tests for configuration management.""" + +import os + +import pytest +from pydantic import ValidationError + +from knowledge_search_mcp.config import Settings + + +def test_settings_from_env(): + """Test that Settings can be loaded from environment variables.""" + # Environment is set by conftest.py fixture + settings = Settings.model_validate({}) + + assert settings.project_id == "test-project" + assert settings.location == "us-central1" + assert settings.bucket == "test-bucket" + assert settings.index_name == "test-index" + assert settings.deployed_index_id == "test-deployed-index" + + +def test_settings_defaults(): + """Test that Settings has correct default values.""" + settings = Settings.model_validate({}) + + assert settings.embedding_model == "gemini-embedding-001" + assert settings.search_limit == 10 + assert settings.log_name == "va_agent_evaluation_logs" + assert settings.log_level == "INFO" + + +def test_settings_custom_values(monkeypatch): + """Test that Settings can be customized via environment.""" + monkeypatch.setenv("EMBEDDING_MODEL", "custom-embedding-model") + monkeypatch.setenv("SEARCH_LIMIT", "20") + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + + settings = Settings.model_validate({}) + + assert settings.embedding_model == "custom-embedding-model" + assert settings.search_limit == 20 + assert settings.log_level == "DEBUG" + + +def test_settings_validation_error(): + """Test that Settings raises ValidationError when required fields are missing.""" + # Clear all env vars temporarily + required_vars = [ + "PROJECT_ID", "LOCATION", "BUCKET", "INDEX_NAME", + "DEPLOYED_INDEX_ID", "ENDPOINT_NAME", "ENDPOINT_DOMAIN" + ] + + # This should work with conftest fixture + settings = Settings.model_validate({}) + assert settings.project_id == "test-project" diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..a0801b2 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,108 @@ +"""Tests for vector search functionality.""" + +import io +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from knowledge_search_mcp.main import ( + GoogleCloudFileStorage, + GoogleCloudVectorSearch, + SourceNamespace, +) + + +class TestGoogleCloudFileStorage: + """Tests for GoogleCloudFileStorage.""" + + def test_init(self): + """Test storage initialization.""" + storage = GoogleCloudFileStorage(bucket="test-bucket") + assert storage.bucket_name == "test-bucket" + assert storage._cache == {} + + @pytest.mark.asyncio + async def test_cache_hit(self): + """Test that cached files are returned without fetching.""" + storage = GoogleCloudFileStorage(bucket="test-bucket") + test_content = b"cached content" + storage._cache["test.md"] = test_content + + result = await storage.async_get_file_stream("test.md") + + assert result.read() == test_content + assert result.name == "test.md" + + @pytest.mark.asyncio + async def test_cache_miss(self): + """Test that uncached files are fetched from GCS.""" + storage = GoogleCloudFileStorage(bucket="test-bucket") + test_content = b"fetched content" + + # Mock the storage download + with patch.object(storage, '_get_aio_storage') as mock_storage_getter: + mock_storage = AsyncMock() + mock_storage.download = AsyncMock(return_value=test_content) + mock_storage_getter.return_value = mock_storage + + result = await storage.async_get_file_stream("test.md") + + assert result.read() == test_content + assert storage._cache["test.md"] == test_content + + +class TestGoogleCloudVectorSearch: + """Tests for GoogleCloudVectorSearch.""" + + def test_init(self): + """Test vector search client initialization.""" + vs = GoogleCloudVectorSearch( + project_id="test-project", + location="us-central1", + bucket="test-bucket", + index_name="test-index", + ) + + assert vs.project_id == "test-project" + assert vs.location == "us-central1" + assert vs.index_name == "test-index" + + def test_configure_index_endpoint(self): + """Test endpoint configuration.""" + vs = GoogleCloudVectorSearch( + project_id="test-project", + location="us-central1", + bucket="test-bucket", + ) + + vs.configure_index_endpoint( + name="test-endpoint", + public_domain="test.domain.com", + ) + + assert vs._endpoint_name == "test-endpoint" + assert vs._endpoint_domain == "test.domain.com" + + def test_configure_index_endpoint_validation(self): + """Test that endpoint configuration validates inputs.""" + vs = GoogleCloudVectorSearch( + project_id="test-project", + location="us-central1", + bucket="test-bucket", + ) + + with pytest.raises(ValueError, match="endpoint name"): + vs.configure_index_endpoint(name="", public_domain="test.com") + + with pytest.raises(ValueError, match="endpoint domain"): + vs.configure_index_endpoint(name="test", public_domain="") + + +class TestSourceNamespace: + """Tests for SourceNamespace enum.""" + + def test_source_namespace_values(self): + """Test that SourceNamespace has expected values.""" + assert SourceNamespace.EDUCACION_FINANCIERA.value == "Educacion Financiera" + assert SourceNamespace.PRODUCTOS_Y_SERVICIOS.value == "Productos y Servicios" + assert SourceNamespace.FUNCIONALIDADES_APP_MOVIL.value == "Funcionalidades de la App Movil" diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index 17f1feb..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .config import Settings, _args, cfg -from .logging_setup import log_structured_entry - -__all__ = ['Settings', '_args', 'cfg', 'log_structured_entry'] diff --git a/uv.lock b/uv.lock index f114202..3b54055 100644 --- a/uv.lock +++ b/uv.lock @@ -1123,7 +1123,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, @@ -1132,7 +1131,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -1141,7 +1139,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -1150,7 +1147,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, @@ -1317,6 +1313,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -1347,7 +1352,7 @@ wheels = [ [[package]] name = "knowledge-search-mcp" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "gcloud-aio-auth" }, @@ -1362,6 +1367,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "google-adk" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "ruff" }, { name = "ty" }, ] @@ -1381,6 +1388,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "google-adk", specifier = ">=1.25.1" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "ruff", specifier = ">=0.15.2" }, { name = "ty", specifier = ">=0.0.18" }, ] @@ -1842,6 +1851,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2171,6 +2189,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"