@aeriondyseti/vector-memory-mcp 0.5.0 → 0.8.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,255 @@
1
+ /**
2
+ * MCP HTTP Transport Handler
3
+ *
4
+ * Provides StreamableHTTP transport for MCP over HTTP.
5
+ * and other HTTP-based MCP clients to connect to the memory server.
6
+ *
7
+ * This implementation handles the MCP protocol directly using Hono's streaming
8
+ * capabilities, since StreamableHTTPServerTransport expects Node.js req/res objects.
9
+ */
10
+
11
+ import { Hono } from "hono";
12
+ import { streamSSE } from "hono/streaming";
13
+ import { randomUUID } from "node:crypto";
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import {
16
+ CallToolRequestSchema,
17
+ ListToolsRequestSchema,
18
+ type JSONRPCMessage,
19
+ type JSONRPCRequest,
20
+ type JSONRPCNotification,
21
+ } from "@modelcontextprotocol/sdk/types.js";
22
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
23
+
24
+ import { tools } from "../mcp/tools.js";
25
+ import { handleToolCall } from "../mcp/handlers.js";
26
+ import type { MemoryService } from "../services/memory.service.js";
27
+
28
+ interface Session {
29
+ server: Server;
30
+ serverTransport: InstanceType<typeof InMemoryTransport>;
31
+ clientTransport: InstanceType<typeof InMemoryTransport>;
32
+ pendingResponses: Map<string | number, (response: JSONRPCMessage) => void>;
33
+ sseClients: Set<(message: JSONRPCMessage) => void>;
34
+ }
35
+
36
+ /**
37
+ * Creates MCP routes for a Hono app.
38
+ *
39
+ * Uses InMemoryTransport internally and bridges to HTTP/SSE manually,
40
+ * since StreamableHTTPServerTransport requires Node.js req/res objects.
41
+ */
42
+ export function createMcpRoutes(memoryService: MemoryService): Hono {
43
+ const app = new Hono();
44
+
45
+ // Store active sessions by session ID
46
+ const sessions: Map<string, Session> = new Map();
47
+
48
+ /**
49
+ * Creates a new MCP server instance configured with memory tools.
50
+ */
51
+ async function createSession(): Promise<Session> {
52
+ const server = new Server(
53
+ { name: "vector-memory-mcp", version: "0.6.0" },
54
+ { capabilities: { tools: {} } }
55
+ );
56
+
57
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
58
+ return { tools };
59
+ });
60
+
61
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
62
+ const { name, arguments: args } = request.params;
63
+ return handleToolCall(name, args, memoryService);
64
+ });
65
+
66
+ // Create linked in-memory transports
67
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
68
+
69
+ // Connect server to its transport
70
+ await server.connect(serverTransport);
71
+
72
+ const session: Session = {
73
+ server,
74
+ serverTransport,
75
+ clientTransport,
76
+ pendingResponses: new Map(),
77
+ sseClients: new Set(),
78
+ };
79
+
80
+ // Handle messages from server (responses and notifications)
81
+ clientTransport.onmessage = (message: JSONRPCMessage) => {
82
+ // Check if this is a response to a pending request
83
+ if ("id" in message && message.id !== undefined) {
84
+ const resolver = session.pendingResponses.get(message.id);
85
+ if (resolver) {
86
+ resolver(message);
87
+ session.pendingResponses.delete(message.id);
88
+ return;
89
+ }
90
+ }
91
+
92
+ // Otherwise, broadcast to SSE clients (notifications)
93
+ for (const sendToClient of session.sseClients) {
94
+ sendToClient(message);
95
+ }
96
+ };
97
+
98
+ return session;
99
+ }
100
+
101
+ /**
102
+ * Handle POST requests - session initialization and message handling
103
+ */
104
+ app.post("/mcp", async (c) => {
105
+ const sessionId = c.req.header("mcp-session-id");
106
+ const body = await c.req.json();
107
+
108
+ let session: Session | undefined;
109
+ let newSessionId: string | undefined;
110
+
111
+ if (sessionId && sessions.has(sessionId)) {
112
+ // Reuse existing session
113
+ session = sessions.get(sessionId)!;
114
+ } else if (isInitializeRequest(body)) {
115
+ // New session initialization
116
+ newSessionId = randomUUID();
117
+ session = await createSession();
118
+ sessions.set(newSessionId, session);
119
+ console.error(`[vector-memory-mcp] MCP session initialized: ${newSessionId}`);
120
+ } else {
121
+ // Invalid request - no session ID and not an initialize request
122
+ return c.json(
123
+ {
124
+ jsonrpc: "2.0",
125
+ error: {
126
+ code: -32000,
127
+ message: "Invalid session. Send initialize request without session ID to start.",
128
+ },
129
+ id: body.id ?? null,
130
+ },
131
+ 400
132
+ );
133
+ }
134
+
135
+ // Send message to server and wait for response
136
+ const response = await sendAndWaitForResponse(session, body);
137
+
138
+ // Include session ID header for new sessions
139
+ if (newSessionId) {
140
+ c.header("mcp-session-id", newSessionId);
141
+ }
142
+
143
+ return c.json(response);
144
+ });
145
+
146
+ /**
147
+ * Handle GET requests - SSE stream for server-to-client notifications
148
+ */
149
+ app.get("/mcp", async (c) => {
150
+ const sessionId = c.req.header("mcp-session-id");
151
+
152
+ if (!sessionId || !sessions.has(sessionId)) {
153
+ return c.json(
154
+ {
155
+ jsonrpc: "2.0",
156
+ error: { code: -32000, message: "Invalid or missing session ID" },
157
+ id: null,
158
+ },
159
+ 400
160
+ );
161
+ }
162
+
163
+ const session = sessions.get(sessionId)!;
164
+
165
+ return streamSSE(c, async (stream) => {
166
+ // Register this SSE client
167
+ const sendMessage = (message: JSONRPCMessage) => {
168
+ stream.writeSSE({
169
+ data: JSON.stringify(message),
170
+ event: "message",
171
+ });
172
+ };
173
+
174
+ session.sseClients.add(sendMessage);
175
+
176
+ // Keep connection open
177
+ try {
178
+ // Send a ping every 30 seconds to keep connection alive
179
+ while (true) {
180
+ await stream.sleep(30000);
181
+ await stream.writeSSE({ event: "ping", data: "" });
182
+ }
183
+ } finally {
184
+ session.sseClients.delete(sendMessage);
185
+ }
186
+ });
187
+ });
188
+
189
+ /**
190
+ * Handle DELETE requests - session termination
191
+ */
192
+ app.delete("/mcp", async (c) => {
193
+ const sessionId = c.req.header("mcp-session-id");
194
+
195
+ if (!sessionId || !sessions.has(sessionId)) {
196
+ return c.json(
197
+ {
198
+ jsonrpc: "2.0",
199
+ error: { code: -32000, message: "Invalid or missing session ID" },
200
+ id: null,
201
+ },
202
+ 400
203
+ );
204
+ }
205
+
206
+ const session = sessions.get(sessionId)!;
207
+
208
+ // Close transports
209
+ await session.clientTransport.close();
210
+ await session.serverTransport.close();
211
+ await session.server.close();
212
+
213
+ sessions.delete(sessionId);
214
+ console.error(`[vector-memory-mcp] MCP session closed: ${sessionId}`);
215
+
216
+ return c.json({ success: true });
217
+ });
218
+
219
+ return app;
220
+ }
221
+
222
+ /**
223
+ * Send a message to the server and wait for its response.
224
+ */
225
+ async function sendAndWaitForResponse(
226
+ session: Session,
227
+ message: JSONRPCRequest | JSONRPCNotification
228
+ ): Promise<JSONRPCMessage> {
229
+ return new Promise((resolve) => {
230
+ // Register response handler for requests (messages with id)
231
+ if ("id" in message && message.id !== undefined) {
232
+ session.pendingResponses.set(message.id, resolve);
233
+ }
234
+
235
+ // Send message to server
236
+ session.clientTransport.send(message);
237
+
238
+ // For notifications (no id), resolve immediately with empty response
239
+ if (!("id" in message) || message.id === undefined) {
240
+ resolve({ jsonrpc: "2.0" } as JSONRPCMessage);
241
+ }
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Check if a message is an initialize request.
247
+ */
248
+ function isInitializeRequest(body: unknown): boolean {
249
+ return (
250
+ typeof body === "object" &&
251
+ body !== null &&
252
+ "method" in body &&
253
+ (body as { method: string }).method === "initialize"
254
+ );
255
+ }
@@ -3,58 +3,43 @@ import { cors } from "hono/cors";
3
3
  import type { MemoryService } from "../services/memory.service.js";
4
4
  import type { Config } from "../config/index.js";
5
5
  import { isDeleted } from "../types/memory.js";
6
+ import { createMcpRoutes } from "./mcp-transport.js";
7
+ import type { Memory } from "../types/memory.js";
6
8
 
7
9
  export interface HttpServerOptions {
8
10
  memoryService: MemoryService;
9
11
  config: Config;
10
12
  }
11
13
 
12
- export function createHttpApp(memoryService: MemoryService): Hono {
14
+ // Track server start time for uptime calculation
15
+ const startedAt = Date.now();
16
+
17
+ export function createHttpApp(memoryService: MemoryService, config: Config): Hono {
13
18
  const app = new Hono();
14
19
 
15
20
  // Enable CORS for local development
16
21
  app.use("/*", cors());
17
22
 
18
- // Health check endpoint
19
- app.get("/health", (c) => {
20
- return c.json({ status: "ok", timestamp: new Date().toISOString() });
21
- });
22
-
23
- // Context endpoint for Claude Code hooks
24
- // Returns relevant memories formatted for injection into conversation
25
- app.post("/context", async (c) => {
26
- try {
27
- const body = await c.req.json();
28
- const query = body.query;
23
+ // Mount MCP routes for StreamableHTTP transport
24
+ const mcpApp = createMcpRoutes(memoryService);
25
+ app.route("/", mcpApp);
29
26
 
30
- if (!query || typeof query !== "string") {
31
- return c.json({ error: "Missing or invalid 'query' field" }, 400);
32
- }
33
-
34
- const memories = await memoryService.search(query, 5);
35
-
36
- if (memories.length === 0) {
37
- return c.json({ context: null });
38
- }
39
-
40
- // Format memories for context injection
41
- const contextLines = memories.map((m, i) => {
42
- const metadata = m.metadata as Record<string, unknown>;
43
- const type = metadata.type ? `[${metadata.type}]` : "";
44
- const date = m.createdAt.toISOString().split("T")[0];
45
- return `${i + 1}. ${type} (${date}): ${m.content}`;
46
- });
47
-
48
- const context = `<relevant-memories>\n${contextLines.join("\n")}\n</relevant-memories>`;
49
-
50
- return c.json({ context, count: memories.length });
51
- } catch (error) {
52
- const message = error instanceof Error ? error.message : "Unknown error";
53
- return c.json({ error: message }, 500);
54
- }
27
+ // Health check endpoint with config info
28
+ app.get("/health", (c) => {
29
+ return c.json({
30
+ status: "ok",
31
+ timestamp: new Date().toISOString(),
32
+ pid: process.pid,
33
+ uptime: Math.floor((Date.now() - startedAt) / 1000),
34
+ config: {
35
+ dbPath: config.dbPath,
36
+ embeddingModel: config.embeddingModel,
37
+ embeddingDimension: config.embeddingDimension,
38
+ },
39
+ });
55
40
  });
56
41
 
57
- // Search endpoint (more detailed than /context)
42
+ // Search endpoint
58
43
  app.post("/search", async (c) => {
59
44
  try {
60
45
  const body = await c.req.json();
@@ -125,6 +110,38 @@ export function createHttpApp(memoryService: MemoryService): Hono {
125
110
  }
126
111
  });
127
112
 
113
+ // Get latest handoff
114
+ app.get("/handoff", async (c) => {
115
+ try {
116
+ const handoff = await memoryService.getLatestHandoff();
117
+
118
+ if (!handoff) {
119
+ return c.json({ error: "No handoff found" }, 404);
120
+ }
121
+
122
+ // Fetch referenced memories if any
123
+ const memoryIds = (handoff.metadata.memory_ids as string[] | undefined) ?? [];
124
+ const referencedMemories: Array<{ id: string; content: string }> = [];
125
+
126
+ for (const id of memoryIds) {
127
+ const memory = await memoryService.get(id);
128
+ if (memory && !isDeleted(memory)) {
129
+ referencedMemories.push({ id: memory.id, content: memory.content });
130
+ }
131
+ }
132
+
133
+ return c.json({
134
+ content: handoff.content,
135
+ metadata: handoff.metadata,
136
+ referencedMemories,
137
+ updatedAt: handoff.updatedAt.toISOString(),
138
+ });
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : "Unknown error";
141
+ return c.json({ error: message }, 500);
142
+ }
143
+ });
144
+
128
145
  // Get single memory
129
146
  app.get("/memories/:id", async (c) => {
130
147
  try {
@@ -155,7 +172,7 @@ export async function startHttpServer(
155
172
  memoryService: MemoryService,
156
173
  config: Config
157
174
  ): Promise<{ stop: () => void }> {
158
- const app = createHttpApp(memoryService);
175
+ const app = createHttpApp(memoryService, config);
159
176
 
160
177
  const server = Bun.serve({
161
178
  port: config.httpPort,
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { config } from "./config/index.js";
3
+ import { loadConfig, parseCliArgs } from "./config/index.js";
4
4
  import { connectToDatabase } from "./db/connection.js";
5
5
  import { MemoryRepository } from "./db/memory.repository.js";
6
6
  import { EmbeddingsService } from "./services/embeddings.service.js";
@@ -9,14 +9,19 @@ import { startServer } from "./mcp/server.js";
9
9
  import { startHttpServer } from "./http/server.js";
10
10
 
11
11
  async function main(): Promise<void> {
12
- // Check for warmup command
13
12
  const args = process.argv.slice(2);
13
+
14
+ // Check for warmup command
14
15
  if (args[0] === "warmup") {
15
16
  const { warmup } = await import("../scripts/warmup.js");
16
17
  await warmup();
17
18
  return;
18
19
  }
19
20
 
21
+ // Parse CLI args and load config
22
+ const overrides = parseCliArgs(args);
23
+ const config = loadConfig(overrides);
24
+
20
25
  // Initialize database
21
26
  const db = await connectToDatabase(config.dbPath);
22
27
 
@@ -25,13 +30,41 @@ async function main(): Promise<void> {
25
30
  const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
26
31
  const memoryService = new MemoryService(repository, embeddings);
27
32
 
28
- // Start HTTP server if enabled
33
+ // Track cleanup functions
34
+ let httpStop: (() => void) | null = null;
35
+
36
+ // Graceful shutdown handler
37
+ const shutdown = () => {
38
+ console.error("[vector-memory-mcp] Shutting down...");
39
+ if (httpStop) httpStop();
40
+ db.close();
41
+ process.exit(0);
42
+ };
43
+
44
+ // Handle signals and stdin close (parent process exit)
45
+ process.on("SIGTERM", shutdown);
46
+ process.on("SIGINT", shutdown);
47
+ process.stdin.on("close", shutdown);
48
+ process.stdin.on("end", shutdown);
49
+
50
+ // Start HTTP server if transport mode includes it
29
51
  if (config.enableHttp) {
30
- await startHttpServer(memoryService, config);
52
+ const http = await startHttpServer(memoryService, config);
53
+ httpStop = http.stop;
54
+ console.error(
55
+ `[vector-memory-mcp] MCP available at http://${config.httpHost}:${config.httpPort}/mcp`
56
+ );
31
57
  }
32
58
 
33
- // Start MCP server (stdio)
34
- await startServer(memoryService);
59
+ // Start stdio transport unless in HTTP-only mode
60
+ if (config.transportMode !== "http") {
61
+ await startServer(memoryService);
62
+ } else {
63
+ // In HTTP-only mode, keep the process running
64
+ console.error("[vector-memory-mcp] Running in HTTP-only mode (no stdio)");
65
+ // Keep process alive - the HTTP server runs indefinitely
66
+ await new Promise(() => {});
67
+ }
35
68
  }
36
69
 
37
70
  main().catch(console.error);