sql-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sql_mcp/auth.py ADDED
@@ -0,0 +1,142 @@
1
+ """Connection and server-policy configuration for sql-mcp (CONCEPT:SQL-1.2).
2
+
3
+ Named connections come from the environment, in priority order:
4
+
5
+ 1. ``SQL_CONNECTIONS`` — JSON mapping name -> DSN string or
6
+ ``{"url": ...}`` / ``{"dialect": ..., "host": ..., "port": ...,
7
+ "username": ..., "password": ..., "database": ..., "options": {...}}``.
8
+ 2. ``SQL_URL`` — a single DSN registered as connection ``"default"``.
9
+ 3. Discrete fields — ``SQL_DIALECT`` + ``SQL_HOST`` / ``SQL_PORT`` /
10
+ ``SQL_USERNAME`` / ``SQL_PASSWORD`` / ``SQL_DATABASE`` / ``SQL_OPTIONS``
11
+ (JSON), registered as ``"default"``.
12
+ 4. Nothing configured — a zero-infra in-memory SQLite connection named
13
+ ``"memory"`` so the server works out of the box.
14
+
15
+ Secrets are never logged: URLs are parsed into ``sqlalchemy.engine.URL``
16
+ objects and only ever rendered with ``hide_password=True``.
17
+ """
18
+
19
+ import json
20
+ import os
21
+
22
+ from agent_utilities.base_utilities import get_logger, to_boolean
23
+ from sqlalchemy.engine import URL, make_url
24
+
25
+ from sql_mcp.dialects import build_url
26
+
27
+ logger = get_logger(__name__)
28
+
29
+ DEFAULT_MAX_ROWS = 500
30
+ DEFAULT_TIMEOUT_SECONDS = 30.0
31
+
32
+
33
+ def _connection_from_spec(name: str, spec: object) -> URL:
34
+ """Build a SQLAlchemy URL from one ``SQL_CONNECTIONS`` entry."""
35
+ if isinstance(spec, str):
36
+ return make_url(spec)
37
+ if isinstance(spec, dict):
38
+ if "url" in spec:
39
+ return make_url(spec["url"])
40
+ if "dialect" in spec:
41
+ return build_url(
42
+ spec["dialect"],
43
+ host=spec.get("host"),
44
+ port=spec.get("port"),
45
+ username=spec.get("username"),
46
+ password=spec.get("password"),
47
+ database=spec.get("database"),
48
+ options=spec.get("options"),
49
+ )
50
+ raise ValueError(
51
+ f"Connection {name!r} in SQL_CONNECTIONS must be a DSN string or an "
52
+ "object with 'url' or 'dialect' fields."
53
+ )
54
+
55
+
56
+ def load_connections() -> dict[str, URL]:
57
+ """Load the named-connection registry from the environment."""
58
+ raw = os.getenv("SQL_CONNECTIONS", "")
59
+ if raw.strip():
60
+ try:
61
+ mapping = json.loads(raw)
62
+ except json.JSONDecodeError as exc:
63
+ raise ValueError(f"SQL_CONNECTIONS is not valid JSON: {exc}") from exc
64
+ if not isinstance(mapping, dict) or not mapping:
65
+ raise ValueError(
66
+ "SQL_CONNECTIONS must be a non-empty JSON object mapping "
67
+ "connection names to DSNs or connection objects."
68
+ )
69
+ return {
70
+ name: _connection_from_spec(name, spec) for name, spec in mapping.items()
71
+ }
72
+
73
+ url = os.getenv("SQL_URL", "")
74
+ if url.strip():
75
+ return {"default": make_url(url)}
76
+
77
+ if os.getenv("SQL_DIALECT") or os.getenv("SQL_HOST"):
78
+ options_raw = os.getenv("SQL_OPTIONS", "")
79
+ options = json.loads(options_raw) if options_raw.strip() else None
80
+ port_raw = os.getenv("SQL_PORT", "")
81
+ return {
82
+ "default": build_url(
83
+ os.getenv("SQL_DIALECT", "postgres"),
84
+ host=os.getenv("SQL_HOST"),
85
+ port=int(port_raw) if port_raw.strip() else None,
86
+ username=os.getenv("SQL_USERNAME"),
87
+ password=os.getenv("SQL_PASSWORD"),
88
+ database=os.getenv("SQL_DATABASE"),
89
+ options=options,
90
+ )
91
+ }
92
+
93
+ logger.info(
94
+ "No SQL connection configured (SQL_CONNECTIONS/SQL_URL/SQL_HOST); "
95
+ "registering a zero-infra in-memory SQLite connection named 'memory'."
96
+ )
97
+ return {"memory": make_url("sqlite+pysqlite:///:memory:")}
98
+
99
+
100
+ def allow_writes() -> bool:
101
+ """Whether DML/DDL via ``sql_execute`` is enabled (default: read-only)."""
102
+ return to_boolean(os.getenv("SQL_ALLOW_WRITES", "False"))
103
+
104
+
105
+ def default_max_rows() -> int:
106
+ """Per-call row cap (``SQL_MAX_ROWS``, default 500); requests are clamped."""
107
+ return int(os.getenv("SQL_MAX_ROWS", str(DEFAULT_MAX_ROWS)))
108
+
109
+
110
+ def default_timeout() -> float:
111
+ """Per-call statement timeout in seconds (``SQL_TIMEOUT_SECONDS``)."""
112
+ return float(os.getenv("SQL_TIMEOUT_SECONDS", str(DEFAULT_TIMEOUT_SECONDS)))
113
+
114
+
115
+ _api = None
116
+
117
+
118
+ def get_api():
119
+ """Return the process-wide :class:`~sql_mcp.api_client.Api` instance.
120
+
121
+ Built lazily from the environment; ``reset_api()`` clears the cache (used
122
+ by tests and after configuration changes).
123
+ """
124
+ global _api
125
+ if _api is None:
126
+ from sql_mcp.api_client import Api
127
+
128
+ _api = Api(
129
+ connections=load_connections(),
130
+ allow_writes=allow_writes(),
131
+ max_rows=default_max_rows(),
132
+ timeout=default_timeout(),
133
+ )
134
+ return _api
135
+
136
+
137
+ def reset_api() -> None:
138
+ """Dispose the cached client so the next ``get_api()`` re-reads the env."""
139
+ global _api
140
+ if _api is not None:
141
+ _api.dispose()
142
+ _api = None
sql_mcp/dialects.py ADDED
@@ -0,0 +1,183 @@
1
+ """Dialect registry for sql-mcp (CONCEPT:SQL-1.1).
2
+
3
+ Mirrors the vector-mcp backend-registry pattern: every supported SQL engine is
4
+ described by a :class:`DialectSpec` entry in :data:`DIALECTS`. The spec carries
5
+ the SQLAlchemy URL scheme, the optional driver package (and which pip extra
6
+ ships it), and the dialect-specific administrative SQL the ``sql_admin`` tool
7
+ uses. Core ships SQLite only (stdlib-backed); the other drivers are optional
8
+ extras so the base install stays thin.
9
+ """
10
+
11
+ import importlib
12
+ from dataclasses import dataclass
13
+
14
+ from sqlalchemy.engine import URL
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class DialectSpec:
19
+ """Static description of one supported SQL dialect."""
20
+
21
+ name: str
22
+ sqlalchemy_scheme: str
23
+ driver_module: str | None
24
+ extra: str | None
25
+ default_port: int | None
26
+ explain_prefix: str | None
27
+ version_sql: str | None
28
+ active_connections_sql: str | None
29
+
30
+
31
+ DIALECTS: dict[str, DialectSpec] = {
32
+ "sqlite": DialectSpec(
33
+ name="sqlite",
34
+ sqlalchemy_scheme="sqlite+pysqlite",
35
+ driver_module=None,
36
+ extra=None,
37
+ default_port=None,
38
+ explain_prefix="EXPLAIN QUERY PLAN",
39
+ version_sql="SELECT sqlite_version()",
40
+ active_connections_sql=None,
41
+ ),
42
+ "postgres": DialectSpec(
43
+ name="postgres",
44
+ sqlalchemy_scheme="postgresql+psycopg",
45
+ driver_module="psycopg",
46
+ extra="postgres",
47
+ default_port=5432,
48
+ explain_prefix="EXPLAIN",
49
+ version_sql="SELECT version()",
50
+ active_connections_sql=(
51
+ "SELECT pid, usename, datname, state, application_name, query_start "
52
+ "FROM pg_stat_activity WHERE pid <> pg_backend_pid()"
53
+ ),
54
+ ),
55
+ "mysql": DialectSpec(
56
+ name="mysql",
57
+ sqlalchemy_scheme="mysql+pymysql",
58
+ driver_module="pymysql",
59
+ extra="mysql",
60
+ default_port=3306,
61
+ explain_prefix="EXPLAIN",
62
+ version_sql="SELECT VERSION()",
63
+ active_connections_sql=(
64
+ "SELECT id, user, host, db, command, time, state "
65
+ "FROM information_schema.processlist"
66
+ ),
67
+ ),
68
+ "mssql": DialectSpec(
69
+ name="mssql",
70
+ sqlalchemy_scheme="mssql+pyodbc",
71
+ driver_module="pyodbc",
72
+ extra="mssql",
73
+ default_port=1433,
74
+ explain_prefix=None,
75
+ version_sql="SELECT @@VERSION",
76
+ active_connections_sql=(
77
+ "SELECT session_id, login_name, status, host_name, program_name "
78
+ "FROM sys.dm_exec_sessions WHERE is_user_process = 1"
79
+ ),
80
+ ),
81
+ "oracle": DialectSpec(
82
+ name="oracle",
83
+ sqlalchemy_scheme="oracle+oracledb",
84
+ driver_module="oracledb",
85
+ extra="oracle",
86
+ default_port=1521,
87
+ explain_prefix="EXPLAIN PLAN FOR",
88
+ version_sql="SELECT banner FROM v$version",
89
+ active_connections_sql=(
90
+ "SELECT sid, username, status, machine, program "
91
+ "FROM v$session WHERE username IS NOT NULL"
92
+ ),
93
+ ),
94
+ }
95
+
96
+ ALIASES: dict[str, str] = {
97
+ "postgresql": "postgres",
98
+ "pg": "postgres",
99
+ "pgsql": "postgres",
100
+ "mariadb": "mysql",
101
+ "sqlserver": "mssql",
102
+ "mssqlserver": "mssql",
103
+ "sqlite3": "sqlite",
104
+ }
105
+
106
+
107
+ def get_dialect(name: str) -> DialectSpec:
108
+ """Resolve a dialect (or alias) name to its :class:`DialectSpec`.
109
+
110
+ Raises ``ValueError`` listing the supported dialects when unknown.
111
+ """
112
+ key = ALIASES.get(name.lower().strip(), name.lower().strip())
113
+ spec = DIALECTS.get(key)
114
+ if spec is None:
115
+ supported = ", ".join(sorted(DIALECTS))
116
+ raise ValueError(f"Unknown SQL dialect {name!r}. Supported: {supported}.")
117
+ return spec
118
+
119
+
120
+ def dialect_for_url(url: URL) -> DialectSpec | None:
121
+ """Best-effort match of a SQLAlchemy URL to a registered dialect spec."""
122
+ backend = url.get_backend_name()
123
+ for spec in DIALECTS.values():
124
+ if spec.sqlalchemy_scheme.split("+", 1)[0] == backend:
125
+ return spec
126
+ alias = ALIASES.get(backend)
127
+ return DIALECTS.get(alias) if alias else None
128
+
129
+
130
+ def require_driver(spec: DialectSpec) -> None:
131
+ """Verify the dialect's DBAPI driver is importable.
132
+
133
+ Raises ``ImportError`` naming the pip extra that ships the driver, so the
134
+ failure is self-explanatory (``pip install sql-mcp[postgres]`` etc.).
135
+ """
136
+ if spec.driver_module is None:
137
+ return
138
+ try:
139
+ importlib.import_module(spec.driver_module)
140
+ except ImportError as exc:
141
+ raise ImportError(
142
+ f"The {spec.name!r} dialect needs the {spec.driver_module!r} driver. "
143
+ f"Install it with: pip install sql-mcp[{spec.extra}]"
144
+ ) from exc
145
+
146
+
147
+ def driver_available(spec: DialectSpec) -> bool:
148
+ """Return True when the dialect's DBAPI driver can be imported."""
149
+ if spec.driver_module is None:
150
+ return True
151
+ try:
152
+ importlib.import_module(spec.driver_module)
153
+ return True
154
+ except ImportError:
155
+ return False
156
+
157
+
158
+ def build_url(
159
+ dialect: str,
160
+ host: str | None = None,
161
+ port: int | None = None,
162
+ username: str | None = None,
163
+ password: str | None = None,
164
+ database: str | None = None,
165
+ options: dict[str, str] | None = None,
166
+ ) -> URL:
167
+ """Build a SQLAlchemy URL from discrete connection fields.
168
+
169
+ Uses ``URL.create`` so credentials are quoted correctly and the password
170
+ is never interpolated into a plain string (redaction-safe by design).
171
+ """
172
+ spec = get_dialect(dialect)
173
+ if spec.name == "sqlite":
174
+ return URL.create(spec.sqlalchemy_scheme, database=database or ":memory:")
175
+ return URL.create(
176
+ spec.sqlalchemy_scheme,
177
+ username=username,
178
+ password=password,
179
+ host=host,
180
+ port=port or spec.default_port,
181
+ database=database,
182
+ query=dict(options or {}),
183
+ )
@@ -0,0 +1,14 @@
1
+ {
2
+ "task": "main-agent",
3
+ "input": "# main-agent\n\nYou are the primary orchestrator for this workspace. Your goal is to help the user manage their projects and coordinate specialized agents.\n\n### Core Principles\n* Be concise and efficient.\n* Use the knowledge graph to discover tools and experts.\n* Verify your work before concluding.\n\nYour personality:\n* **Emoji:** 🤖\n* **Vibe:** Professional, efficient, helpful",
4
+ "type": "prompt",
5
+ "description": "The primary orchestrator agent for this workspace.",
6
+ "tools": [
7
+ "workspace-manager",
8
+ "agent-workflows"
9
+ ],
10
+ "topic": "General Expertise",
11
+ "tone": "technical and precise",
12
+ "style": "professional assistant",
13
+ "goal": "Coordinate specialized agents and manage the workspace."
14
+ }
@@ -0,0 +1,5 @@
1
+ """MCP tool registration for sql_mcp."""
2
+
3
+ from sql_mcp.mcp.mcp_sql import register_sql_tools
4
+
5
+ __all__ = ["register_sql_tools"]
sql_mcp/mcp/mcp_sql.py ADDED
@@ -0,0 +1,224 @@
1
+ """Action-dispatch MCP tools for sql-mcp (CONCEPT:SQL-1.0, CONCEPT:SQL-1.5).
2
+
3
+ Four consolidated tools — ``sql_query``, ``sql_execute``, ``sql_schema``, and
4
+ ``sql_admin`` — each routing an ``action`` + ``params_json`` pair to the
5
+ :class:`~sql_mcp.api_client.Api` facade. The tools are thin shims: parameter
6
+ parsing only, no business logic. Every tool accepts an optional ``connection``
7
+ naming one of the configured connections (defaults to the sole/first one).
8
+ """
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from fastmcp import FastMCP
14
+ from pydantic import Field
15
+
16
+ from sql_mcp.auth import get_api
17
+
18
+
19
+ def register_sql_tools(mcp: FastMCP) -> None:
20
+ """Register the query, execute, schema, and admin tools."""
21
+
22
+ @mcp.tool(tags={"query"})
23
+ async def sql_query(
24
+ action: str = Field(
25
+ description=(
26
+ "Query action. One of: 'execute' (run a read-only SELECT/CTE "
27
+ "with bound parameters), 'explain' (return the dialect's query "
28
+ "plan for a read-only statement)."
29
+ )
30
+ ),
31
+ params_json: str = Field(
32
+ default="{}",
33
+ description=(
34
+ "JSON of arguments. execute: "
35
+ '{"sql": "SELECT * FROM users WHERE id = :id", '
36
+ '"params": {"id": 1}, "max_rows": 100, "timeout": 10}. '
37
+ 'explain: {"sql": "SELECT ...", "params": {...}}. '
38
+ "Statements must be single, read-only (SELECT/WITH/EXPLAIN/"
39
+ "SHOW/DESCRIBE/PRAGMA/VALUES), and use :name bound parameters "
40
+ "— never inline values. max_rows is clamped to the server cap."
41
+ ),
42
+ ),
43
+ connection: str = Field(
44
+ default="",
45
+ description=(
46
+ "Named connection from the server config (see sql_admin "
47
+ "'connections'). Empty = the default (sole/first) connection."
48
+ ),
49
+ ),
50
+ ) -> Any:
51
+ """Run read-only SQL with row cap, timeout, and column metadata."""
52
+ api = get_api()
53
+ p = json.loads(params_json) if params_json else {}
54
+ if action == "execute":
55
+ return api.query(
56
+ p["sql"],
57
+ params=p.get("params"),
58
+ connection=connection or None,
59
+ max_rows=p.get("max_rows"),
60
+ timeout=p.get("timeout"),
61
+ )
62
+ if action == "explain":
63
+ return api.explain(
64
+ p["sql"],
65
+ params=p.get("params"),
66
+ connection=connection or None,
67
+ timeout=p.get("timeout"),
68
+ )
69
+ raise ValueError(f"Unknown query action: {action!r}.")
70
+
71
+ @mcp.tool(tags={"execute"})
72
+ async def sql_execute(
73
+ action: str = Field(
74
+ description=(
75
+ "Write action. One of: 'execute' (one DML/DDL statement in a "
76
+ "transaction; params may be a dict or a list of dicts for "
77
+ "executemany), 'script' (a list of statements in ONE "
78
+ "all-or-nothing transaction). Requires the server to run with "
79
+ "SQL_ALLOW_WRITES=True — the default is read-only."
80
+ )
81
+ ),
82
+ params_json: str = Field(
83
+ default="{}",
84
+ description=(
85
+ "JSON of arguments. execute: "
86
+ '{"sql": "INSERT INTO t (a) VALUES (:a)", "params": {"a": 1}} '
87
+ 'or "params": [{"a": 1}, {"a": 2}] for executemany. '
88
+ 'script: {"statements": ["CREATE TABLE ...", "INSERT ..."]}. '
89
+ "Optional 'timeout' (seconds) on both. Returns affected-row "
90
+ "counts."
91
+ ),
92
+ ),
93
+ connection: str = Field(
94
+ default="",
95
+ description=(
96
+ "Named connection from the server config. Empty = the default "
97
+ "(sole/first) connection."
98
+ ),
99
+ ),
100
+ ) -> Any:
101
+ """Run DML/DDL in transactions (gated by SQL_ALLOW_WRITES)."""
102
+ api = get_api()
103
+ p = json.loads(params_json) if params_json else {}
104
+ if action == "execute":
105
+ return api.execute(
106
+ p["sql"],
107
+ params=p.get("params"),
108
+ connection=connection or None,
109
+ timeout=p.get("timeout"),
110
+ )
111
+ if action == "script":
112
+ return api.execute_script(
113
+ p["statements"],
114
+ connection=connection or None,
115
+ timeout=p.get("timeout"),
116
+ )
117
+ raise ValueError(f"Unknown execute action: {action!r}.")
118
+
119
+ @mcp.tool(tags={"schema"})
120
+ async def sql_schema(
121
+ action: str = Field(
122
+ description=(
123
+ "Schema action. One of: 'schemas' (list schema names), "
124
+ "'tables', 'views' (list names, optional schema), 'columns', "
125
+ "'indexes', 'foreign_keys' (describe a table), 'ddl' (reflect "
126
+ "CREATE TABLE DDL), 'sample' (preview rows with a limit)."
127
+ )
128
+ ),
129
+ params_json: str = Field(
130
+ default="{}",
131
+ description=(
132
+ "JSON of arguments. schemas: {}. tables/views: "
133
+ '{"schema": "public"} (optional). '
134
+ 'columns/indexes/foreign_keys/ddl: {"table": "users", '
135
+ '"schema": "public"}. sample: {"table": "users", '
136
+ '"limit": 10, "schema": "public"} (limit clamped to the '
137
+ "server row cap)."
138
+ ),
139
+ ),
140
+ connection: str = Field(
141
+ default="",
142
+ description=(
143
+ "Named connection from the server config. Empty = the default "
144
+ "(sole/first) connection."
145
+ ),
146
+ ),
147
+ ) -> Any:
148
+ """Inspect schemas, tables, views, columns, indexes, FKs, and DDL."""
149
+ api = get_api()
150
+ p = json.loads(params_json) if params_json else {}
151
+ conn = connection or None
152
+ schema = p.get("schema")
153
+ if action == "schemas":
154
+ return api.list_schemas(connection=conn)
155
+ if action == "tables":
156
+ return api.list_tables(schema=schema, connection=conn)
157
+ if action == "views":
158
+ return api.list_views(schema=schema, connection=conn)
159
+ if action == "columns":
160
+ return api.list_columns(p["table"], schema=schema, connection=conn)
161
+ if action == "indexes":
162
+ return api.list_indexes(p["table"], schema=schema, connection=conn)
163
+ if action == "foreign_keys":
164
+ return api.list_foreign_keys(p["table"], schema=schema, connection=conn)
165
+ if action == "ddl":
166
+ return api.table_ddl(p["table"], schema=schema, connection=conn)
167
+ if action == "sample":
168
+ return api.sample_rows(
169
+ p["table"],
170
+ schema=schema,
171
+ limit=p.get("limit", 10),
172
+ connection=conn,
173
+ )
174
+ raise ValueError(f"Unknown schema action: {action!r}.")
175
+
176
+ @mcp.tool(tags={"admin"})
177
+ async def sql_admin(
178
+ action: str = Field(
179
+ description=(
180
+ "Admin action. One of: 'ping' (connection test + latency), "
181
+ "'version' (server version), 'active_connections' (server "
182
+ "sessions, where the dialect supports it), 'connections' "
183
+ "(list configured connections, passwords redacted), "
184
+ "'dialects' (supported dialects + driver availability)."
185
+ )
186
+ ),
187
+ params_json: str = Field(
188
+ default="{}",
189
+ description="JSON of arguments. All admin actions take {}.",
190
+ ),
191
+ connection: str = Field(
192
+ default="",
193
+ description=(
194
+ "Named connection from the server config. Empty = the default "
195
+ "(sole/first) connection."
196
+ ),
197
+ ),
198
+ ) -> Any:
199
+ """Connection health, server version, sessions, and registry info."""
200
+ api = get_api()
201
+ if params_json:
202
+ json.loads(params_json) # validate early; admin actions take {}
203
+ conn = connection or None
204
+ if action == "ping":
205
+ return api.ping(connection=conn)
206
+ if action == "version":
207
+ return api.server_version(connection=conn)
208
+ if action == "active_connections":
209
+ return api.active_connections(connection=conn)
210
+ if action == "connections":
211
+ return api.describe_connections()
212
+ if action == "dialects":
213
+ from sql_mcp.dialects import DIALECTS, driver_available
214
+
215
+ return [
216
+ {
217
+ "dialect": spec.name,
218
+ "scheme": spec.sqlalchemy_scheme,
219
+ "extra": spec.extra,
220
+ "driver_installed": driver_available(spec),
221
+ }
222
+ for spec in DIALECTS.values()
223
+ ]
224
+ raise ValueError(f"Unknown admin action: {action!r}.")
@@ -0,0 +1,33 @@
1
+ {
2
+ "mcpServers": {
3
+ "sql-mcp": {
4
+ "command": "uv",
5
+ "args": [
6
+ "run",
7
+ "sql-mcp"
8
+ ],
9
+ "env": {
10
+ "FASTMCP_LOG_LEVEL": "<YOUR_FASTMCP_LOG_LEVEL>",
11
+ "NO_COLOR": "<YOUR_NO_COLOR>",
12
+ "TERM": "<YOUR_TERM>",
13
+ "AGENT_DESCRIPTION": "<YOUR_AGENT_DESCRIPTION>",
14
+ "AGENT_SYSTEM_PROMPT": "<YOUR_AGENT_SYSTEM_PROMPT>",
15
+ "DEFAULT_AGENT_NAME": "<YOUR_DEFAULT_AGENT_NAME>",
16
+ "MCP_URL": "<YOUR_MCP_URL>",
17
+ "SQL_CONNECTIONS": "<YOUR_SQL_CONNECTIONS>",
18
+ "SQL_URL": "<YOUR_SQL_URL>",
19
+ "SQL_DIALECT": "<YOUR_SQL_DIALECT>",
20
+ "SQL_HOST": "<YOUR_SQL_HOST>",
21
+ "SQL_PORT": "<YOUR_SQL_PORT>",
22
+ "SQL_USERNAME": "<YOUR_SQL_USERNAME>",
23
+ "SQL_PASSWORD": "<YOUR_SQL_PASSWORD>",
24
+ "SQL_DATABASE": "<YOUR_SQL_DATABASE>",
25
+ "SQL_OPTIONS": "<YOUR_SQL_OPTIONS>",
26
+ "SQL_ALLOW_WRITES": "False",
27
+ "SQL_MAX_ROWS": "500",
28
+ "SQL_TIMEOUT_SECONDS": "30",
29
+ "SQLTOOL": "True"
30
+ }
31
+ }
32
+ }
33
+ }
sql_mcp/mcp_server.py ADDED
@@ -0,0 +1,59 @@
1
+ """Main FastMCP server and tool registration for sql-mcp."""
2
+
3
+ import os
4
+ import sys
5
+ from typing import Any
6
+
7
+ from agent_utilities.base_utilities import to_boolean
8
+ from agent_utilities.mcp_utilities import create_mcp_server
9
+ from dotenv import find_dotenv, load_dotenv
10
+ from fastmcp.utilities.logging import get_logger
11
+ from starlette.requests import Request
12
+ from starlette.responses import JSONResponse
13
+
14
+ from sql_mcp.mcp.mcp_sql import register_sql_tools
15
+
16
+ __version__ = "0.1.0"
17
+ logger = get_logger(name="sql_mcp")
18
+
19
+
20
+ def get_mcp_instance() -> tuple[Any, ...]:
21
+ load_dotenv(find_dotenv())
22
+ args, mcp, middlewares = create_mcp_server(
23
+ name="SQL MCP",
24
+ version=__version__,
25
+ instructions=(
26
+ "Generic SQL database MCP Server - read-only queries, gated "
27
+ "DML/DDL, schema reflection, and connection admin over "
28
+ "SQLAlchemy 2.x Core (SQLite, Postgres, MySQL/MariaDB, MSSQL, "
29
+ "Oracle) with named multi-connection support."
30
+ ),
31
+ )
32
+
33
+ @mcp.custom_route("/health", methods=["GET"])
34
+ async def health_check(request: Request) -> JSONResponse:
35
+ return JSONResponse({"status": "OK"})
36
+
37
+ if to_boolean(os.getenv("SQLTOOL", "True")):
38
+ register_sql_tools(mcp)
39
+
40
+ for mw in middlewares:
41
+ mcp.add_middleware(mw)
42
+ return mcp, args, middlewares
43
+
44
+
45
+ def mcp_server() -> None:
46
+ mcp, args, middlewares = get_mcp_instance()
47
+ print(f"SQL MCP v{__version__}", file=sys.stderr)
48
+ if args.transport == "stdio":
49
+ mcp.run(transport="stdio")
50
+ elif args.transport == "streamable-http":
51
+ mcp.run(transport="streamable-http", host=args.host, port=args.port)
52
+ elif args.transport == "sse":
53
+ mcp.run(transport="sse", host=args.host, port=args.port)
54
+ else:
55
+ mcp.run(transport="stdio")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ mcp_server()