Skip to main content
Glama

Chroma MCP Server

by djm81
test_collection_tools.py65 kB
"""Tests for collection management tools.""" import pytest import uuid import json from typing import Dict, Any, List, Optional from unittest.mock import patch, MagicMock, AsyncMock, ANY, call from contextlib import contextmanager from mcp import types from mcp.shared.exceptions import McpError from mcp.types import INVALID_PARAMS, INTERNAL_ERROR, ErrorData # Import specific errors if needed, or rely on ValidationError/Exception from src.chroma_mcp.utils.errors import ValidationError # --- Add direct import for setting global config --- from src.chroma_mcp import utils as chroma_utils # --- Add import for reset_client --- from src.chroma_mcp.utils import chroma_client as client_utils from src.chroma_mcp.tools.collection_tools import ( _reconstruct_metadata, # Keep helper if used _create_collection_impl, _create_collection_with_metadata_impl, _list_collections_impl, _get_collection_impl, _rename_collection_impl, _delete_collection_impl, _peek_collection_impl, ) # Import Pydantic models used by the tools from src.chroma_mcp.tools.collection_tools import ( CreateCollectionInput, CreateCollectionWithMetadataInput, ListCollectionsInput, GetCollectionInput, RenameCollectionInput, DeleteCollectionInput, PeekCollectionInput, ) # Correct import for get_collection_settings from src.chroma_mcp.utils.config import get_collection_settings from src.chroma_mcp.types import ChromaClientConfig # Add import for direct patching from src.chroma_mcp.tools import collection_tools DEFAULT_SIMILARITY_THRESHOLD = 0.7 # Mock embedding function classes used in chroma_client class MockEmbeddingFunction: def __init__(self, name="default"): # Add name for identification self.name = name def __call__(self, input: List[str]): # Simple mock implementation return [[0.1] * 10 for _ in input] # Use 'input' here too # --- Fixtures --- @pytest.fixture(autouse=True) def mock_logger(): """Auto-mock the logger.""" with patch("src.chroma_mcp.tools.collection_tools.get_logger") as mock_get_logger: mock_log_instance = MagicMock() mock_get_logger.return_value = mock_log_instance yield mock_log_instance @pytest.fixture def mock_chroma_client(): """Mock the ChromaDB client instance.""" mock_client = MagicMock() # Mock the create_collection method specifically mock_client.create_collection = MagicMock() # Mock get_collection, list_collections etc. as needed for other tests mock_client.get_collection = MagicMock() mock_client.list_collections = MagicMock(return_value=[]) mock_client.delete_collection = MagicMock() with patch("chroma_mcp.tools.collection_tools.get_chroma_client", return_value=mock_client): yield mock_client @pytest.fixture def mock_embedding_functions(): """Mock the get_embedding_function lookup.""" mock_functions = { "default": MockEmbeddingFunction(name="default"), "fast": MockEmbeddingFunction(name="fast"), "accurate": MockEmbeddingFunction(name="accurate"), "openai": MockEmbeddingFunction(name="openai"), } def mock_lookup(name): func = mock_functions.get(name.lower()) if func is None: raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown embedding function: {name}")) return func # Patch where get_embedding_function is looked up in the tools module with patch("src.chroma_mcp.tools.collection_tools.get_embedding_function", side_effect=mock_lookup) as mock_getter: yield mock_getter # Yield the mock getter itself for assertions @pytest.fixture def mock_server_config(): """Mock the server configuration retrieval.""" # Make this instance unique per test potentially mock_config = ChromaClientConfig(client_type="ephemeral", embedding_function_name="default") original_config = chroma_utils._global_client_config chroma_utils._global_client_config = mock_config try: yield mock_config # Yield the config object to modify it in tests finally: # Ensure global config is reset after test chroma_utils._global_client_config = original_config @pytest.fixture def mock_collection_settings(): """Mock retrieval of default collection settings.""" default_settings = {"hnsw:space": "cosine"} with patch("chroma_mcp.tools.collection_tools.get_collection_settings", return_value=default_settings): yield default_settings @pytest.fixture def mock_validate_name(): """Mock collection name validation.""" with patch("chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate: yield mock_validate # --- Helper Functions --- def assert_successful_json_result( result: List[types.TextContent], expected_data: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Asserts the tool result is a successful list containing valid JSON, returning the parsed data.""" assert isinstance(result, list) assert len(result) > 0, "Result list cannot be empty for successful JSON result." content_item = result[0] assert isinstance(content_item, types.TextContent), f"Expected TextContent, got {type(content_item)}" assert content_item.type == "text", f"Expected content type 'text', got '{content_item.type}'" assert content_item.text is not None, "Text content cannot be None for JSON result." try: parsed_data = json.loads(content_item.text) assert isinstance(parsed_data, dict), f"Parsed JSON is not a dictionary, got {type(parsed_data)}" except (json.JSONDecodeError, AssertionError) as e: pytest.fail(f"Result content is not valid JSON: {e}\nContent: {content_item.text}") if expected_data is not None: assert parsed_data == expected_data, f"Parsed JSON data mismatch. Expected: {expected_data}, Got: {parsed_data}" return parsed_data # Rework assert_error_result as a context manager for future use @contextmanager def assert_raises_mcp_error(expected_message: str): """Asserts that McpError is raised and optionally checks the error message.""" try: yield except McpError as e: # Check both e.error_data (if exists) and e.args[0] message = "" if hasattr(e, "error_data") and hasattr(e.error_data, "message"): message = str(e.error_data.message) elif e.args and isinstance(e.args[0], ErrorData) and hasattr(e.args[0], "message"): message = str(e.args[0].message) else: message = str(e) # Fallback to the exception string itself assert ( expected_message in message ), f"Expected error message containing '{expected_message}' but got '{message}'" return # Apply usefixtures to the whole class again # Apply the new reset fixture as well # Restore mock_server_config to class level @pytest.mark.usefixtures("mock_embedding_functions", "mock_server_config") class TestCollectionTools: """Test cases for collection management tools.""" # --- _create_collection_impl Tests --- @pytest.mark.asyncio # REMOVE fixtures, use local patching async def test_create_collection_uses_default_ef_from_config(self): """Test that _create_collection_impl uses the EF name from server config.""" collection_name = "test-default-ef" # --- Mocks --- # mock_collection = MagicMock() mock_collection.name = collection_name mock_collection.id = "uuid-123" default_settings = get_collection_settings() # Get defaults stored_metadata = {f"chroma:setting:{k.replace(':', '_')}": v for k, v in default_settings.items()} mock_collection.metadata = stored_metadata mock_collection.count.return_value = 0 mock_client_instance = MagicMock() mock_client_instance.create_collection.return_value = mock_collection mock_ef_instance = MockEmbeddingFunction(name="default") # --- Patching --- # with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=mock_client_instance ) as mock_get_client, patch( "src.chroma_mcp.tools.collection_tools.get_embedding_function", return_value=mock_ef_instance ) as mock_get_ef, patch( "src.chroma_mcp.tools.collection_tools.get_collection_settings", return_value=default_settings ) as mock_get_settings, ): # --- Act --- # input_data = CreateCollectionInput(collection_name=collection_name) result = await _create_collection_impl(input_data) # --- Assert --- # mock_validate_name.assert_called_once_with(collection_name) mock_get_client.assert_called_once() mock_get_ef.assert_called_once_with("default") # Assumes default config mock_get_settings.assert_called_once() # Ensure settings were fetched expected_metadata_passed = {f"chroma:setting:{k.replace(':', '_')}": v for k, v in default_settings.items()} mock_client_instance.create_collection.assert_called_once_with( name=collection_name, metadata=expected_metadata_passed, embedding_function=mock_ef_instance, get_or_create=False, ) assert len(result) == 1 assert result[0].type == "text" # Add assertions on result content if necessary @pytest.mark.asyncio # REMOVE fixtures, use local patching async def test_create_collection_falls_back_to_default_ef(self): """Test _create_collection_impl uses 'default' EF name (implicitly).""" collection_name = "test-fallback-ef" # --- Mocks --- # mock_collection = MagicMock() mock_collection.name = collection_name mock_collection.id = "uuid-123" default_settings = get_collection_settings() # Get defaults stored_metadata = {f"chroma:setting:{k.replace(':', '_')}": v for k, v in default_settings.items()} mock_collection.metadata = stored_metadata mock_collection.count.return_value = 0 mock_client_instance = MagicMock() mock_client_instance.create_collection.return_value = mock_collection mock_ef_instance = MockEmbeddingFunction(name="default") # --- Patching --- # with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=mock_client_instance ) as mock_get_client, patch( "src.chroma_mcp.tools.collection_tools.get_embedding_function", return_value=mock_ef_instance ) as mock_get_ef, patch( "src.chroma_mcp.tools.collection_tools.get_collection_settings", return_value=default_settings ) as mock_get_settings, ): # --- Act --- # input_data = CreateCollectionInput(collection_name=collection_name) await _create_collection_impl(input_data) # --- Assert --- # mock_validate_name.assert_called_once_with(collection_name) mock_get_client.assert_called_once() mock_get_ef.assert_called_once_with("default") # Main assertion: default was used mock_get_settings.assert_called_once() expected_metadata_passed = {f"chroma:setting:{k.replace(':', '_')}": v for k, v in default_settings.items()} mock_client_instance.create_collection.assert_called_once_with( name=collection_name, metadata=expected_metadata_passed, embedding_function=mock_ef_instance, get_or_create=False, ) @pytest.mark.asyncio # REMOVE fixtures, use local patching async def test_create_collection_success(self): """Test successful collection creation.""" collection_name = "test_create_new" mock_collection_id = str(uuid.uuid4()) # --- Mocks --- # mock_collection = MagicMock() mock_collection.name = collection_name mock_collection.id = mock_collection_id default_settings = get_collection_settings() # Get defaults metadata_stored_by_chroma = {f"chroma:setting:{k.replace(':', '_')}": v for k, v in default_settings.items()} mock_collection.metadata = metadata_stored_by_chroma mock_collection.count.return_value = 0 mock_client_instance = MagicMock() mock_client_instance.create_collection.return_value = mock_collection mock_ef_instance = MockEmbeddingFunction(name="default") # --- Patching --- # with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=mock_client_instance ) as mock_get_client, patch( "src.chroma_mcp.tools.collection_tools.get_embedding_function", return_value=mock_ef_instance ) as mock_get_ef, patch( "src.chroma_mcp.tools.collection_tools.get_collection_settings", return_value=default_settings ) as mock_get_settings, ): # --- Act --- # input_model = CreateCollectionInput(collection_name=collection_name) result_list = await _create_collection_impl(input_model) # --- Assert --- # mock_validate_name.assert_called_once_with(collection_name) mock_get_client.assert_called_once() mock_get_ef.assert_called_once_with("default") mock_get_settings.assert_called_once() mock_client_instance.create_collection.assert_called_once() call_args, call_kwargs = mock_client_instance.create_collection.call_args assert call_kwargs["name"] == collection_name assert call_kwargs["metadata"] == metadata_stored_by_chroma assert call_kwargs["get_or_create"] is False assert call_kwargs["embedding_function"] is mock_ef_instance result_data = assert_successful_json_result(result_list) assert result_data.get("name") == collection_name assert result_data.get("id") == mock_collection_id assert "metadata" in result_data assert "settings" in result_data["metadata"] expected_settings_with_colons = {k.replace("_", ":"): v for k, v in default_settings.items()} assert result_data["metadata"]["settings"] == expected_settings_with_colons assert result_data.get("count") == 0 @pytest.mark.asyncio # REMOVE mock_chroma_client fixture # No fixtures needed beyond class level async def test_create_collection_invalid_name(self): """Test collection name validation failure within the implementation.""" invalid_name = "invalid-" error_msg = "Invalid collection name" # Patch validator and client method (to check not called) with ( patch( "src.chroma_mcp.tools.collection_tools.validate_collection_name", side_effect=ValidationError(error_msg) ) as mock_validate_name, patch("chromadb.api.client.Client.create_collection") as mock_create_collection, ): # --- Act & Assert --- input_model = CreateCollectionInput(collection_name=invalid_name) with assert_raises_mcp_error(f"Validation Error: {error_msg}"): await _create_collection_impl(input_model) mock_validate_name.assert_called_once_with(invalid_name) mock_create_collection.assert_not_called() # --- _peek_collection_impl Tests --- @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_success(self): """Test successful peeking into a collection with a specific limit.""" collection_name = "test_peek_exists" limit = 3 expected_peek_result = { "ids": ["id1", "id2", "id3"], "documents": ["doc1", "doc2", "doc3"], "metadatas": [{"m": 1}, {"m": 2}, {"m": 3}], "embeddings": None, } # Local Mocks local_mock_collection = MagicMock() local_mock_collection.peek.return_value = expected_peek_result local_mock_client = MagicMock() local_mock_client.get_collection.return_value = local_mock_collection # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = PeekCollectionInput(collection_name=collection_name, limit=limit) result = await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) local_mock_collection.peek.assert_called_once_with(limit=limit) assert_successful_json_result(result, expected_peek_result) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_success_default_limit(self): """Test successful peeking using the default limit (10).""" collection_name = "test_peek_default" expected_peek_result = {"ids": ["default_id"]} # Local Mocks local_mock_collection = MagicMock() local_mock_collection.peek.return_value = expected_peek_result local_mock_client = MagicMock() local_mock_client.get_collection.return_value = local_mock_collection # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = PeekCollectionInput(collection_name=collection_name) # Use default limit result = await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) local_mock_collection.peek.assert_called_once_with(limit=10) # Check default limit value assert_successful_json_result(result, expected_peek_result) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_validation_error(self): """Test collection name validation failure for peek.""" invalid_name = "peek-invalid--" error_msg = "Bad name for peek" # Patch locally (only validator needed, client shouldn't be called) with ( patch( "src.chroma_mcp.tools.collection_tools.validate_collection_name", side_effect=ValidationError(error_msg) ) as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): input_model = PeekCollectionInput(collection_name=invalid_name) # Expect ValidationError message format with assert_raises_mcp_error(f"Validation Error: {error_msg}"): await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(invalid_name) mock_get_client.assert_not_called() # Client should not be fetched if validation fails @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_not_found_error(self): """Test peek when the collection is not found (error from get_collection).""" collection_name = "peek_not_found" error_msg = f"Collection {collection_name} does not exist." # Local mock client configured to raise error local_mock_client = MagicMock() local_mock_client.get_collection.side_effect = ValueError(error_msg) # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = PeekCollectionInput(collection_name=collection_name) # Expect the specific 'not found' message from the ValueError handler with assert_raises_mcp_error(f"Tool Error: Collection '{collection_name}' not found."): await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_get_other_value_error(self): """Test other ValueError from get_collection during peek.""" collection_name = "peek_get_val_err" error_msg = "Get value error during peek" # Local mock client configured to raise error local_mock_client = MagicMock() local_mock_client.get_collection.side_effect = ValueError(error_msg) # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = PeekCollectionInput(collection_name=collection_name) # Expect the specific 'Problem accessing collection' message from the ValueError handler with assert_raises_mcp_error( f"Tool Error: Problem accessing collection '{collection_name}'. Details: {error_msg}" ): await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_peek_collection_peek_exception(self): """Test handling of exception during the actual collection.peek call.""" collection_name = "peek_itself_fails" error_msg = "Peek failed internally" # Local Mocks local_mock_collection = MagicMock() local_mock_collection.peek.side_effect = Exception(error_msg) local_mock_client = MagicMock() local_mock_client.get_collection.return_value = local_mock_collection # Patch Locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = PeekCollectionInput(collection_name=collection_name) # Expect the generic exception handler's message format expected_error_msg = ( f"Tool Error: An unexpected error occurred peeking collection '{collection_name}'. Details: {error_msg}" ) with assert_raises_mcp_error(expected_error_msg): await _peek_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) # Check default limit for PeekCollectionInput is 10 local_mock_collection.peek.assert_called_once_with(limit=10) # --- _create_collection_with_metadata_impl Tests --- @pytest.mark.asyncio # Remove all fixtures except reset_chroma_client_cache (autouse=True) @pytest.mark.usefixtures() async def test_create_collection_with_metadata_success(self): # No fixture arguments needed """Test successful creation using the _with_metadata variant (as JSON string).""" collection_name = "test_create_with_meta_success" custom_metadata_dict = {"user_key": "user_value"} custom_metadata_json = json.dumps(custom_metadata_dict) mock_collection_id = str(uuid.uuid4()) # --- Setup Local Mocks --- # Mock for the collection object that create_collection will return created_collection_mock = MagicMock() created_collection_mock.name = collection_name created_collection_mock.id = mock_collection_id created_collection_mock.metadata = custom_metadata_dict created_collection_mock.count.return_value = 0 # Local mock for the Chroma client instance local_mock_client = MagicMock() local_mock_client.create_collection = MagicMock(return_value=created_collection_mock) # Define dummy EF class and instance class MockEF: def __call__(self, input): return [[0.0] * 10 for _ in input] local_mock_ef_instance = MockEF() # Local mock for the logger instance local_mock_logger_instance = MagicMock() input_model = CreateCollectionWithMetadataInput(collection_name=collection_name, metadata=custom_metadata_json) # --- Patch all dependencies locally --- with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as local_mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_embedding_function", return_value=local_mock_ef_instance ) as local_mock_get_ef, patch( "src.chroma_mcp.tools.collection_tools.get_logger", return_value=local_mock_logger_instance ) as mock_get_logger_call, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as local_mock_get_client, ): # Note: We still rely on get_server_config implicitly inside the code for the default EF name # --- Act --- (Call the implementation) result_list = await _create_collection_with_metadata_impl(input_model) # --- Assert Internal Calls --- local_mock_validate.assert_called_once_with(collection_name) local_mock_get_ef.assert_called_once_with("default") # Assuming default EF name mock_get_logger_call.assert_called_once() local_mock_get_client.assert_called_once() # Ensure client getter was called # Assert Logger Call (should pass) expected_log_message = f"Attempting to create collection '{collection_name}' with provided parsed metadata: {custom_metadata_dict}" local_mock_logger_instance.info.assert_any_call(expected_log_message) assert local_mock_logger_instance.error.call_count == 0 # --- Assert Actual Client Call on the LOCAL mock --- local_mock_client.create_collection.assert_called_once() # Check args passed to the local client mock call_args = local_mock_client.create_collection.call_args assert call_args.kwargs["name"] == collection_name assert call_args.kwargs["metadata"] == custom_metadata_dict assert call_args.kwargs["embedding_function"] is local_mock_ef_instance # --- Assert Result --- result_data = assert_successful_json_result(result_list) assert result_data.get("name") == collection_name assert result_data.get("id") == mock_collection_id @pytest.mark.asyncio # Patch create_collection via decorator, use local patch for validator @patch("chromadb.api.client.Client.create_collection") @pytest.mark.usefixtures("mock_chroma_client", "mock_server_config", "mock_embedding_functions") async def test_create_collection_with_metadata_validation_error( self, mock_create_collection, mock_chroma_client, mock_server_config, mock_embedding_functions ): """Test create with metadata validation error using local patch for validator.""" invalid_name = "meta-invalid--" error_msg = "Bad name from local patch" valid_metadata_json = '{"key": "value"}' # Patch the validator locally where it's imported in the implementation module with patch( "src.chroma_mcp.tools.collection_tools.validate_collection_name", side_effect=ValidationError(error_msg) ) as local_mock_validate: input_model = CreateCollectionWithMetadataInput(collection_name=invalid_name, metadata=valid_metadata_json) with assert_raises_mcp_error(f"Validation Error: {error_msg}"): await _create_collection_with_metadata_impl(input_model) local_mock_validate.assert_called_once_with(invalid_name) mock_create_collection.assert_not_called() # Client method should not be called @pytest.mark.asyncio async def test_create_collection_with_metadata_invalid_json(self): """Test create with metadata when the metadata string is invalid JSON.""" collection_name = "valid_json_test_coll" invalid_json_string = '{"key": "value", "unterminated' expected_error_substring = "Invalid JSON format for metadata field" # More general check # Patch only the client method to check it's not called with patch("chromadb.api.client.Client.create_collection") as mock_create_collection: input_model = CreateCollectionWithMetadataInput( collection_name=collection_name, metadata=invalid_json_string ) with assert_raises_mcp_error(expected_error_substring): await _create_collection_with_metadata_impl(input_model) mock_create_collection.assert_not_called() @pytest.mark.asyncio async def test_create_collection_with_metadata_already_exists(self): """Test create with metadata when collection already exists (ValueError from client).""" collection_name = "meta_exists_test" error_msg = f"Collection {collection_name} already exists." valid_metadata_json = '{"key": "value"}' # Patch validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): # Configure the mock client returned by get_chroma_client mock_client_instance = MagicMock() # Set the side effect directly on the mock client's method mock_client_instance.create_collection.side_effect = ValueError(error_msg) mock_get_client.return_value = mock_client_instance input_model = CreateCollectionWithMetadataInput( collection_name=collection_name, metadata=valid_metadata_json ) # Check for the specific error message wrapped by the implementation with assert_raises_mcp_error(f"Tool Error: Collection '{collection_name}' already exists."): await _create_collection_with_metadata_impl(input_model) mock_validate_name.assert_called_once_with(collection_name) # Assert the create_collection method was called on the mock client instance mock_client_instance.create_collection.assert_called_once() @pytest.mark.asyncio async def test_create_collection_with_metadata_unexpected_error(self): """Test create with metadata handling unexpected errors from client.""" collection_name = "meta_unexpected_err_test" error_msg = "Something else failed badly" valid_metadata_json = '{"key": "value"}' # Patch validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): # Configure the mock client returned by get_chroma_client mock_client_instance = MagicMock() # Set the side effect directly on the mock client's method mock_client_instance.create_collection.side_effect = Exception(error_msg) mock_get_client.return_value = mock_client_instance input_model = CreateCollectionWithMetadataInput( collection_name=collection_name, metadata=valid_metadata_json ) # Check for the generic error message wrapped by the implementation with assert_raises_mcp_error( f"Tool Error: An unexpected error occurred while creating collection '{collection_name}'. Details: {error_msg}" ): await _create_collection_with_metadata_impl(input_model) mock_validate_name.assert_called_once_with(collection_name) # Assert the create_collection method was called on the mock client instance mock_client_instance.create_collection.assert_called_once() # --- _list_collections_impl Tests --- @pytest.mark.asyncio # REMOVE Fixture - Use local patching async def test_list_collections_success_defaults(self): """Test successful default collection listing (no filters, no pagination).""" all_names = ["coll_a", "coll_b"] # Local Mock local_mock_client = MagicMock() local_mock_client.list_collections.return_value = all_names # Patch locally with patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client: input_model = ListCollectionsInput() result_list = await _list_collections_impl(input_model) # Assertions mock_get_client.assert_called_once() local_mock_client.list_collections.assert_called_once() local_mock_client.get_collection.assert_not_called() # Still shouldn't be called result_data = assert_successful_json_result(result_list) assert result_data.get("collection_names") == all_names assert result_data.get("total_count") == len(all_names) assert result_data.get("limit") == 0 assert result_data.get("offset") == 0 @pytest.mark.asyncio # REMOVE Fixture - Use local patching async def test_list_collections_with_filter_pagination(self): """Test listing with name filter and pagination.""" all_names = ["apple", "banana", "apricot", "avocado"] # Local Mock local_mock_client = MagicMock() local_mock_client.list_collections.return_value = all_names # Patch locally with patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client: input_model = ListCollectionsInput(limit=1, offset=1, name_contains="ap") result_list = await _list_collections_impl(input_model) # Assertions mock_get_client.assert_called_once() local_mock_client.list_collections.assert_called_once() local_mock_client.get_collection.assert_not_called() result_data = assert_successful_json_result(result_list) assert result_data.get("collection_names") == ["apricot"] assert result_data.get("total_count") == 2 assert result_data.get("limit") == 1 assert result_data.get("offset") == 1 @pytest.mark.skip( reason="got empty parameter set ['limit', 'offset', 'expected_error_msg'], function test_list_collections_validation_error at /Users/dominikus/git/nold-ai/chroma_mcp_server/tests/tools/test_collection_tools.py:745" ) @pytest.mark.asyncio @pytest.mark.parametrize( "limit, offset, expected_error_msg", [ # (-1, 0, "limit cannot be negative"), # Pydantic handles this # (0, -1, "offset cannot be negative"), # Pydantic handles this ], ids=[ # "negative_limit", # Removed as Pydantic validation # "negative_offset", # Removed as Pydantic validation ], ) # Remove fixture use here, test only Pydantic validation async def test_list_collections_validation_error( self, limit, offset, expected_error_msg # Remove mock_chroma_client_collections ): """Test input validation for limit and offset in list_collections.""" # This test correctly uses Pydantic's validation, but we need one for client errors # No specific validation error raised IN the function, Pydantic handles it. # Instead, we add a test for client-side errors. # REMOVED the original test body as Pydantic handles this. # The assert_raises_mcp_error needs to be outside the await for Pydantic validation with pytest.raises(Exception): # Pydantic raises its own ValidationError _ = ListCollectionsInput(limit=limit, offset=offset) # Keep the assertion structure for documentation, though it might not be hit # with assert_raises_mcp_error(expected_error_msg): # input_model = ListCollectionsInput(limit=limit, offset=offset) # _ = await _list_collections_impl(input_model) @pytest.mark.asyncio # Patch the actual client method to raise error async def test_list_collections_client_error(self): """Test handling of errors during client.list_collections.""" error_msg = "Client connection failed" # Patch list_collections on the *mock client* returned by get_chroma_client with patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client: # Configure the mock client mock_client_instance = MagicMock() mock_client_instance.list_collections.side_effect = Exception(error_msg) mock_get_client.return_value = mock_client_instance input_model = ListCollectionsInput() with assert_raises_mcp_error(f"Tool Error: Error listing collections. Details: {error_msg}"): await _list_collections_impl(input_model) # Assert the method was called on the mock client mock_client_instance.list_collections.assert_called_once() # --- _get_collection_impl Tests --- @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_success(self): """Test getting existing collection info.""" collection_name = "my_coll" mock_collection_id = "test-id-123" mock_metadata_stored = {"description": "test desc", "chroma:setting:hnsw_space": "l2"} mock_count = 42 # Ensure embeddings are None in the peek result if not expected mock_peek = {"ids": ["p1"], "documents": ["peek doc"], "embeddings": None} # Local Mocks local_mock_collection = MagicMock() local_mock_collection.name = collection_name local_mock_collection.id = mock_collection_id local_mock_collection.metadata = mock_metadata_stored local_mock_collection.count.return_value = mock_count local_mock_collection.peek.return_value = mock_peek local_mock_client = MagicMock() local_mock_client.get_collection.return_value = local_mock_collection # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) result = await _get_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) local_mock_collection.count.assert_called_once() local_mock_collection.peek.assert_called_once_with(limit=5) result_data = assert_successful_json_result(result) assert result_data.get("name") == collection_name assert result_data.get("id") == mock_collection_id assert result_data.get("count") == mock_count assert result_data.get("metadata") == _reconstruct_metadata(mock_metadata_stored) assert result_data.get("sample_entries") == mock_peek @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_not_found(self): """Test getting a non-existent collection (handled in impl).""" collection_name = "not_found_coll" error_message = f"Collection {collection_name} does not exist." # Local Mock Client local_mock_client = MagicMock() # Configure get_collection to raise the specific not found error local_mock_client.get_collection.side_effect = ValueError(error_message) # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) # Expect the specific error message from the ValueError handler with assert_raises_mcp_error(f"Tool Error: Collection '{collection_name}' not found."): await _get_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_unexpected_error(self): """Test handling of unexpected error during get collection.""" collection_name = "test_get_fail" error_msg = "Get failed unexpectedly" # Local Mock Client local_mock_client = MagicMock() # Configure get_collection to raise a generic Exception local_mock_client.get_collection.side_effect = Exception(error_msg) # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) # Expect the generic error message from the final except block expected_error = f"Tool Error: An unexpected error occurred while getting collection '{collection_name}'. Details: {error_msg}" with assert_raises_mcp_error(expected_error): await _get_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_validation_error(self): """Test handling of validation error for collection name.""" collection_name = "invalid--name" error_msg = "Invalid name format" # Patch locally (validator raises, client getter shouldn't be called) with ( patch( "src.chroma_mcp.tools.collection_tools.validate_collection_name", side_effect=ValidationError(error_msg) ) as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) # Expect the specific Validation Error message with assert_raises_mcp_error(f"Validation Error: {error_msg}"): await _get_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_not_called() # get_chroma_client should not be called @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_other_value_error(self): """Test handling of other ValueErrors from get_collection.""" collection_name = "test_get_value_err" error_msg = "Another value error during get" # Local Mock Client local_mock_client = MagicMock() # Configure get_collection to raise a different ValueError local_mock_client.get_collection.side_effect = ValueError(error_msg) # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) # Expect the specific 'Invalid parameter' message from the ValueError handler expected_error = f"Tool Error: Invalid parameter getting collection. Details: {error_msg}" with assert_raises_mcp_error(expected_error): await _get_collection_impl(input_model) # Assertions mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE Fixtures - Use local patching async def test_get_collection_peek_error(self): """Test that get_collection returns info even if peek fails.""" collection_name = "test_peek_fail" mock_collection_id = "peek-fail-id" mock_metadata_stored = {"desc": "test"} mock_count = 5 peek_error_msg = "Peek failed" # Local Mocks local_mock_collection = MagicMock() local_mock_collection.name = collection_name local_mock_collection.id = mock_collection_id local_mock_collection.metadata = mock_metadata_stored local_mock_collection.count.return_value = mock_count # Configure peek to raise an error local_mock_collection.peek.side_effect = Exception(peek_error_msg) local_mock_client = MagicMock() # Configure get_collection to succeed and return the collection mock local_mock_client.get_collection.return_value = local_mock_collection # Patch locally with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=local_mock_client ) as mock_get_client, ): input_model = GetCollectionInput(collection_name=collection_name) result_list = await _get_collection_impl(input_model) # Assertions on internal calls mock_validate.assert_called_once_with(collection_name) mock_get_client.assert_called_once() local_mock_client.get_collection.assert_called_once_with(name=collection_name) local_mock_collection.count.assert_called_once() local_mock_collection.peek.assert_called_once_with(limit=5) # Assert success, but check the sample_entries field for the error result_data = assert_successful_json_result(result_list) assert result_data["name"] == collection_name assert result_data["count"] == mock_count assert "sample_entries" in result_data assert isinstance(result_data["sample_entries"], dict) assert "error" in result_data["sample_entries"] assert peek_error_msg in result_data["sample_entries"]["error"] assert result_data.get("id") == mock_collection_id # Add check for ID assert result_data.get("metadata") == _reconstruct_metadata(mock_metadata_stored) # Add check for metadata # --- _rename_collection_impl Tests --- @pytest.mark.asyncio # Remove fixture, patch manually inside # @pytest.mark.usefixtures("mock_validate_name") async def test_rename_collection_success(self): """Test successful collection renaming.""" original_name = "rename_me" new_name = "renamed_successfully" # Patch validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): # Configure mock client and the collection it returns mock_client_instance = MagicMock() mock_collection_instance = MagicMock() mock_collection_instance.modify.return_value = None # modify returns None on success mock_client_instance.get_collection.return_value = mock_collection_instance mock_get_client.return_value = mock_client_instance # --- Act --- input_model = RenameCollectionInput(collection_name=original_name, new_name=new_name) result = await _rename_collection_impl(input_model) # --- Assert --- mock_validate_name.assert_has_calls([call(original_name), call(new_name)]) mock_get_client.assert_called_once() mock_client_instance.get_collection.assert_called_once_with(name=original_name) mock_collection_instance.modify.assert_called_once_with(name=new_name) # Assert successful result message assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], types.TextContent) assert f"Collection '{original_name}' successfully renamed to '{new_name}'." in result[0].text @pytest.mark.asyncio # Remove fixture, patch manually inside # @pytest.mark.usefixtures("mock_validate_name") async def test_rename_collection_original_not_found(self): """Test renaming when the original collection does not exist.""" original_name = "original_not_found" new_name = "new_name_irrelevant" # Patch validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_client, ): # Configure the mock client's get_collection method to raise the error mock_client_instance = MagicMock() mock_client_instance.get_collection.side_effect = ValueError(f"Collection {original_name} does not exist.") mock_get_client.return_value = mock_client_instance # --- Act --- input_model = RenameCollectionInput(collection_name=original_name, new_name=new_name) with assert_raises_mcp_error(f"Tool Error: Collection '{original_name}' not found."): await _rename_collection_impl(input_model) # --- Assert --- mock_validate_name.assert_has_calls([call(original_name), call(new_name)]) # Assert the get_collection method was called on the mock client mock_client_instance.get_collection.assert_called_once_with(name=original_name) @pytest.mark.asyncio # Remove fixture, patch manually inside # @pytest.mark.usefixtures("mock_validate_name") async def test_rename_collection_new_name_exists(self): """Test renaming when the new name already exists.""" original_name = "original_exists" new_name = "new_name_exists" # Patch validator, client.get_collection, and collection.modify with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("chromadb.api.client.Client.get_collection") as mock_get_collection_method, patch( "chromadb.api.models.Collection.Collection.modify", # Patch the real modify side_effect=ValueError(f"Collection {new_name} already exists."), ) as mock_modify_method, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=MagicMock() ) as mock_get_client, ): # Add this patch # Configure the mock collection returned by get_collection mock_collection_instance = MagicMock() @pytest.mark.asyncio # Remove fixture, patch manually inside # @pytest.mark.usefixtures("mock_validate_name") async def test_rename_collection_unexpected_error(self): """Test unexpected error during rename (e.g., during modify).""" original_name = "rename_fail_orig" new_name = "rename_fail_new" error_msg = "Rename blew up" # Patch validator, client.get_collection, and collection.modify with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("chromadb.api.client.Client.get_collection") as mock_get_collection_method, patch( "chromadb.api.models.Collection.Collection.modify", # Patch the real modify side_effect=Exception(error_msg), ) as mock_modify_method, patch( "src.chroma_mcp.tools.collection_tools.get_chroma_client", return_value=MagicMock() ) as mock_get_client, ): # Add this patch # Configure the mock collection returned by get_collection mock_collection_instance = MagicMock() @pytest.mark.asyncio # Remove fixture, patch manually inside # @pytest.mark.usefixtures("mock_validate_name") async def test_rename_collection_other_value_error(self): """Test handling of other ValueErrors during rename (e.g., from modify).""" original_name = "rename_val_err_orig" new_name = "rename_val_err_new" error_msg = "Some other value issue" # Patch validator AND get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate_name, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): # Configure mock client and the collection it returns mock_client_instance = MagicMock() mock_collection_instance = MagicMock() mock_collection_instance.modify.side_effect = ValueError(error_msg) mock_client_instance.get_collection.return_value = mock_collection_instance # Make get_chroma_client return our mock client mock_get_chroma_client.return_value = mock_client_instance input_model = RenameCollectionInput(collection_name=original_name, new_name=new_name) # Expect the ValueError block (the one that doesn't check specific messages) with assert_raises_mcp_error(f"Invalid parameter during rename. Details: {error_msg}"): await _rename_collection_impl(input_model) # Assertions mock_validate_name.assert_any_call(original_name) mock_validate_name.assert_any_call(new_name) mock_get_chroma_client.assert_called_once() # Assert get_collection was called on the *mock* client mock_client_instance.get_collection.assert_called_once_with(name=original_name) # Assert modify was called on the *mock* collection mock_collection_instance.modify.assert_called_once_with(name=new_name) # --- _delete_collection_impl Tests --- @pytest.mark.asyncio # REMOVE explicit fixture if applied at class level # @pytest.mark.usefixtures("mock_server_config") async def test_delete_collection_success(self): # Keep self only """Test successful collection deletion.""" collection_name = "delete_me" # Use nested patches for validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): # Configure mock client and its delete method mock_client_instance = MagicMock() mock_client_instance.delete_collection.return_value = None # Successful delete mock_get_chroma_client.return_value = mock_client_instance # --- Act --- input_model = DeleteCollectionInput(collection_name=collection_name) result = await _delete_collection_impl(input_model) # --- Assert --- mock_validate.assert_called_once_with(collection_name) mock_get_chroma_client.assert_called_once() # Assert delete was called on the mock client instance mock_client_instance.delete_collection.assert_called_once_with(name=collection_name) # Check plain text result assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], types.TextContent) assert result[0].type == "text" assert result[0].text == f"Collection '{collection_name}' deleted successfully." # assert_successful_json_result(result, {"message": f"Collection '{collection_name}' deleted successfully."}) @pytest.mark.asyncio # REMOVE explicit fixture if applied at class level # @pytest.mark.usefixtures("mock_server_config") async def test_delete_collection_not_found(self): # Keep self only """Test deleting a non-existent collection (error from delete_collection).""" collection_name = "not_found_delete" error_message = f"Collection {collection_name} does not exist." # Use nested patches for validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): # Configure mock client to raise ValueError on delete mock_client_instance = MagicMock() mock_client_instance.delete_collection.side_effect = ValueError(error_message) mock_get_chroma_client.return_value = mock_client_instance # --- Act & Assert --- input_model = DeleteCollectionInput(collection_name=collection_name) with assert_raises_mcp_error(f"Tool Error: Collection '{collection_name}' not found."): await _delete_collection_impl(input_model) # Assert mocks mock_validate.assert_called_once_with(collection_name) mock_get_chroma_client.assert_called_once() mock_client_instance.delete_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE explicit fixture if applied at class level # @pytest.mark.usefixtures("mock_server_config") async def test_delete_collection_unexpected_error(self): # Keep self only """Test unexpected error during the delete_collection call.""" collection_name = "delete_fail" error_msg = "Delete failed unexpectedly" # Use nested patches for validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): # Configure mock client to raise generic Exception on delete mock_client_instance = MagicMock() mock_client_instance.delete_collection.side_effect = Exception(error_msg) mock_get_chroma_client.return_value = mock_client_instance input_model = DeleteCollectionInput(collection_name=collection_name) # Expect the generic Exception handler message with assert_raises_mcp_error( f"An unexpected error occurred deleting collection '{collection_name}'. Details: {error_msg}" ): await _delete_collection_impl(input_model) # Assert mocks mock_validate.assert_called_once_with(collection_name) mock_get_chroma_client.assert_called_once() mock_client_instance.delete_collection.assert_called_once_with(name=collection_name) @pytest.mark.asyncio # REMOVE explicit fixture if applied at class level # @pytest.mark.usefixtures("mock_server_config") async def test_delete_collection_validation_error(self): """Test validation error (e.g., invalid name) before client call.""" collection_name = "invalid name!" error_msg = "Invalid collection name format" # Patch only the validator to raise error with ( patch( "src.chroma_mcp.tools.collection_tools.validate_collection_name", side_effect=ValidationError(error_msg) ) as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): input_model = DeleteCollectionInput(collection_name=collection_name) with assert_raises_mcp_error(f"Validation Error: {error_msg}"): await _delete_collection_impl(input_model) # Assert mocks mock_validate.assert_called_once_with(collection_name) mock_get_chroma_client.assert_not_called() # Client should not be fetched @pytest.mark.asyncio # REMOVE explicit fixture if applied at class level # @pytest.mark.usefixtures("mock_server_config") async def test_delete_collection_other_value_error(self): # Keep self only """Test handling of other ValueErrors during the delete_collection call.""" collection_name = "delete_val_err" error_msg = "Another delete value error" # Use nested patches for validator and get_chroma_client with ( patch("src.chroma_mcp.tools.collection_tools.validate_collection_name") as mock_validate, patch("src.chroma_mcp.tools.collection_tools.get_chroma_client") as mock_get_chroma_client, ): # Configure mock client to raise specific ValueError (not 'not found') mock_client_instance = MagicMock() mock_client_instance.delete_collection.side_effect = ValueError(error_msg) mock_get_chroma_client.return_value = mock_client_instance input_model = DeleteCollectionInput(collection_name=collection_name) # Expect the specific ValueError handler message (check the impl for exact wording) with assert_raises_mcp_error( f"Invalid parameter during delete operation for '{collection_name}'. Details: {error_msg}" ): await _delete_collection_impl(input_model) # Assert mocks mock_validate.assert_called_once_with(collection_name) mock_get_chroma_client.assert_called_once() mock_client_instance.delete_collection.assert_called_once_with(name=collection_name)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/djm81/chroma_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server