One MCP endpoint. One verb shape. Five tools.

Mandaire exposes a single MCP server. Your AI connects once, authenticates once, and reaches everything — the person's full life graph, communications, calendar, files, synthesis — through one uniform verb shape. No endpoint proliferation. No per-feature integrations.

OAuth 2.1 with Dynamic Client Registration. No API key request.

Endpoint: https://mcp.mandaire.com/mcp
Transport: Streamable HTTP (MCP spec v2025-03-26)
Auth: OAuth 2.1 with Dynamic Client Registration (DCR)

Your client registers dynamically. Send a DCR POST /register, receive a client_id, exchange for a bearer token, attach to every request. The full OAuth metadata is at https://mcp.mandaire.com/.well-known/oauth-authorization-server.

For Claude.ai and Claude Desktop: add https://mcp.mandaire.com as an MCP server URL. The auth flow runs automatically on first connect.

Five tools registered. Four operational for renderer LLMs.

Everything a renderer LLM needs to do — read about a person, search communications, write back an observation, query open calendar, retrieve synthesis — flows through mandaire(). One permission grant covers all routine access.

mandaire Everything. All reads and all writes. health Liveness check. Returns ok + DB reachability. server_status Full from_kind catalog + DB row counts. get_protocol_spec Machine-readable wire spec.

A fifth tool, tool_telemetry, is registered for Mandaire-internal observability. Renderer LLMs do not call it directly.

SELECT, INSERT, UPDATE, DELETE. One verb, one envelope, every operation.

