#
tokens: 5053/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```