MindsDB 25.8.2.0__py3-none-any.whl → 25.9.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.
Potentially problematic release.
This version of MindsDB might be problematic. Click here for more details.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +5 -45
- mindsdb/api/a2a/__init__.py +52 -0
- mindsdb/api/a2a/agent.py +17 -28
- mindsdb/api/a2a/common/server/server.py +17 -36
- mindsdb/api/a2a/common/server/task_manager.py +14 -28
- mindsdb/api/a2a/common/types.py +3 -4
- mindsdb/api/a2a/task_manager.py +43 -55
- mindsdb/api/a2a/utils.py +63 -0
- mindsdb/api/common/middleware.py +106 -0
- mindsdb/api/http/initialize.py +13 -15
- mindsdb/api/http/namespaces/agents.py +6 -7
- mindsdb/api/http/namespaces/auth.py +6 -14
- mindsdb/api/http/namespaces/config.py +0 -2
- mindsdb/api/http/namespaces/default.py +74 -106
- mindsdb/api/http/start.py +25 -44
- mindsdb/api/litellm/start.py +11 -10
- mindsdb/api/mcp/__init__.py +165 -0
- mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +33 -64
- mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +86 -85
- mindsdb/integrations/handlers/crate_handler/crate_handler.py +3 -7
- mindsdb/integrations/handlers/derby_handler/derby_handler.py +32 -34
- mindsdb/integrations/handlers/documentdb_handler/requirements.txt +1 -0
- mindsdb/integrations/handlers/dummy_data_handler/dummy_data_handler.py +12 -13
- mindsdb/integrations/handlers/google_books_handler/google_books_handler.py +45 -44
- mindsdb/integrations/handlers/google_calendar_handler/google_calendar_handler.py +101 -95
- mindsdb/integrations/handlers/google_content_shopping_handler/google_content_shopping_handler.py +129 -129
- mindsdb/integrations/handlers/google_fit_handler/google_fit_handler.py +59 -43
- mindsdb/integrations/handlers/google_search_handler/google_search_handler.py +38 -39
- mindsdb/integrations/handlers/informix_handler/informix_handler.py +5 -18
- mindsdb/integrations/handlers/maxdb_handler/maxdb_handler.py +22 -28
- mindsdb/integrations/handlers/monetdb_handler/monetdb_handler.py +3 -7
- mindsdb/integrations/handlers/mongodb_handler/mongodb_handler.py +53 -67
- mindsdb/integrations/handlers/mongodb_handler/requirements.txt +1 -0
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_ast.py +43 -68
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_parser.py +17 -25
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_query.py +10 -16
- mindsdb/integrations/handlers/mongodb_handler/utils/mongodb_render.py +43 -69
- mindsdb/integrations/libs/base.py +1 -1
- mindsdb/interfaces/agents/constants.py +17 -2
- mindsdb/interfaces/agents/langchain_agent.py +83 -18
- mindsdb/interfaces/knowledge_base/controller.py +3 -1
- mindsdb/interfaces/skills/custom/text2sql/mindsdb_sql_toolkit.py +7 -1
- mindsdb/interfaces/skills/skill_tool.py +7 -1
- mindsdb/interfaces/skills/sql_agent.py +6 -2
- mindsdb/utilities/config.py +3 -155
- mindsdb/utilities/fs.py +10 -4
- mindsdb/utilities/log.py +0 -25
- mindsdb/utilities/starters.py +0 -39
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/METADATA +265 -263
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/RECORD +54 -98
- mindsdb/api/a2a/__main__.py +0 -144
- mindsdb/api/a2a/run_a2a.py +0 -86
- mindsdb/api/common/check_auth.py +0 -42
- mindsdb/api/http/gunicorn_wrapper.py +0 -17
- mindsdb/api/mcp/start.py +0 -205
- mindsdb/api/mongo/__init__.py +0 -0
- mindsdb/api/mongo/classes/__init__.py +0 -5
- mindsdb/api/mongo/classes/query_sql.py +0 -19
- mindsdb/api/mongo/classes/responder.py +0 -45
- mindsdb/api/mongo/classes/responder_collection.py +0 -34
- mindsdb/api/mongo/classes/scram.py +0 -86
- mindsdb/api/mongo/classes/session.py +0 -23
- mindsdb/api/mongo/functions/__init__.py +0 -19
- mindsdb/api/mongo/responders/__init__.py +0 -73
- mindsdb/api/mongo/responders/add_shard.py +0 -13
- mindsdb/api/mongo/responders/aggregate.py +0 -90
- mindsdb/api/mongo/responders/buildinfo.py +0 -17
- mindsdb/api/mongo/responders/coll_stats.py +0 -63
- mindsdb/api/mongo/responders/company_id.py +0 -25
- mindsdb/api/mongo/responders/connection_status.py +0 -22
- mindsdb/api/mongo/responders/count.py +0 -21
- mindsdb/api/mongo/responders/db_stats.py +0 -32
- mindsdb/api/mongo/responders/delete.py +0 -105
- mindsdb/api/mongo/responders/describe.py +0 -23
- mindsdb/api/mongo/responders/end_sessions.py +0 -13
- mindsdb/api/mongo/responders/find.py +0 -175
- mindsdb/api/mongo/responders/get_cmd_line_opts.py +0 -18
- mindsdb/api/mongo/responders/get_free_monitoring_status.py +0 -14
- mindsdb/api/mongo/responders/get_parameter.py +0 -23
- mindsdb/api/mongo/responders/getlog.py +0 -14
- mindsdb/api/mongo/responders/host_info.py +0 -28
- mindsdb/api/mongo/responders/insert.py +0 -270
- mindsdb/api/mongo/responders/is_master.py +0 -20
- mindsdb/api/mongo/responders/is_master_lower.py +0 -13
- mindsdb/api/mongo/responders/list_collections.py +0 -55
- mindsdb/api/mongo/responders/list_databases.py +0 -37
- mindsdb/api/mongo/responders/list_indexes.py +0 -22
- mindsdb/api/mongo/responders/ping.py +0 -13
- mindsdb/api/mongo/responders/recv_chunk_start.py +0 -13
- mindsdb/api/mongo/responders/replsetgetstatus.py +0 -13
- mindsdb/api/mongo/responders/sasl_continue.py +0 -34
- mindsdb/api/mongo/responders/sasl_start.py +0 -33
- mindsdb/api/mongo/responders/update_range_deletions.py +0 -12
- mindsdb/api/mongo/responders/whatsmyuri.py +0 -18
- mindsdb/api/mongo/server.py +0 -388
- mindsdb/api/mongo/start.py +0 -15
- mindsdb/api/mongo/utilities/__init__.py +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from flask import request, session
|
|
1
|
+
from flask import request
|
|
4
2
|
from flask_restx import Resource
|
|
5
3
|
from flask_restx import fields
|
|
6
4
|
|
|
@@ -10,143 +8,113 @@ from mindsdb.api.http.utils import http_error
|
|
|
10
8
|
from mindsdb.metrics.metrics import api_endpoint_metrics
|
|
11
9
|
from mindsdb.utilities.config import Config
|
|
12
10
|
from mindsdb.utilities import log
|
|
11
|
+
from mindsdb.api.common.middleware import generate_pat, revoke_pat, verify_pat
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
logger = log.getLogger(__name__)
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
''' checking whether current user is authenticated
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
bool: True if user authentication is approved
|
|
23
|
-
'''
|
|
24
|
-
config = Config()
|
|
25
|
-
if config['auth']['http_auth_enabled'] is False:
|
|
26
|
-
return True
|
|
27
|
-
|
|
28
|
-
if config['auth'].get('provider') == 'cloud':
|
|
29
|
-
if isinstance(session.get('username'), str) is False:
|
|
30
|
-
return False
|
|
31
|
-
|
|
32
|
-
if config['auth']['oauth']['tokens']['expires_at'] < time.time():
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
return True
|
|
36
|
-
|
|
37
|
-
return session.get('username') == config['auth']['username']
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@ns_conf.route('/login', methods=['POST'])
|
|
17
|
+
@ns_conf.route("/login", methods=["POST"])
|
|
41
18
|
class LoginRoute(Resource):
|
|
42
19
|
@ns_conf.doc(
|
|
43
|
-
responses={
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
body=ns_conf.model('request_login', {
|
|
49
|
-
'username': fields.String(description='Username'),
|
|
50
|
-
'password': fields.String(description='Password')
|
|
51
|
-
})
|
|
20
|
+
responses={200: "Success", 400: "Error in username or password", 401: "Invalid username or password"},
|
|
21
|
+
body=ns_conf.model(
|
|
22
|
+
"request_login",
|
|
23
|
+
{"username": fields.String(description="Username"), "password": fields.String(description="Password")},
|
|
24
|
+
),
|
|
52
25
|
)
|
|
53
|
-
@api_endpoint_metrics(
|
|
26
|
+
@api_endpoint_metrics("POST", "/default/login")
|
|
54
27
|
def post(self):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
password = request.json.get('password')
|
|
28
|
+
"""Check user's credentials and creates a session"""
|
|
29
|
+
username = request.json.get("username")
|
|
30
|
+
password = request.json.get("password")
|
|
59
31
|
if (
|
|
60
|
-
isinstance(username, str) is False
|
|
61
|
-
or
|
|
32
|
+
isinstance(username, str) is False
|
|
33
|
+
or len(username) == 0
|
|
34
|
+
or isinstance(password, str) is False
|
|
35
|
+
or len(password) == 0
|
|
62
36
|
):
|
|
63
|
-
return http_error(
|
|
64
|
-
400, 'Error in username or password',
|
|
65
|
-
'Username and password should be string'
|
|
66
|
-
)
|
|
37
|
+
return http_error(400, "Error in username or password", "Username and password should be string")
|
|
67
38
|
|
|
68
39
|
config = Config()
|
|
69
|
-
inline_username = config[
|
|
70
|
-
inline_password = config[
|
|
40
|
+
inline_username = config["auth"]["username"]
|
|
41
|
+
inline_password = config["auth"]["password"]
|
|
71
42
|
|
|
72
|
-
if
|
|
73
|
-
username
|
|
74
|
-
or password != inline_password
|
|
75
|
-
):
|
|
76
|
-
return http_error(
|
|
77
|
-
401, 'Forbidden',
|
|
78
|
-
'Invalid username or password'
|
|
79
|
-
)
|
|
43
|
+
if username != inline_username or password != inline_password:
|
|
44
|
+
return http_error(401, "Forbidden", "Invalid username or password")
|
|
80
45
|
|
|
81
|
-
|
|
82
|
-
session['username'] = username
|
|
83
|
-
session.permanent = True
|
|
46
|
+
logger.info(f"User '{username}' logged in successfully")
|
|
84
47
|
|
|
85
|
-
return
|
|
48
|
+
return {"token": generate_pat()}, 200
|
|
86
49
|
|
|
87
50
|
|
|
88
|
-
@ns_conf.route(
|
|
51
|
+
@ns_conf.route("/logout", methods=["POST"])
|
|
89
52
|
class LogoutRoute(Resource):
|
|
90
|
-
@ns_conf.doc(
|
|
91
|
-
|
|
92
|
-
200: 'Success'
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
@api_endpoint_metrics('POST', '/default/logout')
|
|
53
|
+
@ns_conf.doc(responses={200: "Success"})
|
|
54
|
+
@api_endpoint_metrics("POST", "/default/logout")
|
|
96
55
|
def post(self):
|
|
97
|
-
|
|
98
|
-
|
|
56
|
+
# We can't forcibly log out a user with the
|
|
57
|
+
h = request.headers.get("Authorization")
|
|
58
|
+
if not h or not h.startswith("Bearer "):
|
|
59
|
+
bearer = None
|
|
60
|
+
else:
|
|
61
|
+
bearer = h.split(" ", 1)[1].strip() or None
|
|
62
|
+
revoke_pat(bearer)
|
|
63
|
+
return "", 200
|
|
99
64
|
|
|
100
65
|
|
|
101
|
-
@ns_conf.route(
|
|
66
|
+
@ns_conf.route("/status")
|
|
102
67
|
class StatusRoute(Resource):
|
|
103
68
|
@ns_conf.doc(
|
|
104
|
-
responses={
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
69
|
+
responses={200: "Success"},
|
|
70
|
+
model=ns_conf.model(
|
|
71
|
+
"response_status",
|
|
72
|
+
{
|
|
73
|
+
"environment": fields.String(description="The name of current environment: cloud, local or other"),
|
|
74
|
+
"mindsdb_version": fields.String(description="Current version of mindsdb"),
|
|
75
|
+
"auth": fields.Nested(
|
|
76
|
+
ns_conf.model(
|
|
77
|
+
"response_status_auth",
|
|
78
|
+
{
|
|
79
|
+
"confirmed": fields.Boolean(description="is current user authenticated"),
|
|
80
|
+
"required": fields.Boolean(description="is authenticated required"),
|
|
81
|
+
"provider": fields.Boolean(description="current authenticated provider: local of 3d-party"),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
),
|
|
118
87
|
)
|
|
119
|
-
@api_endpoint_metrics(
|
|
88
|
+
@api_endpoint_metrics("GET", "/default/status")
|
|
120
89
|
def get(self):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
environment = 'local'
|
|
90
|
+
"""returns auth and environment data"""
|
|
91
|
+
environment = "local"
|
|
124
92
|
config = Config()
|
|
125
93
|
|
|
126
|
-
environment = config.get(
|
|
94
|
+
environment = config.get("environment")
|
|
127
95
|
if environment is None:
|
|
128
|
-
if config.get(
|
|
129
|
-
environment =
|
|
130
|
-
elif config.get(
|
|
131
|
-
environment =
|
|
96
|
+
if config.get("cloud", False):
|
|
97
|
+
environment = "cloud"
|
|
98
|
+
elif config.get("aws_marketplace", False):
|
|
99
|
+
environment = "aws_marketplace"
|
|
132
100
|
else:
|
|
133
|
-
environment =
|
|
101
|
+
environment = "local"
|
|
134
102
|
|
|
135
|
-
auth_provider =
|
|
136
|
-
if config[
|
|
137
|
-
if config[
|
|
138
|
-
auth_provider = config[
|
|
103
|
+
auth_provider = "disabled"
|
|
104
|
+
if config["auth"]["http_auth_enabled"] is True:
|
|
105
|
+
if config["auth"].get("provider") is not None:
|
|
106
|
+
auth_provider = config["auth"].get("provider")
|
|
139
107
|
else:
|
|
140
|
-
auth_provider =
|
|
108
|
+
auth_provider = "local"
|
|
141
109
|
|
|
142
110
|
resp = {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
111
|
+
"mindsdb_version": mindsdb_version,
|
|
112
|
+
"environment": environment,
|
|
113
|
+
"auth": {
|
|
114
|
+
"confirmed": verify_pat(request.headers.get("Authorization", "").replace("Bearer ", "")),
|
|
115
|
+
"http_auth_enabled": config["auth"]["http_auth_enabled"],
|
|
116
|
+
"provider": auth_provider,
|
|
117
|
+
},
|
|
150
118
|
}
|
|
151
119
|
|
|
152
120
|
return resp
|
mindsdb/api/http/start.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import gc
|
|
2
|
+
|
|
2
3
|
gc.disable()
|
|
3
4
|
|
|
4
5
|
from flask import Flask
|
|
5
|
-
from waitress import serve
|
|
6
6
|
|
|
7
7
|
from mindsdb.api.http.initialize import initialize_app
|
|
8
8
|
from mindsdb.interfaces.storage import db
|
|
@@ -10,6 +10,16 @@ from mindsdb.utilities import log
|
|
|
10
10
|
from mindsdb.utilities.config import config
|
|
11
11
|
from mindsdb.utilities.functions import init_lexer_parsers
|
|
12
12
|
from mindsdb.integrations.libs.ml_exec_base import process_cache
|
|
13
|
+
from mindsdb.api.common.middleware import PATAuthMiddleware
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from starlette.applications import Starlette
|
|
17
|
+
from starlette.routing import Mount
|
|
18
|
+
from starlette.middleware.wsgi import WSGIMiddleware
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from mindsdb.api.a2a import get_a2a_app
|
|
22
|
+
from mindsdb.api.mcp import get_mcp_app
|
|
13
23
|
|
|
14
24
|
gc.enable()
|
|
15
25
|
|
|
@@ -23,50 +33,21 @@ def start(verbose, no_studio, app: Flask = None):
|
|
|
23
33
|
if app is None:
|
|
24
34
|
app = initialize_app(config, no_studio)
|
|
25
35
|
|
|
26
|
-
port = config[
|
|
27
|
-
host = config[
|
|
28
|
-
server_type = config['api']['http']['server']['type']
|
|
29
|
-
server_config = config['api']['http']['server']['config']
|
|
30
|
-
|
|
36
|
+
port = config["api"]["http"]["port"]
|
|
37
|
+
host = config["api"]["http"]["host"]
|
|
31
38
|
process_cache.init()
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
elif server_type == "flask":
|
|
42
|
-
logger.debug("Serving HTTP app with flask...")
|
|
43
|
-
# that will 'disable access' log in console
|
|
44
|
-
|
|
45
|
-
app.run(debug=False, port=port, host=host, **server_config)
|
|
46
|
-
elif server_type == 'gunicorn':
|
|
47
|
-
try:
|
|
48
|
-
from mindsdb.api.http.gunicorn_wrapper import StandaloneApplication
|
|
49
|
-
except ImportError:
|
|
50
|
-
logger.error(
|
|
51
|
-
"Gunicorn server is not available by default. If you wish to use it, please install 'gunicorn'"
|
|
52
|
-
)
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
def post_fork(arbiter, worker):
|
|
56
|
-
db.engine.dispose()
|
|
40
|
+
routes = []
|
|
41
|
+
# Specific mounts FIRST
|
|
42
|
+
a2a = get_a2a_app()
|
|
43
|
+
a2a.add_middleware(PATAuthMiddleware)
|
|
44
|
+
mcp = get_mcp_app()
|
|
45
|
+
mcp.add_middleware(PATAuthMiddleware)
|
|
46
|
+
routes.append(Mount("/a2a", app=a2a))
|
|
47
|
+
routes.append(Mount("/mcp", app=mcp))
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
not work for processes created by ProcessPoolExecutor, because they execute forever. We need to explicitly
|
|
61
|
-
call 'shutdown' for such processes before exiting.
|
|
62
|
-
"""
|
|
63
|
-
from mindsdb.integrations.libs.process_cache import process_cache
|
|
64
|
-
process_cache.shutdown(wait=True)
|
|
49
|
+
# Root app LAST so it won't shadow the others
|
|
50
|
+
routes.append(Mount("/", app=WSGIMiddleware(app)))
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'post_fork': post_fork,
|
|
69
|
-
'worker_exit': before_worker_exit,
|
|
70
|
-
**server_config
|
|
71
|
-
}
|
|
72
|
-
StandaloneApplication(app, options).run()
|
|
52
|
+
# Setting logging to None makes uvicorn use the existing logging configuration
|
|
53
|
+
uvicorn.run(Starlette(routes=routes, debug=verbose), host=host, port=int(port), log_level=None, log_config=None)
|
mindsdb/api/litellm/start.py
CHANGED
|
@@ -24,12 +24,12 @@ async def start_async(verbose=False):
|
|
|
24
24
|
project_name = config.cmd_args.project or "mindsdb"
|
|
25
25
|
|
|
26
26
|
# Get MCP server connection details
|
|
27
|
-
mcp_host =
|
|
28
|
-
mcp_port = int(config.get(
|
|
27
|
+
mcp_host = "127.0.0.1"
|
|
28
|
+
mcp_port = int(config.get("api", {}).get("http", {}).get("port", 47334))
|
|
29
29
|
|
|
30
30
|
# Get LiteLLM server settings
|
|
31
|
-
litellm_host = config.get(
|
|
32
|
-
litellm_port = int(config.get(
|
|
31
|
+
litellm_host = config.get("api", {}).get("litellm", {}).get("host", "0.0.0.0")
|
|
32
|
+
litellm_port = int(config.get("api", {}).get("litellm", {}).get("port", 8000))
|
|
33
33
|
|
|
34
34
|
logger.info(f"Starting LiteLLM server for agent '{agent_name}' in project '{project_name}'")
|
|
35
35
|
logger.info(f"Connecting to MCP server at {mcp_host}:{mcp_port}")
|
|
@@ -41,7 +41,7 @@ async def start_async(verbose=False):
|
|
|
41
41
|
mcp_host=mcp_host,
|
|
42
42
|
mcp_port=mcp_port,
|
|
43
43
|
host=litellm_host,
|
|
44
|
-
port=litellm_port
|
|
44
|
+
port=litellm_port,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
@@ -52,6 +52,7 @@ def start(verbose=False):
|
|
|
52
52
|
verbose (bool): Whether to enable verbose logging
|
|
53
53
|
"""
|
|
54
54
|
from mindsdb.interfaces.storage import db
|
|
55
|
+
|
|
55
56
|
db.init()
|
|
56
57
|
|
|
57
58
|
# Run the async function in the event loop
|
|
@@ -64,10 +65,10 @@ def start(verbose=False):
|
|
|
64
65
|
config = Config()
|
|
65
66
|
agent_name = config.cmd_args.agent
|
|
66
67
|
project_name = config.cmd_args.project or "mindsdb"
|
|
67
|
-
mcp_host = config.get(
|
|
68
|
-
mcp_port = int(config.get(
|
|
69
|
-
litellm_host = config.get(
|
|
70
|
-
litellm_port = int(config.get(
|
|
68
|
+
mcp_host = config.get("api", {}).get("mcp", {}).get("host", "127.0.0.1")
|
|
69
|
+
mcp_port = int(config.get("api", {}).get("mcp", {}).get("port", 47337))
|
|
70
|
+
litellm_host = config.get("api", {}).get("litellm", {}).get("host", "0.0.0.0")
|
|
71
|
+
litellm_port = int(config.get("api", {}).get("litellm", {}).get("port", 8000))
|
|
71
72
|
|
|
72
73
|
return run_server(
|
|
73
74
|
agent_name=agent_name,
|
|
@@ -75,7 +76,7 @@ def start(verbose=False):
|
|
|
75
76
|
mcp_host=mcp_host,
|
|
76
77
|
mcp_port=mcp_port,
|
|
77
78
|
host=litellm_host,
|
|
78
|
-
port=litellm_port
|
|
79
|
+
port=litellm_port,
|
|
79
80
|
)
|
|
80
81
|
else:
|
|
81
82
|
logger.error("LiteLLM server initialization failed")
|
mindsdb/api/mcp/__init__.py
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
from typing import Any
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.responses import JSONResponse
|
|
10
|
+
|
|
11
|
+
from mindsdb.api.mysql.mysql_proxy.classes.fake_mysql_proxy import FakeMysqlProxy
|
|
12
|
+
from mindsdb.api.executor.data_types.response_type import RESPONSE_TYPE as SQL_RESPONSE_TYPE
|
|
13
|
+
from mindsdb.interfaces.storage import db
|
|
14
|
+
from mindsdb.utilities import log
|
|
15
|
+
|
|
16
|
+
logger = log.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AppContext:
|
|
21
|
+
db: Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@asynccontextmanager
|
|
25
|
+
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
26
|
+
"""Manage application lifecycle with type-safe context"""
|
|
27
|
+
# Initialize on startup
|
|
28
|
+
db.init()
|
|
29
|
+
try:
|
|
30
|
+
yield AppContext(db=db)
|
|
31
|
+
finally:
|
|
32
|
+
# TODO: We need better way to handle this in storage/db.py
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Configure server with lifespan
|
|
37
|
+
mcp = FastMCP(
|
|
38
|
+
"MindsDB",
|
|
39
|
+
lifespan=app_lifespan,
|
|
40
|
+
dependencies=["mindsdb"], # Add any additional dependencies
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# MCP Queries
|
|
45
|
+
LISTING_QUERY = "SHOW DATABASES"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
query_tool_description = dedent("""\
|
|
49
|
+
Executes a SQL query against MindsDB.
|
|
50
|
+
|
|
51
|
+
A database must be specified either in the `context` parameter or directly in the query string (e.g., `SELECT * FROM my_database.my_table`). Queries like `SELECT * FROM my_table` will fail without a `context`.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
query (str): The SQL query to execute.
|
|
55
|
+
context (dict, optional): The default database context. For example, `{"db": "my_postgres"}`.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A dictionary describing the result.
|
|
59
|
+
- For a successful query with no data to return (e.g., an `UPDATE` statement), the response is `{"type": "ok"}`.
|
|
60
|
+
- If the query returns tabular data, the response is a dictionary containing `data` (a list of rows) and `column_names` (a list of column names). For example: `{"type": "table", "data": [[1, "a"], [2, "b"]], "column_names": ["column_a", "column_b"]}`.
|
|
61
|
+
- In case of an error, a response is `{"type": "error", "error_message": "the error message"}`.
|
|
62
|
+
""")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mcp.tool(name="query", description=query_tool_description)
|
|
66
|
+
def query(query: str, context: dict | None = None) -> dict[str, Any]:
|
|
67
|
+
"""Execute a SQL query against MindsDB
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
query: The SQL query to execute
|
|
71
|
+
context: Optional context parameters for the query
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict containing the query results or error information
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
if context is None:
|
|
78
|
+
context = {}
|
|
79
|
+
|
|
80
|
+
logger.debug(f"Incoming MCP query: {query}")
|
|
81
|
+
|
|
82
|
+
mysql_proxy = FakeMysqlProxy()
|
|
83
|
+
mysql_proxy.set_context(context)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = mysql_proxy.process_query(query)
|
|
87
|
+
|
|
88
|
+
if result.type == SQL_RESPONSE_TYPE.OK:
|
|
89
|
+
return {"type": SQL_RESPONSE_TYPE.OK}
|
|
90
|
+
|
|
91
|
+
if result.type == SQL_RESPONSE_TYPE.TABLE:
|
|
92
|
+
return {
|
|
93
|
+
"type": SQL_RESPONSE_TYPE.TABLE,
|
|
94
|
+
"data": result.result_set.to_lists(json_types=True),
|
|
95
|
+
"column_names": [column.alias or column.name for column in result.result_set.columns],
|
|
96
|
+
}
|
|
97
|
+
else:
|
|
98
|
+
return {"type": SQL_RESPONSE_TYPE.ERROR, "error_code": 0, "error_message": "Unknown response type"}
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error processing query: {str(e)}")
|
|
102
|
+
return {"type": SQL_RESPONSE_TYPE.ERROR, "error_code": 0, "error_message": str(e)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
list_databases_tool_description = (
|
|
106
|
+
"Returns a list of all database connections currently available in MindsDB. "
|
|
107
|
+
+ "The tool takes no parameters and responds with a list of database names, "
|
|
108
|
+
+ 'for example: ["my_postgres", "my_mysql", "test_db"].'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mcp.tool(name="list_databases", description=list_databases_tool_description)
|
|
113
|
+
def list_databases() -> list[str]:
|
|
114
|
+
"""
|
|
115
|
+
List all databases in MindsDB
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list[str]: list of databases
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
mysql_proxy = FakeMysqlProxy()
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
result = mysql_proxy.process_query(LISTING_QUERY)
|
|
125
|
+
if result.type == SQL_RESPONSE_TYPE.ERROR:
|
|
126
|
+
return {
|
|
127
|
+
"type": "error",
|
|
128
|
+
"error_code": result.error_code,
|
|
129
|
+
"error_message": result.error_message,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
elif result.type == SQL_RESPONSE_TYPE.OK:
|
|
133
|
+
return {"type": "ok"}
|
|
134
|
+
|
|
135
|
+
elif result.type == SQL_RESPONSE_TYPE.TABLE:
|
|
136
|
+
data = result.result_set.to_lists(json_types=True)
|
|
137
|
+
data = [val[0] for val in data]
|
|
138
|
+
return data
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return {
|
|
142
|
+
"type": "error",
|
|
143
|
+
"error_code": 0,
|
|
144
|
+
"error_message": str(e),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_status(request: Request) -> JSONResponse:
|
|
149
|
+
"""
|
|
150
|
+
Status endpoint that returns basic server information.
|
|
151
|
+
This endpoint can be used by the frontend to check if the MCP server is running.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
status_info = {
|
|
155
|
+
"status": "ok",
|
|
156
|
+
"service": "mindsdb-mcp",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return JSONResponse(status_info)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_mcp_app():
|
|
163
|
+
app = mcp.sse_app()
|
|
164
|
+
app.add_route("/status", _get_status, methods=["GET"])
|
|
165
|
+
return app
|