Use the MCP tools

The @neo4j-labs/agent-memory/mcp subpath ships 12 neo4j-agent-memory tool definitions plus a dispatcher, and @neo4j-labs/agent-memory/mcp/register registers them on a high-level McpServer in one call. Use either to stand up your own MCP server, or to invoke tools programmatically against a MemoryClient.

A runnable example lives at typescript/examples/mcp.

Prerequisites: Node.js 22+, the MCP/Zod peers and NAMS test-workspace credentials.

Procedure

  1. Build/install the source MCP example using its README.

  2. Register the memory tools on a server, applying the allow-list and audit hook you need.

  3. Connect a client using the chosen transport and inspect its actual tools/list response.

When to self-host MCP

Most users don’t need to. The hosted NAMS MCP server at mcp.memory.neo4jlabs.com exposes a much larger, scope-gated surface (ontology, Skills, administration) and supports OAuth — see Reference: the hosted NAMS MCP server.

What this subpath gives you is the 12-tool subset that maps one-to-one onto this client’s methods, which is what you want when you need to wrap, log or filter tool calls, expose a restricted surface, or run an MCP server inside a boundary that cannot reach the hosted server.

The 12 tools

Tool Wraps Annotations

memory_create_conversation

shortTerm.createConversation

write

memory_add_messages

shortTerm.addMessage (bulk-aware)

write

memory_get_context

shortTerm.getContext

read-only, idempotent

memory_search_messages

shortTerm.searchMessages

read-only, idempotent

memory_search_entities

longTerm.searchEntities

read-only, idempotent

memory_get_entity

longTerm.getEntity

read-only, idempotent

memory_add_entity

longTerm.addEntity

write

memory_get_entity_history

longTerm.getEntityHistory

read-only, idempotent

memory_record_step

reasoning.recordStep

write

memory_record_tool_call

reasoning.recordToolCall

write

memory_get_trace

reasoning.getTraceByConversation

read-only, idempotent

memory_explain_decision

reasoning.explainStep

read-only, idempotent

No memory tool is marked destructive. The read/write split is exported as READ_ONLY_MEMORY_TOOLS and memoryToolAnnotations(name) so a server can apply the same policy to tools it adds.

McpServer.registerTool validates arguments, which means its inputSchema must be a Zod shape rather than the JSON Schema the low-level API takes. registerMemoryTools carries that Zod half and does the registration:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { MemoryClient, VERSION } from "@neo4j-labs/agent-memory";
import { registerMemoryTools } from "@neo4j-labs/agent-memory/mcp/register";

const memory = new MemoryClient();
const server = new McpServer({ name: "my-memory-server", version: VERSION });

registerMemoryTools(server, memory, {
  // optional: expose a subset
  only: process.env.MCP_TOOLS?.split(","),
  // optional: one audit line per call — tool name and argument *keys* only
  onCall: ({ tool, argKeys, durationMs, ok }) =>
    console.error(`[mcp] ${tool} keys=${argKeys.join(",")} ${durationMs}ms ok=${ok}`),
});

await server.connect(new StdioServerTransport());

Keep diagnostics on stderr: with the stdio transport, stdout is the protocol channel.

A dispatch failure comes back as a tool result with isError: true and the message as text — the MCP convention — rather than as a thrown protocol error, so the model can see what went wrong. Malformed arguments are rejected by the server’s own schema validation before the dispatcher runs.

Tool results are one JSON text block. No outputSchema is declared: several tools resolve to arrays, which structuredContent cannot carry without re-shaping the payload.

Add a custom tool alongside the memory tools

registerMemoryTools only registers the 12 memory tools; nothing stops you calling server.registerTool again to add tools of your own. Register custom tools in the same setup function, after registerMemoryTools(…​) and before connecting the transport, and follow the same conventions as the built-in tools: validate or scope any model-supplied input yourself, return isError: true with a text message on failure instead of throwing, and give the tool honest readOnlyHint / destructiveHint annotations.

This recipe continues the my-memory-mcp project from the MCP server tutorial, adding a scoped, read-only tool that inspects only the graph records that tutorial’s messages created.

Prerequisites: the my-memory-mcp project through Step 7 of that tutorial, with its src/tutorial-state.ts and src/tutorial-cleanup.ts helpers already in place and its .tutorial-state/mcp.json ledger populated.

  1. Add these imports beside the existing imports in src/server.ts:

    import { isAbsolute } from "node:path";
    import { TutorialRun } from "./tutorial-state.js";
    import { inspectGraph } from "./tutorial-cleanup.js";
  2. Insert this registration inside main(), after registerMemoryTools(…​) and before connecting the transport:

    // In src/server.ts, inside main(), after registerMemoryTools(...):
    server.registerTool(
      "memory_tutorial_graph",
      {
        description: "Inspect graph records linked only to this tutorial's recorded messages.",
        inputSchema: {},
        annotations: { readOnlyHint: true, idempotentHint: true, destructiveHint: false },
      },
      async () => {
        let run: TutorialRun | undefined;
        try {
          const path = process.env.TUTORIAL_STATE_PATH;
          if (!path || !isAbsolute(path)) {
            throw new Error("Set TUTORIAL_STATE_PATH to the absolute path of this lesson's state file.");
          }
          run = TutorialRun.load(path, resolveTutorialConfig(), "mcp");
          const rows = await inspectGraph(run);
          return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          return { content: [{ type: "text", text: message }], isError: true };
        } finally {
          await run?.client.close();
        }
      },
    );

    The tool accepts no model-supplied Cypher or resource IDs. Its helper verifies the saved conversation and message ownership, then uses those message IDs as parameters in the scoped graph query. An empty graph can mean extraction is still pending; it does not negate the exact message readback from the tutorial’s Step 6.

  3. Print the absolute state-file path without exposing its contents, then rebuild:

    printf '%s/.tutorial-state/mcp.json\n' "$PWD"
    npm run build
  4. Add TUTORIAL_STATE_PATH to this server’s private Desktop env block using that exact printed path, alongside the same NAMS credential and endpoint, then restart Claude Desktop.

