# Directory Structure
```
├── .gitignore
├── biome.json
├── bun.lock
├── LICENSE
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── logger.ts
│ ├── types.ts
│ └── utils.ts
├── tsconfig.json
└── tsdown.config.ts
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
*.log
npm-debug.log*
pnpm-debug.log*
# Testing
coverage/
# Database files
*.db
*.db-journal
# OS
.DS_Store
Thumbs.db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-turso
A Model Context Protocol (MCP) server that provides access to the Turso-hosted LibSQL databases. Currently, the server provides the following functionality:
- Retrieving a list of tables in a database
- Retrieving a database's schema
- Retrieving the schema of a table
- Performing SELECT queries
## Configuration
### With Claude Desktop
Add this to your `claude_desktop_config.json`:
```json
{
"mcpServers": [
"turso": {
"command": "npx",
"args": ["-y", "mcp-turso"],
"env": {
"TURSO_DATABASE_URL": "your_url",
"TURSO_AUTH_TOKEN": "your_token"
}
}
]
}
```
You will need an existing database to continue. If you don't have one, [create one](https://docs.turso.tech/quickstart). To get the database URL via the Turso CLI, run:
```bash
turso db show --url <database-name>
```
Then get the database authentication token:
```bash
turso db tokens create <database-name>
```
Add those values to your configuration as shown above.
### With Cursor
To configure the Turso MCP server with Cursor, add the following to your Cursor settings:
1. Open Cursor and go to Settings (⚙️) > Settings (JSON)
2. Add the following configuration to your settings JSON:
```json
"mcpServers": {
"turso": {
"command": "npx",
"args": ["-y", "mcp-turso"],
"env": {
"TURSO_DATABASE_URL": "your_url",
"TURSO_AUTH_TOKEN": "your_token"
}
}
}
```
Replace `your_url` and `your_token` with your Turso database URL and authentication token as described in the previous section.
### Logging
The server includes a custom logger for debugging outside of Claude Desktop. By default, this logger writes to `<parent-dir>/logs/mcp-turso.log`, where `<parent-dir>` is the parent directory of directory containing the `mcp-turso` script. In other words, if the path to `mcp-turso` is `~/foo/bin/mcp-turso`, the logs will be at `~/foo/logs/mcp-turso.log`. If running with NPX as above, the default logs will be:
```
~/.npm/_npx/<npx-dir-name>/node_modules/mcp-turso/logs/mcp-turso.log
```
If you would like to specify a custom path, you can include a `--logs` flag with an **absolute posix path** in the server's configuration:
```json
{
"mcpServers": [
"turso": {
"command": "npx",
"args": ["-y", "mcp-turso", "--logs", "/Users/<username>/path/to/dir/mcp-logs.log"],
"env": {
"TURSO_DATABASE_URL": "your_url",
"TURSO_AUTH_TOKEN": "your_token"
}
}
]
}
```
The path to the log file (default or custom) is always logged to `stderr` when the server is created. For Claude desktop, this will show up in your server logs in `~/Library/Logs/Claude`.
_Note_: Right now, I haven't implemented specifying a custom logging file for Windows, but this is coming.
## Server Capabilities
The server provides the following tools:
- `list_tables`
- Get a list of all the tables in the database
- No input
- Returns: an array of table names
- `get_db_schema`
- Get the schemas of all tables in the database
- No input
- Returns: an array of SQL creation statements
- `describe_table`
- View schema information for a specific table
- Input:
- `table_name` (string): Name of table to describe
- Returns: Array of column definitions with names and types
- `query_database`
- Execute a SELECT query to read data from the database
- Input:
- `sql` (string): The SELECT SQL query to execute
- Returns: Query results as an object of type `{ columns: string[]; rows: Record<string, unknown>[]; rowCount: number; }`
## Todo
- [ ] Add the ability to specify a custom log file on windows
- [ ] Add more query tools
## License
MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/tsdown.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["./src/index.ts"],
target: "es2022",
clean: true,
dts: true,
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "@total-typescript/tsconfig/tsc/no-dom/library",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": false,
"declaration": false,
"moduleResolution": "bundler",
"module": "ESNext",
"resolveJsonModule": true
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}
```
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const TableColumnSchema = z.object({
name: z.string(),
type: z.string(),
notnull: z.number(),
dflt_value: z.union([z.string(), z.null()]),
pk: z.number(),
});
export type TableColumn = z.infer<typeof TableColumnSchema>;
export const envSchema = z.object({
TURSO_DATABASE_URL: z.string().min(1, "Database URL is required"),
TURSO_AUTH_TOKEN: z.string().min(1, "Auth token is required"),
});
export type Config = z.infer<typeof envSchema>;
export type TextContent = {
type: "text";
text: string;
};
export type ImageContent = {
type: "image";
data: string;
mimeType: string;
};
export type Content = TextContent | ImageContent;
export type ContentResult = {
content: Content[];
isError?: boolean;
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-turso",
"version": "0.2.1",
"description": "MCP server for interacting with Turso-hosted LibSQL databases",
"license": "MIT",
"author": "Nico Baier",
"type": "module",
"bin": {
"mcp-turso": "dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "git+https://github.com/nbbaier/mcp-turso.git"
},
"homepage": "https://github.com/nbbaier/mcp-turso#readme",
"bugs": "https://github.com/nbbaier/mcp-turso/issues",
"scripts": {
"build": "tsdown && chmod +x dist/index.js"
},
"keywords": [
"mcp",
"libsql",
"sqlite",
"turso",
"database",
"model-context-protocol",
"mcp-server",
"ai",
"sql"
],
"dependencies": {
"@libsql/client": "^0.15.15",
"@modelcontextprotocol/sdk": "^1.17.5",
"fastmcp": "^3.16.0",
"zod": "^4.1.5"
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.2.21",
"tsdown": "^0.15.0",
"typescript": "^5.9.2"
}
}
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { name } from "../package.json" with { type: "json" };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const parentDir = join(__dirname, "..");
const logsDir = join(parentDir, "logs");
export const DEFAULT_LOG_FILE = join(logsDir, `${name}.log`);
/**
* Formats a log message with timestamp, level, and optional data.
*
* @param level - The log level (INFO, ERROR, DEBUG, etc)
* @param message - The log message text
* @param data - Optional data to include in the log entry
* @returns A formatted log string
*/
function formatLogMessage(
level: string,
message: string,
data?: unknown,
): string {
const timestamp = new Date().toISOString();
const dataStr = data ? `\n${JSON.stringify(data, null, 2)}` : "";
return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
}
/**
* Creates a logger instance that writes to the specified log file.
*
* @param logFile - Path to the log file (defaults to DEFAULT_LOG_FILE)
* @returns A logger object with info, error, and debug methods
*/
/**
* Creates a logger instance that writes log messages to a file.
*
* @param logFile - The path to the log file. Defaults to DEFAULT_LOG_FILE.
* @returns A `logger` object with methods for logging at different levels.
* @returns {Object} `logger` - The logger object.
* @returns {string} `logger.logFile` - The path to the log file.
* @returns {function} `logger.log` - Logs a message with a custom level.
* @returns {function} `logger.info` - Logs an info level message.
* @returns {function} `logger.error` - Logs an error level message.
* @returns {function} `logger.debug` - Logs a debug level message.
*/
export function createLogger(logFile: string = DEFAULT_LOG_FILE) {
const logDir = dirname(logFile);
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
if (!existsSync(logFile)) appendFileSync(logFile, "");
return {
logFile,
log(level: string, message: string, data?: unknown): void {
const logMessage = formatLogMessage(level, message, data);
appendFileSync(logFile, logMessage);
},
info(message: string, data?: unknown): void {
const logMessage = formatLogMessage("INFO", message, data);
appendFileSync(logFile, logMessage);
},
error(message: string, error?: unknown): void {
const logMessage = formatLogMessage("ERROR", message, error);
appendFileSync(logFile, logMessage);
},
debug(message: string, data?: unknown): void {
const logMessage = formatLogMessage("DEBUG", message, data);
appendFileSync(logFile, logMessage);
},
};
}
export default createLogger;
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { FastMCP } from "fastmcp";
import { z } from "zod";
import createLogger from "./logger.js";
import {
getLogFile,
listTables,
content,
dbSchema,
describeTable,
query,
getVersion,
} from "./utils.js";
import { createClient, type Client } from "@libsql/client";
import { version } from "../package.json" with { type: "json" };
const server = new FastMCP({
name: "Turso MCP Server",
version: getVersion(version),
});
const dbUrl = process.env.TURSO_DATABASE_URL;
const authToken = process.env.TURSO_AUTH_TOKEN;
const logFile = getLogFile();
const logger = createLogger(logFile);
if (!dbUrl) {
logger.error("TURSO_DATABASE_URL environment variable is required");
process.exit(1);
}
if (!authToken) {
logger.error("TURSO_AUTH_TOKEN environment variable is required");
process.exit(1);
}
let db: Client;
try {
db = createClient({ url: dbUrl, authToken });
logger.info("Successfully connected to Turso database");
} catch (error) {
logger.error("Failed to connect to Turso database", error);
process.exit(1);
}
server.addTool({
name: "list_tables",
description: "List all tables in the database",
parameters: z.object({}),
execute: async () => {
try {
logger.info("Executing list_tables");
const tables = await listTables(db);
return content(JSON.stringify({ tables }, null, 2));
} catch (error) {
logger.error("Failed to list tables", error);
return content(
`Error listing tables: ${error instanceof Error ? error.message : String(error)}`,
true,
);
}
},
});
server.addTool({
name: "get_db_schema",
description: "Get the schema for all tables in the database",
parameters: z.object({}),
execute: async () => {
try {
const schema = await dbSchema(db);
return content(JSON.stringify({ schema }, null, 2));
} catch (error) {
return content(
`Error getting schema: ${error instanceof Error ? error.message : String(error)}`,
true,
);
}
},
});
server.addTool({
name: "describe_table",
description: "View schema information for a specific table",
parameters: z.object({
table_name: z
.string()
.describe("Name of the table to describe")
.min(1, "Table name is required"),
}),
execute: async ({ table_name }) => {
try {
logger.info(`Executing describe_table for table: ${table_name}`);
const schema = await describeTable(table_name, db);
return content(JSON.stringify({ schema }, null, 2));
} catch (error) {
logger.error(`Failed to describe table ${table_name}`, error);
return content(
`Error describing table: ${error instanceof Error ? error.message : String(error)}`,
true,
);
}
},
});
server.addTool({
name: "query_database",
description: "Execute a SELECT query to read data from the database",
parameters: z.object({
sql: z
.string()
.describe("SQL query to execute")
.min(1, "SQL query is required"),
}),
execute: async ({ sql }) => {
try {
logger.info(`Executing query: ${sql}`);
const result = await query(sql, db);
return content(JSON.stringify(result, null, 2));
} catch (error) {
logger.error("Failed to execute query", error);
return content(
`Error executing query: ${error instanceof Error ? error.message : String(error)}`,
true,
);
}
},
});
process.on("uncaughtException", (error) => {
logger.error("Uncaught exception", error);
});
process.on("unhandledRejection", (reason) => {
logger.error("Unhandled rejection", reason);
});
console.error(`[INFO] Additional logs available at: ${logger.logFile}`);
server.start({
transportType: "stdio",
});
process.on("exit", (code) => {
logger.info("Turso MCP server closed", code);
});
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
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;
}
/**
* Retrieves the version string from a package.json file.
*
* @param pkg - The package.json file content
* @returns The version string
*/
export function getVersion(version: string): `${number}.${number}.${number}` {
return version.match(/^\d+\.\d+\.\d+$/)
? (version as `${number}.${number}.${number}`)
: "0.0.0";
}
```