Skip to content

Commit 80cd97b

Browse files
authored
#149: Optimised resources (#150)
1 parent f68951f commit 80cd97b

File tree

10 files changed

+301
-308
lines changed

10 files changed

+301
-308
lines changed

doc/changes/unreleased.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@
88
* #141: Added annotations to the tools.
99
* #144: Added a reserved words resource.
1010
* #146: Added a resource listing builtin functions.
11+
12+
## Refactoring
13+
14+
* #149: Optimised resources in terms of token usage.

exasol/ai/mcp/server/main.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
str_to_bool,
1818
)
1919
from exasol.ai.mcp.server.mcp_resources import (
20+
builtin_function_categories,
2021
describe_builtin_function,
2122
list_builtin_functions,
2223
)
2324
from exasol.ai.mcp.server.mcp_server import ExasolMCPServer
25+
from exasol.ai.mcp.server.meta_query import SysInfoType
2426
from exasol.ai.mcp.server.server_settings import (
2527
ExaDbResult,
2628
McpServerSettings,
@@ -350,49 +352,80 @@ def list_sql_types() -> ExaDbResult:
350352
return mcp_server.list_sql_types()
351353

352354
@mcp_server.resource(
353-
uri="system://system-tables",
355+
uri="system://system-table/list",
354356
description="List of Exasol system tables.",
355357
annotations={"readOnlyHint": True, "idempotentHint": True},
356358
)
357-
def list_system_tables() -> ExaDbResult:
358-
return mcp_server.list_system_tables()
359+
def list_system_tables() -> list[str]:
360+
return mcp_server.list_system_tables(SysInfoType.SYSTEM)
359361

360362
@mcp_server.resource(
361-
uri="system://statistics-tables",
363+
uri="system://system-table/details/{name}",
364+
description="Information about an Exasol system table with the specified name.",
365+
annotations={"readOnlyHint": True, "idempotentHint": True},
366+
)
367+
def describe_system_table(name: str) -> ExaDbResult:
368+
return mcp_server.describe_system_table(SysInfoType.SYSTEM, name)
369+
370+
@mcp_server.resource(
371+
uri="system://statistics-table/list",
362372
description="List of Exasol statistics tables.",
363373
annotations={"readOnlyHint": True, "idempotentHint": True},
364374
)
365-
def list_statistics_tables() -> ExaDbResult:
366-
return mcp_server.list_statistics_tables()
375+
def list_statistics_tables() -> list[str]:
376+
return mcp_server.list_system_tables(SysInfoType.STATISTICS)
367377

368378
@mcp_server.resource(
369-
uri="dialect://reserved-keywords",
370-
description="List of Exasol reserved keywords.",
379+
uri="system://statistics-table/details/{name}",
380+
description="Information about an Exasol statistics table with the specified name.",
371381
annotations={"readOnlyHint": True, "idempotentHint": True},
372382
)
373-
def list_reserved_keywords() -> ExaDbResult:
374-
return mcp_server.list_reserved_keywords()
383+
def describe_statistics_table(name) -> ExaDbResult:
384+
return mcp_server.describe_system_table(SysInfoType.STATISTICS, name)
375385

376386
@mcp_server.resource(
377-
uri="dialect://built-in-functions/list/{category}",
387+
uri="dialect://keyword/reserved/{letter}",
378388
description=(
379-
"List of Exasol built-in functions by category. Categories: numeric, "
380-
"string, date-time, geospatial, bitwise, conversion, other-scalar, "
381-
"aggregate, analytic. A function can belong to more than one category."
389+
"List of Exasol keywords that are reserved words, "
390+
"and start from a given letter."
382391
),
383392
annotations={"readOnlyHint": True, "idempotentHint": True},
384393
)
385-
def _list_builtin_functions(category: str) -> ExaDbResult:
386-
return list_builtin_functions(category)
394+
def list_reserved_keywords(letter) -> list[str]:
395+
return mcp_server.list_keywords(True, letter)
387396

388397
@mcp_server.resource(
389-
uri="dialect://built-in-functions/details/{name}",
398+
uri="dialect://keyword/non-reserved/{letter}",
390399
description=(
391-
"Information about a built-in function with the specified name. "
392-
"May include usage notes and one or more examples."
400+
"List of Exasol keywords that are not reserved words, "
401+
"and start from a given letter."
393402
),
394403
annotations={"readOnlyHint": True, "idempotentHint": True},
395404
)
405+
def list_non_reserved_keywords(letter: str) -> list[str]:
406+
return mcp_server.list_keywords(False, letter)
407+
408+
@mcp_server.resource(
409+
uri="dialect://built-in-function/categories",
410+
description="Exasol built-in function categories.",
411+
annotations={"readOnlyHint": True, "idempotentHint": True},
412+
)
413+
def _builtin_function_categories() -> list[str]:
414+
return builtin_function_categories()
415+
416+
@mcp_server.resource(
417+
uri="dialect://built-in-function/list/{category}",
418+
description="List of Exasol built-in functions in the specified category.",
419+
annotations={"readOnlyHint": True, "idempotentHint": True},
420+
)
421+
def _list_builtin_functions(category: str) -> list[str]:
422+
return list_builtin_functions(category)
423+
424+
@mcp_server.resource(
425+
uri="dialect://built-in-function/details/{name}",
426+
description="Information about a built-in function with the specified name.",
427+
annotations={"readOnlyHint": True, "idempotentHint": True},
428+
)
396429
def _describe_builtin_function(name: str) -> ExaDbResult:
397430
return describe_builtin_function(name)
398431

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib.resources
22
import json
3+
from functools import cache
34
from typing import Any
45

56
from exasol.ai.mcp.server.server_settings import ExaDbResult
@@ -8,25 +9,33 @@
89
PACKAGE_RESOURCES = f"{__package__}.resources"
910

1011

11-
def list_builtin_functions(category: str) -> ExaDbResult:
12+
@cache
13+
def load_builtin_func_list() -> list[dict[str, Any]]:
14+
with importlib.resources.open_text(PACKAGE_RESOURCES, BUILTIN_FUNCTIONS_JSON) as f:
15+
return json.load(f)
16+
17+
18+
def builtin_function_categories() -> list[str]:
19+
"""
20+
Returns a list of builtin function categories.
21+
"""
22+
func_list = load_builtin_func_list()
23+
categories: set[str] = set()
24+
for func_info in func_list:
25+
categories.update(func_info["types"])
26+
return sorted(categories)
27+
28+
29+
def list_builtin_functions(category: str) -> list[str]:
1230
"""
1331
Selects the list of builtin functions of the specified type (category), reading
14-
the resource json. Only takes the name and the description fields.
32+
the resource json. Returns only the function names.
1533
"""
16-
with importlib.resources.open_text(PACKAGE_RESOURCES, BUILTIN_FUNCTIONS_JSON) as f:
17-
func_list: list[dict[str, Any]] = json.load(f)
18-
allowed_fields = ["name", "description"]
34+
func_list = load_builtin_func_list()
1935
category = category.lower()
20-
selected_info = [
21-
{
22-
field_name: field_value
23-
for field_name, field_value in func_info.items()
24-
if field_name in allowed_fields
25-
}
26-
for func_info in func_list
27-
if category in func_info["types"]
36+
return [
37+
func_info["name"] for func_info in func_list if category in func_info["types"]
2838
]
29-
return ExaDbResult(selected_info)
3039

3140

3241
def describe_builtin_function(name: str) -> ExaDbResult:
@@ -35,8 +44,7 @@ def describe_builtin_function(name: str) -> ExaDbResult:
3544
Returns all fields. Some functions, for example TO_CHAR, can have information in
3645
more than one row.
3746
"""
38-
with importlib.resources.open_text(PACKAGE_RESOURCES, BUILTIN_FUNCTIONS_JSON) as f:
39-
func_list: list[dict[str, Any]] = json.load(f)
47+
func_list = load_builtin_func_list()
4048
name = name.upper()
4149
selected_info = [func_info for func_info in func_list if func_info["name"] == name]
4250
return ExaDbResult(selected_info)

exasol/ai/mcp/server/mcp_server.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def remove_info_column(result: list[dict[str, Any]]) -> list[dict[str, Any]]:
101101
return result
102102

103103

104+
def _take_column(column_name: str, result: ExaDbResult) -> list[str]:
105+
return [row[column_name] for row in result.result]
106+
107+
104108
class ExasolMCPServer(FastMCP):
105109
"""
106110
An Exasol MCP server based on FastMCP.
@@ -341,14 +345,18 @@ def list_sql_types(self) -> ExaDbResult:
341345
query = ExasolMetaQuery.get_sql_types()
342346
return self._execute_meta_query(query)
343347

344-
def list_system_tables(self) -> ExaDbResult:
345-
query = self.meta_query.get_system_tables(SysInfoType.SYSTEM)
346-
return self._execute_meta_query(query)
348+
def list_system_tables(self, info_type: SysInfoType) -> list[str]:
349+
query = self.meta_query.get_system_tables(info_type)
350+
return _take_column(
351+
self.config.tables.name_field, self._execute_meta_query(query)
352+
)
347353

348-
def list_statistics_tables(self) -> ExaDbResult:
349-
query = self.meta_query.get_system_tables(SysInfoType.STATISTICS)
354+
def describe_system_table(
355+
self, info_type: SysInfoType, table_name: str
356+
) -> ExaDbResult:
357+
query = self.meta_query.get_system_tables(info_type, table_name)
350358
return self._execute_meta_query(query)
351359

352-
def list_reserved_keywords(self) -> ExaDbResult:
353-
query = ExasolMetaQuery.get_reserved_keywords()
354-
return self._execute_meta_query(query)
360+
def list_keywords(self, reserved: bool, letter: str) -> list[str]:
361+
query = ExasolMetaQuery.get_keywords(reserved, letter)
362+
return _take_column("KEYWORD", self._execute_meta_query(query))

exasol/ai/mcp/server/meta_query.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,17 @@ def get_sql_types() -> str:
474474
)
475475
return query.sql(dialect="exasol", identify=True)
476476

477-
def get_system_tables(self, info_type: SysInfoType) -> str:
477+
def get_system_tables(
478+
self, info_type: SysInfoType, table_name: str | None = None
479+
) -> str:
478480
"""
479481
Collects names and comments for the system or statistics tables and views.
482+
The output will be restricted to one table if it's name is specified.
480483
"""
481484
conf = self._config.tables
485+
predicates = [self._get_column_eq_predicate("SCHEMA_NAME", info_type.value)]
486+
if table_name:
487+
predicates.append(self._get_column_eq_predicate("OBJECT_NAME", table_name))
482488
query = (
483489
exp.Select()
484490
.select(
@@ -487,20 +493,27 @@ def get_system_tables(self, info_type: SysInfoType) -> str:
487493
exp.column("OBJECT_COMMENT").as_(conf.comment_field),
488494
)
489495
.from_(exp.Table(this="EXA_SYSCAT", db="SYS"))
490-
.where(exp.column("SCHEMA_NAME").eq(exp.Literal.string(info_type.value)))
496+
.where(*predicates)
491497
)
492498
return query.sql(dialect="exasol", identify=True)
493499

494500
@staticmethod
495-
def get_reserved_keywords() -> str:
501+
def get_keywords(reserved: bool, letter: str) -> str:
496502
"""
497-
The query returns a list of reserved words.
503+
The query returns a list of keywords that start from the given letter.
504+
The list is restricted to keywords that are either reserved words or not
505+
reserver words, as per the argument.
498506
"""
499507
query = (
500508
exp.Select()
501509
.select(exp.column("KEYWORD"))
502510
.from_(exp.Table(this="EXA_SQL_KEYWORDS", db="SYS"))
503-
.where(exp.column("RESERVED").eq(exp.Boolean(this=True)))
504-
.order_by(exp.column("RESERVED"))
511+
.where(
512+
exp.column("RESERVED").eq(exp.Boolean(this=reserved)),
513+
exp.func("LEFT", exp.column("KEYWORD"), 1).eq(
514+
exp.Literal.string(letter.upper())
515+
),
516+
)
517+
.order_by(exp.column("KEYWORD"))
505518
)
506519
return query.sql(dialect="exasol", identify=True)

0 commit comments

Comments
 (0)