#
tokens: 3763/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── CHANGELOG.md
├── deno.json
├── deno.lock
├── LICENSE.txt
├── main.ts
└── README.md
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
.zed/
*.log
mcp-server-libsql

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# LibSQL Model Context Protocol Server

The LibSQL Model Context Protocol Server is a server application designed to
interface with LibSQL databases, providing schema information and enabling table
queries. Built using Deno 2.1, this server leverages the Model Context Protocol
(MCP) to handle various requests such as listing resources, reading resource
schemas, completing prompts, and executing SQL queries. It supports both
authenticated and unauthenticated access to LibSQL databases, ensuring
flexibility and security. This project is ideal for developers looking to
integrate LibSQL database functionalities into their applications seamlessly.

## Requirements

- Deno 2.1+
- A LibSQL database URL

## Usage

Install [deno](https://docs.deno.com/runtime) (macos/linux):

```bash
curl -fsSL https://deno.land/install.sh | sh
```

Build the binary:

```bash
deno run build
```

Run the server:

```bash
# If accessing a local libsql db that does not require auth
./mcp-server-libsql <database-url>

# With Auth
./mcp-server-libsql --auth-token <token> <database-url>
```

## License

This project is licensed under the MIT License - see the
[LICENSE.txt](LICENSE.txt) file for details.

```

--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------

```json
{
  "tasks": {
    "dev": "deno run --watch main.ts",
    "build": "deno compile --allow-run --allow-write --allow-read --allow-net --allow-env -o mcp-server-libsql main.ts"
  },
  "imports": {
    "@libsql/client": "npm:@libsql/client@^0.14.0",
    "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.0.1",
    "@std/assert": "jsr:@std/assert@1",
    "@std/cli": "jsr:@std/cli@^1.0.7",
    "@std/csv": "jsr:@std/csv@^1.0.4",
    "@std/fs": "jsr:@std/fs@^1.0.6",
    "@std/log": "jsr:@std/log@^0.224.11",
    "@std/path": "jsr:@std/path@^1.0.8",
    "pg": "npm:pg@^8.13.1",
    "zod": "npm:zod@^3.23.8"
  }
}

```

--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------

```
MIT License

Copyright (c) 2024 - 2025 Nicholas Quezada

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.4.1 - 2025-01-20

### Added

- `executeSql` function to handle db connection and query execution.
- `fetchAllFromTable` function to fetch and format table data.

### Changed

- Moved database client creation inside functions to ensure clients receive
  proper error messages when database connections fail.
- Updated various request handlers to use the new `executeSql` function.

## [0.4.0] - 2024-12-05

### Added

- Added `@std/fs` and `@std/path` dependencies.
- Added `getLogFilePath` function to determine log file path based on OS and
  environment variables.

### Changed

- Updated `logFile` to use the path returned by `getLogFilePath` function.
- Updated `deno.json` to include new permissions for `build` task.

### Fixed

- Fixed versioning

[0.4.0]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.3...v0.4.0

## [0.0.3] - 2024-12-01

### Added

- Dependencies for @std/csv and @std/log.

### Changed

- Refactored logging to use @std/log.
- Removed utils.ts and migrated CSV export functionality to use @std/csv.

[0.0.3]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.2...v0.0.3

## [0.0.2] - 2024-12-01

### Added

- Support for @libsql/client v0.14.0
- Command line argument parsing with @std/cli
- Debug logging functionality
- New prompts: `libsql-schema` and `libsql-query`
- New tool: `query` for running read-only SQL queries
- CSV export functionality
- Logger utility functions

### Changed

- Switched from command line Turso CLI to @libsql/client
- Improved error handling
- Updated server version from 0.0.1 to 0.0.2
- Refactored code structure

### Fixed

- Input validation for table names
- Database connection handling

[0.0.2]: https://github.com/nicholasq/mcp-server-libsql/compare/v0.0.1...v0.0.2

```

--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  CompleteRequestSchema,
  GetPromptRequestSchema,
  ListPromptsRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { parseArgs } from "@std/cli";
import { z } from "zod";
import { createClient, InStatement, ResultSet, Row } from "@libsql/client";
import { stringify as toCsv } from "@std/csv";
import * as log from "@std/log";
import { join } from "@std/path";
import { ensureDir } from "@std/fs";

const VERSION = "0.4.0";
const SCHEMA_PROMPT_NAME = "libsql-schema";
const QUERY_PROMPT_NAME = "libsql-query";
const ALL_TABLES = "all-tables";
const FETCH_ALL_TABLES_SQL = "SELECT * FROM sqlite_master WHERE type = 'table'";

interface SqliteMaster extends Row {
  type: string;
  name: string;
  tbl_name: string;
  rootpage: number;
  sql: string;
}

const args = parseArgs(Deno.args);
const argsSchema = z.object({
  "auth-token": z.string().nullish(),
  "log-file": z.string().nullish(),
  "debug": z.boolean().nullish(),
  "_": z.array(z.string().regex(/^(https?|libsql):\/\//)).nonempty(),
});

argsSchema.parse(args);

const dbUrl = args._[0] as string;
const authToken = args["auth-token"];
const debug = args["debug"];
const logLevel = debug ? "DEBUG" : "WARN";

log.setup({
  handlers: {
    file: new log.FileHandler(logLevel, {
      filename: await getLogFilePath(),
      bufferSize: 0,
      formatter: log.formatters.jsonFormatter,
    }),
  },
  loggers: {
    default: {
      level: logLevel,
      handlers: ["file"],
    },
  },
});

const logger = log.getLogger();

const server = new Server(
  {
    name: "context-server/libsql",
    version: VERSION,
  },
  {
    capabilities: {
      resources: {},
      prompts: {},
      tools: {},
    },
  },
);

async function getLogFilePath() {
  const os = Deno.build.os;
  const homeDir = Deno.env.get("HOME") || Deno.env.get("USERPROFILE");

  if (!homeDir) {
    throw new Error("HOME or USERPROFILE environment variable not set");
  }

  let logDir = join(homeDir, ".local", "share", "mcp-server-libsql");

  if (os === "windows") {
    logDir = join(homeDir, "AppData", "Local", "mcp-server-libsql");
  } else if (os !== "darwin" && os !== "linux") {
    throw new Error(`Unsupported OS: ${os}`);
  }

  await ensureDir(logDir);

  return join(logDir, "mcp-server-libsql.log");
}

function executeSql(inStmt: InStatement): Promise<ResultSet> {
  logger.debug("executeSql", inStmt);
  const db = createClient({ url: dbUrl, authToken });
  return db.execute(inStmt);
}

async function fetchAllFromTable(
  tableName: string,
  page: number = 1,
  limit: number = 25,
  outputFormat: "json" | "csv" = "csv",
): Promise<string> {
  const tablesResultSet = await executeSql(FETCH_ALL_TABLES_SQL);
  const sqliteMasterRows = tablesResultSet.rows as SqliteMaster[];
  const validTableNames = sqliteMasterRows.map((row) => row.tbl_name);

  if (!validTableNames.includes(tableName)) {
    throw new Error(`Invalid table name: ${tableName}`);
  }

  const queryResultSet = await executeSql({
    sql: `SELECT * FROM ${tableName} LIMIT ? OFFSET ?`,
    args: [limit, (page - 1) * limit],
  });

  if (queryResultSet.rows.length === 0) {
    return `No rows found in table ${tableName}`;
  }

  return outputFormat === "csv"
    ? toCsv(queryResultSet.rows, { columns: queryResultSet.columns })
    : JSON.stringify(queryResultSet.rows);
}

server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
  logger.debug("ListResourcesRequestSchema", request);

  const rs = await executeSql(FETCH_ALL_TABLES_SQL);
  const rows = rs.rows as SqliteMaster[];
  const tables = rows.map((row) => row.tbl_name);

  return {
    resources: tables.map((table) => ({
      uri: new URL(`${table}/schema`, dbUrl).href,
      name: `${table} table schema`,
    })),
  };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  logger.debug("ReadResourceRequestSchema", request);
  const resourceUrl = new URL(request.params.uri);
  const pathComponents = resourceUrl.pathname.split("/");
  const schema = pathComponents.pop();
  const tableName = pathComponents.pop()?.trim();

  if (schema !== "schema") {
    throw new Error("Invalid resource URI");
  }

  if (tableName === undefined) {
    throw new Error("No table name provided");
  }

  const rs = await executeSql({
    sql: "SELECT * FROM sqlite_master WHERE type = 'table' AND tbl_name = ?",
    args: [tableName],
  });

  if (rs.rows.length === 0) {
    throw new Error(`Table '${tableName}' not found`);
  }

  const rows = rs.rows as SqliteMaster[];

  return {
    contents: [
      {
        uri: request.params.uri,
        mimeType: "text/plain",
        text: rows[0].sql,
      },
    ],
  };
});

server.setRequestHandler(CompleteRequestSchema, async (request) => {
  logger.debug("CompleteRequestSchema", request);
  if (
    request.params.ref.name === SCHEMA_PROMPT_NAME ||
    request.params.ref.name === QUERY_PROMPT_NAME
  ) {
    const tableNameQuery = request.params.argument.value;
    const alreadyHasArg = /\S*\s/.test(tableNameQuery);

    if (alreadyHasArg) {
      return { completion: { values: [] } };
    }

    const rs = await executeSql(FETCH_ALL_TABLES_SQL);
    const rows = rs.rows as SqliteMaster[];
    const tables = rows.map((row) => row.tbl_name);

    return {
      completion: {
        values: [ALL_TABLES, ...tables],
      },
    };
  }

  throw new Error("unknown prompt");
});

server.setRequestHandler(ListPromptsRequestSchema, (request) => {
  logger.debug("ListPromptsRequestSchema", request);
  return {
    prompts: [
      {
        name: SCHEMA_PROMPT_NAME,
        description:
          "Retrieve the schema for a given table in the libSQL database",
        arguments: [{
          name: "tableName",
          description: "the table to describe",
          required: true,
        }],
      },
      {
        name: QUERY_PROMPT_NAME,
        description: "Query all rows from a table",
        arguments: [{
          name: "tableName",
          description: "the table to query",
          required: true,
        }],
      },
    ],
  };
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  logger.debug("GetPromptRequestSchema", request);
  if (request.params.name === SCHEMA_PROMPT_NAME) {
    const tableName = request.params.arguments?.tableName;

    if (typeof tableName !== "string" || tableName.length === 0) {
      throw new Error(`Invalid tableName: ${tableName}`);
    }

    let schema: string;
    if (tableName === ALL_TABLES) {
      const rs = await executeSql(FETCH_ALL_TABLES_SQL);
      const rows = rs.rows as SqliteMaster[];
      schema = rows.map((row) => row.sql).join("\n\n");
    } else {
      const rs = await executeSql({
        sql: "SELECT * FROM sqlite_master WHERE type='table' AND tbl_name = ?",
        args: [tableName],
      });
      const rows = rs.rows as SqliteMaster[];
      schema = rows.map((row) => row.sql).join("\n\n");
    }

    return {
      description: tableName === ALL_TABLES
        ? "all table schemas"
        : `${tableName} schema`,
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: "```sql\n" + schema + "\n```",
        },
      }],
    };
  }

  if (request.params.name === QUERY_PROMPT_NAME) {
    const tableName = request.params.arguments?.tableName;

    if (typeof tableName !== "string" || tableName.length === 0) {
      throw new Error(`Invalid tableName: ${tableName}`);
    }

    if (tableName === ALL_TABLES) {
      const rs = await executeSql(FETCH_ALL_TABLES_SQL);
      const rows = rs.rows as SqliteMaster[];
      const tables = rows.map((row) => row.tbl_name);

      const allTablesData = tables.filter((table) => table?.trim()).map(
        (tableName) => {
          return fetchAllFromTable(tableName);
        },
      );

      const result = await Promise.all(allTablesData);
      const data = result.join("\n\n");

      return {
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: data,
          },
        }],
      };
    } else {
      const data = await fetchAllFromTable(tableName);

      return {
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: data,
          },
        }],
      };
    }
  }

  throw new Error(`Prompt '${request.params.name}' not implemented`);
});

server.setRequestHandler(ListToolsRequestSchema, (request) => {
  logger.debug("ListToolsRequestSchema", request);
  return {
    tools: [
      {
        name: "query",
        description: "Run a read-only SQL query",
        inputSchema: {
          type: "object",
          properties: {
            sql: { type: "string" },
          },
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  logger.debug("CallToolRequestSchema", request);
  if (request.params.name === "query") {
    const sql = request.params.arguments?.sql as string;
    const res = await executeSql(sql);
    return { content: [{ type: "string", text: JSON.stringify(res.rows) }] };
  }
  throw new Error("Tool not found");
});

const transport = new StdioServerTransport();
await server.connect(transport);

```