# Directory Structure ``` ├── .gitignore ├── CHANGELOG.md ├── deno.json ├── deno.lock ├── LICENSE.txt ├── main.ts └── README.md ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | .zed/ 2 | *.log 3 | mcp-server-libsql 4 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # LibSQL Model Context Protocol Server 2 | 3 | The LibSQL Model Context Protocol Server is a server application designed to 4 | interface with LibSQL databases, providing schema information and enabling table 5 | queries. Built using Deno 2.1, this server leverages the Model Context Protocol 6 | (MCP) to handle various requests such as listing resources, reading resource 7 | schemas, completing prompts, and executing SQL queries. It supports both 8 | authenticated and unauthenticated access to LibSQL databases, ensuring 9 | flexibility and security. This project is ideal for developers looking to 10 | integrate LibSQL database functionalities into their applications seamlessly. 11 | 12 | ## Requirements 13 | 14 | - Deno 2.1+ 15 | - A LibSQL database URL 16 | 17 | ## Usage 18 | 19 | Install [deno](https://docs.deno.com/runtime) (macos/linux): 20 | 21 | ```bash 22 | curl -fsSL https://deno.land/install.sh | sh 23 | ``` 24 | 25 | Build the binary: 26 | 27 | ```bash 28 | deno run build 29 | ``` 30 | 31 | Run the server: 32 | 33 | ```bash 34 | # If accessing a local libsql db that does not require auth 35 | ./mcp-server-libsql <database-url> 36 | 37 | # With Auth 38 | ./mcp-server-libsql --auth-token <token> <database-url> 39 | ``` 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License - see the 44 | [LICENSE.txt](LICENSE.txt) file for details. 45 | ``` -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "tasks": { 3 | "dev": "deno run --watch main.ts", 4 | "build": "deno compile --allow-run --allow-write --allow-read --allow-net --allow-env -o mcp-server-libsql main.ts" 5 | }, 6 | "imports": { 7 | "@libsql/client": "npm:@libsql/client@^0.14.0", 8 | "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.0.1", 9 | "@std/assert": "jsr:@std/assert@1", 10 | "@std/cli": "jsr:@std/cli@^1.0.7", 11 | "@std/csv": "jsr:@std/csv@^1.0.4", 12 | "@std/fs": "jsr:@std/fs@^1.0.6", 13 | "@std/log": "jsr:@std/log@^0.224.11", 14 | "@std/path": "jsr:@std/path@^1.0.8", 15 | "pg": "npm:pg@^8.13.1", 16 | "zod": "npm:zod@^3.23.8" 17 | } 18 | } 19 | ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- ``` 1 | MIT License 2 | 3 | Copyright (c) 2024 - 2025 Nicholas Quezada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## 0.4.1 - 2025-01-20 10 | 11 | ### Added 12 | 13 | - `executeSql` function to handle db connection and query execution. 14 | - `fetchAllFromTable` function to fetch and format table data. 15 | 16 | ### Changed 17 | 18 | - Moved database client creation inside functions to ensure clients receive 19 | proper error messages when database connections fail. 20 | - Updated various request handlers to use the new `executeSql` function. 21 | 22 | ## [0.4.0] - 2024-12-05 23 | 24 | ### Added 25 | 26 | - Added `@std/fs` and `@std/path` dependencies. 27 | - Added `getLogFilePath` function to determine log file path based on OS and 28 | environment variables. 29 | 30 | ### Changed 31 | 32 | - Updated `logFile` to use the path returned by `getLogFilePath` function. 33 | - Updated `deno.json` to include new permissions for `build` task. 34 | 35 | ### Fixed 36 | 37 | - Fixed versioning 38 | 39 | [0.4.0]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.3...v0.4.0 40 | 41 | ## [0.0.3] - 2024-12-01 42 | 43 | ### Added 44 | 45 | - Dependencies for @std/csv and @std/log. 46 | 47 | ### Changed 48 | 49 | - Refactored logging to use @std/log. 50 | - Removed utils.ts and migrated CSV export functionality to use @std/csv. 51 | 52 | [0.0.3]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.2...v0.0.3 53 | 54 | ## [0.0.2] - 2024-12-01 55 | 56 | ### Added 57 | 58 | - Support for @libsql/client v0.14.0 59 | - Command line argument parsing with @std/cli 60 | - Debug logging functionality 61 | - New prompts: `libsql-schema` and `libsql-query` 62 | - New tool: `query` for running read-only SQL queries 63 | - CSV export functionality 64 | - Logger utility functions 65 | 66 | ### Changed 67 | 68 | - Switched from command line Turso CLI to @libsql/client 69 | - Improved error handling 70 | - Updated server version from 0.0.1 to 0.0.2 71 | - Refactored code structure 72 | 73 | ### Fixed 74 | 75 | - Input validation for table names 76 | - Database connection handling 77 | 78 | [0.0.2]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.1...v0.0.2 79 | ``` -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { 4 | CallToolRequestSchema, 5 | CompleteRequestSchema, 6 | GetPromptRequestSchema, 7 | ListPromptsRequestSchema, 8 | ListResourcesRequestSchema, 9 | ListToolsRequestSchema, 10 | ReadResourceRequestSchema, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import { parseArgs } from "@std/cli"; 13 | import { z } from "zod"; 14 | import { createClient, InStatement, ResultSet, Row } from "@libsql/client"; 15 | import { stringify as toCsv } from "@std/csv"; 16 | import * as log from "@std/log"; 17 | import { join } from "@std/path"; 18 | import { ensureDir } from "@std/fs"; 19 | 20 | const VERSION = "0.4.0"; 21 | const SCHEMA_PROMPT_NAME = "libsql-schema"; 22 | const QUERY_PROMPT_NAME = "libsql-query"; 23 | const ALL_TABLES = "all-tables"; 24 | const FETCH_ALL_TABLES_SQL = "SELECT * FROM sqlite_master WHERE type = 'table'"; 25 | 26 | interface SqliteMaster extends Row { 27 | type: string; 28 | name: string; 29 | tbl_name: string; 30 | rootpage: number; 31 | sql: string; 32 | } 33 | 34 | const args = parseArgs(Deno.args); 35 | const argsSchema = z.object({ 36 | "auth-token": z.string().nullish(), 37 | "log-file": z.string().nullish(), 38 | "debug": z.boolean().nullish(), 39 | "_": z.array(z.string().regex(/^(https?|libsql):\/\//)).nonempty(), 40 | }); 41 | 42 | argsSchema.parse(args); 43 | 44 | const dbUrl = args._[0] as string; 45 | const authToken = args["auth-token"]; 46 | const debug = args["debug"]; 47 | const logLevel = debug ? "DEBUG" : "WARN"; 48 | 49 | log.setup({ 50 | handlers: { 51 | file: new log.FileHandler(logLevel, { 52 | filename: await getLogFilePath(), 53 | bufferSize: 0, 54 | formatter: log.formatters.jsonFormatter, 55 | }), 56 | }, 57 | loggers: { 58 | default: { 59 | level: logLevel, 60 | handlers: ["file"], 61 | }, 62 | }, 63 | }); 64 | 65 | const logger = log.getLogger(); 66 | 67 | const server = new Server( 68 | { 69 | name: "context-server/libsql", 70 | version: VERSION, 71 | }, 72 | { 73 | capabilities: { 74 | resources: {}, 75 | prompts: {}, 76 | tools: {}, 77 | }, 78 | }, 79 | ); 80 | 81 | async function getLogFilePath() { 82 | const os = Deno.build.os; 83 | const homeDir = Deno.env.get("HOME") || Deno.env.get("USERPROFILE"); 84 | 85 | if (!homeDir) { 86 | throw new Error("HOME or USERPROFILE environment variable not set"); 87 | } 88 | 89 | let logDir = join(homeDir, ".local", "share", "mcp-server-libsql"); 90 | 91 | if (os === "windows") { 92 | logDir = join(homeDir, "AppData", "Local", "mcp-server-libsql"); 93 | } else if (os !== "darwin" && os !== "linux") { 94 | throw new Error(`Unsupported OS: ${os}`); 95 | } 96 | 97 | await ensureDir(logDir); 98 | 99 | return join(logDir, "mcp-server-libsql.log"); 100 | } 101 | 102 | function executeSql(inStmt: InStatement): Promise<ResultSet> { 103 | logger.debug("executeSql", inStmt); 104 | const db = createClient({ url: dbUrl, authToken }); 105 | return db.execute(inStmt); 106 | } 107 | 108 | async function fetchAllFromTable( 109 | tableName: string, 110 | page: number = 1, 111 | limit: number = 25, 112 | outputFormat: "json" | "csv" = "csv", 113 | ): Promise<string> { 114 | const tablesResultSet = await executeSql(FETCH_ALL_TABLES_SQL); 115 | const sqliteMasterRows = tablesResultSet.rows as SqliteMaster[]; 116 | const validTableNames = sqliteMasterRows.map((row) => row.tbl_name); 117 | 118 | if (!validTableNames.includes(tableName)) { 119 | throw new Error(`Invalid table name: ${tableName}`); 120 | } 121 | 122 | const queryResultSet = await executeSql({ 123 | sql: `SELECT * FROM ${tableName} LIMIT ? OFFSET ?`, 124 | args: [limit, (page - 1) * limit], 125 | }); 126 | 127 | if (queryResultSet.rows.length === 0) { 128 | return `No rows found in table ${tableName}`; 129 | } 130 | 131 | return outputFormat === "csv" 132 | ? toCsv(queryResultSet.rows, { columns: queryResultSet.columns }) 133 | : JSON.stringify(queryResultSet.rows); 134 | } 135 | 136 | server.setRequestHandler(ListResourcesRequestSchema, async (request) => { 137 | logger.debug("ListResourcesRequestSchema", request); 138 | 139 | const rs = await executeSql(FETCH_ALL_TABLES_SQL); 140 | const rows = rs.rows as SqliteMaster[]; 141 | const tables = rows.map((row) => row.tbl_name); 142 | 143 | return { 144 | resources: tables.map((table) => ({ 145 | uri: new URL(`${table}/schema`, dbUrl).href, 146 | name: `${table} table schema`, 147 | })), 148 | }; 149 | }); 150 | 151 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 152 | logger.debug("ReadResourceRequestSchema", request); 153 | const resourceUrl = new URL(request.params.uri); 154 | const pathComponents = resourceUrl.pathname.split("/"); 155 | const schema = pathComponents.pop(); 156 | const tableName = pathComponents.pop()?.trim(); 157 | 158 | if (schema !== "schema") { 159 | throw new Error("Invalid resource URI"); 160 | } 161 | 162 | if (tableName === undefined) { 163 | throw new Error("No table name provided"); 164 | } 165 | 166 | const rs = await executeSql({ 167 | sql: "SELECT * FROM sqlite_master WHERE type = 'table' AND tbl_name = ?", 168 | args: [tableName], 169 | }); 170 | 171 | if (rs.rows.length === 0) { 172 | throw new Error(`Table '${tableName}' not found`); 173 | } 174 | 175 | const rows = rs.rows as SqliteMaster[]; 176 | 177 | return { 178 | contents: [ 179 | { 180 | uri: request.params.uri, 181 | mimeType: "text/plain", 182 | text: rows[0].sql, 183 | }, 184 | ], 185 | }; 186 | }); 187 | 188 | server.setRequestHandler(CompleteRequestSchema, async (request) => { 189 | logger.debug("CompleteRequestSchema", request); 190 | if ( 191 | request.params.ref.name === SCHEMA_PROMPT_NAME || 192 | request.params.ref.name === QUERY_PROMPT_NAME 193 | ) { 194 | const tableNameQuery = request.params.argument.value; 195 | const alreadyHasArg = /\S*\s/.test(tableNameQuery); 196 | 197 | if (alreadyHasArg) { 198 | return { completion: { values: [] } }; 199 | } 200 | 201 | const rs = await executeSql(FETCH_ALL_TABLES_SQL); 202 | const rows = rs.rows as SqliteMaster[]; 203 | const tables = rows.map((row) => row.tbl_name); 204 | 205 | return { 206 | completion: { 207 | values: [ALL_TABLES, ...tables], 208 | }, 209 | }; 210 | } 211 | 212 | throw new Error("unknown prompt"); 213 | }); 214 | 215 | server.setRequestHandler(ListPromptsRequestSchema, (request) => { 216 | logger.debug("ListPromptsRequestSchema", request); 217 | return { 218 | prompts: [ 219 | { 220 | name: SCHEMA_PROMPT_NAME, 221 | description: 222 | "Retrieve the schema for a given table in the libSQL database", 223 | arguments: [{ 224 | name: "tableName", 225 | description: "the table to describe", 226 | required: true, 227 | }], 228 | }, 229 | { 230 | name: QUERY_PROMPT_NAME, 231 | description: "Query all rows from a table", 232 | arguments: [{ 233 | name: "tableName", 234 | description: "the table to query", 235 | required: true, 236 | }], 237 | }, 238 | ], 239 | }; 240 | }); 241 | 242 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 243 | logger.debug("GetPromptRequestSchema", request); 244 | if (request.params.name === SCHEMA_PROMPT_NAME) { 245 | const tableName = request.params.arguments?.tableName; 246 | 247 | if (typeof tableName !== "string" || tableName.length === 0) { 248 | throw new Error(`Invalid tableName: ${tableName}`); 249 | } 250 | 251 | let schema: string; 252 | if (tableName === ALL_TABLES) { 253 | const rs = await executeSql(FETCH_ALL_TABLES_SQL); 254 | const rows = rs.rows as SqliteMaster[]; 255 | schema = rows.map((row) => row.sql).join("\n\n"); 256 | } else { 257 | const rs = await executeSql({ 258 | sql: "SELECT * FROM sqlite_master WHERE type='table' AND tbl_name = ?", 259 | args: [tableName], 260 | }); 261 | const rows = rs.rows as SqliteMaster[]; 262 | schema = rows.map((row) => row.sql).join("\n\n"); 263 | } 264 | 265 | return { 266 | description: tableName === ALL_TABLES 267 | ? "all table schemas" 268 | : `${tableName} schema`, 269 | messages: [{ 270 | role: "user", 271 | content: { 272 | type: "text", 273 | text: "```sql\n" + schema + "\n```", 274 | }, 275 | }], 276 | }; 277 | } 278 | 279 | if (request.params.name === QUERY_PROMPT_NAME) { 280 | const tableName = request.params.arguments?.tableName; 281 | 282 | if (typeof tableName !== "string" || tableName.length === 0) { 283 | throw new Error(`Invalid tableName: ${tableName}`); 284 | } 285 | 286 | if (tableName === ALL_TABLES) { 287 | const rs = await executeSql(FETCH_ALL_TABLES_SQL); 288 | const rows = rs.rows as SqliteMaster[]; 289 | const tables = rows.map((row) => row.tbl_name); 290 | 291 | const allTablesData = tables.filter((table) => table?.trim()).map( 292 | (tableName) => { 293 | return fetchAllFromTable(tableName); 294 | }, 295 | ); 296 | 297 | const result = await Promise.all(allTablesData); 298 | const data = result.join("\n\n"); 299 | 300 | return { 301 | messages: [{ 302 | role: "user", 303 | content: { 304 | type: "text", 305 | text: data, 306 | }, 307 | }], 308 | }; 309 | } else { 310 | const data = await fetchAllFromTable(tableName); 311 | 312 | return { 313 | messages: [{ 314 | role: "user", 315 | content: { 316 | type: "text", 317 | text: data, 318 | }, 319 | }], 320 | }; 321 | } 322 | } 323 | 324 | throw new Error(`Prompt '${request.params.name}' not implemented`); 325 | }); 326 | 327 | server.setRequestHandler(ListToolsRequestSchema, (request) => { 328 | logger.debug("ListToolsRequestSchema", request); 329 | return { 330 | tools: [ 331 | { 332 | name: "query", 333 | description: "Run a read-only SQL query", 334 | inputSchema: { 335 | type: "object", 336 | properties: { 337 | sql: { type: "string" }, 338 | }, 339 | }, 340 | }, 341 | ], 342 | }; 343 | }); 344 | 345 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 346 | logger.debug("CallToolRequestSchema", request); 347 | if (request.params.name === "query") { 348 | const sql = request.params.arguments?.sql as string; 349 | const res = await executeSql(sql); 350 | return { content: [{ type: "string", text: JSON.stringify(res.rows) }] }; 351 | } 352 | throw new Error("Tool not found"); 353 | }); 354 | 355 | const transport = new StdioServerTransport(); 356 | await server.connect(transport); 357 | ```