Merge pull request #1 from dnlbauer/fix-schema-pagination

Fix schema pagination and enhance search functionality
This commit is contained in:
Daniel Bauer
2025-07-05 12:43:55 +02:00
committed by GitHub
6 changed files with 300 additions and 134 deletions

View File

@@ -24,7 +24,11 @@ ensuring safe exploration without risk of data modification or corruption.
### Tools ### Tools
- `search_objects` - Search for digital objects using a query string. - `search_objects` - Search for digital objects using a query string with pagination support.
- `query` - Lucene/Solr compatible search query
- `type` - Optional filter by object type
- `limit` - Number of results per page (default: 1)
- `page_num` - Page number to retrieve, 0-based (default: 0)
## Configuration ## Configuration
@@ -35,7 +39,6 @@ The MCP server can be configured using environment variables with the `CORDRA_`
- `CORDRA_PASSWORD` - Password for authentication (optional) - `CORDRA_PASSWORD` - Password for authentication (optional)
- `CORDRA_VERIFY_SSL` - SSL certificate verification (default: `true`) - `CORDRA_VERIFY_SSL` - SSL certificate verification (default: `true`)
- `CORDRA_TIMEOUT` - Request timeout in seconds (default: `30`) - `CORDRA_TIMEOUT` - Request timeout in seconds (default: `30`)
- `CORDRA_MAX_SEARCH_RESULTS` - Maximum search results (default: `1000`)
## Usage ## Usage

View File

