Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
## Refactoring

* #242: Updated to exasol-toolbox 8.2.0
* #139: Added `# nosec` markers to environment variable name constants to suppress false-positive Sonar security warnings.
* #239: Cleaned up settings removing attributes like `xxx_field` from MetaSettings and derived classes.
16 changes: 8 additions & 8 deletions exasol/ai/mcp/server/connection/connection_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
""" Exasol DB server DSN """
ENV_USER = "EXA_USER"
""" The DB user name to be used by the MCP server """
ENV_PASSWORD = "EXA_PASSWORD"
ENV_PASSWORD = "EXA_PASSWORD" # nosec
""" The DB password for password authentication """
ENV_ACCESS_TOKEN = "EXA_ACCESS_TOKEN"
ENV_ACCESS_TOKEN = "EXA_ACCESS_TOKEN" # nosec
""" Bearer access token """
ENV_REFRESH_TOKEN = "EXA_REFRESH_TOKEN"
ENV_REFRESH_TOKEN = "EXA_REFRESH_TOKEN" # nosec
""" Bearer refresh token """
ENV_USERNAME_CLAIM = "EXA_USERNAME_CLAIM"
"""The name of the claim in the access token containing the DB username"""
Expand All @@ -39,9 +39,9 @@
""" SaaS host, defaults to https://cloud.exasol.com/ """
ENV_SAAS_ACCOUNT_ID = "EXA_SAAS_ACCOUNT_ID"
""" SaaS account id """
ENV_SAAS_PAT = "EXA_SAAS_PAT"
ENV_SAAS_PAT = "EXA_SAAS_PAT" # nosec
""" SaaS PAT in case the server's own credentials are used to connect to SaaS DB """
ENV_SAAS_PAT_HEADER = "EXA_SAAS_PAT_HEADER"
ENV_SAAS_PAT_HEADER = "EXA_SAAS_PAT_HEADER" # nosec
""" Name of the header where the SaaS user's PAT is passed, e.g. x-api-key """
ENV_SAAS_DATABASE_ID = "EXA_SAAS_DATABASE_ID"
""" Name of the SaaS database id, if known """
Expand All @@ -53,7 +53,7 @@
""" Directory where Certification Authority (CA) certificates are stored, or a single CA file """
ENV_SSL_CLIENT_CERT = "EXA_SSL_CLIENT_CERT"
""" Own certificate file in PEM format """
ENV_SSL_PRIVATE_KEY = "EXA_SSL_PRIVATE_KEY"
ENV_SSL_PRIVATE_KEY = "EXA_SSL_PRIVATE_KEY" # nosec
""" Certificate's private key file """
ENV_LOG_CLAIMS = "EXA_LOG_CLAIMS"
""" Log OAuth claims if available (yes/no) """
Expand All @@ -67,11 +67,11 @@
""" On-prem BucketFS bucket name ("default" if not specified) """
ENV_BUCKETFS_USER = "EXA_BUCKETFS_USER"
""" On-prem BucketFS user name """
ENV_BUCKETFS_PASSWORD = "EXA_BUCKETFS_PASSWORD"
ENV_BUCKETFS_PASSWORD = "EXA_BUCKETFS_PASSWORD" # nosec
""" On-prem BucketFS user password """
ENV_BUCKETFS_PATH = "EXA_BUCKETFS_PATH"
""" Optional root directory in the bucket (defaults to the bucket root) """
ENV_NO_AUTH_PASSWORD = "EXA_NO_AUTH_PASSWORD"
ENV_NO_AUTH_PASSWORD = "EXA_NO_AUTH_PASSWORD" # nosec
""" Password for unauthenticated clients (e.g. health checks) used when ENV_PASSWORD is absent """

DEFAULT_CONN_POOL_SIZE = 5
Expand Down
193 changes: 51 additions & 142 deletions exasol/ai/mcp/server/setup/server_settings.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
from typing import Annotated

from pydantic import BaseModel
from pydantic.functional_validators import AfterValidator


def check_no_double_quotes(v: str) -> str:
if '"' in v:
raise ValueError("Double-quote characters are not allowed in a field name.")
return v


NoDoubleQuotesStr = Annotated[str, AfterValidator(check_no_double_quotes)]
import warnings
from typing import Any

from pydantic import (
BaseModel,
model_validator,
)

_DEPRECATED_FIELD_SETTINGS = frozenset(
[
"schema_field",
"name_field",
"comment_field",
"type_field",
"constraint_name_field",
"constraint_type_field",
"constraint_columns_field",
"referenced_schema_field",
"referenced_table_field",
"referenced_columns_field",
"columns_field",
"constraints_field",
"table_comment_field",
"usage_field",
"input_field",
"return_field",
"emit_field",
]
)


