Skip to main content

memory-mcp-server

MCP Protocol Integration

PropertyValue
Repositorymemory-mcp-server
LanguageTypeScript/Node.js
TypeService (MCP Server)
Framework@modelcontextprotocol/sdk

Purpose

Exposes the Memory Engine through the Model Context Protocol (MCP), enabling AI agents (like Claude) to:

  • Store and retrieve memories during conversations
  • Query relevant context before responses
  • Build long-term knowledge about users and organizations

Repository Structure

memory-mcp-server/
├── package.json
├── tsconfig.json
├── README.md
├── Dockerfile

├── src/
│ ├── index.ts # Entry point
│ ├── server.ts # MCP server setup
│ ├── config.ts # Configuration
│ │
│ ├── tools/
│ │ ├── index.ts # Tool exports
│ │ ├── memory-upsert.ts # memory.upsert tool
│ │ ├── memory-query.ts # memory.query tool
│ │ ├── memory-link.ts # memory.link tool
│ │ ├── memory-delete.ts # memory.delete tool
│ │ └── graph-explore.ts # memory.graph_neighbors tool
│ │
│ ├── resources/
│ │ ├── index.ts # Resource exports
│ │ ├── subject-snapshot.ts # memory://subjects/\{subject\}/snapshot
│ │ ├── memory-block.ts # memory://blocks/\{id\}
│ │ └── memory-packs.ts # memory://packs/\{subject\}
│ │
│ ├── client/
│ │ ├── engine-client.ts # HTTP client for engine service
│ │ └── types.ts # API types
│ │
│ └── utils/
│ ├── auth.ts # Auth token management
│ └── errors.ts # Error handling

├── tests/
│ ├── tools/
│ │ └── memory-upsert.test.ts
│ └── resources/
│ └── subject-snapshot.test.ts

└── docs/
└── tools.md

Dependencies

{
"name": "@memory-platform/mcp-server",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"test": "vitest",
"lint": "eslint src/"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"axios": "^1.6.0",
"zod": "^3.22.0",
"dotenv": "^16.3.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"tsx": "^4.6.0",
"vitest": "^1.0.0",
"eslint": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^6.13.0"
}
}

Configuration

// src/config.ts
import { z } from 'zod';

const ConfigSchema = z.object({
// Memory Engine connection
engineUrl: z.string().url().default('http://localhost:8000'),
engineApiKey: z.string().optional(),

// MCP server settings
serverName: z.string().default('memory-mcp-server'),
serverVersion: z.string().default('0.1.0'),

// Default tenant (can be overridden per-request)
defaultTenantId: z.string().optional(),

// Logging
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(): Config {
return ConfigSchema.parse({
engineUrl: process.env.MEMORY_ENGINE_URL,
engineApiKey: process.env.MEMORY_ENGINE_API_KEY,
serverName: process.env.MCP_SERVER_NAME,
serverVersion: process.env.MCP_SERVER_VERSION,
defaultTenantId: process.env.DEFAULT_TENANT_ID,
logLevel: process.env.LOG_LEVEL,
});
}

MCP Server Setup

// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

import { Config } from './config.js';
import { registerTools, handleToolCall } from './tools/index.js';
import { registerResources, handleResourceRead } from './resources/index.js';
import { EngineClient } from './client/engine-client.js';

export async function createServer(config: Config): Promise<Server> {
const server = new Server(
{
name: config.serverName,
version: config.serverVersion,
},
{
capabilities: {
tools: {},
resources: {},
},
}
);

// Initialize engine client
const engineClient = new EngineClient(config);

// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: registerTools(),
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
return handleToolCall(request, engineClient);
});

// Register resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: registerResources(),
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
return handleResourceRead(request, engineClient);
});

return server;
}

export async function runServer(config: Config): Promise<void> {
const server = await createServer(config);
const transport = new StdioServerTransport();

await server.connect(transport);

console.error(`${config.serverName} v${config.serverVersion} running on stdio`);
}

Engine Client

// src/client/engine-client.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import { Config } from '../config.js';
import {
MemoryCreateRequest,
MemoryCreateResponse,
MemoryQueryRequest,
MemoryQueryResponse,
MemoryBlock,
MemoryPack,
GraphNeighborsRequest,
GraphNeighborsResponse,
Subject,
} from './types.js';

