mirror of
https://github.com/dnlbauer/cordra-mcp.git
synced 2026-09-10 21:55:30 +00:00
Merge branch 'main' of github.com:dnlbauer/cordra-mcp
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user