We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/smcdonnell7/meeting-chief-lite'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* Meeting Chief Lite - Search Operations
* Semantic and keyword search across meeting transcripts
*/
import { getDb, getChunksWithVectors, getMeeting } from './database.js';
import { getEmbedding, cosineSimilarity } from './embeddings.js';
import type { SearchResult, SearchResponse, TranscriptChunk } from './types.js';
export interface SearchOptions {
limit?: number;
after?: string; // ISO date string
before?: string; // ISO date string
speaker?: string;
}
/**
* Semantic search using embeddings
*/
export async function semanticSearch(
query: string,
options: SearchOptions = {}
): Promise<SearchResponse> {
const { limit = 10, after, before, speaker } = options;
// Get query embedding
let queryVector: Float32Array;
try {
queryVector = await getEmbedding(query);
} catch (error) {
// Fall back to keyword search if embedding fails
console.error('Embedding failed, falling back to keyword search:', error);
return keywordSearch(query, options);
}
// Get all chunks with vectors
const chunksWithVectors = getChunksWithVectors();
if (chunksWithVectors.length === 0) {
// No vectors yet, fall back to keyword search
return keywordSearch(query, options);
}
// Calculate similarities
const results: (SearchResult & { meeting: any })[] = [];
const db = getDb();
for (const { chunk, vector } of chunksWithVectors) {
// Apply filters
const meeting = db.prepare('SELECT * FROM meetings WHERE id = ?').get(chunk.meeting_id) as any;
if (!meeting) continue;
if (after && meeting.start_time && meeting.start_time < after) continue;
if (before && meeting.start_time && meeting.start_time > before) continue;
if (speaker && chunk.speaker?.toLowerCase() !== speaker.toLowerCase()) continue;
const similarity = cosineSimilarity(queryVector, vector);
results.push({
meeting_id: chunk.meeting_id,
meeting_title: meeting.title,
date: meeting.start_time?.slice(0, 10) || null,
chunk: chunk.content,
speaker: chunk.speaker,
relevance: Math.round(similarity * 1000) / 1000,
meeting,
});
}
// Sort by relevance and limit
results.sort((a, b) => b.relevance - a.relevance);
const topResults = results.slice(0, limit);
return {
query,
results: topResults.map(({ meeting, ...r }) => r),
total: results.length,
};
}
/**
* Keyword search fallback (no embeddings required)
*/
export function keywordSearch(
query: string,
options: SearchOptions = {}
): SearchResponse {
const { limit = 10, after, before, speaker } = options;
const db = getDb();
// Build query
let sql = `
SELECT c.*, m.title as meeting_title, m.start_time
FROM transcript_chunks c
JOIN meetings m ON c.meeting_id = m.id
WHERE c.content LIKE ?
`;
const params: (string | number)[] = [`%${query}%`];
if (after) {
sql += ' AND m.start_time >= ?';
params.push(after);
}
if (before) {
sql += ' AND m.start_time <= ?';
params.push(before);
}
if (speaker) {
sql += ' AND LOWER(c.speaker) = LOWER(?)';
params.push(speaker);
}
sql += ' ORDER BY m.start_time DESC LIMIT ?';
params.push(limit);
const rows = db.prepare(sql).all(...params) as any[];
const results: SearchResult[] = rows.map(row => ({
meeting_id: row.meeting_id,
meeting_title: row.meeting_title,
date: row.start_time?.slice(0, 10) || null,
chunk: row.content,
speaker: row.speaker,
relevance: 1.0, // Keyword match = full relevance
}));
return {
query,
results,
total: results.length,
};
}
/**
* Get all chunks for a meeting (for full transcript retrieval)
*/
export function getMeetingChunks(meetingId: string): TranscriptChunk[] {
const db = getDb();
return db.prepare(`
SELECT * FROM transcript_chunks
WHERE meeting_id = ?
ORDER BY chunk_index
`).all(meetingId) as TranscriptChunk[];
}
/**
* Search for a specific topic and return context
*/
export async function searchWithContext(
query: string,
contextChunks = 2,
options: SearchOptions = {}
): Promise<SearchResponse> {
const searchResults = await semanticSearch(query, { ...options, limit: options.limit || 5 });
// Expand results with surrounding context
const expandedResults: SearchResult[] = [];
const db = getDb();
for (const result of searchResults.results) {
// Get the chunk index
const chunk = db.prepare(`
SELECT chunk_index FROM transcript_chunks
WHERE meeting_id = ? AND content = ?
`).get(result.meeting_id, result.chunk) as { chunk_index: number } | undefined;
if (!chunk) {
expandedResults.push(result);
continue;
}
// Get surrounding chunks
const startIndex = Math.max(0, chunk.chunk_index - contextChunks);
const endIndex = chunk.chunk_index + contextChunks;
const contextRows = db.prepare(`
SELECT content, speaker FROM transcript_chunks
WHERE meeting_id = ? AND chunk_index BETWEEN ? AND ?
ORDER BY chunk_index
`).all(result.meeting_id, startIndex, endIndex) as { content: string; speaker: string }[];
const contextText = contextRows.map(r =>
r.speaker ? `${r.speaker}: ${r.content}` : r.content
).join('\n');
expandedResults.push({
...result,
chunk: contextText,
});
}
return {
query,
results: expandedResults,
total: searchResults.total,
};
}