Verify: reconnecting should list seven tools including memory_tutorial_graph. Ask Desktop to invoke it — any returned graph rows must refer to the recorded message IDs from the tutorial. A state or credential mismatch surfaces as a tool error, not permission to inspect another workspace.

The same server.registerTool pattern works on any McpServer, tutorial project or not — swap the description, inputSchema and handler body for whatever your own tool looks up, keeping the isError and annotation conventions above. A custom tool registered this way is unaffected by the only allow-list below — that option scopes the memory tools registerMemoryTools itself registers, not tools you add separately — so a custom tool is always advertised in tools/list once registered.

Restrict which tools are exposed

Pass only to registerMemoryTools to scope down which of the 12 memory tools get registered:

const toolNames = registerMemoryTools(server, memory, {
  only: ["memory_get_context", "memory_search_messages"],
  onCall: ({ tool, argKeys, durationMs, ok }) =>
    console.error(`[mcp] ${tool} keys=[${argKeys.join(",")}] ${durationMs}ms ok=${ok}`),
});

Anything not on the list is neither advertised in the client’s tools/list response nor callable — an attempted call to an excluded tool (for example memory_create_conversation) is rejected. registerMemoryTools returns the names it registered, so checking toolNames after a change to the list is a quick sanity check. Combine only with the exported READ_ONLY_MEMORY_TOOLS set — only: […​READ_ONLY_MEMORY_TOOLS] — to build, for example, a review-only surface that can search and read context but never write.

The onCall audit hook fires once per call to whichever memory tools registerMemoryTools actually registered, so only scopes it the same way.

Continuing the tutorial project: replace its original registration block in src/server.ts with the one above — do not register the same tools twice — and remove the memory_tutorial_graph registration from the previous recipe if it is still present. Rebuild with npm run build and restart Claude Desktop. Verify: reconnecting should list exactly two tools, and an attempted memory_create_conversation call must be rejected. The tutorial’s CLI cleanup step does not depend on exposing deletion tools to Desktop.

Streamable HTTP

The same registration works over HTTP — only the transport changes:

import { StreamableHTTPServerTransport }
  from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);

// In your HTTP handler, forward POST / GET / DELETE on the MCP route:
//   await transport.handleRequest(req, res, body);

Use Streamable HTTP when the client is not co-located with the server (a container, a sidecar, a network boundary). Put authentication in front of the route — the transport does not authenticate callers for you.

Registering with the low-level Server

If you already use the low-level API, createMemoryTools() emits JSON Schema plus annotations directly:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema }
  from "@modelcontextprotocol/sdk/types.js";
import { MemoryClient } from "@neo4j-labs/agent-memory";
import { createMemoryTools, handleMemoryToolCall }
  from "@neo4j-labs/agent-memory/mcp";

const memory = new MemoryClient();
const tools = createMemoryTools();
const server = new Server({ name: "...", version: "..." }, { capabilities: { tools: {} } });

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: tools.map((t) => ({
    name: t.name,
    description: t.description,
    inputSchema: t.inputSchema,
    annotations: t.annotations,
  })),
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  try {
    const result = await handleMemoryToolCall(
      memory, req.params.name, (req.params.arguments ?? {}) as Record<string, unknown>,
    );
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (e) {
    return { isError: true, content: [{ type: "text", text: String(e) }] };
  }
});

This path does not validate arguments — the dispatcher casts them — so prefer registerMemoryTools for anything a model drives.

Programmatic dispatch

You can also call handleMemoryToolCall directly — useful for tests, or when bridging an LLM that uses a different tool-calling convention than MCP:

const conv = await handleMemoryToolCall(memory, "memory_create_conversation", {
  user_id: "alice",
});

Argument keys use snake_case (matching the JSON schema published by createMemoryTools()).

Deprecated tool names

Five memory.<verb> aliases from v0.1 are still accepted and forward to the new names with a warning: memory.addMessage, memory.getConversation, memory.searchMessages, memory.addEntity, memory.searchEntities. They are scheduled for removal in 1.0 — migrate to the memory_<noun>_<verb> form.

Compatibility

Current-source example dependency declarations (not a release or live-service verification): @modelcontextprotocol/sdk 1.30, zod 4.6, Node.js 22 — 2026-09. @modelcontextprotocol/sdk and zod are optional peer dependencies; they are required only by the ./mcp/register subpath (the MCP SDK already depends on zod).

Verify the integration

In typescript/examples/mcp/, run npm run typecheck and npm test. The offline suite assembles the tutorial’s base server with the custom-tool and restricted-tool recipes on this page, compiles each variant to dist/server.js, connects over stdio, and checks tool counts: 6 base tools, 7 with memory_tutorial_graph, 2 when restricted. It does not require a live NAMS key. Use the tutorial’s returned-id sequence for a separate live check.