Model Context Protocol

MCP server

InvariantDB includes a Model Context Protocol (MCP) server with 28 tools. MCP clients such as Claude, Cursor and VS Code can use it to run Cypher queries, search, save and recall memories and documents, and read property history. Requests are JSON-RPC 2.0 over HTTPS, authenticated with an API key (Authorization: Bearer gq_...) or OAuth sign-in.

28 MCP tools JSON-RPC 2.0 over HTTPS API key or OAuth sign-in
Endpoint
https://invariantdb.com/graphs/{graph}/mcp

Runs tools against the named graph. Replace {graph} with your full graph name. Hosted accounts use this endpoint for every graph.

Create an API key on the API keys page. A graph that does not exist returns HTTP 404 before any MCP method runs. For a server you run yourself, see Self-hosted servers.

Connect with an API key

Clients that read MCP servers from a JSON configuration file, such as Claude Code and Cursor, can send your API key in the Authorization header. Add an entry like this one. YOUR_GRAPH and gq_YOUR_API_KEY are placeholders for your graph name and key. The file name and field names differ between clients; check your client’s MCP documentation.

mcp.json http transport
{
  "mcpServers": {
    "invariantdb": {
      "type": "http",
      "url": "https://invariantdb.com/graphs/YOUR_GRAPH/mcp",
      "headers": {
        "Authorization": "Bearer gq_YOUR_API_KEY"
      }
    }
  }
}

Restart the client or reload its MCP servers. The 28 tools then appear in its tool list. For clients that connect by URL and sign in through a browser instead of storing a key, see Connect with OAuth sign-in.

Other MCP clients

Any client that supports the MCP HTTP transport can connect. Use the endpoint https://invariantdb.com/graphs/YOUR_GRAPH/mcp and send Authorization: Bearer gq_… with each request.

Connect with OAuth sign-in

MCP clients that support OAuth can connect without storing your API key. In your MCP client, add a remote (HTTP) MCP server with the URL https://invariantdb.com/graphs/YOUR_GRAPH/mcp and no Authorization header. The client registers itself with InvariantDB and opens an approval page in your browser. Paste an API key (gq_…) that belongs to your workspace and approve. The client then receives its own short-lived access token and does not store the key. When the token expires, the client asks you to approve again.

The client’s token is limited to the graphs that the approving key can reach, and it works only on the MCP endpoints. Keys that manage a cloud tenant, and keys that have no tenant, cannot approve a connection.

If your client does not support OAuth, add the same URL with an Authorization: Bearer gq_… header instead.

Tool reference

Tools

The tools fall into five groups: query and graph, search and recall, documents, agent memory, and provenance and compliance. All of them use the same endpoint and credential. Each card shows example arguments; the example data is fictional.

Query & graph

01 / 7 tools
cypher_query
{ "query": "MATCH (p:Person) WHERE p.id = $id SET p.status = $status", "parameters": { "id": "p-1", "status": "active" }, "provenance": { "actor": "agent:support-bot", "source": "crm-sync", "reason": "Customer confirmed by email", "confidence": 0.9 } }

Run a Cypher statement and return columns and rows. Cells are plain JSON by default; set format to "tagged" for type-tagged cells. parameters fills $name placeholders. atTime (ISO-8601) reads the graph as of that valid time; it requires at least one top-level MATCH and cannot be combined with a temporal clause in the query, and it does not set the valid time of a write. To record a fact as true from a past date, start the write with AT VALID '<ISO-8601>', for example AT VALID '2025-11-01T00:00:00Z' CREATE (p:Person {name: 'A'}); the prefix also works on MERGE, MATCH ... SET and MATCH ... DELETE. provenance records who made a write and why (actor, source, reason, requestId, confidence from 0.0 to 1.0); the server adds the calling credential as principal. Read it back with CALL db.propertyProvenance("p-1", "status") YIELD value, version, actor, reason, principal. From 1.4.1, a statement stops when the client’s connection closes; a read at a past time that would have to look at more of the graph’s history than the server allows returns error -32603 with a message that names TEMPORAL_READ_LIMIT (no data.subcode) and no rows; and a float that is infinite or not a number is returned as "Infinity", "-Infinity" or "NaN" (1.4: null). From 1.4.2, pass idempotencyKey (1 to 256 characters) when a call may be resent: the same key with the same request returns the first result with replayed: true and writes nothing; the same key with a different request is refused (-32602, IDEMPOTENCY_KEY_REUSED); a key whose first call is still running is waited for and then answers -32000 IDEMPOTENCY_KEY_IN_PROGRESS (send the same call again). The key is used only when the query writes. From 1.4.2 the server records the calling credential as principal for every write of an authenticated call, with or without provenance. From 1.4.2, when the graph has an access policy that restricts the credential, nodes and relationships it may not read are absent from the result and from counts, a write the policy does not allow is refused, and a procedure call, a read at a past time or EXPLAIN is refused with ACCESS_POLICY_SURFACE_UNAVAILABLE. From 1.4.2.4 a call without atTime answers what is valid now: a value written with a future AT VALID time is returned from that time on, and while the graph holds such a value the result carries the warning CURRENT_READ_VALID_NOW. With format: "tagged" a null cell is JSON null (it was the string "null"). From 1.4.2.5 the access-policy refusals ACCESS_DENIED and ACCESS_POLICY_SURFACE_UNAVAILABLE answer -32007 with data.subcode (they answered -32603 without a subcode), and a CREATE or MERGE that names more than 256 labels new to the graph (the default limit) is refused with -32602 TOO_MANY_NEW_LABELS before it runs.

