Agent memory

One memory for every agent.

Agents forget. Semurg gives them a shared memory that outlives a process: facts and relationships written once, found again by the next agent, the next session and the next framework. It speaks MCP and plain HTTP, and it is the same store that serves your graph, vector and retrieval work.

What the memory layer is

Remember

An agent stores entities and the relationships between them, the passages they came from, or plain text, with an optional source label and date. A relationship may name an entity an earlier call stored; a name stored nowhere is refused by name, so a misspelling never slips in. Plain text is stored at once and linked to the names already stored, and when an extraction model is installed it reads the text into new entities and relationships moments later. Storing the same thing again changes nothing.

Recall and context

Name an entity and get back its own facts first, then what is one step away; or ask in plain words and memory walks the graph up to three steps from the names, keywords and meaning it finds. Every item carries a citation: the source it came from, when it was stored and the exact words it quotes. The reply says whether it answered, found nothing stored, or could not judge, and a question also gets a verdict on whether every word of it is covered.

Relate and forget

Add one typed relationship between two entities; an end that is not stored yet is created. Forget deletes one thing per call: an entity with everything that names it, one fact, one stored document, every document under a source label, or the whole scope. What is forgotten is unreachable through every door at once, though it is not yet wiped from the disk; that is not yet released. Forgetting it again answers that it was already forgotten.

Agents on the same node that use the same scope share what they learn, and a different scope stays private. The memory is stored in Semurg itself, not in a separate service, so there is nothing extra to run.

Seven tools

ToolWhat it does
memory_statusReports whether shared memory is on for this node and which of your scopes hold facts.
memory_rememberStores entities, the relationships between them and the passages they came from, or plain text, with an optional source label and date.
memory_recallRecalls what is stored about one named entity, its own facts first, each item cited.
memory_relateAdds one typed relationship between two entities in a scope, creating an end that is not stored yet.
memory_forgetDeletes one thing per call: an entity and everything that names it, one fact, one document, every document under a source label, or the whole scope. Unreachable at once; not yet wiped from the disk.
memory_contextAnswers a question in plain words with cited items and a verdict on whether the store covers it.
memory_documentTakes a whole document (PDF with a text layer, Word, HTML or plain text) and stores it as cited passages in a scope.

Two doors, one store

MCP

Point any MCP client at /v1/mcp on your node. It speaks streamable HTTP and takes a bearer token. An optional X-Semurg-Scope header sets the connection's default scope (a scope in the call wins), and X-Semurg-User pins the connection to one end user, so the agent cannot reach anyone else's memory through it. Current and earlier protocol revisions are served from the same address.

HTTP

The same verbs are plain HTTP under /v1/memory, and they are in the client SDKs.

Try it over HTTP

Both calls ran against a fresh node on release r11.131.20261027.1c0ba4a, and each answer was read back. They use http://127.0.0.1:4000, the address a fresh node serves; behind your own TLS proxy use its https address instead. Set SEMURG_API_TOKEN to your node's API token.

curl -sS http://127.0.0.1:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"demo","facts":{"entities":[{"name":"Acme","type":"Company"},{"name":"Dana","type":"Person"}],"relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]},"source":"curl example"}'
curl -sS http://127.0.0.1:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"demo","subject":"Dana"}'

Integrations

Every agent framework below connects to the same memory. A status appears only when a run has been recorded, and it names the release the run was made on. Anything else reads Results pending.

Every command and snippet below reads your node's API token from the environment variable SEMURG_API_TOKEN. Set it once from the file your node keeps it in (it is never printed): export SEMURG_API_TOKEN=$(sudo grep ^SEMURG_API_TOKEN= /etc/semurg/semurg.env | cut -d= -f2-) on a node installed by the installer, or export SEMURG_API_TOKEN=$(docker exec semurg cat /var/lib/semurg/.api_token) in Docker. If it is not set, the scripts stop and tell you; a curl command run without it is simply answered 401 (unauthorised) by the node, which means the token was missing or wrong. Addresses below are http://127.0.0.1:4000, the address a fresh node serves on the machine it runs on; use https only if you put the node behind your own TLS proxy, and then use that address instead.

