Migrate to package

This commit is contained in:
2026-03-02 17:44:18 +00:00
parent 3e2386b9b6
commit 8dfd2048a5
13 changed files with 356 additions and 31 deletions

View File

@@ -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
```

View File

@@ -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:

View File

@@ -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"

View File

View File

@@ -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]

View File

@@ -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 {}}})

View File

@@ -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()

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Tests for knowledge-search-mcp."""

36
tests/conftest.py Normal file
View File

@@ -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

56
tests/test_config.py Normal file
View File

@@ -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"

108
tests/test_search.py Normal file
View File

@@ -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"

View File

@@ -1,4 +0,0 @@
from .config import Settings, _args, cfg
from .logging_setup import log_structured_entry
__all__ = ['Settings', '_args', 'cfg', 'log_structured_entry']

57
uv.lock generated
View File

@@ -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"