get_node_by_str
{ "str_id": "alice" }

Return a node by its string id as {id, labels, properties, span}, or null if there is none. span holds valid_from, valid_to, txn_from and txn_to in nanoseconds and, from 1.4.2.4, the same times as validFrom, validTo, txnFrom and txnTo in ISO-8601 (null for an open end). From 1.4.2.4 the record is the version valid now: a value saved for a future date is not returned before that date.

get_edge_by_str
{ "str_id": "edge-abc" }

Return a relationship by its string id as {id, edge_type, source, target, properties, span}, or null. source and target are internal integer node ids. span and the version returned are as for get_node_by_str.

schema_info
{}

List labels and relationship types with counts, and the source and target labels of each relationship type. Property names are not included. From 1.4.1 the counts include the latest writes.

list_indexes
{}

List single-property (range) indexes and their state.

list_graphs
{}

List the graphs you can access. A credential tied to a tenant sees its tenant’s graphs; admin credentials see all graphs. From 1.4.2 a credential that has neither a tenant nor an admin role sees only the graph the connection is bound to.

engine_info
{}

Return a sequence number that increases as saved changes become visible (current_snapshot_seq) and the calling credential’s subject, tenant, roles and capabilities. From 1.4.2 the result has graph, the name of the graph this connection is bound to, and graph_dir (the graph’s storage path on the server) is returned to admin credentials only and is null otherwise; on 1.4.1 and earlier every caller gets graph_dir and there is no graph. Use it to check which graph and which credential a connection uses.

Search & recall

02 / 4 tools
search_fulltext
{ "label": "Document", "property": "body", "query": "pricing strategy", "k": 10 }

Keyword (BM25) search over one text property of a label. Returns up to k hits as {node_id, score}; node_id is the internal integer id. With the default analyzer, keywords match only words made of the letters a–z and the digits 0–9; when the query has other letters or digits (accented or non-Latin), the result carries the warning FULLTEXT_TOKENIZER_LIMITED. From 1.4.3, analyzer: "unicode-v2" indexes and matches every script (also unicode-v2+fold, unicode-v2+stem, unicode-v2+fold+stem). From 1.4.3, when a full-text index is declared on the label and property (CALL db.createFulltextIndex(label, property)), it answers by default (index: "auto"; "declared" or "adhoc" choose explicitly): the tool then takes syntax (words, legal, lucene, fts5), default_operator, order, cursor, max_expansions, snippets, total, at_valid and at_recorded, each hit also has str_id, matches and, when asked, snippets, and the result has index: "declared", total (the exact number of matches), complete, cursor (pass it back for the next page), parsed and stale. Send index: "adhoc" to keep the earlier answer. A declared-index argument on a label and property with no declared index is refused with -32602, with a message that names FULLTEXT_INDEX_NOT_FOUND; from 1.4.3.1 the refusal also carries the code in data.subcode (FULLTEXT_INDEX_NOT_FOUND, or FULLTEXT_OPTION_INVALID for an option value it does not accept). From 1.4.2 a node whose other label has a field read rule that hides the searched property from the caller is left out before the ranking, so the result holds the best k hits the caller may read, and hidden nodes are not counted in the scores of the other hits. From 1.4.3.3, with a declared index, the tool also takes fields (a list of properties or a map of property to weight; it must include property), filter and node_ids (applied before ranking), filter_statistics, matched_terms, excerpt and group_by; each hit then has field_scores, matched_words, matched_terms, excerpt, group and group_hits as asked. See Several fields, filters, matched words, excerpts, pages.

search_vector
{ "label": "Doc", "property": "embedding", "queryText": "late delivery complaints", "k": 10 }

