MindsDB 25.3.4.2__py3-none-any.whl → 25.4.2.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.
Potentially problematic release.
This version of MindsDB might be problematic. Click here for more details.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +21 -4
- mindsdb/api/executor/command_executor.py +62 -61
- mindsdb/api/executor/data_types/answer.py +9 -12
- mindsdb/api/executor/datahub/classes/response.py +11 -0
- mindsdb/api/executor/datahub/datanodes/datanode.py +4 -4
- mindsdb/api/executor/datahub/datanodes/information_schema_datanode.py +7 -9
- mindsdb/api/executor/datahub/datanodes/integration_datanode.py +22 -16
- mindsdb/api/executor/datahub/datanodes/project_datanode.py +20 -20
- mindsdb/api/executor/planner/plan_join.py +1 -1
- mindsdb/api/executor/planner/steps.py +2 -1
- mindsdb/api/executor/sql_query/result_set.py +10 -7
- mindsdb/api/executor/sql_query/sql_query.py +36 -82
- mindsdb/api/executor/sql_query/steps/delete_step.py +2 -3
- mindsdb/api/executor/sql_query/steps/fetch_dataframe.py +5 -3
- mindsdb/api/executor/sql_query/steps/insert_step.py +2 -2
- mindsdb/api/executor/sql_query/steps/prepare_steps.py +2 -2
- mindsdb/api/executor/sql_query/steps/subselect_step.py +20 -8
- mindsdb/api/executor/sql_query/steps/update_step.py +4 -6
- mindsdb/api/http/namespaces/sql.py +4 -1
- mindsdb/api/mcp/__init__.py +0 -0
- mindsdb/api/mcp/start.py +152 -0
- mindsdb/api/mysql/mysql_proxy/data_types/mysql_packets/ok_packet.py +1 -1
- mindsdb/api/mysql/mysql_proxy/executor/mysql_executor.py +4 -27
- mindsdb/api/mysql/mysql_proxy/libs/constants/mysql.py +1 -0
- mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +38 -37
- mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +23 -13
- mindsdb/integrations/handlers/mssql_handler/mssql_handler.py +1 -1
- mindsdb/integrations/handlers/mysql_handler/mysql_handler.py +3 -2
- mindsdb/integrations/handlers/oracle_handler/oracle_handler.py +4 -4
- mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +19 -5
- mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +9 -4
- mindsdb/integrations/handlers/redshift_handler/redshift_handler.py +1 -1
- mindsdb/integrations/handlers/snowflake_handler/snowflake_handler.py +18 -11
- mindsdb/integrations/libs/ml_handler_process/learn_process.py +1 -2
- mindsdb/integrations/libs/response.py +9 -4
- mindsdb/integrations/libs/vectordatabase_handler.py +37 -25
- mindsdb/integrations/utilities/rag/rerankers/reranker_compressor.py +35 -15
- mindsdb/interfaces/database/log.py +8 -9
- mindsdb/interfaces/database/projects.py +16 -5
- mindsdb/interfaces/functions/controller.py +59 -17
- mindsdb/interfaces/functions/to_markdown.py +194 -0
- mindsdb/interfaces/jobs/jobs_controller.py +3 -3
- mindsdb/interfaces/knowledge_base/controller.py +143 -26
- mindsdb/interfaces/knowledge_base/preprocessing/document_preprocessor.py +3 -14
- mindsdb/interfaces/query_context/context_controller.py +3 -1
- mindsdb/utilities/config.py +8 -0
- mindsdb/utilities/starters.py +7 -0
- {mindsdb-25.3.4.2.dist-info → mindsdb-25.4.2.0.dist-info}/METADATA +233 -231
- {mindsdb-25.3.4.2.dist-info → mindsdb-25.4.2.0.dist-info}/RECORD +53 -49
- {mindsdb-25.3.4.2.dist-info → mindsdb-25.4.2.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.3.4.2.dist-info → mindsdb-25.4.2.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.3.4.2.dist-info → mindsdb-25.4.2.0.dist-info}/top_level.txt +0 -0
|
@@ -249,11 +249,12 @@ class SubSelectStep(PlanStep):
|
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
class QueryStep(PlanStep):
|
|
252
|
-
def __init__(self, query, from_table=None, *args, **kwargs):
|
|
252
|
+
def __init__(self, query, from_table=None, *args, strict_where=True, **kwargs):
|
|
253
253
|
"""Performs query using injected dataframe"""
|
|
254
254
|
super().__init__(*args, **kwargs)
|
|
255
255
|
self.query = query
|
|
256
256
|
self.from_table = from_table
|
|
257
|
+
self.strict_where = strict_where
|
|
257
258
|
|
|
258
259
|
|
|
259
260
|
class DataStep(PlanStep):
|
|
@@ -50,13 +50,14 @@ def rename_df_columns(df: pd.DataFrame, names: Optional[List] = None) -> None:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class ResultSet:
|
|
53
|
-
def __init__(self, columns=None, values: List[List] = None, df: pd.DataFrame = None):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
def __init__(self, columns=None, values: List[List] = None, df: pd.DataFrame = None, affected_rows: int = None):
|
|
54
|
+
"""
|
|
55
|
+
Args:
|
|
56
|
+
columns: list of Columns
|
|
57
|
+
values (List[List]): data of resultSet, have to be list of lists with length equal to column
|
|
58
|
+
df (pd.DataFrame): injected dataframe, have to have enumerated columns and length equal to columns
|
|
59
|
+
affected_rows (int): number of affected rows
|
|
60
|
+
"""
|
|
60
61
|
if columns is None:
|
|
61
62
|
columns = []
|
|
62
63
|
self._columns = columns
|
|
@@ -67,6 +68,8 @@ class ResultSet:
|
|
|
67
68
|
df = pd.DataFrame(values)
|
|
68
69
|
self._df = df
|
|
69
70
|
|
|
71
|
+
self.affected_rows = affected_rows
|
|
72
|
+
|
|
70
73
|
self.is_prediction = False
|
|
71
74
|
|
|
72
75
|
def __repr__(self):
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* permission of MindsDB Inc
|
|
9
9
|
*******************************************************
|
|
10
10
|
"""
|
|
11
|
-
import re
|
|
12
11
|
import inspect
|
|
13
12
|
from textwrap import dedent
|
|
13
|
+
from typing import Dict
|
|
14
14
|
|
|
15
15
|
from mindsdb_sql_parser import parse_sql
|
|
16
16
|
from mindsdb.api.executor.planner.steps import (
|
|
@@ -23,7 +23,7 @@ from mindsdb.api.executor.planner.exceptions import PlanningException
|
|
|
23
23
|
from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender
|
|
24
24
|
from mindsdb.api.executor.planner import query_planner
|
|
25
25
|
|
|
26
|
-
from mindsdb.api.executor.utilities.sql import
|
|
26
|
+
from mindsdb.api.executor.utilities.sql import get_query_models
|
|
27
27
|
from mindsdb.interfaces.model.functions import get_model_record
|
|
28
28
|
from mindsdb.api.executor.exceptions import (
|
|
29
29
|
BadTableError,
|
|
@@ -38,8 +38,6 @@ from . import steps
|
|
|
38
38
|
from .result_set import ResultSet, Column
|
|
39
39
|
from . steps.base import BaseStepCall
|
|
40
40
|
|
|
41
|
-
superset_subquery = re.compile(r'from[\s\n]*(\(.*\))[\s\n]*as[\s\n]*virtual_table', flags=re.IGNORECASE | re.MULTILINE | re.S)
|
|
42
|
-
|
|
43
41
|
|
|
44
42
|
class SQLQuery:
|
|
45
43
|
|
|
@@ -59,23 +57,13 @@ class SQLQuery:
|
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
self.columns_list = None
|
|
62
|
-
self.steps_data = {}
|
|
60
|
+
self.steps_data: Dict[int, ResultSet] = {}
|
|
63
61
|
|
|
64
|
-
self.planner = None
|
|
62
|
+
self.planner: query_planner.QueryPlanner = None
|
|
65
63
|
self.parameters = []
|
|
66
|
-
self.fetched_data = None
|
|
67
|
-
|
|
68
|
-
self.outer_query = None
|
|
64
|
+
self.fetched_data: ResultSet = None
|
|
69
65
|
|
|
70
66
|
if isinstance(sql, str):
|
|
71
|
-
# region workaround for subqueries in superset
|
|
72
|
-
if 'as virtual_table' in sql.lower():
|
|
73
|
-
subquery = re.findall(superset_subquery, sql)
|
|
74
|
-
if isinstance(subquery, list) and len(subquery) == 1:
|
|
75
|
-
subquery = subquery[0]
|
|
76
|
-
self.outer_query = sql.replace(subquery, 'dataframe')
|
|
77
|
-
sql = subquery.strip('()')
|
|
78
|
-
# endregion
|
|
79
67
|
self.query = parse_sql(sql)
|
|
80
68
|
self.context['query_str'] = sql
|
|
81
69
|
else:
|
|
@@ -89,7 +77,6 @@ class SQLQuery:
|
|
|
89
77
|
self.create_planner()
|
|
90
78
|
|
|
91
79
|
if execute:
|
|
92
|
-
self.prepare_query(prepare=False)
|
|
93
80
|
self.execute_query()
|
|
94
81
|
|
|
95
82
|
@classmethod
|
|
@@ -190,55 +177,42 @@ class SQLQuery:
|
|
|
190
177
|
default_namespace=database,
|
|
191
178
|
)
|
|
192
179
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
180
|
+
def prepare_query(self):
|
|
181
|
+
"""it is prepared statement call
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
for step in self.planner.prepare_steps(self.query):
|
|
185
|
+
data = self.execute_step(step)
|
|
186
|
+
step.set_result(data)
|
|
187
|
+
self.steps_data[step.step_num] = data
|
|
188
|
+
except PlanningException as e:
|
|
189
|
+
raise LogicError(e)
|
|
200
190
|
|
|
201
|
-
|
|
202
|
-
'success': True,
|
|
203
|
-
'result': result
|
|
204
|
-
}
|
|
191
|
+
statement_info = self.planner.get_statement_info()
|
|
205
192
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
for step in self.planner.prepare_steps(self.query):
|
|
211
|
-
data = self.execute_step(step)
|
|
212
|
-
step.set_result(data)
|
|
213
|
-
self.steps_data[step.step_num] = data
|
|
214
|
-
except PlanningException as e:
|
|
215
|
-
raise LogicError(e)
|
|
216
|
-
|
|
217
|
-
statement_info = self.planner.get_statement_info()
|
|
218
|
-
|
|
219
|
-
self.columns_list = []
|
|
220
|
-
for col in statement_info['columns']:
|
|
221
|
-
self.columns_list.append(
|
|
222
|
-
Column(
|
|
223
|
-
database=col['ds'],
|
|
224
|
-
table_name=col['table_name'],
|
|
225
|
-
table_alias=col['table_alias'],
|
|
226
|
-
name=col['name'],
|
|
227
|
-
alias=col['alias'],
|
|
228
|
-
type=col['type']
|
|
229
|
-
)
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
self.parameters = [
|
|
193
|
+
self.columns_list = []
|
|
194
|
+
for col in statement_info['columns']:
|
|
195
|
+
self.columns_list.append(
|
|
233
196
|
Column(
|
|
197
|
+
database=col['ds'],
|
|
198
|
+
table_name=col['table_name'],
|
|
199
|
+
table_alias=col['table_alias'],
|
|
234
200
|
name=col['name'],
|
|
235
201
|
alias=col['alias'],
|
|
236
202
|
type=col['type']
|
|
237
203
|
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
self.parameters = [
|
|
207
|
+
Column(
|
|
208
|
+
name=col['name'],
|
|
209
|
+
alias=col['alias'],
|
|
210
|
+
type=col['type']
|
|
211
|
+
)
|
|
212
|
+
for col in statement_info['parameters']
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
def execute_query(self):
|
|
242
216
|
if self.fetched_data is not None:
|
|
243
217
|
# no need to execute
|
|
244
218
|
return
|
|
@@ -246,7 +220,7 @@ class SQLQuery:
|
|
|
246
220
|
step_result = None
|
|
247
221
|
process_mark = None
|
|
248
222
|
try:
|
|
249
|
-
steps = list(self.planner.execute_steps(
|
|
223
|
+
steps = list(self.planner.execute_steps())
|
|
250
224
|
steps_classes = (x.__class__ for x in steps)
|
|
251
225
|
predict_steps = (ApplyPredictorRowStep, ApplyPredictorStep, ApplyTimeseriesPredictorStep)
|
|
252
226
|
if any(s in predict_steps for s in steps_classes):
|
|
@@ -270,27 +244,7 @@ class SQLQuery:
|
|
|
270
244
|
if len(self.steps_data) == 0:
|
|
271
245
|
return
|
|
272
246
|
|
|
273
|
-
|
|
274
|
-
if self.outer_query is not None:
|
|
275
|
-
# workaround for subqueries in superset. remove it?
|
|
276
|
-
# +++
|
|
277
|
-
# ???
|
|
278
|
-
|
|
279
|
-
result = step_result
|
|
280
|
-
df = result.to_df()
|
|
281
|
-
|
|
282
|
-
df2 = query_df(df, self.outer_query)
|
|
283
|
-
|
|
284
|
-
result2 = ResultSet().from_df(df2, database='', table_name='')
|
|
285
|
-
|
|
286
|
-
self.columns_list = result2.columns
|
|
287
|
-
self.fetched_data = result2
|
|
288
|
-
|
|
289
|
-
else:
|
|
290
|
-
result = step_result
|
|
291
|
-
self.fetched_data = result
|
|
292
|
-
except Exception as e:
|
|
293
|
-
raise UnknownError("error in preparing result query step") from e
|
|
247
|
+
self.fetched_data = step_result
|
|
294
248
|
|
|
295
249
|
try:
|
|
296
250
|
if hasattr(self, 'columns_list') is False:
|
|
@@ -44,6 +44,5 @@ class DeleteStepCall(BaseStepCall):
|
|
|
44
44
|
|
|
45
45
|
query_traversal(query.where, fill_params)
|
|
46
46
|
|
|
47
|
-
dn.query(query=query, session=self.session)
|
|
48
|
-
|
|
49
|
-
return ResultSet()
|
|
47
|
+
response = dn.query(query=query, session=self.session)
|
|
48
|
+
return ResultSet(affected_rows=response.affected_rows)
|
|
@@ -89,10 +89,11 @@ class FetchDataframeStepCall(BaseStepCall):
|
|
|
89
89
|
table_alias = (self.context.get('database'), 'result', 'result')
|
|
90
90
|
|
|
91
91
|
# fetch raw_query
|
|
92
|
-
|
|
92
|
+
response = dn.query(
|
|
93
93
|
native_query=step.raw_query,
|
|
94
94
|
session=self.session
|
|
95
95
|
)
|
|
96
|
+
df = response.data_frame
|
|
96
97
|
else:
|
|
97
98
|
table_alias = get_table_alias(step.query.from_table, self.context.get('database'))
|
|
98
99
|
|
|
@@ -104,13 +105,14 @@ class FetchDataframeStepCall(BaseStepCall):
|
|
|
104
105
|
|
|
105
106
|
query, context_callback = query_context_controller.handle_db_context_vars(query, dn, self.session)
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
response = dn.query(
|
|
108
109
|
query=query,
|
|
109
110
|
session=self.session
|
|
110
111
|
)
|
|
112
|
+
df = response.data_frame
|
|
111
113
|
|
|
112
114
|
if context_callback:
|
|
113
|
-
context_callback(df,
|
|
115
|
+
context_callback(df, response.columns)
|
|
114
116
|
|
|
115
117
|
result = ResultSet()
|
|
116
118
|
|
|
@@ -91,13 +91,13 @@ class InsertToTableCall(BaseStepCall):
|
|
|
91
91
|
else:
|
|
92
92
|
col_names.add(col.alias)
|
|
93
93
|
|
|
94
|
-
dn.create_table(
|
|
94
|
+
response = dn.create_table(
|
|
95
95
|
table_name=table_name,
|
|
96
96
|
result_set=data,
|
|
97
97
|
is_replace=is_replace,
|
|
98
98
|
is_create=is_create
|
|
99
99
|
)
|
|
100
|
-
return ResultSet()
|
|
100
|
+
return ResultSet(affected_rows=response.affected_rows)
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class SaveToTableCall(InsertToTableCall):
|
|
@@ -47,10 +47,10 @@ class GetTableColumnsCall(BaseStepCall):
|
|
|
47
47
|
dn = self.session.datahub.get(step.namespace)
|
|
48
48
|
ds_query = Select(from_table=Identifier(table), targets=[Star()], limit=Constant(0))
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
response = dn.query(ds_query, session=self.session)
|
|
51
51
|
|
|
52
52
|
data = ResultSet()
|
|
53
|
-
for column in
|
|
53
|
+
for column in response.columns:
|
|
54
54
|
data.add_column(Column(
|
|
55
55
|
name=column['name'],
|
|
56
56
|
type=column.get('type'),
|
|
@@ -3,13 +3,7 @@ from collections import defaultdict
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
|
|
5
5
|
from mindsdb_sql_parser.ast import (
|
|
6
|
-
Identifier,
|
|
7
|
-
Select,
|
|
8
|
-
Star,
|
|
9
|
-
Constant,
|
|
10
|
-
Parameter,
|
|
11
|
-
Function,
|
|
12
|
-
Variable
|
|
6
|
+
Identifier, Select, Star, Constant, Parameter, Function, Variable, BinaryOperation
|
|
13
7
|
)
|
|
14
8
|
|
|
15
9
|
from mindsdb.api.mysql.mysql_proxy.libs.constants.mysql import SERVER_VARIABLES
|
|
@@ -87,7 +81,7 @@ class QueryStepCall(BaseStepCall):
|
|
|
87
81
|
|
|
88
82
|
bind = QueryStep
|
|
89
83
|
|
|
90
|
-
def call(self, step):
|
|
84
|
+
def call(self, step: QueryStep):
|
|
91
85
|
query = step.query
|
|
92
86
|
|
|
93
87
|
if step.from_table is not None:
|
|
@@ -190,6 +184,24 @@ class QueryStepCall(BaseStepCall):
|
|
|
190
184
|
fill_params = get_fill_param_fnc(self.steps_data)
|
|
191
185
|
query_traversal(query, fill_params)
|
|
192
186
|
|
|
187
|
+
if not step.strict_where:
|
|
188
|
+
# remove conditions with not-existed columns.
|
|
189
|
+
# these conditions can be already used as input to model or knowledge base
|
|
190
|
+
# but can be absent in their output
|
|
191
|
+
|
|
192
|
+
def remove_not_used_conditions(node, **kwargs):
|
|
193
|
+
# find last in where
|
|
194
|
+
if isinstance(node, BinaryOperation):
|
|
195
|
+
for arg in node.args:
|
|
196
|
+
if isinstance(arg, Identifier) and len(arg.parts) > 1:
|
|
197
|
+
key = tuple(arg.parts[-2:])
|
|
198
|
+
if key not in col_idx:
|
|
199
|
+
# exclude
|
|
200
|
+
node.args = [Constant(0), Constant(0)]
|
|
201
|
+
node.op = '='
|
|
202
|
+
|
|
203
|
+
query_traversal(query.where, remove_not_used_conditions)
|
|
204
|
+
|
|
193
205
|
query_traversal(query, check_fields)
|
|
194
206
|
query.where = query_context_controller.remove_lasts(query.where)
|
|
195
207
|
|
|
@@ -18,8 +18,6 @@ class UpdateToTableCall(BaseStepCall):
|
|
|
18
18
|
bind = UpdateToTable
|
|
19
19
|
|
|
20
20
|
def call(self, step):
|
|
21
|
-
data = ResultSet()
|
|
22
|
-
|
|
23
21
|
if len(step.table.parts) > 1:
|
|
24
22
|
integration_name = step.table.parts[0]
|
|
25
23
|
table_name_parts = step.table.parts[1:]
|
|
@@ -85,8 +83,8 @@ class UpdateToTableCall(BaseStepCall):
|
|
|
85
83
|
|
|
86
84
|
if result_step is None:
|
|
87
85
|
# run as is
|
|
88
|
-
dn.query(query=update_query, session=self.session)
|
|
89
|
-
return
|
|
86
|
+
response = dn.query(query=update_query, session=self.session)
|
|
87
|
+
return ResultSet(affected_rows=response.affected_rows)
|
|
90
88
|
result_data = self.steps_data[result_step.result.step_num]
|
|
91
89
|
|
|
92
90
|
# link nodes with parameters for fast replacing with values
|
|
@@ -125,5 +123,5 @@ class UpdateToTableCall(BaseStepCall):
|
|
|
125
123
|
for param_name, param in params_map_index:
|
|
126
124
|
param.value = row[param_name]
|
|
127
125
|
|
|
128
|
-
dn.query(query=update_query, session=self.session)
|
|
129
|
-
return
|
|
126
|
+
response = dn.query(query=update_query, session=self.session)
|
|
127
|
+
return ResultSet(affected_rows=response.affected_rows)
|
|
@@ -59,7 +59,10 @@ class Query(Resource):
|
|
|
59
59
|
result = mysql_proxy.process_query(query)
|
|
60
60
|
|
|
61
61
|
if result.type == SQL_RESPONSE_TYPE.OK:
|
|
62
|
-
query_response = {
|
|
62
|
+
query_response = {
|
|
63
|
+
"type": SQL_RESPONSE_TYPE.OK,
|
|
64
|
+
"affected_rows": result.affected_rows
|
|
65
|
+
}
|
|
63
66
|
elif result.type == SQL_RESPONSE_TYPE.TABLE:
|
|
64
67
|
data = result.data.to_lists(json_types=True)
|
|
65
68
|
query_response = {
|
|
File without changes
|
mindsdb/api/mcp/start.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
from mindsdb.api.mysql.mysql_proxy.classes.fake_mysql_proxy import FakeMysqlProxy
|
|
8
|
+
from mindsdb.api.executor.data_types.response_type import RESPONSE_TYPE as SQL_RESPONSE_TYPE
|
|
9
|
+
from mindsdb.utilities import log
|
|
10
|
+
from mindsdb.utilities.config import Config
|
|
11
|
+
from mindsdb.interfaces.storage import db
|
|
12
|
+
|
|
13
|
+
logger = log.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AppContext:
|
|
18
|
+
db: Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@asynccontextmanager
|
|
22
|
+
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
23
|
+
"""Manage application lifecycle with type-safe context"""
|
|
24
|
+
# Initialize on startup
|
|
25
|
+
db.init()
|
|
26
|
+
try:
|
|
27
|
+
yield AppContext(db=db)
|
|
28
|
+
finally:
|
|
29
|
+
# TODO: We need better way to handle this in storage/db.py
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Configure server with lifespan
|
|
34
|
+
mcp = FastMCP(
|
|
35
|
+
"MindsDB",
|
|
36
|
+
lifespan=app_lifespan,
|
|
37
|
+
dependencies=["mindsdb"] # Add any additional dependencies
|
|
38
|
+
)
|
|
39
|
+
# MCP Queries
|
|
40
|
+
LISTING_QUERY = "SHOW DATABASES"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def query(query: str, context: Optional[Dict] = None) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Execute a SQL query against MindsDB
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
query: The SQL query to execute
|
|
50
|
+
context: Optional context parameters for the query
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dict containing the query results or error information
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
if context is None:
|
|
57
|
+
context = {}
|
|
58
|
+
|
|
59
|
+
logger.debug(f'Incoming MCP query: {query}')
|
|
60
|
+
|
|
61
|
+
mysql_proxy = FakeMysqlProxy()
|
|
62
|
+
mysql_proxy.set_context(context)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = mysql_proxy.process_query(query)
|
|
66
|
+
|
|
67
|
+
if result.type == SQL_RESPONSE_TYPE.OK:
|
|
68
|
+
return {"type": SQL_RESPONSE_TYPE.OK}
|
|
69
|
+
|
|
70
|
+
if result.type == SQL_RESPONSE_TYPE.TABLE:
|
|
71
|
+
return {
|
|
72
|
+
"type": SQL_RESPONSE_TYPE.TABLE,
|
|
73
|
+
"data": result.data.to_lists(json_types=True),
|
|
74
|
+
"column_names": [
|
|
75
|
+
x["alias"] or x["name"] if "alias" in x else x["name"]
|
|
76
|
+
for x in result.columns
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
else:
|
|
80
|
+
return {
|
|
81
|
+
"type": SQL_RESPONSE_TYPE.ERROR,
|
|
82
|
+
"error_code": 0,
|
|
83
|
+
"error_message": "Unknown response type"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Error processing query: {str(e)}")
|
|
88
|
+
return {
|
|
89
|
+
"type": SQL_RESPONSE_TYPE.ERROR,
|
|
90
|
+
"error_code": 0,
|
|
91
|
+
"error_message": str(e)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def list_databases() -> Dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
List all databases in MindsDB along with their tables
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict containing the list of databases and their tables
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
mysql_proxy = FakeMysqlProxy()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = mysql_proxy.process_query(LISTING_QUERY)
|
|
108
|
+
if result.type == SQL_RESPONSE_TYPE.ERROR:
|
|
109
|
+
return {
|
|
110
|
+
"type": "error",
|
|
111
|
+
"error_code": result.error_code,
|
|
112
|
+
"error_message": result.error_message,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
elif result.type == SQL_RESPONSE_TYPE.OK:
|
|
116
|
+
return {"type": "ok"}
|
|
117
|
+
|
|
118
|
+
elif result.type == SQL_RESPONSE_TYPE.TABLE:
|
|
119
|
+
data = result.data.to_lists(json_types=True)
|
|
120
|
+
return data
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return {
|
|
124
|
+
"type": "error",
|
|
125
|
+
"error_code": 0,
|
|
126
|
+
"error_message": str(e),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def start(*args, **kwargs):
|
|
131
|
+
"""Start the MCP server
|
|
132
|
+
Args:
|
|
133
|
+
host (str): Host to bind to
|
|
134
|
+
port (int): Port to listen on
|
|
135
|
+
"""
|
|
136
|
+
config = Config()
|
|
137
|
+
port = int(config['api'].get('mcp', {}).get('port', 47337))
|
|
138
|
+
host = config['api'].get('mcp', {}).get('host', '127.0.0.1')
|
|
139
|
+
|
|
140
|
+
logger.info(f"Starting MCP server on {host}:{port}")
|
|
141
|
+
mcp.settings.host = host
|
|
142
|
+
mcp.settings.port = port
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
mcp.run(transport="sse") # Use SSE transport instead of stdio
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Error starting MCP server: {str(e)}")
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
start()
|
|
@@ -40,7 +40,7 @@ class OkPacket(Packet):
|
|
|
40
40
|
def setup(self):
|
|
41
41
|
eof = self._kwargs.get('eof', False)
|
|
42
42
|
self.ok_header = Datum('int<1>', 0xFE if eof is True else 0)
|
|
43
|
-
self.affected_rows = Datum('int<lenenc>', self._kwargs.get('affected_rows'
|
|
43
|
+
self.affected_rows = Datum('int<lenenc>', self._kwargs.get('affected_rows') or 0)
|
|
44
44
|
self.last_insert_id = Datum('int<lenenc>', 0)
|
|
45
45
|
status = self._kwargs.get('status', 0x0002)
|
|
46
46
|
self.server_status = Datum('int<2>', status)
|
|
@@ -4,6 +4,7 @@ from mindsdb.api.executor.planner import utils as planner_utils
|
|
|
4
4
|
import mindsdb.utilities.profiler as profiler
|
|
5
5
|
from mindsdb.api.executor.sql_query.result_set import Column
|
|
6
6
|
from mindsdb.api.executor.sql_query import SQLQuery
|
|
7
|
+
from mindsdb.api.executor.data_types.answer import ExecuteAnswer
|
|
7
8
|
from mindsdb.api.executor.command_executor import ExecuteCommands
|
|
8
9
|
from mindsdb.api.mysql.mysql_proxy.utilities import ErSqlSyntaxError
|
|
9
10
|
from mindsdb.utilities import log
|
|
@@ -12,37 +13,20 @@ logger = log.getLogger(__name__)
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class Executor:
|
|
15
|
-
"""This class stores initial and intermediate params
|
|
16
|
-
between different steps of query execution. And it is also
|
|
17
|
-
creates a separate instance of ExecuteCommands to execute the current
|
|
18
|
-
query step.
|
|
19
|
-
|
|
20
|
-
IMPORTANT: A public API of this class is a contract.
|
|
21
|
-
And there are at least 2 classes strongly depend on it:
|
|
22
|
-
ExecuctorClient
|
|
23
|
-
ExecutorService.
|
|
24
|
-
These classes do the same work as Executor when
|
|
25
|
-
MindsDB works in 'modularity' mode.
|
|
26
|
-
Thus please make sure that IF you change the API,
|
|
27
|
-
you must update the API of these two classes as well!"""
|
|
28
|
-
|
|
29
16
|
def __init__(self, session, sqlserver):
|
|
30
17
|
self.session = session
|
|
31
18
|
self.sqlserver = sqlserver
|
|
32
19
|
|
|
33
20
|
self.query = None
|
|
34
21
|
|
|
35
|
-
# returned values
|
|
36
|
-
# all this attributes needs to be added in
|
|
37
|
-
# self.json() method
|
|
38
22
|
self.columns = []
|
|
39
23
|
self.params = []
|
|
40
24
|
self.data = None
|
|
41
|
-
self.state_track = None
|
|
42
25
|
self.server_status = None
|
|
43
26
|
self.is_executed = False
|
|
44
27
|
self.error_message = None
|
|
45
28
|
self.error_code = None
|
|
29
|
+
self.executor_answer: ExecuteAnswer = None
|
|
46
30
|
|
|
47
31
|
self.sql = ""
|
|
48
32
|
self.sql_lower = ""
|
|
@@ -126,14 +110,7 @@ class Executor:
|
|
|
126
110
|
if self.is_executed:
|
|
127
111
|
return
|
|
128
112
|
|
|
129
|
-
|
|
130
|
-
self.
|
|
131
|
-
self.error_message = ret.error_message
|
|
113
|
+
executor_answer: ExecuteAnswer = self.command_executor.execute_command(self.query)
|
|
114
|
+
self.executor_answer = executor_answer
|
|
132
115
|
|
|
133
116
|
self.is_executed = True
|
|
134
|
-
|
|
135
|
-
if ret.data is not None:
|
|
136
|
-
self.data = ret.data
|
|
137
|
-
self.columns = ret.data.columns
|
|
138
|
-
|
|
139
|
-
self.state_track = ret.state_track
|