export class EngineClient {
private client: AxiosInstance;
private config: Config;

constructor(config: Config) {
this.config = config;
this.client = axios.create({
baseURL: config.engineUrl,
headers: {
'Content-Type': 'application/json',
...(config.engineApiKey && {
'X-API-Key': config.engineApiKey,
}),
},
timeout: 30000,
});
}

/**
* Create or update a memory block
*/
async createMemory(request: MemoryCreateRequest): Promise<MemoryCreateResponse> {
const response = await this.client.post<MemoryCreateResponse>(
'/api/v1/memory',
request
);
return response.data;
}

/**
* Get a memory block by ID
*/
async getMemory(memoryId: string): Promise<MemoryBlock | null> {
try {
const response = await this.client.get<MemoryBlock>(
`/api/v1/memory/${memoryId}`
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
}

/**
* Semantic search for memories
*/
async queryMemories(request: MemoryQueryRequest): Promise<MemoryQueryResponse> {
const response = await this.client.post<MemoryQueryResponse>(
'/api/v1/query',
request
);
return response.data;
}

/**
* Get working set for agent context
*/
async getWorkingSet(
subjectType: string,
subjectId: string,
context?: string,
maxBlocks: number = 30
): Promise<MemoryBlock[]> {
const response = await this.client.post<MemoryBlock[]>(
'/api/v1/query/working-set',
null,
{
params: {
subject_type: subjectType,
subject_id: subjectId,
context,
max_blocks: maxBlocks,
},
}
);
return response.data;
}

/**
* Get memory packs for a subject
*/
async getPacks(
subjectType: string,
subjectId: string,
packTypes?: string[]
): Promise<MemoryPack[]> {
const response = await this.client.get<MemoryPack[]>('/api/v1/packs', {
params: {
subject_type: subjectType,
subject_id: subjectId,
pack_types: packTypes?.join(','),
},
});
return response.data;
}

/**
* Explore graph neighborhood
*/
async getGraphNeighbors(
request: GraphNeighborsRequest
): Promise<GraphNeighborsResponse> {
const response = await this.client.post<GraphNeighborsResponse>(
'/api/v1/graph/neighbors',
request
);
return response.data;
}

/**
* Delete a memory block
*/
async deleteMemory(memoryId: string): Promise<boolean> {
try {
await this.client.delete(`/api/v1/memory/${memoryId}`);
return true;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return false;
}
throw error;
}
}
}

MCP Tools

// src/tools/index.ts
import { Tool, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { EngineClient } from '../client/engine-client.js';
import { handleMemoryUpsert, memoryUpsertTool } from './memory-upsert.js';
import { handleMemoryQuery, memoryQueryTool } from './memory-query.js';
import { handleMemoryDelete, memoryDeleteTool } from './memory-delete.js';
import { handleGraphExplore, graphExploreTool } from './graph-explore.js';

export function registerTools(): Tool[] {
return [
memoryUpsertTool,
memoryQueryTool,
memoryDeleteTool,
graphExploreTool,
];
}

export async function handleToolCall(
request: CallToolRequest,
client: EngineClient
): Promise<{ content: Array<{ type: string; text: string }> }> {
const { name, arguments: args } = request.params;

try {
let result: unknown;

switch (name) {
case 'memory.upsert':
result = await handleMemoryUpsert(args, client);
break;
case 'memory.query':
result = await handleMemoryQuery(args, client);
break;
case 'memory.delete':
result = await handleMemoryDelete(args, client);
break;
case 'memory.graph_neighbors':
result = await handleGraphExplore(args, client);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}

return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${message}`,
},
],
};
}
}
// src/tools/memory-upsert.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { EngineClient } from '../client/engine-client.js';

export const memoryUpsertTool: Tool = {
name: 'memory.upsert',
description: `Store a new memory or update an existing one. Use this to remember facts, preferences, insights, or any knowledge about a user, organization, project, or topic that should persist across conversations.

Examples:
- "The user prefers morning meetings" → kind: preference
- "Company was founded in 2019" → kind: fact
- "User seems frustrated with billing" → kind: insight

Returns the created memory ID.`,
inputSchema: {
type: 'object',
properties: {
subject_type: {
type: 'string',
enum: ['user', 'org', 'project', 'tool', 'channel', 'document', 'topic'],
description: 'Type of entity this memory is about',
},
subject_id: {
type: 'string',
description: 'Unique identifier of the entity',
},
kind: {
type: 'string',
enum: ['fact', 'preference', 'insight', 'summary', 'profile', 'note'],
description: 'Classification of the memory',
},
text: {
type: 'string',
description: 'Natural language description of the memory',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Relevant topics/concepts for categorization',
},
confidence: {
type: 'number',
minimum: 0,
maximum: 1,
description: 'How confident you are in this information (0-1)',
},
},
required: ['subject_type', 'subject_id', 'kind', 'text'],
},
};

const InputSchema = z.object({
subject_type: z.enum(['user', 'org', 'project', 'tool', 'channel', 'document', 'topic']),
subject_id: z.string(),
kind: z.enum(['fact', 'preference', 'insight', 'summary', 'profile', 'note']),
text: z.string(),
tags: z.array(z.string()).optional(),
confidence: z.number().min(0).max(1).optional(),
});

export async function handleMemoryUpsert(
args: unknown,
client: EngineClient
): Promise<{ memory_id: string; success: boolean }> {
const input = InputSchema.parse(args);

const response = await client.createMemory({
subject: {
type: input.subject_type,
id: input.subject_id,
},
kind: input.kind,
text: input.text,
tags: input.tags || [],
confidence: input.confidence,
});

return {
memory_id: response.memory_id,
success: response.success,
};
}
// src/tools/memory-query.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { EngineClient } from '../client/engine-client.js';

export const memoryQueryTool: Tool = {
name: 'memory.query',
description: `Search for relevant memories about a subject. Use this before responding to retrieve context about users, organizations, or topics.

The query uses semantic similarity - describe what you're looking for in natural language.

Returns a list of relevant memories ranked by relevance.`,
inputSchema: {
type: 'object',
properties: {
subject_type: {
type: 'string',
enum: ['user', 'org', 'project', 'tool', 'channel', 'document', 'topic'],
description: 'Type of entity to search memories about',
},
subject_id: {
type: 'string',
description: 'Unique identifier of the entity',
},
query: {
type: 'string',
description: 'Natural language description of what information you need',
},
kinds: {
type: 'array',
items: {
type: 'string',
enum: ['fact', 'preference', 'insight', 'summary', 'profile', 'note'],
},
description: 'Filter by specific memory types',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by tags (any match)',
},
limit: {
type: 'number',
minimum: 1,
maximum: 50,
description: 'Maximum number of memories to return',
},
},
required: ['subject_type', 'subject_id', 'query'],
},
};

const InputSchema = z.object({
subject_type: z.enum(['user', 'org', 'project', 'tool', 'channel', 'document', 'topic']),
subject_id: z.string(),
query: z.string(),
kinds: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
limit: z.number().min(1).max(50).optional().default(10),
});

export async function handleMemoryQuery(
args: unknown,
client: EngineClient
): Promise<{
memories: Array<{
id: string;
kind: string;
text: string;
relevance: number;
tags: string[];
}>;
total: number;
}> {
const input = InputSchema.parse(args);

const response = await client.queryMemories({
subject: {
type: input.subject_type,
id: input.subject_id,
},
query_text: input.query,
filters: {
kind: input.kinds,
tags_any: input.tags,
},
limit: input.limit,
});

return {
memories: response.results.map((r) => ({
id: r.memory.id,
kind: r.memory.kind,
text: r.memory.content.text,
relevance: r.relevance_score,
tags: r.memory.tags,
})),
total: response.total_count,
};
}
// src/tools/memory-delete.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { EngineClient } from '../client/engine-client.js';

export const memoryDeleteTool: Tool = {
name: 'memory.delete',
description: `Delete a memory that is no longer accurate or relevant. Use sparingly - prefer updating memories over deleting them.`,
inputSchema: {
type: 'object',
properties: {
memory_id: {
type: 'string',
description: 'ID of the memory to delete',
},
},
required: ['memory_id'],
},
};

const InputSchema = z.object({
memory_id: z.string(),
});

export async function handleMemoryDelete(
args: unknown,
client: EngineClient
): Promise<{ deleted: boolean }> {
const input = InputSchema.parse(args);
const deleted = await client.deleteMemory(input.memory_id);
return { deleted };
}
// src/tools/graph-explore.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { EngineClient } from '../client/engine-client.js';

export const graphExploreTool: Tool = {
name: 'memory.graph_neighbors',
description: `Explore relationships around an entity in the knowledge graph. Useful for understanding connections between users, organizations, projects, and topics.`,
inputSchema: {
type: 'object',
properties: {
vertex_id: {
type: 'string',
description: 'ID of the entity to explore from',
},
max_depth: {
type: 'number',
minimum: 1,
maximum: 3,
description: 'How many relationship hops to traverse',
},
edge_types: {
type: 'array',
items: { type: 'string' },
description: 'Filter by relationship types',
},
},
required: ['vertex_id'],
},
};

const InputSchema = z.object({
vertex_id: z.string(),
max_depth: z.number().min(1).max(3).optional().default(2),
edge_types: z.array(z.string()).optional(),
});

export async function handleGraphExplore(
args: unknown,
client: EngineClient
): Promise<{
center: { id: string; type: string };
neighbors: Array<{ id: string; type: string; relationship: string }>;
}> {
const input = InputSchema.parse(args);

const response = await client.getGraphNeighbors({
vertex_id: input.vertex_id,
max_depth: input.max_depth,
edge_types: input.edge_types,
});

return {
center: {
id: response.center.vertex_id,
type: response.center.type,
},
neighbors: response.nodes.map((n) => ({
id: n.vertex_id,
type: n.type,
relationship: 'related', // Simplified
})),
};
}

MCP Resources

// src/resources/index.ts
import { Resource, ReadResourceRequest } from '@modelcontextprotocol/sdk/types.js';
import { EngineClient } from '../client/engine-client.js';
import { handleSubjectSnapshot } from './subject-snapshot.js';
import { handleMemoryBlock } from './memory-block.js';
import { handleMemoryPacks } from './memory-packs.js';

export function registerResources(): Resource[] {
return [
{
uri: 'memory://subjects/{type}/\{id\}/snapshot',
name: 'Subject Memory Snapshot',
description: 'Complete memory snapshot for a subject (user, org, etc.)',
mimeType: 'application/json',
},
{
uri: 'memory://blocks/\{id\}',
name: 'Memory Block',
description: 'Single memory block by ID',
mimeType: 'application/json',
},
{
uri: 'memory://packs/{type}/\{id\}',
name: 'Memory Packs',
description: 'Organized memory packs for a subject',
mimeType: 'application/json',
},
];
}

export async function handleResourceRead(
request: ReadResourceRequest,
client: EngineClient
): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> {
const uri = request.params.uri;

// Parse URI and route to handler
if (uri.startsWith('memory://subjects/')) {
return handleSubjectSnapshot(uri, client);
}

if (uri.startsWith('memory://blocks/')) {
return handleMemoryBlock(uri, client);
}

if (uri.startsWith('memory://packs/')) {
return handleMemoryPacks(uri, client);
}

throw new Error(`Unknown resource URI: ${uri}`);
}
// src/resources/subject-snapshot.ts
import { EngineClient } from '../client/engine-client.js';

export async function handleSubjectSnapshot(
uri: string,
client: EngineClient
): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> {
// Parse: memory://subjects/{type}/\{id\}/snapshot
const match = uri.match(/memory:\/\/subjects\/([^/]+)\/([^/]+)\/snapshot/);

if (!match) {
throw new Error(`Invalid subject snapshot URI: ${uri}`);
}

const [, subjectType, subjectId] = match;

// Get working set for comprehensive snapshot
const memories = await client.getWorkingSet(subjectType, subjectId);

// Format as readable snapshot
const snapshot = {
subject: { type: subjectType, id: subjectId },
memory_count: memories.length,
memories: memories.map((m) => ({
kind: m.kind,
text: m.content.text,
tags: m.tags,
salience: m.scores.salience,
})),
generated_at: new Date().toISOString(),
};

return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(snapshot, null, 2),
},
],
};
}

Entry Point

// src/index.ts
import { loadConfig } from './config.js';
import { runServer } from './server.js';

async function main() {
const config = loadConfig();
await runServer(config);
}

main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

Dockerfile

FROM node:20-slim

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy built code
COPY dist/ dist/

ENV NODE_ENV=production

# MCP servers communicate via stdio
CMD ["node", "dist/index.js"]

Environment Variables

VariableRequiredDefaultDescription
MEMORY_ENGINE_URLNohttp://localhost:8000Engine service URL
MEMORY_ENGINE_API_KEYYes-API key for engine
MCP_SERVER_NAMENomemory-mcp-serverServer name
DEFAULT_TENANT_IDNo-Default tenant if not in context
LOG_LEVELNoinfoLogging level

MCP Client Configuration

To use with Claude Desktop or other MCP clients:

{
"mcpServers": {
"memory": {
"command": "node",
"args": ["/path/to/memory-mcp-server/dist/index.js"],
"env": {
"MEMORY_ENGINE_URL": "http://localhost:8000",
"MEMORY_ENGINE_API_KEY": "your-api-key"
}
}
}
}

Acceptance Criteria

  • All MCP tools function correctly
  • Resources return valid JSON
  • Error handling provides useful messages
  • Works with Claude Desktop
  • Works with other MCP clients
  • TypeScript compiles without errors
  • Tool descriptions enable good LLM usage