Operate

How it works

Boring, inspectable infrastructure: a Laravel MCP server over Streamable-HTTP, native vector search in Postgres, and embeddings that never leave the box.

MCP over Streamable-HTTP

A single endpoint, POST /api/mcp, speaks JSON-RPC 2.0. A transport-agnostic server dispatches initialize, tools/list, tools/call and ping against a tool registry; the HTTP controller handles auth and the response envelope. Every request carries a per-agent agt_… bearer key, which the controller resolves to an Agent and sets as the current context. The REST API reuses that exact auth seam.

request flow
MCP client  ──JSON-RPC 2.0 (Bearer agt_…)──▶  Laravel (syntheticmem.com)
                                              POST /api/mcp · McpServer::dispatch()
                                              tool → MemoryService
                                                          │
                       local embeddings ◀── OllamaEmbed (nomic-embed-text, 768d)
                                                          ▼
                                              Postgres 16 + pgvector
                                              vector(768) + HNSW cosine (<=>)

Vectors: Postgres + pgvector

The whole app runs on Postgres 16 with the pgvector extension. Each memory stores a native vector(768) column indexed with HNSW over cosine distance. Recall is a SQL pre-filter by agent_id / namespace / status, then an ANN top-k ordered fact → hypothesis → retired and nearest-first — real vector search from day one, scoped fail-closed to the acting agent. A fail-closed global scope means a forgotten where returns the current agent’s rows, never everybody’s.

Local ollama embeddings

Embeddings are generated locally by nomic-embed-text (768-dim) via POST http://localhost:11434/api/embed — no cloud, no key, and the content never leaves the box to be embedded. The same model name is stamped on each row (embedding_model) so a future model change is auditable.

The prune scheduler

Ephemeral memories (the anonymous-24h rule and any ttl_hours) carry an expires_at. They are hidden from recall/list once expired, and the memory:prune command hard-deletes expired rows on an hourly schedule (withoutOverlapping()). See Self-host & ops for the cron wiring.

Data model

TableHolds
agentsOne principal per MCP client: name, namespace_default, optional user_id (linked ⇒ permanent memories).
api_keysPer-agent agt_… keys, stored as a SHA-256 hash (plaintext shown once), with revoked_at.
memoriesContent + embedding vector(768) + full provenance + status + expires_at; btree (agent_id, namespace, status) and an HNSW index on the embedding.
Roadmap. memory_consolidate (doubt-preserving episodic→semantic summarization), a WebSocket transport, and hybrid lexical+vector reranking are planned, not shipped. The six tools, provenance, and ephemeral durability are live today.