Skip to main content
Glama
lampmaster

shop-sql-mcp

by lampmaster

shop-sql-mcp

Ein kleiner MCP-Server, der einem KI-Agenten schreibgeschützten analytischen Zugriff auf die SQLite-Datenbank shop.db über stdio gewährt.

Der Server macht genau drei Dinge und sonst nichts: Er listet Tabellen auf, beschreibt deren Schema und führt pro Aufruf eine einzige schreibgeschützte SQL-Anweisung mit serverseitig erzwungener Pagination aus. Die gesamte Denk- und Planungsarbeit – welche Joins durchgeführt werden, wie aggregiert wird, wann das Schema betrachtet wird – liegt beim Agenten.

AI Agent
    |
    |  MCP over stdio
    v
shop-sql-mcp
    |
    +-- list_tables
    +-- describe_table
    +-- query_database
    |
    v
read-only SQLite connection
    |
    v
shop.db

Anforderungen

  • Node.js 22.5 oder neuer (24+ empfohlen). Der Server verwendet das eingebaute node:sqlite-Modul, sodass keine native SQLite-Abhängigkeit kompiliert werden muss.

  • Keine weiteren Laufzeitvoraussetzungen.

Related MCP server: mcpserve-py

Installation

npm install

Konfiguration

Die Konfiguration ist optional. Standardmäßig öffnet der Server shop.db im Projektverzeichnis.

Variable

Standard

Bedeutung

DATABASE_PATH

<Projekt>/shop.db

Pfad zur SQLite-Datei. Relative Pfade werden relativ zum Projektverzeichnis aufgelöst, sodass der Server nicht vom Arbeitsverzeichnis abhängt, aus dem heraus er gestartet wird.

Kopieren Sie .env.example in .env, wenn Sie lokale Überschreibungen beibehalten möchten. Der Server selbst liest einfache Umgebungsvariablen; ANTHROPIC_API_KEY, EVAL_MODEL und EVAL_MAX_STEPS aus .env.example werden nur von npm run eval verwendet.

Build

npm run build

Kompiliert src/ nach dist/.

Ausführen

npm start              # runs the built server (dist/index.js)
npm run dev            # runs src/index.ts directly, no build step

Der Server spricht MCP über stdin/stdout und schreibt ausschließlich Diagnoseinformationen nach stderr. Wenn er in einem Terminal gestartet wird, wirkt das daher wie ein Hänger – das ist korrekt so. Er ist dafür vorgesehen, von einem MCP-Host gestartet zu werden.

Mit einem MCP-Agenten verbinden

Fügen Sie dies zur Konfiguration Ihres MCP-Hosts hinzu (Claude Desktops claude_desktop_config.json, .mcp.json für Claude Code oder die entsprechende Datei Ihres Hosts) und verwenden Sie dabei einen absoluten Pfad zum Projekt:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"]
    }
  }
}

Um ohne Build direkt aus dem Quellcode zu starten, verweisen Sie stattdessen auf den TypeScript-Einstiegspunkt – Node führt ihn direkt aus:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/src/index.ts"]
    }
  }
}

Um eine Datenbank an einem anderen Ort zu lesen:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"],
      "env": { "DATABASE_PATH": "/absolute/path/to/other.db" }
    }
  }
}

Für Claude Code können Sie ihn auch über die Befehlszeile registrieren:

claude mcp add shop-sql -- node /absolute/path/to/shop-sql-mcp/dist/index.js

Werkzeuge

list_tables

Keine Argumente. Gibt die Benutzertabellen zurück; interne sqlite_*-Tabellen sind ausgeblendet.

{
  "tables": [
    { "name": "customers" },
    { "name": "order_items" },
    { "name": "orders" },
    { "name": "products" }
  ]
}

describe_table

{ table: string }

Liest das Schema direkt live aus SQLite – nichts ist hartkodiert – und berichtet Spalten, Typen, Nullfähigkeit, Primärschlüssel und Fremdschlüssel:

