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.

Files changed (101) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +5 -45
  3. mindsdb/api/a2a/__init__.py +52 -0
  4. mindsdb/api/a2a/agent.py +17 -28
  5. mindsdb/api/a2a/common/server/server.py +17 -36
  6. mindsdb/api/a2a/common/server/task_manager.py +14 -28
  7. mindsdb/api/a2a/common/types.py +3 -4
  8. mindsdb/api/a2a/task_manager.py +43 -55
  9. mindsdb/api/a2a/utils.py +63 -0
  10. mindsdb/api/common/middleware.py +106 -0
  11. mindsdb/api/http/initialize.py +13 -15
  12. mindsdb/api/http/namespaces/agents.py +6 -7
  13. mindsdb/api/http/namespaces/auth.py +6 -14
  14. mindsdb/api/http/namespaces/config.py +0 -2
  15. mindsdb/api/http/namespaces/default.py +74 -106
  16. mindsdb/api/http/start.py +25 -44
  17. mindsdb/api/litellm/start.py +11 -10
  18. mindsdb/api/mcp/__init__.py +165 -0
  19. mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +33 -64
  20. mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +86 -85
  21. mindsdb/integrations/handlers/crate_handler/crate_handler.py +3 -7
  22. mindsdb/integrations/handlers/derby_handler/derby_handler.py +32 -34
  23. mindsdb/integrations/handlers/documentdb_handler/requirements.txt +1 -0
  24. mindsdb/integrations/handlers/dummy_data_handler/dummy_data_handler.py +12 -13
  25. mindsdb/integrations/handlers/google_books_handler/google_books_handler.py +45 -44
  26. mindsdb/integrations/handlers/google_calendar_handler/google_calendar_handler.py +101 -95
  27. mindsdb/integrations/handlers/google_content_shopping_handler/google_content_shopping_handler.py +129 -129
  28. mindsdb/integrations/handlers/google_fit_handler/google_fit_handler.py +59 -43
  29. mindsdb/integrations/handlers/google_search_handler/google_search_handler.py +38 -39
  30. mindsdb/integrations/handlers/informix_handler/informix_handler.py +5 -18
  31. mindsdb/integrations/handlers/maxdb_handler/maxdb_handler.py +22 -28
  32. mindsdb/integrations/handlers/monetdb_handler/monetdb_handler.py +3 -7
  33. mindsdb/integrations/handlers/mongodb_handler/mongodb_handler.py +53 -67
  34. mindsdb/integrations/handlers/mongodb_handler/requirements.txt +1 -0
  35. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_ast.py +43 -68
  36. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_parser.py +17 -25
  37. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_query.py +10 -16
  38. mindsdb/integrations/handlers/mongodb_handler/utils/mongodb_render.py +43 -69
  39. mindsdb/integrations/libs/base.py +1 -1
  40. mindsdb/interfaces/agents/constants.py +17 -2
  41. mindsdb/interfaces/agents/langchain_agent.py +83 -18
  42. mindsdb/interfaces/knowledge_base/controller.py +3 -1
  43. mindsdb/interfaces/skills/custom/text2sql/mindsdb_sql_toolkit.py +7 -1
  44. mindsdb/interfaces/skills/skill_tool.py +7 -1
  45. mindsdb/interfaces/skills/sql_agent.py +6 -2
  46. mindsdb/utilities/config.py +3 -155
  47. mindsdb/utilities/fs.py +10 -4
  48. mindsdb/utilities/log.py +0 -25
  49. mindsdb/utilities/starters.py +0 -39
  50. {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/METADATA +265 -263
  51. {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/RECORD +54 -98
  52. mindsdb/api/a2a/__main__.py +0 -144
  53. mindsdb/api/a2a/run_a2a.py +0 -86
  54. mindsdb/api/common/check_auth.py +0 -42
  55. mindsdb/api/http/gunicorn_wrapper.py +0 -17
  56. mindsdb/api/mcp/start.py +0 -205
  57. mindsdb/api/mongo/__init__.py +0 -0
  58. mindsdb/api/mongo/classes/__init__.py +0 -5
  59. mindsdb/api/mongo/classes/query_sql.py +0 -19
  60. mindsdb/api/mongo/classes/responder.py +0 -45
  61. mindsdb/api/mongo/classes/responder_collection.py +0 -34
  62. mindsdb/api/mongo/classes/scram.py +0 -86
  63. mindsdb/api/mongo/classes/session.py +0 -23
  64. mindsdb/api/mongo/functions/__init__.py +0 -19
  65. mindsdb/api/mongo/responders/__init__.py +0 -73
  66. mindsdb/api/mongo/responders/add_shard.py +0 -13
  67. mindsdb/api/mongo/responders/aggregate.py +0 -90
  68. mindsdb/api/mongo/responders/buildinfo.py +0 -17
  69. mindsdb/api/mongo/responders/coll_stats.py +0 -63
  70. mindsdb/api/mongo/responders/company_id.py +0 -25
  71. mindsdb/api/mongo/responders/connection_status.py +0 -22
  72. mindsdb/api/mongo/responders/count.py +0 -21
  73. mindsdb/api/mongo/responders/db_stats.py +0 -32
  74. mindsdb/api/mongo/responders/delete.py +0 -105
  75. mindsdb/api/mongo/responders/describe.py +0 -23
  76. mindsdb/api/mongo/responders/end_sessions.py +0 -13
  77. mindsdb/api/mongo/responders/find.py +0 -175
  78. mindsdb/api/mongo/responders/get_cmd_line_opts.py +0 -18
  79. mindsdb/api/mongo/responders/get_free_monitoring_status.py +0 -14
  80. mindsdb/api/mongo/responders/get_parameter.py +0 -23
  81. mindsdb/api/mongo/responders/getlog.py +0 -14
  82. mindsdb/api/mongo/responders/host_info.py +0 -28
  83. mindsdb/api/mongo/responders/insert.py +0 -270
  84. mindsdb/api/mongo/responders/is_master.py +0 -20
  85. mindsdb/api/mongo/responders/is_master_lower.py +0 -13
  86. mindsdb/api/mongo/responders/list_collections.py +0 -55
  87. mindsdb/api/mongo/responders/list_databases.py +0 -37
  88. mindsdb/api/mongo/responders/list_indexes.py +0 -22
  89. mindsdb/api/mongo/responders/ping.py +0 -13
  90. mindsdb/api/mongo/responders/recv_chunk_start.py +0 -13
  91. mindsdb/api/mongo/responders/replsetgetstatus.py +0 -13
  92. mindsdb/api/mongo/responders/sasl_continue.py +0 -34
  93. mindsdb/api/mongo/responders/sasl_start.py +0 -33
  94. mindsdb/api/mongo/responders/update_range_deletions.py +0 -12
  95. mindsdb/api/mongo/responders/whatsmyuri.py +0 -18
  96. mindsdb/api/mongo/server.py +0 -388
  97. mindsdb/api/mongo/start.py +0 -15
  98. mindsdb/api/mongo/utilities/__init__.py +0 -0
  99. {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/WHEEL +0 -0
  100. {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/licenses/LICENSE +0 -0
  101. {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 time
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
- def check_auth() -> bool:
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
- 200: 'Success',
45
- 400: 'Error in username or password',
46
- 401: 'Invalid username or password'
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('POST', '/default/login')
26
+ @api_endpoint_metrics("POST", "/default/login")
54
27
  def post(self):
55
- ''' Check user's credentials and creates a session
56
- '''
57
- username = request.json.get('username')
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 or len(username) == 0
61
- or isinstance(password, str) is False or len(password) == 0
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['auth']['username']
70
- inline_password = config['auth']['password']
40
+ inline_username = config["auth"]["username"]
41
+ inline_password = config["auth"]["password"]
71
42
 
72
- if (
73
- username != inline_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
- session.clear()
82
- session['username'] = username
83
- session.permanent = True
46
+ logger.info(f"User '{username}' logged in successfully")
84
47
 
85
- return '', 200
48
+ return {"token": generate_pat()}, 200
86
49
 
87
50
 
88
- @ns_conf.route('/logout', methods=['POST'])
51
+ @ns_conf.route("/logout", methods=["POST"])
89
52
  class LogoutRoute(Resource):
90
- @ns_conf.doc(
91
- responses={
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
- session.clear()
98
- return '', 200
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('/status')
66
+ @ns_conf.route("/status")
102
67
  class StatusRoute(Resource):
103
68
  @ns_conf.doc(
104
- responses={
105
- 200: 'Success'
106
- },
107
- model=ns_conf.model('response_status', {
108
- 'environment': fields.String(description='The name of current environment: cloud, local or other'),
109
- 'mindsdb_version': fields.String(description='Current version of mindsdb'),
110
- 'auth': fields.Nested(
111
- ns_conf.model('response_status_auth', {
112
- 'confirmed': fields.Boolean(description='is current user authenticated'),
113
- 'required': fields.Boolean(description='is authenticated required'),
114
- 'provider': fields.Boolean(description='current authenticated provider: local of 3d-party')
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('GET', '/default/status')
88
+ @api_endpoint_metrics("GET", "/default/status")
120
89
  def get(self):
121
- ''' returns auth and environment data
122
- '''
123
- environment = 'local'
90
+ """returns auth and environment data"""
91
+ environment = "local"
124
92
  config = Config()
125
93
 
126
- environment = config.get('environment')
94
+ environment = config.get("environment")
127
95
  if environment is None:
128
- if config.get('cloud', False):
129
- environment = 'cloud'
130
- elif config.get('aws_marketplace', False):
131
- environment = 'aws_marketplace'
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 = 'local'
101
+ environment = "local"
134
102
 
135
- auth_provider = 'disabled'
136
- if config['auth']['http_auth_enabled'] is True:
137
- if config['auth'].get('provider') is not None:
138
- auth_provider = config['auth'].get('provider')
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 = 'local'
108
+ auth_provider = "local"
141
109
 
142
110
  resp = {
143
- 'mindsdb_version': mindsdb_version,
144
- 'environment': environment,
145
- 'auth': {
146
- 'confirmed': check_auth(),
147
- 'http_auth_enabled': config['auth']['http_auth_enabled'],
148
- 'provider': auth_provider
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['api']['http']['port']
27
- host = config['api']['http']['host']
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
- if server_type == "waitress":
34
- logger.debug("Serving HTTP app with waitress...")
35
- serve(
36
- app,
37
- host='*' if host in ('', '0.0.0.0') else host,
38
- port=port,
39
- **server_config
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
- def before_worker_exit(arbiter, worker):
59
- """Latest version of gunicorn (23.0.0) calls 'join' for each child process before exiting. However this does
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
- options = {
67
- 'bind': f'{host}:{port}',
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)
@@ -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 = config.get('api', {}).get('mcp', {}).get('host', '127.0.0.1')
28
- mcp_port = int(config.get('api', {}).get('mcp', {}).get('port', 47337))
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('api', {}).get('litellm', {}).get('host', '0.0.0.0')
32
- litellm_port = int(config.get('api', {}).get('litellm', {}).get('port', 8000))
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('api', {}).get('mcp', {}).get('host', '127.0.0.1')
68
- mcp_port = int(config.get('api', {}).get('mcp', {}).get('port', 47337))
69
- litellm_host = config.get('api', {}).get('litellm', {}).get('host', '0.0.0.0')
70
- litellm_port = int(config.get('api', {}).get('litellm', {}).get('port', 8000))
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")
@@ -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