class MetaSettings(BaseModel):
Expand All @@ -23,20 +39,21 @@ class MetaSettings(BaseModel):
Allows to disable the listing of a particular type of metadata.
"""

schema_field: NoDoubleQuotesStr = "schema"
"""
The name of the output field that contains the object schema, e.g. "table_schema".
"""

name_field: NoDoubleQuotesStr = "name"
"""
The name of the output field that contains the object name, e.g. "table_name".
"""

comment_field: NoDoubleQuotesStr = "comment"
"""
The name of the output field that contains the comment, e.g. "table_comment".
"""
@model_validator(mode="before")
@classmethod
def _warn_deprecated_field_settings(cls, data: Any) -> Any:
if isinstance(data, dict):
found = _DEPRECATED_FIELD_SETTINGS.intersection(data)
if found:
names = ", ".join(sorted(found))
warnings.warn(
f"The following settings have no effect and will be removed in a "
f"future version: {names}. Output field names are fixed and cannot "
f"be configured.",
DeprecationWarning,
stacklevel=2,
)
return data


class MetaListSettings(MetaSettings):
Expand All @@ -63,126 +80,18 @@ class MetaListSettings(MetaSettings):
"""


class MetaColumnSettings(MetaSettings):
"""
The settings for listing columns and constraints when describing a table.
"""

type_field: NoDoubleQuotesStr = "type"
"""
The name of the output field for the column SQL type.
"""

constraint_name_field: NoDoubleQuotesStr = "name"
"""
The name of the output field for the constraint name if it was specified.
"""

constraint_type_field: NoDoubleQuotesStr = "type"
"""
The name of the output field for the constraint type, e.g. PRIMARY KEY.
"""

constraint_columns_field: NoDoubleQuotesStr = "columns"
"""
The name of the output field for the list of columns the constraint is applied to.
"""

referenced_schema_field: NoDoubleQuotesStr = "referenced_schema"
"""
The name of the output field for the referenced schema in the FOREIGN KEY constraint.
"""

referenced_table_field: NoDoubleQuotesStr = "referenced_table"
"""
The name of the output field for the referenced table in the FOREIGN KEY constraint.
"""

referenced_columns_field: NoDoubleQuotesStr = "referenced_columns"
"""
The name of the output field for the list of columns in a referenced table in the
FOREIGN KEY constraint.
"""

columns_field: NoDoubleQuotesStr = "columns"
"""
The name of the output field for the list of columns in a table being described.
"""

constraints_field: NoDoubleQuotesStr = "constraints"
"""
The name of the output field for the list of constraints in a table being described.
"""

table_comment_field: NoDoubleQuotesStr = "table_comment"
"""
The name of the output field for the table/view comment.
"""

usage_field: NoDoubleQuotesStr = "usage"
"""
The name of the output field for general instructions on using a table.
"""


class MetaParameterSettings(MetaSettings):
"""
The settings for listing input/output parameters when describing a function or a
UDF script.
"""

type_field: NoDoubleQuotesStr = "parameter_type"
"""
The name of the output field for the SQL type of a parameter or return value.
"""

input_field: NoDoubleQuotesStr = "inputs"
"""
The name of the output field for the list of input parameters.
"""

return_field: NoDoubleQuotesStr = "returns"
"""
The name of the output field for the return value.
"""

emit_field: NoDoubleQuotesStr = "emits"
"""
The name of the output field for the list of parameters emitted by a UDF.
"""

usage_field: NoDoubleQuotesStr = "usage"
"""
The name of the output field for a function or UDF usage, e.g. a call example.
"""


class McpServerSettings(BaseModel):
"""
MCP server configuration.
"""

schemas: MetaListSettings = MetaListSettings(
name_field="schema_name", comment_field="schema_comment"
)
tables: MetaListSettings = MetaListSettings(
name_field="table_name", comment_field="table_comment"
)
views: MetaListSettings = MetaListSettings(
enable=False, name_field="table_name", comment_field="table_comment"
)
functions: MetaListSettings = MetaListSettings(
name_field="function_name", comment_field="function_comment"
)
scripts: MetaListSettings = MetaListSettings(
name_field="script_name", comment_field="script_comment"
)
columns: MetaColumnSettings = MetaColumnSettings(
name_field="column_name", comment_field="column_comment"
)
parameters: MetaParameterSettings = MetaParameterSettings(
name_field="parameter_name", comment_field="function_comment"
)
schemas: MetaListSettings = MetaListSettings()
tables: MetaListSettings = MetaListSettings()
views: MetaListSettings = MetaListSettings(enable=False)
functions: MetaListSettings = MetaListSettings()
scripts: MetaListSettings = MetaListSettings()
columns: MetaSettings = MetaSettings()
parameters: MetaSettings = MetaSettings()

