sql-builder-ai-mcp 1.0.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.
server.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQL Builder AI MCP Server
|
|
3
|
+
SQL query building and analysis tools powered by MEOK AI Labs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import sys, os
|
|
8
|
+
sys.path.insert(0, os.path.expanduser('~/clawd/meok-labs-engine/shared'))
|
|
9
|
+
from auth_middleware import check_access
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
mcp = FastMCP("sql-builder-ai", instructions="MEOK AI Labs MCP Server")
|
|
17
|
+
|
|
18
|
+
_call_counts: dict[str, list[float]] = defaultdict(list)
|
|
19
|
+
FREE_TIER_LIMIT = 50
|
|
20
|
+
WINDOW = 86400
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_rate_limit(tool_name: str) -> None:
|
|
24
|
+
now = time.time()
|
|
25
|
+
_call_counts[tool_name] = [t for t in _call_counts[tool_name] if now - t < WINDOW]
|
|
26
|
+
if len(_call_counts[tool_name]) >= FREE_TIER_LIMIT:
|
|
27
|
+
raise ValueError(f"Rate limit exceeded for {tool_name}. Free tier: {FREE_TIER_LIMIT}/day. Upgrade at https://meok.ai/pricing")
|
|
28
|
+
_call_counts[tool_name].append(now)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _quote_id(name: str) -> str:
|
|
32
|
+
return f'"{name}"' if not name.isidentifier() or name.upper() in SQL_KEYWORDS else name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
SQL_KEYWORDS = {"SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "JOIN", "ON",
|
|
36
|
+
"ORDER", "GROUP", "BY", "HAVING", "LIMIT", "OFFSET", "TABLE", "INDEX",
|
|
37
|
+
"CREATE", "DROP", "ALTER", "AND", "OR", "NOT", "IN", "BETWEEN", "LIKE", "AS"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def build_select(
|
|
42
|
+
table: str, columns: list[str] | None = None, where: dict | None = None,
|
|
43
|
+
order_by: str = "", limit: int = 0, joins: list[dict] | None = None
|
|
44
|
+
, api_key: str = "") -> dict:
|
|
45
|
+
"""Build a SELECT SQL query from structured parameters.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
table: Main table name
|
|
49
|
+
columns: List of column names (default: *)
|
|
50
|
+
where: Dict of column:value conditions (AND-joined)
|
|
51
|
+
order_by: Column to order by (prefix with - for DESC)
|
|
52
|
+
limit: LIMIT clause (0 = no limit)
|
|
53
|
+
joins: List of dicts with keys: table, on, type (LEFT/INNER/RIGHT)
|
|
54
|
+
"""
|
|
55
|
+
allowed, msg, tier = check_access(api_key)
|
|
56
|
+
if not allowed:
|
|
57
|
+
return {"error": msg, "upgrade_url": "https://buy.stripe.com/14A4gB3K4eUWgYR56o8k836"}
|
|
58
|
+
|
|
59
|
+
_check_rate_limit("build_select")
|
|
60
|
+
cols = ", ".join(columns) if columns else "*"
|
|
61
|
+
sql = f"SELECT {cols}\nFROM {table}"
|
|
62
|
+
params = []
|
|
63
|
+
if joins:
|
|
64
|
+
for j in joins:
|
|
65
|
+
jtype = j.get("type", "LEFT").upper()
|
|
66
|
+
sql += f"\n{jtype} JOIN {j['table']} ON {j['on']}"
|
|
67
|
+
if where:
|
|
68
|
+
conditions = []
|
|
69
|
+
for col, val in where.items():
|
|
70
|
+
if val is None:
|
|
71
|
+
conditions.append(f"{col} IS NULL")
|
|
72
|
+
elif isinstance(val, list):
|
|
73
|
+
placeholders = ", ".join(["%s"] * len(val))
|
|
74
|
+
conditions.append(f"{col} IN ({placeholders})")
|
|
75
|
+
params.extend(val)
|
|
76
|
+
else:
|
|
77
|
+
conditions.append(f"{col} = %s")
|
|
78
|
+
params.append(val)
|
|
79
|
+
if conditions:
|
|
80
|
+
sql += "\nWHERE " + " AND ".join(conditions)
|
|
81
|
+
if order_by:
|
|
82
|
+
direction = "DESC" if order_by.startswith("-") else "ASC"
|
|
83
|
+
col = order_by.lstrip("-")
|
|
84
|
+
sql += f"\nORDER BY {col} {direction}"
|
|
85
|
+
if limit > 0:
|
|
86
|
+
sql += f"\nLIMIT {limit}"
|
|
87
|
+
return {"sql": sql + ";", "params": params, "type": "SELECT"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
def build_insert(table: str, rows: list[dict], on_conflict: str = "", api_key: str = "") -> dict:
|
|
92
|
+
"""Build an INSERT SQL query from a list of row dicts.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
table: Target table name
|
|
96
|
+
rows: List of dicts (each dict is a row, keys are column names)
|
|
97
|
+
on_conflict: Conflict resolution: '' (none), 'ignore', 'update'
|
|
98
|
+
"""
|
|
99
|
+
allowed, msg, tier = check_access(api_key)
|
|
100
|
+
if not allowed:
|
|
101
|
+
return {"error": msg, "upgrade_url": "https://buy.stripe.com/14A4gB3K4eUWgYR56o8k836"}
|
|
102
|
+
|
|
103
|
+
_check_rate_limit("build_insert")
|
|
104
|
+
if not rows:
|
|
105
|
+
return {"error": "No rows provided"}
|
|
106
|
+
columns = list(rows[0].keys())
|
|
107
|
+
placeholders = ", ".join(["%s"] * len(columns))
|
|
108
|
+
cols_str = ", ".join(columns)
|
|
109
|
+
sql = f"INSERT INTO {table} ({cols_str})\nVALUES"
|
|
110
|
+
all_params = []
|
|
111
|
+
value_rows = []
|
|
112
|
+
for row in rows:
|
|
113
|
+
vals = [row.get(c) for c in columns]
|
|
114
|
+
value_rows.append(f"({placeholders})")
|
|
115
|
+
all_params.extend(vals)
|
|
116
|
+
sql += "\n" + ",\n".join(value_rows)
|
|
117
|
+
if on_conflict == "ignore":
|
|
118
|
+
sql += "\nON CONFLICT DO NOTHING"
|
|
119
|
+
elif on_conflict == "update":
|
|
120
|
+
updates = ", ".join(f"{c} = EXCLUDED.{c}" for c in columns)
|
|
121
|
+
sql += f"\nON CONFLICT DO UPDATE SET {updates}"
|
|
122
|
+
return {"sql": sql + ";", "params": all_params, "type": "INSERT", "row_count": len(rows)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@mcp.tool()
|
|
126
|
+
def explain_query(sql: str, api_key: str = "") -> dict:
|
|
127
|
+
"""Analyze and explain a SQL query's structure and components.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
sql: SQL query string to analyze
|
|
131
|
+
"""
|
|
132
|
+
allowed, msg, tier = check_access(api_key)
|
|
133
|
+
if not allowed:
|
|
134
|
+
return {"error": msg, "upgrade_url": "https://buy.stripe.com/14A4gB3K4eUWgYR56o8k836"}
|
|
135
|
+
|
|
136
|
+
_check_rate_limit("explain_query")
|
|
137
|
+
sql_upper = sql.upper().strip()
|
|
138
|
+
query_type = "UNKNOWN"
|
|
139
|
+
for t in ("SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"):
|
|
140
|
+
if sql_upper.startswith(t):
|
|
141
|
+
query_type = t
|
|
142
|
+
break
|
|
143
|
+
components = {"type": query_type}
|
|
144
|
+
tables = re.findall(r'\bFROM\s+(\w+)', sql, re.IGNORECASE)
|
|
145
|
+
tables += re.findall(r'\bJOIN\s+(\w+)', sql, re.IGNORECASE)
|
|
146
|
+
tables += re.findall(r'\bINTO\s+(\w+)', sql, re.IGNORECASE)
|
|
147
|
+
tables += re.findall(r'\bUPDATE\s+(\w+)', sql, re.IGNORECASE)
|
|
148
|
+
components["tables"] = list(set(tables))
|
|
149
|
+
if re.search(r'\bWHERE\b', sql, re.IGNORECASE):
|
|
150
|
+
where = re.search(r'\bWHERE\b(.+?)(?:\bORDER\b|\bGROUP\b|\bLIMIT\b|\bHAVING\b|;|$)', sql, re.IGNORECASE | re.DOTALL)
|
|
151
|
+
components["where_clause"] = where.group(1).strip() if where else ""
|
|
152
|
+
joins = re.findall(r'((?:LEFT|RIGHT|INNER|OUTER|CROSS|FULL)\s+)?JOIN\s+(\w+)\s+ON\s+([^)]+?)(?=\s+(?:LEFT|RIGHT|INNER|WHERE|ORDER|GROUP|LIMIT|$))', sql, re.IGNORECASE)
|
|
153
|
+
if joins:
|
|
154
|
+
components["joins"] = [{"type": j[0].strip() or "INNER", "table": j[1], "condition": j[2].strip()} for j in joins]
|
|
155
|
+
has_subquery = "(" in sql and "SELECT" in sql_upper.split("(", 1)[-1] if "(" in sql else False
|
|
156
|
+
components["has_subquery"] = has_subquery
|
|
157
|
+
components["complexity"] = "simple" if len(tables) <= 1 and not has_subquery else "moderate" if len(tables) <= 3 else "complex"
|
|
158
|
+
return components
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def optimize_query_hints(sql: str, api_key: str = "") -> dict:
|
|
163
|
+
"""Suggest optimizations for a SQL query.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
sql: SQL query string to analyze for optimizations
|
|
167
|
+
"""
|
|
168
|
+
allowed, msg, tier = check_access(api_key)
|
|
169
|
+
if not allowed:
|
|
170
|
+
return {"error": msg, "upgrade_url": "https://buy.stripe.com/14A4gB3K4eUWgYR56o8k836"}
|
|
171
|
+
|
|
172
|
+
_check_rate_limit("optimize_query_hints")
|
|
173
|
+
hints = []
|
|
174
|
+
sql_upper = sql.upper()
|
|
175
|
+
if "SELECT *" in sql_upper:
|
|
176
|
+
hints.append({"hint": "Avoid SELECT * - specify only needed columns", "severity": "warning", "category": "performance"})
|
|
177
|
+
if "WHERE" not in sql_upper and "SELECT" in sql_upper:
|
|
178
|
+
hints.append({"hint": "No WHERE clause - may scan entire table", "severity": "warning", "category": "performance"})
|
|
179
|
+
if re.search(r'WHERE.*\bLIKE\s+["\']%', sql, re.IGNORECASE):
|
|
180
|
+
hints.append({"hint": "Leading wildcard in LIKE prevents index usage", "severity": "warning", "category": "index"})
|
|
181
|
+
if re.search(r'WHERE.*\bOR\b', sql, re.IGNORECASE):
|
|
182
|
+
hints.append({"hint": "OR conditions may prevent index usage - consider UNION", "severity": "info", "category": "index"})
|
|
183
|
+
if "DISTINCT" in sql_upper:
|
|
184
|
+
hints.append({"hint": "DISTINCT can be expensive - ensure it's necessary", "severity": "info", "category": "performance"})
|
|
185
|
+
if "ORDER BY" in sql_upper and "LIMIT" not in sql_upper:
|
|
186
|
+
hints.append({"hint": "ORDER BY without LIMIT sorts all results", "severity": "info", "category": "performance"})
|
|
187
|
+
if sql_upper.count("SELECT") > 1:
|
|
188
|
+
hints.append({"hint": "Subquery detected - consider using JOINs or CTEs instead", "severity": "info", "category": "readability"})
|
|
189
|
+
if re.search(r'WHERE.*(?:FUNCTION|UPPER|LOWER|CAST|CONVERT)\s*\(', sql, re.IGNORECASE):
|
|
190
|
+
hints.append({"hint": "Function in WHERE clause prevents index usage", "severity": "warning", "category": "index"})
|
|
191
|
+
tables = re.findall(r'\bFROM\s+(\w+)', sql, re.IGNORECASE) + re.findall(r'\bJOIN\s+(\w+)', sql, re.IGNORECASE)
|
|
192
|
+
idx_suggestions = []
|
|
193
|
+
for col_match in re.finditer(r'WHERE\s+(\w+)\s*=', sql, re.IGNORECASE):
|
|
194
|
+
idx_suggestions.append(f"Consider index on {col_match.group(1)}")
|
|
195
|
+
return {"hints": hints, "hint_count": len(hints), "index_suggestions": idx_suggestions,
|
|
196
|
+
"tables_referenced": list(set(tables))}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
mcp.run()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sql-builder-ai-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Sql Builder Ai automation via MCP. Includes build select, build insert, explain query. By MEOK AI Labs.
|
|
5
|
+
Project-URL: Homepage, https://meok.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/CSOAI-ORG/sql-builder-ai-mcp
|
|
7
|
+
Author-email: MEOK AI Labs <nicholas@meok.ai>
|
|
8
|
+
License: MIT License
|
|
9
|
+
Copyright (c) 2026 MEOK AI Labs (meok.ai)
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software to deal in the Software without restriction.
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS".
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai,builder,mcp,meok,sql
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
server.py,sha256=wJKkQptHmp5j9bgQZIB_HDjLvEu4RI5O-qm7GZaAxRg,8743
|
|
2
|
+
sql_builder_ai_mcp-1.0.0.dist-info/METADATA,sha256=WqNIknwgIm_WcJ4dydl5qUHwdz_WJ_sfURMakD4qfjQ,900
|
|
3
|
+
sql_builder_ai_mcp-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
sql_builder_ai_mcp-1.0.0.dist-info/entry_points.txt,sha256=H_ClpAMOCc8XehVp-duYTwszAqBcPZnOW8IM3KtUj_0,51
|
|
5
|
+
sql_builder_ai_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=kK82YVM4AjC95dgHXhU16mmB6-Qwyi3OVOzzZPamZGg,227
|
|
6
|
+
sql_builder_ai_mcp-1.0.0.dist-info/RECORD,,
|