MCP-Turso

by nbbaier
Verified
  • src
import type { Client } from "@libsql/client"; import { parseArgs } from "node:util"; import { z } from "zod"; import { DEFAULT_LOG_FILE } from "./logger.js"; import { type Config, type TableColumn, type TextContent, envSchema, } from "./types.js"; /** * Retrieves a list of all tables in the Turso database. * * @param client - The Turso database client instance * @returns A promise that resolves to an array of table names */ export async function listTables(client: Client): Promise<string[]> { const result = await client.execute({ sql: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", args: [], }); return result.rows.map((row) => row.name as string); } /** * Retrieves the SQL schema definitions for all tables in the database. * * @param client - The Turso database client instance * @returns A promise that resolves to an array of SQL schema statements */ export async function dbSchema(client: Client): Promise<string[]> { const result = await client.execute({ sql: "SELECT sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", args: [], }); return result.rows.map((row) => row.sql as string); } /** * Retrieves detailed schema information for a specific table. * * @param tableName - The name of the table to describe * @param client - The Turso database client instance * @returns A promise that resolves to an array of column definitions * @throws Error if the table name is invalid or the table doesn't exist */ export async function describeTable( tableName: string, client: Client, ): Promise<TableColumn[]> { if (!/^[a-zA-Z0-9_]+$/.test(tableName)) { throw new Error( "Invalid table name. Only alphanumeric characters and underscores are allowed.", ); } const result = await client.execute({ sql: `PRAGMA table_info(${tableName})`, args: [], }); if (result.rows.length === 0) { throw new Error(`Table '${tableName}' not found`); } return result.rows.map((row) => ({ name: row.name as string, type: row.type as string, notnull: row.notnull as number, dflt_value: row.dflt_value as string | null, pk: row.pk as number, })); } /** * Executes a SELECT SQL query against the database. * * @param sql - The SQL query to execute (must be a SELECT query) * @param client - The Turso database client instance * @returns A promise that resolves to an object containing columns, rows, and row count * @throws Error if the query is not a SELECT query */ export async function query<T = Record<string, unknown>>( sql: string, client: Client, ): Promise<{ columns: string[]; rows: T[]; rowCount: number; }> { const trimmedQuery = sql.trim().toUpperCase(); if (!trimmedQuery.startsWith("SELECT")) { throw new Error("Only SELECT queries are allowed for safety reasons"); } const result = await client.execute({ sql, args: [], }); return { columns: result.columns, rows: result.rows as T[], rowCount: result.rows.length, }; } /** * Loads and validates environment configuration for the Turso database. * * @returns A validated configuration object * @throws Error if the configuration is invalid */ export function loadConfig(): Config { const config = envSchema.safeParse(process.env); if (!config.success) { throw new Error(`Configuration error: ${config.error.message}`); } return config.data; } /** * Creates a formatted content response object for MCP tools. * * @param text - The text content to include in the response * @param error - Whether this content represents an error (default: false) * @returns A formatted content result object */ export function content( text: string, error = false, ): { content: TextContent[]; isError: boolean } { return { content: [{ type: "text", text }], isError: error, }; } /** * Determines the log file path based on command line arguments or defaults. * * @returns The path to the log file */ export function getLogFile(): string { const { values } = parseArgs({ args: process.argv, options: { logs: { type: "string", }, }, strict: true, allowPositionals: true, }); const parsedLogs = z .string() .refine((targetPath) => { const posixPath = targetPath.split("\\").join("/"); return targetPath === posixPath && posixPath.includes("/"); }) .safeParse(values.logs); return values.logs && parsedLogs.success ? parsedLogs.data : DEFAULT_LOG_FILE; }