Migrate to package
This commit is contained in:
37
README.md
37
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
|
||||
```
|
||||
|
||||
2
agent.py
2
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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
0
src/knowledge_search_mcp/__init__.py
Normal file
0
src/knowledge_search_mcp/__init__.py
Normal 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]
|
||||
@@ -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 {}}})
|
||||
@@ -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
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for knowledge-search-mcp."""
|
||||
36
tests/conftest.py
Normal file
36
tests/conftest.py
Normal 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
56
tests/test_config.py
Normal 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
108
tests/test_search.py
Normal 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"
|
||||
@@ -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
57
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user