feat: simplify mcp installation + ui standalone (#41)

This commit is contained in:
Nicolò Boschi
2025-12-18 10:24:56 +01:00
committed by GitHub
parent 8ecb5d3a0c
commit 1c6acc3ba0
19 changed files with 729 additions and 5503 deletions
+50 -1
View File
@@ -117,6 +117,47 @@ jobs:
path: hindsight-clients/typescript/*.tgz
retention-days: 1
release-control-plane:
runs-on: ubuntu-latest
environment: npm
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build TypeScript client (dependency)
run: npm run build --workspace=hindsight-clients/typescript
- name: Build
run: npm run build --workspace=hindsight-control-plane
- name: Publish to npm
working-directory: ./hindsight-control-plane
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Pack for GitHub release
working-directory: ./hindsight-control-plane
run: npm pack
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: control-plane
path: hindsight-control-plane/*.tgz
retention-days: 1
release-rust-cli:
runs-on: ${{ matrix.os }}
strategy:
@@ -287,7 +328,7 @@ jobs:
create-github-release:
runs-on: ubuntu-latest
needs: [release-python-packages, release-typescript-client, release-rust-cli, release-docker-images, release-helm-chart]
needs: [release-python-packages, release-typescript-client, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
permissions:
contents: write
@@ -310,6 +351,12 @@ jobs:
name: typescript-client
path: ./artifacts/typescript-client
- name: Download Control Plane
uses: actions/download-artifact@v4
with:
name: control-plane
path: ./artifacts/control-plane
- name: Download Rust CLI (Linux)
uses: actions/download-artifact@v4
with:
@@ -344,6 +391,8 @@ jobs:
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
# TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true
# Control Plane
cp artifacts/control-plane/*.tgz release-assets/ || true
# Rust CLI binaries
cp artifacts/rust-cli-linux/hindsight-linux-amd64 release-assets/ || true
cp artifacts/rust-cli-darwin-amd64/hindsight-darwin-amd64 release-assets/ || true
+27
View File
@@ -80,6 +80,33 @@ jobs:
- name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript
build-control-plane:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build TypeScript client (dependency)
run: npm run build --workspace=hindsight-clients/typescript
- name: Build control plane
run: npm run build --workspace=hindsight-control-plane
- name: Verify standalone build
run: |
test -f hindsight-control-plane/standalone/server.js || exit 1
node hindsight-control-plane/bin/cli.js --help
build-docs:
runs-on: ubuntu-latest
+26 -17
View File
@@ -72,30 +72,39 @@ FROM node:20-slim AS cp-builder
ARG INCLUDE_CP
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
WORKDIR /app
# Copy built SDK
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Create directory structure matching the monorepo layout
# This is required because build:standalone script expects .next/standalone/memory-poc/hindsight-control-plane
WORKDIR /app/memory-poc/hindsight-control-plane
# Install Control Plane dependencies
# Only copy package.json (not package-lock.json) to ensure npm installs
# correct platform-specific native bindings for lightningcss/tailwindcss
COPY hindsight-control-plane/package.json ./
# Remove the file: dependency on SDK (we'll copy it directly later)
RUN sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
RUN npm install
# Copy Control Plane source (excluding node_modules via .dockerignore)
COPY hindsight-control-plane/ ./
# Remove package-lock.json to avoid conflicts with installed native bindings
RUN rm -f package-lock.json
# Also remove the file: dependency from package.json (restored by COPY above)
RUN rm -f package-lock.json && sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
# Link SDK (temporary for build)
RUN cd /app/sdk && npm link && cd /app && npm link @vectorize-io/hindsight-client
# Copy built SDK directly into node_modules (more reliable than npm link in Docker)
COPY --from=sdk-builder /app/hindsight-clients/typescript ./node_modules/@vectorize-io/hindsight-client
# Build Control Plane
RUN npm run build
# Build Control Plane - run next build first, then custom standalone copy
# (The build:standalone script expects a specific path structure that differs in Docker)
RUN npm exec -- next build
# Create public directory if it doesn't exist
RUN mkdir -p public
# Create standalone directory structure manually
# Next.js standalone output structure varies, so we find server.js and work from there
RUN mkdir -p standalone/.next && \
STANDALONE_ROOT=$(dirname $(find .next/standalone -name "server.js" | head -1)) && \
cp -r "$STANDALONE_ROOT"/* standalone/ && \
cp -r .next/static standalone/.next/static && \
mkdir -p standalone/public && \
cp -r public/* standalone/public/ 2>/dev/null || true
# =============================================================================
# Stage: Final Image - API Only
@@ -172,9 +181,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
COPY --from=cp-builder /app/.next/standalone ./
COPY --from=cp-builder /app/.next/static ./.next/static
COPY --from=cp-builder /app/public ./public
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app
@@ -226,9 +235,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
COPY --from=cp-builder /app/.next/standalone ./
COPY --from=cp-builder /app/.next/static ./.next/static
COPY --from=cp-builder /app/public ./public
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app
+24 -1
View File
@@ -31,6 +31,7 @@ ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
# Default values
DEFAULT_DATABASE_URL = "pg0"
@@ -50,6 +51,26 @@ DEFAULT_MCP_ENABLED = True
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
DEFAULT_MCP_LOCAL_BANK_ID = "mcp"
# Default MCP tool descriptions (can be customized via env vars)
DEFAULT_MCP_RETAIN_DESCRIPTION = """Store important information to long-term memory.
Use this tool PROACTIVELY whenever the user shares:
- Personal facts, preferences, or interests
- Important events or milestones
- User history, experiences, or background
- Decisions, opinions, or stated preferences
- Goals, plans, or future intentions
- Relationships or people mentioned
- Work context, projects, or responsibilities"""
DEFAULT_MCP_RECALL_DESCRIPTION = """Search memories to provide personalized, context-aware responses.
Use this tool PROACTIVELY to:
- Check user's preferences before making suggestions
- Recall user's history to provide continuity
- Remember user's goals and context
- Personalize responses based on past interactions"""
# Required embedding dimension for database schema
EMBEDDING_DIMENSION = 384
@@ -142,7 +163,9 @@ class HindsightConfig:
def configure_logging(self) -> None:
"""Configure Python logging based on the log level."""
logging.basicConfig(
level=self.get_python_log_level(), format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
level=self.get_python_log_level(),
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
force=True, # Override any existing configuration
)
def log_config(self) -> None:
@@ -172,7 +172,7 @@ class LLMProvider:
# Check if model supports reasoning parameter (o1, o3, gpt-5 families)
model_lower = self.model.lower()
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3", "deepseek"])
# For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000
# For GPT-4o models, cap to 16384
@@ -194,7 +194,7 @@ class LLMProvider:
call_params["temperature"] = temperature
# Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3)
if is_reasoning_model and self.provider == "openai":
if is_reasoning_model:
call_params["reasoning_effort"] = self.reasoning_effort
# Provider-specific parameters
@@ -203,7 +203,6 @@ class LLMProvider:
extra_body = {"service_tier": "auto"}
# Only add reasoning parameters for reasoning models
if is_reasoning_model:
extra_body["reasoning_effort"] = self.reasoning_effort
extra_body["include_reasoning"] = False
call_params["extra_body"] = extra_body
@@ -107,6 +107,10 @@ async def retain_batch(
)
if not extracted_facts:
total_time = time.time() - start_time
logger.info(
f"RETAIN_BATCH COMPLETE: 0 facts extracted from {len(contents)} contents in {total_time:.3f}s (nothing to store)"
)
return [[] for _ in contents]
# Apply fact_type_override if provided
+2
View File
@@ -127,8 +127,10 @@ def main():
port=args.port,
log_level=args.log_level,
mcp_enabled=config.mcp_enabled,
graph_retriever=config.graph_retriever,
)
config.configure_logging()
config.log_config()
# Register cleanup handlers
atexit.register(_cleanup)
+31 -28
View File
@@ -28,7 +28,15 @@ Environment variables:
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: "info").
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."
"""
import logging
@@ -36,14 +44,19 @@ import os
import sys
from mcp.server.fastmcp import FastMCP
from mcp.types import Icon
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,
)
# Configure logging - default to info
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
# 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,
@@ -79,22 +92,21 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
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")
@mcp.tool()
@mcp.tool(description=retain_description)
async def retain(content: str, context: str = "general") -> dict:
"""
Store important information to long-term memory.
Use this tool PROACTIVELY whenever the user shares:
- Personal facts, preferences, or interests
- Important events or milestones
- User history, experiences, or background
- Decisions, opinions, or stated preferences
- Goals, plans, or future intentions
- Relationships or people mentioned
- Work context, projects, or responsibilities
Args:
content: The fact/memory to store (be specific and include relevant details)
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
@@ -111,17 +123,9 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
asyncio.create_task(_retain())
return {"status": "accepted", "message": "Memory storage initiated"}
@mcp.tool()
@mcp.tool(description=recall_description)
async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
"""
Search memories to provide personalized, context-aware responses.
Use this tool PROACTIVELY to:
- Check user's preferences before making suggestions
- Recall user's history to provide continuity
- Remember user's goals and context
- Personalize responses based on past interactions
Args:
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
max_tokens: Maximum tokens to return in results (default: 4096)
@@ -153,10 +157,9 @@ async def _initialize_and_run(bank_id: str):
from hindsight_api import MemoryEngine
# Create and initialize memory engine with pg0 embedded database
print("Initializing memory engine...", file=sys.stderr)
# 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()
print("Memory engine initialized.", file=sys.stderr)
# Create and run the server
mcp = create_local_mcp_server(bank_id, memory=memory)
@@ -179,8 +182,8 @@ def main():
# Get bank ID from environment, default to "mcp"
bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID)
# Print startup message to stderr (stdout is reserved for MCP protocol)
print(f"Hindsight MCP server starting (bank_id={bank_id})...", file=sys.stderr)
# 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))
+56
View File
@@ -91,6 +91,9 @@ enum Commands {
#[command(alias = "tui")]
Explore,
/// Launch the web-based control plane UI
Ui,
/// Configure the CLI (API URL, etc.)
#[command(after_help = "Configuration priority:\n 1. Environment variable (HINDSIGHT_API_URL) - highest priority\n 2. Config file (~/.hindsight/config)\n 3. Default (http://localhost:8888)")]
Configure {
@@ -373,6 +376,11 @@ fn run() -> Result<()> {
return handle_configure(api_url, output_format);
}
// Handle ui command - needs config but not API client
if let Commands::Ui = cli.command {
return handle_ui(output_format);
}
// Load configuration
let config = Config::from_env().unwrap_or_else(|e| {
ui::print_error(&format!("Configuration error: {}", e));
@@ -390,6 +398,7 @@ fn run() -> Result<()> {
// Execute command and handle errors
let result: Result<()> = match cli.command {
Commands::Configure { .. } => unreachable!(), // Handled above
Commands::Ui => unreachable!(), // Handled above
Commands::Explore => commands::explore::run(&client),
Commands::Bank(bank_cmd) => match bank_cmd {
BankCommands::List => commands::bank::list(&client, verbose, output_format),
@@ -521,3 +530,50 @@ fn handle_configure(api_url: Option<String>, output_format: OutputFormat) -> Res
Ok(())
}
fn handle_ui(output_format: OutputFormat) -> Result<()> {
use std::process::Command;
// Load configuration to get the API URL
let config = Config::load().unwrap_or_else(|e| {
ui::print_error(&format!("Configuration error: {}", e));
errors::print_config_help();
std::process::exit(1);
});
let api_url = config.api_url();
if output_format == OutputFormat::Pretty {
ui::print_info("Launching Hindsight Control Plane UI...");
println!();
println!(" API URL: {}", api_url);
println!();
}
// Run npx @vectorize-io/hindsight-control-plane --api-url {api_url}
let status = Command::new("npx")
.arg("@vectorize-io/hindsight-control-plane")
.arg("--api-url")
.arg(api_url)
.status();
match status {
Ok(exit_status) => {
if !exit_status.success() {
if let Some(code) = exit_status.code() {
std::process::exit(code);
} else {
std::process::exit(1);
}
}
}
Err(e) => {
ui::print_error(&format!("Failed to launch control plane UI: {}", e));
ui::print_info("Make sure you have Node.js and npm installed.");
ui::print_info("You can also install the control plane globally: npm install -g @vectorize-io/hindsight-control-plane");
std::process::exit(1);
}
}
Ok(())
}
+1
View File
@@ -14,6 +14,7 @@
# production
/build
/standalone
# misc
.DS_Store
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env node
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const args = process.argv.slice(2);
// Parse command line arguments
let port = process.env.PORT || 9999;
let hostname = process.env.HOSTNAME || '0.0.0.0';
let apiUrl = process.env.HINDSIGHT_CP_DATAPLANE_API_URL;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' || args[i] === '-p') {
port = args[++i];
} else if (args[i] === '--hostname' || args[i] === '-H') {
hostname = args[++i];
} else if (args[i] === '--api-url' || args[i] === '-a') {
apiUrl = args[++i];
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Hindsight Control Plane
Usage: hindsight-control-plane [options]
Options:
-p, --port <port> Port to listen on (default: 9999, env: PORT)
-H, --hostname <host> Hostname to bind to (default: 0.0.0.0, env: HOSTNAME)
-a, --api-url <url> Hindsight API URL (env: HINDSIGHT_CP_DATAPLANE_API_URL)
-h, --help Show this help message
Environment Variables:
PORT Port to listen on
HOSTNAME Hostname to bind to
HINDSIGHT_CP_DATAPLANE_API_URL URL of the Hindsight API server
`);
process.exit(0);
}
}
// Find the standalone server
const standaloneDir = path.join(__dirname, '..', 'standalone');
const serverPath = path.join(standaloneDir, 'server.js');
if (!fs.existsSync(serverPath)) {
console.error('Error: Standalone server not found at', serverPath);
console.error('This package may not have been built correctly.');
process.exit(1);
}
// Set up environment
const env = {
...process.env,
PORT: String(port),
HOSTNAME: hostname,
};
if (apiUrl) {
env.HINDSIGHT_CP_DATAPLANE_API_URL = apiUrl;
}
console.log(`Starting Hindsight Control Plane on http://${hostname}:${port}`);
if (apiUrl) {
console.log(`API URL: ${apiUrl}`);
}
// Run the standalone server
const server = spawn('node', [serverPath], {
cwd: standaloneDir,
env,
stdio: 'inherit',
});
server.on('error', (err) => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
server.on('close', (code) => {
process.exit(code || 0);
});
// Handle signals
process.on('SIGTERM', () => server.kill('SIGTERM'));
process.on('SIGINT', () => server.kill('SIGINT'));
+16 -7
View File
@@ -1,17 +1,26 @@
{
"name": "hindsight-control-plane",
"name": "@vectorize-io/hindsight-control-plane",
"version": "0.1.8",
"private": true,
"description": "Control plane for Hindsight - Semantic memory system",
"bin": {
"hindsight-control-plane": "./bin/cli.js"
},
"files": [
"bin",
"standalone",
"public"
],
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build && npm run build:standalone",
"build:standalone": "rm -rf standalone && STANDALONE_ROOT=$(dirname $(find .next/standalone -name 'server.js' | head -1)) && cp -r \"$STANDALONE_ROOT\" standalone && mkdir -p standalone/.next && cp -r .next/static standalone/.next/static && mkdir -p standalone/public && cp -r public/* standalone/public/ 2>/dev/null || true",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prepublishOnly": "npm run build"
},
"keywords": [],
"keywords": ["hindsight", "memory", "semantic", "ai"],
"author": "Hindsight Team",
"license": "ISC",
"description": "Control plane for Hindsight - Semantic memory system",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@@ -27,7 +36,6 @@
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -50,6 +58,7 @@
"typescript": "^5.9.3"
},
"devDependencies": {
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
@@ -133,6 +133,38 @@ hindsight-api --workers 4 # Multiple worker processes
hindsight-api --log-level debug # Verbose logging
```
### Control Plane
The Control Plane (Web UI) can be run standalone using npx:
```bash
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888
```
This connects to your running API server and provides a visual interface for managing memory banks, exploring entities, and testing queries.
#### Options
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
| `-p, --port` | `PORT` | 9999 | Port to listen on |
| `-H, --hostname` | `HOSTNAME` | 0.0.0.0 | Hostname to bind to |
| `-a, --api-url` | `HINDSIGHT_CP_DATAPLANE_API_URL` | http://localhost:8888 | Hindsight API URL |
#### Examples
```bash
# Run on custom port
npx @vectorize-io/hindsight-control-plane --port 9999 --api-url http://localhost:8888
# Using environment variables
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com
npx @vectorize-io/hindsight-control-plane
# Production deployment
PORT=80 HINDSIGHT_CP_DATAPLANE_API_URL=https://api.hindsight.io npx @vectorize-io/hindsight-control-plane
```
---
## Next Steps
+3 -5
View File
@@ -25,12 +25,10 @@ Web UI for managing and exploring your memory banks:
- View ingestion history and operations
- Test recall queries interactively
```
hindsight-control-plane # Default port: 9999
```
The Control Plane connects to the API service and provides a visual interface for development and debugging.
For bare metal deployments, you can run the Control Plane standalone using npx. See [Installation - Bare Metal](./installation#control-plane) for details.
## Deployment Options
| Deployment | Services | Use Case |
@@ -39,4 +37,4 @@ The Control Plane connects to the API service and provides a visual interface fo
| **Helm / Kubernetes** | Separate pods | Production, scaling |
| **Bare metal** | Run independently | Custom deployments |
In the Docker quickstart, both services run in a single container. For production Kubernetes deployments, they run as separate pods with independent scaling.
In the Docker quickstart, both services run in a single container. For production Kubernetes deployments, they run as separate pods with independent scaling. For bare metal, you can run the API via pip and the Control Plane via npx.
+19
View File
@@ -177,6 +177,25 @@ hindsight memory recall <bank_id> "query" -o yaml
| `--help` | Show help |
| `--version` | Show version |
## Control Plane UI
Launch the web-based Control Plane UI directly from the CLI:
```bash
hindsight ui
```
This runs the Control Plane locally on port 9999 using the API URL from your configuration. The UI provides:
- **Memory bank management** — Browse and manage all your banks
- **Entity explorer** — Visualize the knowledge graph
- **Query testing** — Interactive recall and reflect testing
- **Operation history** — View ingestion and processing logs
:::tip
The UI command requires Node.js to be installed. It automatically downloads and runs the `@vectorize-io/hindsight-control-plane` package via npx.
:::
## Interactive Explorer
Launch the TUI explorer for visual navigation of your memory banks:
@@ -7,28 +7,35 @@ sidebar_position: 2
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.
This is ideal for:
- **Personal use with Claude Code** — Give Claude long-term memory across conversations
- **Personal use with 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 Start
### With uvx (recommended)
## Quick Install
```bash
uvx --from hindsight-api hindsight-local-mcp
curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
--app claude-desktop \
--set HINDSIGHT_API_LLM_API_KEY=sk-...
```
### With pip
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
```bash
pip install hindsight-api
hindsight-local-mcp
```
:::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.
:::
## Claude Code Configuration
## Manual Configuration
Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`):
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
{
@@ -37,7 +44,7 @@ Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`):
"command": "uvx",
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": {
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key"
"HINDSIGHT_API_LLM_API_KEY": "sk-..."
}
}
}
@@ -55,7 +62,7 @@ By default, memories are stored in a bank called `mcp`. To use a different bank:
"command": "uvx",
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": {
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key",
"HINDSIGHT_API_LLM_API_KEY": "sk-...",
"HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory"
}
}
@@ -72,6 +79,20 @@ All standard [Hindsight configuration variables](/developer/configuration) are s
| 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:
```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."
```
These instructions are appended to the default tool descriptions, guiding Claude on when and how to use the memory tools.
## Available Tools
+301
View File
@@ -0,0 +1,301 @@
#!/bin/bash
#
# Install Hindsight MCP server for Claude Desktop
#
# Usage:
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- --app claude-desktop --set HINDSIGHT_API_LLM_API_KEY=YOUR_KEY
#
# Options:
# --app Required. Target application (currently only: claude-desktop)
# --set ENV=VALUE Set environment variable (can be repeated)
#
# Examples:
# # With OpenAI
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
# --app claude-desktop \
# --set HINDSIGHT_API_LLM_API_KEY=sk-...
#
# # With Ollama (local LLM)
# curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
# --app claude-desktop \
# --set HINDSIGHT_API_LLM_PROVIDER=ollama \
# --set HINDSIGHT_API_LLM_MODEL=llama3.2
#
# # With custom memory instructions
# 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 and code you write."
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_info() {
echo -e "${BLUE}${NC} $1"
}
print_success() {
echo -e "${GREEN}✓${NC} $1"
}
print_error() {
echo -e "${RED}✗${NC} $1"
exit 1
}
print_warning() {
echo -e "${YELLOW}⚠${NC} $1"
}
print_banner() {
echo ""
# ANSI logo
echo -e " \033[38;2;9;127;184m▄\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m▄\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m▄\033[0m\033[38;2;7;140;156m▄\033[0m "
echo -e " \033[38;2;8;125;192m▄\033[0m \033[38;2;3;132;191m▀\033[0m\033[38;2;2;133;192m▄\033[0m \033[38;2;3;132;180m▄\033[0m\033[38;2;1;137;184m▄\033[0m\033[38;2;3;133;174m▄\033[0m \033[38;2;3;142;176m▄\033[0m\033[38;2;4;142;169m▀\033[0m \033[38;2;10;144;164m▄\033[0m "
echo -e "\033[38;2;6;121;195m▀\033[0m\033[38;2;5;128;203m▀\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m▄\033[0m\033[38;2;2;126;196m▄\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m▄\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m▄\033[0m\033[38;2;1;141;196m▀\033[0m\033[38;2;1;135;183m▀\033[0m\033[38;2;1;148;198m▀\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m▄\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m▄\033[0m\033[38;2;3;138;173m▄\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m▄\033[0m\033[38;2;7;144;169m▀\033[0m\033[38;2;7;139;158m▀\033[0m"
echo -e " \033[48;2;2;128;202m\033[38;2;2;124;201m▄\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m▄\033[0m\033[38;2;2;128;196m▄\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m▄\033[0m \033[38;2;1;135;186m▄\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m▄\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m▄\033[0m "
echo -e " \033[48;2;8;118;200m\033[38;2;8;121;209m▄\033[0m\033[38;2;3;121;203m▀\033[0m \033[38;2;3;122;192m▀\033[0m\033[38;2;1;138;216m▀\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m▄\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m▄\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m▄\033[0m\033[38;2;1;140;196m▀\033[0m \033[38;2;4;134;175m▀\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m▄\033[0m "
echo ""
echo -e " ${BLUE}HINDSIGHT MCP INSTALLER${NC}"
echo ""
}
# Parse arguments
APP=""
declare -a ENV_VARS=()
while [[ $# -gt 0 ]]; do
case $1 in
--app)
APP="$2"
shift 2
;;
--set)
ENV_VARS+=("$2")
shift 2
;;
-h|--help)
echo "Usage: $0 --app <app> --set ENV=VALUE [--set ENV2=VALUE2 ...]"
echo ""
echo "Options:"
echo " --app Required. Target application (currently only: claude-desktop)"
echo " --set ENV=VALUE Set environment variable (can be repeated)"
echo ""
echo "Examples:"
echo " # With OpenAI"
echo " $0 --app claude-desktop --set HINDSIGHT_API_LLM_API_KEY=sk-..."
echo ""
echo " # With Ollama (local LLM, no API key needed)"
echo " $0 --app claude-desktop --set HINDSIGHT_API_LLM_PROVIDER=ollama --set HINDSIGHT_API_LLM_MODEL=llama3.2"
exit 0
;;
*)
print_error "Unknown option: $1. Use --help for usage."
;;
esac
done
# Validate required arguments
if [ -z "$APP" ]; then
print_error "Missing required argument: --app. Use --help for usage."
fi
if [ "$APP" != "claude-desktop" ]; then
print_error "Unsupported app: $APP. Currently only 'claude-desktop' is supported."
fi
# Detect OS
detect_os() {
case "$(uname -s)" in
Darwin*) echo "macos" ;;
Linux*) echo "linux" ;;
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
OS=$(detect_os)
# Get Claude Desktop config path based on OS
get_claude_config_path() {
case "$OS" in
macos)
echo "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
;;
linux)
echo "$HOME/.config/Claude/claude_desktop_config.json"
;;
windows)
echo "$APPDATA/Claude/claude_desktop_config.json"
;;
*)
print_error "Unsupported operating system: $OS"
;;
esac
}
# Check if uvx is installed and return its path
find_uvx() {
# Check if in PATH
if command -v uvx &> /dev/null; then
command -v uvx
return 0
fi
# Check common installation paths
local paths=(
"$HOME/.local/bin/uvx"
"$HOME/.cargo/bin/uvx"
"/usr/local/bin/uvx"
)
for path in "${paths[@]}"; do
if [ -f "$path" ]; then
echo "$path"
return 0
fi
done
return 1
}
# Install uv (which includes uvx)
install_uv() {
print_info "Installing uv..."
if [ "$OS" = "windows" ]; then
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
else
curl -LsSf https://astral.sh/uv/install.sh | sh
fi
# Source the env to get uvx in path
if [ -f "$HOME/.local/bin/env" ]; then
source "$HOME/.local/bin/env"
fi
# Find uvx again
if ! UVX_PATH=$(find_uvx); then
print_error "uv installed but uvx not found. Please check your installation."
fi
print_success "uv installed successfully"
}
# Update Claude Desktop config
update_claude_config() {
local config_path="$1"
local uvx_path="$2"
shift 2
local env_vars=("$@")
# Create config directory if it doesn't exist
mkdir -p "$(dirname "$config_path")"
# Check if jq is available (needed for both new and existing configs)
if ! command -v jq &> /dev/null; then
print_warning "jq not found. Installing..."
if [ "$OS" = "macos" ]; then
if command -v brew &> /dev/null; then
brew install jq
else
print_error "Please install jq: brew install jq"
fi
elif [ "$OS" = "linux" ]; then
if command -v apt-get &> /dev/null; then
sudo apt-get install -y jq
elif command -v yum &> /dev/null; then
sudo yum install -y jq
else
print_error "Please install jq manually"
fi
fi
fi
# Build the env object from env_vars array
local env_json="{}"
for env_var in "${env_vars[@]}"; do
local key="${env_var%%=*}"
local value="${env_var#*=}"
env_json=$(echo "$env_json" | jq --arg k "$key" --arg v "$value" '. + {($k): $v}')
done
# Build the hindsight server config
local hindsight_config
hindsight_config=$(jq -n \
--arg uvx "$uvx_path" \
--argjson env "$env_json" \
'{
"command": $uvx,
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
"env": $env
}')
# Check if config file exists and has content
if [ -f "$config_path" ] && [ -s "$config_path" ]; then
print_info "Updating existing Claude Desktop config..."
# Backup existing config
cp "$config_path" "${config_path}.backup"
print_info "Backed up existing config to ${config_path}.backup"
# Add or update hindsight server in existing config
local new_config
new_config=$(jq --argjson hs "$hindsight_config" '.mcpServers.hindsight = $hs' "$config_path")
echo "$new_config" > "$config_path"
else
print_info "Creating new Claude Desktop config..."
# Create new config with hindsight server
local new_config
new_config=$(jq -n --argjson hs "$hindsight_config" '{"mcpServers": {"hindsight": $hs}}')
echo "$new_config" > "$config_path"
fi
print_success "Claude Desktop config updated: $config_path"
}
# Main installation flow
main() {
print_banner
print_info "App: $APP"
if [ ${#ENV_VARS[@]} -gt 0 ]; then
print_info "Environment variables: ${#ENV_VARS[@]} configured"
fi
echo ""
# Step 1: Check/Install uvx
print_info "Checking for uvx..."
if UVX_PATH=$(find_uvx); then
print_success "uvx found at: $UVX_PATH"
else
print_warning "uvx not found. Installing uv..."
install_uv
UVX_PATH=$(find_uvx)
fi
# Step 2: Update Claude Desktop config
CONFIG_PATH=$(get_claude_config_path)
print_info "Configuring Claude Desktop..."
update_claude_config "$CONFIG_PATH" "$UVX_PATH" "${ENV_VARS[@]}"
# Done!
echo ""
print_success "Installation complete!"
echo ""
print_info "Next steps:"
echo " 1. Restart Claude Desktop"
echo " 2. Look for the 'hindsight' tools (retain, recall) in Claude"
echo ""
}
main "$@"
+12 -5425
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@
set -e
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR/hindsight-control-plane" || exit 1
cd "$ROOT_DIR" || exit 1
# Check if .env exists in workspace root
if [ ! -f "$ROOT_DIR/.env" ]; then
@@ -30,4 +30,4 @@ fi
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
# Run dev server
npm run dev -w hindsight-control-plane
npm run dev -w @vectorize-io/hindsight-control-plane