Skip to content

MCP Integration

The mempill-mcp package is a FastMCP server that exposes the mempill engine over the MCP stdio transport. It lets any MCP-compatible agent (Claude Desktop, Claude Code, custom) read and write mempill claims without embedding the Python wheel directly.

The server registers five tools, implemented in mempill-mcp/src/mempill_mcp/tools.py:

Tool Signature Description
ingest_claim (subject, predicate, value, provenance, ...) Write a belief claim
query_memory (subject, predicate, as_of_tx_time?, valid_at?) Read the canonical belief
end_fact (subject, predicate, at, provenance?, confidence?) Explicitly close an open-ended fact and enable clean succession (0.4.0)
reconcile (subject_lines) Trigger conflict reconciliation
audit (limit?, claim_ref?, from_tx_time?) Read the immutable audit ledger

ingest_claim, query_memory, reconcile, and audit map directly to the corresponding engine.* dict-based method. end_fact instead calls the ergonomic Python end_fact(engine, agent_id, subject, predicate, at, ...) module function, which resolves the subject-line to a claim internally and calls engine.assert_validity under the hood. The agent_id is injected from the environment — the MCP client never needs to pass it.

query_subject (subject-scoped enumeration of every predicate known about a subject, available in the Rust core and Python wheel as of 0.3.0) is not yet exposed as an MCP tool. Use Python (Advanced) if you need it today.

subject str Entity (e.g. "user:alice")
predicate str Property (e.g. "city")
value any JSON-serialisable value
provenance str | dict "External:UserAsserted" | "External:ExternalFirstHand" |
"RecallReEntry" | "ModelDerived" | raw wire dict
cardinality str = "Functional" "Functional" | "SetValued" | "Unknown"
confidence_value float = 0.9 Value confidence [0, 1]
confidence_valid_time float = 0.9 Temporal confidence [0, 1]
criticality str = "Low" "Low" | "Medium" | "High" | "Critical"
valid_time dict | None {"start"?: ISO-8601, "end"?: ISO-8601,
"valid_time_confidence": float, # required if valid_time is set
"start_granularity"?: "year"|"month"|"day"|"instant",
"end_granularity"?: "year"|"month"|"day"|"instant"}
derived_from list[str] | None Source claim UUIDs

When disposition is non-committed (Contested, QueuedForAdjudication, etc.), the response includes a human-readable status_reason field explaining why.

