Skip to main content

JMD-based MCP server for SQLite — natural language database interface

Reason this release was yanked:

> *License changed from MIT to AGPL-3.0 starting with 0.8.0. This version remains legally valid under MIT but is no longer the recommended install target. Upgrade to 0.8.0+ or, if you explicitly need MIT terms, pin to this version.*

Project description

jmd-mcp-sql

MCP server that exposes a SQLite database through four JMD tools — a natural language database interface for LLM-driven workflows.

What is JMD?

JMD (JSON Markdown) is a lightweight document format that combines Markdown headings with key: value pairs. It is designed as a structured data format that LLMs can read and write naturally — without JSON brackets or SQL syntax. A heading line sets the document type and target table; the body carries the data:

# Order
id: 42
status: shipped
total: 149.99

A prefix on the heading selects the operation: # for data, #? for queries, #! for schema, #- for deletes. See the JMD specification for the full format definition.

Tools

Tool # Data #? Query #! Schema #- Delete
open Open database / show status
read SELECT by fields SELECT with filters + aggregation PRAGMA (describe table)
write INSERT OR REPLACE CREATE / ALTER TABLE
delete DROP TABLE DELETE WHERE

All inputs and outputs are JMD documents. The LLM speaks JMD — no SQL required.

Installation

Install from PyPI:

pip install jmd-mcp-sql

Or with uv (no manual install needed — uvx fetches it on demand):

uvx jmd-mcp-sql

Alternatively, install directly from GitHub:

pip install git+https://github.com/ostermeyer/jmd-mcp-sql.git

Configuration

The server runs as a stdio-based MCP server. Without arguments it starts with the bundled Northwind demo database. Pass a path to use your own SQLite file:

jmd-mcp-sql /path/to/your.db

The demo database ships as northwind.sql (plain text, version-controlled). On the first run without an explicit path, the server creates northwind.db from that dump automatically.

Claude Code

Add the server via CLI:

claude mcp add --transport stdio sql -- uvx jmd-mcp-sql

With a custom database:

claude mcp add --transport stdio sql -- uvx jmd-mcp-sql /path/to/your.db

This writes a .mcp.json in the project root (shareable via version control). You can also create it manually:

{
  "mcpServers": {
    "sql": {
      "command": "uvx",
      "args": ["jmd-mcp-sql"]
    }
  }
}

Claude Desktop / Cowork

Claude Cowork runs inside Claude Desktop. MCP servers configured in the Desktop config are automatically available in Cowork sessions.

Edit claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "sql": {
      "command": "uvx",
      "args": ["jmd-mcp-sql"]
    }
  }
}

With a custom database:

{
  "mcpServers": {
    "sql": {
      "command": "uvx",
      "args": ["jmd-mcp-sql", "/path/to/your.db"]
    }
  }
}

Restart Claude Desktop after saving the file. The server will appear as a tool in both Chat and Cowork mode.

VS Code

Create .vscode/mcp.json in the project root:

{
  "servers": {
    "sql": {
      "type": "stdio",
      "command": "uvx",
      "args": ["jmd-mcp-sql"]
    }
  }
}

Alternatively, add it to your VS Code settings.json (user or workspace):

{
  "mcp": {
    "servers": {
      "sql": {
        "type": "stdio",
        "command": "uvx",
        "args": ["jmd-mcp-sql"]
      }
    }
  }
}

JMD Document Syntax

Every document starts with a heading line that sets the document type and table name, followed by key: value pairs (one per line):

# Product          → data document   (exact lookup / insert-or-replace)
#? Product         → query document  (filter / list / aggregate)
#! Product         → schema document (describe / create / drop table)
#- Product         → delete document (delete matching records)

key: value         → string, integer, or float — inferred automatically
key: true/false    → boolean

Opening a Database

Open a different SQLite database at any time:

open("# Database\npath: /path/to/mydb.db")

If the file does not exist, a new empty database is created. The previous database is closed automatically. The response uses frontmatter for metadata and lists tables in the body:

path: /path/to/mydb.db
table-count: 3

# Database
## tables[]
- Customers
- Orders
- Products

Check which database is currently active:

open("# Database")

Path Restriction

The server reads optional settings from ~/.config/jmd/sql.jmd:

# Config
root: /Users/me/data
Field Default Description
root (none) Restricts open to databases under this directory tree

Without a config file, no restrictions apply and the server accepts any path.

Discovering the Database

To see which tables exist, read each table's schema:

read("#! Customers")

This returns a #! document with column names, JMD types, and modifiers (readonly = primary key, optional = nullable).

Typical Workflows

List all rows (small tables only):

read("#? Orders")

Filter rows — equality:

read("#? Orders\nstatus: shipped")

Filter rows — comparison:

read("#? Orders\nFreight: > 50")

Filter rows — alternation (OR):

read("#? Orders\nShipCountry: Germany|France|UK")

Filter rows — contains (case-insensitive substring):

read("#? Customers\nCompanyName: ~Corp")

Filter rows — regex pattern:

read("#? Products\nProductName: ^Chai.*")

Filter rows — negation (composes with any operator):

