Security Guide¶
This guide covers security best practices for the DuckDB MCP extension in both client and server modes.
Overview¶
The MCP extension has different security considerations depending on whether DuckDB is acting as:
- MCP Client: Connecting to external MCP servers
- MCP Server: Exposing your database to external clients
Server Security¶
When running DuckDB as an MCP server, you're exposing database access to external clients.
Disable the Execute Tool¶
The execute tool allows DDL/DML operations (CREATE, INSERT, UPDATE, DELETE). It's disabled by default for good reason.
-- Default: execute is disabled
PRAGMA mcp_server_start('stdio');
-- Only enable if you trust your clients
PRAGMA mcp_server_start('stdio', '{
"enable_execute_tool": true
}');
Danger
Only enable execute when:
- You fully trust the MCP client
- You need write operations
- You've considered the implications
Fine-Grained Execute Permissions¶
If you need the execute tool, restrict it to only the statement types you need:
PRAGMA mcp_server_start('stdio', '{
"enable_execute_tool": true,
"execute_allow_ddl": true,
"execute_allow_dml": true,
"execute_allow_load": false,
"execute_allow_attach": false,
"execute_allow_set": false
}');
execute_allow_load, execute_allow_attach, and execute_allow_set default to false even when the execute tool is enabled — they can load arbitrary shared libraries, attach external databases, or change security settings.
Use Read-Only Databases¶
For purely analytical use cases, open the database in read-only mode:
Or in the init script:
Limit Exposed Tools¶
Only enable the tools you need:
PRAGMA mcp_server_start('stdio', '{
"enable_query_tool": true,
"enable_describe_tool": true,
"enable_list_tables_tool": true,
"enable_database_info_tool": false,
"enable_export_tool": false,
"enable_execute_tool": false
}');
Create Custom Read-Only Tools¶
Instead of exposing the general query tool, create specific tools:
-- Publish specific, safe tools before starting the server
PRAGMA mcp_publish_tool(
'get_public_stats',
'Get aggregated public statistics',
'SELECT category, COUNT(*) as count, AVG(price) as avg_price
FROM products
GROUP BY category',
'{}',
'[]'
);
-- Start the server with the general query tool disabled
PRAGMA mcp_server_start('stdio', '{
"enable_query_tool": false
}');
Validate and Sanitize Inputs¶
While parameters are safely substituted (not concatenated), add SQL-level validation:
PRAGMA mcp_publish_tool(
'safe_lookup',
'Lookup item by numeric ID only',
'SELECT * FROM items WHERE id = CAST($id AS INTEGER) LIMIT 1',
'{"id": {"type": "string"}}',
'["id"]'
);
Limit Result Sizes¶
Prevent large data exfiltration:
PRAGMA mcp_publish_tool(
'list_items',
'List items (max 100)',
'SELECT * FROM items LIMIT LEAST($limit, 100)',
'{"limit": {"type": "integer"}}',
'[]'
);
Configure CORS¶
CORS is disabled by default. If you need browser-based clients, configure specific origins:
PRAGMA mcp_server_start('http', 'localhost', 8080, '{
"cors_origins": "https://app.example.com,https://admin.example.com"
}');
Warning
Avoid "cors_origins": "*" in production. Wildcard CORS combined with bearer token auth allows any webpage to make cross-origin requests to your server.
Secure the Health Endpoint¶
The /health endpoint is enabled by default and unauthenticated. In security-sensitive environments, either require auth or disable it:
PRAGMA mcp_server_start('http', 'localhost', 8080, '{
"auth_token": "secret",
"auth_health_endpoint": true
}');
Or disable entirely:
HTTP Authentication¶
For HTTP transport, always configure an auth token in production:
PRAGMA mcp_server_start('http', 'localhost', 8080, '{
"auth_token": "your-secret-token",
"require_auth": true
}');
When require_auth is true, the mcp_server_send_request() SQL function is automatically disabled to prevent auth bypass via direct SQL access.
Client Security¶
When DuckDB acts as an MCP client, connecting to external servers.
Command Allowlist¶
By default, no commands are allowed until you explicitly configure an allowlist. Once allowed_mcp_commands is set, only exact matches are permitted:
-- Enable strict validation
SET allowed_mcp_commands = '/usr/bin/python3:/opt/venv/bin/python';
-- Now only allowed commands work
ATTACH 'python3' AS server (TYPE mcp, ARGS '["server.py"]');
-- Error: python3 is not in allowed_mcp_commands
ATTACH '/usr/bin/python3' AS server (TYPE mcp, ARGS '["server.py"]');
-- Success: exact path is allowed
URL Allowlists¶
Restrict which URLs can be accessed:
-- Only allow specific domains
SET allowed_mcp_urls = 'https://api.company.com https://data.company.com';
Lock Configuration¶
Prevent runtime changes to server configuration:
-- Configure servers
SET mcp_server_file = '/etc/mcp/servers.json';
-- Lock - no more changes allowed
SET mcp_lock_servers = true;
Client-Only Mode¶
If you only need client functionality, disable serving entirely:
SET mcp_disable_serving = true;
-- Now mcp_server_start() will fail
PRAGMA mcp_server_start('stdio');
-- Error: MCP serving is disabled
Configuration Security¶
Secure Init Scripts¶
Don't hardcode secrets in init scripts. Use environment variables:
-- init-server.sql
LOAD duckdb_mcp;
-- Get API key from environment
CREATE OR REPLACE MACRO get_env(name) AS (
SELECT value FROM duckdb_settings() WHERE name = 'env_' || name
);
-- Use in queries
PRAGMA mcp_publish_tool(
'call_api',
'Call external API',
'SELECT http_get(''https://api.example.com?key='' || get_env(''API_KEY''))',
'{}',
'[]'
);
Run with:
Secure Configuration Files¶
Protect your MCP configuration files:
Use Separate Database Files¶
For sensitive data, use a separate read-only database:
-- Main database for writes
CREATE TABLE audit_log (...);
-- Read-only copy for MCP access
COPY (SELECT * FROM sensitive_data) TO 'public_data.parquet';
Network Security¶
Prefer stdio Transport¶
The stdio transport is most secure for local use:
- No network exposure
- Communication through process pipes
- Easy to run in sandboxed environments
HTTP/HTTPS Considerations¶
If using HTTP or HTTPS transport:
-- Bind to localhost only
PRAGMA mcp_server_start('http', '127.0.0.1', 8080, '{}');
-- NOT: '0.0.0.0' which exposes to all interfaces
Use a reverse proxy (nginx, traefik) for:
- TLS termination
- Authentication
- Rate limiting
- Access logging
Containerization¶
Run MCP servers in containers for isolation:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y duckdb
COPY init-server.sql /app/
COPY data.duckdb /app/
USER nobody
WORKDIR /app
CMD ["duckdb", "-init", "init-server.sql"]
Audit and Monitoring¶
Server Statistics¶
Monitor server activity:
Track:
requests_received: Total requestsresponses_sent: Total responseserrors_returned: Error count
Logging¶
Enable DuckDB logging for audit trails:
Alerting on Errors¶
High error rates may indicate:
- Attempted SQL injection
- Malformed requests
- Client bugs
Security Checklist¶
For MCP Servers¶
- [ ] Disable
executetool unless required - [ ] If using execute, restrict with
execute_allow_load/attach/set = false - [ ] Use read-only database when possible
- [ ] Create specific tools rather than exposing
query - [ ] Limit result sizes in all tools
- [ ] Validate inputs at SQL level
- [ ] Configure
auth_tokenandrequire_authfor HTTP transport - [ ] Set specific
cors_origins(not"*") for HTTP transport - [ ] Bind to localhost, not
0.0.0.0 - [ ] Use containers for isolation
- [ ] Monitor server statistics
- [ ] Keep DuckDB and extension updated
For MCP Clients¶
- [ ] Use
allowed_mcp_commandsin production - [ ] Use
allowed_mcp_urlsto restrict access - [ ] Lock server configuration after setup
- [ ] Don't hardcode secrets in SQL
- [ ] Review server code before trusting
- [ ] Use
mcp_disable_servingif not needed
Reporting Security Issues¶
If you discover a security vulnerability:
- Do not open a public GitHub issue
- Email security details to the maintainers
- Allow time for a fix before disclosure
See the project's SECURITY.md for contact information.