1. Executive Summary
yantrikdb-mcp is the MCP server its author ships for the YantrikDB engine: twenty-one tools over
the engine's Python binding, run in process against one SQLite file or
forwarded to a YantrikDB server cluster. Its
care is at the parameter level. When the engine cannot honour an
argument, the tool refuses and names the fix instead of dropping it. Its
weakness is that the boundaries it adds are per-tool while the store is
shared. Scope is a namespace the model may omit, and omitted means every
namespace. The skill gate guards one write door while
remember reaches the same namespace through another.
The memory mechanism is the engine's. Decay, consolidation, contradiction detection, correction with revision history, supersession and the as-of read are described in the engine report. This report covers what the MCP layer adds or changes: the tool surface, where scope is decided, how writes are gated, and what reaches the model.
The engine that runs at this pin is 0.23.1.
pyproject.toml:133 requires
yantrikdb>=0.15.4,<0.24.0, and the documented install
is uvx yantrikdb-mcp, which resolves the newest release in
range. PyPI published 0.23.1 at 18:51 UTC on 19 September 2026, before
the pinned commit at 23:19 UTC. Tag v0.23.1 (597fdcee…)
is four commits past the engine report's pin and changes only version
strings and one CLI print, so the engine report's reading applies
unchanged.
This is not the MCP server inside the engine
package. The engine ships its own 628-line
src/yantrikdb/mcp/ with memory_* tools; that
is the one the server report cites. This
repository is the separately distributed one.
Four findings, all read rather than run:
- Default recall spans every namespace.
recalldeclaresnamespace=Noneand forwards it, and the engine emits no namespace predicate forNone. The cold-startsession(action="digest")the server instructions prescribe is whole-store unless the model passesscope. - The skill gate has a second door.
skill(action="define")runs a gate that is off by default, then schema, content scanners, attribution and an audit line.rememberwrites any namespace with any metadata, andskill(action="surface")serves anyskill_substraterow taggedrecord_type: skill, treating a missing hash as a legacy row. - The review queue has no reader. Rule-type skills go
to
skill_pending_review; nothing in the tree reads that namespace by name, and default recall returns it. - The rate limit resets every call. The per-session limiter is keyed on the JSON-RPC request id.
One mark: negative_eval. Section 9 names the six
withheld.
2. Mental Model
A memory is an engine row, and every state it can hold is the engine's: active, consolidated or tombstoned, a synthesis state, a supersession edge, a revision chain. The MCP layer adds no state of its own to that row. What it adds is who decides each step, and at this layer the answer is the agent for every one.
- Becoming a memory. The agent calls
remember, choosing the text, type, importance, certainty, source, domain and namespace. The injected instructions tell it to use memory "AUTOMATICALLY without the user asking" (src/yantrikdb_mcp/server.py:43-45). - Changing.
correctupdates in place with a required reason; the engine archives the prior state. - Stopping.
forgetby rid, a conflict resolution the agent picks, or anauto_resolvepass whose dry run defaults on. - Where it lives. A namespace string the agent
passes, or
default.
Skills are the one place the server adds a lifecycle. A
define lands in skill_substrate or, for a
rule, in skill_pending_review. A skill replaced by
on_conflict="replace" is forgotten first. Outcome events
land in outcome_substrate. These are namespace conventions,
not engine states, so every read that does not name a namespace sees all
three.
Diagram source
%% caption: two write doors into the skill namespace and a default read that names no namespace, so the pending queue and the catalog both reach the model through plain recall
flowchart TB
DEF["skill(action=define)"] --> GATE{"YANTRIKDB_SKILLS_WRITE_ENABLED<br/>frozen at import, default off"}
GATE -->|"off"| REF["refused, audit line"]
GATE -->|"on"| CHK["schema, scanners, rate limit<br/>keyed on request_id,<br/>attribution, body_sha256"]
CHK -->|"skill_type = rule"| PEND[("skill_pending_review")]
CHK -->|"other types"| LIVE[("skill_substrate")]
REM["remember(namespace=skill_substrate,<br/>metadata record_type=skill)"] -->|"no gate, no hash"| LIVE
REM2["remember(text)"] --> DEFNS[("default namespace")]
SURF["skill(action=surface)"] --> LIVE
SURF --> HASH{"body_sha256 present?"}
HASH -->|"absent: treated as legacy"| SERVED["served as a skill"]
HASH -->|"present and matches"| SERVED
REC["recall(query), namespace omitted"] --> NOPRED["engine: no namespace predicate"]
NOPRED --> PEND
NOPRED --> LIVE
NOPRED --> DEFNS
PEND -.->|"no reader, no promote verb"| NOWHERE["never resolved"]3. Architecture
| Module | Role |
|---|---|
src/yantrikdb_mcp/server.py |
Injected instructions, a lazily opened process-singleton engine, embedded or HTTP backend selection |
src/yantrikdb_mcp/tools.py |
All tools: 3,447 lines of argument shaping, refusals and response pruning |
src/yantrikdb_mcp/embedder.py |
Embedder selection: bundled 64-dim, ONNX 384-dim, multilingual 256-dim |
src/yantrikdb_mcp/http_backend.py |
The same method names over a YantrikDB server, with unsupported ones raising a typed error |
src/yantrikdb_mcp/skill_security.py,
skill_validation.py,
skill_content_scanner.py |
The skill gate |
src/yantrikdb_mcp/_compat.py, auth.py |
MCP SDK 1.x and 2.x shims, network app, bearer-token middleware |
One engine handle per process. _LazyDB
opens the engine on the first tool call. Under SSE the lifespan
re-enters per client session. A fresh handle per session raced on
closing the same SQLite WAL, so the handle is now a singleton closed
once at exit (server.py:237-265, issue #11).
Network mode is one principal.
--transport sse or streamable-http binds
0.0.0.0 by default
(src/yantrikdb_mcp/__init__.py:77). It wraps the app in
bearer-token auth only when YANTRIKDB_API_KEY is set, and
otherwise logs that the server is unauthenticated (:89-98).
DNS-rebinding protection is disabled and all hosts and origins are
allowed (_compat.py:117-121). Every client that holds the
key reaches the same store with the same tools.
Deployment and ergonomics
- Install:
uvx yantrikdb-mcp, no account, no API key. The engine wheel bundles its embedder, so the default path has no ML dependency. - Store: one SQLite file, not hand-readable; the
atlastool exports a static page of every memory and serves it on 127.0.0.1. - Cluster: set
YANTRIKDB_SERVER_URL.list_memoriesand several other methods raiseRemoteUnsupportedErrorthere (http_backend.py:525-551), so skillget, skilllistandtemporal(action="range")without a query do not work in cluster mode.
Auto embedder selection misreads its own default
store. With YANTRIKDB_EMBEDDER unset,
load_engine picks ONNX whenever the file already holds
memories, and bundled otherwise (embedder.py:238-244). The
README says the ONNX branch is for "an existing pre-v0.6 database". The
probe cannot tell one from the bundled store the same default created on
first run. tests/test_embedder_loader.py:99-117 builds its
legacy.db fixture with YantrikDB.with_default,
the bundled path, and asserts that a restart without the
[onnx] extra raises. Every other test that spawns a server
sets the embedder explicitly. Read, not reproduced; section 12 carries
the question.
4. Essential Implementation Paths
- Remember —
tools.py:330-606. Claims and backdating are refused before any write on an engine that cannot hold them (:404-438). A keyed batch skipsrecord_batch, because that path would drop the keys (:478-508). Single mode forwardsnamespace, metadata andevent_timebounds todb.record(:586-596). - Recall —
:661-922. Default pathdb.recall_with_response(..., namespace=namespace, ...)(:828-833). An explicitorderorinclude_supersededroutes todb.recall(:809-826).min_score_ratiois applied client-side because the default engine path lacks it (:834-856). - Forget, correct —
:926-956,:960-1017, by rid, no namespace. - Think —
:1021-1172. An explicitdry_run=Trueon a plain think is refused;Nonemeans preview for the maintenance cycle (:1109-1116). - Skill —
:2317-2835. Gate:2422-2452, define:2455-2626, surface:2628-2689. - Digest —
session(action="digest"),:1793-1845;scopeis forwarded only when given (:1806). - Engine side — the namespace branch in
crates/yantrikdb-core/src/engine/recall.rs:1850-1880andpasses_recall_filters(:285-327) atv0.23.1;forget(rid)incrates/yantrikdb-python/src/py_engine/memory.rs:737-740.
5. Memory Data Model
The row is the engine's; section 5 of the engine report describes it. The server adds three conventions in metadata and namespace.
Skills. A defined skill is a procedural memory in
skill_substrate. Its metadata carries
record_type: skill, skill_id,
skill_type, applies_to,
body_sha256, and attribution: request id, OS user,
hostname, wall clock, origin and a nonce
(tools.py:2569-2585,
skill_security.py:232-248). A rule carries
pending_review: true and lands in
skill_pending_review
(skill_security.py:528-540).
Outcomes. One episodic memory per reported use, in
outcome_substrate, with no rollup onto the skill.
Valid time. event_time is written as
event_time_min and event_time_max in metadata,
which the engine mirrors to columns (tools.py:609-618).
created_at backdates the record time.
The names are reserved by convention only. The
engine mentions skill_substrate in one doc comment and
guards none of the three names, and remember accepts any
namespace string and any metadata.
6. Retrieval Mechanics
The model sees the engine's hybrid recall: vector and FTS lanes, graph expansion, decay-weighted scoring. The server makes three changes to what reaches it.
It forces entity expansion on. The engine flipped
the expand_entities default to off at 0.13, and the tool
passes True explicitly (pyproject.toml:78).
The manifest records why the floor rose to 0.14.1: a case-sensitive
entity stoplist let "AT" become an entity that matched every query
containing "at". The engine's benchmark missed it because the engine
defaults expansion off and this server turns it on
(pyproject.toml:99-118).
It makes each hit auditable. Every hit carries
created_at in ISO form, similarity beside the
blended score, and why_retrieved
(tools.py:860-898). A since/until
window filters on created_at before ranking and is echoed
back (:908-913).
It leaves scope to the caller. recall,
memory(action="list"),
graph(action="recall_with_links") and temporal
take namespace=None. The engine appends
AND m.namespace = ? only when a namespace is supplied
(recall.rs:1859-1863, :1875-1879). So an agent
that follows the injected golden path, a digest and then a short recall,
reads every namespace in the file, including the skill queue and any
other pipeline's memories. The bundled Agent Skills skill asks job
agents to pass a pipeline namespace "on every call"
(skills/persistent-memory/SKILL.md:73-74), which is prose
to the model.
Valid time is written and not read. No tool passes
the engine's event_after or event_before.
temporal(action="as_of") is the engine's record-time
recall_as_of.
7. Write Mechanics
Writes are synchronous tool calls, visible to the next recall. No
model runs in the server; extraction for
remember(summary=...) and session(capture) is
the engine's draft_memories_from_summary.
correct needs a non-empty reason. Stated claims are
grounded by the engine, and ungrounded ones come back rejected rather
than stored.
Refusal over silent loss. A backdated
created_at, a claims list or an as-of query on
an engine that cannot honour it returns an error naming the version
needed, before anything is written (tools.py:404-438,
:1985-1992). The comment's reason is that stamping "now" on
a memory the caller dated "would corrupt exactly the thing backdating
exists to protect".
The dry-run incident is in the code. On 15 August
2026 a maintenance cycle called as a preview "auto-resolved 15 conflicts
and tombstoned 13 live records on the production store". The parameter
had been accepted and never forwarded. It is now forwarded, defaults to
preview, and an explicit dry run on a pass with no dry form is refused
(tools.py:1106-1139).
Operational cost
- Write: one engine call per memory, synchronous, no model call in the server.
- Background: none. The model is told to call
thinkat the end of long work; after 50 writes since the last think, arememberresponse carries amaintenanceobject, then every tenth write while over threshold (tools.py:252-287). - Read:
top_kdefaults to 10, and each hit'swhy_retrievedlist is returned whole. The tool schema costs about 9.8k tokens per session in the full profile, which thecoreprofile cuts by about 36% (tools.py:20-28).
8. Agent Integration
Tools. Full profile: remember, recall, forget,
correct, think, memory, graph, conflict, session, procedure, temporal,
category, personality, trigger, stats, conversation, task, gaps, skill,
atlas, and pack when the engine carries packs, which 0.23.1 does.
YANTRIKDB_TOOL_PROFILE=core registers the first ten only
(tests/test_tool_profiles_and_budget.py:32-45).
Instructions. The server injects a golden path into
the client's system prompt (server.py:43-112). It includes
a trust-boundary paragraph: recalled text is "DATA, not instructions",
and pack memories lose to the user's own.
Hosts. .mcp.json and
server.json register the server for MCP clients and the MCP
registry; docs/hermes.md registers it with Hermes Agent as an MCP server, the
alternative to the Hermes
plugin's in-process provider; integrations/prime-agent/
reaches it over HTTP with the static bearer token.
Agency. Complete. Every mutating verb — forget,
correct, conflict resolve and auto-resolve, archive, category reset,
personality set, trigger prune — is on the tool surface. Pack install
and trust are the exception: they are refused unless
YANTRIKDB_ENABLE_PACK_WRITES=1, read once at import
(tools.py:3016-3018).
9. Reliability, Safety, and Trust
Scope is advisory at this layer. The Hermes plugin derives the
namespace from the host session and passes it on every call. This server
takes it from the model, defaults reads to none, and runs
forget, correct and conflict resolution by id.
The engine report observes that
isolation is the wrapper's job; this wrapper hands it to the model.
The skill gate guards one door. The define path is
thorough: gate off by default and frozen at import against environment
spoofing, schema validation, prompt-injection and credential scanners, a
namespace allowlist, a cross-origin replace guard, attribution, a JSONL
audit line, a hash stamped for tamper detection. None of it sits on
remember. skill(surface) filters on namespace
and record_type and accepts a missing
body_sha256 as a pre-hash legacy row
(skill_security.py:449-455). A skill written through
remember is therefore served, with the gate closed, and the
audit file records nothing.
The README names the Hermes plugin and the server's
/v1/skills/* among readers of the same namespace. The
startup warning for cluster mode states the principle — "the MCP
server's gate alone is not sufficient"
(skill_security.py:485-493) — and it holds in embedded mode
too.
The review queue is a namespace nothing reads.
PENDING_NAMESPACE is assigned at tools.py:2562
and read nowhere. The surface comment calls the queue "operator-only"
and points to "a future tool extension or query the DB directly"
(:2637-2640). No verb promotes a pending skill, and default
recall returns it as an ordinary procedural memory.
The rate limit counts one call per bucket. Define
and outcome take their session id from
ctx.request_context.request_id
(tools.py:2408-2415). In the MCP SDK that field is the
JSON-RPC request id, which differs on every call, so the 30-per-minute
sliding window never holds more than one entry per key. The limiter's
tests pass fixed strings
(tests/test_skill_security.py:373-393).
Capability marks:
negative_eval— awarded; section 10.scope_enforced— withheld. The row has a namespace and the engine filters on it, but every read at this layer takes the key from the model and defaults to none, and the cold-start digest is whole-store by default.trust_state,bitemporal,audit_log— withheld at this layer; the engine carries all three, in process. This server adds apending_reviewflag no recall consults, writes valid time that no tool reads back, and keeps a skill-only JSONL file, off by default, outside the store, whose write failures are swallowed.tombstone— withheld. Forget, conflict resolution and skill replace all act on a rid.human_review— withheld. The conflictresolveandauto_resolveverbs are on the agent's tool surface, and the skill queue has no drain.
10. Tests, Evals, and Benchmarks
Nothing was installed or run for this report; everything below is
from reading the tests at the pin. CI runs the unit suite on Python
3.10, 3.12 and 3.14 against MCP SDK 1.x and 2.x with the
[onnx] extra installed, and an end-to-end stdio suite per
embedder backend (.github/workflows/ci.yml).
The negative cases.
tests/test_temporal_as_of.py:99-120 records port 8420,
takes a cut, records port 9000 in the same namespace, and asserts the
temporal tool's as-of read returns 8420 and not 9000.
tests/test_time_window_range.py:71-94 seeds four in-window
rows and a 300-day-old decoy and asserts the window keeps one and drops
the other, in both directions. Both run the tool function against the
real engine with the positive side asserted first. That earns
negative_eval.
The contract gate.
tests/test_mcp_semantic_contract.py drives a spawned server
over JSON-RPC and states a 100% pass bar. Its namespace case C5
(:171-177) remembers a secret in tenant_alpha
and asserts it is absent from a tenant_beta recall. Nothing
in the module writes to tenant_beta, so a recall returning
nothing passes. Seeding one tenant_beta fact and asserting
it present would repair it.
Not covered. No test writes through
remember into skill_substrate, reads a pending
skill through plain recall, or calls a gated skill action twice through
the tool to exercise the limiter. The review queue is tested as a
predicate (tests/test_skill_security.py:493-503).
The paper. The README links Skill as Memory, Not
Document (doi:10.5281/zenodo.20128887),
which the Zenodo record lists as a preprint dated 12 May 2026. The
README calls it "peer-reviewed" (README.md:350,
:394). Its abstract reports the substrate rejecting 70 of
70 adversarially malformed skills at write time. In this server that
validation runs in skill(action="define") and not on
remember, which reaches the same namespace. The paper's
harness was not read.
11. For Your Own Build
Steal
- Refuse what you cannot honour, and name the fix. A backdated write on an engine that would stamp "now", a time window on a path that would drop it: an error with the required version beats a success that means something else.
- Default destructive previews to preview. After a dry run ran wet, the default became preview, and an explicit dry run on an operation with no dry form became an error.
- Surface maintenance debt as data inside the responses the agent already reads, rate-limited, with no urgency prose.
- Put
created_atand a similarity number on every hit, so a caller can overrule a ranking that picked a stale revision. - Bound each hard dependency to the tested minor, with the reason beside the pin. The manifest records the flipped default and the major release that broke installs.
Avoid
- A write gate on one tool when another tool writes the same records. Put the check where the namespace is written, or make the namespace unreachable from the generic writer.
- Treating a missing integrity field as valid. A legacy exemption becomes the path of least resistance for every new writer.
- A review queue with no reader. Either ship the promote path and exclude the queue from ordinary reads, or do not route to it.
- Keying a per-session limit on a per-request identifier.
- An optional scope whose default is everything, on a server that serves several clients from one file.
Fit
This suits one person who wants the YantrikDB engine behind Claude Code, Cursor or Hermes over MCP, on one machine, with the model deciding what to remember. The engine does the memory work well, and this layer passes it through with unusual honesty about what each argument does. It does not suit a shared deployment. Scope, deletion and skill admission all trust the model or the network, and the network mode binds wide with auth optional. A team wanting per-person or per-project isolation should derive the namespace outside the model, as the Hermes plugin does, or run one file per principal.
12. Open Questions
- Does a default install fail on its second start? The auto rule and
its test say a bundled store with memories resolves to ONNX, which
raises without the extra. Settling it needs one run with no
YANTRIKDB_EMBEDDERset. - Is a skill written through
rememberserved by the Hermes plugin's and the server's skill readers too? The README says they share the namespace; their filters were not read here. - Is the review queue meant to be drained by a server route? No reader was found in this repository or in the engine.
- Does the paper's admission harness drive
skill(define), the server's/v1/skills/*, or the engine directly?
Appendix: File Index
- Server and instructions:
src/yantrikdb_mcp/server.py,src/yantrikdb_mcp/__init__.py,src/yantrikdb_mcp/_compat.py,src/yantrikdb_mcp/auth.py. - Tools:
src/yantrikdb_mcp/tools.py(remember:330, recall:661, forget:926, correct:960, think:1021, session:1681, temporal:1852, skill:2317, pack:3078, atlas:3370). - Skill gate:
src/yantrikdb_mcp/skill_security.py,skill_validation.py,skill_content_scanner.py. - Backends:
src/yantrikdb_mcp/embedder.py,src/yantrikdb_mcp/http_backend.py. - Manifest and hosts:
pyproject.toml,.mcp.json,server.json,skills/persistent-memory/SKILL.md,docs/hermes.md,integrations/prime-agent/. - Tests:
tests/test_temporal_as_of.py,tests/test_time_window_range.py,tests/test_mcp_semantic_contract.py,tests/test_skill_security.py,tests/test_embedder_loader.py,tests/test_tool_profiles_and_budget.py,tests/test_v010_engine_contract_cases.py. - Engine at
v0.23.1:crates/yantrikdb-core/src/engine/recall.rs,crates/yantrikdb-python/src/py_engine/memory.rs,src/yantrikdb/mcp/.
Recorded searches
Checked against the checkout at the pinned revision, and the engine
at tag v0.23.1 where marked.
grep -rn 'event_after\|event_before' . --exclude-dir=.git— no match; no tool binds the valid-time filter.grep -rn 'skill_substrate\|PENDING_NAMESPACE\|skill_pending_review\|OUTCOME_NAMESPACE\|SKILL_NAMESPACE' --include='*.py' . | grep -v '^./tests'—PENDING_NAMESPACEis defined inskill_security.py:528, imported attools.py:2379and assigned at:2562; no read.- Engine:
grep -rn 'skill_substrate\|skill_pending_review\|outcome_substrate' crates src— one doc comment inpy_engine/memory.rs:422-429; no guard. grep -rn 'request_id' src/—tools.py:2408and:2413only.grep -n 'rate' tests/test_skill_security.py tests/test_e2e_mcp.py— the limiter tests attest_skill_security.py:373-393callcheck_rate_limitwith fixed strings.grep -n 'YANTRIKDB_EMBEDDER' tests/*.py— every spawned server sets it; onlytest_embedder_loader.pyexercises auto.grep -rniE 'arxiv|bibtex|@article|@misc|doi\.org|CITATION|zenodo' . --exclude-dir=.git— the README's Zenodo link and BibTeX block; noCITATION.cff.gh api repos/yantrikos/yantrikdb/compare/49c7aac1b78b74b296f7708a95d227073b4a666b...v0.23.1— four commits, seven files: version strings,src/yantrikdb/cli.pyandtests/test_cli.py.- PyPI
yantrikdbrelease list — 0.23.1 uploaded 2026-09-19T18:51:01Z, the newest release below 0.24.0 before the pin commit at 2026-09-19T23:19:17Z.
History
2026-09-26 — 364e19a5…
— first reading, at the head of main, a commit dated 19
September 2026. One mark, negative_eval. The engine was
read at tag v0.23.1, the release the manifest range
resolves to at the pin. Screened before reading: 3 auto-run surfaces
(.mcp.json, mcp.json and
server.json, each launching
uvx yantrikdb-mcp), no build-time execution, 2 unpinned
manifests, 2 inside the cooldown with every file of the depth-1 clone
dated to the tip, and no agent-instruction file. The engine's partial
checkout showed one build.rs and three files inside the
cooldown. Nothing was installed, built or run.