hebbrix-mcp 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hebbrix_mcp/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Hebbrix MCP server — memory + knowledge graph tools for any MCP-compatible agent.
|
|
2
|
+
|
|
3
|
+
Tool surface (14 tools + a profile resource + a context prompt):
|
|
4
|
+
Memory: hebbrix_remember, hebbrix_search, hebbrix_get, hebbrix_update,
|
|
5
|
+
hebbrix_forget, hebbrix_list, hebbrix_history
|
|
6
|
+
Graph: hebbrix_search_entities, hebbrix_entity_timeline,
|
|
7
|
+
hebbrix_graph_query, hebbrix_contradictions
|
|
8
|
+
Reason: hebbrix_confidence, hebbrix_log_decision
|
|
9
|
+
Scope: hebbrix_list_collections
|
|
10
|
+
|
|
11
|
+
Transports: stdio (default) and streamable-http (--transport streamable-http).
|
|
12
|
+
|
|
13
|
+
AGENT MODE (accountless): with no HEBBRIX_API_KEY and no saved credentials,
|
|
14
|
+
the server mints a shadow account automatically (POST /v1/agent-signup) and
|
|
15
|
+
starts in <10s — no email, no dashboard. Every tool result then carries a
|
|
16
|
+
`hebbrix_usage` block (tier, limits, expiry, claim command) so the agent can
|
|
17
|
+
tell its human when to claim. `hebbrix-mcp claim --email <you>` upgrades to
|
|
18
|
+
the free monthly tier with the same key and all memories intact.
|
|
19
|
+
|
|
20
|
+
Configure with env vars:
|
|
21
|
+
HEBBRIX_API_KEY — optional (agent mode mints one), your bearer token
|
|
22
|
+
HEBBRIX_COLLECTION_ID — optional, default collection for new memories
|
|
23
|
+
HEBBRIX_API_BASE — optional, default https://api.hebbrix.com/v1
|
|
24
|
+
HEBBRIX_MCP_HOST/PORT — optional, streamable-http bind (default 127.0.0.1:8080)
|
|
25
|
+
HEBBRIX_CONFIG — optional, credentials path (default ~/.hebbrix/config.json)
|
|
26
|
+
"""
|
|
27
|
+
from .server import mcp, run
|
|
28
|
+
|
|
29
|
+
__version__ = "0.3.1"
|
|
30
|
+
__all__ = ["mcp", "run", "__version__"]
|
hebbrix_mcp/server.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hebbrix MCP Server — long-term memory + knowledge graph for any MCP agent.
|
|
3
|
+
|
|
4
|
+
This exposes Hebbrix as a rich tool surface: memory CRUD with version history,
|
|
5
|
+
a temporal knowledge graph (entities, timelines, relationships, contradictions),
|
|
6
|
+
and a reasoning layer (act-confidence + decision logging) that no plain memory
|
|
7
|
+
store has.
|
|
8
|
+
|
|
9
|
+
Transports (choose at launch, see run()):
|
|
10
|
+
- stdio local: Claude Desktop, Cline, Cursor, Continue
|
|
11
|
+
- streamable-http remote/self-hosted: point clients at the URL
|
|
12
|
+
|
|
13
|
+
Configured via env vars:
|
|
14
|
+
HEBBRIX_API_KEY required, Bearer token (get one at hebbrix.com)
|
|
15
|
+
HEBBRIX_API_BASE optional, default https://api.hebbrix.com/v1
|
|
16
|
+
HEBBRIX_COLLECTION_ID optional, default collection for writes/reads
|
|
17
|
+
HEBBRIX_MCP_HOST optional, default 127.0.0.1 (streamable-http only)
|
|
18
|
+
HEBBRIX_MCP_PORT optional, default 8080 (streamable-http only)
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from contextvars import ContextVar
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
from mcp.server.fastmcp import FastMCP
|
|
31
|
+
|
|
32
|
+
# Multi-tenant (hosted) mode: each HTTP request's own Authorization header is
|
|
33
|
+
# the key, so ONE deployed instance serves many users (Supermemory/Mem0
|
|
34
|
+
# pattern). Set per-request by _HeaderAuthMiddleware; empty = use global KEY.
|
|
35
|
+
_REQUEST_KEY: ContextVar[str] = ContextVar("hebbrix_request_key", default="")
|
|
36
|
+
|
|
37
|
+
BASE = os.environ.get("HEBBRIX_API_BASE", "https://api.hebbrix.com/v1").rstrip("/")
|
|
38
|
+
KEY = os.environ.get("HEBBRIX_API_KEY", "")
|
|
39
|
+
DEFAULT_COLLECTION = os.environ.get("HEBBRIX_COLLECTION_ID", "")
|
|
40
|
+
HOST = os.environ.get("HEBBRIX_MCP_HOST", "127.0.0.1")
|
|
41
|
+
PORT = int(os.environ.get("HEBBRIX_MCP_PORT", "8080"))
|
|
42
|
+
|
|
43
|
+
# Saved credentials from a previous auto-provision (agent mode). Env vars win.
|
|
44
|
+
CONFIG_PATH = Path(os.environ.get("HEBBRIX_CONFIG", "~/.hebbrix/config.json")).expanduser()
|
|
45
|
+
|
|
46
|
+
# Usage snapshot from the most recent API response's X-Hebbrix-* headers.
|
|
47
|
+
# Attached to every tool result so the AGENT sees tier/limits/expiry and can
|
|
48
|
+
# relay the claim command to its human at the right moment.
|
|
49
|
+
_LAST_USAGE: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
# A server-level instructions block teaches the model the data model and when to
|
|
52
|
+
# reach for each tool. This is the single cheapest lever on agent behavior.
|
|
53
|
+
INSTRUCTIONS = """\
|
|
54
|
+
Hebbrix is a long-term memory and knowledge-graph service for this agent.
|
|
55
|
+
|
|
56
|
+
The data model:
|
|
57
|
+
- MEMORIES are atomic facts, decisions, and preferences. They have an id, are
|
|
58
|
+
versioned (edits keep history), and are scoped to a COLLECTION (a tenant/space).
|
|
59
|
+
- The KNOWLEDGE GRAPH is entities (people, orgs, tools, places) connected by typed,
|
|
60
|
+
time-stamped relationships extracted from memories. It answers "who/what relates
|
|
61
|
+
to whom" and "what was true when."
|
|
62
|
+
- The REASONING layer scores how confident the agent should be before acting, and
|
|
63
|
+
records decision outcomes so future confidence improves.
|
|
64
|
+
|
|
65
|
+
How to use it well:
|
|
66
|
+
- Call hebbrix_search BEFORE answering anything that depends on prior context,
|
|
67
|
+
decisions, or user preferences. Do not guess when memory can tell you.
|
|
68
|
+
- Call hebbrix_remember whenever the user shares a durable fact, decision, or
|
|
69
|
+
preference. Prefer one clear fact per call.
|
|
70
|
+
- To correct a stored fact, hebbrix_update it (keeps history) rather than
|
|
71
|
+
remembering a contradicting copy.
|
|
72
|
+
- For "who/what/when" questions about entities, use hebbrix_search_entities,
|
|
73
|
+
hebbrix_entity_timeline, or hebbrix_graph_query, not plain search.
|
|
74
|
+
- Before a consequential autonomous action, call hebbrix_confidence, then log the
|
|
75
|
+
result with hebbrix_log_decision so the system learns.
|
|
76
|
+
All content stays scoped to the configured collection unless you pass collection_id.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
mcp = FastMCP("hebbrix", instructions=INSTRUCTIONS, host=HOST, port=PORT)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------------------- #
|
|
83
|
+
# Credentials: env var > saved config > auto-provision (agent mode) #
|
|
84
|
+
# --------------------------------------------------------------------------- #
|
|
85
|
+
def _load_saved_credentials() -> bool:
|
|
86
|
+
"""Fill KEY/DEFAULT_COLLECTION from ~/.hebbrix/config.json (env wins)."""
|
|
87
|
+
global KEY, DEFAULT_COLLECTION
|
|
88
|
+
try:
|
|
89
|
+
cfg = json.loads(CONFIG_PATH.read_text())
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
if not KEY and cfg.get("api_key"):
|
|
93
|
+
KEY = cfg["api_key"]
|
|
94
|
+
if not DEFAULT_COLLECTION and cfg.get("collection_id"):
|
|
95
|
+
DEFAULT_COLLECTION = cfg["collection_id"]
|
|
96
|
+
return bool(KEY)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _save_credentials(data: dict[str, Any]) -> None:
|
|
100
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
CONFIG_PATH.write_text(json.dumps(data, indent=2) + "\n")
|
|
102
|
+
try:
|
|
103
|
+
CONFIG_PATH.chmod(0o600) # the key is a bearer credential
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _auto_provision() -> bool:
|
|
109
|
+
"""Accountless start: mint a shadow identity via POST /agent-signup.
|
|
110
|
+
|
|
111
|
+
Gives any agent a working Hebbrix account in one call — no email, no
|
|
112
|
+
dashboard. The account has lifetime caps and expires if unclaimed; every
|
|
113
|
+
tool response carries a `hebbrix_usage` block telling the agent when and
|
|
114
|
+
how to suggest claiming.
|
|
115
|
+
"""
|
|
116
|
+
global KEY, DEFAULT_COLLECTION
|
|
117
|
+
caller = "claude-code" if os.environ.get("CLAUDECODE") else (
|
|
118
|
+
"cursor" if os.environ.get("CURSOR_TRACE_ID") else "unknown")
|
|
119
|
+
try:
|
|
120
|
+
r = httpx.post(f"{BASE}/agent-signup", json={"agent_caller": caller}, timeout=20.0)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"hebbrix-mcp: auto-signup failed ({e}). Set HEBBRIX_API_KEY instead.",
|
|
123
|
+
file=sys.stderr)
|
|
124
|
+
return False
|
|
125
|
+
if r.status_code != 201:
|
|
126
|
+
print(f"hebbrix-mcp: auto-signup unavailable (HTTP {r.status_code}: {r.text[:160]}). "
|
|
127
|
+
"Set HEBBRIX_API_KEY instead.", file=sys.stderr)
|
|
128
|
+
return False
|
|
129
|
+
data = r.json()
|
|
130
|
+
KEY = data["api_key"]
|
|
131
|
+
DEFAULT_COLLECTION = data.get("collection_id", "")
|
|
132
|
+
_save_credentials({
|
|
133
|
+
"api_key": KEY,
|
|
134
|
+
"collection_id": DEFAULT_COLLECTION,
|
|
135
|
+
"agent_id": data.get("agent_id"),
|
|
136
|
+
"tier": data.get("tier", "shadow"),
|
|
137
|
+
"expires_at": data.get("expires_at"),
|
|
138
|
+
"api_base": BASE,
|
|
139
|
+
})
|
|
140
|
+
print(
|
|
141
|
+
"hebbrix-mcp: started in agent mode (no account needed).\n"
|
|
142
|
+
f" free allowance: {data.get('limits')}\n"
|
|
143
|
+
f" expires: {data.get('expires_at')} if unclaimed\n"
|
|
144
|
+
f" claim it anytime: {data.get('claim_command', 'hebbrix-mcp claim --email <you>')}\n"
|
|
145
|
+
f" credentials saved to {CONFIG_PATH}",
|
|
146
|
+
file=sys.stderr,
|
|
147
|
+
)
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------------------- #
|
|
152
|
+
# HTTP helpers #
|
|
153
|
+
# --------------------------------------------------------------------------- #
|
|
154
|
+
def _client() -> httpx.AsyncClient:
|
|
155
|
+
# Headers built per call — the key may come from a per-request Authorization
|
|
156
|
+
# header (multi-tenant hosted mode) or be set by auto-provision after import.
|
|
157
|
+
key = _REQUEST_KEY.get() or KEY
|
|
158
|
+
return httpx.AsyncClient(
|
|
159
|
+
timeout=30.0,
|
|
160
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _cid(collection_id: Optional[str]) -> Optional[str]:
|
|
165
|
+
return collection_id or DEFAULT_COLLECTION or None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _err(r: httpx.Response) -> dict[str, Any]:
|
|
169
|
+
return {"error": f"HTTP {r.status_code}: {r.text[:300]}"}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _capture_usage(r: httpx.Response) -> None:
|
|
173
|
+
"""Remember the X-Hebbrix-* usage block (shadow accounts only send it)."""
|
|
174
|
+
h = r.headers
|
|
175
|
+
if "x-hebbrix-tier" not in h:
|
|
176
|
+
return
|
|
177
|
+
_LAST_USAGE.clear()
|
|
178
|
+
_LAST_USAGE.update({
|
|
179
|
+
"tier": h.get("x-hebbrix-tier"),
|
|
180
|
+
"status": h.get("x-hebbrix-status"),
|
|
181
|
+
"writes": {"used": int(h.get("x-hebbrix-writes-used", 0)),
|
|
182
|
+
"limit": int(h.get("x-hebbrix-writes-limit", 0))},
|
|
183
|
+
"retrievals": {"used": int(h.get("x-hebbrix-retrievals-used", 0)),
|
|
184
|
+
"limit": int(h.get("x-hebbrix-retrievals-limit", 0))},
|
|
185
|
+
"expires_at": h.get("x-hebbrix-expires-at"),
|
|
186
|
+
"claim_command": h.get("x-hebbrix-claim"),
|
|
187
|
+
})
|
|
188
|
+
status_ = _LAST_USAGE.get("status")
|
|
189
|
+
if status_ in ("warning", "limited", "read_only"):
|
|
190
|
+
w = _LAST_USAGE["writes"]
|
|
191
|
+
_LAST_USAGE["action_for_human"] = (
|
|
192
|
+
f"Hebbrix agent allowance at {w['used']}/{w['limit']} writes "
|
|
193
|
+
f"(status: {status_}). Run `{_LAST_USAGE.get('claim_command')}` to claim "
|
|
194
|
+
"this account and unlock the free monthly tier — the key and all "
|
|
195
|
+
"memories carry over."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _u(out: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
"""Attach the usage block to a tool result (agents relay it to humans)."""
|
|
201
|
+
if _LAST_USAGE and isinstance(out, dict):
|
|
202
|
+
out.setdefault("hebbrix_usage", dict(_LAST_USAGE))
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _get(path: str, params: Optional[dict] = None) -> Any:
|
|
207
|
+
async with _client() as c:
|
|
208
|
+
r = await c.get(f"{BASE}{path}", params={k: v for k, v in (params or {}).items() if v is not None})
|
|
209
|
+
_capture_usage(r)
|
|
210
|
+
return _err(r) if r.status_code >= 400 else r.json()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def _post(path: str, body: dict) -> Any:
|
|
214
|
+
async with _client() as c:
|
|
215
|
+
r = await c.post(f"{BASE}{path}", json={k: v for k, v in body.items() if v is not None})
|
|
216
|
+
_capture_usage(r)
|
|
217
|
+
return _err(r) if r.status_code >= 400 else r.json()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _patch(path: str, body: dict) -> Any:
|
|
221
|
+
async with _client() as c:
|
|
222
|
+
r = await c.patch(f"{BASE}{path}", json={k: v for k, v in body.items() if v is not None})
|
|
223
|
+
_capture_usage(r)
|
|
224
|
+
return _err(r) if r.status_code >= 400 else r.json()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def _delete(path: str) -> dict[str, Any]:
|
|
228
|
+
async with _client() as c:
|
|
229
|
+
r = await c.delete(f"{BASE}{path}")
|
|
230
|
+
_capture_usage(r)
|
|
231
|
+
return {"status": r.status_code, "ok": r.status_code < 400}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mem_row(m: dict) -> dict[str, Any]:
|
|
235
|
+
return {
|
|
236
|
+
"id": m.get("id") or m.get("memory_id"),
|
|
237
|
+
"content": m.get("content"),
|
|
238
|
+
"importance": m.get("importance"),
|
|
239
|
+
"created_at": m.get("created_at"),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# --------------------------------------------------------------------------- #
|
|
244
|
+
# Memory tools (CRUD + version history) #
|
|
245
|
+
# --------------------------------------------------------------------------- #
|
|
246
|
+
@mcp.tool()
|
|
247
|
+
async def hebbrix_remember(
|
|
248
|
+
content: str,
|
|
249
|
+
tags: Optional[list[str]] = None,
|
|
250
|
+
collection_id: Optional[str] = None,
|
|
251
|
+
verbatim: bool = False,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
"""Store a memory. Use this whenever the user shares a fact, decision, or
|
|
254
|
+
preference worth recalling later. Prefer one clear fact per call.
|
|
255
|
+
|
|
256
|
+
verbatim=True stores the text exactly as given, skipping fact-extraction.
|
|
257
|
+
Returns {"id", "status", "importance"} or {"error"}.
|
|
258
|
+
"""
|
|
259
|
+
cid = _cid(collection_id)
|
|
260
|
+
if not cid:
|
|
261
|
+
return {"error": "no collection_id and HEBBRIX_COLLECTION_ID not set"}
|
|
262
|
+
body: dict[str, Any] = {"content": content, "collection_id": cid, "infer": not verbatim}
|
|
263
|
+
if tags:
|
|
264
|
+
body["tags"] = tags
|
|
265
|
+
data = await _post("/memories/raw", body)
|
|
266
|
+
if "error" in data:
|
|
267
|
+
return data
|
|
268
|
+
return _u({"id": data.get("id"), "status": data.get("processing_status", "pending"),
|
|
269
|
+
"importance": data.get("importance")})
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
async def hebbrix_search(
|
|
274
|
+
query: str,
|
|
275
|
+
limit: int = 5,
|
|
276
|
+
collection_id: Optional[str] = None,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
"""Semantic search over memories. Always call this BEFORE answering questions
|
|
279
|
+
that depend on prior context, decisions, or user preferences.
|
|
280
|
+
|
|
281
|
+
Returns {"query", "count", "results": [{"id","content","score"}]}.
|
|
282
|
+
"""
|
|
283
|
+
cid = _cid(collection_id)
|
|
284
|
+
if not cid:
|
|
285
|
+
return {"error": "no collection_id and HEBBRIX_COLLECTION_ID not set"}
|
|
286
|
+
data = await _post("/search", {"query": query, "collection_id": cid, "limit": limit})
|
|
287
|
+
if "error" in data:
|
|
288
|
+
return data
|
|
289
|
+
out = [{"id": i.get("memory_id"), "content": i.get("content"),
|
|
290
|
+
"score": round(i.get("score") or 0.0, 3)} for i in (data.get("results") or [])[:limit]]
|
|
291
|
+
return _u({"query": query, "count": len(out), "results": out,
|
|
292
|
+
"processing_time_ms": data.get("processing_time_ms")})
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@mcp.tool()
|
|
296
|
+
async def hebbrix_get(memory_id: str) -> dict[str, Any]:
|
|
297
|
+
"""Fetch one memory by id, including its full content and metadata."""
|
|
298
|
+
data = await _get(f"/memories/{memory_id}")
|
|
299
|
+
return _u(data if "error" in data else _mem_row(data) | {"metadata": data.get("metadata")})
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@mcp.tool()
|
|
303
|
+
async def hebbrix_update(
|
|
304
|
+
memory_id: str,
|
|
305
|
+
content: Optional[str] = None,
|
|
306
|
+
importance: Optional[float] = None,
|
|
307
|
+
) -> dict[str, Any]:
|
|
308
|
+
"""Update a memory in place (keeps version history). Use this to CORRECT a
|
|
309
|
+
stored fact instead of remembering a contradicting copy. Pass the new content.
|
|
310
|
+
"""
|
|
311
|
+
if content is None and importance is None:
|
|
312
|
+
return {"error": "pass content and/or importance to update"}
|
|
313
|
+
data = await _patch(f"/memories/{memory_id}", {"content": content, "importance": importance})
|
|
314
|
+
return _u(data if "error" in data else _mem_row(data) | {"updated": True})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool()
|
|
318
|
+
async def hebbrix_forget(memory_id: str) -> dict[str, Any]:
|
|
319
|
+
"""Delete a memory by id."""
|
|
320
|
+
return _u(await _delete(f"/memories/{memory_id}"))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@mcp.tool()
|
|
324
|
+
async def hebbrix_list(limit: int = 20, collection_id: Optional[str] = None) -> dict[str, Any]:
|
|
325
|
+
"""List recent memories in a collection."""
|
|
326
|
+
cid = _cid(collection_id)
|
|
327
|
+
if not cid:
|
|
328
|
+
return {"error": "no collection_id and HEBBRIX_COLLECTION_ID not set"}
|
|
329
|
+
data = await _get("/memories", {"collection_id": cid, "limit": limit})
|
|
330
|
+
if "error" in data:
|
|
331
|
+
return data
|
|
332
|
+
items = data.get("items") or data.get("memories") or (data if isinstance(data, list) else [])
|
|
333
|
+
return _u({"count": len(items), "memories": [
|
|
334
|
+
{"id": m.get("id"), "content": (m.get("content") or "")[:160]} for m in items[:limit]]})
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool()
|
|
338
|
+
async def hebbrix_history(memory_id: str) -> dict[str, Any]:
|
|
339
|
+
"""Show the version history of a memory (how it changed over time, including
|
|
340
|
+
supersessions). Useful to see what a fact used to be."""
|
|
341
|
+
data = await _get(f"/memories/{memory_id}/history")
|
|
342
|
+
if "error" in data:
|
|
343
|
+
return data
|
|
344
|
+
versions = data.get("history") or data.get("versions") or (data if isinstance(data, list) else [])
|
|
345
|
+
return _u({"memory_id": memory_id, "versions": versions})
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# --------------------------------------------------------------------------- #
|
|
349
|
+
# Knowledge-graph tools (the differentiator) #
|
|
350
|
+
# --------------------------------------------------------------------------- #
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
async def hebbrix_search_entities(
|
|
353
|
+
entity_type: Optional[str] = None,
|
|
354
|
+
limit: int = 20,
|
|
355
|
+
collection_id: Optional[str] = None,
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
"""List entities in the knowledge graph (people, organizations, tools, places),
|
|
358
|
+
optionally filtered by entity_type. Use for "who/what do I know about" questions.
|
|
359
|
+
"""
|
|
360
|
+
data = await _get("/knowledge-graph/entities",
|
|
361
|
+
{"entity_type": entity_type, "limit": limit, "collection_id": _cid(collection_id)})
|
|
362
|
+
if "error" in data:
|
|
363
|
+
return data
|
|
364
|
+
ents = data.get("entities") or (data if isinstance(data, list) else [])
|
|
365
|
+
return _u({"count": data.get("count", len(ents)), "entities": [
|
|
366
|
+
{"name": e.get("name"), "type": e.get("type") or e.get("entity_type"),
|
|
367
|
+
"mentions": e.get("mention_count") or e.get("mentions")} for e in ents[:limit]]})
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@mcp.tool()
|
|
371
|
+
async def hebbrix_entity_timeline(entity_name: str, collection_id: Optional[str] = None) -> dict[str, Any]:
|
|
372
|
+
"""Bi-temporal timeline for one entity: what facts were true about it and when.
|
|
373
|
+
Use this for "what changed" / "what was true at time X" questions about a person,
|
|
374
|
+
company, or thing."""
|
|
375
|
+
return _u(await _get(f"/knowledge-graph/timeline/{entity_name}", {"collection_id": _cid(collection_id)}))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@mcp.tool()
|
|
379
|
+
async def hebbrix_graph_query(
|
|
380
|
+
query: Optional[str] = None,
|
|
381
|
+
entity: Optional[str] = None,
|
|
382
|
+
relation_type: Optional[str] = None,
|
|
383
|
+
depth: int = 2,
|
|
384
|
+
timestamp: Optional[str] = None,
|
|
385
|
+
collection_id: Optional[str] = None,
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Query the knowledge graph for relationships and facts. Give a natural-language
|
|
388
|
+
query OR an entity (+ optional relation_type). Pass an ISO timestamp to ask what
|
|
389
|
+
was true at that point in time (bi-temporal). depth = graph hops (1-5).
|
|
390
|
+
"""
|
|
391
|
+
return _u(await _post("/knowledge-graph/query", {
|
|
392
|
+
"query": query, "entity": entity, "relation_type": relation_type,
|
|
393
|
+
"depth": depth, "timestamp": timestamp, "collection_id": _cid(collection_id)}))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
async def hebbrix_contradictions(memory_id: Optional[str] = None) -> dict[str, Any]:
|
|
398
|
+
"""Surface contradicting facts in the knowledge graph (e.g. two different values
|
|
399
|
+
for the same attribute). Pass a memory_id to check one memory, or omit to scan.
|
|
400
|
+
Use before trusting a fact that feels ambiguous."""
|
|
401
|
+
return _u(await _get("/knowledge-graph/contradictions", {"memory_id": memory_id}))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# --------------------------------------------------------------------------- #
|
|
405
|
+
# Reasoning layer (unique to Hebbrix: confidence + decision outcomes) #
|
|
406
|
+
# --------------------------------------------------------------------------- #
|
|
407
|
+
@mcp.tool()
|
|
408
|
+
async def hebbrix_confidence(query: str, collection_id: Optional[str] = None) -> dict[str, Any]:
|
|
409
|
+
"""Ask how confident the agent should be before acting on something, grounded in
|
|
410
|
+
stored memory and past decision outcomes. Call this before a consequential
|
|
411
|
+
autonomous action. Returns a confidence score and a recommended action.
|
|
412
|
+
"""
|
|
413
|
+
data = await _get("/confidence", {"query": query, "collection_id": _cid(collection_id)})
|
|
414
|
+
if "error" in data:
|
|
415
|
+
return data
|
|
416
|
+
return _u({"confidence": data.get("confidence"),
|
|
417
|
+
"recommended_action": data.get("recommended_action"),
|
|
418
|
+
"answer_confidence": data.get("answer_confidence"),
|
|
419
|
+
"decision_count": data.get("decision_count"),
|
|
420
|
+
"reasoning": data.get("reasoning") or data.get("explanation")})
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@mcp.tool()
|
|
424
|
+
async def hebbrix_log_decision(
|
|
425
|
+
description: str,
|
|
426
|
+
outcome: Optional[str] = None,
|
|
427
|
+
decision_type: Optional[str] = None,
|
|
428
|
+
collection_id: Optional[str] = None,
|
|
429
|
+
) -> dict[str, Any]:
|
|
430
|
+
"""Record a decision the agent made and, if known, its outcome
|
|
431
|
+
(success | failure | partial). This feeds hebbrix_confidence so future
|
|
432
|
+
recommendations improve. Log both the choice and how it turned out."""
|
|
433
|
+
data = await _post("/decisions", {
|
|
434
|
+
"description": description, "outcome": outcome, "decision_type": decision_type,
|
|
435
|
+
"collection_id": _cid(collection_id)})
|
|
436
|
+
if "error" in data:
|
|
437
|
+
return data
|
|
438
|
+
return _u({"id": data.get("id") or data.get("decision_id"), "logged": True})
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@mcp.tool()
|
|
442
|
+
async def hebbrix_list_collections() -> dict[str, Any]:
|
|
443
|
+
"""List the collections (memory spaces / tenants) available to this API key."""
|
|
444
|
+
data = await _get("/collections", {"limit": 100})
|
|
445
|
+
if "error" in data:
|
|
446
|
+
return data
|
|
447
|
+
items = data.get("items") or (data if isinstance(data, list) else [])
|
|
448
|
+
return _u({"count": len(items), "collections": [
|
|
449
|
+
{"id": c.get("id"), "name": c.get("name"), "memory_count": c.get("memory_count")} for c in items]})
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@mcp.tool()
|
|
453
|
+
async def hebbrix_account_status() -> dict[str, Any]:
|
|
454
|
+
"""Tier, usage, limits, and expiry for this agent's account. In agent mode
|
|
455
|
+
(auto-provisioned account), relay the claim command to the human when usage
|
|
456
|
+
status is 'warning' or worse — claiming is one command and keeps all memories."""
|
|
457
|
+
return _u(await _get("/agent-signup/whoami"))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# --------------------------------------------------------------------------- #
|
|
461
|
+
# Resource + prompt (Supermemory pattern: inject a compiled profile) #
|
|
462
|
+
# --------------------------------------------------------------------------- #
|
|
463
|
+
@mcp.resource("hebbrix://profile")
|
|
464
|
+
async def profile_resource() -> str:
|
|
465
|
+
"""The user's compiled profile (stable preferences + recent facts)."""
|
|
466
|
+
data = await _get("/profile")
|
|
467
|
+
if isinstance(data, dict) and "error" in data:
|
|
468
|
+
return "Profile unavailable."
|
|
469
|
+
facts = (data or {}).get("facts") or (data or {}).get("profile") or data
|
|
470
|
+
return f"User profile:\n{facts}"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.prompt()
|
|
474
|
+
async def context() -> str:
|
|
475
|
+
"""Inject the user's profile as context and nudge the model to use memory."""
|
|
476
|
+
data = await _get("/profile")
|
|
477
|
+
facts = (data or {}).get("facts") if isinstance(data, dict) else None
|
|
478
|
+
return (
|
|
479
|
+
"Before responding, use Hebbrix memory. Search it for relevant context, and "
|
|
480
|
+
"remember any new durable facts the user shares.\n\n"
|
|
481
|
+
f"Known user profile: {facts or '(none yet)'}"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# --------------------------------------------------------------------------- #
|
|
486
|
+
# Entry point #
|
|
487
|
+
# --------------------------------------------------------------------------- #
|
|
488
|
+
class _HeaderAuthMiddleware:
|
|
489
|
+
"""ASGI middleware for hosted (multi-tenant) mode: stashes each request's
|
|
490
|
+
Bearer token in a contextvar so tool calls use the CALLER's key, never a
|
|
491
|
+
shared one. Works with stateless streamable HTTP (tool executes within
|
|
492
|
+
the request that carried the header)."""
|
|
493
|
+
|
|
494
|
+
def __init__(self, app):
|
|
495
|
+
self.app = app
|
|
496
|
+
|
|
497
|
+
async def __call__(self, scope, receive, send):
|
|
498
|
+
if scope.get("type") == "http":
|
|
499
|
+
headers = {k.decode().lower(): v.decode()
|
|
500
|
+
for k, v in (scope.get("headers") or [])}
|
|
501
|
+
auth = headers.get("authorization", "")
|
|
502
|
+
token = auth[7:] if auth.lower().startswith("bearer ") else ""
|
|
503
|
+
reset = _REQUEST_KEY.set(token)
|
|
504
|
+
try:
|
|
505
|
+
await self.app(scope, receive, send)
|
|
506
|
+
finally:
|
|
507
|
+
_REQUEST_KEY.reset(reset)
|
|
508
|
+
else:
|
|
509
|
+
await self.app(scope, receive, send)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _cmd_claim(argv: list[str]) -> None:
|
|
513
|
+
"""`hebbrix-mcp claim --email you@example.com` — Tier 0 -> Tier 1.
|
|
514
|
+
|
|
515
|
+
Two steps: request a code (emailed), then enter it. Same key, all
|
|
516
|
+
memories intact; limits switch from lifetime to monthly.
|
|
517
|
+
"""
|
|
518
|
+
email = None
|
|
519
|
+
if "--email" in argv:
|
|
520
|
+
i = argv.index("--email")
|
|
521
|
+
if i + 1 < len(argv):
|
|
522
|
+
email = argv[i + 1]
|
|
523
|
+
if not email:
|
|
524
|
+
raise SystemExit("usage: hebbrix-mcp claim --email you@example.com")
|
|
525
|
+
_load_saved_credentials()
|
|
526
|
+
if not KEY:
|
|
527
|
+
raise SystemExit("No agent credentials found. Run `hebbrix-mcp` once first.")
|
|
528
|
+
auth = {"Authorization": f"Bearer {KEY}"}
|
|
529
|
+
|
|
530
|
+
r = httpx.post(f"{BASE}/agent-signup/claim", json={"email": email},
|
|
531
|
+
headers=auth, timeout=20.0)
|
|
532
|
+
if r.status_code == 404:
|
|
533
|
+
print(
|
|
534
|
+
"Claiming from the CLI isn't available on this server yet. Your "
|
|
535
|
+
"agent account keeps working — sign in at "
|
|
536
|
+
f"https://www.hebbrix.com/dashboard to manage it. Agent id: "
|
|
537
|
+
f"{json.loads(CONFIG_PATH.read_text()).get('agent_id', '?')}"
|
|
538
|
+
)
|
|
539
|
+
return
|
|
540
|
+
if r.status_code >= 400:
|
|
541
|
+
raise SystemExit(f"claim failed: HTTP {r.status_code}: {r.text[:300]}")
|
|
542
|
+
print(f"Verification code sent to {email} (expires in ~15 minutes).")
|
|
543
|
+
|
|
544
|
+
for _ in range(3):
|
|
545
|
+
code = input("Enter the 6-digit code from the email: ").strip()
|
|
546
|
+
if not (len(code) == 6 and code.isdigit()):
|
|
547
|
+
print("That doesn't look like a 6-digit code — try again.")
|
|
548
|
+
continue
|
|
549
|
+
v = httpx.post(f"{BASE}/agent-signup/claim/verify", json={"code": code},
|
|
550
|
+
headers=auth, timeout=20.0)
|
|
551
|
+
if v.status_code < 400:
|
|
552
|
+
data = v.json()
|
|
553
|
+
print(f"✅ Claimed as {data.get('email')} (tier: {data.get('tier')}). "
|
|
554
|
+
"Same key, all memories intact — expiry no longer applies.")
|
|
555
|
+
# Reflect the claim in the saved config.
|
|
556
|
+
try:
|
|
557
|
+
cfg = json.loads(CONFIG_PATH.read_text())
|
|
558
|
+
cfg["tier"] = data.get("tier", "free")
|
|
559
|
+
cfg["claimed_email"] = data.get("email")
|
|
560
|
+
cfg.pop("expires_at", None)
|
|
561
|
+
_save_credentials(cfg)
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
return
|
|
565
|
+
print(f"Verify failed: HTTP {v.status_code}: {v.text[:200]}")
|
|
566
|
+
raise SystemExit("Too many attempts here — run the claim command again.")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def run() -> None:
|
|
570
|
+
"""Console entry point. Serves MCP over stdio by default.
|
|
571
|
+
|
|
572
|
+
Usage:
|
|
573
|
+
hebbrix-mcp # stdio (Claude Desktop, Cursor, ...)
|
|
574
|
+
hebbrix-mcp --transport streamable-http # remote / self-hosted at HOST:PORT
|
|
575
|
+
hebbrix-mcp claim --email <you> # claim an auto-provisioned account
|
|
576
|
+
|
|
577
|
+
Credentials, in order: HEBBRIX_API_KEY env var; saved ~/.hebbrix/config.json;
|
|
578
|
+
otherwise AGENT MODE — a shadow account is minted automatically (no email,
|
|
579
|
+
no dashboard) and the server starts in under 10 seconds.
|
|
580
|
+
"""
|
|
581
|
+
if len(sys.argv) > 1 and sys.argv[1] == "claim":
|
|
582
|
+
_cmd_claim(sys.argv[2:])
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
transport = "stdio"
|
|
586
|
+
if "--transport" in sys.argv:
|
|
587
|
+
i = sys.argv.index("--transport")
|
|
588
|
+
if i + 1 < len(sys.argv):
|
|
589
|
+
transport = sys.argv[i + 1]
|
|
590
|
+
|
|
591
|
+
# Hosted multi-tenant mode: no server-side key at all — every request must
|
|
592
|
+
# bring its own Authorization header (or hit /agent-signup itself first).
|
|
593
|
+
multi_tenant = os.environ.get("HEBBRIX_MCP_MULTI_TENANT", "").lower() in ("1", "true", "yes")
|
|
594
|
+
if multi_tenant:
|
|
595
|
+
if transport not in ("streamable-http", "http"):
|
|
596
|
+
raise SystemExit("HEBBRIX_MCP_MULTI_TENANT requires --transport streamable-http")
|
|
597
|
+
import uvicorn
|
|
598
|
+
|
|
599
|
+
mcp.settings.stateless_http = True # tool runs inside the request that carried the header
|
|
600
|
+
app = _HeaderAuthMiddleware(mcp.streamable_http_app())
|
|
601
|
+
print(f"hebbrix-mcp: multi-tenant streamable-http on {HOST}:{PORT} "
|
|
602
|
+
"(per-request Authorization: Bearer <key>)", file=sys.stderr)
|
|
603
|
+
uvicorn.run(app, host=HOST, port=PORT, log_level="warning")
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
if not KEY:
|
|
607
|
+
_load_saved_credentials()
|
|
608
|
+
if not KEY and not _auto_provision():
|
|
609
|
+
raise SystemExit(
|
|
610
|
+
"Could not start: no HEBBRIX_API_KEY, no saved credentials, and "
|
|
611
|
+
"accountless signup is unavailable. Get a key at "
|
|
612
|
+
"https://www.hebbrix.com/dashboard/api-keys"
|
|
613
|
+
)
|
|
614
|
+
if transport in ("streamable-http", "http"):
|
|
615
|
+
mcp.run(transport="streamable-http")
|
|
616
|
+
elif transport == "sse":
|
|
617
|
+
mcp.run(transport="sse")
|
|
618
|
+
else:
|
|
619
|
+
mcp.run()
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
if __name__ == "__main__":
|
|
623
|
+
run()
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hebbrix-mcp
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Hebbrix MCP server — long-term memory and knowledge graph for any MCP-compatible agent (Claude Desktop, Cline, Cursor, etc.)
|
|
5
|
+
Project-URL: Homepage, https://www.hebbrix.com
|
|
6
|
+
Project-URL: Documentation, https://www.hebbrix.com/integrations/mcp
|
|
7
|
+
Project-URL: Repository, https://github.com/Hebbrix/hebbrix-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/Hebbrix/hebbrix-mcp/issues
|
|
9
|
+
Author-email: Hebbrix <support@hebbrix.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,claude,cline,cursor,hebbrix,long-term-memory,mcp,memory,model-context-protocol
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: mcp[cli]>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# hebbrix-mcp
|
|
33
|
+
|
|
34
|
+
**Long-term memory and a knowledge graph for any MCP-compatible agent.**
|
|
35
|
+
|
|
36
|
+
Your agent forgets everything when the session ends. This server fixes that — and goes further than a plain memory store:
|
|
37
|
+
|
|
38
|
+
- **Memory** — store, search, correct, and version facts across sessions
|
|
39
|
+
- **Knowledge graph** — entities, relationships, timelines, and "what was true at time X"
|
|
40
|
+
- **Reasoning** — ask how confident the agent should be before acting, and log outcomes so it improves
|
|
41
|
+
|
|
42
|
+
Works with Claude Desktop, Claude Code, Cursor, Cline, Continue, and any other MCP client. Backed by [Hebbrix](https://www.hebbrix.com).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick start (10 seconds, no account)
|
|
47
|
+
|
|
48
|
+
**1. Install**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install hebbrix-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**2. Add to your MCP client** — no API key needed:
|
|
55
|
+
|
|
56
|
+
<details open>
|
|
57
|
+
<summary><b>Claude Desktop</b> — <code>~/Library/Application Support/Claude/claude_desktop_config.json</code></summary>
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"hebbrix": { "command": "hebbrix-mcp" }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
</details>
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary><b>Claude Code</b></summary>
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
claude mcp add hebbrix -- hebbrix-mcp
|
|
73
|
+
```
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary><b>Cursor</b> — <code>~/.cursor/mcp.json</code></summary>
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"hebbrix": { "command": "hebbrix-mcp" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
</details>
|
|
87
|
+
|
|
88
|
+
<details>
|
|
89
|
+
<summary><b>Cline / Continue / other</b></summary>
|
|
90
|
+
|
|
91
|
+
Point your MCP servers config at the `hebbrix-mcp` command (stdio). Same shape as above.
|
|
92
|
+
</details>
|
|
93
|
+
|
|
94
|
+
**3. Restart the client.** Done — your agent now has persistent memory.
|
|
95
|
+
|
|
96
|
+
### What just happened?
|
|
97
|
+
|
|
98
|
+
On first run with no API key, the server mints a **free agent account** automatically (no email, no dashboard, ~1 second) and saves the credentials to `~/.hebbrix/config.json`. The account includes:
|
|
99
|
+
|
|
100
|
+
| | |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Learning events (writes) | 300 |
|
|
103
|
+
| Retrievals (searches) | 2,000 |
|
|
104
|
+
| Expiry | 14 days after last activity, if unclaimed |
|
|
105
|
+
|
|
106
|
+
Every tool result includes a `hebbrix_usage` block (tier, usage, expiry), so the agent always knows where it stands and will tell you when it's time to claim.
|
|
107
|
+
|
|
108
|
+
### Keep it forever (one command)
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
hebbrix-mcp claim --email you@example.com
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You'll get a 6-digit code by email; enter it and the account switches to the **free monthly tier with no expiry**. Same API key, all memories carry over. Confirming decision outcomes (`hebbrix_log_decision`) also extends the trial — the system rewards exactly the usage that makes it smarter.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Using your own API key
|
|
119
|
+
|
|
120
|
+
Already have a Hebbrix account? Get a key at [hebbrix.com/dashboard/api-keys](https://www.hebbrix.com/dashboard/api-keys) and pass it instead:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"hebbrix": {
|
|
126
|
+
"command": "hebbrix-mcp",
|
|
127
|
+
"env": {
|
|
128
|
+
"HEBBRIX_API_KEY": "mem_sk_...",
|
|
129
|
+
"HEBBRIX_COLLECTION_ID": "your-default-collection-uuid"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The env var always wins over saved agent-mode credentials.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Tools
|
|
141
|
+
|
|
142
|
+
15 tools, one resource, one prompt. A server-level instruction block teaches the model when to reach for each, so a well-behaved agent searches before answering and remembers what matters — without being told.
|
|
143
|
+
|
|
144
|
+
### Memory
|
|
145
|
+
|
|
146
|
+
| Tool | What it does |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `hebbrix_remember(content, tags?, collection_id?, verbatim?)` | Store a fact, decision, or preference. `verbatim=true` skips fact-extraction |
|
|
149
|
+
| `hebbrix_search(query, limit?, collection_id?)` | Semantic search (hybrid vector + BM25 + graph retrieval) |
|
|
150
|
+
| `hebbrix_get(memory_id)` | Fetch one memory with metadata |
|
|
151
|
+
| `hebbrix_update(memory_id, content?, importance?)` | Correct a memory **in place** — old versions are kept |
|
|
152
|
+
| `hebbrix_forget(memory_id)` | Delete a memory |
|
|
153
|
+
| `hebbrix_list(limit?, collection_id?)` | List recent memories |
|
|
154
|
+
| `hebbrix_history(memory_id)` | See how a memory changed over time |
|
|
155
|
+
|
|
156
|
+
### Knowledge graph
|
|
157
|
+
|
|
158
|
+
Reads are available on **every tier** (including agent mode). Graph writes and inference need a Pro plan.
|
|
159
|
+
|
|
160
|
+
| Tool | What it does |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `hebbrix_search_entities(entity_type?, limit?, collection_id?)` | List known entities (people, orgs, tools, places) |
|
|
163
|
+
| `hebbrix_entity_timeline(entity_name, collection_id?)` | What was true about an entity, and when |
|
|
164
|
+
| `hebbrix_graph_query(query?, entity?, relation_type?, depth?, timestamp?)` | Query relationships — pass a `timestamp` to ask about a point in time |
|
|
165
|
+
| `hebbrix_contradictions(memory_id?)` | Surface facts that conflict with each other |
|
|
166
|
+
|
|
167
|
+
### Reasoning & account
|
|
168
|
+
|
|
169
|
+
| Tool | What it does |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `hebbrix_confidence(query, collection_id?)` | How confident should the agent be before acting? Grounded in memory + past outcomes |
|
|
172
|
+
| `hebbrix_log_decision(description, outcome?, decision_type?)` | Record a decision and how it turned out — feeds future confidence |
|
|
173
|
+
| `hebbrix_list_collections()` | List the memory spaces this key can use |
|
|
174
|
+
| `hebbrix_account_status()` | Tier, usage, limits, and expiry |
|
|
175
|
+
|
|
176
|
+
**Also:** the `hebbrix://profile` resource and the `context` prompt inject the user's compiled profile into the conversation.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Running modes
|
|
181
|
+
|
|
182
|
+
### 1. Local (default) — stdio
|
|
183
|
+
|
|
184
|
+
What the quick start above does. One process per client, credentials from env or `~/.hebbrix/config.json`.
|
|
185
|
+
|
|
186
|
+
### 2. Self-hosted HTTP — one instance, your machines
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
HEBBRIX_API_KEY=mem_sk_... hebbrix-mcp --transport streamable-http
|
|
190
|
+
# serves http://127.0.0.1:8080/mcp
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{ "mcpServers": { "hebbrix": { "url": "http://127.0.0.1:8080/mcp" } } }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 3. Hosted multi-tenant — one instance, many users
|
|
198
|
+
|
|
199
|
+
The server holds **no key at all**; every request authenticates with its own `Authorization` header:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
HEBBRIX_MCP_MULTI_TENANT=1 HEBBRIX_MCP_HOST=0.0.0.0 hebbrix-mcp --transport streamable-http
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "mcpServers": { "hebbrix": {
|
|
207
|
+
"url": "https://your-host/mcp",
|
|
208
|
+
"headers": { "Authorization": "Bearer mem_sk_..." }
|
|
209
|
+
}}}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
In this mode there is no default collection — pass `collection_id` on tool calls.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
All optional. With nothing set, the server starts in agent mode.
|
|
219
|
+
|
|
220
|
+
| Variable | Default | Purpose |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `HEBBRIX_API_KEY` | *(agent mode mints one)* | Your Hebbrix bearer token |
|
|
223
|
+
| `HEBBRIX_COLLECTION_ID` | *(agent mode sets one)* | Default collection for writes/reads |
|
|
224
|
+
| `HEBBRIX_API_BASE` | `https://api.hebbrix.com/v1` | API endpoint override |
|
|
225
|
+
| `HEBBRIX_CONFIG` | `~/.hebbrix/config.json` | Where agent-mode credentials are saved |
|
|
226
|
+
| `HEBBRIX_MCP_HOST` | `127.0.0.1` | Bind host (HTTP transports) |
|
|
227
|
+
| `HEBBRIX_MCP_PORT` | `8080` | Bind port (HTTP transports) |
|
|
228
|
+
| `HEBBRIX_MCP_MULTI_TENANT` | off | Hosted mode: per-request header auth |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## How it works
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
┌──────────────────┐ MCP (stdio or HTTP) ┌─────────────┐ HTTPS ┌──────────┐
|
|
236
|
+
│ Claude / Cursor / │ ───────────────────────→│ hebbrix-mcp │─────────────→│ Hebbrix │
|
|
237
|
+
│ Cline / any agent │ tool calls │ (this) │ REST API │ cloud │
|
|
238
|
+
└──────────────────┘ └─────────────┘ └──────────┘
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
This package owns **zero state**. Tool calls become REST calls against your Hebbrix account; memories, embeddings, the knowledge graph, and retrieval all live in the Hebbrix backend. Delete this package and your memories are still there.
|
|
242
|
+
|
|
243
|
+
### Limits degrade gracefully
|
|
244
|
+
|
|
245
|
+
Agent-mode accounts never break mid-task. When a limit is reached you get a structured error, not a failure:
|
|
246
|
+
|
|
247
|
+
| Code | Meaning | What still works |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `WRITE_LIMIT_REACHED` | 300 lifetime writes used | Reads, searches, confirmations |
|
|
250
|
+
| `READ_LIMIT_REACHED` | 2,000 lifetime retrievals used | Claim to continue |
|
|
251
|
+
| `SHADOW_READ_ONLY` | Unclaimed past 14 days | Reads (7-day grace window) |
|
|
252
|
+
| `SHADOW_EXPIRED` | Past the grace window | Nothing — account is reaped |
|
|
253
|
+
| `CLAIM_REQUIRED_FOR_BATCH` | Batch writes need a claimed account | Everything else |
|
|
254
|
+
|
|
255
|
+
Every error carries a `resolve` field with the exact command to fix it, so agents can relay it to you verbatim.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Troubleshooting
|
|
260
|
+
|
|
261
|
+
**"HTTP 401" on every call** — the key is wrong or revoked. Unset `HEBBRIX_API_KEY`, delete `~/.hebbrix/config.json`, and restart to re-provision; or paste a fresh key from the dashboard.
|
|
262
|
+
|
|
263
|
+
**Agent mode won't start (`auto-signup unavailable`)** — signup may be at daily capacity (it's capped) or your network blocks the API. Set `HEBBRIX_API_KEY` from the dashboard instead.
|
|
264
|
+
|
|
265
|
+
**`claim` says `EMAIL_IN_USE`** — v1 claiming needs an email with no existing Hebbrix account. Use a fresh address (a `you+agent@gmail.com` alias works).
|
|
266
|
+
|
|
267
|
+
**A memory doesn't show up in search immediately** — indexing is asynchronous; typical convergence is under 30 seconds.
|
|
268
|
+
|
|
269
|
+
**Multi-tenant mode returns errors about collections** — there's no default collection in hosted mode; pass `collection_id` explicitly.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Development
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
git clone https://github.com/Hebbrix/hebbrix-mcp
|
|
277
|
+
cd hebbrix-mcp
|
|
278
|
+
./quick_setup.sh # venv + editable install
|
|
279
|
+
source venv/bin/activate
|
|
280
|
+
pytest tests/ -q # 11 offline tests, no network needed
|
|
281
|
+
hebbrix-mcp # starts in agent mode on stdio
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history and [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute.
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT — see [LICENSE](LICENSE).
|
|
289
|
+
|
|
290
|
+
## Related
|
|
291
|
+
|
|
292
|
+
- [Hebbrix documentation](https://www.hebbrix.com/docs)
|
|
293
|
+
- [MCP integration guide](https://www.hebbrix.com/integrations/mcp)
|
|
294
|
+
- [Model Context Protocol](https://modelcontextprotocol.io)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
hebbrix_mcp/__init__.py,sha256=O-S-IKKzycFvDmBxot3wUFQDNj1Tp_gS8LhPXWoD_4I,1540
|
|
2
|
+
hebbrix_mcp/server.py,sha256=dIdnN07P213UL4CW_LXBz6opLh73FipQ2pFZ_SWEXiY,26700
|
|
3
|
+
hebbrix_mcp-0.3.1.dist-info/METADATA,sha256=x7ge695d-m_jQzE853sQ0QQkKlRf8loQg_V3R2JbH8w,11108
|
|
4
|
+
hebbrix_mcp-0.3.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
hebbrix_mcp-0.3.1.dist-info/entry_points.txt,sha256=aUX_X0e_3gGby3ctMXSermDlOpHzCHUJcCr7rPPr2G0,55
|
|
6
|
+
hebbrix_mcp-0.3.1.dist-info/licenses/LICENSE,sha256=OfdrD_jjLTXJfETK_k2snkfOqZnlt7yAkoB-ndsrILQ,1064
|
|
7
|
+
hebbrix_mcp-0.3.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hebbrix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|