diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c02ff720..4ad0160c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -174,6 +174,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu @@ -233,6 +234,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -361,6 +363,7 @@ jobs: COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prefer CPU-only PyTorch in CI (but keep PyPI for everything else) UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu @@ -417,6 +420,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prefer CPU-only PyTorch in CI (but keep PyPI for everything else) @@ -458,6 +462,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -493,6 +498,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prefer CPU-only PyTorch in CI (but keep PyPI for everything else) @@ -539,6 +545,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -574,6 +581,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prefer CPU-only PyTorch in CI (but keep PyPI for everything else) @@ -619,6 +627,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -654,6 +663,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prefer CPU-only PyTorch in CI (but keep PyPI for everything else) @@ -693,6 +703,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -732,6 +743,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 HINDSIGHT_EMBED_PACKAGE_PATH: ${{ github.workspace }}/hindsight-embed GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -798,6 +810,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -833,6 +846,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu @@ -889,6 +903,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -982,6 +997,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex # Prefer CPU-only PyTorch in CI UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu @@ -1026,6 +1042,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex # For test_server_integration.py compatibility HINDSIGHT_LLM_PROVIDER: groq HINDSIGHT_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} @@ -1075,6 +1092,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex HINDSIGHT_API_URL: http://localhost:8888 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu @@ -1130,6 +1148,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }} HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }} HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=${{ env.HINDSIGHT_API_LLM_GROQ_SERVICE_TIER }} EOF - name: Start API server @@ -1167,6 +1186,7 @@ jobs: HINDSIGHT_API_LLM_PROVIDER: groq HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b + HINDSIGHT_API_LLM_GROQ_SERVICE_TIER: flex GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu diff --git a/hindsight-api/hindsight_api/config.py b/hindsight-api/hindsight_api/config.py index 885197bc..a87330f5 100644 --- a/hindsight-api/hindsight_api/config.py +++ b/hindsight-api/hindsight_api/config.py @@ -233,8 +233,6 @@ ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER" ENV_MPFP_TOP_K_NEIGHBORS = "HINDSIGHT_API_MPFP_TOP_K_NEIGHBORS" ENV_RECALL_MAX_CONCURRENT = "HINDSIGHT_API_RECALL_MAX_CONCURRENT" ENV_RECALL_CONNECTION_BUDGET = "HINDSIGHT_API_RECALL_CONNECTION_BUDGET" -ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID" -ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS" ENV_MENTAL_MODEL_REFRESH_CONCURRENCY = "HINDSIGHT_API_MENTAL_MODEL_REFRESH_CONCURRENCY" # OpenTelemetry tracing configuration @@ -389,7 +387,6 @@ DEFAULT_GRAPH_RETRIEVER = "link_expansion" # Options: "link_expansion", "mpfp", DEFAULT_MPFP_TOP_K_NEIGHBORS = 20 # Fan-out limit per node in MPFP graph traversal DEFAULT_RECALL_MAX_CONCURRENT = 32 # Max concurrent recall operations per worker DEFAULT_RECALL_CONNECTION_BUDGET = 4 # Max concurrent DB connections per recall operation -DEFAULT_MCP_LOCAL_BANK_ID = "mcp" DEFAULT_MENTAL_MODEL_REFRESH_CONCURRENCY = 8 # Max concurrent mental model refreshes # Retain settings diff --git a/hindsight-api/hindsight_api/mcp_local.py b/hindsight-api/hindsight_api/mcp_local.py index 644b6600..65c75bd7 100644 --- a/hindsight-api/hindsight_api/mcp_local.py +++ b/hindsight-api/hindsight_api/mcp_local.py @@ -1,8 +1,14 @@ """ -Local MCP server for use with Claude Code (stdio transport). +Local MCP server entry point for use with Claude Code (HTTP transport). -This runs a fully local Hindsight instance with embedded PostgreSQL (pg0). -No external database or server required. +This is a thin wrapper around the main hindsight-api server that pre-configures +sensible defaults for local use (embedded PostgreSQL via pg0, warning log level). + +The full API runs on localhost:8888. Configure Claude Code's MCP settings: + claude mcp add --transport http hindsight http://localhost:8888/mcp/ + +Or pinned to a specific bank (single-bank mode): + claude mcp add --transport http hindsight http://localhost:8888/mcp/default/ Run with: hindsight-local-mcp @@ -10,148 +16,24 @@ Run with: Or with uvx: uvx hindsight-api@latest hindsight-local-mcp -Configure in Claude Code's MCP settings: - { - "mcpServers": { - "hindsight": { - "command": "uvx", - "args": ["hindsight-api@latest", "hindsight-local-mcp"], - "env": { - "HINDSIGHT_API_LLM_API_KEY": "your-openai-key" - } - } - } - } - Environment variables: HINDSIGHT_API_LLM_API_KEY: Required. API key for LLM provider. HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai"). HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini"). - HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp"). - HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "warning"). - HINDSIGHT_API_MCP_INSTRUCTIONS: Optional. Additional instructions appended to both retain and recall tools. - -Example custom instructions (these are ADDED to the default behavior): - To also store assistant actions: - HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls, code written, and decisions made." - - To also store conversation summaries: - HINDSIGHT_API_MCP_INSTRUCTIONS="Also store summaries of important conversations and their outcomes." + HINDSIGHT_API_DATABASE_URL: Optional. Override database URL (default: pg0://hindsight-mcp). """ -import logging import os -import sys - -from mcp.server.fastmcp import FastMCP - -from hindsight_api.config import ( - DEFAULT_MCP_LOCAL_BANK_ID, - DEFAULT_MCP_RECALL_DESCRIPTION, - DEFAULT_MCP_RETAIN_DESCRIPTION, - ENV_MCP_INSTRUCTIONS, - ENV_MCP_LOCAL_BANK_ID, -) -from hindsight_api.mcp_tools import MCPToolsConfig, register_mcp_tools - -# Configure logging - default to warning to avoid polluting stderr during MCP init -# MCP clients interpret stderr output as errors, so we suppress INFO logs by default -_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "warning").lower() -_log_level_map = { - "critical": logging.CRITICAL, - "error": logging.ERROR, - "warning": logging.WARNING, - "info": logging.INFO, - "debug": logging.DEBUG, -} -logging.basicConfig( - level=_log_level_map.get(_log_level_str, logging.WARNING), - format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", - stream=sys.stderr, # MCP uses stdout for protocol, logs go to stderr -) -logger = logging.getLogger(__name__) -def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP: - """ - Create a stdio MCP server with retain/recall tools. +def main() -> None: + """Start the Hindsight API server with local defaults.""" + # Set local defaults (only if not already configured by the user) + os.environ.setdefault("HINDSIGHT_API_DATABASE_URL", "pg0://hindsight-mcp") - Args: - bank_id: The memory bank ID to use for all operations. - memory: Optional MemoryEngine instance. If not provided, creates one with pg0. + from hindsight_api.main import main as api_main - Returns: - Configured FastMCP server instance. - """ - # Import here to avoid slow startup if just checking --help - from hindsight_api import MemoryEngine - - # Create memory engine with pg0 embedded database if not provided - if memory is None: - memory = MemoryEngine(db_url="pg0://hindsight-mcp") - - # Get custom instructions from environment variable (appended to both tools) - extra_instructions = os.environ.get(ENV_MCP_INSTRUCTIONS, "") - - retain_description = DEFAULT_MCP_RETAIN_DESCRIPTION - recall_description = DEFAULT_MCP_RECALL_DESCRIPTION - - if extra_instructions: - retain_description = f"{DEFAULT_MCP_RETAIN_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}" - recall_description = f"{DEFAULT_MCP_RECALL_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}" - - mcp = FastMCP("hindsight") - - # Configure and register tools using shared module - config = MCPToolsConfig( - bank_id_resolver=lambda: bank_id, - include_bank_id_param=False, # Local MCP uses fixed bank_id - tools={"retain", "recall"}, # Local MCP only has retain and recall - retain_description=retain_description, - recall_description=recall_description, - retain_fire_and_forget=True, # Local MCP uses fire-and-forget pattern - ) - - register_mcp_tools(mcp, memory, config) - - return mcp - - -async def _initialize_and_run(bank_id: str): - """Initialize memory and run the MCP server.""" - from hindsight_api import MemoryEngine - - # Create and initialize memory engine with pg0 embedded database - # Note: We avoid printing to stderr during init as MCP clients show it as "errors" - memory = MemoryEngine(db_url="pg0://hindsight-mcp") - await memory.initialize() - - # Create and run the server - mcp = create_local_mcp_server(bank_id, memory=memory) - await mcp.run_stdio_async() - - -def main(): - """Main entry point for the stdio MCP server.""" - import asyncio - - from hindsight_api.config import ENV_LLM_API_KEY, get_config - - # Check for required environment variables - config = get_config() - if not config.llm_api_key: - print(f"Error: {ENV_LLM_API_KEY} environment variable is required", file=sys.stderr) - print("Set it in your MCP configuration or shell environment", file=sys.stderr) - sys.exit(1) - - # Get bank ID from environment, default to "mcp" - bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID) - - # Note: We don't print to stderr as MCP clients display it as "error output" - # Use HINDSIGHT_API_LOG_LEVEL=debug for verbose startup logging - - # Run the async initialization and server - asyncio.run(_initialize_and_run(bank_id)) + api_main() if __name__ == "__main__": diff --git a/hindsight-api/tests/test_mcp_local.py b/hindsight-api/tests/test_mcp_local.py deleted file mode 100644 index 643d1f24..00000000 --- a/hindsight-api/tests/test_mcp_local.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Test local MCP server.""" - -import asyncio -import pytest -from unittest.mock import AsyncMock, MagicMock - - -@pytest.fixture -def mock_memory(): - """Create a mock MemoryEngine.""" - memory = MagicMock() - memory._initialized = True - memory.retain_batch_async = AsyncMock() - memory.recall_async = AsyncMock(return_value=MagicMock(results=[])) - return memory - - -@pytest.mark.asyncio -async def test_local_mcp_server_retain(mock_memory): - """Test that retain tool fires async and returns immediately.""" - from hindsight_api.mcp_local import create_local_mcp_server - - bank_id = "test-bank" - mcp_server = create_local_mcp_server(bank_id, memory=mock_memory) - - # Get the tools - tools = mcp_server._tool_manager._tools - assert "retain" in tools - - # Call retain - retain_tool = tools["retain"] - result = await retain_tool.fn(content="test content", context="test_context") - - # Returns immediately with accepted status - assert result["status"] == "accepted" - - # Wait for background task to complete - await asyncio.sleep(0.1) - - # Verify the memory was called correctly - mock_memory.retain_batch_async.assert_called_once() - call_kwargs = mock_memory.retain_batch_async.call_args.kwargs - assert call_kwargs["bank_id"] == "test-bank" - assert call_kwargs["contents"] == [{"content": "test content", "context": "test_context"}] - - -@pytest.mark.asyncio -async def test_local_mcp_server_recall(mock_memory): - """Test that recall tool calls memory.recall_async with correct params.""" - from hindsight_api.mcp_local import create_local_mcp_server - from hindsight_api.engine.memory_engine import Budget - - # Mock recall_async to return a proper pydantic model - mock_result = MagicMock() - mock_result.model_dump.return_value = {"results": []} - mock_memory.recall_async = AsyncMock(return_value=mock_result) - - bank_id = "test-bank" - mcp_server = create_local_mcp_server(bank_id, memory=mock_memory) - - # Get the tools - tools = mcp_server._tool_manager._tools - assert "recall" in tools - - # Call recall - recall_tool = tools["recall"] - result = await recall_tool.fn(query="test query", max_tokens=2048) - - # Result is a dict - assert isinstance(result, dict) - - # Verify the memory was called correctly - mock_memory.recall_async.assert_called_once() - call_kwargs = mock_memory.recall_async.call_args.kwargs - assert call_kwargs["bank_id"] == "test-bank" - assert call_kwargs["query"] == "test query" - assert call_kwargs["max_tokens"] == 2048 - assert call_kwargs["budget"] == Budget.HIGH - - -@pytest.mark.asyncio -async def test_local_mcp_server_retain_with_default_context(mock_memory): - """Test that retain uses default context when not provided.""" - from hindsight_api.mcp_local import create_local_mcp_server - - bank_id = "test-bank" - mcp_server = create_local_mcp_server(bank_id, memory=mock_memory) - - tools = mcp_server._tool_manager._tools - retain_tool = tools["retain"] - - # Call retain without context - await retain_tool.fn(content="test content") - - # Wait for background task - await asyncio.sleep(0.1) - - call_kwargs = mock_memory.retain_batch_async.call_args.kwargs - assert call_kwargs["contents"] == [{"content": "test content", "context": "general"}] - - -@pytest.mark.asyncio -async def test_local_mcp_server_retain_error_handling(mock_memory): - """Test that retain errors are logged but don't affect response.""" - from hindsight_api.mcp_local import create_local_mcp_server - - mock_memory.retain_batch_async = AsyncMock(side_effect=Exception("Test error")) - - mcp_server = create_local_mcp_server("test-bank", memory=mock_memory) - - tools = mcp_server._tool_manager._tools - retain_tool = tools["retain"] - - # Retain returns immediately with accepted status (fire and forget) - result = await retain_tool.fn(content="test content") - assert result["status"] == "accepted" - - # Wait for background task to complete (and log error) - await asyncio.sleep(0.1) - - -@pytest.mark.asyncio -async def test_local_mcp_server_recall_error_handling(mock_memory): - """Test that recall handles errors gracefully.""" - from hindsight_api.mcp_local import create_local_mcp_server - - mock_memory.recall_async = AsyncMock(side_effect=Exception("Test error")) - - mcp_server = create_local_mcp_server("test-bank", memory=mock_memory) - - tools = mcp_server._tool_manager._tools - recall_tool = tools["recall"] - - result = await recall_tool.fn(query="test query") - - # Result is a dict with error - assert isinstance(result, dict) - assert "error" in result - assert result["results"] == [] - - -@pytest.mark.asyncio -async def test_local_mcp_server_recall_with_defaults(mock_memory): - """Test that recall uses default max_tokens and HIGH budget.""" - from hindsight_api.mcp_local import create_local_mcp_server - from hindsight_api.engine.memory_engine import Budget - - mock_result = MagicMock() - mock_result.model_dump.return_value = {"results": []} - mock_memory.recall_async = AsyncMock(return_value=mock_result) - - mcp_server = create_local_mcp_server("test-bank", memory=mock_memory) - - tools = mcp_server._tool_manager._tools - recall_tool = tools["recall"] - - # Call with defaults - await recall_tool.fn(query="test query") - - call_kwargs = mock_memory.recall_async.call_args.kwargs - assert call_kwargs["max_tokens"] == 4096 - assert call_kwargs["budget"] == Budget.HIGH - - -@pytest.mark.asyncio -async def test_local_mcp_server_retain_with_timestamp(mock_memory): - """Test that retain passes timestamp as event_date.""" - from datetime import datetime, timezone - from hindsight_api.mcp_local import create_local_mcp_server - - mcp_server = create_local_mcp_server("test-bank", memory=mock_memory) - - tools = mcp_server._tool_manager._tools - retain_tool = tools["retain"] - - # Call retain with timestamp - result = await retain_tool.fn( - content="test content", context="test_context", timestamp="2024-01-15T10:30:00Z" - ) - - assert result["status"] == "accepted" - - # Wait for background task - await asyncio.sleep(0.1) - - call_kwargs = mock_memory.retain_batch_async.call_args.kwargs - contents = call_kwargs["contents"] - assert len(contents) == 1 - assert contents[0]["content"] == "test content" - assert contents[0]["context"] == "test_context" - assert "event_date" in contents[0] - assert contents[0]["event_date"] == datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc) - - -@pytest.mark.asyncio -async def test_local_mcp_server_retain_with_invalid_timestamp(mock_memory): - """Test that retain rejects invalid timestamp format.""" - from hindsight_api.mcp_local import create_local_mcp_server - - mcp_server = create_local_mcp_server("test-bank", memory=mock_memory) - - tools = mcp_server._tool_manager._tools - retain_tool = tools["retain"] - - # Call retain with invalid timestamp - result = await retain_tool.fn(content="test content", timestamp="not-a-date") - - assert result["status"] == "error" - assert "Invalid timestamp format" in result["message"] - - # Verify retain_batch_async was NOT called - mock_memory.retain_batch_async.assert_not_called() diff --git a/hindsight-docs/docs/sdks/integrations/local-mcp.md b/hindsight-docs/docs/sdks/integrations/local-mcp.md index 95fe2c67..8d3740a3 100644 --- a/hindsight-docs/docs/sdks/integrations/local-mcp.md +++ b/hindsight-docs/docs/sdks/integrations/local-mcp.md @@ -4,188 +4,119 @@ sidebar_position: 2 # Local MCP Server -Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required. +Hindsight provides a local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required. This is ideal for: -- **Personal use with Claude Desktop** — Give Claude long-term memory across conversations +- **Personal use with Claude Code / Claude Desktop** — Give Claude long-term memory across conversations - **Development and testing** — Quick setup without infrastructure - **Privacy-focused setups** — All data stays on your machine -## Quick Install +## How It Works + +Running `hindsight-local-mcp` starts the full Hindsight API on `localhost:8888` with an embedded PostgreSQL database (pg0). You then connect your MCP client to it over HTTP. + +- Starts an embedded PostgreSQL (pg0) automatically +- Runs database migrations on startup +- Exposes the full MCP endpoint at `http://localhost:8888/mcp/` +- Data persists in `~/.pg0/hindsight-mcp/` across restarts + +## Setup + +### 1. Start the server ```bash -curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \ - --app claude-desktop \ - --set HINDSIGHT_API_LLM_API_KEY=sk-... +HINDSIGHT_API_LLM_API_KEY=sk-... uvx --from hindsight-api hindsight-local-mcp ``` -This script will: -1. Install [uv](https://docs.astral.sh/uv/) if not already installed -2. Configure Claude Desktop to use the Hindsight MCP server -3. Set the provided environment variables in the MCP configuration - -:::info Other MCP Applications -The quick install script currently supports Claude Desktop only. For other MCP-compatible applications (Cursor, Cline, etc.), follow the [Manual Configuration](#manual-configuration) steps below. -::: - -## Manual Configuration - -Add the following to your MCP client's configuration. For Claude Desktop: - -- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -- **Linux**: `~/.config/Claude/claude_desktop_config.json` - -For other MCP clients, refer to their documentation for the configuration file location. - -```json -{ - "mcpServers": { - "hindsight": { - "command": "uvx", - "args": ["--from", "hindsight-api", "hindsight-local-mcp"], - "env": { - "HINDSIGHT_API_LLM_API_KEY": "sk-..." - } - } - } -} -``` - -### With Custom Bank ID - -By default, memories are stored in a bank called `mcp`. To use a different bank: - -```json -{ - "mcpServers": { - "hindsight": { - "command": "uvx", - "args": ["--from", "hindsight-api", "hindsight-local-mcp"], - "env": { - "HINDSIGHT_API_LLM_API_KEY": "sk-...", - "HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory" - } - } - } -} -``` - -## Environment Variables - -All standard [Hindsight configuration variables](/developer/configuration) are supported. - -### Local MCP Specific - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use | -| `HINDSIGHT_API_MCP_INSTRUCTIONS` | No | - | Additional instructions appended to both `retain` and `recall` tools | - -### Customizing Tool Behavior - -You can customize what gets stored by adding instructions to the tools. Re-run the install script with the additional `--set` flag: +Or with Ollama (no API key needed): ```bash -curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \ - --app claude-desktop \ - --set HINDSIGHT_API_LLM_API_KEY=sk-... \ - --set HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, code you write, and files you modify." +HINDSIGHT_API_LLM_PROVIDER=ollama HINDSIGHT_API_LLM_MODEL=llama3.2 uvx --from hindsight-api hindsight-local-mcp ``` -These instructions are appended to the default tool descriptions, guiding Claude on when and how to use the memory tools. +### 2. Configure your MCP client + +**Claude Code:** + +```bash +claude mcp add --transport http hindsight http://localhost:8888/mcp/ +``` + +**Other MCP clients** — add an HTTP transport entry pointing to `http://localhost:8888/mcp/`. + +## Bank Modes + +The local server supports the same two modes as the hosted API: + +### Multi-bank mode (default) + +Use `http://localhost:8888/mcp/` — exposes all tools including bank management. Bank is selected per-request via the `bank_id` tool parameter or the `X-Bank-Id` header. + +```bash +claude mcp add --transport http hindsight http://localhost:8888/mcp/ +``` + +### Single-bank mode + +Use `http://localhost:8888/mcp//` — pins all tools to one bank, no `bank_id` parameter needed. This replaces the old `HINDSIGHT_API_MCP_LOCAL_BANK_ID` env var. + +```bash +claude mcp add --transport http hindsight http://localhost:8888/mcp/my-bank/ +``` ## Available Tools -### retain +The local server exposes the full tool set: -Store information to long-term memory. This is a **fire-and-forget** operation — it returns immediately while processing happens in the background. +| Tool | Description | +|------|-------------| +| `retain` | Store information to long-term memory (fire-and-forget) | +| `recall` | Search memories with natural language | +| `reflect` | Synthesize memories into a reasoned answer | +| `list_banks` | List all memory banks | +| `create_bank` | Create or configure a memory bank | +| `list_mental_models` | List pinned reflections for a bank | +| `get_mental_model` | Get a specific mental model | +| `create_mental_model` | Create a new mental model | +| `update_mental_model` | Update a mental model's metadata | +| `delete_mental_model` | Delete a mental model | +| `refresh_mental_model` | Regenerate a mental model's content | -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `content` | string | Yes | The fact or memory to store | -| `context` | string | No | Category for the memory (default: `general`) | +## Environment Variables -**Example:** -```json -{ - "name": "retain", - "arguments": { - "content": "User's favorite color is blue", - "context": "preferences" - } -} -``` +All standard [Hindsight configuration variables](/developer/configuration) are supported. Key ones for local use: -**Response:** -```json -{ - "status": "accepted", - "message": "Memory storage initiated" -} -``` +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `HINDSIGHT_API_LLM_API_KEY` | Yes* | — | API key for your LLM provider | +| `HINDSIGHT_API_LLM_PROVIDER` | No | `openai` | LLM provider (`openai`, `anthropic`, `ollama`, etc.) | +| `HINDSIGHT_API_LLM_MODEL` | No | `gpt-4o-mini` | Model name | +| `HINDSIGHT_API_DATABASE_URL` | No | `pg0://hindsight-mcp` | Override the database URL | +| `HINDSIGHT_API_PORT` | No | `8888` | Port to listen on | +| `HINDSIGHT_API_LOG_LEVEL` | No | `info` | Log level | -### recall - -Search memories to provide personalized responses. - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `query` | string | Yes | Natural language search query | -| `max_tokens` | integer | No | Maximum tokens to return (default: 4096) | - -**Example:** -```json -{ - "name": "recall", - "arguments": { - "query": "What are the user's color preferences?", - "max_tokens": 2048 - } -} -``` - -## How It Works - -The local MCP server: - -1. **Starts an embedded PostgreSQL** (pg0) on an automatically assigned port -2. **Initializes the Hindsight memory engine** with local embeddings -3. **Connects via stdio** to Claude Code using the MCP protocol - -Data is persisted in the pg0 data directory (`~/.pg0/hindsight-mcp/`), so your memories survive restarts. +*Not required when using a local provider like Ollama. ## Troubleshooting -### "HINDSIGHT_API_LLM_API_KEY required" +### Slow first startup -Make sure you've set the API key in your MCP configuration: +The first startup downloads the local embedding model (~100MB) and initializes the database. Subsequent starts are faster. -```json -{ - "env": { - "HINDSIGHT_API_LLM_API_KEY": "sk-..." - } -} +### Port already in use + +Set a different port: + +```bash +HINDSIGHT_API_LLM_API_KEY=sk-... HINDSIGHT_API_PORT=9000 uvx --from hindsight-api hindsight-local-mcp ``` -### Slow startup - -The first startup may take longer as it: -- Downloads the embedding model (~100MB) -- Initializes the PostgreSQL database - -Subsequent starts are faster. +Then update your MCP client URL to `http://localhost:9000/mcp/`. ### Checking logs Set `HINDSIGHT_API_LOG_LEVEL=debug` for verbose output: -```json -{ - "env": { - "HINDSIGHT_API_LOG_LEVEL": "debug" - } -} +```bash +HINDSIGHT_API_LLM_API_KEY=sk-... HINDSIGHT_API_LOG_LEVEL=debug uvx --from hindsight-api hindsight-local-mcp ``` - -Logs are written to stderr and visible in Claude Code's MCP server output.