Nearest-neighbour search over a vector property. Pass queryText to have the server embed the text, or vector with a precomputed embedding. Returns up to k hits as {node_id, score}. Pass exactly one of queryText and vector; both or neither is refused with -32602. From 1.4.1 the tool’s inputSchema no longer declares this rule as a top-level anyOf (some clients refuse one); the rule is in the property descriptions and the server enforces it. From 1.4.2 a node whose other label has a field read rule that hides the searched property from the caller is left out before the ranking, so the result holds the best k hits the caller may read (1.4.1 dropped such hits after the ranking and could return fewer than k). From 1.4.2.2, while the label’s vector data is being rewritten (after an upgrade or a re-ingest of the label) and the label is over the scan size limit, the search is answered exactly from the label’s records, more slowly, with the warning VECTOR_FILE_REWRITE_PENDING; it was refused with -32000 until the rewrite finished. That refusal remains when too many such searches run at once or the label holds more than 16 GiB of vectors.

recall
{ "query": "address mismatch", "k": 10 }

Search saved memories (text written by remember and record_episode), best matches first. From 1.4.1 each search runs by keyword and by meaning and the two rankings are combined. The result has memories: up to k matches (1 to 100, default 10; match: "match"), each followed by the turns before and after it in the same session (match: "context"; neighbours, 0 to 3, default 1), each with id, text, payload, kind, session, validFrom and savedAt; count, the number of matches; documents: passages from the current version of saved documents as {uri, revision, title, text, validFrom} (documents, 0 to 20, default 3); facts: facts saved with remember that are current now, each with subject, predicate, value or object, validFrom, validTo, source, single and previously, the earlier values it replaced (facts, 0 to 50, default 10); and search, which says how the search ran. When the search by meaning could not run, search.warning says so and the results come from keyword search only; search.notYetSearchableByMeaning counts memories whose embedding is still pending. On 1.4 and earlier recall searches by keyword only, returns {memories: [{id, text, payload}], count} and does not search saved documents: to find text in documents there, pass "label": "Chunk", "property": "text", or list documents with cypher_query and read one with get_document. Keywords match only words made of the letters a–z and the digits 0–9; when the query has other letters or digits (accented or non-Latin), the result carries a note saying that memories containing them may be missing. From 1.4.2 a search that asks to count, total, list or compare (for example “how many”, “in total”, “list all”, “average”) reads wider: up to 40 more matches after the first k (without neighbouring turns, marked wide: true), the other current facts with the same subject and predicate as the top facts, and items, a numbered list of the distinct matching events and facts in date order, at most 60. An event stated in several memories is one item; each item has line (the item as one line for a reader), happenedOn (when it took place, when the fact was saved with its own date), mentionedOn (the date of the memory that states it), facts and mentions (the facts and memories that state it), matches (whether it matches the search) and, when the search names a period such as “in the past two weeks”, inWindow. A value that a later one replaced is listed under the current item’s previously, not as an item. The list can include items that do not answer the question: count the items yourself. Options: breadth (auto, the default; normal never reads wider; wide always does), items (false leaves the list out), breadthBudget (the most tokens the wider read adds, default 4000, at most 16000) and referenceTime (ISO-8601; the time a period in the search is counted from, by default now). search.breadth says when a search read wider and why. The search is read with English cue words; pass breadth: "wide" for a question phrased without them. Other searches return what they returned on 1.4.1. Figures computed by the server (aggregates) are experimental and off by default. From 1.4.2 a memory or fact hidden from the caller by a field read rule does not change the scores of the others. From 1.4.3, when a full-text index is declared on the recalled label and property and is ready (CALL db.createFulltextIndex('Event', 'text')), the keyword side reads it, so memories written in any script are found by keyword; while it is built or rebuilt, recall searches as before. From 1.4.3 recall is faster, with the same answers. From 1.4.3.1 recall adds linked memories (on unless the server operator turns them off): when the search names a person, place or thing that saved facts are about (I, me and my name the user), those facts, and the facts of the subjects of the top facts, are followed one or two steps to other facts that share a topic word with the search or are as close in meaning as the best facts; the memories that state those facts and were not already returned follow the others, with match: "linked" and via (the entity, hops and the fact that linked it), at most 12 and within linkBudget tokens (default 2000, at most 8000). search.links says when this happened. Most linked memories do not answer the question; they widen what a reader sees. Send links: "off" to leave them out.

recall_as_of
{ "atTime": "2026-03-05T00:00:00Z", "query": "risk score", "k": 10 }