IntegrationKindStatus
Hermes AgentAgentConnected
OpenClawAgentConnected
LangChain / LangGraphLibrary, PythonRun end to end
CrewAILibrary, PythonRun end to end
Microsoft Agent FrameworkLibrary, PythonRun end to end
OpenAI Agents SDKLibrary, PythonRun end to end
LlamaIndexLibrary, PythonRun end to end
MastraLibrary, TypeScriptRun end to end
Google ADKLibrary, PythonRun end to end
Pydantic AILibrary, PythonRun end to end
Vercel AI SDKLibrary, TypeScriptRun end to end
LangChain (Elixir)Library, ElixirRun end to end
JidoLibrary, ElixirRun end to end
Claude CodeAgentConnected
Claude DesktopAgentResults pending
CursorAgentResults pending
OpenAI Codex CLIAgentResults pending
Gemini CLIAgentResults pending
ChatGPT (custom connector)AgentResults pending

Agents

Connect with the agent's own configuration. Connected means the agent's own client listed every memory tool the node serves: nine on this release. Memory calls need a scope, a named and separate memory: each configuration below sends one in the X-Semurg-Scope header, so change it per project.

Hermes Agent Connected

one entry in ~/.hermes/config.yaml mcp_servers; token in ~/.hermes/.env.

Connected on release r11.131.20261027.1c0ba4a: the agent's own client listed every memory tool that release served. Hermes Agent 0.22.5.

curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --non-interactive
mcp_servers:
  semurg:
    url: "http://127.0.0.1:4000/v1/mcp"
    headers:
      Authorization: "Bearer ${SEMURG_API_TOKEN}"
      X-Semurg-Scope: "default"

OpenClaw Connected

openclaw mcp set <name> <json>, then openclaw mcp doctor <name> --probe.

Connected on release r11.131.20261027.1c0ba4a: the agent's own client listed every memory tool that release served. OpenClaw 2026.10.7.

curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
openclaw mcp set semurg '{"url":"http://127.0.0.1:4000/v1/mcp","transport":"streamable-http","headers":{"Authorization":"Bearer ${SEMURG_API_TOKEN}","X-Semurg-Scope":"default"}}'
openclaw mcp doctor semurg --probe

Claude Code Connected

one user-scope entry written by claude mcp add-json, so it reaches every Claude Code session on the machine (for one repository use --scope project instead: Claude Code asks once to approve the project .mcp.json, and a headless run passes --mcp-config); the token stays an environment-variable reference.

Connected on release r11.131.20261027.1c0ba4a: the agent's own client listed every memory tool that release served. Claude Code 2.1.311.

curl -fsSL https://claude.ai/install.sh | bash
claude mcp add-json --scope user semurg '{"type":"http","url":"http://127.0.0.1:4000/v1/mcp","headers":{"Authorization":"Bearer ${SEMURG_API_TOKEN}","X-Semurg-Scope":"default"}}'
# Claude Code reads the token from the SEMURG_API_TOKEN environment variable when it starts

Claude Desktop Results pending

Claude Desktop runs on macOS and Windows and Semurg runs on Linux x86_64, so the node is reached at 127.0.0.1:4000 one of two ways: on a Windows machine, run the node on the same machine under WSL2 (Ubuntu 24.04) (Docker Desktop on Windows is being tested); otherwise run it on a Linux machine and forward its port to the desktop with ssh -N -L 4000:127.0.0.1:4000 you@your-node, so the token never crosses the network over plain HTTP. A remote node over HTTPS with sign-in (OAuth) is not covered here yet. Claude Desktop starts only local (stdio) MCP servers, so the mcp-remote bridge, run by npx (it needs Node.js), carries it to the node's streamable HTTP door. Put the configuration below in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %APPDATA%\Claude\), put your token in place of <token>, restart Claude Desktop, and keep the file private: it holds the token. It is Connected only once its own client has listed every memory tool the node serves.

{
  "mcpServers": {
    "semurg": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:4000/v1/mcp",
               "--header", "Authorization:${SEMURG_AUTH}",
               "--header", "X-Semurg-Scope:default"],
      "env": { "SEMURG_AUTH": "Bearer <token>" }
    }
  }
}

