pretensor 0.1.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.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +237 -0
- pretensor/introspection/models/dsn.py +550 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +487 -0
- pretensor/semantic/yaml_layer.py +180 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""MCP markdown resources (databases, per-db overview, clusters, metrics)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pretensor.intelligence.cluster_labeler import HEURISTIC_CLUSTER_DESCRIPTION
|
|
9
|
+
from pretensor.mcp.payload_types import iso_format, stale_threshold_days, staleness_days
|
|
10
|
+
from pretensor.mcp.service_context import (
|
|
11
|
+
get_effective_graph_config,
|
|
12
|
+
get_effective_visibility_filter,
|
|
13
|
+
)
|
|
14
|
+
from pretensor.mcp.service_registry import (
|
|
15
|
+
counts_for_graph,
|
|
16
|
+
graph_path_for_entry,
|
|
17
|
+
load_registry,
|
|
18
|
+
open_store_for_entry,
|
|
19
|
+
release_store,
|
|
20
|
+
resolve_registry_entry,
|
|
21
|
+
)
|
|
22
|
+
from pretensor.mcp.tools.list import list_databases_payload
|
|
23
|
+
from pretensor.visibility.filter import VisibilityFilter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clusters_resource_markdown(
|
|
27
|
+
graph_dir: Path,
|
|
28
|
+
name: str,
|
|
29
|
+
*,
|
|
30
|
+
visibility_filter: VisibilityFilter | None = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Markdown for ``pretensor://db/{name}/clusters``."""
|
|
33
|
+
reg = load_registry(graph_dir)
|
|
34
|
+
entry = resolve_registry_entry(reg, name)
|
|
35
|
+
if entry is None:
|
|
36
|
+
return f"# Unknown database `{name}`\n\nNo matching registry entry."
|
|
37
|
+
gp = graph_path_for_entry(entry)
|
|
38
|
+
if not gp.exists():
|
|
39
|
+
return f"# Clusters — {name}\n\n_Graph file missing._"
|
|
40
|
+
|
|
41
|
+
db_key = str(entry.database)
|
|
42
|
+
vf = visibility_filter or get_effective_visibility_filter()
|
|
43
|
+
cn = str(entry.connection_name)
|
|
44
|
+
store = open_store_for_entry(entry)
|
|
45
|
+
try:
|
|
46
|
+
rows = store.query_all_rows(
|
|
47
|
+
"""
|
|
48
|
+
MATCH (t:SchemaTable)-[:IN_CLUSTER]->(c:Cluster)
|
|
49
|
+
WHERE c.database_key = $db
|
|
50
|
+
RETURN c.label, c.description, c.cohesion_score,
|
|
51
|
+
collect(concat(t.schema_name, '.', t.table_name)) AS tables
|
|
52
|
+
ORDER BY c.cohesion_score DESC
|
|
53
|
+
""",
|
|
54
|
+
{"db": db_key},
|
|
55
|
+
)
|
|
56
|
+
lines = [
|
|
57
|
+
f"# Domain Clusters — {entry.connection_name}",
|
|
58
|
+
"",
|
|
59
|
+
f"_Logical database:_ `{db_key}`",
|
|
60
|
+
"",
|
|
61
|
+
]
|
|
62
|
+
if not rows:
|
|
63
|
+
lines.append("_No clusters indexed yet; re-run `pretensor index`._")
|
|
64
|
+
return "\n".join(lines)
|
|
65
|
+
|
|
66
|
+
for lab, desc, coh, tables in rows:
|
|
67
|
+
tlist = [str(x) for x in (tables or []) if x]
|
|
68
|
+
if vf is not None:
|
|
69
|
+
filtered: list[str] = []
|
|
70
|
+
for qual in tlist:
|
|
71
|
+
if "." not in qual:
|
|
72
|
+
continue
|
|
73
|
+
sn, _, tn = qual.partition(".")
|
|
74
|
+
if vf.is_table_visible(cn, sn, tn):
|
|
75
|
+
filtered.append(qual)
|
|
76
|
+
tlist = sorted(set(filtered))
|
|
77
|
+
else:
|
|
78
|
+
tlist = sorted(set(tlist))
|
|
79
|
+
cohesion = float(coh) if coh is not None else 0.0
|
|
80
|
+
lines.append(f"## {lab} _(cohesion: {cohesion:.2f})_")
|
|
81
|
+
lines.append("")
|
|
82
|
+
if desc and str(desc) != HEURISTIC_CLUSTER_DESCRIPTION:
|
|
83
|
+
lines.append(str(desc))
|
|
84
|
+
lines.append("")
|
|
85
|
+
show = tlist[:40]
|
|
86
|
+
suffix = f" and {len(tlist) - 40} more" if len(tlist) > 40 else ""
|
|
87
|
+
lines.append("**Tables:** " + ", ".join(show) + suffix)
|
|
88
|
+
lines.append("")
|
|
89
|
+
lines.append(
|
|
90
|
+
f"*Last indexed: {iso_format(entry.last_indexed_at)}. "
|
|
91
|
+
"Use `context` for table detail or `traverse` for join paths.*"
|
|
92
|
+
)
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
finally:
|
|
95
|
+
release_store(store)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def databases_resource_markdown(
|
|
99
|
+
graph_dir: Path,
|
|
100
|
+
*,
|
|
101
|
+
visibility_filter: VisibilityFilter | None = None,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Markdown for ``pretensor://databases``."""
|
|
104
|
+
data = list_databases_payload(graph_dir, visibility_filter=visibility_filter)
|
|
105
|
+
lines = ["# Indexed databases", ""]
|
|
106
|
+
for item in data["databases"]:
|
|
107
|
+
lines.append(f"## {item['name']}")
|
|
108
|
+
# ``database`` is omitted from the payload when it equals ``name`` (see
|
|
109
|
+
# list_databases_payload); fall back to ``name`` for the markdown row.
|
|
110
|
+
lines.append(f"- **Database:** {item.get('database', item['name'])}")
|
|
111
|
+
lines.append(f"- **Tables:** {item['table_count']}")
|
|
112
|
+
lines.append(f"- **Row count (sum):** {item['row_count']}")
|
|
113
|
+
schemas = item.get("schemas") or []
|
|
114
|
+
lines.append("- **Schemas:** " + (", ".join(schemas) if schemas else "_none_"))
|
|
115
|
+
lines.append(
|
|
116
|
+
f"- **dbt manifest:** {item.get('has_dbt_manifest', 'not_attempted')}"
|
|
117
|
+
)
|
|
118
|
+
lines.append(
|
|
119
|
+
f"- **LLM enrichment:** {item.get('has_llm_enrichment', 'not_attempted')}"
|
|
120
|
+
)
|
|
121
|
+
lines.append(
|
|
122
|
+
f"- **External consumers:** {item.get('has_external_consumers', 'not_attempted')}"
|
|
123
|
+
)
|
|
124
|
+
lines.append(f"- **Last indexed:** {item['last_indexed']}")
|
|
125
|
+
lines.append(f"- **Stale (>7d):** {item['is_stale']}")
|
|
126
|
+
lines.append(f"- **Graph:** `{item['graph_path']}`")
|
|
127
|
+
lines.append("")
|
|
128
|
+
if len(data["databases"]) == 0:
|
|
129
|
+
lines.append("_No entries in registry._")
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def db_overview_resource_markdown(
|
|
134
|
+
graph_dir: Path,
|
|
135
|
+
name: str,
|
|
136
|
+
*,
|
|
137
|
+
visibility_filter: VisibilityFilter | None = None,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Markdown for ``pretensor://db/{name}/overview``."""
|
|
140
|
+
reg = load_registry(graph_dir)
|
|
141
|
+
entry = resolve_registry_entry(reg, name)
|
|
142
|
+
if entry is None:
|
|
143
|
+
return f"# Unknown database `{name}`\n\nNo matching registry entry."
|
|
144
|
+
gp = graph_path_for_entry(entry)
|
|
145
|
+
entity_count = 0
|
|
146
|
+
table_count = 0
|
|
147
|
+
column_count = 0
|
|
148
|
+
row_sum = 0
|
|
149
|
+
vf = visibility_filter or get_effective_visibility_filter()
|
|
150
|
+
if gp.exists():
|
|
151
|
+
store = open_store_for_entry(entry)
|
|
152
|
+
try:
|
|
153
|
+
counts = counts_for_graph(
|
|
154
|
+
store,
|
|
155
|
+
connection_name=entry.connection_name,
|
|
156
|
+
visibility_filter=vf,
|
|
157
|
+
)
|
|
158
|
+
table_count = counts.table_count
|
|
159
|
+
column_count = counts.column_count
|
|
160
|
+
row_sum = counts.row_count
|
|
161
|
+
er = store.query_all_rows("MATCH (e:Entity) RETURN count(*)")
|
|
162
|
+
if er:
|
|
163
|
+
entity_count = int(er[0][0])
|
|
164
|
+
finally:
|
|
165
|
+
release_store(store)
|
|
166
|
+
days = staleness_days(entry.last_indexed_at)
|
|
167
|
+
threshold = stale_threshold_days(get_effective_graph_config())
|
|
168
|
+
stale = days > threshold
|
|
169
|
+
lines = [
|
|
170
|
+
f"# Overview: {entry.connection_name}",
|
|
171
|
+
"",
|
|
172
|
+
f"- **Logical database:** {entry.database}",
|
|
173
|
+
f"- **Tables:** {table_count}",
|
|
174
|
+
f"- **Columns (graph):** {column_count}",
|
|
175
|
+
f"- **Entities:** {entity_count}",
|
|
176
|
+
f"- **Row count (sum over tables):** {row_sum}",
|
|
177
|
+
f"- **Last indexed:** {iso_format(entry.last_indexed_at)}",
|
|
178
|
+
f"- **Staleness (days):** {days} (warn if >{threshold})",
|
|
179
|
+
f"- **Graph file:** `{entry.graph_path}`",
|
|
180
|
+
"",
|
|
181
|
+
]
|
|
182
|
+
if stale:
|
|
183
|
+
lines.append(
|
|
184
|
+
f"> **Warning:** Graph index is {days} days old; run "
|
|
185
|
+
"`pretensor reindex <dsn>` or `pretensor index`."
|
|
186
|
+
)
|
|
187
|
+
return "\n".join(lines)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def metrics_resource_markdown(
|
|
191
|
+
graph_dir: Path,
|
|
192
|
+
name: str,
|
|
193
|
+
*,
|
|
194
|
+
visibility_filter: VisibilityFilter | None = None,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Markdown for ``pretensor://db/{name}/metrics``."""
|
|
197
|
+
reg = load_registry(graph_dir)
|
|
198
|
+
entry = resolve_registry_entry(reg, name)
|
|
199
|
+
if entry is None:
|
|
200
|
+
return f"# Unknown database `{name}`\n\nNo matching registry entry."
|
|
201
|
+
gp = graph_path_for_entry(entry)
|
|
202
|
+
if not gp.exists():
|
|
203
|
+
return f"# Metrics — {name}\n\n_Graph file missing._"
|
|
204
|
+
|
|
205
|
+
db_key = str(entry.database)
|
|
206
|
+
cn = str(entry.connection_name)
|
|
207
|
+
vf = visibility_filter or get_effective_visibility_filter()
|
|
208
|
+
store = open_store_for_entry(entry)
|
|
209
|
+
try:
|
|
210
|
+
rows = store.query_all_rows(
|
|
211
|
+
"""
|
|
212
|
+
MATCH (m:MetricTemplate {connection_name: $cn, database: $db})
|
|
213
|
+
OPTIONAL MATCH (m)-[:METRIC_DEPENDS]->(t:SchemaTable)
|
|
214
|
+
WITH m, collect(concat(t.schema_name, '.', t.table_name)) AS dep_tables
|
|
215
|
+
RETURN m.name, m.display_name, m.description, m.sql_template, m.validated,
|
|
216
|
+
m.validation_errors_json, m.generated_at_iso, m.stale, m.dialect, dep_tables
|
|
217
|
+
ORDER BY m.name
|
|
218
|
+
""",
|
|
219
|
+
{"cn": cn, "db": db_key},
|
|
220
|
+
)
|
|
221
|
+
lines = [
|
|
222
|
+
f"# Metric templates — {entry.connection_name}",
|
|
223
|
+
"",
|
|
224
|
+
f"_Logical database:_ `{db_key}`",
|
|
225
|
+
"",
|
|
226
|
+
]
|
|
227
|
+
if not rows:
|
|
228
|
+
lines.append(
|
|
229
|
+
"_No `MetricTemplate` nodes yet. The default OSS indexing flow does "
|
|
230
|
+
"not create them automatically; this resource only lists templates "
|
|
231
|
+
"when they are already present in the graph._"
|
|
232
|
+
)
|
|
233
|
+
return "\n".join(lines)
|
|
234
|
+
|
|
235
|
+
for (
|
|
236
|
+
mname,
|
|
237
|
+
dname,
|
|
238
|
+
desc,
|
|
239
|
+
sql,
|
|
240
|
+
ok,
|
|
241
|
+
errs_json,
|
|
242
|
+
gen_iso,
|
|
243
|
+
stale,
|
|
244
|
+
dialect,
|
|
245
|
+
dep_tables,
|
|
246
|
+
) in rows:
|
|
247
|
+
tlist_raw = sorted(
|
|
248
|
+
{str(x) for x in (dep_tables or []) if x is not None and str(x).strip()}
|
|
249
|
+
)
|
|
250
|
+
if vf is not None:
|
|
251
|
+
tlist_vis = []
|
|
252
|
+
for qual in tlist_raw:
|
|
253
|
+
if "." not in qual:
|
|
254
|
+
continue
|
|
255
|
+
sn, _, tn = qual.partition(".")
|
|
256
|
+
if vf.is_table_visible(cn, sn, tn):
|
|
257
|
+
tlist_vis.append(qual)
|
|
258
|
+
if not tlist_vis and tlist_raw:
|
|
259
|
+
continue
|
|
260
|
+
tlist = tlist_vis
|
|
261
|
+
else:
|
|
262
|
+
tlist = tlist_raw
|
|
263
|
+
lines.append(f"## `{mname}`")
|
|
264
|
+
lines.append("")
|
|
265
|
+
if dname:
|
|
266
|
+
lines.append(f"**Display:** {dname}")
|
|
267
|
+
lines.append("")
|
|
268
|
+
if desc:
|
|
269
|
+
lines.append(str(desc))
|
|
270
|
+
lines.append("")
|
|
271
|
+
validated = bool(ok) if ok is not None else False
|
|
272
|
+
lines.append(f"- **Validated:** {validated}")
|
|
273
|
+
if dialect:
|
|
274
|
+
lines.append(f"- **Dialect:** {dialect}")
|
|
275
|
+
if stale:
|
|
276
|
+
lines.append(
|
|
277
|
+
"- **Stale:** yes — graph changed since this template was generated; "
|
|
278
|
+
"re-run full `pretensor index` or `--recompute-intelligence` on reindex."
|
|
279
|
+
)
|
|
280
|
+
if gen_iso:
|
|
281
|
+
lines.append(f"- **Generated:** {gen_iso}")
|
|
282
|
+
if tlist:
|
|
283
|
+
lines.append(f"- **Tables:** {', '.join(tlist)}")
|
|
284
|
+
lines.append("")
|
|
285
|
+
lines.append("```sql")
|
|
286
|
+
lines.append(str(sql or "").strip() or "-- (empty)")
|
|
287
|
+
lines.append("```")
|
|
288
|
+
lines.append("")
|
|
289
|
+
if errs_json and str(errs_json).strip() not in ("", "[]"):
|
|
290
|
+
try:
|
|
291
|
+
parsed = json.loads(str(errs_json))
|
|
292
|
+
if isinstance(parsed, list) and parsed:
|
|
293
|
+
lines.append("_Validation messages:_")
|
|
294
|
+
for item in parsed[:8]:
|
|
295
|
+
lines.append(f"- {item}")
|
|
296
|
+
lines.append("")
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
lines.append(
|
|
301
|
+
f"*Last registry index: {iso_format(entry.last_indexed_at)}. "
|
|
302
|
+
"Use `query` to search tables when available.*"
|
|
303
|
+
)
|
|
304
|
+
return "\n".join(lines)
|
|
305
|
+
finally:
|
|
306
|
+
release_store(store)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
__all__ = [
|
|
310
|
+
"clusters_resource_markdown",
|
|
311
|
+
"databases_resource_markdown",
|
|
312
|
+
"db_overview_resource_markdown",
|
|
313
|
+
"metrics_resource_markdown",
|
|
314
|
+
]
|
pretensor/mcp/server.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""MCP server (stdio) exposing Pretensor graph tools and resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import mcp.types as types
|
|
13
|
+
from mcp.server import NotificationOptions, Server
|
|
14
|
+
from mcp.server.stdio import stdio_server
|
|
15
|
+
from pydantic import AnyUrl
|
|
16
|
+
|
|
17
|
+
from pretensor.config import PretensorConfig
|
|
18
|
+
from pretensor.mcp.service import (
|
|
19
|
+
clusters_resource_markdown,
|
|
20
|
+
databases_resource_markdown,
|
|
21
|
+
db_overview_resource_markdown,
|
|
22
|
+
mcp_config_json,
|
|
23
|
+
metrics_resource_markdown,
|
|
24
|
+
)
|
|
25
|
+
from pretensor.mcp.service_context import build_server_context, set_server_context
|
|
26
|
+
from pretensor.mcp.tool_registry import McpTool, McpToolRegistry
|
|
27
|
+
from pretensor.mcp.tools import (
|
|
28
|
+
compile_metric as compile_metric_tool,
|
|
29
|
+
)
|
|
30
|
+
from pretensor.mcp.tools import (
|
|
31
|
+
consumers as consumers_tool,
|
|
32
|
+
)
|
|
33
|
+
from pretensor.mcp.tools import (
|
|
34
|
+
context as context_tool,
|
|
35
|
+
)
|
|
36
|
+
from pretensor.mcp.tools import (
|
|
37
|
+
cypher as cypher_tool,
|
|
38
|
+
)
|
|
39
|
+
from pretensor.mcp.tools import (
|
|
40
|
+
detect_changes as detect_changes_tool,
|
|
41
|
+
)
|
|
42
|
+
from pretensor.mcp.tools import (
|
|
43
|
+
impact as impact_tool,
|
|
44
|
+
)
|
|
45
|
+
from pretensor.mcp.tools import (
|
|
46
|
+
list as list_tool,
|
|
47
|
+
)
|
|
48
|
+
from pretensor.mcp.tools import (
|
|
49
|
+
schema as schema_tool,
|
|
50
|
+
)
|
|
51
|
+
from pretensor.mcp.tools import (
|
|
52
|
+
search as search_tool,
|
|
53
|
+
)
|
|
54
|
+
from pretensor.mcp.tools import (
|
|
55
|
+
semantic_search as semantic_search_tool,
|
|
56
|
+
)
|
|
57
|
+
from pretensor.mcp.tools import (
|
|
58
|
+
traverse as traverse_tool,
|
|
59
|
+
)
|
|
60
|
+
from pretensor.mcp.tools import (
|
|
61
|
+
validate_sql as validate_sql_tool,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
__all__ = ["create_server", "run_server", "print_mcp_config"]
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
_DB_OVERVIEW_PATTERN = re.compile(
|
|
69
|
+
r"^pretensor://db/(?P<name>[^/]+)/overview$",
|
|
70
|
+
)
|
|
71
|
+
_DB_CLUSTERS_PATTERN = re.compile(
|
|
72
|
+
r"^pretensor://db/(?P<name>[^/]+)/clusters$",
|
|
73
|
+
)
|
|
74
|
+
_DB_METRICS_PATTERN = re.compile(
|
|
75
|
+
r"^pretensor://db/(?P<name>[^/]+)/metrics$",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def print_mcp_config(graph_dir: Path, *, stream: Any = None) -> None:
|
|
80
|
+
"""Print the ``mcpServers`` JSON block (for Claude / Cursor) to a text stream.
|
|
81
|
+
|
|
82
|
+
Defaults to **stderr** so stdout stays reserved for MCP JSON-RPC over stdio
|
|
83
|
+
after ``serve`` starts the server.
|
|
84
|
+
"""
|
|
85
|
+
out = stream if stream is not None else __import__("sys").stderr
|
|
86
|
+
out.write(mcp_config_json(graph_dir) + "\n")
|
|
87
|
+
out.flush()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_oss_registry(graph_dir: Path) -> McpToolRegistry:
|
|
91
|
+
"""Build a :class:`McpToolRegistry` pre-loaded with all OSS tools.
|
|
92
|
+
|
|
93
|
+
Internal helper — use :func:`create_server` as the extension point (it
|
|
94
|
+
accepts ``extra_tools`` for downstream registration).
|
|
95
|
+
"""
|
|
96
|
+
registry = McpToolRegistry()
|
|
97
|
+
for create in (
|
|
98
|
+
list_tool.create_tool,
|
|
99
|
+
schema_tool.create_tool,
|
|
100
|
+
cypher_tool.create_tool,
|
|
101
|
+
search_tool.create_tool,
|
|
102
|
+
semantic_search_tool.create_tool,
|
|
103
|
+
context_tool.create_tool,
|
|
104
|
+
traverse_tool.create_tool,
|
|
105
|
+
impact_tool.create_tool,
|
|
106
|
+
consumers_tool.create_tool,
|
|
107
|
+
detect_changes_tool.create_tool,
|
|
108
|
+
compile_metric_tool.create_tool,
|
|
109
|
+
validate_sql_tool.create_tool,
|
|
110
|
+
):
|
|
111
|
+
registry.register(create(graph_dir))
|
|
112
|
+
return registry
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_resource_markdown(graph_dir: Path, key: str) -> str:
|
|
116
|
+
"""Resolve a resource URI to markdown, with a sanitized error fallback.
|
|
117
|
+
|
|
118
|
+
On any handler error the full stack is logged server-side under a
|
|
119
|
+
correlation id and the client receives a generic message — never the
|
|
120
|
+
exception text or a filesystem path.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
if key == "pretensor://databases":
|
|
124
|
+
return databases_resource_markdown(graph_dir)
|
|
125
|
+
match = _DB_OVERVIEW_PATTERN.match(key)
|
|
126
|
+
if match:
|
|
127
|
+
return db_overview_resource_markdown(graph_dir, match.group("name"))
|
|
128
|
+
match_c = _DB_CLUSTERS_PATTERN.match(key)
|
|
129
|
+
if match_c:
|
|
130
|
+
return clusters_resource_markdown(graph_dir, match_c.group("name"))
|
|
131
|
+
match_m = _DB_METRICS_PATTERN.match(key)
|
|
132
|
+
if match_m:
|
|
133
|
+
return metrics_resource_markdown(graph_dir, match_m.group("name"))
|
|
134
|
+
return f"# Resource not found\n\nUnknown URI: `{key}`"
|
|
135
|
+
except Exception:
|
|
136
|
+
correlation_id = uuid.uuid4().hex
|
|
137
|
+
logger.exception(
|
|
138
|
+
"Error reading MCP resource %r [correlation_id=%s]",
|
|
139
|
+
key,
|
|
140
|
+
correlation_id,
|
|
141
|
+
)
|
|
142
|
+
return f"# Error\n\nFailed to load resource. Correlation ID: `{correlation_id}`"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_server(
|
|
146
|
+
graph_dir: Path,
|
|
147
|
+
*,
|
|
148
|
+
visibility_path: Path | None = None,
|
|
149
|
+
profile: str | None = None,
|
|
150
|
+
config: PretensorConfig | None = None,
|
|
151
|
+
extra_tools: list[McpTool] | None = None,
|
|
152
|
+
) -> Server[object, object]:
|
|
153
|
+
"""Build the low-level MCP :class:`Server` bound to ``graph_dir``.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
graph_dir: Root directory of the Pretensor graph store.
|
|
157
|
+
visibility_path: Optional path to a visibility filter config file.
|
|
158
|
+
profile: Optional named profile for the server context.
|
|
159
|
+
config: Optional central Pretensor configuration for MCP runtime wiring.
|
|
160
|
+
extra_tools: Additional :class:`McpTool` instances to register on top
|
|
161
|
+
of the 7 built-in OSS tools (e.g. from plugins).
|
|
162
|
+
"""
|
|
163
|
+
set_server_context(
|
|
164
|
+
build_server_context(
|
|
165
|
+
graph_dir,
|
|
166
|
+
visibility_path=visibility_path,
|
|
167
|
+
profile=profile,
|
|
168
|
+
config=config,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
registry = _build_oss_registry(graph_dir)
|
|
173
|
+
for tool in extra_tools or []:
|
|
174
|
+
registry.register(tool)
|
|
175
|
+
|
|
176
|
+
server = Server[object, object](
|
|
177
|
+
"pretensor",
|
|
178
|
+
instructions=(
|
|
179
|
+
"Pretensor graph MCP: discover indexed databases, search metadata, "
|
|
180
|
+
"and fetch full context for physical tables (Kuzu schema graph)."
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@server.list_tools()
|
|
185
|
+
async def _list_tools() -> list[types.Tool]:
|
|
186
|
+
return registry.list_tools()
|
|
187
|
+
|
|
188
|
+
@server.call_tool()
|
|
189
|
+
async def _call_tool(name: str, arguments: dict[str, Any] | None) -> dict[str, Any]:
|
|
190
|
+
try:
|
|
191
|
+
return await registry.call_tool(name, arguments)
|
|
192
|
+
except Exception:
|
|
193
|
+
correlation_id = uuid.uuid4().hex
|
|
194
|
+
logger.exception(
|
|
195
|
+
"Unexpected error dispatching MCP tool %r [correlation_id=%s]",
|
|
196
|
+
name,
|
|
197
|
+
correlation_id,
|
|
198
|
+
)
|
|
199
|
+
return {
|
|
200
|
+
"error": "Internal server error",
|
|
201
|
+
"tool": name,
|
|
202
|
+
"correlation_id": correlation_id,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@server.list_resources()
|
|
206
|
+
async def _list_resources() -> list[types.Resource]:
|
|
207
|
+
return [
|
|
208
|
+
types.Resource(
|
|
209
|
+
uri=AnyUrl("pretensor://databases"),
|
|
210
|
+
name="databases",
|
|
211
|
+
title="All indexed databases",
|
|
212
|
+
description="Registry overview (markdown)",
|
|
213
|
+
mimeType="text/markdown",
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
@server.list_resource_templates()
|
|
218
|
+
async def _list_resource_templates() -> list[types.ResourceTemplate]:
|
|
219
|
+
return [
|
|
220
|
+
types.ResourceTemplate(
|
|
221
|
+
uriTemplate="pretensor://db/{name}/overview",
|
|
222
|
+
name="db-overview",
|
|
223
|
+
title="Per-database overview",
|
|
224
|
+
description="Table count, entity count, staleness (markdown)",
|
|
225
|
+
mimeType="text/markdown",
|
|
226
|
+
),
|
|
227
|
+
types.ResourceTemplate(
|
|
228
|
+
uriTemplate="pretensor://db/{name}/clusters",
|
|
229
|
+
name="db-clusters",
|
|
230
|
+
title="Per-database domain clusters",
|
|
231
|
+
description="Leiden clusters and table groupings (markdown)",
|
|
232
|
+
mimeType="text/markdown",
|
|
233
|
+
),
|
|
234
|
+
types.ResourceTemplate(
|
|
235
|
+
uriTemplate="pretensor://db/{name}/metrics",
|
|
236
|
+
name="db-metrics",
|
|
237
|
+
title="Per-database metric templates",
|
|
238
|
+
description="MetricTemplate nodes: SQL, validation, dependencies (markdown)",
|
|
239
|
+
mimeType="text/markdown",
|
|
240
|
+
),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
@server.read_resource()
|
|
244
|
+
async def _read_resource(uri: AnyUrl) -> str:
|
|
245
|
+
return render_resource_markdown(graph_dir, str(uri))
|
|
246
|
+
|
|
247
|
+
return server
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def _run_async(
|
|
251
|
+
graph_dir: Path,
|
|
252
|
+
*,
|
|
253
|
+
visibility_path: Path | None = None,
|
|
254
|
+
profile: str | None = None,
|
|
255
|
+
config: PretensorConfig | None = None,
|
|
256
|
+
) -> None:
|
|
257
|
+
server = create_server(
|
|
258
|
+
graph_dir,
|
|
259
|
+
visibility_path=visibility_path,
|
|
260
|
+
profile=profile,
|
|
261
|
+
config=config,
|
|
262
|
+
)
|
|
263
|
+
init = server.create_initialization_options(
|
|
264
|
+
notification_options=NotificationOptions(resources_changed=False)
|
|
265
|
+
)
|
|
266
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
267
|
+
await server.run(read_stream, write_stream, init)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def run_server(
|
|
271
|
+
graph_dir: Path,
|
|
272
|
+
*,
|
|
273
|
+
visibility_path: Path | None = None,
|
|
274
|
+
profile: str | None = None,
|
|
275
|
+
config: PretensorConfig | None = None,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Start the MCP server on stdio (blocking)."""
|
|
278
|
+
asyncio.run(
|
|
279
|
+
_run_async(
|
|
280
|
+
graph_dir,
|
|
281
|
+
visibility_path=visibility_path,
|
|
282
|
+
profile=profile,
|
|
283
|
+
config=config,
|
|
284
|
+
)
|
|
285
|
+
)
|
pretensor/mcp/service.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""MCP-facing read logic: stable import surface for tools and resources.
|
|
2
|
+
|
|
3
|
+
Implementation lives in ``pretensor.mcp.tools`` and sibling modules; this module
|
|
4
|
+
re-exports the public API used by ``server.py`` and tests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pretensor.mcp.config_json import mcp_config_json
|
|
10
|
+
from pretensor.mcp.resources import (
|
|
11
|
+
clusters_resource_markdown,
|
|
12
|
+
databases_resource_markdown,
|
|
13
|
+
db_overview_resource_markdown,
|
|
14
|
+
metrics_resource_markdown,
|
|
15
|
+
)
|
|
16
|
+
from pretensor.mcp.service_registry import resolve_registry_entry
|
|
17
|
+
from pretensor.mcp.tools.compile_metric import compile_metric_payload
|
|
18
|
+
from pretensor.mcp.tools.consumers import consumers_payload
|
|
19
|
+
from pretensor.mcp.tools.context import context_payload
|
|
20
|
+
from pretensor.mcp.tools.cypher import cypher_payload
|
|
21
|
+
from pretensor.mcp.tools.detect_changes import detect_changes_payload
|
|
22
|
+
from pretensor.mcp.tools.impact import impact_payload
|
|
23
|
+
from pretensor.mcp.tools.list import list_databases_payload
|
|
24
|
+
from pretensor.mcp.tools.schema import schema_payload
|
|
25
|
+
from pretensor.mcp.tools.search import query_payload
|
|
26
|
+
from pretensor.mcp.tools.semantic_search import semantic_search_payload
|
|
27
|
+
from pretensor.mcp.tools.traverse import traverse_payload
|
|
28
|
+
from pretensor.mcp.tools.validate_sql import validate_sql_payload
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"list_databases_payload",
|
|
32
|
+
"query_payload",
|
|
33
|
+
"semantic_search_payload",
|
|
34
|
+
"cypher_payload",
|
|
35
|
+
"context_payload",
|
|
36
|
+
"traverse_payload",
|
|
37
|
+
"impact_payload",
|
|
38
|
+
"consumers_payload",
|
|
39
|
+
"detect_changes_payload",
|
|
40
|
+
"compile_metric_payload",
|
|
41
|
+
"schema_payload",
|
|
42
|
+
"validate_sql_payload",
|
|
43
|
+
"databases_resource_markdown",
|
|
44
|
+
"db_overview_resource_markdown",
|
|
45
|
+
"metrics_resource_markdown",
|
|
46
|
+
"clusters_resource_markdown",
|
|
47
|
+
"resolve_registry_entry",
|
|
48
|
+
"mcp_config_json",
|
|
49
|
+
]
|