Search memories as they stood at a past valid time. A memory counts from its validFrom: the time given to remember, or the moment it was saved. A memory saved without validFrom does not appear at an earlier atTime even when its text names an earlier date. From 1.4.1, with query the search is the same as recall (keyword and meaning, neighbouring turns, the same result shape) over the memories valid at atTime; document passages come from the version current then, and facts are those current then, a value replaced later carrying validTo. Without query the memories valid at atTime are listed, latest validFrom first. k is at most 100. On 1.4 and earlier, query keeps memories whose text contains that exact text (case-sensitive), results are not ranked, the result is {columns, rows} and k is at most 200. From 1.4.2, with query, a search that asks to count, total, list or compare reads wider as recall does (breadth, items, breadthBudget), over what was valid at atTime; a period in the search is counted from atTime unless referenceTime is given. From 1.4.2.4, with query the search finds a memory that was valid at atTime and deleted afterwards; before, it left such memories out. From 1.4.3 the keyword side reads a declared full-text index on the recalled label and property only when it was declared with history: 'all'. From 1.4.3.1, with query, linked memories are added as for recall (links, linkBudget), over what was valid at atTime.

Documents

03 / 7 tools
save_document
{ "uri": "docs://pricing-policy", "revision": "3", "title": "Pricing policy", "text": "...", "entity_types": ["Product", "Supplier", "Region"], "min_confidence": 0.6 }

Save a document under a stable uri as its current version; earlier versions stay in history. When fact extraction is enabled on the server, people, facts and relationships are also extracted; extracted is true when extraction ran and found at least one fact or entity. entity_types and min_confidence apply to this save. From 1.4 the tool waits up to about 45 seconds (before 1.4: about two minutes); if the save is still running it returns pending: true with the save’s jobId and status, and the same call again reports on it. From 1.4, pass idempotencyKey (1 to 256 characters) when a call may be resent: the same key with the same request returns the first result with replayed: true; the same key with a different request is refused (-32602, IDEMPOTENCY_KEY_REUSED). From 1.4.2.2 a save that a rule of the graph refuses (a constraint; for example the same uri and revision saved again with the text changed in a passage that recorded quotes cite) answers saved: false, refused: true, reason: "constraint", and nothing of the save is written; save the changed text under a new revision.

get_document
{ "uri": "docs://pricing-policy" }

Return the current version of a document as one row per chunk, in order. Pass as_of (ISO-8601) for the version that was current at that time.

list_document_revisions
{ "uri": "docs://pricing-policy" }

List the saved versions of a document, oldest first, with the time each was current (validFrom, validTo; validTo is null for the current one) and when it was saved (txnAt). From 1.4.2.4 these times are ISO-8601 and the nanoseconds are in the last columns validFromNanos, validToNanos and txnAtNanos; before, the three columns held nanoseconds. To read a past version, pass its validFrom to get_document as as_of.

list_documents
{ "limit": 50 }

From 1.4.2.4. List saved documents, most recently saved first: uri, title, revision (the current version’s label), savedAt (ISO-8601) and savedAtNanos. limit defaults to 50, at most 500. Use it to find the uri of a document saved in an earlier conversation before you save a new revision of it.

find_document
{ "query": "pricing" }

From 1.4.2.4. Find saved documents whose title or uri contains query, ignoring case. Same output as list_documents; limit defaults to 20, at most 500.

set_ontology
{ "entity_types": ["Product", "Supplier", "Region"], "relationship_types": ["SUPPLIES", "SOLD_IN"], "min_confidence": 0.6 }

Set the categories extraction uses on this graph, and optionally relationship types and a minimum confidence. Each call replaces the category and relationship lists. From 1.4.2, pass idempotencyKey (1 to 256 characters) when a call may be resent: the same key with the same request returns the first result with replayed: true and writes nothing; the same key with a different request is refused (-32602, IDEMPOTENCY_KEY_REUSED); a key whose first call is still running is waited for and then answers -32000 IDEMPOTENCY_KEY_IN_PROGRESS (send the same call again).

describe_memory
{}

Show the categories and relationship types set up for this graph and how many items each category holds. From 1.4.1 the counts include the latest writes.

Agent memory

04 / 5 tools
record_episode
{ "session": "conv-abc-123", "kind": "observation", "text": "User asked about Q3", "payload": {"topic": "forecast"} }

Append an entry to a session’s log. The session is created on first use. Returns eventId and ts. From 1.4, pass idempotencyKey (1 to 256 characters) when a call may be resent: the same key with the same request returns the first result with replayed: true; the same key with a different request is refused (-32602, IDEMPOTENCY_KEY_REUSED). From 1.4.1, pass validFrom (ISO-8601) to say when the event happened; the default is the time of the save; on 1.4.1 to 1.4.2.3 a time more than 5 minutes in the future is refused, and from 1.4.2.4 a future time is accepted as for remember. From 1.4.1 the result also has validFrom and embedding: stored (the event can be found by meaning at once), pending (the embedding model was not available; the event is saved, keyword search finds it at once, and a job started by this save, or by a later save or recall call, adds it to search by meaning; no timer starts that job) or off (the server has no embedding model).