enable_read_query: bool = False
enable_write_query: bool = False
Expand Down
4 changes: 2 additions & 2 deletions exasol/ai/mcp/server/tools/parameter_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from exasol.ai.mcp.server.connection.db_connection import DbConnection
from exasol.ai.mcp.server.setup.server_settings import (
McpServerSettings,
MetaParameterSettings,
MetaSettings,
)
from exasol.ai.mcp.server.tools.meta_query import (
ExasolMetaQuery,
Expand Down Expand Up @@ -37,7 +37,7 @@


class ParameterParser(ABC):
def __init__(self, connection: DbConnection, conf: MetaParameterSettings) -> None:
def __init__(self, connection: DbConnection, conf: MetaSettings) -> None:
self.connection = connection
self.conf = conf
self._parameter_extract_pattern: re.Pattern | None = None
Expand Down
23 changes: 11 additions & 12 deletions test/integration/tools/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@

from exasol.ai.mcp.server.setup.server_settings import (
McpServerSettings,
MetaColumnSettings,
MetaListSettings,
MetaParameterSettings,
MetaSettings,
)
from exasol.ai.mcp.server.tools.schema.db_output_schema import (
COLUMNS_FIELD,
Expand Down Expand Up @@ -630,7 +629,7 @@ def test_describe_table(
Test the `describe_table` tool. The tool is tested on each table of every schema.
"""
config = McpServerSettings(
columns=MetaColumnSettings(
columns=MetaSettings(
enable=True,
),
case_sensitive=case_sensitive,
Expand All @@ -653,7 +652,7 @@ def test_describe_sys_table(pyexasol_connection) -> None:
"""
Test the `describe_table` tool, passing the name of a system table to it.
"""
config = McpServerSettings(columns=MetaColumnSettings(enable=True))
config = McpServerSettings(columns=MetaSettings(enable=True))
result = run_tool(
pyexasol_connection,
config,
Expand All @@ -675,7 +674,7 @@ def test_describe_view_comment(
pyexasol_connection, setup_database, db_schemas, db_views, case_sensitive
) -> None:
config = McpServerSettings(
columns=MetaColumnSettings(
columns=MetaSettings(
enable=True,
),
case_sensitive=case_sensitive,
Expand Down Expand Up @@ -710,8 +709,8 @@ def test_describe_no_schema_name(
is not provided.
"""
config = McpServerSettings(
columns=MetaColumnSettings(enable=True),
parameters=MetaParameterSettings(enable=True),
columns=MetaSettings(enable=True),
parameters=MetaSettings(enable=True),
)
with pytest.raises(ToolError):
run_tool(pyexasol_connection, config, tool_name=tool_name, **other_kwargs)
Expand All @@ -733,8 +732,8 @@ def test_describe_no_db_object_name(
db object to be described is not provided.
"""
config = McpServerSettings(
columns=MetaColumnSettings(enable=True),
parameters=MetaParameterSettings(enable=True),
columns=MetaSettings(enable=True),
parameters=MetaSettings(enable=True),
)
for schema in db_schemas:
with pytest.raises(ToolError):
Expand All @@ -755,7 +754,7 @@ def test_describe_function(
of every schema.
"""
config = McpServerSettings(
parameters=MetaParameterSettings(
parameters=MetaSettings(
enable=True,
),
case_sensitive=case_sensitive,
Expand Down Expand Up @@ -784,7 +783,7 @@ def test_describe_script(
of every schema.
"""
config = McpServerSettings(
parameters=MetaParameterSettings(
parameters=MetaSettings(
enable=True,
),
case_sensitive=case_sensitive,
Expand Down Expand Up @@ -844,7 +843,7 @@ def test_summarize_table(
"""
config = McpServerSettings(
enable_summarize_table=True,
columns=MetaColumnSettings(enable=True),
columns=MetaSettings(enable=True),
)
ski_resort = next(t for t in db_tables if t.name == "ski_resort")

Expand Down
7 changes: 3 additions & 4 deletions test/integration/tools/test_tool_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@

from exasol.ai.mcp.server.setup.server_settings import (
McpServerSettings,
MetaColumnSettings,
MetaListSettings,
MetaParameterSettings,
MetaSettings,
)


Expand All @@ -23,8 +22,8 @@ def test_tool_hints(pyexasol_connection) -> None:
views=enable_meta_list,
functions=enable_meta_list,
scripts=enable_meta_list,
columns=MetaColumnSettings(enable=True),
parameters=MetaParameterSettings(enable=True),
columns=MetaSettings(enable=True),
parameters=MetaSettings(enable=True),
enable_read_query=True,
enable_write_query=True,
enable_query_profiling=True,
Expand Down
Loading