sqlserver-semantic-mcp 0.5.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.
Files changed (74) hide show
  1. sqlserver_semantic_mcp/__init__.py +1 -0
  2. sqlserver_semantic_mcp/config.py +78 -0
  3. sqlserver_semantic_mcp/domain/__init__.py +0 -0
  4. sqlserver_semantic_mcp/domain/enums.py +48 -0
  5. sqlserver_semantic_mcp/domain/models/__init__.py +0 -0
  6. sqlserver_semantic_mcp/domain/models/column.py +14 -0
  7. sqlserver_semantic_mcp/domain/models/object.py +13 -0
  8. sqlserver_semantic_mcp/domain/models/relationship.py +11 -0
  9. sqlserver_semantic_mcp/domain/models/table.py +29 -0
  10. sqlserver_semantic_mcp/infrastructure/__init__.py +0 -0
  11. sqlserver_semantic_mcp/infrastructure/background.py +59 -0
  12. sqlserver_semantic_mcp/infrastructure/cache/__init__.py +0 -0
  13. sqlserver_semantic_mcp/infrastructure/cache/semantic.py +132 -0
  14. sqlserver_semantic_mcp/infrastructure/cache/store.py +152 -0
  15. sqlserver_semantic_mcp/infrastructure/cache/structural.py +203 -0
  16. sqlserver_semantic_mcp/infrastructure/connection.py +78 -0
  17. sqlserver_semantic_mcp/infrastructure/queries/__init__.py +0 -0
  18. sqlserver_semantic_mcp/infrastructure/queries/comment_queries.py +18 -0
  19. sqlserver_semantic_mcp/infrastructure/queries/metadata_queries.py +70 -0
  20. sqlserver_semantic_mcp/infrastructure/queries/object_queries.py +15 -0
  21. sqlserver_semantic_mcp/main.py +90 -0
  22. sqlserver_semantic_mcp/policy/__init__.py +0 -0
  23. sqlserver_semantic_mcp/policy/analyzer.py +194 -0
  24. sqlserver_semantic_mcp/policy/enforcer.py +104 -0
  25. sqlserver_semantic_mcp/policy/intents/__init__.py +16 -0
  26. sqlserver_semantic_mcp/policy/intents/ast_analyzer.py +24 -0
  27. sqlserver_semantic_mcp/policy/intents/base.py +17 -0
  28. sqlserver_semantic_mcp/policy/intents/regex_analyzer.py +11 -0
  29. sqlserver_semantic_mcp/policy/intents/router.py +21 -0
  30. sqlserver_semantic_mcp/policy/loader.py +90 -0
  31. sqlserver_semantic_mcp/policy/models.py +43 -0
  32. sqlserver_semantic_mcp/server/__init__.py +0 -0
  33. sqlserver_semantic_mcp/server/app.py +125 -0
  34. sqlserver_semantic_mcp/server/compact.py +74 -0
  35. sqlserver_semantic_mcp/server/prompts/__init__.py +5 -0
  36. sqlserver_semantic_mcp/server/prompts/analysis.py +56 -0
  37. sqlserver_semantic_mcp/server/prompts/discovery.py +55 -0
  38. sqlserver_semantic_mcp/server/prompts/execution.py +64 -0
  39. sqlserver_semantic_mcp/server/prompts/registry.py +41 -0
  40. sqlserver_semantic_mcp/server/resources/__init__.py +1 -0
  41. sqlserver_semantic_mcp/server/resources/schema.py +144 -0
  42. sqlserver_semantic_mcp/server/tools/__init__.py +42 -0
  43. sqlserver_semantic_mcp/server/tools/cache.py +24 -0
  44. sqlserver_semantic_mcp/server/tools/metadata.py +167 -0
  45. sqlserver_semantic_mcp/server/tools/metrics.py +44 -0
  46. sqlserver_semantic_mcp/server/tools/object_tool.py +113 -0
  47. sqlserver_semantic_mcp/server/tools/policy.py +48 -0
  48. sqlserver_semantic_mcp/server/tools/query.py +159 -0
  49. sqlserver_semantic_mcp/server/tools/relationship.py +104 -0
  50. sqlserver_semantic_mcp/server/tools/semantic.py +112 -0
  51. sqlserver_semantic_mcp/server/tools/shape.py +204 -0
  52. sqlserver_semantic_mcp/server/tools/workflow.py +307 -0
  53. sqlserver_semantic_mcp/services/__init__.py +0 -0
  54. sqlserver_semantic_mcp/services/metadata_service.py +173 -0
  55. sqlserver_semantic_mcp/services/metrics_service.py +124 -0
  56. sqlserver_semantic_mcp/services/object_service.py +187 -0
  57. sqlserver_semantic_mcp/services/policy_service.py +59 -0
  58. sqlserver_semantic_mcp/services/query_service.py +321 -0
  59. sqlserver_semantic_mcp/services/relationship_service.py +160 -0
  60. sqlserver_semantic_mcp/services/semantic_service.py +277 -0
  61. sqlserver_semantic_mcp/workflows/__init__.py +26 -0
  62. sqlserver_semantic_mcp/workflows/bundle.py +157 -0
  63. sqlserver_semantic_mcp/workflows/contracts.py +64 -0
  64. sqlserver_semantic_mcp/workflows/discovery_flow.py +116 -0
  65. sqlserver_semantic_mcp/workflows/facade.py +117 -0
  66. sqlserver_semantic_mcp/workflows/query_flow.py +120 -0
  67. sqlserver_semantic_mcp/workflows/recommendations.py +161 -0
  68. sqlserver_semantic_mcp/workflows/router.py +59 -0
  69. sqlserver_semantic_mcp-0.5.0.dist-info/METADATA +679 -0
  70. sqlserver_semantic_mcp-0.5.0.dist-info/RECORD +74 -0
  71. sqlserver_semantic_mcp-0.5.0.dist-info/WHEEL +5 -0
  72. sqlserver_semantic_mcp-0.5.0.dist-info/entry_points.txt +2 -0
  73. sqlserver_semantic_mcp-0.5.0.dist-info/licenses/LICENSE +21 -0
  74. sqlserver_semantic_mcp-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ """Prompts for the direct-execution path."""