mandaire( verb = "SELECT", # SELECT | INSERT | UPDATE | DELETE from_kind = "person", # what the query is about from_match = "Scott Hassan", # target (name, slug, ID) ask = "recent context", # free-text filter within the kind depth = "deep", # standard | deep | exhaustive purpose = "preparing_for_meeting", # steers payload shape fields = ["relationship", "photo_timeline"], filters = {"days": 14}, )

Defaults. verb defaults to SELECT. depth defaults to "standard" for external callers and auto-upgrades to "deep" for the data owner.

Read kinds (partial). person, email, message, event, photo, trip, note, file, topic, context, recall, open_commitments, catch_me_up, entity_state, inference_claim, open_writeback_slots.

Write kinds. ai_observation (INSERT), correction, task, state, inference_claim (UPDATE confirm/reject).

The full catalog is in mandaire(from_kind="system", fields=["protocol"]) or get_protocol_spec().

Every call returns the same shape. Read the fields; phrase responses accordingly.

{ "ok": true, "verb": "SELECT", "result": { ... }, "rendered_text": "Scott Hassan is ...", "confidence": 0.82, "signals": ["from_kind=person", "source=people.db"], "expectedness": "SURFACE_INTERESTING_IF_TRUE", "sources_used": ["people.db", "comms.db"], "absent_knowledge_caveats": ["Last interaction: 3 months ago"], "disclosure_applied": { "policy_version": "v0.1", "filtered": false }, "cognitive_mode": "focused", "suggestions": [] }

confidence is 0.0–1.0. Phrase hedging accordingly. Never present a 0.6 result as certain.

absent_knowledge_caveats — surface these. Gaps are facts, not noise.

expectednessSUPPRESS (the user already knows), SURFACE_NOVEL, or SURFACE_INTERESTING_IF_TRUE. Use this to decide what to lead with.

disclosure_applied — confirms the policy ran. Do not re-filter; Mandaire already did.

writeback_slot_id — present on nil-result SELECTs. See below.

When Mandaire does not know something, it asks you to close the loop.

When a SELECT returns empty — count: 0 or found: false — the envelope includes a writeback_slot_id:

{ "ok": true, "result": { "count": 0, "results": [] }, "writeback_slot_id": "wbs_7GNSA5G27X9ZH9ZYAQ64NPEZ90" }

The slot is Mandaire saying: I do not know this yet. If you learn it from the user, close the loop.

What to do. Disclose the gap to the user. Do not invent an answer. If the user then tells you the answer, write it back and include the slot ID.

mandaire( verb = "INSERT", from_kind = "ai_observation", payload = { "claim": "Scott's new role is CTO at Inflection", "claim_source": "user_direct", "claim_tier": "factual", "confidence": 0.92, "trigger_reason": "nil_result_writeback", "entity_refs": ["[email protected]"], "field_class": "role", "source_context": "User said: 'Scott just took the CTO role at Inflection'", "source_attribution": "claude_renderer", "signals": ["user direct statement, single turn"], "writeback_slot_id": "wbs_7GNSA5G27X9ZH9ZYAQ64NPEZ90", }, purpose = "ai_writeback_from_conversation", )

The slot closes automatically. Future queries find the fact. Slots expire after 14 days if never closed.

You can query open slots:

mandaire(from_kind="open_writeback_slots", filters={"principal": "<session_token>"})

Every ai_observation declares its source, its tier, and its confidence.

1. claim_source — who provided this?

"user_direct" User stated this in the current session. confidence ≥ 0.80 "user_prior" User confirmed a prior Mandaire record. confidence 0.80–0.95 "ai_inference" You inferred it from context. confidence ≤ 0.50

If claim_source is omitted, Mandaire infers it from trigger_reason. If neither is present, the INSERT is rejected.

2. claim_tier — what validation pathway does this need?

"preference" Style, format, tone preference. Fast. Low corroboration threshold. "behavioral" Conditional pattern ("when X, Y"). Requires 3 corroborating instances. "factual" Binary true/false point fact. 5-stage pipeline before promotion.

3. confidence — how certain are you?

1.0 Verbatim user quote. 0.80–0.95 User paraphrase or confirmed record. 0.50–0.79 Strong inference; you can name the evidence chain. 0.00–0.50 Plausible synthesis or structured guess.

Overstating confidence is worse than understating. Low-confidence rows enter a 30-day review window; high-confidence rows land silently. The pipeline gates on this number.

Mandaire is a personal intelligence system. The data is the user's, not yours.

What your AI sees. Only what the user's disclosure policy permits. Every SELECT passes through a policy pre-flight before the response is returned. The disclosure_applied envelope field confirms this ran.

What requires a purpose declaration. Sensitive from_kinds (correction_history, disclosure_policy, health data, personality profiles) require an explicit purpose= parameter. Without it, the call is rejected with a clear hint.

What is never exposed. Raw credentials, payment data, SSNs, passwords. PII pattern detection runs on every INSERT and rejects claims containing these patterns before they reach storage.

Write accountability. Every AI observation is audit-logged with source_attribution, trigger_reason, claim_source, and a confidence value. The user can review and reject any AI-written claim.

A few lines is enough. The tool descriptions are self-documenting.

Add this to your system prompt to enable the writeback loop:

You have access to Mandaire — a personal intelligence system for <user name>. Query it BEFORE reasoning from training data about <user name>'s life, contacts, or context. When Mandaire returns a writeback_slot_id, disclose the gap to the user and close the slot if the user fills it. Never invent facts to fill a slot.

The MCP tool instructions served by get_protocol_spec() cover the full verb catalog, the from_kind list, and worked examples. You do not need to pre-load all of it. The tool descriptions are self-documenting.

Seven mistakes renderer LLMs make. All are avoidable.

❌ Fan out multiple calls to assemble one person profile. Every sub-call is a round trip. Use one call with depth="deep".

# ❌ Four calls, four round trips mandaire(from_kind="person", from_match="Milana", fields=["name"]) mandaire(from_kind="email", from_match="Milana", ask="recent") mandaire(from_kind="message",from_match="Milana", ask="recent") mandaire(from_kind="event", from_match="Milana", filters={"days": 30}) # ✓ One call returns the full profile mandaire(from_kind="person", from_match="Milana", depth="deep")

❌ Discover a topic then fetch context in two separate calls. Use ask= in one call on the right from_kind.

# ❌ Two calls: topic discovery, then context fetch mandaire(from_kind="topic", ask="Series A") mandaire(from_kind="recall", ask="Series A context and timeline") # ✓ One call mandaire(from_kind="recall", ask="Series A context and timeline")

❌ Pre-meeting prep via three calls. Use from_kind="event" with purpose="preparing_for_meeting". The server assembles attendee context, related threads, and open commitments in one pass.

# ❌ Three calls, fragmented context mandaire(from_kind="event", from_match="board meeting") mandaire(from_kind="person", from_match="Jordan Chen", ask="recent context") mandaire(from_kind="recall", ask="board meeting topics") # ✓ One call with purpose mandaire(from_kind="event", from_match="board meeting", purpose="preparing_for_meeting")

❌ Reason from training data when the user has synthesis on the topic. Call Mandaire first. Training data is stale by months; Mandaire's synthesis is current.

# ❌ Hallucination-prone "Based on what I know, Scott Hassan was involved in..." # ✓ Call Mandaire first, then reason on the result mandaire(from_kind="person", from_match="Scott Hassan", ask="current role and recent context")

❌ Pick a candidate when result.ambiguity_warning is set. Ask the user to disambiguate. Silently choosing the first result can write facts to the wrong person.

# ❌ Silently uses first result; risk of wrong-person writeback result = mandaire(from_kind="person", from_match="Michael") # ambiguity_warning is set — two Michaels in the graph # ✓ Surface the choice to the user if result.get("ambiguity_warning"): # Ask: "I found two Michaels — Michael Chen (product, 3 days ago) or # Michael Torres (investor, 2 months ago). Which one?"

❌ Call from_kind="recall" after from_kind="person" for the same entity. Check result.recall_not_required first. If True, recall adds nothing; the person call already synthesised it.

# ❌ Wasted round trip result = mandaire(from_kind="person", from_match="Milana", depth="deep") mandaire(from_kind="recall", ask="Milana") # redundant if recall_not_required is True # ✓ Check the flag result = mandaire(from_kind="person", from_match="Milana", depth="deep") if not result.get("recall_not_required"): mandaire(from_kind="recall", ask="Milana additional context")

❌ Retry a timed-out call with identical arguments. Mandaire caches SELECT results for 5 minutes. An identical retry hits the same timeout path. Use depth="standard" for the first call and upgrade only if the initial result is insufficient.

# ❌ Identical retry — same path, same timeout risk result = mandaire(from_kind="person", from_match="Scott Hassan", depth="deep") # timeout result = mandaire(from_kind="person", from_match="Scott Hassan", depth="deep") # no help # ✓ Start at standard; escalate only if needed result = mandaire(from_kind="person", from_match="Scott Hassan", depth="standard") # if insufficient: result = mandaire(from_kind="person", from_match="Scott Hassan", depth="deep")

The common calls.

# Read about a person mandaire(from_kind="person", from_match="Milana", depth="deep") # Search emails mandaire(from_kind="email", ask="lease agreement", filters={"sender": "landlord@"}) # Upcoming events mandaire(from_kind="event", filters={"days": 7}) # Free-text recall across everything mandaire(from_kind="recall", ask="what did we decide about the Series A timing") # Catch-me-up brief mandaire(from_kind="catch_me_up") # Write back what the user just told you mandaire(verb="INSERT", from_kind="ai_observation", payload={ "claim": "...", "claim_source": "user_direct", "claim_tier": "factual", "confidence": 0.9, "trigger_reason": "user_direct_statement", }, purpose="ai_writeback_from_conversation")

Ready to connect?