Cursor Results pending

a remote server is declared with url and headers in ~/.cursor/mcp.json, or .cursor/mcp.json inside a project, and the token stays an environment-variable reference, so the file holds no token; the Cursor CLI is documented to use the same configuration as the editor, and agent mcp list-tools semurg prints the memory tools. It is Connected only once its own client has listed every memory tool the node serves.

curl https://cursor.com/install -fsS | bash
{
  "mcpServers": {
    "semurg": {
      "url": "http://127.0.0.1:4000/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${env:SEMURG_API_TOKEN}",
        "X-Semurg-Scope": "default"
      }
    }
  }
}

OpenAI Codex CLI Results pending

codex mcp add NAME --url URL --bearer-token-env-var VAR, or a [mcp_servers.NAME] table with url and bearer_token_env_var in ~/.codex/config.toml; codex mcp list and codex mcp get show only the configuration and never contact the node, and the documentation describes no command that lists a server's tools, so a run of codex exec is what shows Codex reaching it: its MCP startup sends initialize and tools/list before it needs a model, and exec then fails on the OpenAI side when no credentials exist. Memory calls need a scope: Codex's config.toml documents http_headers for it, so add http_headers = { "X-Semurg-Scope" = "default" } under [mcp_servers.semurg] (we have not run that line). It is Connected only once its own client has listed every memory tool the node serves.

npm install -g @openai/codex
codex mcp add semurg --url http://127.0.0.1:4000/v1/mcp --bearer-token-env-var SEMURG_API_TOKEN

Gemini CLI Results pending

gemini mcp add --transport http --header "Authorization: Bearer ..." NAME URL, or mcpServers.NAME with httpUrl and headers in ~/.gemini/settings.json; gemini mcp list shows each server's configuration and connection status. Trust the project folder first: in a folder Gemini does not trust it disables every MCP server (for a headless run the setting that worked was security.folderTrust.enabled=false in settings.json, which switches that check off for every folder). gemini mcp add writes the token you give it into ./.gemini/settings.json as plain text, so keep that file out of version control; the ${SEMURG_API_TOKEN} reference shown below was observed to work on Gemini CLI 0.63.0 although its documentation describes expansion only for the env block. It is Connected only once its own client has listed every memory tool the node serves.

npm install -g @google/gemini-cli
{
  "mcpServers": {
    "semurg": {
      "httpUrl": "http://127.0.0.1:4000/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${SEMURG_API_TOKEN}",
        "X-Semurg-Scope": "default"
      }
    }
  }
}

ChatGPT (custom connector) Results pending

a remote MCP server added as a connector in ChatGPT; third-party guides state that the server must be reachable from the public internet over HTTPS, so a laptop or a private node cannot be tested.

Libraries

Each demo is customer-side code in the framework's own language. It calls Semurg over MCP or HTTP and runs in its own container. Each snippet shown here is a complete program you can paste into a file and run: it reads SEMURG_API_TOKEN from the environment, sends it with a scope in the X-Semurg-Scope header, stores two facts and reads them back. A snippet is shown only after a recorded run executed that exact file as written against a fresh node. URL defaults to http://127.0.0.1:4000/v1/mcp; set SEMURG_MCP_URL to use another address.

LangChain / LangGraph Run end to end

MultiServerMCPClient({...}).get_tools(), bound into a LangGraph agent.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. langchain-mcp-adapters 0.3.2, langchain-core 1.7.0, mcp 1.30.0.

pip install langchain-mcp-adapters
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from langchain_mcp_adapters.client import MultiServerMCPClient

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    client = MultiServerMCPClient({"semurg": {"url": URL, "transport": "streamable_http", "headers": HEADERS}})
    tools = {t.name: t for t in await client.get_tools()}
    await tools["memory_remember"].ainvoke({"facts": FACTS})
    print(await tools["memory_recall"].ainvoke({"subject": "Ada"}))


asyncio.run(main())

CrewAI Run end to end

MCPServerAdapter(params) from crewai-tools yields the memory tools as CrewAI tools.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. crewai 1.15.30, crewai-tools 1.15.30, mcp 1.30.0.

