diff --git a/src/codealive_mcp_server.py b/src/codealive_mcp_server.py index 5bfe7b0..5122950 100644 --- a/src/codealive_mcp_server.py +++ b/src/codealive_mcp_server.py @@ -25,7 +25,7 @@ # Import core components from core import codealive_lifespan, setup_debug_logging -from tools import codebase_consultant, get_data_sources, codebase_search +from tools import codebase_consultant, get_data_sources, codebase_search, get_overview # Initialize FastMCP server with lifespan and enhanced system instructions mcp = FastMCP( @@ -106,6 +106,7 @@ async def health_check(request: Request) -> JSONResponse: mcp.tool()(codebase_consultant) mcp.tool()(get_data_sources) mcp.tool()(codebase_search) +mcp.tool()(get_overview) def main(): diff --git a/src/tests/test_overview.py b/src/tests/test_overview.py new file mode 100644 index 0000000..3239f31 --- /dev/null +++ b/src/tests/test_overview.py @@ -0,0 +1,308 @@ +"""Tests for the get_overview tool.""" + +import pytest +from unittest.mock import AsyncMock, patch, MagicMock + +from mcp.server.fastmcp import Context + +from tools.overview import get_overview +from core.client import CodeAliveContext +import httpx + + +@pytest.mark.asyncio +@patch("tools.overview.get_api_key_from_context") +async def test_get_overview_success(mock_get_api_key): + """Test successful retrieval of data source overview.""" + # Mock API key + mock_get_api_key.return_value = "test-api-key" + + # Mock response + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "name": "test-repo", + "overview": "# Purpose\nTest repository\n## Responsibilities\n- Task 1" + } + ] + mock_response.status_code = 200 + + # Mock client + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + + # Mock context + mock_context = MagicMock(spec=CodeAliveContext) + mock_context.client = mock_client + mock_context.base_url = "https://app.codealive.ai" + + mock_ctx = MagicMock(spec=Context) + mock_ctx.request_context.lifespan_context = mock_context + + # Call tool + result = await get_overview(mock_ctx, ["test-repo"]) + + # Assertions + assert '' in result + assert '' in result + assert '# Purpose' in result + assert 'Test repository' in result + assert '## Responsibilities' in result + assert '- Task 1' in result + + # Verify API call + mock_client.get.assert_called_once() + call_args = mock_client.get.call_args + assert call_args.kwargs['headers'] == {"Authorization": "Bearer test-api-key"} + assert "https://app.codealive.ai/api/overview" in call_args.args[0] + + +@pytest.mark.asyncio +@patch("tools.overview.get_api_key_from_context") +async def test_get_overview_multiple_repos(mock_get_api_key): + """Test retrieval of multiple data source overviews.""" + mock_get_api_key.return_value = "test-api-key" + + # Mock response with 3 repositories + mock_response = MagicMock() + mock_response.json.return_value = [ + {"name": "repo-1", "overview": "Overview 1"}, + {"name": "repo-2", "overview": "Overview 2"}, + {"name": "repo-3", "overview": "Overview 3"} + ] + mock_response.status_code = 200 + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + + mock_context = MagicMock(spec=CodeAliveContext) + mock_context.client = mock_client + mock_context.base_url = "https://app.codealive.ai" + + mock_ctx = MagicMock(spec=Context) + mock_ctx.request_context.lifespan_context = mock_context + + result = await get_overview(mock_ctx, ["repo-1", "repo-2", "repo-3"]) + + # Verify 3 repository blocks + assert result.count('' in result + assert '' in result + assert '' in result + assert 'Overview 1' in result + assert 'Overview 2' in result + assert 'Overview 3' in result + + +@pytest.mark.asyncio +@patch("tools.overview.get_api_key_from_context") +async def test_get_overview_no_data_sources(mock_get_api_key): + """Test retrieval without specifying data sources (all data sources).""" + mock_get_api_key.return_value = "test-api-key" + + mock_response = MagicMock() + mock_response.json.return_value = [ + {"name": "all-repo-1", "overview": "Overview 1"}, + {"name": "all-repo-2", "overview": "Overview 2"} + ] + mock_response.status_code = 200 + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + + mock_context = MagicMock(spec=CodeAliveContext) + mock_context.client = mock_client + mock_context.base_url = "https://app.codealive.ai" + + mock_ctx = MagicMock(spec=Context) + mock_ctx.request_context.lifespan_context = mock_context + + result = await get_overview(mock_ctx, None) + + # Verify API called without Names[] params + mock_client.get.assert_called_once() + call_args = mock_client.get.call_args + # When data_sources is None, params should be empty dict + assert call_args.kwargs['params'] == {} + + # Verify returns overviews for all repos + assert '' in result + assert '' in result + + +@pytest.mark.asyncio +@patch("tools.overview.get_api_key_from_context") +async def test_get_overview_empty_result(mock_get_api_key): + """Test handling of empty API response.""" + mock_get_api_key.return_value = "test-api-key" + + # Empty array response + mock_response = MagicMock() + mock_response.json.return_value = [] + mock_response.status_code = 200 + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + + mock_context = MagicMock(spec=CodeAliveContext) + mock_context.client = mock_client + mock_context.base_url = "https://app.codealive.ai" + + mock_ctx = MagicMock(spec=Context) + mock_ctx.request_context.lifespan_context = mock_context + + result = await get_overview(mock_ctx, ["nonexistent"]) + + # Should return empty root element + assert '' in result + # Should not contain any repository elements + assert ' str: + """Get high-level overview of data sources including purpose, responsibilities, ubiquitous language, and domain descriptions. + + This tool retrieves domain-focused information about data sources (repositories or workspaces) + to help users understand the business context and vocabulary of codebases. It returns structured + information including: + - Purpose: What the data source is for + - Responsibilities: What it does + - Ubiquitous Language: Domain-specific terminology and concepts + - Domain(s): Business domains covered with their vocabulary + + Args: + ctx: FastMCP context containing API client and configuration + data_sources: Optional list of repository/workspace names. If not provided, returns + overviews for all available data sources + + Returns: + XML formatted string containing data source overviews in markdown format + + Example: + # Get overview for specific data sources + result = await get_overview(ctx, ["my-backend-api", "frontend-app"]) + + # Get overviews for all data sources + result = await get_overview(ctx) + + # Example output structure: + # + # + # + # # Purpose + # Backend API for e-commerce platform + # + # ## Responsibilities + # - Handle user authentication + # - Process orders and payments + # + # ## Ubiquitous Language + # - Order: A customer purchase request + # - Cart: Collection of items before checkout + # + # ## Domains + # ### E-commerce + # - Product catalog management + # - Order processing + # + # + # + """ + context: CodeAliveContext = ctx.request_context.lifespan_context + + try: + api_key = get_api_key_from_context(ctx) + + # Normalize data_sources (handles Claude Desktop serialization issues) + data_source_names = normalize_data_source_names(data_sources) + + # Build request params + params = {} + if data_source_names and len(data_source_names) > 0: + formatted_names = format_data_source_names(data_source_names) + params = formatted_names + + # Prepare headers + headers = {"Authorization": f"Bearer {api_key}"} + + # Log the request + endpoint = "/api/overview" + full_url = urljoin(context.base_url, endpoint) + request_id = log_api_request("GET", full_url, headers, params=params) + + # Make API request + response = await context.client.get(endpoint, headers=headers, params=params) + + # Log the response + log_api_response(response, request_id) + + response.raise_for_status() + + # Parse JSON response + overview_data = response.json() + + # Transform to XML format + root = ET.Element("repository_overviews") + + for repo in overview_data: + repo_element = ET.SubElement(root, "repository") + repo_element.set("name", repo.get("name", "unknown")) + + overview_element = ET.SubElement(repo_element, "overview") + overview_element.text = repo.get("overview", "") + + # Convert to string with proper formatting + xml_string = ET.tostring(root, encoding="unicode", method="xml") + + return xml_string + + except (httpx.HTTPStatusError, Exception) as e: + return await handle_api_error(ctx, e, "get repository overview")