Skip to content

Commit 3ada1e7

Browse files
committed
Add Database Management Tools to Memory Core Server #7531
1 parent 93f278f commit 3ada1e7

6 files changed

Lines changed: 232 additions & 48 deletions

File tree

.github/ISSUE/epic-agent-managed-databases.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ Enhance the `knowledge-base` and `memory-core` MCP servers with tools that allow
1616

1717
- `ticket-kb-add-db-tools.md`: Add `start/stop_database` tools to the Knowledge Base server.
1818
- `ticket-mc-add-db-tools.md`: Add `start/stop_database` tools to the Memory Core server.
19+
- `ticket-kb-make-start-hybrid-aware.md`: Make the `start_database` tool hybrid-aware.

ai/mcp/server/config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ const aiConfig = {
6262
* Configuration for the project's main knowledge base.
6363
*/
6464
knowledgeBase: {
65+
/**
66+
* The hostname of the ChromaDB server for the knowledge base.
67+
* @type {string}
68+
*/
69+
host: 'localhost',
70+
/**
71+
* The port the ChromaDB server for the knowledge base is listening on.
72+
* @type {number}
73+
*/
74+
port: 8000,
6575
/**
6676
* The path to the generated knowledge base JSONL file.
6777
* @type {string}

ai/mcp/server/memory-core/openapi.yaml

Lines changed: 101 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ servers:
2323
tags:
2424
- name: Health
2525
description: Server health and status endpoints
26+
- name: "Database Lifecycle"
27+
description: "Tools for starting and stopping the database process"
2628
- name: Memories
2729
description: Operations on raw agent interaction memories
2830
- name: Summaries
@@ -59,6 +61,54 @@ paths:
5961
schema:
6062
$ref: '#/components/schemas/ErrorResponse'
6163

64+
/db/start:
65+
post:
66+
summary: Start Database
67+
operationId: start_database
68+
description: |
69+
Starts the ChromaDB database instance for the Memory Core as a background process.
70+
71+
**When to Use:**
72+
Use this tool if a `healthcheck` reveals that the database process is not running.
73+
tags: ["Database Lifecycle"]
74+
responses:
75+
'200':
76+
description: The database process was started successfully.
77+
content:
78+
application/json:
79+
schema:
80+
$ref: '#/components/schemas/DatabaseLifecycleResponse'
81+
'500':
82+
description: The database process is already running or failed to start.
83+
content:
84+
application/json:
85+
schema:
86+
$ref: '#/components/schemas/ErrorResponse'
87+
88+
/db/stop:
89+
post:
90+
summary: Stop Database
91+
operationId: stop_database
92+
description: |
93+
Stops the running ChromaDB database instance for the Memory Core.
94+
95+
**When to Use:**
96+
Use this tool to shut down the database process at the end of a session to free up resources.
97+
tags: ["Database Lifecycle"]
98+
responses:
99+
'200':
100+
description: The database process was stopped successfully.
101+
content:
102+
application/json:
103+
schema:
104+
$ref: '#/components/schemas/DatabaseLifecycleResponse'
105+
'500':
106+
description: The database process is not running or failed to stop.
107+
content:
108+
application/json:
109+
schema:
110+
$ref: '#/components/schemas/ErrorResponse'
111+
62112
/memories:
63113
post:
64114
summary: Add New Memory
@@ -445,36 +495,48 @@ components:
445495
database:
446496
type: object
447497
properties:
448-
connected:
449-
type: boolean
450-
example: true
451-
collections:
498+
process:
452499
type: object
453500
properties:
454-
memories:
455-
type: object
456-
properties:
457-
name:
458-
type: string
459-
example: "neo-agent-memory"
460-
exists:
461-
type: boolean
462-
example: true
463-
count:
464-
type: integer
465-
example: 1234
466-
summaries:
501+
running:
502+
type: boolean
503+
example: true
504+
pid:
505+
type: integer
506+
example: 12345
507+
connection:
508+
type: object
509+
properties:
510+
connected:
511+
type: boolean
512+
example: true
513+
collections:
467514
type: object
468515
properties:
469-
name:
470-
type: string
471-
example: "neo-agent-sessions"
472-
exists:
473-
type: boolean
474-
example: true
475-
count:
476-
type: integer
477-
example: 56
516+
memories:
517+
type: object
518+
properties:
519+
name:
520+
type: string
521+
example: "neo-agent-memory"
522+
exists:
523+
type: boolean
524+
example: true
525+
count:
526+
type: integer
527+
example: 1234
528+
summaries:
529+
type: object
530+
properties:
531+
name:
532+
type: string
533+
example: "neo-agent-sessions"
534+
exists:
535+
type: boolean
536+
example: true
537+
count:
538+
type: integer
539+
example: 56
478540
version:
479541
type: string
480542
example: "1.0.0"
@@ -483,6 +545,18 @@ components:
483545
description: Server uptime in seconds
484546
example: 3600
485547

548+
DatabaseLifecycleResponse:
549+
type: object
550+
properties:
551+
status:
552+
type: string
553+
enum: [started, stopped, already_running, not_running]
554+
pid:
555+
type: integer
556+
nullable: true
557+
detail:
558+
type: string
559+
486560
AddMemoryRequest:
487561
type: object
488562
required:
@@ -812,4 +886,4 @@ components:
812886
example: "Could not connect to ChromaDB on localhost:8001"
813887
code:
814888
type: string
815-
example: "DB_CONNECTION_ERROR"
889+
example: "DB_CONNECTION_ERROR"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { ChromaClient } from 'chromadb';
2+
import { spawn } from 'child_process';
3+
import aiConfig from '../../config.mjs';
4+
5+
// This will hold the child process object for the ChromaDB server
6+
let chromaProcess = null;
7+
8+
/**
9+
* Checks if a ChromaDB instance is already running on the configured port.
10+
* @returns {Promise<boolean>}
11+
*/
12+
async function isDbRunning() {
13+
try {
14+
const { host, port } = aiConfig.memory;
15+
const client = new ChromaClient({ host, port });
16+
await client.heartbeat();
17+
return true;
18+
} catch (e) {
19+
return false;
20+
}
21+
}
22+
23+
/**
24+
* Starts the ChromaDB server as a background process, if not already running.
25+
* @returns {Promise<object>} A promise that resolves with the status.
26+
*/
27+
async function start_database() {
28+
if (chromaProcess && !chromaProcess.killed) {
29+
return { status: 'already_running', pid: chromaProcess.pid, detail: 'Server was started by this process.' };
30+
}
31+
32+
if (await isDbRunning()) {
33+
return { status: 'already_running', pid: null, detail: 'Server was started externally.' };
34+
}
35+
36+
return new Promise((resolve, reject) => {
37+
const { port, path: dbPath } = aiConfig.memory;
38+
const args = ['run', '--path', dbPath, '--port', port.toString()];
39+
40+
chromaProcess = spawn('chroma', args, {
41+
detached: true,
42+
stdio: 'ignore'
43+
});
44+
45+
chromaProcess.on('spawn', () => {
46+
console.log(`ChromaDB (Memory Core) process started with PID: ${chromaProcess.pid}`);
47+
resolve({ status: 'started', pid: chromaProcess.pid });
48+
});
49+
50+
chromaProcess.on('error', (err) => {
51+
console.error('Failed to start ChromaDB (Memory Core) process:', err);
52+
chromaProcess = null;
53+
reject(err);
54+
});
55+
56+
chromaProcess.unref();
57+
});
58+
}
59+
60+
/**
61+
* Stops the ChromaDB server process if it was started by this server.
62+
* @returns {Promise<object>} A promise that resolves with the status.
63+
*/
64+
async function stop_database() {
65+
if (!chromaProcess || chromaProcess.killed) {
66+
return { status: 'not_running', detail: 'No process was started by this server.' };
67+
}
68+
69+
return new Promise((resolve) => {
70+
chromaProcess.on('exit', () => {
71+
console.log(`ChromaDB process with PID: ${chromaProcess.pid} has been stopped.`);
72+
chromaProcess = null;
73+
resolve({ status: 'stopped' });
74+
});
75+
76+
process.kill(-chromaProcess.pid, 'SIGTERM');
77+
});
78+
}
79+
80+
/**
81+
* Gets the status of the ChromaDB process.
82+
* @returns {object} The status of the ChromaDB process.
83+
*/
84+
function get_database_status() {
85+
if (chromaProcess && !chromaProcess.killed) {
86+
return { running: true, pid: chromaProcess.pid, managed: true };
87+
}
88+
return { running: false, pid: null, managed: false };
89+
}
90+
91+
export {
92+
start_database,
93+
stop_database,
94+
get_database_status
95+
};

ai/mcp/server/memory-core/services/healthService.mjs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import chromaManager from './chromaManager.mjs';
1+
import { get_database_status } from './databaseLifecycleService.mjs';
22
import aiConfig from '../../config.mjs';
3+
import chromaManager from './chromaManager.mjs';
34

4-
/**
5-
* Verifies that the server is running and can successfully connect to the
6-
* ChromaDB vector database, including checking for collection existence and counts.
7-
* @returns {Promise<object>} A promise that resolves to the health check status object.
8-
*/
95
export async function buildHealthResponse() {
6+
const processStatus = get_database_status();
107
try {
118
await chromaManager.client.heartbeat();
129

1310
let memoryCollection, summaryCollection;
1411
let memoryCount = 0, summaryCount = 0;
1512

16-
// These calls will throw if the collection doesn't exist. We let the outer block catch it
17-
// if it's a connection issue, but for "not found", we handle it gracefully.
1813
memoryCollection = await chromaManager.getMemoryCollection().catch(() => null);
1914
if (memoryCollection) {
2015
memoryCount = await memoryCollection.count();
@@ -28,17 +23,20 @@ export async function buildHealthResponse() {
2823
return {
2924
status: "healthy",
3025
database: {
31-
connected: true,
32-
collections: {
33-
memories: {
34-
name: aiConfig.memory.collectionName,
35-
exists: !!memoryCollection,
36-
count: memoryCount
37-
},
38-
summaries: {
39-
name: aiConfig.sessions.collectionName,
40-
exists: !!summaryCollection,
41-
count: summaryCount
26+
process: processStatus,
27+
connection: {
28+
connected: true,
29+
collections: {
30+
memories: {
31+
name: aiConfig.memory.collectionName,
32+
exists: !!memoryCollection,
33+
count: memoryCount
34+
},
35+
summaries: {
36+
name: aiConfig.sessions.collectionName,
37+
exists: !!summaryCollection,
38+
count: summaryCount
39+
}
4240
}
4341
}
4442
},
@@ -49,8 +47,11 @@ export async function buildHealthResponse() {
4947
return {
5048
status: "unhealthy",
5149
database: {
52-
connected: false,
53-
error: error.message
50+
process: processStatus,
51+
connection: {
52+
connected: false,
53+
error: error.message
54+
}
5455
}
5556
};
5657
}

ai/mcp/server/memory-core/services/toolService.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as healthService from './healthService.mjs';
66
import * as memoryService from './memoryService.mjs';
77
import * as sessionService from './sessionService.mjs';
88
import * as summaryService from './summaryService.mjs';
9+
import * as databaseLifecycleService from './databaseLifecycleService.mjs';
910

1011
const __filename = fileURLToPath(import.meta.url);
1112
const __dirname = path.dirname(__filename);
@@ -22,6 +23,8 @@ const serviceMapping = {
2223
summarize_sessions : sessionService.summarizeSessions,
2324
export_database : dbService.exportDatabase,
2425
import_database : dbService.importDatabase,
26+
start_database : databaseLifecycleService.start_database,
27+
stop_database : databaseLifecycleService.stop_database
2528
};
2629

2730
initialize(serviceMapping, openApiFilePath);

0 commit comments

Comments
 (0)