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.
- sqlserver_semantic_mcp/__init__.py +1 -0
- sqlserver_semantic_mcp/config.py +78 -0
- sqlserver_semantic_mcp/domain/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/enums.py +48 -0
- sqlserver_semantic_mcp/domain/models/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/models/column.py +14 -0
- sqlserver_semantic_mcp/domain/models/object.py +13 -0
- sqlserver_semantic_mcp/domain/models/relationship.py +11 -0
- sqlserver_semantic_mcp/domain/models/table.py +29 -0
- sqlserver_semantic_mcp/infrastructure/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/background.py +59 -0
- sqlserver_semantic_mcp/infrastructure/cache/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/cache/semantic.py +132 -0
- sqlserver_semantic_mcp/infrastructure/cache/store.py +152 -0
- sqlserver_semantic_mcp/infrastructure/cache/structural.py +203 -0
- sqlserver_semantic_mcp/infrastructure/connection.py +78 -0
- sqlserver_semantic_mcp/infrastructure/queries/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/queries/comment_queries.py +18 -0
- sqlserver_semantic_mcp/infrastructure/queries/metadata_queries.py +70 -0
- sqlserver_semantic_mcp/infrastructure/queries/object_queries.py +15 -0
- sqlserver_semantic_mcp/main.py +90 -0
- sqlserver_semantic_mcp/policy/__init__.py +0 -0
- sqlserver_semantic_mcp/policy/analyzer.py +194 -0
- sqlserver_semantic_mcp/policy/enforcer.py +104 -0
- sqlserver_semantic_mcp/policy/intents/__init__.py +16 -0
- sqlserver_semantic_mcp/policy/intents/ast_analyzer.py +24 -0
- sqlserver_semantic_mcp/policy/intents/base.py +17 -0
- sqlserver_semantic_mcp/policy/intents/regex_analyzer.py +11 -0
- sqlserver_semantic_mcp/policy/intents/router.py +21 -0
- sqlserver_semantic_mcp/policy/loader.py +90 -0
- sqlserver_semantic_mcp/policy/models.py +43 -0
- sqlserver_semantic_mcp/server/__init__.py +0 -0
- sqlserver_semantic_mcp/server/app.py +125 -0
- sqlserver_semantic_mcp/server/compact.py +74 -0
- sqlserver_semantic_mcp/server/prompts/__init__.py +5 -0
- sqlserver_semantic_mcp/server/prompts/analysis.py +56 -0
- sqlserver_semantic_mcp/server/prompts/discovery.py +55 -0
- sqlserver_semantic_mcp/server/prompts/execution.py +64 -0
- sqlserver_semantic_mcp/server/prompts/registry.py +41 -0
- sqlserver_semantic_mcp/server/resources/__init__.py +1 -0
- sqlserver_semantic_mcp/server/resources/schema.py +144 -0
- sqlserver_semantic_mcp/server/tools/__init__.py +42 -0
- sqlserver_semantic_mcp/server/tools/cache.py +24 -0
- sqlserver_semantic_mcp/server/tools/metadata.py +167 -0
- sqlserver_semantic_mcp/server/tools/metrics.py +44 -0
- sqlserver_semantic_mcp/server/tools/object_tool.py +113 -0
- sqlserver_semantic_mcp/server/tools/policy.py +48 -0
- sqlserver_semantic_mcp/server/tools/query.py +159 -0
- sqlserver_semantic_mcp/server/tools/relationship.py +104 -0
- sqlserver_semantic_mcp/server/tools/semantic.py +112 -0
- sqlserver_semantic_mcp/server/tools/shape.py +204 -0
- sqlserver_semantic_mcp/server/tools/workflow.py +307 -0
- sqlserver_semantic_mcp/services/__init__.py +0 -0
- sqlserver_semantic_mcp/services/metadata_service.py +173 -0
- sqlserver_semantic_mcp/services/metrics_service.py +124 -0
- sqlserver_semantic_mcp/services/object_service.py +187 -0
- sqlserver_semantic_mcp/services/policy_service.py +59 -0
- sqlserver_semantic_mcp/services/query_service.py +321 -0
- sqlserver_semantic_mcp/services/relationship_service.py +160 -0
- sqlserver_semantic_mcp/services/semantic_service.py +277 -0
- sqlserver_semantic_mcp/workflows/__init__.py +26 -0
- sqlserver_semantic_mcp/workflows/bundle.py +157 -0
- sqlserver_semantic_mcp/workflows/contracts.py +64 -0
- sqlserver_semantic_mcp/workflows/discovery_flow.py +116 -0
- sqlserver_semantic_mcp/workflows/facade.py +117 -0
- sqlserver_semantic_mcp/workflows/query_flow.py +120 -0
- sqlserver_semantic_mcp/workflows/recommendations.py +161 -0
- sqlserver_semantic_mcp/workflows/router.py +59 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/METADATA +679 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/RECORD +74 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/WHEEL +5 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/entry_points.txt +2 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|