diff --git a/mcp_server_cli.py b/mcp_server_cli.py index ba79d1c..6f6ce90 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -100,6 +100,17 @@ class MCPServer: "required": ["query"] } }, + { + "name": "global_search", + "description": "Search projects, todos and campfire messages across all projects", + "inputSchema": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + } + }, { "name": "get_comments", "description": "Get comments for a Basecamp item", @@ -369,6 +380,16 @@ class MCPServer: "results": results } + elif tool_name == "global_search": + query = arguments.get("query") + search = BasecampSearch(client=client) + results = search.global_search(query) + return { + "status": "success", + "query": query, + "results": results + } + elif tool_name == "get_comments": recording_id = arguments.get("recording_id") project_id = arguments.get("project_id") diff --git a/search_utils.py b/search_utils.py index 1b44b3e..4e1f446 100644 --- a/search_utils.py +++ b/search_utils.py @@ -556,3 +556,50 @@ class BasecampSearch: except Exception as e: logger.error(f"Error searching campfire lines: {str(e)}") return [] + + def search_all_campfire_lines(self, query=None): + """Search campfire chat lines across all projects.""" + all_lines = [] + + try: + projects = self.client.get_projects() + + for project in projects: + project_id = project["id"] + try: + campfires = self.client.get_campfires(project_id) + for campfire in campfires: + campfire_id = campfire["id"] + lines = self.client.get_campfire_lines(project_id, campfire_id) + + for line in lines: + line["project"] = {"id": project_id, "name": project.get("name")} + line["campfire"] = {"id": campfire_id, "title": campfire.get("title")} + all_lines.append(line) + except Exception as e: + logger.error(f"Error getting campfire lines for project {project_id}: {str(e)}") + + if query and all_lines: + q = query.lower() + filtered = [] + for line in all_lines: + content = line.get("content", "") or "" + creator_name = "" + if line.get("creator"): + creator_name = line["creator"].get("name", "") + if q in content.lower() or (creator_name and q in creator_name.lower()): + filtered.append(line) + return filtered + + return all_lines + except Exception as e: + logger.error(f"Error searching all campfire lines: {str(e)}") + return [] + + def global_search(self, query=None): + """Search projects, todos and campfire lines at once.""" + return { + "projects": self.search_projects(query), + "todos": self.search_todos(query), + "campfire_lines": self.search_all_campfire_lines(query), + } diff --git a/tests/test_cli_server.py b/tests/test_cli_server.py index 86f9792..c7c6c74 100644 --- a/tests/test_cli_server.py +++ b/tests/test_cli_server.py @@ -102,7 +102,7 @@ def test_cli_server_tools_list(): # Check that expected tools are present tool_names = [tool["name"] for tool in tools] - expected_tools = ["get_projects", "search_basecamp", "get_todos"] + expected_tools = ["get_projects", "search_basecamp", "get_todos", "global_search"] for expected_tool in expected_tools: assert expected_tool in tool_names @@ -175,6 +175,55 @@ def test_cli_server_tool_call_no_auth(mock_get_token): if proc.poll() is None: proc.terminate() +@patch.object(token_storage, 'get_token') +def test_cli_server_global_search_call_no_auth(mock_get_token): + """Test global search tool call without authentication.""" + init_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": {} + } + + tool_request = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "global_search", + "arguments": {"query": "test"} + } + } + + proc = subprocess.Popen( + [sys.executable, "mcp_server_cli.py"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + try: + input_data = json.dumps(init_request) + "\n" + json.dumps(tool_request) + "\n" + stdout, stderr = proc.communicate( + input=input_data, + timeout=10 + ) + + lines = stdout.strip().split('\n') + assert len(lines) >= 2 + + tool_response = json.loads(lines[1]) + + assert tool_response["jsonrpc"] == "2.0" + assert tool_response["id"] == 2 + assert "result" in tool_response + assert "content" in tool_response["result"] + + finally: + if proc.poll() is None: + proc.terminate() + def test_cli_server_invalid_method(): """Test that the CLI server handles invalid methods.""" request = {