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.
Files changed (198) hide show
  1. pretensor/__init__.py +50 -0
  2. pretensor/benchmark/__init__.py +54 -0
  3. pretensor/benchmark/cli.py +294 -0
  4. pretensor/benchmark/fixtures.py +84 -0
  5. pretensor/benchmark/l1/__init__.py +23 -0
  6. pretensor/benchmark/l1/metrics.py +141 -0
  7. pretensor/benchmark/l1/pipeline.py +188 -0
  8. pretensor/benchmark/l1/runner.py +245 -0
  9. pretensor/benchmark/l2/__init__.py +27 -0
  10. pretensor/benchmark/l2/gold.py +236 -0
  11. pretensor/benchmark/l2/metrics.py +146 -0
  12. pretensor/benchmark/l2/pipeline.py +124 -0
  13. pretensor/benchmark/l2/runner.py +530 -0
  14. pretensor/benchmark/l3/__init__.py +73 -0
  15. pretensor/benchmark/l3/agent.py +316 -0
  16. pretensor/benchmark/l3/db.py +188 -0
  17. pretensor/benchmark/l3/gold.py +85 -0
  18. pretensor/benchmark/l3/llm_client.py +395 -0
  19. pretensor/benchmark/l3/mcp_client.py +357 -0
  20. pretensor/benchmark/l3/pretensor_runner.py +456 -0
  21. pretensor/benchmark/l3/prompt.py +132 -0
  22. pretensor/benchmark/l3/runner.py +358 -0
  23. pretensor/benchmark/l3/sql_equivalence.py +176 -0
  24. pretensor/benchmark/release_gate.py +448 -0
  25. pretensor/benchmark/results.py +298 -0
  26. pretensor/benchmark/runner.py +109 -0
  27. pretensor/cli/__init__.py +1 -0
  28. pretensor/cli/commands/_source_runner.py +147 -0
  29. pretensor/cli/commands/analyze.py +201 -0
  30. pretensor/cli/commands/connections/__init__.py +7 -0
  31. pretensor/cli/commands/connections/add_remove.py +126 -0
  32. pretensor/cli/commands/connections/register.py +12 -0
  33. pretensor/cli/commands/export.py +131 -0
  34. pretensor/cli/commands/index.py +559 -0
  35. pretensor/cli/commands/list.py +76 -0
  36. pretensor/cli/commands/quickstart.py +207 -0
  37. pretensor/cli/commands/reindex.py +646 -0
  38. pretensor/cli/commands/semantic.py +190 -0
  39. pretensor/cli/commands/serve.py +144 -0
  40. pretensor/cli/commands/sync_grants.py +149 -0
  41. pretensor/cli/commands/validate.py +176 -0
  42. pretensor/cli/config_file.py +442 -0
  43. pretensor/cli/constants.py +10 -0
  44. pretensor/cli/dbt_enrichment.py +96 -0
  45. pretensor/cli/main.py +109 -0
  46. pretensor/cli/paths.py +43 -0
  47. pretensor/cli/plugin.py +52 -0
  48. pretensor/config.py +226 -0
  49. pretensor/connectors/__init__.py +29 -0
  50. pretensor/connectors/base.py +165 -0
  51. pretensor/connectors/bigquery.py +468 -0
  52. pretensor/connectors/inspect.py +321 -0
  53. pretensor/connectors/lineage_sqlglot.py +97 -0
  54. pretensor/connectors/models.py +130 -0
  55. pretensor/connectors/mysql.py +402 -0
  56. pretensor/connectors/pg_array_parse.py +53 -0
  57. pretensor/connectors/postgres.py +938 -0
  58. pretensor/connectors/registry.py +93 -0
  59. pretensor/connectors/snapshot.py +244 -0
  60. pretensor/connectors/snowflake.py +908 -0
  61. pretensor/core/__init__.py +1 -0
  62. pretensor/core/builder.py +307 -0
  63. pretensor/core/dsn_crypto.py +51 -0
  64. pretensor/core/graph_schema_manager.py +246 -0
  65. pretensor/core/graph_store.py +1226 -0
  66. pretensor/core/ids.py +101 -0
  67. pretensor/core/portable_export.py +276 -0
  68. pretensor/core/query_runner.py +67 -0
  69. pretensor/core/registry.py +209 -0
  70. pretensor/core/schema.py +473 -0
  71. pretensor/core/secure_io.py +93 -0
  72. pretensor/core/store.py +469 -0
  73. pretensor/enrichment/__init__.py +1 -0
  74. pretensor/enrichment/analyze/__init__.py +0 -0
  75. pretensor/enrichment/analyze/classify.py +49 -0
  76. pretensor/enrichment/analyze/extract_python.py +196 -0
  77. pretensor/enrichment/analyze/parse.py +141 -0
  78. pretensor/enrichment/analyze/pipeline.py +195 -0
  79. pretensor/enrichment/analyze/summary.py +38 -0
  80. pretensor/enrichment/analyze/walker.py +98 -0
  81. pretensor/enrichment/analyze/writers.py +214 -0
  82. pretensor/enrichment/dbt/__init__.py +30 -0
  83. pretensor/enrichment/dbt/lineage.py +100 -0
  84. pretensor/enrichment/dbt/manifest.py +300 -0
  85. pretensor/enrichment/dbt/metadata.py +263 -0
  86. pretensor/enrichment/dbt/pipeline.py +77 -0
  87. pretensor/enrichment/dbt/resolution.py +101 -0
  88. pretensor/enrichment/dbt/signals.py +305 -0
  89. pretensor/entities/__init__.py +27 -0
  90. pretensor/entities/builder.py +63 -0
  91. pretensor/entities/classifier.py +383 -0
  92. pretensor/entities/llm_extract.py +66 -0
  93. pretensor/errors.py +35 -0
  94. pretensor/graph_models/__init__.py +17 -0
  95. pretensor/graph_models/base.py +11 -0
  96. pretensor/graph_models/consumer.py +71 -0
  97. pretensor/graph_models/edge.py +35 -0
  98. pretensor/graph_models/entity.py +21 -0
  99. pretensor/graph_models/node.py +79 -0
  100. pretensor/graph_models/relationship.py +33 -0
  101. pretensor/integrations/__init__.py +42 -0
  102. pretensor/integrations/_base.py +138 -0
  103. pretensor/integrations/google_adk.py +49 -0
  104. pretensor/integrations/langchain.py +55 -0
  105. pretensor/integrations/llamaindex.py +53 -0
  106. pretensor/intelligence/__init__.py +33 -0
  107. pretensor/intelligence/cluster_labeler.py +425 -0
  108. pretensor/intelligence/clustering.py +168 -0
  109. pretensor/intelligence/combining.py +32 -0
  110. pretensor/intelligence/discovery.py +114 -0
  111. pretensor/intelligence/embeddings.py +317 -0
  112. pretensor/intelligence/graph_export.py +200 -0
  113. pretensor/intelligence/heuristic.py +544 -0
  114. pretensor/intelligence/join_paths/__init__.py +130 -0
  115. pretensor/intelligence/join_paths/on_demand.py +516 -0
  116. pretensor/intelligence/join_paths/storage.py +70 -0
  117. pretensor/intelligence/llm_infer.py +78 -0
  118. pretensor/intelligence/llm_runtime.py +62 -0
  119. pretensor/intelligence/metric_templates.py +193 -0
  120. pretensor/intelligence/pipeline.py +364 -0
  121. pretensor/intelligence/role_exemplars.py +263 -0
  122. pretensor/intelligence/schema_classification.py +360 -0
  123. pretensor/intelligence/scoring.py +76 -0
  124. pretensor/intelligence/semantic.py +240 -0
  125. pretensor/intelligence/shadow_alias.py +101 -0
  126. pretensor/intelligence/statistical.py +50 -0
  127. pretensor/intelligence/steps.py +191 -0
  128. pretensor/intelligence/steps_embedding.py +168 -0
  129. pretensor/introspection/__init__.py +6 -0
  130. pretensor/introspection/inspector.py +5 -0
  131. pretensor/introspection/models/__init__.py +0 -0
  132. pretensor/introspection/models/base.py +5 -0
  133. pretensor/introspection/models/config.py +237 -0
  134. pretensor/introspection/models/dsn.py +550 -0
  135. pretensor/introspection/models/plan.py +116 -0
  136. pretensor/introspection/models/schema.py +10 -0
  137. pretensor/introspection/models/semantic.py +121 -0
  138. pretensor/introspection/models/validation.py +116 -0
  139. pretensor/introspection/snapshot.py +46 -0
  140. pretensor/mcp/__init__.py +16 -0
  141. pretensor/mcp/config_json.py +24 -0
  142. pretensor/mcp/payload_types.py +274 -0
  143. pretensor/mcp/resources/__init__.py +17 -0
  144. pretensor/mcp/resources/markdown.py +314 -0
  145. pretensor/mcp/server.py +285 -0
  146. pretensor/mcp/service.py +49 -0
  147. pretensor/mcp/service_context.py +142 -0
  148. pretensor/mcp/service_registry.py +294 -0
  149. pretensor/mcp/store_cache.py +43 -0
  150. pretensor/mcp/tool_registry.py +136 -0
  151. pretensor/mcp/tools/__init__.py +1 -0
  152. pretensor/mcp/tools/_rank.py +244 -0
  153. pretensor/mcp/tools/_timed.py +26 -0
  154. pretensor/mcp/tools/compile_metric.py +144 -0
  155. pretensor/mcp/tools/consumers.py +161 -0
  156. pretensor/mcp/tools/context.py +1121 -0
  157. pretensor/mcp/tools/cypher.py +509 -0
  158. pretensor/mcp/tools/detect_changes.py +254 -0
  159. pretensor/mcp/tools/impact.py +271 -0
  160. pretensor/mcp/tools/list.py +131 -0
  161. pretensor/mcp/tools/schema.py +170 -0
  162. pretensor/mcp/tools/search.py +316 -0
  163. pretensor/mcp/tools/semantic_search.py +282 -0
  164. pretensor/mcp/tools/traverse.py +1027 -0
  165. pretensor/mcp/tools/validate_sql.py +150 -0
  166. pretensor/observability.py +203 -0
  167. pretensor/py.typed +0 -0
  168. pretensor/quickstart/README.md +29 -0
  169. pretensor/quickstart/__init__.py +6 -0
  170. pretensor/quickstart/docker-compose.yml +18 -0
  171. pretensor/quickstart/pagila_data.sql +63 -0
  172. pretensor/quickstart/pagila_ddl.sql +92 -0
  173. pretensor/search/__init__.py +6 -0
  174. pretensor/search/base.py +80 -0
  175. pretensor/search/index.py +435 -0
  176. pretensor/semantic/__init__.py +24 -0
  177. pretensor/semantic/base.py +123 -0
  178. pretensor/semantic/compiler.py +487 -0
  179. pretensor/semantic/yaml_layer.py +180 -0
  180. pretensor/skills/__init__.py +5 -0
  181. pretensor/skills/generator.py +235 -0
  182. pretensor/staleness/__init__.py +15 -0
  183. pretensor/staleness/graph_patcher.py +355 -0
  184. pretensor/staleness/impact_analyzer.py +162 -0
  185. pretensor/staleness/snapshot_store.py +38 -0
  186. pretensor/validation/__init__.py +9 -0
  187. pretensor/validation/query_validator.py +436 -0
  188. pretensor/visibility/__init__.py +23 -0
  189. pretensor/visibility/config.py +126 -0
  190. pretensor/visibility/filter.py +143 -0
  191. pretensor/visibility/kuzu_helpers.py +32 -0
  192. pretensor/visibility/runtime.py +36 -0
  193. pretensor/visibility/sync_grants.py +188 -0
  194. pretensor-0.1.0.dist-info/METADATA +251 -0
  195. pretensor-0.1.0.dist-info/RECORD +198 -0
  196. pretensor-0.1.0.dist-info/WHEEL +4 -0
  197. pretensor-0.1.0.dist-info/entry_points.txt +2 -0
  198. 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
+ ]
@@ -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
+ )
@@ -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
+ ]