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
| Tool | What it does |
|---|---|
| memory_status | Reports whether shared memory is on for this node and which of your scopes hold facts. |
| memory_remember | Stores entities, the relationships between them and the passages they came from, or plain text, with an optional source label and date. |
| memory_recall | Recalls what is stored about one named entity, its own facts first, each item cited. |
| memory_relate | Adds one typed relationship between two entities in a scope, creating an end that is not stored yet. |
| memory_forget | Deletes 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_context | Answers a question in plain words with cited items and a verdict on whether the store covers it. |
| memory_document | Takes 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.
| Integration | Kind | Status |
|---|---|---|
| Hermes Agent | Agent | Connected |
| OpenClaw | Agent | Connected |
| LangChain / LangGraph | Library, Python | Run end to end |
| CrewAI | Library, Python | Run end to end |
| Microsoft Agent Framework | Library, Python | Run end to end |
| OpenAI Agents SDK | Library, Python | Run end to end |
| LlamaIndex | Library, Python | Run end to end |
| Mastra | Library, TypeScript | Run end to end |
| Google ADK | Library, Python | Run end to end |
| Pydantic AI | Library, Python | Run end to end |
| Vercel AI SDK | Library, TypeScript | Run end to end |
| LangChain (Elixir) | Library, Elixir | Run end to end |
| Jido | Library, Elixir | Run end to end |
| Claude Code | Agent | Connected |
| Claude Desktop | Agent | Results pending |
| Cursor | Agent | Results pending |
| OpenAI Codex CLI | Agent | Results pending |
| Gemini CLI | Agent | Results pending |
| ChatGPT (custom connector) | Agent | Results 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
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.