# 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:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 |
4 | # Build output
5 | dist/
6 | build/
7 |
8 | # Environment variables
9 | .env
10 | .env.local
11 | .env.*.local
12 |
13 | # IDE
14 | .vscode/
15 | .idea/
16 | *.swp
17 | *.swo
18 |
19 | # Logs
20 | *.log
21 | npm-debug.log*
22 | pnpm-debug.log*
23 |
24 | # Testing
25 | coverage/
26 |
27 | # Database files
28 | *.db
29 | *.db-journal
30 |
31 | # OS
32 | .DS_Store
33 | Thumbs.db
34 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # mcp-turso
2 |
3 | A Model Context Protocol (MCP) server that provides access to the Turso-hosted LibSQL databases. Currently, the server provides the following functionality:
4 |
5 | - Retrieving a list of tables in a database
6 | - Retrieving a database's schema
7 | - Retrieving the schema of a table
8 | - Performing SELECT queries
9 |
10 | ## Configuration
11 |
12 | ### With Claude Desktop
13 |
14 | Add this to your `claude_desktop_config.json`:
15 |
16 | ```json
17 | {
18 | "mcpServers": [
19 | "turso": {
20 | "command": "npx",
21 | "args": ["-y", "mcp-turso"],
22 | "env": {
23 | "TURSO_DATABASE_URL": "your_url",
24 | "TURSO_AUTH_TOKEN": "your_token"
25 | }
26 | }
27 | ]
28 | }
29 | ```
30 |
31 | 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:
32 |
33 | ```bash
34 | turso db show --url <database-name>
35 | ```
36 |
37 | Then get the database authentication token:
38 |
39 | ```bash
40 | turso db tokens create <database-name>
41 | ```
42 |
43 | Add those values to your configuration as shown above.
44 |
45 | ### With Cursor
46 |
47 | To configure the Turso MCP server with Cursor, add the following to your Cursor settings:
48 |
49 | 1. Open Cursor and go to Settings (⚙️) > Settings (JSON)
50 | 2. Add the following configuration to your settings JSON:
51 |
52 | ```json
53 | "mcpServers": {
54 | "turso": {
55 | "command": "npx",
56 | "args": ["-y", "mcp-turso"],
57 | "env": {
58 | "TURSO_DATABASE_URL": "your_url",
59 | "TURSO_AUTH_TOKEN": "your_token"
60 | }
61 | }
62 | }
63 | ```
64 |
65 | Replace `your_url` and `your_token` with your Turso database URL and authentication token as described in the previous section.
66 |
67 | ### Logging
68 |
69 | 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:
70 |
71 | ```
72 | ~/.npm/_npx/<npx-dir-name>/node_modules/mcp-turso/logs/mcp-turso.log
73 | ```
74 |
75 | 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:
76 |
77 | ```json
78 | {
79 | "mcpServers": [
80 | "turso": {
81 | "command": "npx",
82 | "args": ["-y", "mcp-turso", "--logs", "/Users/<username>/path/to/dir/mcp-logs.log"],
83 | "env": {
84 | "TURSO_DATABASE_URL": "your_url",
85 | "TURSO_AUTH_TOKEN": "your_token"
86 | }
87 | }
88 | ]
89 | }
90 | ```
91 |
92 | 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`.
93 |
94 | _Note_: Right now, I haven't implemented specifying a custom logging file for Windows, but this is coming.
95 |
96 | ## Server Capabilities
97 |
98 | The server provides the following tools:
99 |
100 | - `list_tables`
101 | - Get a list of all the tables in the database
102 | - No input
103 | - Returns: an array of table names
104 | - `get_db_schema`
105 | - Get the schemas of all tables in the database
106 | - No input
107 | - Returns: an array of SQL creation statements
108 | - `describe_table`
109 | - View schema information for a specific table
110 | - Input:
111 | - `table_name` (string): Name of table to describe
112 | - Returns: Array of column definitions with names and types
113 | - `query_database`
114 | - Execute a SELECT query to read data from the database
115 | - Input:
116 | - `sql` (string): The SELECT SQL query to execute
117 | - Returns: Query results as an object of type `{ columns: string[]; rows: Record<string, unknown>[]; rowCount: number; }`
118 |
119 | ## Todo
120 |
121 | - [ ] Add the ability to specify a custom log file on windows
122 | - [ ] Add more query tools
123 |
124 | ## License
125 |
126 | MIT License - see the [LICENSE](LICENSE) file for details.
127 |
```
--------------------------------------------------------------------------------
/tsdown.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from "tsdown";
2 |
3 | export default defineConfig({
4 | entry: ["./src/index.ts"],
5 | target: "es2022",
6 | clean: true,
7 | dts: true,
8 | });
9 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "@total-typescript/tsconfig/tsc/no-dom/library",
3 | "compilerOptions": {
4 | "forceConsistentCasingInFileNames": true,
5 | "rootDir": "src",
6 | "outDir": "dist",
7 | "sourceMap": false,
8 | "declaration": false,
9 | "moduleResolution": "bundler",
10 | "module": "ESNext",
11 | "resolveJsonModule": true
12 | },
13 | "exclude": ["node_modules", "dist"],
14 | "include": ["src/**/*"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "indentStyle": "tab"
15 | },
16 | "organizeImports": {
17 | "enabled": true
18 | },
19 | "linter": {
20 | "enabled": true,
21 | "rules": {
22 | "recommended": true
23 | }
24 | },
25 | "javascript": {
26 | "formatter": {
27 | "quoteStyle": "double"
28 | }
29 | }
30 | }
31 |
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | export const TableColumnSchema = z.object({
4 | name: z.string(),
5 | type: z.string(),
6 | notnull: z.number(),
7 | dflt_value: z.union([z.string(), z.null()]),
8 | pk: z.number(),
9 | });
10 |
11 | export type TableColumn = z.infer<typeof TableColumnSchema>;
12 |
13 | export const envSchema = z.object({
14 | TURSO_DATABASE_URL: z.string().min(1, "Database URL is required"),
15 | TURSO_AUTH_TOKEN: z.string().min(1, "Auth token is required"),
16 | });
17 |
18 | export type Config = z.infer<typeof envSchema>;
19 |
20 | export type TextContent = {
21 | type: "text";
22 | text: string;
23 | };
24 |
25 | export type ImageContent = {
26 | type: "image";
27 | data: string;
28 | mimeType: string;
29 | };
30 |
31 | export type Content = TextContent | ImageContent;
32 |
33 | export type ContentResult = {
34 | content: Content[];
35 | isError?: boolean;
36 | };
37 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-turso",
3 | "version": "0.2.1",
4 | "description": "MCP server for interacting with Turso-hosted LibSQL databases",
5 | "license": "MIT",
6 | "author": "Nico Baier",
7 | "type": "module",
8 | "bin": {
9 | "mcp-turso": "dist/index.js"
10 | },
11 | "files": [
12 | "dist",
13 | "README.md",
14 | "LICENSE"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/nbbaier/mcp-turso.git"
19 | },
20 | "homepage": "https://github.com/nbbaier/mcp-turso#readme",
21 | "bugs": "https://github.com/nbbaier/mcp-turso/issues",
22 | "scripts": {
23 | "build": "tsdown && chmod +x dist/index.js"
24 | },
25 | "keywords": [
26 | "mcp",
27 | "libsql",
28 | "sqlite",
29 | "turso",
30 | "database",
31 | "model-context-protocol",
32 | "mcp-server",
33 | "ai",
34 | "sql"
35 | ],
36 | "dependencies": {
37 | "@libsql/client": "^0.15.15",
38 | "@modelcontextprotocol/sdk": "^1.17.5",
39 | "fastmcp": "^3.16.0",
40 | "zod": "^4.1.5"
41 | },
42 | "devDependencies": {
43 | "@total-typescript/tsconfig": "^1.0.4",
44 | "@types/bun": "^1.2.21",
45 | "tsdown": "^0.15.0",
46 | "typescript": "^5.9.2"
47 | }
48 | }
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { appendFileSync, existsSync, mkdirSync } from "node:fs";
2 | import { dirname, join } from "node:path";
3 | import { fileURLToPath } from "node:url";
4 | import { name } from "../package.json" with { type: "json" };
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 | const parentDir = join(__dirname, "..");
9 | const logsDir = join(parentDir, "logs");
10 |
11 | export const DEFAULT_LOG_FILE = join(logsDir, `${name}.log`);
12 |
13 | /**
14 | * Formats a log message with timestamp, level, and optional data.
15 | *
16 | * @param level - The log level (INFO, ERROR, DEBUG, etc)
17 | * @param message - The log message text
18 | * @param data - Optional data to include in the log entry
19 | * @returns A formatted log string
20 | */
21 | function formatLogMessage(
22 | level: string,
23 | message: string,
24 | data?: unknown,
25 | ): string {
26 | const timestamp = new Date().toISOString();
27 | const dataStr = data ? `\n${JSON.stringify(data, null, 2)}` : "";
28 | return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
29 | }
30 |
31 | /**
32 | * Creates a logger instance that writes to the specified log file.
33 | *
34 | * @param logFile - Path to the log file (defaults to DEFAULT_LOG_FILE)
35 | * @returns A logger object with info, error, and debug methods
36 | */
37 | /**
38 | * Creates a logger instance that writes log messages to a file.
39 | *
40 | * @param logFile - The path to the log file. Defaults to DEFAULT_LOG_FILE.
41 | * @returns A `logger` object with methods for logging at different levels.
42 | * @returns {Object} `logger` - The logger object.
43 | * @returns {string} `logger.logFile` - The path to the log file.
44 | * @returns {function} `logger.log` - Logs a message with a custom level.
45 | * @returns {function} `logger.info` - Logs an info level message.
46 | * @returns {function} `logger.error` - Logs an error level message.
47 | * @returns {function} `logger.debug` - Logs a debug level message.
48 | */
49 | export function createLogger(logFile: string = DEFAULT_LOG_FILE) {
50 | const logDir = dirname(logFile);
51 | if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
52 | if (!existsSync(logFile)) appendFileSync(logFile, "");
53 |
54 | return {
55 | logFile,
56 | log(level: string, message: string, data?: unknown): void {
57 | const logMessage = formatLogMessage(level, message, data);
58 | appendFileSync(logFile, logMessage);
59 | },
60 | info(message: string, data?: unknown): void {
61 | const logMessage = formatLogMessage("INFO", message, data);
62 | appendFileSync(logFile, logMessage);
63 | },
64 | error(message: string, error?: unknown): void {
65 | const logMessage = formatLogMessage("ERROR", message, error);
66 | appendFileSync(logFile, logMessage);
67 | },
68 | debug(message: string, data?: unknown): void {
69 | const logMessage = formatLogMessage("DEBUG", message, data);
70 | appendFileSync(logFile, logMessage);
71 | },
72 | };
73 | }
74 |
75 | export default createLogger;
76 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { FastMCP } from "fastmcp";
2 | import { z } from "zod";
3 | import createLogger from "./logger.js";
4 | import {
5 | getLogFile,
6 | listTables,
7 | content,
8 | dbSchema,
9 | describeTable,
10 | query,
11 | getVersion,
12 | } from "./utils.js";
13 | import { createClient, type Client } from "@libsql/client";
14 | import { version } from "../package.json" with { type: "json" };
15 |
16 | const server = new FastMCP({
17 | name: "Turso MCP Server",
18 | version: getVersion(version),
19 | });
20 |
21 | const dbUrl = process.env.TURSO_DATABASE_URL;
22 | const authToken = process.env.TURSO_AUTH_TOKEN;
23 | const logFile = getLogFile();
24 |
25 | const logger = createLogger(logFile);
26 |
27 | if (!dbUrl) {
28 | logger.error("TURSO_DATABASE_URL environment variable is required");
29 | process.exit(1);
30 | }
31 |
32 | if (!authToken) {
33 | logger.error("TURSO_AUTH_TOKEN environment variable is required");
34 | process.exit(1);
35 | }
36 |
37 | let db: Client;
38 |
39 | try {
40 | db = createClient({ url: dbUrl, authToken });
41 | logger.info("Successfully connected to Turso database");
42 | } catch (error) {
43 | logger.error("Failed to connect to Turso database", error);
44 | process.exit(1);
45 | }
46 |
47 | server.addTool({
48 | name: "list_tables",
49 | description: "List all tables in the database",
50 | parameters: z.object({}),
51 | execute: async () => {
52 | try {
53 | logger.info("Executing list_tables");
54 | const tables = await listTables(db);
55 | return content(JSON.stringify({ tables }, null, 2));
56 | } catch (error) {
57 | logger.error("Failed to list tables", error);
58 | return content(
59 | `Error listing tables: ${error instanceof Error ? error.message : String(error)}`,
60 | true,
61 | );
62 | }
63 | },
64 | });
65 |
66 | server.addTool({
67 | name: "get_db_schema",
68 | description: "Get the schema for all tables in the database",
69 | parameters: z.object({}),
70 | execute: async () => {
71 | try {
72 | const schema = await dbSchema(db);
73 | return content(JSON.stringify({ schema }, null, 2));
74 | } catch (error) {
75 | return content(
76 | `Error getting schema: ${error instanceof Error ? error.message : String(error)}`,
77 | true,
78 | );
79 | }
80 | },
81 | });
82 |
83 | server.addTool({
84 | name: "describe_table",
85 | description: "View schema information for a specific table",
86 | parameters: z.object({
87 | table_name: z
88 | .string()
89 | .describe("Name of the table to describe")
90 | .min(1, "Table name is required"),
91 | }),
92 | execute: async ({ table_name }) => {
93 | try {
94 | logger.info(`Executing describe_table for table: ${table_name}`);
95 | const schema = await describeTable(table_name, db);
96 | return content(JSON.stringify({ schema }, null, 2));
97 | } catch (error) {
98 | logger.error(`Failed to describe table ${table_name}`, error);
99 | return content(
100 | `Error describing table: ${error instanceof Error ? error.message : String(error)}`,
101 | true,
102 | );
103 | }
104 | },
105 | });
106 |
107 | server.addTool({
108 | name: "query_database",
109 | description: "Execute a SELECT query to read data from the database",
110 | parameters: z.object({
111 | sql: z
112 | .string()
113 | .describe("SQL query to execute")
114 | .min(1, "SQL query is required"),
115 | }),
116 | execute: async ({ sql }) => {
117 | try {
118 | logger.info(`Executing query: ${sql}`);
119 | const result = await query(sql, db);
120 | return content(JSON.stringify(result, null, 2));
121 | } catch (error) {
122 | logger.error("Failed to execute query", error);
123 | return content(
124 | `Error executing query: ${error instanceof Error ? error.message : String(error)}`,
125 | true,
126 | );
127 | }
128 | },
129 | });
130 |
131 | process.on("uncaughtException", (error) => {
132 | logger.error("Uncaught exception", error);
133 | });
134 |
135 | process.on("unhandledRejection", (reason) => {
136 | logger.error("Unhandled rejection", reason);
137 | });
138 |
139 | console.error(`[INFO] Additional logs available at: ${logger.logFile}`);
140 |
141 | server.start({
142 | transportType: "stdio",
143 | });
144 |
145 | process.on("exit", (code) => {
146 | logger.info("Turso MCP server closed", code);
147 | });
148 |
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Client } from "@libsql/client";
2 | import { parseArgs } from "node:util";
3 | import { z } from "zod";
4 | import { DEFAULT_LOG_FILE } from "./logger.js";
5 | import {
6 | type Config,
7 | type TableColumn,
8 | type TextContent,
9 | envSchema,
10 | } from "./types.js";
11 |
12 | /**
13 | * Retrieves a list of all tables in the Turso database.
14 | *
15 | * @param client - The Turso database client instance
16 | * @returns A promise that resolves to an array of table names
17 | */
18 | export async function listTables(client: Client): Promise<string[]> {
19 | const result = await client.execute({
20 | sql: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
21 | args: [],
22 | });
23 |
24 | return result.rows.map((row) => row.name as string);
25 | }
26 |
27 | /**
28 | * Retrieves the SQL schema definitions for all tables in the database.
29 | *
30 | * @param client - The Turso database client instance
31 | * @returns A promise that resolves to an array of SQL schema statements
32 | */
33 | export async function dbSchema(client: Client): Promise<string[]> {
34 | const result = await client.execute({
35 | sql: "SELECT sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
36 | args: [],
37 | });
38 |
39 | return result.rows.map((row) => row.sql as string);
40 | }
41 |
42 | /**
43 | * Retrieves detailed schema information for a specific table.
44 | *
45 | * @param tableName - The name of the table to describe
46 | * @param client - The Turso database client instance
47 | * @returns A promise that resolves to an array of column definitions
48 | * @throws Error if the table name is invalid or the table doesn't exist
49 | */
50 | export async function describeTable(
51 | tableName: string,
52 | client: Client,
53 | ): Promise<TableColumn[]> {
54 | if (!/^[a-zA-Z0-9_]+$/.test(tableName)) {
55 | throw new Error(
56 | "Invalid table name. Only alphanumeric characters and underscores are allowed.",
57 | );
58 | }
59 |
60 | const result = await client.execute({
61 | sql: `PRAGMA table_info(${tableName})`,
62 | args: [],
63 | });
64 |
65 | if (result.rows.length === 0) {
66 | throw new Error(`Table '${tableName}' not found`);
67 | }
68 |
69 | return result.rows.map((row) => ({
70 | name: row.name as string,
71 | type: row.type as string,
72 | notnull: row.notnull as number,
73 | dflt_value: row.dflt_value as string | null,
74 | pk: row.pk as number,
75 | }));
76 | }
77 |
78 | /**
79 | * Executes a SELECT SQL query against the database.
80 | *
81 | * @param sql - The SQL query to execute (must be a SELECT query)
82 | * @param client - The Turso database client instance
83 | * @returns A promise that resolves to an object containing columns, rows, and row count
84 | * @throws Error if the query is not a SELECT query
85 | */
86 | export async function query<T = Record<string, unknown>>(
87 | sql: string,
88 | client: Client,
89 | ): Promise<{
90 | columns: string[];
91 | rows: T[];
92 | rowCount: number;
93 | }> {
94 | const trimmedQuery = sql.trim().toUpperCase();
95 | if (!trimmedQuery.startsWith("SELECT")) {
96 | throw new Error("Only SELECT queries are allowed for safety reasons");
97 | }
98 |
99 | const result = await client.execute({
100 | sql,
101 | args: [],
102 | });
103 |
104 | return {
105 | columns: result.columns,
106 | rows: result.rows as T[],
107 | rowCount: result.rows.length,
108 | };
109 | }
110 |
111 | /**
112 | * Loads and validates environment configuration for the Turso database.
113 | *
114 | * @returns A validated configuration object
115 | * @throws Error if the configuration is invalid
116 | */
117 | export function loadConfig(): Config {
118 | const config = envSchema.safeParse(process.env);
119 |
120 | if (!config.success) {
121 | throw new Error(`Configuration error: ${config.error.message}`);
122 | }
123 |
124 | return config.data;
125 | }
126 |
127 | /**
128 | * Creates a formatted content response object for MCP tools.
129 | *
130 | * @param text - The text content to include in the response
131 | * @param error - Whether this content represents an error (default: false)
132 | * @returns A formatted content result object
133 | */
134 | export function content(
135 | text: string,
136 | error = false,
137 | ): { content: TextContent[]; isError: boolean } {
138 | return {
139 | content: [{ type: "text", text }],
140 | isError: error,
141 | };
142 | }
143 |
144 | /**
145 | * Determines the log file path based on command line arguments or defaults.
146 | *
147 | * @returns The path to the log file
148 | */
149 | export function getLogFile(): string {
150 | const { values } = parseArgs({
151 | args: process.argv,
152 | options: {
153 | logs: {
154 | type: "string",
155 | },
156 | },
157 | strict: true,
158 | allowPositionals: true,
159 | });
160 |
161 | const parsedLogs = z
162 | .string()
163 | .refine((targetPath) => {
164 | const posixPath = targetPath.split("\\").join("/");
165 | return targetPath === posixPath && posixPath.includes("/");
166 | })
167 | .safeParse(values.logs);
168 |
169 | return values.logs && parsedLogs.success ? parsedLogs.data : DEFAULT_LOG_FILE;
170 | }
171 |
172 | /**
173 | * Retrieves the version string from a package.json file.
174 | *
175 | * @param pkg - The package.json file content
176 | * @returns The version string
177 | */
178 | export function getVersion(version: string): `${number}.${number}.${number}` {
179 | return version.match(/^\d+\.\d+\.\d+$/)
180 | ? (version as `${number}.${number}.${number}`)
181 | : "0.0.0";
182 | }
183 |
```