The Model Context Protocol (MCP) defines a standard interface through which AI assistants access external tools, data sources, and services. The memory system implements a server that speaks both MCP over stdio (for direct integration with AI assistants like Claude) and HTTP via the Hono web framework (for browser dashboards, REST clients, and other services). This dual-protocol architecture means the same memory storage backend can serve an AI assistant’s tool calls and a human-facing web dashboard simultaneously, with shared authentication providers ensuring consistent access control across both surfaces.
The server is built as a Node.js CLI that accepts flags to start one or both protocols. When launched with --mcp, it binds an MCP stdio server that exposes tools like store_memory, recall_memory, and search_memories. When launched with --http, it starts a Hono REST API with equivalent endpoints plus a static file server for the dashboard UI. When launched with both flags (or the default --dual mode), both servers share the same in-memory or persistent storage layer and the same auth provider, so a memory stored via MCP is immediately visible in the HTTP dashboard and vice versa.
This architecture pattern—a single service exposing multiple protocol surfaces over a shared core—is increasingly common as AI tooling matures. It separates the transport concern (stdio vs. HTTP) from the business logic (memory CRUD, search, namespacing) and enables each protocol surface to evolve independently while maintaining a single source of truth for data and authorization.
MCP servers communicate with AI assistants over standard input/output using JSON-RPC messages. The assistant sends tool call requests as JSON objects on stdin; the server processes them and writes JSON responses to stdout. This stdio transport requires no network binding, making it trivially easy to start: the assistant spawns the server as a child process and communicates directly. The MCP SDK handles JSON-RPC framing, message parsing, and tool registration, letting the server implementation focus on business logic.
Hono is a lightweight, multi-runtime web framework (runs on Node.js, Deno, Bun, Cloudflare Workers) that provides Express-like routing with better TypeScript support and smaller bundle size. The memory system HTTP layer uses Hono to expose RESTful endpoints for memory operations: POST /memories, GET /memories/:id, GET /memories/search, and DELETE /memories/:id. Hono’s middleware system handles authentication, CORS, request logging, and error formatting in a composable pipeline.
Both the MCP and HTTP surfaces delegate authentication to a shared provider interface. The provider can be a simple API key check, an OAuth token validator, or a custom auth backend. By sharing a single provider instance, the server guarantees that a user who is authorized via the HTTP dashboard has the same permissions when their AI assistant connects via MCP. This eliminates the risk of auth policy divergence between the two protocol surfaces.
Memories are organized into namespaces—logical partitions that isolate memories by project, user, or purpose. When the server starts, it reads a configuration file or environment variables to bootstrap a set of default namespaces with their access policies. Each namespace acts as an independent memory store with its own retention policies, search indexes, and access control lists. The namespace abstraction maps cleanly to both MCP (tool calls include a namespace parameter) and HTTP (namespace appears as a URL path segment).
The Hono HTTP server also serves a static dashboard UI that provides a browser-based interface for browsing, searching, and managing memories. The dashboard is bundled as static HTML/CSS/JS files and served via Hono’s serveStatic middleware. This keeps the dashboard deployment self-contained—no separate web server or CDN is needed—and ensures the dashboard always communicates with the correct API version since both are served from the same process.
The CLI parses flags to determine which protocol surfaces to start. In dual mode (the default), both MCP and HTTP servers are initialized with a shared storage backend and auth provider.
import { parseArgs } from 'node:util';
import { createStorage } from './storage.js';
import { createAuthProvider } from './auth.js';
import { startMcpServer } from './mcp-server.js';
import { startHttpServer } from './http-server.js';
const { values } = parseArgs({
options: {
mode: { type: 'string', default: 'dual' },
port: { type: 'string', default: '3100' },
'storage-path': { type: 'string', default: './data' },
'api-key': { type: 'string' },
},
});
const storage = await createStorage(values['storage-path']);
const auth = createAuthProvider({ apiKey: values['api-key'] });
if (values.mode === 'mcp' || values.mode === 'dual') {
await startMcpServer({ storage, auth });
console.error('MCP server started on stdio');
}
if (values.mode === 'http' || values.mode === 'dual') {
const port = Number(values.port);
await startHttpServer({ storage, auth, port });
console.error(`HTTP server started on port ${port}`);
}
The MCP server registers tools that map directly to storage operations. Each tool declaration includes a JSON Schema for its parameters and a handler function. The MCP SDK handles JSON-RPC transport over stdio.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
export async function startMcpServer({ storage, auth }) {
const server = new Server(
{ name: 'memory-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'store_memory',
description: 'Store a new memory in the specified namespace',
inputSchema: {
type: 'object',
required: ['namespace', 'content'],
properties: {
namespace: { type: 'string' },
content: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
metadata: { type: 'object' },
},
},
},
{
name: 'search_memories',
description: 'Search memories by query string within a namespace',
inputSchema: {
type: 'object',
required: ['namespace', 'query'],
properties: {
namespace: { type: 'string' },
query: { type: 'string' },
limit: { type: 'number', default: 10 },
},
},
},
],
}));
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'store_memory') {
const id = await storage.store(args.namespace, {
content: args.content,
tags: args.tags ?? [],
metadata: args.metadata ?? {},
});
return { content: [{ type: 'text', text: `Stored memory ${id}` }] };
}
if (name === 'search_memories') {
const results = await storage.search(args.namespace, args.query, args.limit);
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
}
throw new Error(`Unknown tool: ${name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
}
The HTTP layer exposes REST endpoints that call the same storage methods used by the MCP server. Middleware handles auth token validation and CORS headers. The dashboard is served as static files from a bundled directory.
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serveStatic } from '@hono/node-server/serve-static';
import { serve } from '@hono/node-server';
export async function startHttpServer({ storage, auth, port }) {
const app = new Hono();
app.use('*', cors());
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!auth.validate(token)) {
return c.json({ error: 'Unauthorized' }, 401);
}
await next();
});
app.post('/api/memories', async (c) => {
const body = await c.req.json();
const id = await storage.store(body.namespace, {
content: body.content,
tags: body.tags ?? [],
metadata: body.metadata ?? {},
});
return c.json({ id }, 201);
});
app.get('/api/memories/search', async (c) => {
const namespace = c.req.query('namespace');
const query = c.req.query('query');
const limit = Number(c.req.query('limit') ?? '10');
const results = await storage.search(namespace, query, limit);
return c.json({ results });
});
app.get('/api/memories/:id', async (c) => {
const memory = await storage.get(c.req.param('id'));
if (!memory) return c.json({ error: 'Not found' }, 404);
return c.json(memory);
});
app.delete('/api/memories/:id', async (c) => {
await storage.delete(c.req.param('id'));
return c.json({ deleted: true });
});
app.use('/dashboard/*', serveStatic({ root: './public' }));
serve({ fetch: app.fetch, port });
}
On startup, the storage layer reads a configuration file to create default namespaces with their retention and access policies. This ensures the server is ready to accept memories immediately without manual setup.
import { readFile } from 'node:fs/promises';
interface NamespaceConfig {
name: string;
retentionDays: number;
maxMemories: number;
allowedRoles: string[];
}
export async function bootstrapNamespaces(
storage: Storage,
configPath: string
): Promise<void> {
const raw = await readFile(configPath, 'utf-8');
const configs: NamespaceConfig[] = JSON.parse(raw);
for (const config of configs) {
const exists = await storage.namespaceExists(config.name);
if (!exists) {
await storage.createNamespace(config.name, {
retentionDays: config.retentionDays,
maxMemories: config.maxMemories,
allowedRoles: config.allowedRoles,
});
}
}
}
The dual-protocol pattern used by the memory system addresses a fundamental tension in AI tooling: AI assistants need programmatic tool interfaces (MCP, function calling), while humans need visual interfaces (dashboards, browsers). By serving both from a single process with shared state, the memory server eliminates synchronization issues that arise when separate services maintain independent views of the same data.
In a development workflow, a developer might use an AI assistant that stores contextual memories via MCP as it works on a codebase—architectural decisions, discovered patterns, debugging notes. Later, the same developer opens the HTTP dashboard to browse those memories, search across namespaces, or export them for documentation. Because both interfaces share the same storage backend, there is zero lag between an MCP write and its visibility in the dashboard.
The namespace architecture also enables multi-tenant deployments where different projects or teams maintain isolated memory stores. A shared development server can host namespaces for multiple projects, each with its own retention policy and access controls, while exposing all of them through both MCP and HTTP. This is particularly useful in organizations where multiple AI assistants or tools need to share contextual knowledge without cross-contaminating project-specific information.
Keep all business logic in a protocol-agnostic storage and service layer. The MCP server and HTTP server should be thin adapters that translate protocol-specific requests into calls on the shared core. This means the storage interface defines the canonical API, and protocol layers only handle serialization, authentication, and transport.
Both protocol surfaces must reference the same storage and auth provider instances, not independent copies. Create these instances once in the CLI entry point and inject them into both servers. This dependency injection approach ensures consistency and makes testing straightforward—inject mock storage to test protocol handling without touching disk.
When running in MCP mode, stdout is reserved for JSON-RPC messages. All diagnostic output (startup messages, warnings, debug logs) must go to stderr. This is a critical constraint: any non-JSON output on stdout will corrupt the MCP transport and crash the assistant. Use console.error instead of console.log for all server-side logging.