@@ -129,16 +129,21 @@ class CordraClient:
f"Failed to retrieve object {object_id}: {e}" f"Failed to retrieve object {object_id}: {e}"
) from e ) from e
async def find(self, query: str, object_type: str | None = None, limit: int | None = None) -> list[dict[str, Any]]: async def find(self, query: str, object_type: str | None = None, page_size: int = 20, page_num: int = 0) -> dict[str, Any]:
"""Find objects using a Cordra query. """Find objects using a Cordra query with pagination support.
Args: Args:
query: The query string to search for objects query: The query string to search for objects
object_type: Optional filter by object type object_type: Optional filter by object type
limit: Optional limit on number of results page_size: Number of results per page (if None, no limit)
page_num: Page number to retrieve (0-based, default: 0)
Returns: Returns:
List of objects matching the query as dictionaries Dict containing:
- results: List of objects matching the query as dictionaries
- total_size: Total number of results available
- page_num: Current page number
- page_size: Number of results per page
Raises: Raises:
ValueError: If query is empty ValueError: If query is empty
@@ -151,11 +156,11 @@ class CordraClient:
final_query = f"type:{object_type} AND ({query})" final_query = f"type:{object_type} AND ({query})"
url = f"{self.config.base_url}/search" url = f"{self.config.base_url}/search"
params = {"query": final_query} params = {
"query": final_query,
# Add pageSize if limit is specified "pageSize": str(page_size),
if limit is not None: "pageNum": str(page_num),
params["pageSize"] = str(limit) }
try: try:
response = self.session.get(url, params=params, timeout=self.config.timeout) response = self.session.get(url, params=params, timeout=self.config.timeout)
@@ -167,11 +172,12 @@ class CordraClient:
search_result = response.json() search_result = response.json()
# Extract the results array from the response return {
if isinstance(search_result, dict) and "results" in search_result: "results": search_result["results"],
return search_result["results"] # type: ignore "total_size": search_result["size"],
else: "page_num": search_result["pageNum"],
return [] "page_size": search_result["pageSize"]
}
except requests.RequestException as e: except requests.RequestException as e:
raise CordraClientError( raise CordraClientError(
@@ -196,7 +202,8 @@ class CordraClient:
query = f"type:Schema AND /name:{schema_name}" query = f"type:Schema AND /name:{schema_name}"
try: try:
schemas = await self.find(query) search_result = await self.find(query)
schemas = search_result["results"]
if not schemas: if not schemas:
raise CordraNotFoundError(f"Schema '{schema_name}' not found") raise CordraNotFoundError(f"Schema '{schema_name}' not found")

View File

@@ -22,9 +22,6 @@ class CordraConfig(BaseSettings):
password: str | None = Field( password: str | None = Field(
default=None, description="Password for Cordra authentication" default=None, description="Password for Cordra authentication"
) )
max_search_results: int = Field(
default=1000, description="Maximum number of search results to return"
)
verify_ssl: bool = Field( verify_ssl: bool = Field(
default=True, description="Whether to verify SSL certificates" default=True, description="Whether to verify SSL certificates"
) )

View File

@@ -35,14 +35,20 @@ Examples:
- /author:smith - Find objects by author Smith - /author:smith - Find objects by author Smith
- /name:John AND type:Person - Complex queries - /name:John AND type:Person - Complex queries
Pagination:
- Results are paginated with 0-based page numbering
- Use 'limit' to control page size (default: 1)
- Use 'page_num' to specify which page to retrieve (default: 0)
Returns a JSON list of matching objects with their full metadata.""" Returns a JSON list of matching objects with their full metadata."""
) )
async def search_objects( async def search_objects(
query: str, query: str,
type: str | None = None, type: str | None = None,
limit: int | None = None, limit: int = 1,
page_num: int = 0,
) -> str: ) -> str:
"""Search for digital objects in the Cordra repository. """Search for digital objects in the Cordra repository with pagination support.
Args: Args:
query: The search query string (Lucene/Solr compatible). Examples: query: The search query string (Lucene/Solr compatible). Examples:
@@ -50,14 +56,15 @@ async def search_objects(
- "/author:smith" - Find objects by author Smith - "/author:smith" - Find objects by author Smith
- "/name:John AND type:Person" - Complex queries - "/name:John AND type:Person" - Complex queries
type: Optional filter by object type (e.g., "Person", "Document", "Project") type: Optional filter by object type (e.g., "Person", "Document", "Project")
limit: Optional limit on number of results (default: config max_search_results) limit: Page size - number of results per page (default: 1)
page_num: Page number to retrieve, 0-based (default: 0 for first page)
Returns: Returns:
JSON string containing list of matching objects with their full metadata JSON string containing list of matching objects with their full metadata
""" """
try: try:
effective_limit = limit if limit is not None else config.max_search_results search_result = await cordra_client.find(query, object_type=type, page_size=limit, page_num=page_num)
results = await cordra_client.find(query, object_type=type, limit=effective_limit) results = search_result["results"]
return json.dumps(results, indent=2) return json.dumps(results, indent=2)
except ValueError as e: except ValueError as e:
@@ -155,10 +162,23 @@ async def create_schema_resource(schema_name: str) -> str:
async def register_schema_resources() -> None: async def register_schema_resources() -> None:
"""Register individual schema resources dynamically.""" """Register individual schema resources dynamically."""
try: try:
# Get all available schemas # Get all available schemas using pagination
schemas = await cordra_client.find("type:Schema") all_schemas = []
page_num = 0
page_size = 20
for schema in schemas: while True:
search_result = await cordra_client.find("type:Schema", page_size=page_size, page_num=page_num)
schemas = search_result["results"]
all_schemas.extend(schemas)
# Check if we've retrieved all schemas
if len(schemas) < page_size:
break
page_num += 1
for schema in all_schemas:
schema_name = schema.get("content", {}).get("name") schema_name = schema.get("content", {}).get("name")
if not schema_name: if not schema_name:
logger.warning("Schema without a name found, skipping.") logger.warning("Schema without a name found, skipping.")
@@ -180,7 +200,7 @@ async def register_schema_resources() -> None:
) )
) )
logger.info(f"Registered {len(schemas)} schema resources") logger.info(f"Registered {len(all_schemas)} schema resources")
except Exception as e: except Exception as e:
logger.warning(f"Failed to register schema resources: {e}") logger.warning(f"Failed to register schema resources: {e}")

View File

