#
tokens: 4925/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# 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";
}

```