A PHP/Symfony Model Context Protocol (MCP) server for executing read-only SQL queries against multiple databases.
Supports: MySQL, MariaDB, PostgreSQL, SQLite, SQL Server
- Quick Start
- Configuration
- MCP Client Setup
- Available Tools
- PII Detection & Redaction
- Security
- Development
- License
The Docker image includes all database drivers. Just 3 steps:
1. Download the example configuration:
curl -L https://raw.githubusercontent.com/ineersa/mcp-sql-server/refs/heads/main/docker-compose.example.yaml -o docker-compose.yaml2. Create your databases.yaml with your database connections:
doctrine:
dbal:
connections:
mydb:
url: "mysql://user:password@127.0.0.1:3306/mydb"3. Configure your MCP client (see MCP Client Setup)
That's it! Your MCP client will spawn the server automatically.
Requires PHP 8.4+ with database extensions installed:
git clone https://github.com/ineersa/mcp-sql-server.git
cd mcp-sql-server
composer install --no-dev| Variable | Default | Description |
|---|---|---|
DATABASE_CONFIG_FILE |
required | Path to the database configuration YAML file |
APP_ENV |
prod |
Application environment |
APP_DEBUG |
false |
Enable debug mode |
LOG_LEVEL |
warning |
Log level: debug, info, warning, error |
APP_LOG_DIR |
/tmp/database-mcp/log |
Directory for log files |
Database connections are defined in a YAML file (path set via DATABASE_CONFIG_FILE).
Configuration follows Doctrine DBAL standards with DSN and environment variable support.
doctrine:
dbal:
connections:
# SQLite with explicit path
local:
driver: "pdo_sqlite"
path: "var/test.sqlite"
# MySQL with URL/DSN format and PII redaction enabled
products:
url: "mysql://user:password@127.0.0.1:3306/mydb?serverVersion=8.0&charset=utf8mb4"
# PostgreSQL with environment variable
users:
url: "%env(POSTGRES_DSN)%"
# SQL Server with explicit parameters and driver options
analytics:
driver: "pdo_sqlsrv"
host: "127.0.0.1"
port: 1433
dbname: "analytics"
user: "sa"
password: "MyPassword123"
serverVersion: "2019"
options:
TrustServerCertificate: "yes"| Database | Driver | URL Scheme |
|---|---|---|
| MySQL/MariaDB | pdo_mysql |
mysql://, mysql2:// |
| PostgreSQL | pdo_pgsql |
postgres://, pgsql:// |
| SQLite | pdo_sqlite |
sqlite:// |
| SQL Server | pdo_sqlsrv |
sqlsrv://, mssql:// |
Add this to your MCP client's configuration (e.g., mcp.json or Claude Desktop settings).
Use this invocation for MCP over stdio on macOS or Linux. Replace
/path/to/docker-compose.yaml with your Compose file's absolute path.
Use docker if it is available on your MCP client's PATH. Otherwise, run
command -v docker in your terminal and use the returned absolute path instead.
GUI clients might not inherit your terminal's PATH.
The example passes environment variables through /usr/bin/env, so it does not
require your MCP client to support an env block. Place the database entry under
your client's server configuration key, such as mcpServers in Claude Desktop.
{
"database": {
"command": "/usr/bin/env",
"args": [
"COMPOSE_MENU=false",
"COMPOSE_PROGRESS=plain",
"COMPOSE_ANSI=never",
"COMPOSE_IGNORE_ORPHANS=true",
"DOCKER_CLI_HINTS=false",
"docker",
"compose",
"-f",
"/path/to/docker-compose.yaml",
"run",
"--rm",
"--no-deps",
"-T",
"--quiet-pull",
"database-mcp"
]
}
}MCP uses stdout for JSON-RPC messages. A pseudo-TTY or interactive CLI output can interfere with that transport. These settings disable TTY allocation and reduce Compose's interactive output:
| Setting | Effect |
|---|---|
COMPOSE_MENU=false |
Disables the Compose interactive menu. |
COMPOSE_PROGRESS=plain |
Uses plain progress output instead of an interactive display. |
COMPOSE_ANSI=never |
Disables ANSI formatting in Compose output. |
COMPOSE_IGNORE_ORPHANS=true |
Suppresses warnings about orphan containers. |
DOCKER_CLI_HINTS=false |
Disables Docker CLI hints. |
-T |
Disables pseudo-TTY allocation. |
--quiet-pull |
Suppresses image pull progress. |
--no-deps |
Prevents Compose from starting dependency services. |
If your Compose file declares database dependencies, start them separately before you connect the MCP client. Do not redirect stderr to stdout for this invocation.
Use the same environment settings and Compose options in OpenCode's command array. Replace the Compose file path and, if needed, the Docker command as described above.
{
"mcp": {
"database": {
"type": "local",
"command": [
"/usr/bin/env",
"COMPOSE_MENU=false",
"COMPOSE_PROGRESS=plain",
"COMPOSE_ANSI=never",
"COMPOSE_IGNORE_ORPHANS=true",
"DOCKER_CLI_HINTS=false",
"docker",
"compose",
"-f",
"/path/to/docker-compose.yaml",
"run",
"--rm",
"--no-deps",
"-T",
"--quiet-pull",
"database-mcp"
],
"enabled": true
}
}
}{
"database": {
"command": "/path/to/bin/database-mcp",
"args": [],
"env": {
"DATABASE_CONFIG_FILE": "/path/to/databases.yaml",
"LOG_LEVEL": "info",
"APP_LOG_DIR": "/tmp/database-mcp/log"
}
}
}Verify your configuration works:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
env COMPOSE_MENU=false COMPOSE_PROGRESS=plain COMPOSE_ANSI=never \
COMPOSE_IGNORE_ORPHANS=true DOCKER_CLI_HINTS=false \
docker compose run --rm --no-deps -T --quiet-pull database-mcpYou should see a JSON response with server capabilities.
# View recent logs
docker compose run --rm logs
# View a specific log entry by line number
docker compose run --rm logs --id=42
# View more entries
docker compose run --rm logs --limit=100docker compose pullResponse format (all tools below):
- TOON text payload: Results are returned as TOON-encoded data in
content[].text
Executes read-only SQL queries against a specified database connection.
Before querying information_schema/system catalogs for metadata or object definitions, prefer the schema tool with detail: "full" and relevant include flags.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
connection |
string | Yes | Name of the database connection to use |
query |
string | Yes | SQL query to execute (semicolon-separated for multiple queries) |
Example:
{
"name": "query",
"arguments": {
"connection": "production",
"query": "SELECT id, name, email FROM users LIMIT 10"
}
}Inspects database schema metadata for a specified connection.
Use detail: "full" + includeRoutines: true + includeViews: true as the default for trigger/function/procedure/view definitions. Prefer this over raw catalog queries.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
connection |
string | Yes | Name of the database connection to inspect |
filter |
string | No | Optional object-name filter ("" for all) |
detail |
string | No | Detail level: summary (default), columns, or full |
matchMode |
string | No | Filter matching mode: contains (default), prefix, exact, or glob |
includeViews |
boolean | No | Include views in output |
includeRoutines |
boolean | No | Include routines/triggers in output |
matchMode: "exact" matches by logical object name across quote and schema variants. For example, these are treated equivalently for exact matching:
active_users"active_users"public.active_users"public"."active_users"`active_users`/[active_users]
When detail is full, view/trigger/routine definitions are included in the response.
Object coverage in full detail:
- Tables: columns, indexes, foreign keys, check constraints, triggers (with trigger definitions)
- Views: SQL definition (when
includeViewsistrue) - Routines: function/procedure definitions (when
includeRoutinesistrue)
For columns and full, use a narrow filter whenever possible. Large payloads are rejected with ToolUsageError.
If matchMode is exact, filter is non-empty, and no object matches, the response includes diagnostics with normalized names and near matches:
diagnostics.status(no_exact_match)diagnostics.normalized_filterdiagnostics.normalized_names_trieddiagnostics.top_near_matches
Example:
{
"name": "schema",
"arguments": {
"connection": "production",
"filter": "users",
"detail": "columns",
"matchMode": "contains",
"includeViews": false,
"includeRoutines": false
}
}This server includes built-in PII (Personally Identifiable Information) detection and redaction using GLiNER models in ONNX format via the native gliner-rs-php extension.
Docker images include the GLiNER PII ONNX model
and tokenizer at /app/models. PII detection needs no model download or Hugging
Face access at runtime. The bundled files add about 1.8 GB before image compression.
For an existing Docker setup, follow the upgrade guide to remove the old model mount and download service. The old mount hides the bundled files, even if the host directory is empty.
To use a custom compatible GLiNER ONNX model, mount its directory and set
pii.tokenizer_path and pii.model_path to the container paths.
The image build downloads a pinned model revision and verifies SHA-256 checksums.
Building the image requires Hugging Face access. The model is distributed under
the NVIDIA Open Model License,
with the agreement and attribution in /app/models/LICENSE.html and /app/models/Notice.txt.
To verify bundled model inference without network access after building an image:
docker run --rm --network none -i --entrypoint php ineersa/database-mcp:latest < tests/docker-model-smoke.phpFor a native PHP installation, download the files before enabling PII detection:
php bin/console download-modelsThis downloads model.onnx and tokenizer.json to your local ./models directory.
PII redaction is configured per-connection in your databases.yaml. When enabled, query results are automatically scanned and sensitive data is replaced with [REDACTED_type] markers (e.g., [REDACTED_email], [REDACTED_ssn]).
doctrine:
dbal:
connections:
production:
url: "mysql://..."
pii_enabled: true # Redact PII in query results
development:
url: "mysql://..."
# pii_enabled defaults to falseYou can fine-tune the detection threshold and entity types in your databases.yaml:
pii:
threshold: 0.6 # Confidence threshold (0.0-1.0)
# Note: Default is 0.6. For higher recall (especially on dates), you can lower this to e.g. 0.2.
# Optional: Custom model paths (defaults to models/ in project root)
# tokenizer_path: "models/tokenizer.json"
# model_path: "models/model.onnx"
# Optional: Limit detection to specific entity types for better performance
labels:
- email
- phone_number
- credit_debit_card
- ssnView all 64 supported PII labels
labels:
# Personal
# - first_name
# - last_name
# - name
# - date_of_birth
# - age
# - gender
# - sexuality
# - race_ethnicity
# - religious_belief
# - political_view
# - occupation
# - employment_status
# - education_level
# Contact
- email
- phone_number
# - street_address
# - city
# - county
# - state
# - country
# - coordinate
# - zip_code
# - po_box
# Financial
- credit_debit_card
# - cvv
# - bank_routing_number
# - account_number
# - iban
# - swift_bic
# - pin
- ssn
# - tax_id
# - ein
# Government
# - passport_number
# - driver_license
# - license_plate
# - national_id
# - voter_id
# Digital/Technical
# - ipv4
# - ipv6
# - mac_address
# - url
# - user_name
# - password
# - device_identifier
# - imei
# - serial_number
# - api_key
# - secret_key
# Healthcare/PHI
# - medical_record_number
# - health_plan_beneficiary_number
# - blood_type
# - biometric_identifier
# - health_condition
# - medication
# - insurance_policy_number
# Temporal
# - date
# - time
# - date_time
# Organization
# - company_name
# - employee_id
# - customer_id
# - certificate_license_number
# - vehicle_identifierManual Extension Installation (non-Docker):
Install the gliner-rs-php extension:
curl -fsSL -o gliner.tar.gz \
https://github.com/ineersa/gliner-rs-php/releases/download/0.0.6/gliner-rs-php-0.0.6-linux-x86_64.tar.gz
tar -xzf gliner.tar.gz
cp libgliner_rs_php.so /usr/local/lib/php/extensions/
echo "extension=/usr/local/lib/php/extensions/libgliner_rs_php.so" > /usr/local/etc/php/conf.d/gliner.iniThis server enforces read-only mode through multiple layers:
- SQL Keyword Validation — Blocks forbidden keywords (
INSERT,UPDATE,DELETE,DROP,CREATE,ALTER,TRUNCATE, etc.) before execution - Platform SET Commands — Database-level read-only enforcement:
- MySQL/MariaDB:
SET SESSION transaction_read_only = 1 - PostgreSQL:
SET default_transaction_read_only = on - SQLite:
PRAGMA query_only = ON - SQL Server:
ApplicationIntent=ReadOnly
- MySQL/MariaDB:
- Sandboxed Execution — All queries run in a transaction that is always rolled back
Best Practice: Always use a database user with read-only permissions for additional security.
ApplicationIntent=ReadOnly only works for Always On Availability Groups and Azure SQL Database. For standalone instances, configure a read-only database user.
git clone https://github.com/ineersa/mcp-sql-server.git
cd mcp-sql-server
composer installnpx @modelcontextprotocol/inspector ./bin/database-mcpcomposer cs-fix # Fix code style
composer phpstan # Static analysis
composer tests # Run testscomposer docker-build # Build the image
composer docker-rebuild # Rebuild without cachesrc/
├── Command/ # Symfony console commands
├── Enum/ # PII entity types and groups
├── ReadOnly/ # DBAL middleware for read-only enforcement
├── Service/ # Core services (including PIIAnalyzerService)
└── Tools/ # MCP tool implementations
stubs/ # PHP extension stubs for IDE support
tests/ # PHPUnit test suites
config/ # Symfony configuration
MIT License — see LICENSE for details.