From d6387cd545107a04a29eb4612a5827d898e10ff3 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Sat, 27 Jun 2026 12:03:37 -0400 Subject: [PATCH] feat: fix possible crash --- src/mcp-server.ts | 82 ++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 9669e6a..df86f7a 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -19,15 +19,17 @@ try { // --- FTS5 syntax validation --- function validateFts5Query(query: string): string | null { - // Try a harmless MATCH against the FTS table to validate syntax + // Try a harmless MATCH against the FTS table to validate syntax. + // Use LIMIT 1 (not LIMIT 0) so SQLite fully evaluates the MATCH + // expression instead of short-circuiting. try { db.query( - "SELECT 1 FROM threads_fts WHERE threads_fts MATCH $q LIMIT 0", + "SELECT 1 FROM threads_fts WHERE threads_fts MATCH $q LIMIT 1", ).get({ $q: query }); return null; // valid } catch (e) { const msg = e instanceof Error ? e.message : String(e); - return `Invalid FTS5 query: ${msg}\n\nSyntax guide:\n word - matches any thread containing "word"\n "exact phrase" - matches the exact phrase\n search* - prefix match\n apple AND banana - both terms required\n apple OR banana - either term\n apple NOT banana - exclude term\n\nAvoid unbalanced quotes, trailing operators, or empty parentheses.`; + return `Invalid FTS5 query: ${msg}\n\nSyntax guide:\n word - matches any thread containing "word"\n "exact phrase" - matches the exact phrase\n search* - prefix match\n apple AND banana - both terms required\n apple OR banana - either term\n apple NOT banana - exclude term\n\nAvoid unbalanced quotes, trailing operators, or empty parentheses.\nNote: version numbers like "4.0" can confuse FTS5. Try quoting them ("4.0") or dropping the version.`; } } @@ -137,12 +139,13 @@ function err(message: string) { return { content: [{ type: "text" as const, text: message }], isError: true }; } -function safeQuery(fn: () => T, context: string): T | null { +function safeQuery(fn: () => T, context: string): { ok: true; data: T } | { ok: false; error: string } { try { - return fn(); + return { ok: true, data: fn() }; } catch (e) { - console.error(`Error in ${context}: ${e}`); - return null; + const msg = e instanceof Error ? e.message : String(e); + console.error(`Error in ${context}: ${msg}`); + return { ok: false, error: msg }; } } @@ -197,7 +200,7 @@ Use get_thread to read the full conversation of a result.`, const syntaxError = validateFts5Query(query); if (syntaxError) return err(syntaxError); - const results = safeQuery( + const result = safeQuery( () => stmtSearchThreads.all({ $query: query, @@ -209,8 +212,8 @@ Use get_thread to read the full conversation of a result.`, }), "search", ); - if (!results) return err("Search failed unexpectedly."); - return ok(results); + if (!result.ok) return err(`Search failed: ${result.error}`); + return ok(result.data); }, ); @@ -244,7 +247,7 @@ server.registerTool( const syntaxError = validateFts5Query(query); if (syntaxError) return err(syntaxError); - const results = safeQuery( + const result = safeQuery( () => stmtChannelSearch.all({ $query: query, @@ -256,9 +259,9 @@ server.registerTool( }), "channel_search", ); - if (!results) - return err("Search failed. Check channel name and query syntax."); - return ok(results); + if (!result.ok) + return err(`Search failed: ${result.error}. Check channel name and query syntax.`); + return ok(result.data); }, ); @@ -275,25 +278,27 @@ server.registerTool( const result = safeQuery( () => stmtGetThread.get({ $id: id }), "get_thread", - ) as Record | null; - if (!result) return err(`Thread ${id} not found`); + ); + if (!result.ok) return err(`Thread lookup failed: ${result.error}`); + const row = result.data as Record | null; + if (!row) return err(`Thread ${id} not found`); // Parse messages_json into structured array let messages: unknown[] = []; try { - messages = JSON.parse(result.messages_json as string); + messages = JSON.parse(row.messages_json as string); } catch { messages = []; } return ok({ - id: result.id, - channel_name: result.channel_name, - category_name: result.category_name, - author_names: result.author_names, - message_count: result.message_count, - first_timestamp: result.first_timestamp, - last_timestamp: result.last_timestamp, + id: row.id, + channel_name: row.channel_name, + category_name: row.category_name, + author_names: row.author_names, + message_count: row.message_count, + first_timestamp: row.first_timestamp, + last_timestamp: row.last_timestamp, messages, }); }, @@ -307,16 +312,18 @@ server.registerTool( inputSchema: {}, }, async () => { - const rows = safeQuery( + const result = safeQuery( () => stmtListChannels.all(), "list_channels", - ) as Array<{ + ); + if (!result.ok) return err(`Failed to list channels: ${result.error}`); + + const rows = result.data as Array<{ name: string; category: string; message_count: number; topic: string | null; - }> | null; - if (!rows) return err("Failed to list channels"); + }>; // Group by category const grouped: Record< @@ -358,7 +365,7 @@ server.registerTool( }, }, async ({ channel, limit, before }) => { - const results = safeQuery( + const result = safeQuery( () => stmtGetMessages.all({ $channel: channel, @@ -367,12 +374,13 @@ server.registerTool( }), "get_messages", ); - if (!results) return err("Failed to get messages. Check channel name."); - if ((results as unknown[]).length === 0) + if (!result.ok) return err(`Failed to get messages: ${result.error}. Check channel name.`); + const rows = result.data as unknown[]; + if (rows.length === 0) return err( `No messages found in channel "${channel}". Check spelling with list_channels.`, ); - return ok(results); + return ok(rows); }, ); @@ -385,8 +393,8 @@ server.registerTool( }, async () => { const result = safeQuery(() => stmtStats.get(), "stats"); - if (!result) return err("Failed to get stats"); - return ok(result); + if (!result.ok) return err(`Failed to get stats: ${result.error}`); + return ok(result.data); }, ); @@ -404,9 +412,9 @@ server.registerTool( if (!trimmed.startsWith("SELECT") && !trimmed.startsWith("WITH")) { return err("Only SELECT and WITH queries are allowed"); } - const results = safeQuery(() => db.query(sql).all(), "raw_query"); - if (!results) return err("Query failed. Check syntax."); - return ok(results); + const result = safeQuery(() => db.query(sql).all(), "raw_query"); + if (!result.ok) return err(`Query failed: ${result.error}. Check syntax.`); + return ok(result.data); }, ); -- 2.51.2