replay_episodes
{ "session": "conv-abc-123", "limit": 50 }

Return a session’s events oldest first, up to limit (default 50).

remember
{ "sessionId": "acme-account", "kind": "preference", "text": "Acme prefers quarterly reviews." }

Save one fact, decision or preference as a memory. Find it again with recall. From 1.4.1, pass validFrom (ISO-8601, for example 2023-03-01) when the fact became true at an earlier time; without it the memory is valid from the moment it is saved; a value that is not an ISO-8601 time is refused with -32602. On 1.4.1 to 1.4.2.3 a time more than 5 minutes in the future is refused (-32603). From 1.4.2.4 a future time is saved and the memory becomes current at that time: recall finds it from then on, and recall_as_of with an atTime at or after it finds it at once; with facts a future time is refused (-32602, FUTURE_VALID_FROM_WITH_FACTS). To keep an upcoming date findable now, leave validFrom out and write the date in text. On 1.4 and earlier remember has no input for an earlier date; write the fact with cypher_query and an AT VALID prefix. From 1.4.1, facts takes up to 64 facts the text states, saved in the same transaction: each has subject, predicate, and value (a string, number or boolean) or object (a person, place or thing), with optional text, validFrom, single and confidence. A fact with single: true and a new value replaces the value that was current at its validFrom; the earlier value is kept as true until then. Predicates that differ only in case, separators, a count wrapper (number_of_x, x_count) or a plural s are one predicate. Facts are shared by the whole graph. From 1.4.1 the result also has validFrom, embedding (stored, pending or off, as for record_episode) and, when facts were given, facts: per fact {fact, status, validFrom} with status created or restated; a created fact also has embedding, plus supersedes when it replaced a value and supersededBy when a later value replaces it; a restated fact has only the first three. From 1.4.2.4 the result also has eventId and savedAt (ISO-8601). A memory whose embedding cannot be computed is saved with the embedding pending; the write does not fail. From 1.4, pass idempotencyKey (1 to 256 characters) when a call may be resent: the same key with the same request returns the first result with replayed: true; the same key with a different request is refused (-32602, IDEMPOTENCY_KEY_REUSED).

forget
{ "str_id": "memory-123" }

Not available in this release: each call returns error -32006 with data.subcode TOOL_UNAVAILABLE. The error’s data.alternative names destroy_subject_key, which is in development; contact us about erasure requirements.

belief_lineage
{ "belief_key": "churn-risk-acme" }

Trace a belief back through its revisions to the first version, one row per version. Evidence is not included; to get it, run CALL db.beliefLineage("churn-risk-acme", 1) YIELD beliefId, claim, evidence through cypher_query.

Provenance & compliance

05 / 5 tools
property_provenance
{ "str_id": "claim-99", "property": "approved" }

Show each version of a property with the attribution recorded for it: actor, source, reason, requestId and confidence. Attribution is present for writes that included provenance. From 1.4.2 the result has the column principal, the credential that made each write; the server records it for every write of an authenticated request and clients cannot set it. It is null for unauthenticated writes, for writes made before 1.4.2 without provenance, for import and other background jobs, and for the legacy POST /write route. On 1.4.1 and earlier, run CALL db.propertyProvenance(...) YIELD ..., principal through cypher_query. Identify the node with str_id or node_id; passing neither is refused with -32602. From 1.4.1 the tool’s inputSchema no longer declares this rule as a top-level anyOf. From 1.4.2.4 validFrom, validTo and txnAt are ISO-8601 (before: nanoseconds), the nanoseconds are in the new last columns validFromNanos, validToNanos and txnAtNanos, a version that a later write replaced shows where it ended (before: null when the later write had no AT VALID time), and a null cell is JSON null.

derived_from
{ "node_str_id": "claim-99", "max_depth": 5 }

Follow DERIVED_FROM relationships from a node to its sources, up to max_depth hops. Returns source, kind and depth. From 1.4.2.2 node_str_id is resolved as db.linkDerivedFrom resolves it: a storage string id, then an id property value. A reference that names no node is refused (-32603, data.subcode LINEAGE_NOT_FOUND; LINEAGE_AMBIGUOUS when an id value matches more than one node); a node that exists and has no sources answers no rows. On 1.4.2 and earlier only the storage string id was read, and an id that named no node returned no rows and no error.

acl_events
{ "since_id": 0, "principal_filter": "apikey:key_123", "limit": 100 }