{
  "table": "order_items",
  "columns": [
    { "name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true },
    { "name": "order_id", "type": "INTEGER", "nullable": false, "primaryKey": false }
  ],
  "foreignKeys": [
    { "column": "order_id", "referencesTable": "orders", "referencesColumn": "id" },
    { "column": "product_id", "referencesTable": "products", "referencesColumn": "id" }
  ]
}

Ein unbekannter Name ist ein behebbarer Fehler, kein Absturz:

{ "error": { "code": "TABLE_NOT_FOUND", "message": "TABLE_NOT_FOUND: Table \"foo\" does not exist." } }

Hinweis: Eine Spalte mit INTEGER PRIMARY KEY wird als nullable: false gemeldet. SQLites table_info sagt zwar etwas anderes, aber eine solche Spalte ist ein Alias für die Zeilen-ID (rowid) und kann niemals NULL enthalten.

query_database

{ sql: string; limit?: number; offset?: number }

Führt eine schreibgeschützte Anweisung aus – SELECT ... oder WITH ... SELECT ... – wobei JOIN, WHERE, GROUP BY, HAVING, ORDER BY, Unterabfragen, Aggregate und Datumsfilterung vollständig unterstützt werden.

{
  "columns": ["category", "revenue"],
  "rows": [["Electronics", 1234567.89]],
  "returnedRows": 1,
  "limit": 100,
  "offset": 0,
  "hasMore": false
}

Zeilen sind Arrays von Werten in der Reihenfolge von columns. Dadurch bleiben die Ergebnis-Payloads kompakt und eindeutig, wenn eine Abfrage zwei Spalten mit demselben Namen erzeugt.

Fehler kommen als normales Tool-Ergebnis mit gesetztem isError und einer kurzen, handlungsleitenden Payload zurück, sodass der Agent sein SQL korrigieren und erneut versuchen kann:

{ "error": { "code": "SQL_ERROR", "message": "no such column: total" } }

Fehlercodes: SQL_ERROR, READ_ONLY_VIOLATION, MULTIPLE_STATEMENTS, TABLE_NOT_FOUND, INVALID_ARGUMENT, DATABASE_UNAVAILABLE. Stack-Traces werden nie zurückgegeben.

Pagination

Die Pagination wird vom Server erzwungen, nicht durch das SQL des Modells.

  • Der Standardwert für limit ist 100, das Maximum 500; offset ist standardmäßig 0.

  • Die Abfrage des Agenten wird umschlossen als SELECT * FROM (<Ihr SQL>) LIMIT ? OFFSET ?, sodass eine Abfrage mit eigenem LIMIT 100000 niemals mehr Zeilen als limit zurückgeben kann.

  • Der Server ruft intern limit + 1 Zeilen ab, um über hasMore zu entscheiden – ohne eine zweite Zählabfrage – und gibt höchstens limit zurück.

  • Ein einzelner Aufruf gibt daher nie mehr als 500 Zeilen zurück, was verhindert, dass ein breites SELECT * den Kontext des Modells überschwemmt.

Um durch die Ergebnisse zu blättern, halten Sie das SQL identisch (mit einem deterministischen ORDER BY) und erhöhen Sie offset um limit, solange hasMore wahr ist.

Schreibgeschützte Sicherheit

Zwei unabhängige Ebenen, sodass keine allein tragend ist.

1. SQL-Validierung (sqlSafety.ts). Ein kleiner Lexer überspringt Kommentare, String-Literale und Bezeichner in Anführungszeichen und verlangt dann:

  • dass die Anweisung mit SELECT oder WITH beginnt – ein naives startsWith("SELECT") würde gültige schreibgeschützte CTEs ablehnen;

  • dass es genau eine Anweisung gibt (alles nach dem ersten ; wird abgelehnt, ein ; innerhalb eines Literals oder Kommentars ist kein Trennzeichen);

  • dass kein verbotenes Schlüsselwort irgendwo erscheint, auch nicht verschachtelt in einer CTE: INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REPLACEATTACH, DETACH, VACUUM, REINDEX, PRAGMA, ANALYZE, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, load_extension, writable_schema.

Verbotenes SQL wird immer mit einem expliziten Fehler abgelehnt – niemals still ignoriert und niemals teilweise ausgeführt. REPLACE(a, b, c) bleibt als Skalarfunktion erlaubt, da nur die Anweisung REPLACE INTO ein Schreibvorgang ist.

2. The SQLite-Verbindung selbst. shop.db wird mit new DatabaseSync(path, { readOnly: true }) geöffnet. Selbst wenn ein Schreibvorgang die Validierung umginge, lehnt SQLite ihn mit "attempt to write a readonly database" ab. Die Testsuite prüft diese Eigenschaft direkt, indem sie Schreibvorgänge über die Verbindung ausführt und dabei den Validator umgeht.

Schlechte oder verbotene Abfragen werden als Tool-Fehler zurückgegeben und beenden den Prozess nie, sodass eine Sitzung beliebig viele fehlgeschlagene Versuche überlebt.

Tests ausführen

npm test

Führt nur die deterministische Testsuite aus – kein Netzwerk, keine API-Schlüssel, kein LLM. Der eingebaute Test Runner von Node führt die TypeScript-Quelldateien direkt aus. Die Abdeckung umfasst: list_tables, describe_table (Spalten, Typen, Nullfähigkeit, Primärschlüssel, Fremdschlüssel, unbekannte Tabellen), einfache SELECTs, Filtern, Aggregation, Joins, GROUP BY, schreibgeschützte CTEs, Datumsfilterung, Pagination (Standard-Limit, maximales Limit, Offset, hasMore-Grenzen), ungültiges SQL, unbekannte Spalten und Tabellen, die Ablehnung von INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/REPLACE/ATTACH/DETACH/VACUUM/REINDEX/PRAGMA sowie mehrerer Anweisungen, den Beweis, dass die Datenbank nach jedem abgelehnten Schreibvorgang byte-identisch ist, und End-to-End-MCP-Aufrufe über stdio, die bestätigen, dass der Server nach Fehlern nutzbar bleibt.

Eval manuell ausführen

export ANTHROPIC_API_KEY=sk-...
npm run eval

Starten Sie dies manuell. Es ist bewusst von npm test ausgeschlossen, da es ein echtes LLM über stdio gegen den echten MCP-Server verwendet und kostenpflichtige API-Aufrufe auslöst.

Es startet den Server, übergibt dem Modell die drei MCP-Tools plus ein submit_answer-Tool, dessen JSON-Schema pro Aufgabe festgelegt ist, und vergleicht die strukturierte Antwort mit einem Referenzwert, der direkt aus SQLite berechnet wird – nicht mit natürlichsprachlichem Text. Die Aufgaben umfassen Tabellenentdeckung, mehrstufige Schema-Entdeckung, Filterung, Aggregation, Joins, Kundenausgaben, Kundenbestellanzahlen, Produktverkäufe, Kategorie-Umsätze, Umsatz im Jahr 2025 sowie eine zerstörerische Anforderung, die abgelehnt werden muss (die Prüfung stellt auch sicher, dass die Datenbank danach unverändert ist).

Optional: EVAL_MODEL (Standardwert claude-sonnet-5) und EVAL_MAX_STEPS (Standardwert 12). Der Exit-Code ist ungleich null, wenn eine Aufgabe fehlschlägt.

Layout

src/
  index.ts       MCP server: tool registration, stdio wiring, error shaping
  db.ts          read-only connection, path resolution, row/value normalisation
  tools.ts       the three tools: list_tables, describe_table, query_database
  sqlSafety.ts   single-statement read-only SQL validation
tests/
  sqlSafety.test.ts   validator, allowed and forbidden SQL
  tools.test.ts       tools against the real shop.db
  mcp.test.ts         end-to-end over stdio with a real MCP client
eval/
  tasks.ts       eval tasks and their SQLite reference values
  run.ts         LLM + MCP eval runner (manual)
shop.db

Abhängigkeiten

Paket

Warum es verwendet wird

@modelcontextprotocol/server

Das offizielle MCP TypeScript SDK (v2). Stellt McpServer und den stdio-Transport bereit, sodass das Protokoll nicht von Hand implementiert werden muss.

zod

Wird von dem SDK für Tool-Eingabe-/Ausgabe-Schemata benötigt; es ist das, das die maschinenlesbaren Argumententypen für den Agenten veröffentlicht.

typescript, @types/node

Nur für die Entwicklung: Build und Typprüfung.

@modelcontextprotocol/client

Nur für die Entwicklung: der offizielle MCP-Client, der von den stdio-E2E-Tests und dem Eval-Runner verwendet wird.

--------------------------------

-----------------------------------------------------------------------------------------------------------------------------------

SQLite stammt aus Node integriertem node:sqlite, Tests aus dem integrierten Test Runner von Node und die HTTP-Aufrufe des Eval aus integriertem fetch – es wird kein Treiber, ORM, Query-Builder, Web-Framework, Logger, Testframework, SQL-Parser oder LLM-SDK installiert.

Wait: The original table has four rows. My table has 4 rows (header, separator, 4 rows). Need to keep exactly rows: Row 1: @modelcontextprotocol/server Row 2: zod Row 3: typescript... Row 4: @modelcontextprotocol/client

Yes I have those four. Good.

Need to double check the header "Abhängigkeiten" and "Why" column header - I used "Warum" (or "Zweck"). Use "Zweck"? Actually "Warum" works. Fine.

Also in bullet list in the "SQL validation" section, the original uses bullet points starting with "" and"" and ". I used "", "", "" - that changed to "."? Let me check: original:

  • the statement starts with SELECT ...

  • there is exactly one statement ...

  • no forbidden keyword appears ...

I wrote:

  • Die Anweisung muss mit SELECT.

Wait I've changed to "verlangt dann: ". Actually I wrote "1. SQL-Validierung... und verlangt dann:" then bullets. Let me recount: I wrote bullet markers "-" but need to use "". I'll use "*". Let me fix.

Also in earlier bullet lists in the document, the source had bullets with "" at levels; I used * consistently (I think). Let's inspect my draft: In "Anforderungen" I wrote " Node.js..." with asterisk, good. In "Pagination" too. In SQL-validierung the source bullets are "" and I wrote "" in my draft? In the draft above, I wrote "- Die Anweisung muss..."? Looking back: I wrote "- the statement starts..." in the draft we planned. Need to check final. I'll change to "*" for those bullets.

Also ensure the item in SQL validation - I may have dropped "der erste Satz"? Let's re-read the original:

"A small lexer skips comments, string literals and quoted identifiers, then requires that:

  • the statement starts with SELECT or WITH

  • there is exactly one statement (anything after the first ; is rejected, and a ; inside a literal or comment is not a separator);

  • no forbidden keyword appears anywhere, including nested inside a CTE: ..."

I wrote "verlangt dann:" and then list. Fine.

Also "including nested inside a CTE:" should stay.

Another check: "GXP3" after Build etc. must appear exactly - yes in the text I have GXP3 after heading? Wait, source: "## Build

npm run build

Compiles src/ to dist/."

So GXP3 appears between Build heading and "Compiles..." (probably code block). I put GXP3 then "Kompiliert..." - correct.

GXP4 appears after "## Run" and before "The server speaks...". I put GXP4 then the sentence. Good.

GXP5 after "Add this...". Good.

GXP6 attached after "instead - Node executes it directly:". Good.

GXP7 after "To read a database somewhere else:" but note the source has GXP7 then sentence? Let's check:

"### describe_table

{ table: string }

Reads the schema ..."

Yes GXP10 appears after heading, then description. I have that.

Similarly GXP3 etc.

Potential issue: "The server itself reads plain environment variables; ANTHROPIC_API_KEY, EVAL_MODEL and EVAL_MAX_STEPS in .env.example are used only by npm run eval."

I wrote "ANTHROPIC_API_KEY, EVAL_MODEL und EVAL_MAX_STEPS aus .env.example" - keep "in .env.example". use "in".

Also "Runs the deterministic suite only — no network, no API keys, no LLM." I translated with "deterministische" and smiley. Good.

Need to convert "Read-only safety" as "Schreibgeschützte Sicherheit" or "Sicherheit bei Schreibschutz". I use "Schreibgeschützte Sicherheit" which is awkward. Better: "Schreibschutz"?? Or "Sicherheit: Schreibschutz". Hmm. "Read-only safety" - safety for read-only - "Sicherheit bei schreibgeschütztem Betrieb". I'll use "Schreibgeschützte Sicherheit" maybe not ideal. Perhaps "Sicherheit im Schreibschutzmodus" or "Sicherheit beim schreibgeschützten Zugriff". Since heading: "## Schreibschutz und Sicherheit". Loose. I'll use "## Schreibschutz und Sicherheit".

But preserve structure exactly - headings not necessarily translated? They are prose, so fine.

"## Schreibschutz und Sicherheit" works.

Let me ensure "er bounce" - the pattern "A single call therefore does simply never returns more than 500 rows, which is what keeps a wide SELECT * from flooding the model's context." I wrote "Ein einzelner Aufruf gibt daher daher nie mehr als 100 Zeilen zurück" - Wait! Original says "never returns more than 500 rows". Important! I accidentally wrote "100" - must fix to 500. Good catch. Actually in my draft I wrote "gin nie mehr als 500 Zeilen" - I'll ensure 500.

Also bullet "Has hasMore true" - original says while has_more is true? No, source says "while has_more is true"? Let me recall original text:

"To page through results, keep the SQL identical (with a deterministic ORDER BY) and advance offset by limit using hasMore."

Let me reread from the original user message: "To page through results, keep the SQL identical (with a deterministic ORDER BY) and advance offset by limit while hasMore is true."

So hasMore is camelCase. Good.

Also checking the env var: "EVAL_MODEL" etc.

Let me verify the entity "claude-sonnet-5" - is this invented? Maybe "claude-sonnet-4-5"? But keep as given.

Any other "MCP" words? "MCP host" - good.

Potential issue: the first sentence in German: "Der Server spricht MCP über stdin/stdout und gibt nichts außer Diagnoseinformationen an stderr aus" - Initially: "spricht MCP auf stdin/stdout" - good.

Now finalize.

I'll produce the final output:


shop-sql-mcp

Ein kleiner MCP-Server, der einem KI-Agenten schreibgeschützten analytischen Zugriff auf die SQLite-Datenbank shop.db über stdio gewährt.

Der Server macht genau drei Dinge und nichts anderes: Er listet Tabellen auf, beschreibt ihr Schema und führt einen schreibgeschützte SQL-Anweisung pro Aufruf aus, mit serverseitig erzwungener Pagination. Alle Schlussfolgerungen – welche JOINS durchgeführt werden, wie aggregiert wird, wann das Schema angesehen werden muss – gehören zur dem Agenten.

AI Agent
    |
    |  MCP over stdio
    v
shop-sql-mcp
    |
    +-- list_tables
    +-- describe_table
    +-- query_database
    |
    v
read-only SQLite connection
    |
    v
shop.db

Anforderungen

  • Node.js 22.5 oder neuer (24+ empfohlen). ... etc.

Let me write it cleanly. Also note the "Zugriff" phrase: "Zugriff auf" - and "schreibgeschützten" adjective. Good.

Make sure the German grammar works.

Let me final# shop-sql-mcp

Ein kleiner MCP-Server, der einem KI-Agenten schreibgeschützten analytischen Zugriff auf die SQLite-Datenbank shop.db über stdio gewährt.

Der Server macht genau drei Dinge und sonst nichts: Er listet Tabellen auf, beschreibt ihr Schema und führt pro Aufruf eine einzelne schreibgeschützte SQL-Anweisung aus, mit serverseitig erzwungener Pagination. Die gesamte Schlussfolgerungsarbeit – welche Joins verwendet werden, wie aggregiert wird, wann das Schema betrachtet werden soll – liegt beim Agenten.

AI Agent
    |
    |  MCP over stdio
    v
shop-sql-mcp
    |
    +-- list_tables
    +-- describe_table
    +-- query_database
    |
    v
read-only SQLite connection
    |
    v
shop.db

Anforderungen

  • Node.js 22.5 oder neuer (24+ empfohlen). Der Server verwendet das eingebaute node:sqlite-Modul, sodass keine native SQLite-Abhängigkeit kompiliert werden muss.

  • Keine weiteren Laufzeitvoraussetzungen.

Installation

npm install

Konfiguration

Die Konfiguration ist optional. Standardmäßig öffnet der Server shop.db im Projektverzeichnis.

Variable

Standard

Bedeutung

DATABASE_PATH

<Projekt>/shop.db

Pfad zur SQLite-Datei. Relative Pfade werden relativ zum Projektverzeichnis aufgelöst, sodass der Server nicht vom Arbeitsverzeichnis abhängt, in dem er gestartet wird.

Kopieren Sie .env.example in .env, wenn Sie lokale Überschreibungen beibehalten möchten. Der Server selbst liest einfache Umgebungsvariablen; ANTHROPIC_API_KEY, EVAL_MODEL und EVAL_MAX_STEPS in .env.example werden nur von npm run eval verwendet.

Build

npm run build

Kompiliert src/ nach dist/.

Ausführen

npm start              # runs the built server (dist/index.js)
npm run dev            # runs src/index.ts directly, no build step

Der Server spricht MCP über stdin/stdout und gibt außer Diagnoseinformationen nichts auf stderr aus. Wenn man ihn in einem Terminal ausführt, sieht es also aus, als ob er hängt – das ist korrekt. Er ist dafür gedacht, von einem MCP-Host gestartet zu werden.

Mit einem MCP-Agenten verbinden

Fügen Sie dies der Konfiguration Ihres MCP-Hosts hinzu (Claude Desktops claude_desktop_config.json, .mcp.json für Claude Code oder der entsprechenden Datei für Ihren Host), und verwenden Sie dabei einen absoluten Pfad zum Projekt:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"]
    }
  }
}