pip install 'crewai-tools[mcp]'
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import os
from crewai_tools import MCPServerAdapter

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}

params = {"url": URL, "transport": "streamable-http", "headers": HEADERS}
with MCPServerAdapter(params, connect_timeout=60) as tools:
    by = {t.name: t for t in tools}
    by["memory_remember"].run(facts=FACTS)
    print(by["memory_recall"].run(subject="Ada"))

Microsoft Agent Framework Run end to end

MCPStreamableHTTPTool(name, url, static_headers) attached to an agent.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. agent-framework 1.23.0, agent-framework-core 1.23.0, mcp 2.3.0.

pip install agent-framework
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from agent_framework import MCPStreamableHTTPTool

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    async with MCPStreamableHTTPTool(name="semurg", url=URL, static_headers=HEADERS) as mcp:
        by = {f.name: f for f in mcp.functions}
        await by["memory_remember"].invoke(facts=FACTS)
        result = await by["memory_recall"].invoke(subject="Ada")
        print("\n".join(getattr(c, "text", None) or str(c) for c in (result if isinstance(result, (list, tuple)) else [result])))


asyncio.run(main())

OpenAI Agents SDK Run end to end

MCPServerStreamableHttp(params={url, headers}) passed to the agent as mcp_servers.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. openai-agents 0.24.0, mcp 2.3.0.

pip install openai-agents
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from agents.mcp import MCPServerStreamableHttp

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    async with MCPServerStreamableHttp(name="semurg", params={"url": URL, "headers": HEADERS, "timeout": 20},
                                       client_session_timeout_seconds=30) as server:
        await server.call_tool("memory_remember", {"facts": FACTS})
        print(await server.call_tool("memory_recall", {"subject": "Ada"}))


asyncio.run(main())

LlamaIndex Run end to end

McpToolSpec(BasicMCPClient(url)).to_tool_list_async() into a FunctionAgent.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. llama-index-tools-mcp 0.6.0, llama-index-core 0.14.25, mcp 2.3.0.

pip install llama-index-tools-mcp
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    client = BasicMCPClient(URL, headers=HEADERS)
    by = {t.metadata.name: t for t in await McpToolSpec(client=client).to_tool_list_async()}
    await by["memory_remember"].acall(facts=FACTS)
    print(await by["memory_recall"].acall(subject="Ada"))


asyncio.run(main())

Mastra Run end to end

MCPClient({servers: {...}}).listTools() returns the memory tools.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. @mastra/mcp 2.5.0, @mastra/core 1.79.0.

npm install @mastra/mcp @mastra/core tsx zod
// Run: npx tsx snippet.ts   (SEMURG_API_TOKEN must be set in the environment)
import { MCPClient } from "@mastra/mcp";

const url = process.env.SEMURG_MCP_URL ?? "http://127.0.0.1:4000/v1/mcp";
const headers = {
  Authorization: `Bearer ${process.env.SEMURG_API_TOKEN}`,
  "X-Semurg-Scope": process.env.SEMURG_MCP_SCOPE ?? "default",
};
const facts = {
  entities: [{ name: "Ada", type: "person" }, { name: "Acme", type: "organisation" }],
  relations: [{ source: "Ada", target: "Acme", type: "WORKS_AT" }],
};