List field reads that access rules denied in Cypher, and the searches and node creates they refused, with the credential, label, property, time and reason. From 1.4.3.3, with no principal_filter (or an empty one) it returns every event the caller may see: all events for an administrator, the caller’s own events otherwise (before, it returned nothing).

subject_export
{ "subject": "user_42" }

Export one data subject’s node, its relationships and its property versions (GDPR Article 20). On a graph with no declared encrypted property it matches only the node whose string id equals the argument. A subject id held only in a property is not matched, and nodes created with Cypher CREATE or MERGE get a generated string id. An empty result does not show that the graph holds nothing about the subject.

destroy_subject_key
{ "subject": "user_42" }

Destroy one data subject’s encryption key and record a receipt. Crypto-shred (per-subject encryption keys and key destruction) is in development: on the current release, declaring an encrypted property is refused by default, and the call is refused without recording a receipt. Contact us before relying on it. The operation cannot be reversed, so get confirmation from a person before calling it.

From 1.4.2 · experimental · off by default

Run traces and shared workspace

These tools are experimental. 1.4.2 adds nine tools that record what an agent did, step by step, in the graph that holds its memory, and let several agents share tasks. They are listed and served only where the server operator has turned them on. Otherwise they are not in tools/list, and a call answers -32601. The same operations are HTTP routes: POST /graphs/{name}/trace/{start|step|end|replay} and POST /graphs/{name}/workspace/{task_update|task_claim|note_post|note_ack|verdict_post}; the body is the tool’s argument object.

  • trace_start creates a run. trace_step records one step of a run (run, kind) with its payloads and its links to the memory nodes it read or wrote, in one write; it accepts idempotencyKey. From 1.4.2.5 a step sent without ordinal can be confirmed by sending the same content again with the ordinal the server returned; the recorded step comes back with replayed: true (it was refused with STEP_EXISTS). trace_end sets the run’s final status.
  • trace_replay returns the steps of a run up to a step, and the memory nodes those steps read or wrote as they were stored when that step was recorded. It only reads. One answer holds at most 500 steps and about 4 MiB of steps, and reads at most 2,000 memory nodes; fromStep, truncated and memoryTruncated say what was left out.
  • task_update creates or changes a shared task. task_claim claims a task for an agent and is refused with -32009 TASK_HELD while another agent holds it; of two claims made at the same time under different agent names, one succeeds. note_post posts an instruction, question or answer to an agent or a task, note_ack records that an agent read a note, and verdict_post adds a review result on a task.
  • Payload text is limited to 64 KiB per payload by default. Pass a larger payload as ref, sha256 and bytes; the server stores the reference and does not fetch the object.
  • Every write records the calling credential (recorded_by on the node, and principal in the provenance of the write). No argument sets it. The optional agent argument is the caller’s name for itself and is not tied to a credential.
  • If a step cannot be stored, trace_step answers recorded: false and the run is marked incomplete; onRecordFailure: "fail" on trace_start returns the error instead.
  • The eight write tools need a credential that may write when the server enforces write roles, and a capability token needs the write scope for them (-32007). A private-mode credential is refused on all nine.
  • On a graph that holds runs or a workspace, the labels Run, Step, Payload, Agent, Task, Verdict and Note are reserved, and set_ontology refuses them as categories.
  • Limits: runs, steps, tasks and notes are ordinary graph data, so a credential with write access to the graph can change or delete them with Cypher, recorded_by included; earlier versions stay in the graph’s history. Claims do not expire. No tool lists an agent’s unread notes. Field write rules do not apply to these tools. The SDKs do not include them.
Security

Access control and audit

  • Which tools change data. cypher_query runs any Cypher statement, including CREATE, MERGE, SET, DELETE, DETACH DELETE, and index and constraint statements. remember, record_episode, save_document and set_ontology write to the graph. destroy_subject_key destroys a subject’s encryption key (in development). The other tools only read.
  • History. Committed node and relationship writes are appended to the graph’s hash-chained history; verify it with GET /graphs/{name}/audit/verify. Field reads that access rules deny in a Cypher statement, refused searches and refused node creates are logged separately; verify that log with CALL db.verifyAclChain() and read it with the acl_events tool. Tool calls are not logged as such. A call leaves an entry in that log only in those cases; a field left out by a tool that reads one record is not logged.
  • Private-mode API keys. Only expressions built on count, sum or avg may be returned; noise is added and each aggregate spends from the key’s ε budget. This release does not give a differential-privacy guarantee; do not rely on private mode alone to protect individual records. See private-mode details.
  • Capability tokens. An admin can issue a token bound to one graph with POST /capabilities/issue. Tokens last at most 300 seconds by default. Send one as Authorization: Capability <token>. On the MCP endpoints only the graph binding is checked; the token’s scope does not limit which tools run, except the run trace and workspace write tools (from 1.4.2), which need the write scope.
  • Access policies (from 1.4.2). When a graph has an access policy that restricts the credential, cypher_query and the tools that run Cypher answer as that credential. get_node_by_str and get_edge_by_str answer a record the credential may not read as a missing one. A credential whose reads are restricted is refused search_fulltext, search_vector, recall, schema_info and describe_memory, the tools that call a procedure or read at a past time (for example list_indexes, replay_episodes, belief_lineage, recall_as_of), save_document and set_ontology, with -32007 ACCESS_POLICY_SURFACE_UNAVAILABLE and nothing run; save_document and set_ontology are also refused to a credential whose writes only are restricted. A credential the policy does not restrict uses every tool as before. See the Cypher reference.
  • Field read rules and search (from 1.4.2). A node that a field read rule hides from the caller through another of its labels is left out of search results and is not counted in the scores of the other hits.