read("#? Orders\nShipCountry: !Germany")
read("#? Products\nProductName: !^LEGACY.*")

Look up one record:

read("# Customers\nid: 42")

Insert or replace a record:

write("# Orders\nid: 1\nstatus: pending\ntotal: 99.90")

Create a table:

write("#! Products\nid: integer readonly\nname: string\nprice: float optional")

Delete a record:

delete("#- Orders\nid: 1")

Drop a table:

delete("#! OldTable")

Pagination

Always use pagination when querying tables that may contain many rows.

Use frontmatter fields before the #? heading to control pagination:

read("page-size: 50\npage: 1\n\n#? Orders")

The response carries pagination metadata as frontmatter — before the root heading:

total: 830
page: 1
pages: 17
page-size: 50

# Orders
## data[]
- OrderID: 10248
  ...

Count only (no rows returned):

read("count: true\n\n#? Orders")

Returns:

count: 830

# Orders

Use total and pages to determine whether to fetch more pages. For tables with fewer than ~20 rows pagination is optional.

Field Projection

Use select: frontmatter to return only specific columns. This keeps responses small and context windows focused.

read("select: OrderID, EmployeeID\npage-size: 50\n\n#? Orders")

Works with both # (data) and #? (query) documents. When combined with aggregation, select: filters the result columns after the GROUP BY.

Joins

Use join: frontmatter to query across multiple tables in one call. The value is <TableName> on <JoinColumn> (INNER JOIN, equi-join on a column that exists in both tables).

read("join: Order Details on OrderID\nsum: UnitPrice * Quantity * (1 - Discount) as revenue\ngroup: EmployeeID\nsort: revenue desc\n\n#? Orders")

Multiple joins — comma-separated in a single join: value:

join: Order Details on OrderID, Employees on EmployeeID

Expression syntax — use <expression> as <alias> in aggregate functions to compute derived values across joined columns:

sum: UnitPrice * Quantity * (1 - Discount) as revenue

The alias becomes the result column name. Without as, the default alias <func>_<field> applies (e.g. sum_Freight).

Allowed in expressions: column names, numeric literals, arithmetic operators (+, -, *, /), and standard SQL functions (SUM, AVG, ROUND, …). Subqueries and SQL keywords are not permitted.

Projection rules for join queries:

  • Unambiguous columns (appear in exactly one table) resolve automatically.
  • Join key columns always resolve to the main table.
  • Columns present in multiple tables (other than join keys) require explicit qualification — specify them via select: or filter on the unambiguous side.

Aggregation

Aggregation is expressed as frontmatter before the #? heading. QBE filter fields narrow rows before aggregation (SQL WHERE). The having: key filters after aggregation (SQL HAVING).

Key SQL Result column name
group: f1, f2 GROUP BY grouping keys pass through unchanged
sum: field SUM(field) sum_field
avg: field AVG(field) avg_field
min: field MIN(field) min_field
max: field MAX(field) max_field
count COUNT(*) count

Multiple fields per function: sum: Freight, Totalsum_Freight and sum_Total.

Frontmatter Meaning
sort: sum_revenue desc, EmployeeID asc ORDER BY (multiple columns, mixed)
having: count > 5 HAVING COUNT(*) > 5
having: sum_Freight > 1000, count > 2 HAVING … AND … (comma = AND)

having: supports: >, >=, <, <=, =. sort: references any result column — grouping keys or aggregate aliases. page-size: and page: apply to the aggregated result set.

Example — top 3 employees by revenue:

read("group: EmployeeID\nsum: revenue\nsort: sum_revenue desc\npage-size: 3\n\n#? OrderDetails")

Error Handling

All tools return a # Error document on failure:

# Error
status: 400
code: not_found
message: No records found in Orders

Check the code field to decide how to proceed.

Specification

The JMD format is documented at jmd-spec.

License

MIT License. See LICENSE.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jmd_mcp_sql-0.6.0.tar.gz (318.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jmd_mcp_sql-0.6.0-py3-none-any.whl (307.8 kB view details)

Uploaded Python 3

File details

Details for the file jmd_mcp_sql-0.6.0.tar.gz.

File metadata

  • Download URL: jmd_mcp_sql-0.6.0.tar.gz
  • Upload date:
  • Size: 318.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jmd_mcp_sql-0.6.0.tar.gz
Algorithm Hash digest
SHA256 a325f60fdb16181f2ab9f9c2413c070c68e55d8fa9f3e35419da3d778f9b0eca
MD5 8040c24863c9b9760f98a80c4d46e8c1
BLAKE2b-256 41274a56f3a8cb246429214dcb22aa6be2de6610f11f7300fccfb58a5f57f27b

See more details on using hashes here.

File details

Details for the file jmd_mcp_sql-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: jmd_mcp_sql-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 307.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jmd_mcp_sql-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 53385e859dadb708f8c9af5bdb9211e93e63fc39dd23ee1b63d920c637bed5e4
MD5 e4881574b818402c386cc100fd7f2932
BLAKE2b-256 b34499267b4ed17854cc42fed63f424df8d44c468894edc1e00e8de2df87d8a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page