subject str
predicate str
as_of_tx_time str | None ISO-8601 UTC — rewinds the transaction-time axis ("what did the
engine know as of this timestamp?")
valid_at str | None ISO-8601 UTC — filters by the valid-time axis ("what was true in
the real world at this point in time?")

as_of_tx_time and valid_at are independent. Use valid_at to ask “who was CEO on 2021-06-01?” regardless of when that claim was recorded. Use as_of_tx_time to rewind to what the engine knew at an earlier transaction timestamp.

Returns {"belief": {...}}. Contested beliefs also include status_reason.

When a belief has a primary claim, the response includes pre-rendered display strings:

{
"belief": {
"status": "Resolved",
"primary": {
"fact": {"value": "Alice"},
"valid_from_display": "2020-03",
"valid_until_display": null,
"valid_time": {
"start": "2020-03-01T00:00:00Z",
"end": null,
"valid_time_confidence": 0.9,
"start_granularity": "month",
"end_granularity": null
}
}
}
}

valid_from_display / valid_until_display (on the belief slot) reflect the recorded precision (no fabricated day or month components). Use these strings when surfacing valid-time bounds to users or downstream tools. valid_time.start_granularity / valid_time.end_granularity carry the machine-readable label (lowercase: "year", "month", "day", "instant", or null for legacy claims).

subject str Entity (e.g. "acme:ceo")
predicate str Property (e.g. "held_by")
at str ISO-8601 UTC instant (accepts YYYY / YYYY-MM / YYYY-MM-DD / RFC3339)
— the point at which to close the fact
provenance str | dict = None "External:UserAsserted" (default when omitted) |
"External:ExternalFirstHand" | raw wire dict — must be
first-hand external evidence
confidence float = 1.0 Confidence in this validity assertion [0, 1] — a single value,
unlike ingest_claim's separate confidence_value / confidence_valid_time

end_fact calls the ergonomic Python end_fact(engine, agent_id, subject, predicate, at, ...), which resolves the live claim on the subject-line by (subject, predicate) (same canonical fold as query_memory/query_history) and bounds its valid-time window at the given instant. The bound is a validity assertion applied at read time; a later non-overlapping claim then folds as a clean succession.

Returns {"claim_ref": str, "disposition": str, "effective_at": str, "no_op": bool}. no_op is true when no new write was made: an idempotent repeat of an identical Bound, or a Bound whose at is at/after the claim’s own valid_time.end (has zero effect on the derived window). Repeating end_fact on an already-fully-closed line (nothing left live) raises NotFoundError, not a no-op — there is no live claim left to close.

Date precision (0.4.0): The at string’s precision is automatically captured — e.g., end_fact(..., at="2024-09") records the granularity as "month" in the validity assertion, which appears as valid_until_granularity == "month" in subsequent query_history responses. This uses the shared Rust date parser to ensure precision consistency across Python and Rust.

Errors:

  • NotFoundError — zero live claims on the subject-line (including a repeat call on an already-closed line).
  • ValidationError — more than one live claim (Contested or SetValued, cannot auto-select which to close); or at is before the target claim’s own valid-time start; or provenance is not first-hand external.

end_fact only binds — there is no MCP tool to reopen a bounded claim. Reopening requires calling engine.assert_validity({"target": claim_ref, "assertion": {"type": "Reopen"}, ...}) directly against the Python wheel (not currently exposed as an MCP tool).

subject_lines list[list[str]] Each inner list is [subject, predicate]

Example: [["user:alice", "city"], ["user:bob", "location"]]

limit int = 50
claim_ref str | None UUID of a specific claim to filter
from_tx_time str | None ISO-8601 UTC lower bound

from_tx_time is an inclusive lower bound, so a paged walk must de-duplicate by entry_id, and pages should be large enough to hold all entries sharing one timestamp.

Variable Required Default Meaning
MEMPILL_AGENT_ID Yes — Agent identity for all writes in this session. Must be set before launch — the server fails fast with a clear error if missing.
MEMPILL_DB_DIR No in-memory Base directory for SQLite storage. The database file is derived automatically as MEMPILL_DB_DIR/agent_{MEMPILL_AGENT_ID}.db. If omitted the server uses an ephemeral in-memory engine (data lost on exit).

MEMPILL_AGENT_ID is the single-writer key: all claims written in a session belong to this agent. Use a stable, meaningful identifier (e.g. "claude-desktop-alice").

mempill and mempill-mcp are both published on PyPI:

Terminal window
pip install mempill mempill-mcp
Terminal window
# Test the server
MEMPILL_AGENT_ID=test python -m mempill_mcp
# Should start and wait for MCP stdio input; Ctrl-C to exit.

To work against an unreleased change on main, build both packages from source into the same venv instead:

Terminal window
# 1. Build and install the mempill wheel from source
cd mempill/mempill-python
maturin develop --release
# 2. Install the MCP adapter from source into the same virtual environment
cd ../mempill-mcp
pip install .

The easiest approach is to use the mempill-demo repository, which ships a pre-built venv with all dependencies. See Examples for the demo.

Claude Desktop reads mcpServers from its config file. Copy the block below, replace the absolute paths, and restart Claude Desktop (full quit — menu bar → Quit, not just close):

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json

claude_desktop_config.json
{
"mcpServers": {
"mempill": {
"command": "/ABSOLUTE/PATH/TO/mempill-demo/.venv/bin/python",
"args": ["-m", "mempill_mcp"],
"env": {
"MEMPILL_AGENT_ID": "my-agent",
"MEMPILL_DB_DIR": "/ABSOLUTE/PATH/TO/mempill-demo/data"
}
}
}
}

Important notes:

  • Use the python binary from the project venv that has mempill and mempill-mcp installed. Launching mempill-mcp as a bare command requires both packages to be on the system Python, which is unlikely. The venv python is the reliable path.
  • Claude Desktop does not expand $HOME or shell variables in this file — use literal absolute paths.
  • The MEMPILL_DB_DIR directory is shared across all Claude Desktop sessions for this agent; the actual database file is derived per-agent as MEMPILL_DB_DIR/agent_{MEMPILL_AGENT_ID}.db. Use distinct MEMPILL_AGENT_ID values (or distinct MEMPILL_DB_DIR directories) if you need per-agent isolation.

This config is sourced from mempill-demo/mcp/claude_desktop_config.json.example.

Claude Code reads .mcp.json from the project root. This file is gitignored in mempill-demo. Claude Code supports ${VAR:-default} expansion in the env block:

.mcp.json (at project root)
{
"mcpServers": {
"mempill": {
"type": "stdio",
"command": "/ABSOLUTE/PATH/TO/mempill-demo/.venv/bin/python",
"args": ["-m", "mempill_mcp"],
"env": {
"MEMPILL_AGENT_ID": "${MEMPILL_AGENT_ID:-demo-agent}",
"MEMPILL_DB_DIR": "${MEMPILL_DB_DIR}"
}
}
}
}

If MEMPILL_DB_DIR is unset in the shell environment, the server opens an in-memory (ephemeral) engine.

This config is sourced from mempill-demo/mcp/.mcp.json.example.

The server opens the mempill engine once in a FastMCP lifespan context manager and yields {"engine": engine, "agent_id": agent_id} to all tool functions. This means:

  • Engine startup cost (migrations, WAL setup) happens once per server process, not per call.
  • All five tools share the same engine instance and agent identity.
  • SQLite WAL mode is active — concurrent reads from other processes do not block.
  • When MEMPILL_DB_DIR is set, the database file persists after the server exits.

mempill-demo/mcp/verify_stdio.py is a small script that sends a tool-list request over stdio and validates the response. Run it against the installed server:

Terminal window
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| MEMPILL_AGENT_ID=test .venv/bin/python -m mempill_mcp

You should see a JSON response listing all five tools.

When query_memory returns a Contested belief, the response includes:

{
"belief": {
"status": "Contested",
"primary": null,
"alternatives": [
{"fact": {"value": "Berlin"}},
{"fact": {"value": "Munich"}}
]
},
"status_reason": "This claim conflicts with one or more existing beliefs..."
}

A well-behaved MCP agent should surface this to the user rather than silently picking one value. See Concepts: Contested and Dispositions.