@knowall-ai/reverie 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,418 @@
1
+ /**
2
+ * `reverie serve`: the same MCP server over Streamable HTTP at /mcp, plus the read-only Brain API
3
+ * (/brain/graph, /brain/state, /brain/events as Server-Sent Events) the Agents Portal's Brain tab
4
+ * uses. One bearer token (REVERIE_SERVE_TOKEN) protects everything except the health check.
5
+ * Plain node:http: nothing here needs a framework.
6
+ */
7
+ import http from 'node:http';
8
+ import { timingSafeEqual } from 'node:crypto';
9
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
+ import { diffSnapshot, isEmptyDiff, parseLimit, snapshot, snapshotAsDiff, state } from './brain.js';
11
+ import { neo4jConfigError, neo4jConfigFromEnv } from './config.js';
12
+ import { START_CURSOR, eventsPath, readSince } from './events.js';
13
+ import { Neo4jClient } from './neo4j-client.js';
14
+ import { createMcpServer, safeCreateEmbedder } from './server.js';
15
+ export class ConfigError extends Error {
16
+ }
17
+ const MAX_BODY_BYTES = 1024 * 1024;
18
+ const PING_SECONDS = 15;
19
+ const SHUTDOWN_GRACE_MS = 10_000;
20
+ const HEALTH_CACHE_MS = 5_000;
21
+ const HEALTH_TIMEOUT_MS = 5_000;
22
+ const BACKPRESSURE_GRACE_MS = 30_000;
23
+ const PENDING_ACTIVATIONS_MAX = 1000;
24
+ function settingsFromEnv(env) {
25
+ const token = env.REVERIE_SERVE_TOKEN?.trim();
26
+ if (!token) {
27
+ throw new ConfigError('REVERIE_SERVE_TOKEN is required for reverie serve');
28
+ }
29
+ const rawPort = env.REVERIE_HTTP_PORT?.trim() || '8643';
30
+ const port = /^\d{1,5}$/.test(rawPort) ? Number.parseInt(rawPort, 10) : Number.NaN;
31
+ if (!Number.isInteger(port) || port > 65535) {
32
+ throw new ConfigError(`REVERIE_HTTP_PORT must be a port number, got ${env.REVERIE_HTTP_PORT}`);
33
+ }
34
+ const seconds = (variable, fallback) => {
35
+ const parsed = Number.parseFloat(env[variable]?.trim() || '');
36
+ return Number.isFinite(parsed) && parsed > 0 ? parsed * 1000 : fallback * 1000;
37
+ };
38
+ return {
39
+ token,
40
+ // Streamable HTTP requires Origin validation. Non-browser clients send no Origin; a browser
41
+ // origin is accepted only when listed in REVERIE_ALLOWED_ORIGINS (comma-separated).
42
+ allowedOrigins: new Set((env.REVERIE_ALLOWED_ORIGINS ?? '').split(',').map((o) => o.trim().toLowerCase()).filter(Boolean)),
43
+ host: env.REVERIE_HTTP_HOST?.trim() || '127.0.0.1',
44
+ port,
45
+ graphPollMs: seconds('REVERIE_GRAPH_POLL_SECONDS', 5),
46
+ statePollMs: seconds('REVERIE_STATE_SECONDS', 5),
47
+ eventPollMs: seconds('REVERIE_EVENT_POLL_SECONDS', 1)
48
+ };
49
+ }
50
+ /** Constant-time bearer check; a wrong length is a plain mismatch, never a timing tell. */
51
+ export function isAuthorized(header, token) {
52
+ if (!header || !token) {
53
+ return false;
54
+ }
55
+ const match = /^Bearer\s+(\S+)$/i.exec(header.trim());
56
+ if (!match) {
57
+ return false;
58
+ }
59
+ const given = Buffer.from(match[1], 'utf8');
60
+ const expected = Buffer.from(token, 'utf8');
61
+ return given.length === expected.length && timingSafeEqual(given, expected);
62
+ }
63
+ function sendJson(res, status, body) {
64
+ const payload = JSON.stringify(body);
65
+ res.writeHead(status, {
66
+ 'Content-Type': 'application/json',
67
+ 'Content-Length': Buffer.byteLength(payload),
68
+ 'Cache-Control': 'no-store'
69
+ });
70
+ res.end(payload);
71
+ }
72
+ class BodyTooLarge extends Error {
73
+ }
74
+ function readJsonBody(req) {
75
+ return new Promise((resolve, reject) => {
76
+ let chunks = [];
77
+ let size = 0;
78
+ let tooLarge = false;
79
+ req.on('data', (chunk) => {
80
+ size += chunk.length;
81
+ if (tooLarge) {
82
+ return;
83
+ }
84
+ if (size > MAX_BODY_BYTES) {
85
+ // Keep draining so the client receives the 413 instead of a connection reset.
86
+ tooLarge = true;
87
+ chunks = [];
88
+ return;
89
+ }
90
+ chunks.push(chunk);
91
+ });
92
+ req.on('end', () => {
93
+ if (tooLarge) {
94
+ reject(new BodyTooLarge('request body exceeds 1 MiB'));
95
+ return;
96
+ }
97
+ const text = Buffer.concat(chunks).toString('utf8');
98
+ if (!text.trim()) {
99
+ resolve(undefined);
100
+ return;
101
+ }
102
+ try {
103
+ resolve(JSON.parse(text));
104
+ }
105
+ catch (error) {
106
+ reject(new SyntaxError(`invalid JSON body: ${error instanceof Error ? error.message : error}`));
107
+ }
108
+ });
109
+ req.on('error', reject);
110
+ // A client abort can close the request without 'end' or 'error'; do not leave the handler pending.
111
+ req.on('close', () => {
112
+ if (!req.readableEnded) {
113
+ reject(new Error('request closed before the body was received'));
114
+ }
115
+ });
116
+ });
117
+ }
118
+ function errorMessage(error) {
119
+ return (error instanceof Error ? error.message : String(error)).slice(0, 200);
120
+ }
121
+ export async function startHttpServer(env = process.env) {
122
+ const settings = settingsFromEnv(env);
123
+ const configError = neo4jConfigError(env);
124
+ if (configError) {
125
+ throw new ConfigError(configError);
126
+ }
127
+ const config = neo4jConfigFromEnv(env);
128
+ const neo4j = config ? new Neo4jClient(config.uri, config.username, config.password, config.database) : null;
129
+ const embedder = config ? safeCreateEmbedder() : null;
130
+ const run = neo4j ? (cypher, params) => neo4j.executeQuery(cypher, params ?? {}) : null;
131
+ // The health probe is unauthenticated, so it is cached and coalesced: one Neo4j query in flight
132
+ // at a time, tracked until it really settles (a timed-out answer does not release it), and its
133
+ // result cached for HEALTH_CACHE_MS. Callers wait at most HEALTH_TIMEOUT_MS and get false meanwhile.
134
+ let healthCache = null;
135
+ let healthQuery = null;
136
+ function neo4jHealthy() {
137
+ if (!neo4j) {
138
+ return Promise.resolve(false);
139
+ }
140
+ if (healthCache && Date.now() - healthCache.at < HEALTH_CACHE_MS) {
141
+ return Promise.resolve(healthCache.ok);
142
+ }
143
+ if (!healthQuery) {
144
+ healthQuery = neo4j
145
+ .executeQuery('RETURN 1 AS ok')
146
+ .then(() => true, () => false)
147
+ .then((ok) => {
148
+ healthCache = { at: Date.now(), ok };
149
+ healthQuery = null;
150
+ return ok;
151
+ });
152
+ }
153
+ let timer;
154
+ const timeout = new Promise((resolve) => {
155
+ timer = setTimeout(() => resolve(false), HEALTH_TIMEOUT_MS);
156
+ timer.unref();
157
+ });
158
+ return Promise.race([healthQuery, timeout]).finally(() => clearTimeout(timer));
159
+ }
160
+ async function handleMcp(req, res) {
161
+ let body;
162
+ if (req.method === 'POST') {
163
+ try {
164
+ body = await readJsonBody(req);
165
+ }
166
+ catch (error) {
167
+ if (error instanceof BodyTooLarge) {
168
+ sendJson(res, 413, { error: error.message });
169
+ }
170
+ else {
171
+ sendJson(res, 400, { error: errorMessage(error) });
172
+ }
173
+ return;
174
+ }
175
+ }
176
+ // Stateless: a fresh protocol server and transport per request, sharing the Neo4j client and embedder.
177
+ const server = createMcpServer(neo4j, embedder);
178
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
179
+ res.on('close', () => {
180
+ transport.close().catch(() => undefined);
181
+ server.close().catch(() => undefined);
182
+ });
183
+ await server.connect(transport);
184
+ await transport.handleRequest(req, res, body);
185
+ }
186
+ function streamEvents(req, res, limit) {
187
+ res.writeHead(200, {
188
+ 'Content-Type': 'text/event-stream; charset=utf-8',
189
+ 'Cache-Control': 'no-cache',
190
+ Connection: 'keep-alive',
191
+ 'X-Accel-Buffering': 'no'
192
+ });
193
+ // Backpressure: when the client stops reading, stop producing. Activations wait in a bounded
194
+ // queue and the graph baseline only advances once its frame was handed to the socket, so
195
+ // nothing is silently lost; a connection that has not drained in BACKPRESSURE_GRACE_MS is dropped.
196
+ let blocked = false;
197
+ let blockedSince = 0;
198
+ const pending = [];
199
+ const write = (chunk) => {
200
+ if (blocked || res.writableEnded) {
201
+ return false;
202
+ }
203
+ if (!res.write(chunk)) {
204
+ blocked = true;
205
+ blockedSince = Date.now();
206
+ res.once('drain', () => {
207
+ blocked = false;
208
+ flushPending();
209
+ });
210
+ }
211
+ return true;
212
+ };
213
+ const send = (event, data) => write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
214
+ const flushPending = () => {
215
+ while (pending.length > 0 && !blocked && !res.writableEnded) {
216
+ send('activation', pending.shift());
217
+ }
218
+ };
219
+ const queueActivations = (events) => {
220
+ pending.push(...events);
221
+ flushPending(); // deliver what the socket will take before any cap applies
222
+ if (pending.length > PENDING_ACTIVATIONS_MAX) {
223
+ // A client that has not read for this long loses the oldest queued activations; say so.
224
+ const dropped = pending.splice(0, pending.length - PENDING_ACTIVATIONS_MAX).length;
225
+ pending.unshift({ ts: Date.now() / 1000, kind: 'gap', dropped });
226
+ }
227
+ };
228
+ const file = eventsPath(env);
229
+ let cursor = START_CURSOR;
230
+ if (file) {
231
+ // One read gives both the recent history to replay and the cursor to continue from, so no
232
+ // event can fall between the two.
233
+ const initial = readSince(file, START_CURSOR);
234
+ cursor = initial.cursor;
235
+ queueActivations(initial.events.slice(-30));
236
+ }
237
+ let prev = null;
238
+ let polling = false;
239
+ const pollGraph = async () => {
240
+ if (polling || blocked || !run) {
241
+ return;
242
+ }
243
+ polling = true;
244
+ try {
245
+ const curr = await snapshot(run, limit);
246
+ // The first frame is the full snapshot, so the stream never depends on what the client
247
+ // fetched from /brain/graph moments earlier.
248
+ const diff = prev ? diffSnapshot(prev, curr) : snapshotAsDiff(curr);
249
+ if (isEmptyDiff(diff) || send('graph', { ...diff, stats: curr.stats })) {
250
+ prev = curr;
251
+ }
252
+ }
253
+ catch (error) {
254
+ send('error', { message: errorMessage(error) });
255
+ }
256
+ finally {
257
+ polling = false;
258
+ }
259
+ };
260
+ const timers = [];
261
+ const stop = () => {
262
+ for (const timer of timers) {
263
+ clearInterval(timer);
264
+ }
265
+ if (!res.writableEnded) {
266
+ res.end();
267
+ }
268
+ };
269
+ req.on('close', stop);
270
+ res.on('error', stop);
271
+ pollGraph().then(() => send('state', state(undefined, undefined, env)));
272
+ timers.push(setInterval(() => {
273
+ if (!file) {
274
+ return;
275
+ }
276
+ const next = readSince(file, cursor);
277
+ cursor = next.cursor;
278
+ queueActivations(next.events);
279
+ }, settings.eventPollMs), setInterval(() => void pollGraph(), settings.graphPollMs), setInterval(() => send('state', state(undefined, undefined, env)), settings.statePollMs), setInterval(() => {
280
+ if (blocked && Date.now() - blockedSince > BACKPRESSURE_GRACE_MS) {
281
+ stop();
282
+ res.destroy();
283
+ return;
284
+ }
285
+ write(': ping\n\n');
286
+ }, PING_SECONDS * 1000));
287
+ }
288
+ async function route(req, res) {
289
+ const url = new URL(req.url ?? '/', 'http://localhost');
290
+ const path = url.pathname.replace(/\/+$/, '') || '/';
291
+ const method = req.method ?? 'GET';
292
+ // Browser callers: an Origin is accepted only when allowlisted, and then gets real CORS
293
+ // answers (preflight before auth, since browsers send preflights without credentials).
294
+ const origin = req.headers.origin?.trim();
295
+ const originAllowed = origin !== undefined && settings.allowedOrigins.has(origin.toLowerCase());
296
+ if (origin !== undefined && !originAllowed) {
297
+ sendJson(res, 403, { error: 'Origin not allowed' });
298
+ return;
299
+ }
300
+ if (originAllowed) {
301
+ res.setHeader('Access-Control-Allow-Origin', origin);
302
+ res.setHeader('Vary', 'Origin');
303
+ if (method === 'OPTIONS') {
304
+ res.writeHead(204, {
305
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
306
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version',
307
+ 'Access-Control-Max-Age': '600'
308
+ });
309
+ res.end();
310
+ return;
311
+ }
312
+ }
313
+ if ((path === '/health' || path === '/brain/health') && method === 'GET') {
314
+ sendJson(res, 200, { ok: true, neo4j: await neo4jHealthy(), ts: Date.now() / 1000 });
315
+ return;
316
+ }
317
+ if (!isAuthorized(req.headers.authorization, settings.token)) {
318
+ sendJson(res, 401, { error: 'Unauthorized' });
319
+ return;
320
+ }
321
+ if (path === '/mcp') {
322
+ if (method !== 'POST' && method !== 'GET' && method !== 'DELETE') {
323
+ sendJson(res, 405, { error: 'Method not allowed' });
324
+ return;
325
+ }
326
+ await handleMcp(req, res);
327
+ return;
328
+ }
329
+ if (path.startsWith('/brain/')) {
330
+ if (method !== 'GET') {
331
+ sendJson(res, 405, { error: 'Method not allowed' });
332
+ return;
333
+ }
334
+ const limit = parseLimit(url.searchParams.get('limit'));
335
+ if (limit === null) {
336
+ sendJson(res, 400, { error: 'limit must be an integer between 1 and 1500' });
337
+ return;
338
+ }
339
+ if (!run) {
340
+ sendJson(res, 503, { error: 'Neo4j not configured' });
341
+ return;
342
+ }
343
+ if (path === '/brain/graph') {
344
+ try {
345
+ const snap = await snapshot(run, limit);
346
+ sendJson(res, 200, { ...snap, state: state(undefined, undefined, env) });
347
+ }
348
+ catch (error) {
349
+ sendJson(res, 502, { error: errorMessage(error) });
350
+ }
351
+ return;
352
+ }
353
+ if (path === '/brain/state') {
354
+ sendJson(res, 200, state(undefined, undefined, env));
355
+ return;
356
+ }
357
+ if (path === '/brain/events') {
358
+ streamEvents(req, res, limit);
359
+ return;
360
+ }
361
+ }
362
+ sendJson(res, 404, { error: 'Not found' });
363
+ }
364
+ const server = http.createServer((req, res) => {
365
+ const started = Date.now();
366
+ res.on('finish', () => console.error(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - started}ms`));
367
+ route(req, res).catch((error) => {
368
+ console.error('Request failed:', error);
369
+ if (!res.headersSent) {
370
+ sendJson(res, 500, { jsonrpc: '2.0', error: { code: -32603, message: errorMessage(error) }, id: null });
371
+ }
372
+ else if (!res.writableEnded) {
373
+ res.end();
374
+ }
375
+ });
376
+ });
377
+ await new Promise((resolve, reject) => {
378
+ server.once('error', reject);
379
+ server.listen(settings.port, settings.host, () => {
380
+ server.off('error', reject);
381
+ resolve();
382
+ });
383
+ });
384
+ const address = server.address();
385
+ const port = typeof address === 'object' && address ? address.port : settings.port;
386
+ console.error(`Reverie HTTP server listening on http://${settings.host}:${port}`);
387
+ let closing = null;
388
+ const close = () => {
389
+ if (!closing) {
390
+ closing = new Promise((resolve) => {
391
+ server.close(() => resolve());
392
+ server.closeAllConnections?.();
393
+ }).then(() => neo4j?.close());
394
+ }
395
+ return closing;
396
+ };
397
+ return { server, host: settings.host, port, close };
398
+ }
399
+ /** Wire SIGINT/SIGTERM to a bounded shutdown; used by the `reverie serve` entry point. */
400
+ export function installShutdown(handle) {
401
+ const shutdown = () => {
402
+ console.error('Shutting down Reverie HTTP server…');
403
+ const force = setTimeout(() => {
404
+ console.error('Forced shutdown');
405
+ process.exit(1);
406
+ }, SHUTDOWN_GRACE_MS);
407
+ force.unref();
408
+ handle
409
+ .close()
410
+ .then(() => process.exit(0))
411
+ .catch((error) => {
412
+ console.error('Error during shutdown:', error);
413
+ process.exit(1);
414
+ });
415
+ };
416
+ process.once('SIGINT', shutdown);
417
+ process.once('SIGTERM', shutdown);
418
+ }
package/build/index.js CHANGED
@@ -1,65 +1,63 @@
1
1
  #!/usr/bin/env node
2
+ import { neo4jConfigError, neo4jConfigFromEnv } from './config.js';
2
3
  import { Neo4jServer } from './server.js';
3
4
  // Debug environment for Smithery
4
5
  if (process.env.DEBUG_SMITHERY) {
5
6
  console.error('Environment variables:', {
6
7
  NEO4J_URI: process.env.NEO4J_URI,
7
8
  NEO4J_USERNAME: process.env.NEO4J_USERNAME,
8
- NEO4J_PASSWORD: process.env.NEO4J_PASSWORD,
9
+ NEO4J_PASSWORD: process.env.NEO4J_PASSWORD ? '(set)' : undefined,
9
10
  NEO4J_DATABASE: process.env.NEO4J_DATABASE
10
11
  });
11
12
  }
12
- // Check if we have database configuration (ignore empty strings)
13
- const uri = process.env.NEO4J_URI?.trim();
14
- const username = process.env.NEO4J_USERNAME?.trim();
15
- const password = process.env.NEO4J_PASSWORD?.trim();
16
- const hasDbConfig = uri && username && password;
17
- // Only validate environment variables if at least one non-empty value is provided
18
- if ((uri || username || password) && !hasDbConfig) {
19
- // If any Neo4j env var is set, all required ones must be set
20
- if (!password) {
21
- console.error('Error: NEO4J_PASSWORD environment variable is required');
22
- process.exit(1);
23
- }
24
- if (!uri) {
25
- console.error('Error: NEO4J_URI environment variable is required');
26
- process.exit(1);
27
- }
28
- if (!username) {
29
- console.error('Error: NEO4J_USERNAME environment variable is required');
30
- process.exit(1);
31
- }
32
- }
33
- const config = hasDbConfig ? {
34
- uri: uri,
35
- username: username,
36
- password: password,
37
- database: process.env.NEO4J_DATABASE?.trim() || undefined, // Optional for Neo4j Community Edition
38
- } : undefined;
39
- // サーバーの起動
40
- const server = new Neo4jServer(config);
41
- server.run().catch((error) => {
42
- console.error('Failed to start Neo4j MCP server:', error);
13
+ const configError = neo4jConfigError();
14
+ if (configError) {
15
+ console.error(configError);
43
16
  process.exit(1);
44
- });
45
- // 終了時のクリーンアップ
46
- process.on('SIGINT', async () => {
17
+ }
18
+ const config = neo4jConfigFromEnv();
19
+ if (process.argv[2] === 'serve') {
20
+ const { ConfigError, installShutdown, startHttpServer } = await import('./http-server.js');
47
21
  try {
48
- await server.close();
49
- process.exit(0);
22
+ installShutdown(await startHttpServer());
50
23
  }
51
24
  catch (error) {
52
- console.error('Error during shutdown:', error);
25
+ console.error(error instanceof ConfigError ? `Error: ${error.message}` : `Failed to start Reverie HTTP server: ${error instanceof Error ? error.message : error}`);
53
26
  process.exit(1);
54
27
  }
55
- });
56
- process.on('SIGTERM', async () => {
57
- try {
58
- await server.close();
59
- process.exit(0);
60
- }
61
- catch (error) {
62
- console.error('Error during shutdown:', error);
28
+ }
29
+ else if (process.argv.length > 2) {
30
+ console.error('Usage: reverie [serve]');
31
+ process.exit(2);
32
+ }
33
+ else {
34
+ startStdio();
35
+ }
36
+ function startStdio() {
37
+ const server = new Neo4jServer(config);
38
+ server.run().catch((error) => {
39
+ console.error('Failed to start Neo4j MCP server:', error);
63
40
  process.exit(1);
64
- }
65
- });
41
+ });
42
+ let shuttingDown = false;
43
+ const shutdown = async () => {
44
+ if (shuttingDown) {
45
+ return;
46
+ }
47
+ shuttingDown = true;
48
+ process.off('SIGINT', shutdown);
49
+ process.off('SIGTERM', shutdown);
50
+ const force = setTimeout(() => process.exit(1), 10_000);
51
+ force.unref();
52
+ try {
53
+ await server.close();
54
+ process.exit(0);
55
+ }
56
+ catch (error) {
57
+ console.error('Error during shutdown:', error);
58
+ process.exit(1);
59
+ }
60
+ };
61
+ process.on('SIGINT', shutdown);
62
+ process.on('SIGTERM', shutdown);
63
+ }
package/build/server.js CHANGED
@@ -6,7 +6,7 @@ import { handleToolCall } from './handlers/index.js';
6
6
  import { Neo4jClient } from './neo4j-client.js';
7
7
  import { tools } from './tools/definitions.js';
8
8
  /** A misconfigured provider (missing key, http endpoint…) must not stop the server; search degrades to keyword. */
9
- function safeCreateEmbedder() {
9
+ export function safeCreateEmbedder() {
10
10
  try {
11
11
  return createEmbedder();
12
12
  }
@@ -15,47 +15,35 @@ function safeCreateEmbedder() {
15
15
  return null;
16
16
  }
17
17
  }
18
+ export const VERSION = '0.5.0';
19
+ /** The MCP protocol server with Reverie's tools, bound to a (possibly absent) Neo4j client and embedder. */
20
+ export function createMcpServer(neo4j, embedder) {
21
+ const server = new Server({ name: 'reverie', version: VERSION }, { capabilities: { tools: {} } });
22
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
23
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
24
+ if (!neo4j) {
25
+ return {
26
+ content: [
27
+ {
28
+ type: 'text',
29
+ text: 'Neo4j connection not configured. Please set NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD environment variables.',
30
+ },
31
+ ],
32
+ isError: true,
33
+ };
34
+ }
35
+ const { name, arguments: args } = request.params;
36
+ return handleToolCall(name, args, neo4j, embedder);
37
+ });
38
+ server.onerror = (error) => console.error('[MCP Error]', error);
39
+ return server;
40
+ }
18
41
  export class Neo4jServer {
19
42
  server;
20
43
  neo4j;
21
- embedder;
22
44
  constructor(config) {
23
- this.server = new Server({
24
- name: 'reverie',
25
- version: '0.4.0',
26
- }, {
27
- capabilities: {
28
- tools: {},
29
- },
30
- });
31
45
  this.neo4j = config ? new Neo4jClient(config.uri, config.username, config.password, config.database) : null;
32
- this.embedder = config ? safeCreateEmbedder() : null;
33
- this.setupToolHandlers();
34
- this.server.onerror = (error) => console.error('[MCP Error]', error);
35
- process.on('SIGINT', async () => {
36
- await this.close();
37
- process.exit(0);
38
- });
39
- }
40
- setupToolHandlers() {
41
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
42
- tools,
43
- }));
44
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
45
- if (!this.neo4j) {
46
- return {
47
- content: [
48
- {
49
- type: 'text',
50
- text: 'Neo4j connection not configured. Please set NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD environment variables.',
51
- },
52
- ],
53
- isError: true,
54
- };
55
- }
56
- const { name, arguments: args } = request.params;
57
- return handleToolCall(name, args, this.neo4j, this.embedder);
58
- });
46
+ this.server = createMcpServer(this.neo4j, config ? safeCreateEmbedder() : null);
59
47
  }