async function main() {
  const mcp = new MCPClient({ servers: { semurg: { url: new URL(url), requestInit: { headers } } } });
  try {
    const tools: Record<string, any> = await mcp.listTools(); // Mastra prefixes each tool with the server name
    await tools["semurg_memory_remember"].execute({ facts }, {});
    console.log(JSON.stringify(await tools["semurg_memory_recall"].execute({ subject: "Ada" }, {})));
  } finally {
    await mcp.disconnect();
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Google ADK Run end to end

McpToolset(connection_params=StreamableHTTPConnectionParams(url, headers)) in the agent's tools.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. google-adk 2.13.0, mcp 1.30.0.

pip install google-adk 'mcp<2'
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset, StreamableHTTPConnectionParams

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    toolset = McpToolset(connection_params=StreamableHTTPConnectionParams(url=URL, headers=HEADERS))
    try:
        by = {t.name: t for t in await toolset.get_tools()}
        await by["memory_remember"].run_async(args={"facts": FACTS}, tool_context=None)
        print(await by["memory_recall"].run_async(args={"subject": "Ada"}, tool_context=None))
    finally:
        await toolset.close()


asyncio.run(main())

Pydantic AI Run end to end

MCPToolset(url, headers=...) lists the memory tools and calls them.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. pydantic-ai-slim 2.62.0, mcp 2.3.0.

pip install 'pydantic-ai-slim[mcp]'
# Run: python snippet.py   (SEMURG_API_TOKEN must be set in the environment)
import asyncio, os
from pydantic_ai.mcp import MCPToolset

URL = os.environ.get("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp")
HEADERS = {"Authorization": "Bearer " + os.environ["SEMURG_API_TOKEN"],
           "X-Semurg-Scope": os.environ.get("SEMURG_MCP_SCOPE", "default")}
FACTS = {"entities": [{"name": "Ada", "type": "person"}, {"name": "Acme", "type": "organisation"}],
         "relations": [{"source": "Ada", "target": "Acme", "type": "WORKS_AT"}]}


async def main():
    server = MCPToolset(URL, headers=HEADERS)
    async with server:
        await server.direct_call_tool("memory_remember", {"facts": FACTS})
        print(await server.direct_call_tool("memory_recall", {"subject": "Ada"}))


asyncio.run(main())

Vercel AI SDK Run end to end

createMCPClient({transport: {type: 'http', url, headers}}).tools().

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. ai 7.0.154, @ai-sdk/mcp 2.0.85.

npm install ai tsx zod @ai-sdk/mcp
// Run: npx tsx snippet.ts   (SEMURG_API_TOKEN must be set in the environment)
import { createMCPClient } from "@ai-sdk/mcp";

const url = process.env.SEMURG_MCP_URL ?? "http://127.0.0.1:4000/v1/mcp";
const headers = {
  Authorization: `Bearer ${process.env.SEMURG_API_TOKEN}`,
  "X-Semurg-Scope": process.env.SEMURG_MCP_SCOPE ?? "default",
};
const facts = {
  entities: [{ name: "Ada", type: "person" }, { name: "Acme", type: "organisation" }],
  relations: [{ source: "Ada", target: "Acme", type: "WORKS_AT" }],
};

async function main() {
  const client = await createMCPClient({ transport: { type: "http", url, headers } });
  try {
    const tools: Record<string, any> = await client.tools();
    const opts = { toolCallId: "1", messages: [] };
    await tools["memory_remember"].execute({ facts }, opts);
    console.log(JSON.stringify(await tools["memory_recall"].execute({ subject: "Ada" }, opts)));
  } finally {
    await client.close();
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

LangChain (Elixir) Run end to end

a client module (use LangChain.MCP.Client) started with the streamable-http transport; Adapter.new(client: module) |> Adapter.to_functions() yields the memory tools as LangChain functions.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. langchain 0.4.1, langchain_mcp 0.2.0, anubis_mcp 0.16.0, plug 1.20.3.

Mix.install([{:langchain, "~> 0.4"}, {:langchain_mcp, "~> 0.2"}, {:plug, "~> 1.0"}])
# Run: elixir snippet.exs   (SEMURG_API_TOKEN must be set in the environment)
Mix.install([{:langchain, "~> 0.4"}, {:langchain_mcp, "~> 0.2"}, {:plug, "~> 1.0"}])   # langchain_mcp compiles a test server that needs Plug.Router

Logger.configure(level: :warning)   # the MCP client logs every request at debug level

url = URI.parse(System.get_env("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp"))
headers = %{
  "Authorization" => "Bearer " <> System.fetch_env!("SEMURG_API_TOKEN"),
  "X-Semurg-Scope" => System.get_env("SEMURG_MCP_SCOPE", "default")
}
facts = %{
  "entities" => [%{"name" => "Ada", "type" => "person"}, %{"name" => "Acme", "type" => "organisation"}],
  "relations" => [%{"source" => "Ada", "target" => "Acme", "type" => "WORKS_AT"}]
}

defmodule Semurg.MCP do
  use LangChain.MCP.Client, name: "semurg", version: "1.0.0", protocol_version: "2025-03-26"
end

{:ok, _} = Semurg.MCP.start_link(transport: {:streamable_http, base_url: "#{url.scheme}://#{url.host}:#{url.port}", mcp_path: url.path, headers: headers})
adapter = LangChain.MCP.Adapter.new(client: Semurg.MCP)

# the client does its MCP handshake in the background: ask until the memory tools are listed
wait = fn wait, tries ->
  tools = LangChain.MCP.Adapter.to_functions(adapter)
  cond do
    length(tools) >= 6 -> tools
    tries == 0 -> raise "the node listed no memory tools"
    true -> Process.sleep(500); wait.(wait, tries - 1)
  end
end

by_name = wait.(wait, 60) |> Map.new(&{&1.name, &1})
by_name["memory_remember"].function.(%{"facts" => facts}, %{})
IO.inspect(by_name["memory_recall"].function.(%{"subject" => "Ada"}, %{}), limit: :infinity, printable_limit: :infinity)
System.halt(0)

Jido Run end to end

Jido.Action modules (memory_status ... memory_context) call the node over HTTP; Jido.Exec.run/2 and an agent's cmd/2 run them.

Ran on release r11.131.20261027.1c0ba4a: remember, recall, relate, recall again, forget, each answer read back. jido 2.4.0, jido_action 2.4.0, req 0.7.5.

Mix.install([{:jido, "~> 2.0"}, {:jido_action, "~> 2.0"}, {:req, "~> 0.5"}])
# Run: elixir snippet.exs   (SEMURG_API_TOKEN must be set in the environment)
Mix.install([{:jido, "~> 2.0"}, {:jido_action, "~> 2.0"}, {:req, "~> 0.5"}])

defmodule Semurg.Node do
  @base System.get_env("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp") |> String.replace_suffix("/v1/mcp", "")
  @scope System.get_env("SEMURG_MCP_SCOPE", "default")
  @token System.fetch_env!("SEMURG_API_TOKEN")

  # the HTTP memory routes read the scope from the body
  def post(verb, body) do
    {:ok, %Req.Response{status: 200, body: answer}} =
      Req.post(@base <> "/v1/memory/" <> verb, json: Map.put(body, :scope, @scope), headers: [{"authorization", "Bearer " <> @token}], retry: false)

    {:ok, %{answer: answer}}
  end
end

defmodule Semurg.Remember do
  use Jido.Action, name: "memory_remember", description: "Store facts in shared agent memory", schema: [facts: [type: :map, required: true]]
  @impl true
  def run(params, _context), do: Semurg.Node.post("remember", Map.take(params, [:facts]))
end

defmodule Semurg.Recall do
  use Jido.Action, name: "memory_recall", description: "Recall what is stored about ONE entity", schema: [subject: [type: :string, required: true]]
  @impl true
  def run(params, _context), do: Semurg.Node.post("recall", Map.take(params, [:subject]))
end

facts = %{entities: [%{name: "Ada", type: "person"}, %{name: "Acme", type: "organisation"}], relations: [%{source: "Ada", target: "Acme", type: "WORKS_AT"}]}
{:ok, _} = Jido.Exec.run(Semurg.Remember, %{facts: facts})
IO.inspect(Jido.Exec.run(Semurg.Recall, %{subject: "Ada"}), limit: :infinity, printable_limit: :infinity)

Customer-side code

No Python in the engine or the server. The framework demos are customer-side examples. Each is written in its framework's own language, runs in its own container, and calls Semurg over MCP or HTTP. The engine and the server are written in Elixir and Rust; outside the Python client and these demos, the only Python we keep is one script that helps build this website.

Run the same check on your own node

Each check follows the same five steps against your node: connect with the integration's snippet, remember two entities and a relation, recall one of them, relate it to a third, recall again, and forget it. Use the API token your node generated at install. The kit that runs the checks in clean containers and records the result is available on request: semurg@semurg.org.