@@ -170,6 +170,8 @@ class TestCordraClient:
{"name": "Document", "identifier": "test/doc-schema"}, {"name": "Document", "identifier": "test/doc-schema"},
], ],
"size": 3, "size": 3,
"pageNum": 0,
"pageSize": 20,
} }
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
@@ -178,21 +180,25 @@ class TestCordraClient:
result = await client.find("type:Schema") result = await client.find("type:Schema")
assert len(result) == 3 assert isinstance(result, dict)
assert result[0]["name"] == "User" assert len(result["results"]) == 3
assert result[1]["name"] == "Project" assert result["results"][0]["name"] == "User"
assert result[2]["name"] == "Document" assert result["results"][1]["name"] == "Project"
assert result["results"][2]["name"] == "Document"
assert result["total_size"] == 3
assert result["page_num"] == 0
assert result["page_size"] == 20
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "type:Schema"}, params={"query": "type:Schema", "pageSize": "20", "pageNum": "0"},
timeout=30, timeout=30,
) )
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_empty_results(self, mock_get, client): async def test_find_empty_results(self, mock_get, client):
"""Test find with empty results.""" """Test find with empty results."""
mock_response_data = {"results": [], "size": 0} mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = mock_response_data mock_response.json.return_value = mock_response_data
@@ -200,26 +206,17 @@ class TestCordraClient:
result = await client.find("type:NonExistent") result = await client.find("type:NonExistent")
assert result == [] assert isinstance(result, dict)
assert result["results"] == []
assert result["total_size"] == 0
assert result["page_num"] == 0
assert result["page_size"] == 20
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "type:NonExistent"}, params={"query": "type:NonExistent", "pageSize": "20", "pageNum": "0"},
timeout=30, timeout=30,
) )
@patch("cordra_mcp.client.requests.Session.get")
async def test_find_no_results_key(self, mock_get, client):
"""Test find with response missing results key."""
mock_response_data = {"size": 0} # No results key
mock_response = mock_get.return_value
mock_response.status_code = 200
mock_response.json.return_value = mock_response_data
mock_response.raise_for_status.return_value = None
result = await client.find("type:Schema")
assert result == []
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_error(self, mock_get, client): async def test_find_error(self, mock_get, client):
"""Test find error handling.""" """Test find error handling."""
@@ -236,64 +233,108 @@ class TestCordraClient:
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_with_type_filter(self, mock_get, client): async def test_find_with_type_filter(self, mock_get, client):
"""Test find operation with type filter constructs correct query.""" """Test find operation with type filter constructs correct query."""
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = {"results": []} mock_response.json.return_value = mock_response_data
mock_response.ok = True mock_response.ok = True
await client.find("name:John", object_type="Person") await client.find("name:John", object_type="Person")
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "type:Person AND (name:John)"}, params={"query": "type:Person AND (name:John)", "pageSize": "20", "pageNum": "0"},
timeout=30, timeout=30,
) )
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_with_limit(self, mock_get, client): async def test_find_with_page_size(self, mock_get, client):
"""Test find operation with limit adds pageSize parameter.""" """Test find operation with custom page size."""
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 50}
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = {"results": []} mock_response.json.return_value = mock_response_data
mock_response.ok = True mock_response.ok = True
await client.find("type:Test", limit=50) await client.find("type:Test", page_size=50)
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "type:Test", "pageSize": "50"}, params={"query": "type:Test", "pageSize": "50", "pageNum": "0"},
timeout=30, timeout=30,
) )
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_with_type_and_limit(self, mock_get, client): async def test_find_with_type_and_page_size(self, mock_get, client):
"""Test find operation with both type filter and limit.""" """Test find operation with both type filter and page size."""
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 25}
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = {"results": []} mock_response.json.return_value = mock_response_data
mock_response.ok = True mock_response.ok = True
await client.find("title:Report", object_type="Document", limit=25) await client.find("title:Report", object_type="Document", page_size=25)
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "type:Document AND (title:Report)", "pageSize": "25"}, params={"query": "type:Document AND (title:Report)", "pageSize": "25", "pageNum": "0"},
timeout=30, timeout=30,
) )
@patch("cordra_mcp.client.requests.Session.get") @patch("cordra_mcp.client.requests.Session.get")
async def test_find_no_optional_params(self, mock_get, client): async def test_find_default_params(self, mock_get, client):
"""Test find operation with no optional parameters.""" """Test find operation with default parameters."""
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = {"results": []} mock_response.json.return_value = mock_response_data
mock_response.ok = True mock_response.ok = True
await client.find("content:test") await client.find("content:test")
mock_get.assert_called_once_with( mock_get.assert_called_once_with(
"https://test.example.com/search", "https://test.example.com/search",
params={"query": "content:test"}, params={"query": "content:test", "pageSize": "20", "pageNum": "0"},
timeout=30,
)
@patch("cordra_mcp.client.requests.Session.get")
async def test_find_with_page_num(self, mock_get, client):
"""Test find operation with specific page number."""
mock_response_data = {"results": [], "size": 100, "pageNum": 2, "pageSize": 20}
mock_response = mock_get.return_value
mock_response.status_code = 200
mock_response.json.return_value = mock_response_data
mock_response.ok = True
result = await client.find("type:Schema", page_num=2)
assert result["page_num"] == 2
assert result["page_size"] == 20
assert result["total_size"] == 100
mock_get.assert_called_once_with(
"https://test.example.com/search",
params={"query": "type:Schema", "pageSize": "20", "pageNum": "2"},
timeout=30,
)
@patch("cordra_mcp.client.requests.Session.get")
async def test_find_with_custom_page_size_and_num(self, mock_get, client):
"""Test find operation with custom page size and page number."""
mock_response_data = {"results": [], "size": 500, "pageNum": 5, "pageSize": 10}
mock_response = mock_get.return_value
mock_response.status_code = 200
mock_response.json.return_value = mock_response_data
mock_response.ok = True
result = await client.find("type:Document", page_size=10, page_num=5)
assert result["page_num"] == 5
assert result["page_size"] == 10
assert result["total_size"] == 500
mock_get.assert_called_once_with(
"https://test.example.com/search",
params={"query": "type:Document", "pageSize": "10", "pageNum": "5"},
timeout=30, timeout=30,
) )
@@ -372,6 +413,5 @@ class TestCordraConfig:
assert config.base_url == "https://localhost:8443" assert config.base_url == "https://localhost:8443"
assert config.username is None assert config.username is None
assert config.password is None assert config.password is None
assert config.max_search_results == 1000
assert config.verify_ssl is True assert config.verify_ssl is True
assert config.timeout == 30 assert config.timeout == 30