60
48
  async run() {
61
49
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowall-ai/reverie",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Reverie — graph memory that dreams. Graph-based memory system for AI agents. Store people, places, organizations as nodes. Build semantic relationships (KNOWS, WORKS_AT, CREATED). Hybrid keyword + semantic search finds partial matches and close variants. Filter by date, traverse relationships, maintain temporal context. Simple tools, LLM handles complexity. 12 tools for complete memory management.",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -16,6 +16,7 @@
16
16
  "prepare": "npm run build",
17
17
  "watch": "tsc --watch",
18
18
  "inspector": "npx @modelcontextprotocol/inspector build/index.js",
19
+ "start:http": "node build/index.js serve",
19
20
  "test": "node --test tests/unit.test.mjs tests/startup.test.mjs tests/integration.test.mjs",
20
21
  "test:unit": "node --test tests/unit.test.mjs tests/startup.test.mjs",
21
22
  "test:integration": "node --test tests/integration.test.mjs",
@@ -23,10 +24,11 @@
23
24
  },
24
25
  "dependencies": {
25
26
  "@huggingface/transformers": "^3.8.1",
26
- "@modelcontextprotocol/sdk": "0.6.0",
27
+ "@modelcontextprotocol/sdk": "^1.30.0",
27
28
  "dotenv": "^17.2.0",
28
29
  "neo4j-driver": "^5.27.0",
29
- "uuid": "^11.0.4"
30
+ "uuid": "^11.0.4",
31
+ "zod": "^3.25.76"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@types/node": "^20.11.24",