# 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 |
```