2
+ from __future__ import annotations
3
+
4
+ from mcp.types import (
5
+ GetPromptResult, Prompt, PromptArgument, PromptMessage, TextContent,
6
+ )
7
+
8
+ from .registry import register_prompt
9
+
10
+
11
+ _PROMPT = Prompt(
12
+ name="safe_sql_execution",
13
+ description=(
14
+ "Execute an agent-authored SQL in the shortest safe path, using the "
15
+ "v0.5 plan_or_execute_query entry."
16
+ ),
17
+ arguments=[
18
+ PromptArgument(
19
+ name="query",
20
+ description="SQL the agent already believes is ready to run.",
21
+ required=True,
22
+ ),
23
+ PromptArgument(
24
+ name="return_mode",
25
+ description="summary | rows | sample | count_only (default: summary).",
26
+ required=False,
27
+ ),
28
+ ],
29
+ )
30
+
31
+
32
+ _BODY = """You have a SQL query already drafted. Prefer the shortest safe path:
33
+
34
+ 1. Call `plan_or_execute_query` with mode="auto" and return_mode={return_mode!r}.
35
+ 2. If the response has `path="direct_execute"` and `executed=true`, you are done — present the rows / summary as-is.
36
+ 3. If the response has `path="direct_validate"` and `allowed=false`, read `reason` and either:
37
+ - revise the SQL, or
38
+ - call `estimate_execution_risk` for more detail before revising.
39
+ 4. Do NOT call `get_tables` / `describe_table` / `find_join_path` first when the SQL is already known — that only wastes tokens.
40
+
41
+ Query:
42
+ ```sql
43
+ {query}
44
+ ```
45
+ """
46
+
47
+
48
+ async def _handler(arguments: dict) -> GetPromptResult:
49
+ query = arguments.get("query", "")
50
+ return_mode = arguments.get("return_mode") or "summary"
51
+ text = _BODY.format(query=query, return_mode=return_mode)
52
+ return GetPromptResult(
53
+ description="Shortest safe path for SQL-ready agents.",
54
+ messages=[
55
+ PromptMessage(
56
+ role="user",
57
+ content=TextContent(type="text", text=text),
58
+ ),
59
+ ],
60
+ )
61
+
62
+
63
+ def register() -> None:
64
+ register_prompt(_PROMPT, _handler)
@@ -0,0 +1,41 @@
1
+ """Prompt registry wired up against ``server.app.app``."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Awaitable, Callable
5
+
6
+ from mcp.types import GetPromptResult, Prompt, PromptArgument
7
+
8
+ from ..app import app
9
+
10
+
11
+ Handler = Callable[[dict[str, Any]], Awaitable[GetPromptResult]]
12
+ _REGISTRY: dict[str, tuple[Prompt, Handler]] = {}
13
+
14
+
15
+ def register_prompt(prompt: Prompt, handler: Handler) -> None:
16
+ _REGISTRY[prompt.name] = (prompt, handler)
17
+
18
+
19
+ @app.list_prompts()
20
+ async def _list_prompts() -> list[Prompt]:
21
+ return [p for (p, _) in _REGISTRY.values()]
22
+
23
+
24
+ @app.get_prompt()
25
+ async def _get_prompt(name: str, arguments: dict | None = None) -> GetPromptResult:
26
+ if name not in _REGISTRY:
27
+ raise ValueError(f"Unknown prompt: {name}")
28
+ _p, handler = _REGISTRY[name]
29
+ return await handler(arguments or {})
30
+
31
+
32
+ def register_prompts() -> None:
33
+ """Import prompt modules to trigger their registrations."""
34
+ from . import execution, discovery, analysis # noqa: F401
35
+
36
+ execution.register()
37
+ discovery.register()
38
+ analysis.register()
39
+
40
+
41
+ __all__ = ["PromptArgument", "Prompt", "register_prompt", "register_prompts"]
@@ -0,0 +1 @@
1
+ from . import schema # noqa: F401
@@ -0,0 +1,144 @@
1
+ import json
2
+
3
+ from mcp.types import Resource, ResourceTemplate
4
+ from pydantic import AnyUrl
5
+
6
+ from ...services import metadata_service, semantic_service, object_service
7
+ from ..app import app, get_context
8
+
9
+
10
+ @app.list_resources()
11
+ async def list_resources() -> list[Resource]:
12
+ # Only resources without a tool equivalent are listed here.
13
+ # `get_tables` tool supersedes the old semantic://schema/tables resource.
14
+ return [
15
+ Resource(
16
+ uri=AnyUrl("semantic://summary/database"),
17
+ name="Database summary", mimeType="application/json",
18
+ ),
19
+ ]
20
+
21
+
22
+ @app.list_resource_templates()
23
+ async def list_resource_templates() -> list[ResourceTemplate]:
24
+ return [
25
+ ResourceTemplate(
26
+ uriTemplate="semantic://schema/tables/{qualified}",
27
+ name="Table metadata", mimeType="application/json",
28
+ ),
29
+ ResourceTemplate(
30
+ uriTemplate="semantic://analysis/classification/{qualified}",
31
+ name="Table classification", mimeType="application/json",
32
+ ),
33
+ ResourceTemplate(
34
+ uriTemplate="semantic://summary/table/{qualified}",
35
+ name="AI-ready table summary (join-focused)",
36
+ mimeType="application/json",
37
+ ),
38
+ ResourceTemplate(
39
+ uriTemplate="semantic://summary/object/{type}/{qualified}",
40
+ name="AI-ready object summary (reads/writes/depends)",
41
+ mimeType="application/json",
42
+ ),
43
+ ResourceTemplate(
44
+ uriTemplate="semantic://bundle/joining/{qualified}",
45
+ name="Joining bundle for a single table",
46
+ mimeType="application/json",
47
+ ),
48
+ ]
49
+
50
+
51
+ def _split_qualified(qualified: str, uri: str) -> tuple[str, str]:
52
+ parts = qualified.split(".", 1)
53
+ if len(parts) != 2 or not parts[0] or not parts[1]:
54
+ raise ValueError(
55
+ f"Invalid resource URI '{uri}': expected schema.table format"
56
+ )
57
+ return parts[0], parts[1]
58
+
59
+
60
+ @app.read_resource()
61
+ async def read_resource(uri: AnyUrl) -> str:
62
+ ctx = get_context()
63
+ cp = ctx.cfg.cache_path
64
+ db = ctx.cfg.mssql_database
65
+ s = str(uri)
66
+
67
+ if s == "semantic://schema/tables":
68
+ return json.dumps(await metadata_service.list_tables(cp, db), default=str)
69
+
70
+ if s == "semantic://summary/database":
71
+ return json.dumps(
72
+ await metadata_service.database_summary(cp, db), default=str,
73
+ )
74
+
75
+ if s.startswith("semantic://schema/tables/"):
76
+ qualified = s[len("semantic://schema/tables/"):]
77
+ schema, table = _split_qualified(qualified, s)
78
+ return json.dumps(
79
+ await metadata_service.describe_table(cp, db, schema, table),
80
+ default=str,
81
+ )
82
+
83
+ if s.startswith("semantic://analysis/classification/"):
84
+ qualified = s[len("semantic://analysis/classification/"):]
85
+ schema, table = _split_qualified(qualified, s)
86
+ return json.dumps(
87
+ await semantic_service.classify_table(cp, db, schema, table),
88
+ default=str,
89
+ )
90
+
91
+ if s.startswith("semantic://analysis/dependencies/"):
92
+ qualified = s[len("semantic://analysis/dependencies/"):]
93
+ parts = qualified.split("/")
94
+ if len(parts) != 2:
95
+ raise ValueError(
96
+ f"Invalid resource URI '{s}': expected <type>/<schema>.<name>"
97
+ )
98
+ obj_type = parts[0].upper()
99
+ schema, name = _split_qualified(parts[1], s)
100
+ return json.dumps(
101
+ await object_service.describe_object(schema, name, obj_type, ctx.cfg),
102
+ default=str,
103
+ )
104
+
105
+ if s.startswith("semantic://summary/table/"):
106
+ qualified = s[len("semantic://summary/table/"):]
107
+ schema, table = _split_qualified(qualified, s)
108
+ summary = await semantic_service.summarize_for_joining(
109
+ cp, db, schema, table,
110
+ )
111
+ return json.dumps(summary or {}, default=str)
112
+
113
+ if s.startswith("semantic://summary/object/"):
114
+ rest = s[len("semantic://summary/object/"):]
115
+ parts = rest.split("/", 1)
116
+ if len(parts) != 2:
117
+ raise ValueError(
118
+ f"Invalid resource URI '{s}': expected <type>/<schema>.<name>"
119
+ )
120
+ obj_type = parts[0].upper()
121
+ schema, name = _split_qualified(parts[1], s)
122
+ obj = await object_service.describe_object(
123
+ schema, name, obj_type, ctx.cfg,
124
+ )
125
+ payload = {
126
+ "object": f"{schema}.{name}",
127
+ "type": obj_type,
128
+ "status": obj.get("status") if obj else None,
129
+ "reads": list((obj or {}).get("read_tables", []) or []),
130
+ "writes": list((obj or {}).get("write_tables", []) or []),
131
+ "depends_on": list((obj or {}).get("dependencies", []) or []),
132
+ }
133
+ return json.dumps(payload, default=str)
134
+
135
+ if s.startswith("semantic://bundle/joining/"):
136
+ qualified = s[len("semantic://bundle/joining/"):]
137
+ schema, table = _split_qualified(qualified, s)
138
+ bundle = await ctx.workflow.bundle_context_for_next_step(
139
+ [{"kind": "table", "schema": schema, "table": table}],
140
+ goal="joining",
141
+ )
142
+ return json.dumps(bundle, default=str)
143
+
144
+ raise ValueError(f"Unknown resource URI: {s}")
@@ -0,0 +1,42 @@
1
+ from . import (
2
+ metadata, policy, query, cache,
3
+ relationship, object_tool, semantic, metrics, workflow,
4
+ ) # noqa: F401
5
+
6
+
7
+ _GROUP_REGISTRATIONS = {
8
+ "metadata": metadata.register,
9
+ "policy": policy.register,
10
+ "query": query.register,
11
+ "cache": cache.register,
12
+ "relationship": relationship.register,
13
+ "object": object_tool.register,
14
+ "semantic": semantic.register,
15
+ "metrics": metrics.register,
16
+ "workflow": workflow.register,
17
+ }
18
+
19
+
20
+ def _resolve_profile_groups(profile: str) -> list[str]:
21
+ if not profile or profile == "all":
22
+ return list(_GROUP_REGISTRATIONS.keys())
23
+ requested = [g.strip() for g in profile.split(",") if g.strip()]
24
+ if not requested:
25
+ return list(_GROUP_REGISTRATIONS.keys())
26
+ unknown = [g for g in requested if g not in _GROUP_REGISTRATIONS]
27
+ if unknown:
28
+ raise ValueError(
29
+ f"Unknown tool profile group(s): {', '.join(unknown)}. "
30
+ f"Valid groups: {', '.join(sorted(_GROUP_REGISTRATIONS.keys()))}"
31
+ )
32
+ return requested
33
+
34
+
35
+ def register_all() -> None:
36
+ from ...config import get_config
37
+ cfg = get_config()
38
+ groups = _resolve_profile_groups(cfg.tool_profile)
39
+ if not cfg.workflow_tools_enabled and "workflow" in groups:
40
+ groups = [g for g in groups if g != "workflow"]
41
+ for group in groups:
42
+ _GROUP_REGISTRATIONS[group]()
@@ -0,0 +1,24 @@
1
+ from mcp.types import Tool
2
+
3
+ from ...infrastructure.cache.structural import warmup_structural_cache
4
+ from ..app import get_context, register_tool
5
+
6
+
7
+ def register() -> None:
8
+ register_tool(
9
+ Tool(
10
+ name="refresh_schema_cache",
11
+ description=(
12
+ "Re-fetch structural metadata from SQL Server and update SQLite cache. "
13
+ "Semantic rows whose hash changed become 'dirty' and will recompute."
14
+ ),
15
+ inputSchema={"type": "object", "properties": {}},
16
+ ),
17
+ _refresh,
18
+ )
19
+
20
+
21
+ async def _refresh(args: dict) -> dict:
22
+ ctx = get_context()
23
+ result = await warmup_structural_cache(ctx.cfg)
24
+ return {"refreshed": True, **result}
@@ -0,0 +1,167 @@
1
+ from typing import Optional
2
+
3
+ from mcp.types import Tool
4
+
5
+ from ...services import metadata_service, semantic_service
6
+ from ..app import get_context, register_tool
7
+ from .shape import (
8
+ project_describe_table, project_get_columns, resolve_detail,
9
+ )
10
+
11
+
12
+ _DETAIL_PROP = {
13
+ "type": "string", "enum": ["brief", "standard", "full"],
14
+ "default": "brief",
15
+ "description": "Response verbosity. brief = minimal identification + counts; "
16
+ "standard = columns+FKs; full = indexes+descriptions.",
17
+ }
18
+
19
+
20
+ _BUDGET_PROP = {
21
+ "type": "string", "enum": ["tiny", "low", "medium", "high"],
22
+ "description": "Payload budget hint. tiny = minimum; high = full.",
23
+ }
24
+
25
+
26
+ _BUDGET_LIMITS = {
27
+ "tiny": 20,
28
+ "low": 100,
29
+ "medium": 500,
30
+ "high": 2000,
31
+ }
32
+
33
+
34
+ def register() -> None:
35
+ register_tool(
36
+ Tool(
37
+ name="get_tables",
38
+ description=(
39
+ "List tables in the database. Supports schema / keyword filters "
40
+ "so the response stays small on large DBs. Pass limit or "
41
+ "token_budget_hint to further cap the payload."
42
+ ),
43
+ inputSchema={
44
+ "type": "object",
45
+ "properties": {
46
+ "schema": {"oneOf": [{"type": "string"},
47
+ {"type": "array",
48
+ "items": {"type": "string"}}]},
49
+ "keyword": {"type": "string"},
50
+ "limit": {"type": "integer", "minimum": 1},
51
+ "token_budget_hint": _BUDGET_PROP,
52
+ },
53
+ },
54
+ ),
55
+ _get_tables,
56
+ )
57
+ register_tool(
58
+ Tool(
59
+ name="describe_table",
60
+ description=(
61
+ "Return table metadata. detail=brief (default) returns a compact "
62
+ "summary {table, column_count, pk, fk_to, important_columns, "
63
+ "classification}. standard adds columns+FKs; full adds indexes "
64
+ "and descriptions."
65
+ ),
66
+ inputSchema={
67
+ "type": "object",
68
+ "properties": {
69
+ "schema": {"type": "string"},
70
+ "table": {"type": "string"},
71
+ "detail": _DETAIL_PROP,
72
+ },
73
+ "required": ["schema", "table"],
74
+ },
75
+ ),
76
+ _describe_table,
77
+ )
78
+ register_tool(
79
+ Tool(
80
+ name="get_columns",
81
+ description=(
82
+ "List columns of a table. detail=brief (default) returns name + "
83
+ "semantic tag only; standard adds type/nullable; full returns "
84
+ "all metadata."
85
+ ),
86
+ inputSchema={
87
+ "type": "object",
88
+ "properties": {
89
+ "schema": {"type": "string"},
90
+ "table": {"type": "string"},
91
+ "detail": _DETAIL_PROP,
92
+ },
93
+ "required": ["schema", "table"],
94
+ },
95
+ ),
96
+ _get_columns,
97
+ )
98
+
99
+
100
+ def _normalize_schema_filter(raw) -> Optional[list[str]]:
101
+ if raw is None:
102
+ return None
103
+ if isinstance(raw, str):
104
+ return [raw] if raw else None
105
+ if isinstance(raw, list):
106
+ vals = [s for s in raw if isinstance(s, str) and s]
107
+ return vals or None
108
+ return None
109
+
110
+
111
+ def _resolve_list_limit(args: dict, cfg_default: str) -> int | None:
112
+ explicit = args.get("limit")
113
+ if isinstance(explicit, int) and explicit > 0:
114
+ return explicit
115
+ hint = args.get("token_budget_hint") or cfg_default
116
+ return _BUDGET_LIMITS.get(hint)
117
+
118
+
119
+ async def _get_tables(args: dict) -> list[dict]:
120
+ ctx = get_context()
121
+ schemas = _normalize_schema_filter(args.get("schema"))
122
+ keyword = args.get("keyword") or None
123
+ rows = await metadata_service.list_tables(
124
+ ctx.cfg.cache_path, ctx.cfg.mssql_database,
125
+ schemas=schemas, keyword=keyword,
126
+ )
127
+ cap = _resolve_list_limit(args, ctx.cfg.default_token_budget_hint)
128
+ if cap is not None and len(rows) > cap:
129
+ return rows[:cap]
130
+ return rows
131
+
132
+
133
+ async def _describe_table(args: dict) -> Optional[dict]:
134
+ ctx = get_context()
135
+ detail = resolve_detail(args)
136
+ cp = ctx.cfg.cache_path
137
+ db = ctx.cfg.mssql_database
138
+ schema = args["schema"]
139
+ table = args["table"]
140
+
141
+ full = await metadata_service.describe_table(cp, db, schema, table)
142
+ if full is None:
143
+ return None
144
+
145
+ classification = await semantic_service.classify_table(cp, db, schema, table)
146
+ semantic_map = {
147
+ c["column_name"]: (semantic_service._column_semantic(c) or "generic")
148
+ for c in full.get("columns", [])
149
+ }
150
+ return project_describe_table(
151
+ full, detail=detail,
152
+ classification=classification, column_semantics=semantic_map,
153
+ )
154
+
155
+
156
+ async def _get_columns(args: dict) -> list[dict]:
157
+ ctx = get_context()
158
+ detail = resolve_detail(args)
159
+ cols = await metadata_service.list_columns(
160
+ ctx.cfg.cache_path, ctx.cfg.mssql_database,
161
+ args["schema"], args["table"],
162
+ )
163
+ semantic_map = {
164
+ c["column_name"]: (semantic_service._column_semantic(c) or "generic")
165
+ for c in cols
166
+ }
167
+ return project_get_columns(cols, detail=detail, semantic_map=semantic_map)
@@ -0,0 +1,44 @@
1
+ from mcp.types import Tool
2
+
3
+ from ...services import metrics_service
4
+ from ..app import get_context, register_tool
5
+
6
+
7
+ def register() -> None:
8
+ register_tool(
9
+ Tool(
10
+ name="get_tool_metrics",
11
+ description=(
12
+ "Return per-tool payload metrics (call_count, total_bytes, "
13
+ "avg_bytes, p95_bytes, max_bytes) ordered by total_bytes desc. "
14
+ "Use to find the heaviest tools for optimization targeting."
15
+ ),
16
+ inputSchema={
17
+ "type": "object",
18
+ "properties": {
19
+ "limit": {"type": "integer", "minimum": 1, "default": 10},
20
+ },
21
+ },
22
+ ),
23
+ _top,
24
+ )
25
+ register_tool(
26
+ Tool(
27
+ name="reset_tool_metrics",
28
+ description="Delete all recorded tool metrics. Returns deleted count.",
29
+ inputSchema={"type": "object", "properties": {}},
30
+ ),
31
+ _reset,
32
+ )
33
+
34
+
35
+ async def _top(args: dict) -> list[dict]:
36
+ ctx = get_context()
37
+ limit = int(args.get("limit", 10))
38
+ return await metrics_service.query_top_tools(ctx.cfg.cache_path, limit=limit)
39
+
40
+
41
+ async def _reset(args: dict) -> dict:
42
+ ctx = get_context()
43
+ n = await metrics_service.clear_metrics(ctx.cfg.cache_path)
44
+ return {"deleted": n}
@@ -0,0 +1,113 @@
1
+ import hashlib
2
+ from typing import Any
3
+
4
+ from mcp.types import Tool
5
+
6
+ from ...services import object_service
7
+ from ..app import get_context, register_tool
8
+ from .shape import project_describe_object, resolve_detail
9
+
10
+
11
+ _DETAIL_PROP = {
12
+ "type": "string", "enum": ["brief", "standard", "full"],
13
+ "default": "brief",
14
+ "description": "Response verbosity. brief strips definition; full always "
15
+ "includes it. include_definition overrides to include at "
16
+ "brief/standard tiers.",
17
+ }
18
+
19
+
20
+ def _input_schema() -> dict:
21
+ return {
22
+ "type": "object",
23
+ "properties": {
24
+ "schema": {"type": "string"},
25
+ "name": {"type": "string"},
26
+ "detail": _DETAIL_PROP,
27
+ "include_definition": {"type": "boolean", "default": False},
28
+ },
29
+ "required": ["schema", "name"],
30
+ }
31
+
32
+
33
+ def register() -> None:
34
+ register_tool(
35
+ Tool(
36
+ name="describe_view",
37
+ description=(
38
+ "Return view metadata + dependencies. detail=brief (default) "
39
+ "strips SQL definition; detail=full or include_definition=true "
40
+ "returns the full text."
41
+ ),
42
+ inputSchema=_input_schema(),
43
+ ),
44
+ _describe_view,
45
+ )
46
+ register_tool(
47
+ Tool(
48
+ name="describe_procedure",
49
+ description=(
50
+ "Return procedure metadata + dependencies. detail=brief (default) "
51
+ "strips SQL definition; detail=full or include_definition=true "
52
+ "returns the full text."
53
+ ),
54
+ inputSchema=_input_schema(),
55
+ ),
56
+ _describe_procedure,
57
+ )
58
+ register_tool(
59
+ Tool(
60
+ name="trace_object_dependencies",
61
+ description="Return a list of objects/tables the given object depends on.",
62
+ inputSchema={
63
+ "type": "object",
64
+ "properties": {
65
+ "schema": {"type": "string"},
66
+ "name": {"type": "string"},
67
+ "type": {"type": "string",
68
+ "enum": ["VIEW", "PROCEDURE", "FUNCTION"]},
69
+ },
70
+ "required": ["schema", "name", "type"],
71
+ },
72
+ ),
73
+ _trace,
74
+ )
75
+
76
+
77
+ def _attach_hash_and_bytes(obj: dict) -> dict:
78
+ definition = obj.get("definition")
79
+ if not isinstance(definition, str) or not definition:
80
+ return obj
81
+ if "definition_hash" in obj and "definition_bytes" in obj:
82
+ return obj
83
+ encoded = definition.encode("utf-8")
84
+ out = dict(obj)
85
+ out.setdefault("definition_hash", hashlib.sha1(encoded).hexdigest()[:8])
86
+ out.setdefault("definition_bytes", len(encoded))
87
+ return out
88
+
89
+
90
+ async def _describe_object(args: dict, object_type: str) -> dict:
91
+ ctx = get_context()
92
+ detail = resolve_detail(args)
93
+ include = bool(args.get("include_definition", False))
94
+ obj = await object_service.describe_object(
95
+ args["schema"], args["name"], object_type, ctx.cfg,
96
+ )
97
+ obj = _attach_hash_and_bytes(obj)
98
+ return project_describe_object(obj, detail=detail, include_definition=include)
99
+
100
+
101
+ async def _describe_view(args: dict) -> dict:
102
+ return await _describe_object(args, "VIEW")
103
+
104
+
105
+ async def _describe_procedure(args: dict) -> dict:
106
+ return await _describe_object(args, "PROCEDURE")
107
+
108
+
109
+ async def _trace(args: dict) -> list[str]:
110
+ ctx = get_context()
111
+ return await object_service.trace_dependencies(
112
+ args["schema"], args["name"], args["type"], ctx.cfg,
113
+ )