Protocol

Protocol and result format

  • JSON-RPC 2.0 over HTTPS POST. Methods: initialize, tools/list, tools/call. Other methods, including ping, resources/list and prompts/list, return error -32601. The server does not use GET event streams or session ids, and does not accept batch requests.
  • initialize accepts protocol versions 2025-06-18, 2025-03-26 and 2024-11-05 and returns the version the client asked for, or 2025-06-18 for any other value. Server name: invariantdb-mcp. Capabilities: tools only.
  • The initialize result includes an instructions string for the model. It tells the model how to use the memory and document tools, and asks it to describe results in plain language without technical terms such as Cypher, node labels or indexes. From 1.4.1 it also asks the model to save a lasting fact the user mentions (a name, date, number, booking, place or preference) with remember before it answers, with validFrom when the fact became true earlier, and to tell the user when a recall result carries search.warning.
  • Each response is one JSON body. If the request’s Accept header lists text/event-stream, the same response is sent as a single server-sent event.
  • A tool result has content (one text item containing the result as JSON) and structuredContent (the same result as an object). Tool failures are returned as JSON-RPC errors, not as results with isError: true.
  • Request bodies larger than 2 MB are refused with HTTP 413.
Errors

Error codes

  • -32700 The body is valid JSON but not a single JSON-RPC request object, for example a batch array.
  • -32601 Unknown method or tool.
  • -32602 Invalid arguments: a missing or wrongly typed argument; a Cypher syntax error; atTime that is not a string, is combined with a temporal clause, or is used on a query with no top-level MATCH; an invalid provenance.confidence; an unknown embedding model; or document saving not enabled on the server. From 1.4 also IDEMPOTENCY_KEY_REUSED and IDEMPOTENCY_KEY_INVALID. From 1.4.2.2 also STATEMENT_TOO_LARGE (statement text over 2,097,152 bytes) and PARSE_BUDGET_EXCEEDED (the statement could not be parsed within its budget). From 1.4.2.4 also CLOSED_INTERVAL_WRITE_UNSUPPORTED (a write with a closed valid interval) and FUTURE_VALID_FROM_WITH_FACTS. From 1.4.2.5 also TOO_MANY_NEW_LABELS. From 1.4.3 also search_fulltext with a declared-index argument and no declared index (the message names FULLTEXT_INDEX_NOT_FOUND; from 1.4.3.1 data.subcode carries it). Fix the request before you retry.
  • -32603 Cypher execution error (on 1.3.x including constraint violations), or an internal error. The message gives the cause. From 1.3.10, data.subcode is ARITHMETIC_ERROR for integer overflow or division by zero (change the statement) and INTERNAL_ERROR for an engine fault (report it). From 1.4, READ_ONLY_INSTANCE: this server accepts no writes. From 1.4.1, a read at a past time that is over the server’s history limits arrives here with a message that names TEMPORAL_READ_LIMIT and without data.subcode; read one label at a time or use a later time.
  • -32000 Nothing ran; wait data.retryAfterMs milliseconds and retry. data.subcode says why: GRAPH_CONCURRENCY_CAP (the graph is at its concurrent-query limit), GRAPH_BUSY, SEARCH_BUSY, GDS_BUSY, QUERY_MEMORY_BUDGET, or a vector build or graph algorithm that does not fit in memory yet. From 1.4 also WRITE_LOCK_TIMEOUT and WRITE_LOCK_QUEUE_FULL (another write statement held the graph too long), MEMORY_PRESSURE, DISK_SPACE_LOW and IDEMPOTENCY_KEY_IN_PROGRESS.
  • -32003 The credential lacks the capability the tool requires. Returned only when the server enforces tool capabilities.
  • -32004 The query is not in the approved query catalog for your tenant.
  • -32005 The approved catalog entry requires a capability the credential lacks.
  • -32006 The tool is not available in this release (forget, data.subcode TOOL_UNAVAILABLE). data.alternative names a related tool (destroy_subject_key, in development). Erasure refusals from destroy_subject_key also use -32006, with a CRYPTO_SHRED_* subcode. From 1.4 also VECTOR_TIER_REFUSED: a vector index type the server no longer serves; rebuild the index as flat or hnsw. From 1.4.2.4 also VALID_END_BEFORE_VALID_FROM: a delete whose valid end would come before the version’s start; nothing was deleted (from 1.4.2.5 also a delete without AT VALID of a version that starts later). From 1.4.2.5 also WRITE_WOULD_REPLACE_LATER_VERSIONS: a backdated write would replace versions recorded for later valid times and lose their values; nothing was written.
  • -32007 QUERY_TIMEOUT: the statement ran past its time limit and nothing was written; narrow it before you retry. Also an access refusal where nothing ran: WRITE_FORBIDDEN (the credential may not write), FIELD_READ_FORBIDDEN (a search over a property the credential may not read) or PRIVATE_MODE_NON_AGGREGATE_RETURN. From 1.4.2 also ACCESS_POLICY_SURFACE_UNAVAILABLE: the graph’s access policy restricts the credential and the tool or statement form is not available to it. From 1.4.2.5 every tool answers access-policy refusals here, with data.subcode ACCESS_DENIED or ACCESS_POLICY_SURFACE_UNAVAILABLE; on 1.4.2 to 1.4.2.4 the tools that run Cypher answered them with -32603 without a subcode.
  • -32008 STATEMENT_PARTIALLY_APPLIED: the statement stopped after part of its writes were committed (from 1.4 only a statement too large to commit at once, which commits in parts, can end this way). The committed part is kept; read the data back and do not resend the whole statement. Also DEFAULT_GRAPH_FORBIDDEN on POST /mcp (see Self-hosted servers).
  • -32009 Do not resend the call as it is; data.subcode says why: WRITE_OUTCOME_UNKNOWN (the write could not be confirmed; read the data back), the same index build or another heavy operation is already running, a vector build or graph algorithm cannot fit in memory on this server, or SEARCH_LABEL_TOO_LARGE (the search has to read a label larger than the server allows). From 1.4 also CONSTRAINT_VIOLATION (the write would break a constraint; nothing was written) and the other schema-rule refusals, STATEMENT_MEMORY_LIMIT (the statement needs more memory than one statement may use), and MEMORY_PRESSURE or DISK_SPACE_LOW after earlier writes of the call committed.

