MindsDB 25.8.3.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 (94) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +2 -44
  3. mindsdb/api/a2a/__init__.py +52 -0
  4. mindsdb/api/a2a/agent.py +11 -12
  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/task_manager.py +20 -21
  8. mindsdb/api/a2a/utils.py +1 -1
  9. mindsdb/api/common/middleware.py +106 -0
  10. mindsdb/api/http/initialize.py +13 -15
  11. mindsdb/api/http/namespaces/auth.py +6 -14
  12. mindsdb/api/http/namespaces/config.py +0 -2
  13. mindsdb/api/http/namespaces/default.py +74 -106
  14. mindsdb/api/http/start.py +25 -44
  15. mindsdb/api/litellm/start.py +11 -10
  16. mindsdb/api/mcp/__init__.py +165 -0
  17. mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +33 -64
  18. mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +86 -85
  19. mindsdb/integrations/handlers/crate_handler/crate_handler.py +3 -7
  20. mindsdb/integrations/handlers/derby_handler/derby_handler.py +32 -34
  21. mindsdb/integrations/handlers/documentdb_handler/requirements.txt +1 -0
  22. mindsdb/integrations/handlers/dummy_data_handler/dummy_data_handler.py +12 -13
  23. mindsdb/integrations/handlers/google_books_handler/google_books_handler.py +45 -44
  24. mindsdb/integrations/handlers/google_calendar_handler/google_calendar_handler.py +101 -95
  25. mindsdb/integrations/handlers/google_content_shopping_handler/google_content_shopping_handler.py +129 -129
  26. mindsdb/integrations/handlers/google_fit_handler/google_fit_handler.py +59 -43
  27. mindsdb/integrations/handlers/google_search_handler/google_search_handler.py +38 -39
  28. mindsdb/integrations/handlers/informix_handler/informix_handler.py +5 -18
  29. mindsdb/integrations/handlers/maxdb_handler/maxdb_handler.py +22 -28
  30. mindsdb/integrations/handlers/monetdb_handler/monetdb_handler.py +3 -7
  31. mindsdb/integrations/handlers/mongodb_handler/mongodb_handler.py +53 -67
  32. mindsdb/integrations/handlers/mongodb_handler/requirements.txt +1 -0
  33. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_ast.py +43 -68
  34. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_parser.py +17 -25
  35. mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_query.py +10 -16
  36. mindsdb/integrations/handlers/mongodb_handler/utils/mongodb_render.py +43 -69
  37. mindsdb/integrations/libs/base.py +1 -1
  38. mindsdb/interfaces/agents/constants.py +1 -0
  39. mindsdb/interfaces/knowledge_base/controller.py +3 -1
  40. mindsdb/utilities/config.py +3 -155
  41. mindsdb/utilities/log.py +0 -25
  42. mindsdb/utilities/starters.py +0 -39
  43. {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/METADATA +263 -261
  44. {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/RECORD +47 -91
  45. mindsdb/api/a2a/__main__.py +0 -144
  46. mindsdb/api/a2a/run_a2a.py +0 -86
  47. mindsdb/api/common/check_auth.py +0 -42
  48. mindsdb/api/http/gunicorn_wrapper.py +0 -17
  49. mindsdb/api/mcp/start.py +0 -205
  50. mindsdb/api/mongo/__init__.py +0 -0
  51. mindsdb/api/mongo/classes/__init__.py +0 -5
  52. mindsdb/api/mongo/classes/query_sql.py +0 -19
  53. mindsdb/api/mongo/classes/responder.py +0 -45
  54. mindsdb/api/mongo/classes/responder_collection.py +0 -34
  55. mindsdb/api/mongo/classes/scram.py +0 -86
  56. mindsdb/api/mongo/classes/session.py +0 -23
  57. mindsdb/api/mongo/functions/__init__.py +0 -19
  58. mindsdb/api/mongo/responders/__init__.py +0 -73
  59. mindsdb/api/mongo/responders/add_shard.py +0 -13
  60. mindsdb/api/mongo/responders/aggregate.py +0 -90
  61. mindsdb/api/mongo/responders/buildinfo.py +0 -17
  62. mindsdb/api/mongo/responders/coll_stats.py +0 -63
  63. mindsdb/api/mongo/responders/company_id.py +0 -25
  64. mindsdb/api/mongo/responders/connection_status.py +0 -22
  65. mindsdb/api/mongo/responders/count.py +0 -21
  66. mindsdb/api/mongo/responders/db_stats.py +0 -32
  67. mindsdb/api/mongo/responders/delete.py +0 -105
  68. mindsdb/api/mongo/responders/describe.py +0 -23
  69. mindsdb/api/mongo/responders/end_sessions.py +0 -13
  70. mindsdb/api/mongo/responders/find.py +0 -175
  71. mindsdb/api/mongo/responders/get_cmd_line_opts.py +0 -18
  72. mindsdb/api/mongo/responders/get_free_monitoring_status.py +0 -14
  73. mindsdb/api/mongo/responders/get_parameter.py +0 -23
  74. mindsdb/api/mongo/responders/getlog.py +0 -14
  75. mindsdb/api/mongo/responders/host_info.py +0 -28
  76. mindsdb/api/mongo/responders/insert.py +0 -270
  77. mindsdb/api/mongo/responders/is_master.py +0 -20
  78. mindsdb/api/mongo/responders/is_master_lower.py +0 -13
  79. mindsdb/api/mongo/responders/list_collections.py +0 -55
  80. mindsdb/api/mongo/responders/list_databases.py +0 -37
  81. mindsdb/api/mongo/responders/list_indexes.py +0 -22
  82. mindsdb/api/mongo/responders/ping.py +0 -13
  83. mindsdb/api/mongo/responders/recv_chunk_start.py +0 -13
  84. mindsdb/api/mongo/responders/replsetgetstatus.py +0 -13
  85. mindsdb/api/mongo/responders/sasl_continue.py +0 -34
  86. mindsdb/api/mongo/responders/sasl_start.py +0 -33
  87. mindsdb/api/mongo/responders/update_range_deletions.py +0 -12
  88. mindsdb/api/mongo/responders/whatsmyuri.py +0 -18
  89. mindsdb/api/mongo/server.py +0 -388
  90. mindsdb/api/mongo/start.py +0 -15
  91. mindsdb/api/mongo/utilities/__init__.py +0 -0
  92. {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/WHEEL +0 -0
  93. {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/licenses/LICENSE +0 -0
  94. {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,106 @@
1
+ from starlette.middleware.base import BaseHTTPMiddleware
2
+ from starlette.responses import JSONResponse
3
+ from starlette.requests import Request
4
+ from http import HTTPStatus
5
+ from typing import Optional
6
+ import secrets
7
+ import hmac
8
+ import hashlib
9
+ import os
10
+ import traceback
11
+
12
+ from mindsdb.utilities import log
13
+ from mindsdb.utilities.config import config
14
+
15
+ logger = log.getLogger(__name__)
16
+
17
+ SECRET_KEY = os.environ.get("AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
18
+ # We store token (fingerprints) in memory, which means everyone is logged out if the process restarts
19
+ TOKENS = []
20
+
21
+
22
+ def get_pat_fingerprint(token: str) -> str:
23
+ """Hash the token with HMAC-SHA256 using secret_key as pepper."""
24
+ return hmac.new(SECRET_KEY.encode(), token.encode(), hashlib.sha256).hexdigest()
25
+
26
+
27
+ def generate_pat() -> str:
28
+ logger.debug("Generating new auth token")
29
+ token = "pat_" + secrets.token_urlsafe(32)
30
+ TOKENS.append(get_pat_fingerprint(token))
31
+ return token
32
+
33
+
34
+ def verify_pat(raw_token: str) -> bool:
35
+ """Verify if the raw_token matches a stored fingerprint.
36
+ Returns token_id if valid, None if not.
37
+ """
38
+ if not raw_token:
39
+ return False
40
+ fp = get_pat_fingerprint(raw_token)
41
+ for stored_fp in TOKENS:
42
+ if hmac.compare_digest(fp, stored_fp):
43
+ return True
44
+ return False
45
+
46
+
47
+ def revoke_pat(raw_token: str) -> bool:
48
+ """Revoke raw_token from active tokens"""
49
+ if not raw_token:
50
+ return False
51
+ fp = get_pat_fingerprint(raw_token)
52
+ for stored_fp in TOKENS:
53
+ if hmac.compare_digest(fp, stored_fp):
54
+ TOKENS.remove(stored_fp)
55
+ return True
56
+ return False
57
+
58
+
59
+ class PATAuthMiddleware(BaseHTTPMiddleware):
60
+ def _extract_bearer(self, request: Request) -> Optional[str]:
61
+ h = request.headers.get("Authorization")
62
+ if not h or not h.startswith("Bearer "):
63
+ return None
64
+ return h.split(" ", 1)[1].strip() or None
65
+
66
+ async def dispatch(self, request: Request, call_next):
67
+ if config.get("auth", {}).get("http_auth_enabled", False) is False:
68
+ return await call_next(request)
69
+
70
+ token = self._extract_bearer(request)
71
+ if not token or not verify_pat(token):
72
+ return JSONResponse({"detail": "Unauthorized"}, status_code=HTTPStatus.UNAUTHORIZED)
73
+
74
+ request.state.user = config["auth"].get("username")
75
+ return await call_next(request)
76
+
77
+
78
+ # Used by mysql and postgres protocols
79
+ def check_auth(username, password, scramble_func, salt, company_id, config):
80
+ try:
81
+ hardcoded_user = config["auth"].get("username")
82
+ hardcoded_password = config["auth"].get("password")
83
+ if hardcoded_password is None:
84
+ hardcoded_password = ""
85
+ hardcoded_password_hash = scramble_func(hardcoded_password, salt)
86
+ hardcoded_password = hardcoded_password.encode()
87
+
88
+ if password is None:
89
+ password = ""
90
+ if isinstance(password, str):
91
+ password = password.encode()
92
+
93
+ if username != hardcoded_user:
94
+ logger.warning(f"Check auth, user={username}: user mismatch")
95
+ return {"success": False}
96
+
97
+ if password != hardcoded_password and password != hardcoded_password_hash:
98
+ logger.warning(f"check auth, user={username}: password mismatch")
99
+ return {"success": False}
100
+
101
+ logger.info(f"Check auth, user={username}: Ok")
102
+ return {"success": True, "username": username}
103
+ except Exception as e:
104
+ logger.error(f"Check auth, user={username}: ERROR")
105
+ logger.error(e)
106
+ logger.error(traceback.format_exc())
@@ -1,5 +1,4 @@
1
1
  import os
2
- import secrets
3
2
  import mimetypes
4
3
  import threading
5
4
  import traceback
@@ -27,7 +26,7 @@ from mindsdb.api.http.namespaces.chatbots import ns_conf as chatbots_ns
27
26
  from mindsdb.api.http.namespaces.jobs import ns_conf as jobs_ns
28
27
  from mindsdb.api.http.namespaces.config import ns_conf as conf_ns
29
28
  from mindsdb.api.http.namespaces.databases import ns_conf as databases_ns
30
- from mindsdb.api.http.namespaces.default import ns_conf as default_ns, check_auth
29
+ from mindsdb.api.http.namespaces.default import ns_conf as default_ns
31
30
  from mindsdb.api.http.namespaces.file import ns_conf as file_ns
32
31
  from mindsdb.api.http.namespaces.handlers import ns_conf as handlers_ns
33
32
  from mindsdb.api.http.namespaces.knowledge_bases import ns_conf as knowledge_bases_ns
@@ -53,6 +52,7 @@ from mindsdb.utilities.json_encoder import CustomJSONProvider
53
52
  from mindsdb.utilities.ps import is_pid_listen_port, wait_func_is_true
54
53
  from mindsdb.utilities.sentry import sentry_sdk # noqa: F401
55
54
  from mindsdb.utilities.otel import trace # noqa: F401
55
+ from mindsdb.api.common.middleware import verify_pat
56
56
 
57
57
  logger = log.getLogger(__name__)
58
58
 
@@ -314,12 +314,19 @@ def initialize_app(config, no_studio):
314
314
  ctx.set_default()
315
315
  config = Config()
316
316
 
317
+ h = request.headers.get("Authorization")
318
+ if not h or not h.startswith("Bearer "):
319
+ bearer = None
320
+ else:
321
+ bearer = h.split(" ", 1)[1].strip() or None
322
+
317
323
  # region routes where auth is required
318
324
  if (
319
325
  config["auth"]["http_auth_enabled"] is True
320
326
  and any(request.path.startswith(f"/api{ns.path}") for ns in protected_namespaces)
321
- and check_auth() is False
327
+ and verify_pat(bearer) is False
322
328
  ):
329
+ logger.debug(f"Auth failed for path {request.path}")
323
330
  return http_error(
324
331
  HTTPStatus.UNAUTHORIZED,
325
332
  "Unauthorized",
@@ -340,29 +347,23 @@ def initialize_app(config, no_studio):
340
347
  except Exception:
341
348
  user_id = 0
342
349
 
343
- try:
344
- session_id = request.cookies.get("session")
345
- except Exception:
346
- session_id = "unknown"
347
-
348
350
  if company_id is not None:
349
351
  try:
350
352
  company_id = int(company_id)
351
353
  except Exception as e:
352
- logger.error(f"Cloud not parse company id: {company_id} | exception: {e}")
354
+ logger.error(f"Could not parse company id: {company_id} | exception: {e}")
353
355
  company_id = None
354
356
 
355
357
  if user_class is not None:
356
358
  try:
357
359
  user_class = int(user_class)
358
360
  except Exception as e:
359
- logger.error(f"Cloud not parse user_class: {user_class} | exception: {e}")
361
+ logger.error(f"Could not parse user_class: {user_class} | exception: {e}")
360
362
  user_class = 0
361
363
  else:
362
364
  user_class = 0
363
365
 
364
366
  ctx.user_id = user_id
365
- ctx.session_id = session_id
366
367
  ctx.company_id = company_id
367
368
  ctx.user_class = user_class
368
369
  ctx.email_confirmed = email_confirmed
@@ -394,14 +395,11 @@ def initialize_flask(config, init_static_thread, no_studio):
394
395
  FlaskInstrumentor().instrument_app(app)
395
396
  RequestsInstrumentor().instrument()
396
397
 
397
- app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32))
398
- app.config["SESSION_COOKIE_NAME"] = "session"
399
- app.config["PERMANENT_SESSION_LIFETIME"] = config["auth"]["http_permanent_session_lifetime"]
400
398
  app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 60
401
399
  app.config["SWAGGER_HOST"] = "http://localhost:8000/mindsdb"
402
400
  app.json = CustomJSONProvider()
403
401
 
404
- authorizations = {"apikey": {"type": "session", "in": "query", "name": "session"}}
402
+ authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}}
405
403
 
406
404
  logger.debug("Creating swagger API..")
407
405
  api = Swagger_Api(
@@ -4,7 +4,7 @@ import time
4
4
  import urllib
5
5
 
6
6
  import requests
7
- from flask import redirect, request, session, url_for
7
+ from flask import redirect, request, url_for
8
8
  from flask_restx import Resource
9
9
 
10
10
  from mindsdb.api.http.namespaces.configs.auth import ns_conf
@@ -21,9 +21,7 @@ def get_access_token() -> str:
21
21
  Returns:
22
22
  str: token
23
23
  """
24
- return (
25
- Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
26
- )
24
+ return Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
27
25
 
28
26
 
29
27
  def request_user_info(access_token: str = None) -> dict:
@@ -58,7 +56,7 @@ def request_user_info(access_token: str = None) -> dict:
58
56
  @ns_conf.hide
59
57
  class Auth(Resource):
60
58
  @ns_conf.doc(params={"code": "authentification code"})
61
- @api_endpoint_metrics('GET', '/auth/code')
59
+ @api_endpoint_metrics("GET", "/auth/code")
62
60
  def get(self):
63
61
  """callback from auth server if authentification is successful"""
64
62
  config = Config()
@@ -72,9 +70,7 @@ class Auth(Resource):
72
70
  client_id = oauth_meta["client_id"]
73
71
  client_secret = oauth_meta["client_secret"]
74
72
  auth_server = oauth_meta["server_host"]
75
- client_basic = base64.b64encode(
76
- f"{client_id}:{client_secret}".encode()
77
- ).decode()
73
+ client_basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
78
74
 
79
75
  redirect_uri = f"https://{public_hostname}{request.path}"
80
76
  response = requests.post(
@@ -115,7 +111,7 @@ class Auth(Resource):
115
111
  "public_hostname": public_hostname,
116
112
  "ami_id": aws_meta_data.get("ami-id"),
117
113
  },
118
- headers={"Authorization": f'Bearer {tokens["access_token"]}'},
114
+ headers={"Authorization": f"Bearer {tokens['access_token']}"},
119
115
  timeout=5,
120
116
  )
121
117
  if resp.status_code != 200:
@@ -123,10 +119,6 @@ class Auth(Resource):
123
119
  except Exception as e:
124
120
  logger.warning(f"Cant't send request to cloud server: {e}")
125
121
 
126
- session["username"] = user_data["name"]
127
- session["auth_provider"] = "cloud"
128
- session.permanent = True
129
-
130
122
  if request.path.endswith("/auth/callback/cloud_home"):
131
123
  return redirect(f"https://{auth_server}")
132
124
  else:
@@ -140,7 +132,7 @@ class CloudLoginRoute(Resource):
140
132
  responses={302: "Redirect to auth server"},
141
133
  params={"location": "final redirection should lead to that location"},
142
134
  )
143
- @api_endpoint_metrics('GET', '/auth/cloud_login')
135
+ @api_endpoint_metrics("GET", "/auth/cloud_login")
144
136
  def get(self):
145
137
  """redirect to cloud login form"""
146
138
  location = request.args.get("location")
@@ -32,8 +32,6 @@ class GetConfig(Resource):
32
32
  value = config.get(key)
33
33
  if value is not None:
34
34
  resp[key] = value
35
- if "a2a" in config["api"]:
36
- resp["a2a"] = config["api"]["a2a"]
37
35
  return resp
38
36
 
39
37
  @ns_conf.doc("put_config")
@@ -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")