Skip to content

Commit 1c6acc3

Browse files
authored
feat: simplify mcp installation + ui standalone (#41)
1 parent 8ecb5d3 commit 1c6acc3

19 files changed

Lines changed: 1322 additions & 6096 deletions

File tree

.github/workflows/release.yml

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,47 @@ jobs:
117117
path: hindsight-clients/typescript/*.tgz
118118
retention-days: 1
119119

120+
release-control-plane:
121+
runs-on: ubuntu-latest
122+
environment: npm
123+
124+
steps:
125+
- uses: actions/checkout@v4
126+
127+
- name: Set up Node.js
128+
uses: actions/setup-node@v4
129+
with:
130+
node-version: '20'
131+
registry-url: 'https://registry.npmjs.org'
132+
cache: 'npm'
133+
cache-dependency-path: package-lock.json
134+
135+
- name: Install dependencies
136+
run: npm ci
137+
138+
- name: Build TypeScript client (dependency)
139+
run: npm run build --workspace=hindsight-clients/typescript
140+
141+
- name: Build
142+
run: npm run build --workspace=hindsight-control-plane
143+
144+
- name: Publish to npm
145+
working-directory: ./hindsight-control-plane
146+
run: npm publish --access public
147+
env:
148+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
149+
150+
- name: Pack for GitHub release
151+
working-directory: ./hindsight-control-plane
152+
run: npm pack
153+
154+
- name: Upload artifacts
155+
uses: actions/upload-artifact@v4
156+
with:
157+
name: control-plane
158+
path: hindsight-control-plane/*.tgz
159+
retention-days: 1
160+
120161
release-rust-cli:
121162
runs-on: ${{ matrix.os }}
122163
strategy:
@@ -287,7 +328,7 @@ jobs:
287328

288329
create-github-release:
289330
runs-on: ubuntu-latest
290-
needs: [release-python-packages, release-typescript-client, release-rust-cli, release-docker-images, release-helm-chart]
331+
needs: [release-python-packages, release-typescript-client, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
291332
permissions:
292333
contents: write
293334

@@ -310,6 +351,12 @@ jobs:
310351
name: typescript-client
311352
path: ./artifacts/typescript-client
312353

354+
- name: Download Control Plane
355+
uses: actions/download-artifact@v4
356+
with:
357+
name: control-plane
358+
path: ./artifacts/control-plane
359+
313360
- name: Download Rust CLI (Linux)
314361
uses: actions/download-artifact@v4
315362
with:
@@ -344,6 +391,8 @@ jobs:
344391
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
345392
# TypeScript client
346393
cp artifacts/typescript-client/*.tgz release-assets/ || true
394+
# Control Plane
395+
cp artifacts/control-plane/*.tgz release-assets/ || true
347396
# Rust CLI binaries
348397
cp artifacts/rust-cli-linux/hindsight-linux-amd64 release-assets/ || true
349398
cp artifacts/rust-cli-darwin-amd64/hindsight-darwin-amd64 release-assets/ || true

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,33 @@ jobs:
8080
- name: Build TypeScript client
8181
run: npm run build --workspace=hindsight-clients/typescript
8282

83+
build-control-plane:
84+
runs-on: ubuntu-latest
85+
86+
steps:
87+
- uses: actions/checkout@v4
88+
89+
- name: Set up Node.js
90+
uses: actions/setup-node@v4
91+
with:
92+
node-version: '20'
93+
cache: 'npm'
94+
cache-dependency-path: package-lock.json
95+
96+
- name: Install dependencies
97+
run: npm ci
98+
99+
- name: Build TypeScript client (dependency)
100+
run: npm run build --workspace=hindsight-clients/typescript
101+
102+
- name: Build control plane
103+
run: npm run build --workspace=hindsight-control-plane
104+
105+
- name: Verify standalone build
106+
run: |
107+
test -f hindsight-control-plane/standalone/server.js || exit 1
108+
node hindsight-control-plane/bin/cli.js --help
109+
83110
build-docs:
84111
runs-on: ubuntu-latest
85112

docker/standalone/Dockerfile

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,39 @@ FROM node:20-slim AS cp-builder
7272
ARG INCLUDE_CP
7373
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
7474

75-
WORKDIR /app
76-
77-
# Copy built SDK
78-
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
75+
# Create directory structure matching the monorepo layout
76+
# This is required because build:standalone script expects .next/standalone/memory-poc/hindsight-control-plane
77+
WORKDIR /app/memory-poc/hindsight-control-plane
7978

8079
# Install Control Plane dependencies
8180
# Only copy package.json (not package-lock.json) to ensure npm installs
8281
# correct platform-specific native bindings for lightningcss/tailwindcss
8382
COPY hindsight-control-plane/package.json ./
83+
# Remove the file: dependency on SDK (we'll copy it directly later)
84+
RUN sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
8485
RUN npm install
8586

8687
# Copy Control Plane source (excluding node_modules via .dockerignore)
8788
COPY hindsight-control-plane/ ./
8889
# Remove package-lock.json to avoid conflicts with installed native bindings
89-
RUN rm -f package-lock.json
90+
# Also remove the file: dependency from package.json (restored by COPY above)
91+
RUN rm -f package-lock.json && sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
9092

91-
# Link SDK (temporary for build)
92-
RUN cd /app/sdk && npm link && cd /app && npm link @vectorize-io/hindsight-client
93+
# Copy built SDK directly into node_modules (more reliable than npm link in Docker)
94+
COPY --from=sdk-builder /app/hindsight-clients/typescript ./node_modules/@vectorize-io/hindsight-client
9395

94-
# Build Control Plane
95-
RUN npm run build
96+
# Build Control Plane - run next build first, then custom standalone copy
97+
# (The build:standalone script expects a specific path structure that differs in Docker)
98+
RUN npm exec -- next build
9699

97-
# Create public directory if it doesn't exist
98-
RUN mkdir -p public
100+
# Create standalone directory structure manually
101+
# Next.js standalone output structure varies, so we find server.js and work from there
102+
RUN mkdir -p standalone/.next && \
103+
STANDALONE_ROOT=$(dirname $(find .next/standalone -name "server.js" | head -1)) && \
104+
cp -r "$STANDALONE_ROOT"/* standalone/ && \
105+
cp -r .next/static standalone/.next/static && \
106+
mkdir -p standalone/public && \
107+
cp -r public/* standalone/public/ 2>/dev/null || true
99108

100109
# =============================================================================
101110
# Stage: Final Image - API Only
@@ -172,9 +181,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
172181

173182
# Copy Control Plane standalone build
174183
WORKDIR /app/control-plane
175-
COPY --from=cp-builder /app/.next/standalone ./
176-
COPY --from=cp-builder /app/.next/static ./.next/static
177-
COPY --from=cp-builder /app/public ./public
184+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
185+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
186+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
178187

179188
WORKDIR /app
180189

@@ -226,9 +235,9 @@ COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
226235

227236
# Copy Control Plane standalone build
228237
WORKDIR /app/control-plane
229-
COPY --from=cp-builder /app/.next/standalone ./
230-
COPY --from=cp-builder /app/.next/static ./.next/static
231-
COPY --from=cp-builder /app/public ./public
238+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
239+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
240+
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
232241

233242
WORKDIR /app
234243

hindsight-api/hindsight_api/config.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
3232
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
3333
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
34+
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
3435

3536
# Default values
3637
DEFAULT_DATABASE_URL = "pg0"
@@ -50,6 +51,26 @@
5051
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
5152
DEFAULT_MCP_LOCAL_BANK_ID = "mcp"
5253

54+
# Default MCP tool descriptions (can be customized via env vars)
55+
DEFAULT_MCP_RETAIN_DESCRIPTION = """Store important information to long-term memory.
56+
57+
Use this tool PROACTIVELY whenever the user shares:
58+
- Personal facts, preferences, or interests
59+
- Important events or milestones
60+
- User history, experiences, or background
61+
- Decisions, opinions, or stated preferences
62+
- Goals, plans, or future intentions
63+
- Relationships or people mentioned
64+
- Work context, projects, or responsibilities"""
65+
66+
DEFAULT_MCP_RECALL_DESCRIPTION = """Search memories to provide personalized, context-aware responses.
67+
68+
Use this tool PROACTIVELY to:
69+
- Check user's preferences before making suggestions
70+
- Recall user's history to provide continuity
71+
- Remember user's goals and context
72+
- Personalize responses based on past interactions"""
73+
5374
# Required embedding dimension for database schema
5475
EMBEDDING_DIMENSION = 384
5576

@@ -142,7 +163,9 @@ def get_python_log_level(self) -> int:
142163
def configure_logging(self) -> None:
143164
"""Configure Python logging based on the log level."""
144165
logging.basicConfig(
145-
level=self.get_python_log_level(), format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
166+
level=self.get_python_log_level(),
167+
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
168+
force=True, # Override any existing configuration
146169
)
147170

148171
def log_config(self) -> None:

hindsight-api/hindsight_api/engine/llm_wrapper.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async def call(
172172

173173
# Check if model supports reasoning parameter (o1, o3, gpt-5 families)
174174
model_lower = self.model.lower()
175-
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
175+
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3", "deepseek"])
176176

177177
# For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000
178178
# For GPT-4o models, cap to 16384
@@ -194,7 +194,7 @@ async def call(
194194
call_params["temperature"] = temperature
195195

196196
# Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3)
197-
if is_reasoning_model and self.provider == "openai":
197+
if is_reasoning_model:
198198
call_params["reasoning_effort"] = self.reasoning_effort
199199

200200
# Provider-specific parameters
@@ -203,7 +203,6 @@ async def call(
203203
extra_body = {"service_tier": "auto"}
204204
# Only add reasoning parameters for reasoning models
205205
if is_reasoning_model:
206-
extra_body["reasoning_effort"] = self.reasoning_effort
207206
extra_body["include_reasoning"] = False
208207
call_params["extra_body"] = extra_body
209208

hindsight-api/hindsight_api/engine/retain/orchestrator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ async def retain_batch(
107107
)
108108

109109
if not extracted_facts:
110+
total_time = time.time() - start_time
111+
logger.info(
112+
f"RETAIN_BATCH COMPLETE: 0 facts extracted from {len(contents)} contents in {total_time:.3f}s (nothing to store)"
113+
)
110114
return [[] for _ in contents]
111115

112116
# Apply fact_type_override if provided

hindsight-api/hindsight_api/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,10 @@ def main():
127127
port=args.port,
128128
log_level=args.log_level,
129129
mcp_enabled=config.mcp_enabled,
130+
graph_retriever=config.graph_retriever,
130131
)
131132
config.configure_logging()
133+
config.log_config()
132134

133135
# Register cleanup handlers
134136
atexit.register(_cleanup)

hindsight-api/hindsight_api/mcp_local.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,35 @@
2828
HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai").
2929
HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
3030
HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp").
31-
HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "info").
31+
HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "warning").
32+
HINDSIGHT_API_MCP_INSTRUCTIONS: Optional. Additional instructions appended to both retain and recall tools.
33+
34+
Example custom instructions (these are ADDED to the default behavior):
35+
To also store assistant actions:
36+
HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls, code written, and decisions made."
37+
38+
To also store conversation summaries:
39+
HINDSIGHT_API_MCP_INSTRUCTIONS="Also store summaries of important conversations and their outcomes."
3240
"""
3341

3442
import logging
3543
import os
3644
import sys
3745

3846
from mcp.server.fastmcp import FastMCP
47+
from mcp.types import Icon
3948

4049
from hindsight_api.config import (
4150
DEFAULT_MCP_LOCAL_BANK_ID,
51+
DEFAULT_MCP_RECALL_DESCRIPTION,
52+
DEFAULT_MCP_RETAIN_DESCRIPTION,
53+
ENV_MCP_INSTRUCTIONS,
4254
ENV_MCP_LOCAL_BANK_ID,
4355
)
4456

45-
# Configure logging - default to info
46-
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
57+
# Configure logging - default to warning to avoid polluting stderr during MCP init
58+
# MCP clients interpret stderr output as errors, so we suppress INFO logs by default
59+
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "warning").lower()
4760
_log_level_map = {
4861
"critical": logging.CRITICAL,
4962
"error": logging.ERROR,
@@ -79,22 +92,21 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
7992
if memory is None:
8093
memory = MemoryEngine(db_url="pg0://hindsight-mcp")
8194

95+
# Get custom instructions from environment variable (appended to both tools)
96+
extra_instructions = os.environ.get(ENV_MCP_INSTRUCTIONS, "")
97+
98+
retain_description = DEFAULT_MCP_RETAIN_DESCRIPTION
99+
recall_description = DEFAULT_MCP_RECALL_DESCRIPTION
100+
101+
if extra_instructions:
102+
retain_description = f"{DEFAULT_MCP_RETAIN_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}"
103+
recall_description = f"{DEFAULT_MCP_RECALL_DESCRIPTION}\n\nAdditional instructions: {extra_instructions}"
104+
82105
mcp = FastMCP("hindsight")
83106

84-
@mcp.tool()
107+
@mcp.tool(description=retain_description)
85108
async def retain(content: str, context: str = "general") -> dict:
86109
"""
87-
Store important information to long-term memory.
88-
89-
Use this tool PROACTIVELY whenever the user shares:
90-
- Personal facts, preferences, or interests
91-
- Important events or milestones
92-
- User history, experiences, or background
93-
- Decisions, opinions, or stated preferences
94-
- Goals, plans, or future intentions
95-
- Relationships or people mentioned
96-
- Work context, projects, or responsibilities
97-
98110
Args:
99111
content: The fact/memory to store (be specific and include relevant details)
100112
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
@@ -111,17 +123,9 @@ async def _retain():
111123
asyncio.create_task(_retain())
112124
return {"status": "accepted", "message": "Memory storage initiated"}
113125

114-
@mcp.tool()
126+
@mcp.tool(description=recall_description)
115127
async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
116128
"""
117-
Search memories to provide personalized, context-aware responses.
118-
119-
Use this tool PROACTIVELY to:
120-
- Check user's preferences before making suggestions
121-
- Recall user's history to provide continuity
122-
- Remember user's goals and context
123-
- Personalize responses based on past interactions
124-
125129
Args:
126130
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
127131
max_tokens: Maximum tokens to return in results (default: 4096)
@@ -153,10 +157,9 @@ async def _initialize_and_run(bank_id: str):
153157
from hindsight_api import MemoryEngine
154158

155159
# Create and initialize memory engine with pg0 embedded database
156-
print("Initializing memory engine...", file=sys.stderr)
160+
# Note: We avoid printing to stderr during init as MCP clients show it as "errors"
157161
memory = MemoryEngine(db_url="pg0://hindsight-mcp")
158162
await memory.initialize()
159-
print("Memory engine initialized.", file=sys.stderr)
160163

161164
# Create and run the server
162165
mcp = create_local_mcp_server(bank_id, memory=memory)
@@ -179,8 +182,8 @@ def main():
179182
# Get bank ID from environment, default to "mcp"
180183
bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID)
181184

182-
# Print startup message to stderr (stdout is reserved for MCP protocol)
183-
print(f"Hindsight MCP server starting (bank_id={bank_id})...", file=sys.stderr)
185+
# Note: We don't print to stderr as MCP clients display it as "error output"
186+
# Use HINDSIGHT_API_LOG_LEVEL=debug for verbose startup logging
184187

185188
# Run the async initialization and server
186189
asyncio.run(_initialize_and_run(bank_id))

0 commit comments

Comments
 (0)