import { createTool } from '@mastra/core/tools'; import { createClient } from '@libsql/client'; import { z } from 'zod'; const db = createClient({ url: 'file:./data.db' }); const MAX_QUERY_LENGTH = 2000; const MAX_ROWS = 100; const BLOCKED_PATTERNS = [ /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|REPLACE|UPSERT)\b/i, /\b(ATTACH|DETACH|PRAGMA|VACUUM|REINDEX|ANALYZE)\b/i, /\b(load_extension|readfile|writefile|randomblob|zeroblob)\s*\(/i, /--|\/\*|\*\//, // block SQL comments /;/, // block additional statements after optional trailing semicolon is removed ]; export const executeSql = createTool({ id: 'execute-sql', description: 'Executes one read-only SQLite SELECT query against the public demo database. Results are limited to 100 rows.', inputSchema: z.object({ query: z .string() .min(1) .max(MAX_QUERY_LENGTH) .describe('One SQLite SELECT query to execute'), }), outputSchema: z.object({ rows: z.array(z.record(z.string(), z.unknown())).describe('Query result rows'), rowCount: z.number().describe('Number of rows returned'), truncated: z.boolean().describe('Whether additional rows were excluded'), maxRows: z.number().describe('Maximum rows returned by the public demo'), }), execute: async ({ query }) => { const trimmed = query.trim().replace(/;\s*$/, '').trim(); if (trimmed.length === 0 || trimmed.length > MAX_QUERY_LENGTH) { throw new Error(`Query must contain between 1 and ${MAX_QUERY_LENGTH} characters.`); } if (!/^SELECT\b/i.test(trimmed)) { throw new Error('Only a single SELECT query is allowed.'); } for (const pattern of BLOCKED_PATTERNS) { if (pattern.test(trimmed)) { throw new Error('This query is not permitted in the public demo.'); } } const limitedQuery = ` SELECT * FROM (${trimmed}) AS public_demo_result LIMIT ${MAX_ROWS + 1} `; const result = await db.execute(limitedQuery); const allRows = result.rows as Record[]; const truncated = allRows.length > MAX_ROWS; const rows = allRows.slice(0, MAX_ROWS); return { rows, rowCount: rows.length, truncated, maxRows: MAX_ROWS, }; }, });