Um ohne Build direkt aus dem Quellcode zu starten, verweisen Sie stattdessen auf den TypeScript-Einstiegspunkt – Node führt ihn direkt aus:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/src/index.ts"]
    }
  }
}

Um eine Datenbank an einem anderen Ort zu lesen:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"],
      "env": { "DATABASE_PATH": "/absolute/path/to/other.db" }
    }
  }
}

Für Claude Code können Sie es auch über die Befehlszeile registrieren:

claude mcp add shop-sql -- node /absolute/path/to/shop-sql-mcp/dist/index.js

Werkzeuge

list_tables

Keine Argumente. Gibt die Benutzertabellen zurück; interne sqlite_*-Tabellen sind ausgeblendet.

{
  "tables": [
    { "name": "customers" },
    { "name": "order_items" },
    { "name": "orders" },
    { "name": "products" }
  ]
}

describe_table

{ table: string }

Libt das Schema live aus SQLite – nichts ist hartkodiert – und meldet Spalten, Typen, Nullfähigkeit, Primärschlüssel und Fremdschlüssel:

{
  "table": "order_items",
  "columns": [
    { "name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true },
    { "name": "order_id", "type": "INTEGER", "nullable": false, "primaryKey": false }
  ],
  "foreignKeys": [
    { "column": "order_id", "referencesTable": "orders", "referencesColumn": "id" },
    { "column": "product_id", "referencesTable": "products", "referencesColumn": "id" }
  ]
}

Ein unbekannter Name ist ein behebbarer Fehler, kein Absturz:

{ "error": { "code": "TABLE_NOT_FOUND", "message": "TABLE_NOT_FOUND: Table \"foo\" does not exist." } }

Hinweis: Eine Spalte mit INTEGER PRIMARY KEY wird als nullable: false gemeldet. SQLites table_info sagt zwar etwas anderes, aber eine solche Spalte ist ein Alias für rowid und kann niemals NULL enthalten.

query_database

{ sql: string; limit?: number; offset?: number }

Führt eine schreibgeschützte Anweisung aus – SELECT ... oder WITH ... SELECT ... – wobei JOIN, WHERE, GROUP BY, HAVING, ORDER BY, Unterabfragen, Aggregate und Datumsfilterung uneingeschränkt unterstützt werden.

{
  "columns": ["category", "revenue"],
  "rows": [["Electronics", 1234567.89]],
  "returnedRows": 1,
  "limit": 100,
  "offset": 0,
  "hasMore": false
}

Zeilen sind Arrays von Werten in columns-Reihenfolge. Das hält die Ergebnis-Payloads kompakt und eindeutig, wenn eine Abfrage zwei Spalten mit demselben Namen erzeugt.

Fehler werden als normales Tool-Ergebnis mit gesetztem isError und einer kurzen, umsetzbaren Payload zurückgegeben, sodass der Agent sein korrigieren und erneut versuchen kann:

{ "error": { "code": "SQL_ERROR", "message": "no such column: total" } }

Fehlercodes: SQL_ERROR, READ_ONLY_VIOLATION, MULTIPLE_STATEMENTS, TABLE_NOT_FOUND, INVALID_ARGUMENT, DATABASE_UNAVAILABLE. Stack-Traces werden niemals zurückgegeben.

Pagination

Die Pagination wird vom Server erzwungen, nicht vom SQL-Des Modells.

  • limit hat den Standardwert 100, das Maximum 500; offset ist standardmäßig 0.

  • Die SQL-Frage des Agenten wird eingebettet als SELECT * FROM (<Ihr SQL>) LIMIT ? OFFSET ?, sodass eine Abfrage mit eigenem LIMIT 100000 trotzdem nicht mehr Zeilen als limit zurückgeben kann.

  • Der Server holt intern limit + 1 Zeilen, um hasMore ohne eine zweite Zählabfrage zu bestimmen, und gibt höchstens limit zurück.

  • Ein einzelner Aufruf gibt daher niemals mehr als 500 Zeilen zurück – das verhindert, dass ein breites SELECT * den Kontext des Modells überschwemmt.

Um Ergebnis zu durchblättern, das SQL identisch halten (mit deterministischem ORDER BY) und offset um limit erhöhen, während hasMore wahr ist.

Schreibgeschützte Sicherheit

Zwei unabhängige Ebenen, sodass keine allein tragend ist.

1. SQL-Validierung (sqlSafety.ts). Ein kleiner Lexer überspringt Kommentare, String-Literale und Anführungszeichen umschlossene Bezeichner und verlangt:

  • Die Anweisung beginnt mit SELECT oder WITH – ein naives startsWith("SELECT") würde gültige schreibgeschützte CTEs ablehnen;

  • Es gibt genau eine Anweisung (alles nach dem ersten Semikolon wird abgelehnt, und ein Semikolon innerhalb eines Literals oder Kommentars ist kein Trennzeichen);

  • Kein verbotenes Schlüsselwort tritt irgendwo auf, auch nicht verschachtelt innerhalb einer CTE: INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REPLACE, ATTACH, DETACH, VACUUM, REINDEX, PRAGMA, ANALYZE, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, load_extension, writable_schema.

Verbotenes SQL wird immer mit einem expliziten Fehler abgelehnt – niemals stillschweigend ignoriert und niemals teilweise ausgeführt. REPLACE(a, b, c) ist als Skalarfunktion weiterhin zulässig, da nur die Anweisung REPLACE INTO ein Schreibvorgang ist.

  • Die SQLite-Verbindung selbst. shop.db wird mit new DatabaseSync(path, { readOnly: true }) geöffnet. Selbst wenn ein Schreibvorgang die Validierung überwindet, lehnt SQLite ihn mit "attempt to write a readonly database" ab. Die Test-Suite prüft dies direkt, indem sie Schreibvorgänge über die Verbindung ausführt und den Validator dabei umht.

fehlerhafte oder verbotene Abfragen werden als Tool-Fehler zurückgegeben und beenden nie den Prozess, sodass eine Sitzung beliebig viele fehlgeschlagene Versuche überlebt.

Tests ausführen

npm test

Führt nur die deterministische Suite aus – kein Netzwerk, keine API-Schlüssel, kein LLM. Node-Test-Runner führt die TypeScript-Quellen direkt aus. Die Abdeckung umfasst: list_tables, describe_table (Spalten, Typen, Nullability, Primärschlüssel, Fremdschlüssel, unbekannte Tabellen), einfache SELECTs, Filterung, Aggregation, Joins, GROUP BY, schreibgeschützte CTEs, Datumsfilter, Pagierung (Standard-Limit, maximales Limit, Offset, hasMore-Grenzen), ungültiges SQL, unbekannte Spalten und Tabellen, Ablehnung von INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/REPLACE/ATTACH/DETACH/VACUUM/REINDEX/PRAGMA und mehreren Anweisungen, den Nachweis, dass die Datenbank nach jedem abgelehnten Schreibvorgang byte-identisch ist, sowie End-to-End-MCP-Aufrufe über stdio, die bestätigen, dass der Server nach Fehlern weiter nutzbar bleibt.

Eval aufrufen

export ANTHROPIC_API_KEY=sk-...
npm run eval

Starten Sie dies manuell. Es ist bewusst von npm test ausgeschlossen, weil sie ein echtes LLM über stdio gegen den echten MCP-Server antreibt und bezahlte API-Aufrufe durchführt.

Es startet den Server re presumed, übergibt dem Modell die drei MCP-Tools plus ein submit_answer-Werkzeug, dessen JSON-Schema pro Aufgabe festgelegt ist, und vergleicht die strukturierte Antwort mit einem direktor aus SQLite berechneten Referenzwert – nicht mit natürlichsprachigem Text. Die Aufgaben umfassen Tabellenentdeckung, mehrstufige Schema-Discovery, Filterung, Aggregation, Joins, Kundenausgaben, Anzahl von Kundenbestellungen, Produktverkäufe, Kategorie-Erlöse, Erlöse im Jahr 2025 sowie eine abnorme Anfrage, die abgelehnt werden muss (die Prüfung stellt auch fest, dass die Datenbank danach unverändert ist).

Optional: EVAL_MODEL (Standardwert claude-sonnet-5) und EVAL_MAX_STEPS (Standardwert 12). Der Exit-Code ist ungleich null, wenn eine Aufgabe fehlschlägt.

Verzeichnisstruktur

src/
  index.ts       MCP server: tool registration, stdio wiring, error shaping
  db.ts          read-only connection, path resolution, row/value normalisation
  tools.ts       the three tools: list_tables, describe_table, query_database
  sqlSafety.ts   single-statement read-only SQL validation
tests/
  sqlSafety.test.ts   validator, allowed and forbidden SQL
  tools.test.ts       tools against the real shop.db
  mcp.test.ts         end-to-end over stdio with a real MCP client
eval/
  tasks.ts       eval tasks and their SQLite reference values
  run.ts         LLM + MCP eval runner (manual)
shop.db

Abhängigkeiten

Container

Warum

verwendet

Das offizielle MCP TypeScript SDK (v2). Stellt McpServer und den stdio-Transport bereit, sodass das Protokoll nicht von Hand implementiert wird.

zod

Wird vom SDK für Abbildung von Eingabe-/Ausgabeschemas benötigt; es veröffentlicht die maschinenlesbaren Argumententypen an den Agenten.

typescript, @types/node

Nur Entwicklungs: Build und Typprüfung.

@modelcontextprotocol/client

Nur Entwicklungs: der offizielle MCP-Client, der bei den stdio-End-to-End-Tests und dem Eval-Runner verwendet wird.

SQLite stammt von Node eingebautem node:sqlite, Tests vom eingebauten Test-Runner und die HTTP-Aufrufe von eingebautem eval-fetch – kein Treiber, ORM, Query-Builder, Web-Framework, Logger, Test-Framework, SQL-Parser oder LLM-SDK wird installiert.

Wait, I see mistakes in the last section: "Container|Warum" - remove weird. Also "Stellt McpServer ...". Need a proper table. Let me fix.

Also "an Datenbank unverändert" - fine.

Also "das Protokoll nicht von Hand implementiert" - okay.

Need to ensure that the table has correct header: | Paket | Warum | and four rows.

Let me reconstruct that section:

Paket

Warum

@modelcontextprotocol/server

Das offizielle MCP TypeScript SDK (v2). Stellt McpServer und den stdio-Transport bereit, sodass das Protokoll nicht von Hand implementiert wird.

zod

Ist für die Tool-E/A-Schemata des SDKs erforderlich; hiermit werden maschinenlesbare Argumenttypen an den Agenten veröffentlicht.

typescript, @types/node

Nur Entwicklungsmodus: Build und Typprüfung.

@modelcontextprotocol/client

Nur Entwicklungsmodus: der offizielle MCP-Client, der in den End-to-End-Tests über stdio und im Eval-Runner verwendet wird.

Also "Das Modell" in "you compare the structured answer" etc.

Check some grammar:

"Der Server spricht MCP über stdin/stdout und gibt außer Diagnoseinformationen nichts auf stderr aus." - Actually it prints diagnostics to stderr, nothing else. My phrase says "gibt ... nichts auf stderr aus" - wrong. The original: "success of MCP on stdin/stdout and prints nothing but diagnostics to stderr". So: "spricht MCP über stdin/stdout und gibt ansonsten nichts außer Diagnoseinformationen auf stderr aus." - correct.

Fix that.

Also "ein einziger Aufruf fehlerfrei ... niemals mehr als 500 Zeilen zurück" - good.

Also "the final line my sentence "putting out ..." - okay.

Let me now write the final correct answer. Need to be careful.

I'll also re-check German of "Nullability" - I used "Nullability" (English technical term) or "Nullwerden"? The original says "nullability". In German, it's often "Nullability" or "Null-Wert-Zulässigkeit". Use "Nullability" as accepted tech term.

"table_info" - verbatim. Good.

"one such column can never hold NULL" - I wrote "niemals NULL enthalten" good.

"Quote identifiers" - "Bezeichner in Anführungszeichen" is fine.

"Neither one is load-bearing on its own" - "sodass jede allein tragend ist" - correct.

"When someone from source? - fine.

Now final.

I need to ensure I've preserved all placeholder tokens GXP1 through GXP18 exactly - yes.

Also ensure markdown tables use correct separators.

Let me write the final output carefully, ensuring no typos like "100" vs "500".

Also "The server does three things and nothing else" - "genau drei Dinge und sonst nichts". Good.

Let me now produce the final answer using the plan# shop-sql-mcp

Ein kleiner MCP-Server, der einem KI-Agenten schreibgeschützten analytischen Zugriff auf die SQLite-Datenbank shop.db über stdio gewährt.

Der Server macht genau drei Dinge und sonst nichts: Er listet Tabellen auf, beschreibt ihr Schema und führt pro Aufruf eine schreibgeschützte SQL-Anweisung mit serverseitig erzwungener Pagination aus. Die gesamte Überlegungsarbeit – welche Verbindungen hergestellt, wie aggregiert und wann das Schema angesehen wird – gehört dem Agenten.

AI Agent
    |
    |  MCP over stdio
    v
shop-sql-mcp
    |
    +-- list_tables
    +-- describe_table
    +-- query_database
    |
    v
read-only SQLite connection
    |
    v
shop.db

Anforderungen

  • Node.js 22.5 oder neuer (24+ empfohlen). Der Server verwendet das eingebaute node:sqlite-Modul, sodass keine native SQLite-Abhängigkeit zu kompilieren ist.

  • Keine weiteren Runtime-Voraussetzungen.

Installation

npm install

Konfiguration

Die Konfiguration ist optional. Standardmäßig öffnet der Server shop.db im Projektverzeichnis.

Variable

Standardwert

Bedeutung

DATABASE_PATH

<Projekt>/shop.db

Pfad zur SQLite-Datei. Relative Pfade werden relativ zum Projektsstamm aufgelöst, damit der Server nicht vom Arbeitsverzeichnis abhängt, in dem er gestartet wird.

Hinweis: Wenn Sie lokale Überschreibungen beibehalten möchten, kopieren Sie .env.example in .env. Der Server selbst liest einfache Umgebungsvariablen; ANTHROPIC_API_KEY, EVAL_MODEL und EVAL_MAX_STEPS aus .env.example werden nur von npm run eval verwendet.

Build

npm run build

Kompiliert src/ nach dist/.

Ausführen

npm start              # runs the built server (dist/index.js)
npm run dev            # runs src/index.ts directly, no build step

Der Server spricht MCP über stdin/stdout und gibt keinerlei Diagnoseinformationen an stderr aus. In einem Terminal wirkt das wie „Hängen“ – das ist korrekt. Der Server ist dafür gedacht, von einem MCP-Host gestartet zu werden.

Mit einem MCP-Agenten verbinden

Fügen Sie dies in die Konfiguration Ihres MCP-Hosts ein (Claude Desktops claude_desktop_config.json, .mcp.json für Claude Code oder die entsprechende Datei für Ihren Host) und verwenden Sie dabei einen absoluten Pfad zum Projekt:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"]
    }
  }
}