From 1.4.2.4, results carry times as ISO-8601 strings where this page says so, with the nanoseconds in separate ...Nanos fields or columns; other times in tool results are nanoseconds since the Unix epoch. The reserved Cypher properties _valid_from, _valid_to, _txn_from and _txn_to return milliseconds.

From 1.4.2 every error carries data.retryable (true or false) and data.retry: retryable, terminal, or ambiguous (the call may have been applied; resend it only with its idempotencyKey).

HTTP errors are returned before MCP handling: 400 when the body is not valid JSON; 401 when the credential is missing or invalid (for clients that use OAuth sign-in, the WWW-Authenticate header points to the sign-in metadata); 403 when the key’s graphs list excludes the graph, the graph belongs to another tenant, or a capability token is bound to a different graph; 404 when the graph does not exist; 413 when the body is too large; 429 when one address sends too many requests (retry after the time in the Retry-After header).

Self-hosted

Self-hosted servers

If you run your own InvariantDB server, each graph’s MCP endpoint is POST /graphs/{name}/mcp on your server’s address. A self-hosted server also serves POST /mcp, which runs tools against that server’s default graph. Authentication uses the same API keys. On the hosted service, use the per-graph endpoint: from 1.3.9, POST /mcp there answers every method, initialize included, with error -32008 and data.subcode DEFAULT_GRAPH_FORBIDDEN unless the credential is an operator credential or the default graph’s owner. A self-hosted operator can turn on the same rule.

See the SDK page for client invocation examples.

Not available over MCP

Operations not available over MCP

Creating and deleting graphs, managing API keys, backup and restore, and server maintenance are not MCP tools; use the HTTP API with an admin credential. Data changes, including deletes and index and constraint statements, are available through cypher_query and the memory and document tools.

To limit which graphs an agent can reach, give it an API key whose graphs list names only those graphs. To control which properties Cypher statements can read, declare field access rules in the graph schema. Write rules are checked only when a Cypher CREATE creates a node.

Get started

Connect an agent

Create an API key, then add the endpoint to your MCP client.