1- """MCP tools for database connection — create, update , and test database connections.
1+ """MCP tools for database connections — list, get , and test database connections.
22
3- Each tool gates on the ``administer`` permission. The per-flavor connection shape
4- (which auth modes exist and which ``connection_params`` keys each needs) lives in
5- ``testgen.common.database.connection_service`` and is exposed to the model through
6- the ``testgen://connection-parameters/{flavor}`` resource. Validation and
7- auth-path normalization are delegated to that same module so the rules stay in
8- one place.
3+ The per-flavor connection shape (which auth modes exist and which ``connection_params`` keys
4+ each needs) lives in ``testgen.common.database.connection_service`` and is exposed to the
5+ model through the ``testgen://connection-parameters/{flavor}`` resource. Validation and
6+ auth-path normalization are delegated to that same module so the rules stay in one place.
97"""
108
119from __future__ import annotations
1210
13- from typing import Any
14-
1511from testgen .common .database .connection_service import (
1612 apply_connection_defaults ,
1713 normalize_auth_fields ,
1814 test_connection_status ,
1915)
20- from testgen .common .models import get_current_session , with_database_session
16+ from testgen .common .models import with_database_session
2117from testgen .common .models .connection import Connection
2218from testgen .mcp .exceptions import MCPResourceNotAccessible , MCPUserError
2319from testgen .mcp .permissions import get_project_permissions , mcp_permission
2420from testgen .mcp .tools .common import (
2521 SQL_FLAVOR_CODE_TO_LABEL ,
22+ DocGroup ,
2623 apply_connection_params ,
27- connection_display_fields ,
28- connection_field_labels ,
24+ effective_mode ,
2925 format_flavor_label ,
3026 format_page_footer ,
3127 format_page_info ,
32- infer_mode ,
3328 parse_sql_flavor ,
29+ raise_validation_error ,
30+ render_connection_body ,
3431 resolve_connection ,
3532 validate_connection_fields ,
3633 validate_limit ,
3734 validate_page ,
3835)
3936from testgen .mcp .tools .markdown import MdDoc
4037
38+ _DOC_GROUP = DocGroup .MANAGE
39+
4140
4241@with_database_session
4342@mcp_permission ("view" )
@@ -104,7 +103,7 @@ def get_connection(connection_id: int) -> str:
104103 connection = resolve_connection (connection_id )
105104 doc = MdDoc ()
106105 doc .heading (1 , f"Connection `{ connection .connection_name } `" )
107- _render_connection_body (doc , connection )
106+ render_connection_body (doc , connection )
108107 return doc .render ()
109108
110109
@@ -156,7 +155,7 @@ def test_connection(
156155 connection = Connection (sql_flavor = family , sql_flavor_code = code )
157156
158157 if connection_params is not None or connection_mode is not None :
159- mode = connection_mode if inline else _effective_mode (connection , connection_mode )
158+ mode = connection_mode if inline else effective_mode (connection , connection_mode )
160159 apply_connection_params (connection , connection .sql_flavor_code , mode , connection_params or {})
161160
162161 normalize_auth_fields (connection )
@@ -165,7 +164,7 @@ def test_connection(
165164
166165 errors = validate_connection_fields (connection )
167166 if errors :
168- _raise_validation_error (errors , "Cannot test connection. Required fields missing or invalid." )
167+ raise_validation_error (errors , "Cannot test connection. Required fields missing or invalid." )
169168
170169 status = test_connection_status (connection )
171170
@@ -186,132 +185,3 @@ def test_connection(
186185 if status .details :
187186 doc .code_block (status .details )
188187 return doc .render ()
189-
190-
191- # ---------------------------------------------------------------------------
192- # Helpers
193- # ---------------------------------------------------------------------------
194-
195-
196- def _effective_mode (connection : Connection , connection_mode : str | None ) -> str | None :
197- """Mode label to apply: the explicit override, else the connection's current mode."""
198- if connection_mode is not None :
199- return connection_mode
200- inferred = infer_mode (connection )
201- return str (inferred ) if inferred is not None else None
202-
203-
204- def _raise_validation_error (errors : list [str ], header : str ) -> None :
205- bullets = "\n " .join (f"- { err } " for err in errors )
206- raise MCPUserError (f"{ header } \n \n { bullets } " )
207-
208-
209- def _render_created_connection (connection : Connection ) -> str :
210- doc = MdDoc ()
211- doc .heading (1 , f"Connection `{ connection .connection_name } ` created" )
212- _render_connection_body (doc , connection )
213- return doc .render ()
214-
215-
216- def _render_connection_body (doc : MdDoc , connection : Connection ) -> None :
217- """Render every non-secret connection field below the heading.
218-
219- Shared by ``create_connection`` (the response after a successful create) and
220- ``get_connection`` (read tool) so the surfaced field set stays consistent.
221- Encrypted columns are filtered out via ``ConnField.secret``.
222- """
223- doc .field ("ID" , connection .connection_id , code = True )
224- doc .field ("Project" , connection .project_code , code = True )
225- doc .field ("Type" , format_flavor_label (connection .sql_flavor_code ))
226-
227- # Each populated, non-secret field under its flavor-specific label
228- # (e.g. "Catalog" for Databricks, "Login URL" for Salesforce).
229- for fld in connection_display_fields (connection ):
230- if fld .secret :
231- continue
232- value = getattr (connection , fld .column , None )
233- if value in (None , "" ):
234- continue
235- doc .field (fld .label , value , code = fld .column != "project_port" )
236-
237- doc .field ("Authentication" , _authentication_label (connection ))
238- if connection .max_threads is not None :
239- doc .field ("Max Threads" , connection .max_threads )
240- if connection .max_query_chars is not None :
241- doc .field ("Max Expression Length" , connection .max_query_chars )
242-
243-
244- def _authentication_label (connection : Connection ) -> str :
245- """The connection's auth method: the active connection mode for multi-mode
246- flavors, else the implicit method (service account key, else password).
247- """
248- mode = infer_mode (connection )
249- if mode is not None :
250- return str (mode )
251- if connection .service_account_key :
252- return "Service Account Key"
253- return "Password"
254-
255-
256- def _snapshot (connection : Connection ) -> dict [str , Any ]:
257- return {attr : getattr (connection , attr , None ) for attr in _DIFF_ATTRS }
258-
259-
260- def _render_field_value (attr : str , value : Any ) -> str | None :
261- if attr == "sql_flavor_code" and value is not None :
262- return SQL_FLAVOR_CODE_TO_LABEL .get (value , value ).value
263- if isinstance (value , bool ):
264- return "Yes" if value else "No"
265- if value is None or value == "" :
266- return None
267- return str (value )
268-
269-
270- _DIFF_ATTRS : tuple [str , ...] = (
271- "connection_name" ,
272- "sql_flavor_code" ,
273- "project_host" ,
274- "project_port" ,
275- "project_db" ,
276- "project_user" ,
277- "project_pw_encrypted" ,
278- "url" ,
279- "connect_by_url" ,
280- "connect_by_key" ,
281- "private_key" ,
282- "private_key_passphrase" ,
283- "connect_with_identity" ,
284- "warehouse" ,
285- "http_path" ,
286- "service_account_key" ,
287- "max_threads" ,
288- "max_query_chars" ,
289- )
290-
291- _DIFF_LABELS : dict [str , str ] = {
292- "connection_name" : "Name" ,
293- "sql_flavor_code" : "Type" ,
294- "project_host" : "Host" ,
295- "project_port" : "Port" ,
296- "project_db" : "Database" ,
297- "project_user" : "Username" ,
298- "project_pw_encrypted" : "Password" ,
299- "url" : "URL" ,
300- "connect_by_url" : "Connect by URL" ,
301- "connect_by_key" : "Connect by Key-Pair" ,
302- "private_key" : "Private Key" ,
303- "private_key_passphrase" : "Private Key Passphrase" ,
304- "connect_with_identity" : "Connect with Managed Identity" ,
305- "warehouse" : "Warehouse" ,
306- "http_path" : "HTTP Path" ,
307- "service_account_key" : "Service Account Key" ,
308- "max_threads" : "Max Threads" ,
309- "max_query_chars" : "Max Expression Length" ,
310- }
311-
312- _ATTR_IS_SECRET : dict [str , bool ] = {
313- "project_pw_encrypted" : True ,
314- "private_key" : True ,
315- "private_key_passphrase" : True ,
316- "service_account_key" : True ,
317- }
0 commit comments