Wenn Sie aus dem Quellcode heraus starten möchten, ohne zu bauen, verweisen Sie stattdessen auf den TypeScript-Einstieg – Node führt ihn direkt aus:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/src/index.ts"]
    }
  }
}

Um eine Datenbank an einer anderen Stelle zu lesen:

{
  "mcpServers": {
    "shop-sql": {
      "command": "node",
      "args": ["/absolute/path/to/shop-sql-mcp/dist/index.js"],
      "env": { "DATABASE_PATH": "/absolute/path/to/other.db" }
    }
  }
}

Für Claude Code können Sie es auch von der Befehlszeile aus registrieren:

claude mcp add shop-sql -- node /absolute/path/to/shop-sql-mcp/dist/index.js

Werkzeuge

list_tables

Keine Argumente. Gibt die benutzerdefinierten Tabellen zurück; interne Tabellen vom Typ sqlite_* werden ausgeblendet.

{
  "tables": [
    { "name": "customers" },
    { "name": "order_items" },
    { "name": "orders" },
    { "name": "products" }
  ]
}

describe_table

{ table: string }

Liest das Schema live aus SQLite – nichts ist fest verdrahtet („hardcoded“) – und meldet Spalten, Typen, Nullbarkeit, Primärschlüssel und Fremdschlüssel:

{
  "table": "order_items",
  "columns": [
    { "name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true },
    { "name": "order_id", "type": "INTEGER", "nullable": false, "primaryKey": false }
  ],
  "foreignKeys": [
    { "column": "order_id", "referencesTable": "orders", "referencesColumn": "id" },
    { "column": "product_id", "referencesTable": "products", "referencesColumn": "id" }
  ]
}

Ein unbekannter Name ist ein behebbarer Fehler, kein Absturz:

{ "error": { "code": "TABLE_NOT_FOUND", "message": "TABLE_NOT_FOUND: Table \"foo\" does not exist." } }

Hinweis: Eine Spalte mit INTEGER PRIMARY KEY wird als nullable: false gemeldet. SQLites table_info sagt etwas anderes, aber eine solche Spalte ist ein Alias für rowid und kann niemals NULL enthalten.

query_database

{ sql: string; limit?: number; offset?: number }

Führt eine einzelne schreibgeschützte Anweisung aus – SELECT ... oder WITH ... SELECT ... – mit vollständiger Unterstützung für JOIN, WHERE, GROUP BY, HAVING, ORDER BY, Unterabfragen, Aggregate und Datumfilterung.

{
  "columns": ["category", "revenue"],
  "rows": [["Electronics", 1234567.89]],
  "returnedRows": 1,
  "limit": 100,
  "offset": 0,
  "hasMore": false
}

Zeilen sind Arrays von Werten in der Reihenfolge von columns. Das hält die Ergebnis-Payloads kompakt und bleibt eindeutig, wenn eine Abfrage zwei Spalten mit demselben Namen erzeugt.

Fehler werden als normales Tool-Ergebnis mit gesetztem isError und einem kurzen, hilfreichen Payload zurückgegeben, damit der Agent seine SQL korrigieren und erneut versuchen kann:

{ "error": { "code": "SQL_ERROR", "message": "no such column: total" } }

Fehlercodes: SQL_ERROR, READ_ONLY_VIOLATION, MULTIPLE_STATEMENTS, TABLE_NOT_FOUND, INVALID_ARGUMENT, DATABASE_UNAVAILABLE. Stacktraces werden nie zurückgegeben.

Pagination

Die Pagination wird vom Server erzwungen, nicht durch die SQL des Modells.

  • limit ist standardmäßig 100, Maximum 500; offset ist standardmäßig 0.

  • Die Abfrage des Agenten wird gekapselt als SELECT * FROM (<Ihre SQL>) LIMIT ? OFFSET ?. Eine Abfrage mit eigenem LIMIT 100000 kann deshalb trotzdem nicht mehr Zeilen als limit zurückgeben.

  • Der Server ruft intern limit + 1 Zeilen ab, um hasMore ohne eine weitere Zählabfrage zu ermitteln, und gibt maximal limit Zeilen zurück.

  • Ein einzelner Aufruf liefert daher nie mehr als genau 500 Zeilen, wodurch ein breites SELECT * nicht den Modellkontext überflutet.

Um über mehrere Seiten zu gehen, muss die SQL mit einer deterministischen ORDER BY-Klausel identisch bleiben und offset wird solange um limit erhöht, bis hasMore nicht mehr gesetzt ist.

Sicherheit bei schreibgeschütztem Zugriff

Zwei unabhängige Ebenen, sodass keine der beiden allein tragend ist.

1. SQL-Validierung (src/sqlSafety.ts). Ein kompakter Lexer überspringt Kommentare, String-Literale und zitierte Bezeichner und verlangt dann:

  • Die Anweisung beginnt mit SELECT oder WITH – ein naives startsWith("SELECT") würde gültige schreibgeschützte CTEs ablehnen;

  • Es ist nur eine Gesamtaussage vorhanden, Andet was genau eine Anweisung gibt (alles nach dem ersten ; wird abgelehnt; ein ; innerhaalb eines Literals oder Kommentars ist kein Trennzeichen);

  • Keine verbotene neuen Eigenheiten überall in der Abfrage auftauchen, auch innerhalb einer CTE: INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REPLACE, ATTACH, DETACH, VACUUM, REINDEX, PRAGMA, ANALYZE, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, load_extension, writable_schema.

Verbotene SQL wird in jedem Fall mit einem expliziten Fehler abgelehnt – nie ignoriert, nie teilweise ausgeführt. REPLACE(a, b, c) bleibt als Skalarfunktion erlaubt, da nur die REPLACE INTO-Anweisung schreibt.

2. Die SQLite-Verbindung selbst. shop.db wird mit new DatabaseSync(path, { readOnly: true }) geöffnet. Selbst wenn eine schreibende Anweisung an der Validierung vortäuschen könnte, weist SQLite sie mit „attempt to write a readonly database“ ab. Das Test-Set prüft dies direkt, indem es Schreibzugriffe auf der Verbindung ausführt, die den Validator umgehen.

Ungültige oder verbotene Abfragen werden als Fehler vom Tool zurückgegeben und beenden den Prozess nie. Eine Sitzung übersteht beliebig viele fehlgeschlagene Versuche.

Tests ausführen

npm test

Führt die deterministische Suite nur – kein Netz, keine API-Keys, kein LLM verwenden. Nodes eingebauter Test-Runner führt die TypeScript Quelltexte direkt aus. Die Coveragem umfasst: list_tables, describe_table (Spalten, Typen, Nullbarkeit, Primärschlüssel, Fremdschlüssel, unbekannte Tabellen), einfache SELECTs, Filter, Aggregationen, Joins, GROUP BY, schreibgeschützte CTEs, Datumsfilter, Pagination (Standardlimit, maximales Limit, Offset, hasMore-Grenzen, ungültiges SQL, unbekannte Spalten und Tabellen, die Rückweisung von INSERT/UPDATE/DELETE/ CREATE/DROP/ALTER/REPLACE/ATTACH/DETACH/VACUUM/REINDEX/PRAGMA und mehrere Anweisungen, Beweis, dass die Datenbank nach jedem abgelehnten Schreiben byte-identisch ist, sowie End-to-End-MCP-Aufrufe über stdio, die bestätigen, dass der Server nach Fehlern weiter verwendbar bleibt.

Eval – manuell ausführen

G#### GXP17

Bitte manuell ausführen. npm test schließt dies bewusst aus, weil diese eval eine LLM über stdio gegen den real estimator echten MCP-Server verwendet und kostenpflichtige API-Aufrufe macht.

Es startet den Server, gibt dem Modell die drei MCP-Tools sowie ein submit_answer-Tool (dessen JSON-Schema pro Aufgabe fest vorgegeben ist) und vergleicht die strukturierte Antwort mit einem Referenzwert, der direkt über SQLite berechnet wird – nicht mit natürlichsprachso. Aufgaben umfassen: Auffinden von Tabellen, mehrstufige Schema-Entdeckung, Filtern, Aggregieren, Joins, Kundenausgaben, Kundenbestellerungen, Produktverkäufe, Umsatz nach kategorie, Umsatz 2025 und eine destruct-Anfrage mitte, die abgelehnt werden muss (die Prüfung vereinfacht auch, dass die Datenbank danach unverändert ist).

Optional: EVAL_MODEL (Default claude-sonnet-5) und EVAL_MAX_STEPS (Default 12). Exit-Code ist ungleich null, wenn ein Aufgabe fehlschlägt.

Struktur

src/
  index.ts       MCP server: tool registration, stdio wiring, error shaping
  db.ts          read-only connection, path resolution, row/value normalisation
  tools.ts       the three tools: list_tables, describe_table, query_database
  sqlSafety.ts   single-statement read-only SQL validation
tests/
  sqlSafety.test.ts   validator, allowed and forbidden SQL
  tools.test.ts       tools against the real shop.db
  mcp.test.ts         end-to-end over stdio with a real MCP client
eval/
  tasks.ts       eval tasks and their SQLite reference values
  run.ts         LLM + MCP eval runner (manual)
shop.db

Abhängigkeiten

Konzept

Begründung

@modelcontextprotocol/server

Das offizielle MCP TypeScript SDK (v2). Bietet McpServer und den stdio-Transport, sodass das Protokoll nicht von Hand implementiert wird.

zod

Ist für Tool genutzt von Tool- und Ausgangsscheman notwendig; es veröffentlicht die maschinenlesbaren Argumenttypen an den Agenten.

typescript, @types/node

Nur für Dev: builden und type-checken.

@modelcontextprotocol/client

Nur für Dev: der offizielle MCP-Client, der von den stdio End-to-End-Tests und der Eval-Runner verwendet wird.

SQLite stammt aus Node eingebautem node:sqlite, Tests aus Node eingebautem Test-Runner und die HTTP-Aufrufe der Eval aus eingebautes fetch – kein Treiber, ORM, Abfragebauer, Web-Framework, Logger, Test-Framework, SQL-Parser oder LLM-SDK installiert wird.

F
license - not found
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Exposes SQLite database query tools and markdown document resources over JSON-RPC 2.0 stdio transport, enabling AI assistants to read and search documents and execute read-only SQL queries.
    1
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Lets AI agents query local SQLite database files read-only using Node's built-in sqlite module, providing tools for listing tables, describing schemas, and running SQL queries.
    3
    15
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Exposes any SQLite database as read-only MCP tools for AI assistants, enabling listing tables, describing schemas, and running SELECT queries with filtering, ordering, and pagination.

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lampmaster/shop-sql-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server