import { Hono } from "hono";
import { cors } from "hono/cors";
import { serve as nodeServe } from "@hono/node-server";
import { createServer } from "net";
import type { MemoryService } from "../services/memory.service.js";
import type { Config } from "../config/index.js";
import { isDeleted } from "../types/memory.js";
import { createMcpRoutes } from "./mcp-transport.js";
import type { Memory, SearchIntent } from "../types/memory.js";
// Detect runtime
const isBun = typeof globalThis.Bun !== "undefined";
/**
* Check if a port is available by attempting to bind to it
*/
async function isPortAvailable(port: number, host: string): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
server.once("error", () => {
resolve(false);
});
server.once("listening", () => {
server.close(() => resolve(true));
});
server.listen(port, host);
});
}
/**
* Find an available port, starting with the preferred port.
* If preferred port is unavailable, picks a random available port.
*/
async function findAvailablePort(
preferredPort: number,
host: string
): Promise<number> {
if (await isPortAvailable(preferredPort, host)) {
return preferredPort;
}
console.error(
`[vector-memory-mcp] Port ${preferredPort} is in use, finding an available port...`
);
// Let the OS pick a random available port
return new Promise((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.once("listening", () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
server.close(() => resolve(port));
});
server.listen(0, host);
});
}
export interface HttpServerOptions {
memoryService: MemoryService;
config: Config;
}
// Track server start time for uptime calculation
const startedAt = Date.now();
export function createHttpApp(memoryService: MemoryService, config: Config): Hono {
const app = new Hono();
// Enable CORS for local development
app.use("/*", cors());
// Mount MCP routes for StreamableHTTP transport
const mcpApp = createMcpRoutes(memoryService);
app.route("/", mcpApp);
// Health check endpoint with config info
app.get("/health", (c) => {
return c.json({
status: "ok",
timestamp: new Date().toISOString(),
pid: process.pid,
uptime: Math.floor((Date.now() - startedAt) / 1000),
config: {
dbPath: config.dbPath,
embeddingModel: config.embeddingModel,
embeddingDimension: config.embeddingDimension,
},
});
});
// Search endpoint
app.post("/search", async (c) => {
try {
const body = await c.req.json();
const query = body.query;
const intent = (body.intent as SearchIntent) ?? "fact_check";
const limit = body.limit ?? 10;
if (!query || typeof query !== "string") {
return c.json({ error: "Missing or invalid 'query' field" }, 400);
}
const memories = await memoryService.search(query, intent, limit);
return c.json({
memories: memories.map((m) => ({
id: m.id,
content: m.content,
metadata: m.metadata,
createdAt: m.createdAt.toISOString(),
})),
count: memories.length,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return c.json({ error: message }, 500);
}
});
// Store endpoint
app.post("/store", async (c) => {
try {
const body = await c.req.json();
const { content, metadata, embeddingText } = body;
if (!content || typeof content !== "string") {
return c.json({ error: "Missing or invalid 'content' field" }, 400);
}
const memory = await memoryService.store(
content,
metadata ?? {},
embeddingText
);
return c.json({
id: memory.id,
createdAt: memory.createdAt.toISOString(),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return c.json({ error: message }, 500);
}
});
// Delete endpoint
app.delete("/memories/:id", async (c) => {
try {
const id = c.req.param("id");
const deleted = await memoryService.delete(id);
if (!deleted) {
return c.json({ error: "Memory not found" }, 404);
}
return c.json({ deleted: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return c.json({ error: message }, 500);
}
});
// Get latest handoff
app.get("/handoff", async (c) => {
try {
const handoff = await memoryService.getLatestHandoff();
if (!handoff) {
return c.json({ error: "No handoff found" }, 404);
}
// Fetch referenced memories if any
const memoryIds = (handoff.metadata.memory_ids as string[] | undefined) ?? [];
const referencedMemories: Array<{ id: string; content: string }> = [];
for (const id of memoryIds) {
const memory = await memoryService.get(id);
if (memory && !isDeleted(memory)) {
referencedMemories.push({ id: memory.id, content: memory.content });
}
}
return c.json({
content: handoff.content,
metadata: handoff.metadata,
referencedMemories,
updatedAt: handoff.updatedAt.toISOString(),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return c.json({ error: message }, 500);
}
});
// Get single memory
app.get("/memories/:id", async (c) => {
try {
const id = c.req.param("id");
const memory = await memoryService.get(id);
if (!memory || isDeleted(memory)) {
return c.json({ error: "Memory not found" }, 404);
}
return c.json({
id: memory.id,
content: memory.content,
metadata: memory.metadata,
createdAt: memory.createdAt.toISOString(),
updatedAt: memory.updatedAt.toISOString(),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return c.json({ error: message }, 500);
}
});
return app;
}
export async function startHttpServer(
memoryService: MemoryService,
config: Config
): Promise<{ stop: () => void; port: number }> {
const app = createHttpApp(memoryService, config);
// Find an available port (uses configured port if available, otherwise picks a random one)
const actualPort = await findAvailablePort(config.httpPort, config.httpHost);
if (isBun) {
// Use Bun's native server
const server = Bun.serve({
port: actualPort,
hostname: config.httpHost,
fetch: app.fetch,
});
console.error(
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
);
return {
stop: () => server.stop(),
port: actualPort,
};
} else {
// Use Node.js server via @hono/node-server
const server = nodeServe({
fetch: app.fetch,
port: actualPort,
hostname: config.httpHost,
});
console.error(
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
);
return {
stop: () => server.close(),
port: actualPort,
};
}
}