import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { McpDependencies } from '../server/mcp-server.js';
import { memorySearchInput } from './schemas.js';
import { embed } from '../services/ollama-client.js';
import { SemanticCache } from '../services/semantic-cache.js';
import { auditToolCall } from '../middleware/audit.js';
import { logger } from '../utils/logger.js';
export function registerMemorySearch(server: McpServer, deps: McpDependencies): void {
server.registerTool('memory_search', {
description:
'Search stored memories using semantic similarity. Returns ranked results with content and scores.',
inputSchema: memorySearchInput,
}, async (args, extra) => {
const start = Date.now();
const { query, scope, top_k = 5, min_score = 0.3 } = args;
try {
// Check cache
const cacheKey = SemanticCache.keyFromString(`search:${query}:${scope ?? ''}:${top_k}`);
const cached = deps.cache.get(cacheKey);
if (cached) {
logger.debug({ query }, 'memory.search cache hit');
return { content: [{ type: 'text' as const, text: cached }] };
}
// Embed query
const [queryEmbedding] = await embed(query);
// Vector search
const results = await deps.vectorStore.search(queryEmbedding, {
topK: top_k,
minScore: min_score,
scope,
});
const output = JSON.stringify({
query,
results: results.map((r) => ({
document_id: r.documentId,
content: r.content,
score: Math.round(r.score * 1000) / 1000,
meta: r.meta,
})),
total: results.length,
});
deps.cache.set(cacheKey, output);
auditToolCall(deps.db, 'memory_search', extra.sessionId, query, `${results.length} results`, Date.now() - start);
return { content: [{ type: 'text' as const, text: output }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error({ err, query }, 'memory.search failed');
auditToolCall(deps.db, 'memory_search', extra.sessionId, query, `error: ${msg}`, Date.now() - start);
return { content: [{ type: 'text' as const, text: `Error: ${msg}` }], isError: true };
}
});
}