Skip to content

Commit dfec80f

Browse files
Aki-07tobiu
authored andcommitted
feat(memory-mcp): add server scaffold
1 parent 1cee840 commit dfec80f

File tree

11 files changed

+1070
-344
lines changed

11 files changed

+1070
-344
lines changed

ai/mcp/server/memory/app.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import express from 'express';
2+
import fs from 'fs';
3+
import morgan from 'morgan';
4+
import swaggerUi from 'swagger-ui-express';
5+
import yaml from 'js-yaml';
6+
import healthRouter from './routes/health.mjs';
7+
import memoriesRouter from './routes/memories.mjs';
8+
import summariesRouter from './routes/summaries.mjs';
9+
import errorHandler from './middleware/errorHandler.mjs';
10+
import notFoundHandler from './middleware/notFoundHandler.mjs';
11+
import serverConfig from './config.mjs';
12+
13+
const app = express();
14+
15+
// Middleware
16+
app.use(express.json({limit: serverConfig.requestBodyLimit}));
17+
app.use(morgan(serverConfig.logFormat));
18+
19+
// Swagger docs
20+
const openApiDocument = yaml.load(fs.readFileSync(serverConfig.openApiFilePath, 'utf8'));
21+
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument, {explorer: true}));
22+
23+
// Routes
24+
app.use('/', healthRouter);
25+
app.use('/', memoriesRouter);
26+
app.use('/', summariesRouter);
27+
28+
// 404 + Error handlers
29+
app.use(notFoundHandler);
30+
app.use(errorHandler);
31+
32+
export default app;

ai/mcp/server/memory/config.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import dotenv from 'dotenv';
2+
import path from 'path';
3+
4+
const cwd = process.cwd();
5+
6+
// Load environment variables from the project root, but stay silent if the file does not exist.
7+
dotenv.config({path: path.resolve(cwd, '.env'), quiet: true});
8+
9+
/**
10+
* Central configuration for the Memory MCP server.
11+
*/
12+
const serverConfig = {
13+
host : process.env.MEMORY_MCP_HOST || '0.0.0.0',
14+
port : parseInt(process.env.MEMORY_MCP_PORT || '8010', 10),
15+
logFormat : process.env.MEMORY_MCP_LOG_FORMAT || 'dev',
16+
openApiFilePath : path.resolve(cwd, 'buildScripts/mcp/memory/openapi.yaml'),
17+
requestBodyLimit: process.env.MEMORY_MCP_BODY_LIMIT || '1mb'
18+
};
19+
20+
export default serverConfig;

ai/mcp/server/memory/index.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import app from './app.mjs';
2+
import serverConfig from './config.mjs';
3+
4+
const {host, port} = serverConfig;
5+
6+
app.listen(port, host, () => {
7+
// eslint-disable-next-line no-console
8+
console.log(`[Memory MCP] Server listening on http://${host}:${port}`);
9+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Wraps async Express route handlers so errors bubble into the error middleware.
3+
* @param {Function} fn The async handler to wrap
4+
* @returns {Function}
5+
*/
6+
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
7+
8+
export default asyncHandler;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Generic error handler that logs the error and sends a structured JSON response.
3+
* @param {Error} err
4+
* @param {import('express').Request} req
5+
* @param {import('express').Response} res
6+
* @param {import('express').NextFunction} next
7+
*/
8+
function errorHandler(err, req, res, next) {
9+
// eslint-disable-next-line no-console
10+
console.error('[Memory MCP] Unhandled error:', err);
11+
12+
if (res.headersSent) {
13+
next(err);
14+
return;
15+
}
16+
17+
const status = err.status ?? 500;
18+
19+
res.status(status).json({
20+
error: {
21+
message: err.message || 'Internal Server Error',
22+
code : err.code || 'internal_error'
23+
}
24+
});
25+
}
26+
27+
export default errorHandler;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Express middleware that handles unknown routes with a 404 response.
3+
* @param {import('express').Request} req
4+
* @param {import('express').Response} res
5+
*/
6+
function notFoundHandler(req, res) {
7+
res.status(404).json({
8+
error: {
9+
message: 'Endpoint not found',
10+
code : 'not_found'
11+
}
12+
});
13+
}
14+
15+
export default notFoundHandler;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Router} from 'express';
2+
import asyncHandler from '../middleware/asyncHandler.mjs';
3+
import {buildHealthResponse} from '../services/healthService.mjs';
4+
5+
const router = Router();
6+
7+
router.get('/healthcheck', asyncHandler(async (req, res) => {
8+
const payload = await buildHealthResponse();
9+
10+
res.status(200).json(payload);
11+
}));
12+
13+
export default router;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {ChromaClient} from 'chromadb';
2+
import aiConfig from '../../../../../buildScripts/ai/aiConfig.mjs';
3+
4+
/**
5+
* Simple manager around the Chroma client that lazily caches frequently used collections.
6+
*/
7+
class ChromaManager {
8+
constructor() {
9+
const {host, port} = aiConfig.memory;
10+
11+
this.client = new ChromaClient({host, port, ssl: false});
12+
this.memoryCollection = null;
13+
this.summaryCollection = null;
14+
}
15+
16+
/**
17+
* Ensures the process can reach the Chroma server and both collections are available.
18+
* @returns {Promise<{heartbeat: number, memoryCollection: string, summaryCollection: string}>}
19+
*/
20+
async checkConnectivity() {
21+
const heartbeat = await this.client.heartbeat();
22+
23+
const memory = await this.getMemoryCollection();
24+
const summaries = await this.getSummaryCollection();
25+
26+
return {
27+
heartbeat,
28+
memoryCollection : memory.name,
29+
summaryCollection : summaries.name
30+
};
31+
}
32+
33+
/**
34+
* @returns {Promise<import('chromadb').Collection>}
35+
*/
36+
async getMemoryCollection() {
37+
if (!this.memoryCollection) {
38+
const {collectionName} = aiConfig.memory;
39+
40+
this.memoryCollection = await this.client.getCollection({
41+
name : collectionName,
42+
embeddingFunction: aiConfig.dummyEmbeddingFunction
43+
});
44+
}
45+
46+
return this.memoryCollection;
47+
}
48+
49+
/**
50+
* @returns {Promise<import('chromadb').Collection>}
51+
*/
52+
async getSummaryCollection() {
53+
if (!this.summaryCollection) {
54+
const {collectionName} = aiConfig.sessions;
55+
56+
this.summaryCollection = await this.client.getOrCreateCollection({
57+
name : collectionName,
58+
embeddingFunction: aiConfig.dummyEmbeddingFunction
59+
});
60+
}
61+
62+
return this.summaryCollection;
63+
}
64+
}
65+
66+
const chromaManager = new ChromaManager();
67+
68+
export default chromaManager;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import aiConfig from '../../../../../buildScripts/ai/aiConfig.mjs';
2+
import chromaManager from './chromaManager.mjs';
3+
4+
/**
5+
* Builds the payload returned by GET /healthcheck.
6+
* @returns {Promise<Object>}
7+
*/
8+
export async function buildHealthResponse() {
9+
const {heartbeat, memoryCollection, summaryCollection} = await chromaManager.checkConnectivity();
10+
11+
return {
12+
status : 'healthy',
13+
timestamp: new Date().toISOString(),
14+
database : {
15+
host : aiConfig.memory.host,
16+
port : aiConfig.memory.port,
17+
heartbeat,
18+
collections: {
19+
memories : memoryCollection,
20+
summaries: summaryCollection
21+
}
22+
},
23+
uptime: process.uptime()
24+
};
25+
}

0 commit comments

Comments
 (0)