d73ff9b0fb
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
Tests / save-durations (push) Has been cancelled
78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sqlite3
|
|
from typing import Any
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
|
|
mcp = FastMCP("__SERVER_NAME__")
|
|
|
|
DATABASE_PATH = os.getenv("SQLITE_PATH", "./app.db")
|
|
MAX_ROWS = int(os.getenv("SQLITE_MAX_ROWS", "200"))
|
|
TABLE_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
|
|
|
|
def _connect() -> sqlite3.Connection:
|
|
return sqlite3.connect(f"file:{DATABASE_PATH}?mode=ro", uri=True)
|
|
|
|
|
|
def _reject_mutation(sql: str) -> None:
|
|
normalized = sql.strip().lower()
|
|
if not normalized.startswith("select"):
|
|
raise ValueError("Only SELECT queries are allowed")
|
|
|
|
|
|
def _validate_table_name(table_name: str) -> str:
|
|
if not TABLE_NAME_RE.fullmatch(table_name):
|
|
raise ValueError("Invalid table name")
|
|
return table_name
|
|
|
|
|
|
@mcp.tool
|
|
def list_tables() -> list[str]:
|
|
"""List user-defined SQLite tables."""
|
|
with _connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
).fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
|
|
@mcp.tool
|
|
def describe_table(table_name: str) -> list[dict[str, Any]]:
|
|
"""Describe columns for a SQLite table."""
|
|
safe_table_name = _validate_table_name(table_name)
|
|
with _connect() as conn:
|
|
rows = conn.execute(f"PRAGMA table_info({safe_table_name})").fetchall()
|
|
return [
|
|
{
|
|
"cid": row[0],
|
|
"name": row[1],
|
|
"type": row[2],
|
|
"notnull": bool(row[3]),
|
|
"default": row[4],
|
|
"pk": bool(row[5]),
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
@mcp.tool
|
|
def query(sql: str, limit: int = 50) -> dict[str, Any]:
|
|
"""Run a read-only SELECT query and return rows plus column names."""
|
|
_reject_mutation(sql)
|
|
safe_limit = max(0, min(limit, MAX_ROWS))
|
|
wrapped_sql = f"SELECT * FROM ({sql.strip().rstrip(';')}) LIMIT {safe_limit}"
|
|
with _connect() as conn:
|
|
cursor = conn.execute(wrapped_sql)
|
|
columns = [column[0] for column in cursor.description or []]
|
|
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
return {"limit": safe_limit, "columns": columns, "rows": rows}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|