View File

@@ -190,12 +190,17 @@ class TestSchemaResourceFunctions:
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_register_schema_resources_success(self, mock_client): async def test_register_schema_resources_success(self, mock_client):
"""Test successful schema resource registration.""" """Test successful schema resource registration."""
mock_schemas = [ mock_search_result = {
{"content": {"name": "User"}, "id": "test/user-schema"}, "results": [
{"content": {"name": "Project"}, "id": "test/project-schema"}, {"content": {"name": "User"}, "id": "test/user-schema"},
{"content": {"name": "Document"}, "id": "test/doc-schema"} {"content": {"name": "Project"}, "id": "test/project-schema"},
] {"content": {"name": "Document"}, "id": "test/doc-schema"}
mock_client.find = AsyncMock(return_value=mock_schemas) ],
"total_size": 3,
"page_num": 0,
"page_size": 20
}
mock_client.find = AsyncMock(return_value=mock_search_result)
# Mock the mcp.add_resource method # Mock the mcp.add_resource method
with patch('cordra_mcp.server.mcp') as mock_mcp: with patch('cordra_mcp.server.mcp') as mock_mcp:
@@ -203,7 +208,7 @@ class TestSchemaResourceFunctions:
await register_schema_resources() await register_schema_resources()
# Verify the client was called with correct query # Verify the client was called with correct query
mock_client.find.assert_called_once_with("type:Schema") mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0)
# Verify add_resource was called for each schema # Verify add_resource was called for each schema
assert mock_mcp.add_resource.call_count == 3 assert mock_mcp.add_resource.call_count == 3
@@ -211,12 +216,17 @@ class TestSchemaResourceFunctions:
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_register_schema_resources_missing_name(self, mock_client): async def test_register_schema_resources_missing_name(self, mock_client):
"""Test schema resource registration with objects missing name field.""" """Test schema resource registration with objects missing name field."""
mock_schemas = [ mock_search_result = {
{"content": {"name": "User"}, "id": "test/user-schema"}, "results": [
{"content": {}, "id": "test/no-name-schema"}, # Missing name field {"content": {"name": "User"}, "id": "test/user-schema"},
{"content": {"name": "Project"}, "id": "test/project-schema"} {"content": {}, "id": "test/no-name-schema"}, # Missing name field
] {"content": {"name": "Project"}, "id": "test/project-schema"}
mock_client.find = AsyncMock(return_value=mock_schemas) ],
"total_size": 3,
"page_num": 0,
"page_size": 20
}
mock_client.find = AsyncMock(return_value=mock_search_result)
with patch('cordra_mcp.server.mcp') as mock_mcp: with patch('cordra_mcp.server.mcp') as mock_mcp:
from cordra_mcp.server import register_schema_resources from cordra_mcp.server import register_schema_resources
@@ -234,22 +244,63 @@ class TestSchemaResourceFunctions:
from cordra_mcp.server import register_schema_resources from cordra_mcp.server import register_schema_resources
await register_schema_resources() # Should complete without raising await register_schema_resources() # Should complete without raising
mock_client.find.assert_called_once_with("type:Schema") mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client')
async def test_register_schema_resources_pagination(self, mock_client):
"""Test schema resource registration with pagination."""
# Mock multiple pages of results
# First page with full 20 results (simulating more schemas)
first_page_schemas = [{"content": {"name": f"Schema{i}"}, "id": f"test/schema{i}"} for i in range(20)]
first_page = {
"results": first_page_schemas,
"total_size": 25,
"page_num": 0,
"page_size": 20
}
# Second page with fewer results (indicating last page)
second_page = {
"results": [
{"content": {"name": "Document"}, "id": "test/doc-schema"},
],
"total_size": 25,
"page_num": 1,
"page_size": 20
}
# Return first page, then second page (with fewer results indicating last page)
mock_client.find = AsyncMock(side_effect=[first_page, second_page])
with patch('cordra_mcp.server.mcp') as mock_mcp:
from cordra_mcp.server import register_schema_resources
await register_schema_resources()
# Verify pagination calls
assert mock_client.find.call_count == 2
mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=0)
mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=1)
# Verify all 21 schemas were registered (20 from first page + 1 from second page)
assert mock_mcp.add_resource.call_count == 21
class TestSearchObjects: class TestSearchObjects:
"""Test the search_objects tool.""" """Test the search_objects tool."""
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_success(self, mock_client):
async def test_search_objects_success(self, mock_config, mock_client):
"""Test successful object search.""" """Test successful object search."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_results = [ "results": [
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
{"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}}, {"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}},
] ],
mock_client.find = AsyncMock(return_value=mock_results) "total_size": 2,
"page_num": 0,
"page_size": 1000
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("name:John") result = await search_objects("name:John")
@@ -260,17 +311,20 @@ class TestSearchObjects:
assert parsed_result[1]["id"] == "people/jane-smith" assert parsed_result[1]["id"] == "people/jane-smith"
# Verify the client was called with correct parameters # Verify the client was called with correct parameters
mock_client.find.assert_called_once_with("name:John", object_type=None, limit=1000) mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=1, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_with_type_filter(self, mock_client):
async def test_search_objects_with_type_filter(self, mock_config, mock_client):
"""Test object search with type filter.""" """Test object search with type filter."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_results = [ "results": [
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
] ],
mock_client.find = AsyncMock(return_value=mock_results) "total_size": 1,
"page_num": 0,
"page_size": 1000
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("name:John", type="Person") result = await search_objects("name:John", type="Person")
@@ -280,17 +334,20 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Person" assert parsed_result[0]["type"] == "Person"
# Verify the client was called with type filter # Verify the client was called with type filter
mock_client.find.assert_called_once_with("name:John", object_type="Person", limit=1000) mock_client.find.assert_called_once_with("name:John", object_type="Person", page_size=1, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_with_limit(self, mock_client):
async def test_search_objects_with_limit(self, mock_config, mock_client):
"""Test object search with custom limit.""" """Test object search with custom limit."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_results = [ "results": [
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
] ],
mock_client.find = AsyncMock(return_value=mock_results) "total_size": 1,
"page_num": 0,
"page_size": 50
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("name:John", limit=50) result = await search_objects("name:John", limit=50)
@@ -299,17 +356,20 @@ class TestSearchObjects:
assert len(parsed_result) == 1 assert len(parsed_result) == 1
# Verify the client was called with custom limit # Verify the client was called with custom limit
mock_client.find.assert_called_once_with("name:John", object_type=None, limit=50) mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=50, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_with_all_parameters(self, mock_client):
async def test_search_objects_with_all_parameters(self, mock_config, mock_client):
"""Test object search with all parameters.""" """Test object search with all parameters."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_results = [ "results": [
{"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}}, {"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}},
] ],
mock_client.find = AsyncMock(return_value=mock_results) "total_size": 1,
"page_num": 0,
"page_size": 25
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("title:Report", type="Document", limit=25) result = await search_objects("title:Report", type="Document", limit=25)
@@ -319,14 +379,18 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Document" assert parsed_result[0]["type"] == "Document"
# Verify the client was called with all parameters # Verify the client was called with all parameters
mock_client.find.assert_called_once_with("title:Report", object_type="Document", limit=25) mock_client.find.assert_called_once_with("title:Report", object_type="Document", page_size=25, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_empty_results(self, mock_client):
async def test_search_objects_empty_results(self, mock_config, mock_client):
"""Test object search with no results.""" """Test object search with no results."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_client.find = AsyncMock(return_value=[]) "results": [],
"total_size": 0,
"page_num": 0,
"page_size": 1000
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("nonexistent:data") result = await search_objects("nonexistent:data")
@@ -334,43 +398,42 @@ class TestSearchObjects:
parsed_result = json.loads(result) parsed_result = json.loads(result)
assert parsed_result == [] assert parsed_result == []
mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, limit=1000) mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, page_size=1, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_client_error(self, mock_client):
async def test_search_objects_client_error(self, mock_config, mock_client):
"""Test object search with client error.""" """Test object search with client error."""
mock_config.max_search_results = 1000
mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed")) mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed"))
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await search_objects("test:query") await search_objects("test:query")
assert "Search failed:" in str(exc_info.value) assert "Search failed:" in str(exc_info.value)
mock_client.find.assert_called_once_with("test:query", object_type=None, limit=1000) mock_client.find.assert_called_once_with("test:query", object_type=None, page_size=1, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_value_error(self, mock_client):
async def test_search_objects_value_error(self, mock_config, mock_client):
"""Test object search with value error.""" """Test object search with value error."""
mock_config.max_search_results = 1000
mock_client.find = AsyncMock(side_effect=ValueError("Invalid query")) mock_client.find = AsyncMock(side_effect=ValueError("Invalid query"))
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await search_objects("invalid:query") await search_objects("invalid:query")
assert "Invalid search parameters:" in str(exc_info.value) assert "Invalid search parameters:" in str(exc_info.value)
mock_client.find.assert_called_once_with("invalid:query", object_type=None, limit=1000) mock_client.find.assert_called_once_with("invalid:query", object_type=None, page_size=1, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
@patch('cordra_mcp.server.config') async def test_search_objects_json_formatting(self, mock_client):
async def test_search_objects_json_formatting(self, mock_config, mock_client):
"""Test that search results are properly formatted as JSON.""" """Test that search results are properly formatted as JSON."""
mock_config.max_search_results = 1000 mock_search_result = {
mock_results = [ "results": [
{"id": "test/object", "type": "Test", "content": {"data": "value"}}, {"id": "test/object", "type": "Test", "content": {"data": "value"}},
] ],
mock_client.find = AsyncMock(return_value=mock_results) "total_size": 1,
"page_num": 0,
"page_size": 1000
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await search_objects("test:query") result = await search_objects("test:query")
@@ -386,6 +449,42 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Test" assert parsed_result[0]["type"] == "Test"
assert parsed_result[0]["content"]["data"] == "value" assert parsed_result[0]["content"]["data"] == "value"
@patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_page_num(self, mock_client):
"""Test object search with page number parameter."""
mock_search_result = {
"results": [
{"id": "documents/doc-21", "type": "Document", "content": {"title": "Page 2 Doc"}},
],
"total_size": 50,
"page_num": 1,
"page_size": 20
}
mock_client.find = AsyncMock(return_value=mock_search_result)
await search_objects("type:Document", page_num=1)
# Verify the client was called with correct page number
mock_client.find.assert_called_once_with("type:Document", object_type=None, page_size=1, page_num=1)
@patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_all_pagination_params(self, mock_client):
"""Test object search with all pagination parameters."""
mock_search_result = {
"results": [
{"id": "reports/report-51", "type": "Report", "content": {"title": "Report 51"}},
],
"total_size": 100,
"page_num": 5,
"page_size": 10
}
mock_client.find = AsyncMock(return_value=mock_search_result)
await search_objects("type:Report", type="Report", limit=10, page_num=5)
# Verify the client was called with all parameters
mock_client.find.assert_called_once_with("type:Report", object_type="Report", page_size=10, page_num=5)
class TestGetCordraDesign: class TestGetCordraDesign:
"""Test the get_cordra_design resource handler.""" """Test the get_cordra_design resource handler."""