coalesce-transform-mcp 0.3.0 → 0.4.2

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 (279) hide show
  1. package/README.md +74 -3
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +6 -2
  4. package/dist/client.js.map +1 -1
  5. package/dist/coalesce/api/environments.d.ts +0 -12
  6. package/dist/coalesce/api/environments.d.ts.map +1 -1
  7. package/dist/coalesce/api/environments.js +0 -4
  8. package/dist/coalesce/api/environments.js.map +1 -1
  9. package/dist/coalesce/api/jobs.d.ts +3 -5
  10. package/dist/coalesce/api/jobs.d.ts.map +1 -1
  11. package/dist/coalesce/api/jobs.js +3 -6
  12. package/dist/coalesce/api/jobs.js.map +1 -1
  13. package/dist/coalesce/api/nodes.d.ts +3 -3
  14. package/dist/coalesce/api/nodes.d.ts.map +1 -1
  15. package/dist/coalesce/api/nodes.js +6 -4
  16. package/dist/coalesce/api/nodes.js.map +1 -1
  17. package/dist/coalesce/api/runs.d.ts.map +1 -1
  18. package/dist/coalesce/api/runs.js +11 -1
  19. package/dist/coalesce/api/runs.js.map +1 -1
  20. package/dist/coalesce/api/scan.d.ts +14 -0
  21. package/dist/coalesce/api/scan.d.ts.map +1 -0
  22. package/dist/coalesce/api/scan.js +64 -0
  23. package/dist/coalesce/api/scan.js.map +1 -0
  24. package/dist/coalesce/api/subgraphs.d.ts +3 -2
  25. package/dist/coalesce/api/subgraphs.d.ts.map +1 -1
  26. package/dist/coalesce/api/subgraphs.js +3 -2
  27. package/dist/coalesce/api/subgraphs.js.map +1 -1
  28. package/dist/coalesce/run-schemas.d.ts.map +1 -1
  29. package/dist/coalesce/run-schemas.js +26 -16
  30. package/dist/coalesce/run-schemas.js.map +1 -1
  31. package/dist/coalesce/tool-response.d.ts +1 -13
  32. package/dist/coalesce/tool-response.d.ts.map +1 -1
  33. package/dist/coalesce/tool-response.js +20 -6
  34. package/dist/coalesce/tool-response.js.map +1 -1
  35. package/dist/coalesce/tool-schemas.d.ts +1 -2
  36. package/dist/coalesce/tool-schemas.d.ts.map +1 -1
  37. package/dist/coalesce/tool-schemas.js +368 -5
  38. package/dist/coalesce/tool-schemas.js.map +1 -1
  39. package/dist/coalesce/types.d.ts +8 -0
  40. package/dist/coalesce/types.d.ts.map +1 -1
  41. package/dist/coalesce/types.js +3 -1
  42. package/dist/coalesce/types.js.map +1 -1
  43. package/dist/constants.d.ts +18 -0
  44. package/dist/constants.d.ts.map +1 -0
  45. package/dist/constants.js +21 -0
  46. package/dist/constants.js.map +1 -0
  47. package/dist/mcp/cache.d.ts +2 -1
  48. package/dist/mcp/cache.d.ts.map +1 -1
  49. package/dist/mcp/cache.js +122 -138
  50. package/dist/mcp/cache.js.map +1 -1
  51. package/dist/mcp/environments.d.ts +2 -1
  52. package/dist/mcp/environments.d.ts.map +1 -1
  53. package/dist/mcp/environments.js +56 -112
  54. package/dist/mcp/environments.js.map +1 -1
  55. package/dist/mcp/git-accounts.d.ts +2 -1
  56. package/dist/mcp/git-accounts.d.ts.map +1 -1
  57. package/dist/mcp/git-accounts.js +74 -96
  58. package/dist/mcp/git-accounts.js.map +1 -1
  59. package/dist/mcp/jobs.d.ts +2 -1
  60. package/dist/mcp/jobs.d.ts.map +1 -1
  61. package/dist/mcp/jobs.js +68 -122
  62. package/dist/mcp/jobs.js.map +1 -1
  63. package/dist/mcp/lineage.d.ts +5 -0
  64. package/dist/mcp/lineage.d.ts.map +1 -0
  65. package/dist/mcp/lineage.js +410 -0
  66. package/dist/mcp/lineage.js.map +1 -0
  67. package/dist/mcp/node-type-corpus.d.ts +2 -1
  68. package/dist/mcp/node-type-corpus.d.ts.map +1 -1
  69. package/dist/mcp/node-type-corpus.js +148 -151
  70. package/dist/mcp/node-type-corpus.js.map +1 -1
  71. package/dist/mcp/nodes.d.ts +2 -1
  72. package/dist/mcp/nodes.d.ts.map +1 -1
  73. package/dist/mcp/nodes.js +358 -464
  74. package/dist/mcp/nodes.js.map +1 -1
  75. package/dist/mcp/pipelines.d.ts +2 -1
  76. package/dist/mcp/pipelines.d.ts.map +1 -1
  77. package/dist/mcp/pipelines.js +514 -314
  78. package/dist/mcp/pipelines.js.map +1 -1
  79. package/dist/mcp/projects.d.ts +2 -1
  80. package/dist/mcp/projects.d.ts.map +1 -1
  81. package/dist/mcp/projects.js +66 -100
  82. package/dist/mcp/projects.js.map +1 -1
  83. package/dist/mcp/repo-node-types.d.ts +2 -1
  84. package/dist/mcp/repo-node-types.d.ts.map +1 -1
  85. package/dist/mcp/repo-node-types.js +92 -121
  86. package/dist/mcp/repo-node-types.js.map +1 -1
  87. package/dist/mcp/runs.d.ts +3 -2
  88. package/dist/mcp/runs.d.ts.map +1 -1
  89. package/dist/mcp/runs.js +93 -148
  90. package/dist/mcp/runs.js.map +1 -1
  91. package/dist/mcp/skills.d.ts +13 -0
  92. package/dist/mcp/skills.d.ts.map +1 -0
  93. package/dist/mcp/skills.js +85 -0
  94. package/dist/mcp/skills.js.map +1 -0
  95. package/dist/mcp/subgraphs.d.ts +2 -1
  96. package/dist/mcp/subgraphs.d.ts.map +1 -1
  97. package/dist/mcp/subgraphs.js +61 -98
  98. package/dist/mcp/subgraphs.js.map +1 -1
  99. package/dist/mcp/tool-helpers.d.ts +37 -0
  100. package/dist/mcp/tool-helpers.d.ts.map +1 -0
  101. package/dist/mcp/tool-helpers.js +82 -0
  102. package/dist/mcp/tool-helpers.js.map +1 -0
  103. package/dist/mcp/users.d.ts +2 -1
  104. package/dist/mcp/users.d.ts.map +1 -1
  105. package/dist/mcp/users.js +92 -145
  106. package/dist/mcp/users.js.map +1 -1
  107. package/dist/mcp/workshop.d.ts +2 -1
  108. package/dist/mcp/workshop.d.ts.map +1 -1
  109. package/dist/mcp/workshop.js +66 -101
  110. package/dist/mcp/workshop.js.map +1 -1
  111. package/dist/mcp/workspaces.d.ts +2 -1
  112. package/dist/mcp/workspaces.d.ts.map +1 -1
  113. package/dist/mcp/workspaces.js +19 -34
  114. package/dist/mcp/workspaces.js.map +1 -1
  115. package/dist/prompts/index.d.ts.map +1 -1
  116. package/dist/prompts/index.js +85 -0
  117. package/dist/prompts/index.js.map +1 -1
  118. package/dist/resources/context/pipeline-workshop-guide.md +1 -1
  119. package/dist/resources/context/tool-usage.md +7 -0
  120. package/dist/resources/index.d.ts +13 -0
  121. package/dist/resources/index.d.ts.map +1 -1
  122. package/dist/resources/index.js +105 -5
  123. package/dist/resources/index.js.map +1 -1
  124. package/dist/schemas/node-payloads.d.ts +2 -2
  125. package/dist/server.d.ts +2 -1
  126. package/dist/server.d.ts.map +1 -1
  127. package/dist/server.js +158 -41
  128. package/dist/server.js.map +1 -1
  129. package/dist/services/cache/snapshots.d.ts.map +1 -1
  130. package/dist/services/cache/snapshots.js +9 -5
  131. package/dist/services/cache/snapshots.js.map +1 -1
  132. package/dist/services/config/schema-resolver.d.ts.map +1 -1
  133. package/dist/services/config/schema-resolver.js +3 -6
  134. package/dist/services/config/schema-resolver.js.map +1 -1
  135. package/dist/services/lineage/lineage-cache.d.ts +53 -0
  136. package/dist/services/lineage/lineage-cache.d.ts.map +1 -0
  137. package/dist/services/lineage/lineage-cache.js +335 -0
  138. package/dist/services/lineage/lineage-cache.js.map +1 -0
  139. package/dist/services/lineage/lineage-documentation.d.ts +29 -0
  140. package/dist/services/lineage/lineage-documentation.d.ts.map +1 -0
  141. package/dist/services/lineage/lineage-documentation.js +80 -0
  142. package/dist/services/lineage/lineage-documentation.js.map +1 -0
  143. package/dist/services/lineage/lineage-propagation.d.ts +47 -0
  144. package/dist/services/lineage/lineage-propagation.d.ts.map +1 -0
  145. package/dist/services/lineage/lineage-propagation.js +176 -0
  146. package/dist/services/lineage/lineage-propagation.js.map +1 -0
  147. package/dist/services/lineage/lineage-search.d.ts +33 -0
  148. package/dist/services/lineage/lineage-search.d.ts.map +1 -0
  149. package/dist/services/lineage/lineage-search.js +133 -0
  150. package/dist/services/lineage/lineage-search.js.map +1 -0
  151. package/dist/services/lineage/lineage-traversal.d.ts +34 -0
  152. package/dist/services/lineage/lineage-traversal.d.ts.map +1 -0
  153. package/dist/services/lineage/lineage-traversal.js +283 -0
  154. package/dist/services/lineage/lineage-traversal.js.map +1 -0
  155. package/dist/services/pipelines/clause-extraction.d.ts +3 -0
  156. package/dist/services/pipelines/clause-extraction.d.ts.map +1 -0
  157. package/dist/services/pipelines/clause-extraction.js +27 -0
  158. package/dist/services/pipelines/clause-extraction.js.map +1 -0
  159. package/dist/services/pipelines/column-helpers.d.ts +8 -0
  160. package/dist/services/pipelines/column-helpers.d.ts.map +1 -0
  161. package/dist/services/pipelines/column-helpers.js +125 -0
  162. package/dist/services/pipelines/column-helpers.js.map +1 -0
  163. package/dist/services/pipelines/cte-parsing.d.ts +29 -0
  164. package/dist/services/pipelines/cte-parsing.d.ts.map +1 -0
  165. package/dist/services/pipelines/cte-parsing.js +160 -0
  166. package/dist/services/pipelines/cte-parsing.js.map +1 -0
  167. package/dist/services/pipelines/cte-planning.d.ts +22 -0
  168. package/dist/services/pipelines/cte-planning.d.ts.map +1 -0
  169. package/dist/services/pipelines/cte-planning.js +206 -0
  170. package/dist/services/pipelines/cte-planning.js.map +1 -0
  171. package/dist/services/pipelines/execution.d.ts.map +1 -1
  172. package/dist/services/pipelines/execution.js +0 -1
  173. package/dist/services/pipelines/execution.js.map +1 -1
  174. package/dist/services/pipelines/intent-parsing.d.ts +24 -0
  175. package/dist/services/pipelines/intent-parsing.d.ts.map +1 -0
  176. package/dist/services/pipelines/intent-parsing.js +245 -0
  177. package/dist/services/pipelines/intent-parsing.js.map +1 -0
  178. package/dist/services/pipelines/intent-resolution.d.ts +24 -0
  179. package/dist/services/pipelines/intent-resolution.d.ts.map +1 -0
  180. package/dist/services/pipelines/intent-resolution.js +141 -0
  181. package/dist/services/pipelines/intent-resolution.js.map +1 -0
  182. package/dist/services/pipelines/intent.d.ts +4 -45
  183. package/dist/services/pipelines/intent.d.ts.map +1 -1
  184. package/dist/services/pipelines/intent.js +14 -408
  185. package/dist/services/pipelines/intent.js.map +1 -1
  186. package/dist/services/pipelines/node-type-candidates.d.ts +6 -0
  187. package/dist/services/pipelines/node-type-candidates.d.ts.map +1 -0
  188. package/dist/services/pipelines/node-type-candidates.js +165 -0
  189. package/dist/services/pipelines/node-type-candidates.js.map +1 -0
  190. package/dist/services/pipelines/node-type-intent.d.ts +1 -5
  191. package/dist/services/pipelines/node-type-intent.d.ts.map +1 -1
  192. package/dist/services/pipelines/node-type-intent.js +1 -5
  193. package/dist/services/pipelines/node-type-intent.js.map +1 -1
  194. package/dist/services/pipelines/node-type-scoring.d.ts +13 -0
  195. package/dist/services/pipelines/node-type-scoring.d.ts.map +1 -0
  196. package/dist/services/pipelines/node-type-scoring.js +322 -0
  197. package/dist/services/pipelines/node-type-scoring.js.map +1 -0
  198. package/dist/services/pipelines/node-type-selection.d.ts +22 -2
  199. package/dist/services/pipelines/node-type-selection.d.ts.map +1 -1
  200. package/dist/services/pipelines/node-type-selection.js +16 -538
  201. package/dist/services/pipelines/node-type-selection.js.map +1 -1
  202. package/dist/services/pipelines/plan-builder.d.ts +33 -0
  203. package/dist/services/pipelines/plan-builder.d.ts.map +1 -0
  204. package/dist/services/pipelines/plan-builder.js +224 -0
  205. package/dist/services/pipelines/plan-builder.js.map +1 -0
  206. package/dist/services/pipelines/planning-types.d.ts +543 -0
  207. package/dist/services/pipelines/planning-types.d.ts.map +1 -0
  208. package/dist/services/pipelines/planning-types.js +85 -0
  209. package/dist/services/pipelines/planning-types.js.map +1 -0
  210. package/dist/services/pipelines/planning.d.ts +8 -537
  211. package/dist/services/pipelines/planning.d.ts.map +1 -1
  212. package/dist/services/pipelines/planning.js +10 -1956
  213. package/dist/services/pipelines/planning.js.map +1 -1
  214. package/dist/services/pipelines/review.d.ts.map +1 -1
  215. package/dist/services/pipelines/review.js +3 -8
  216. package/dist/services/pipelines/review.js.map +1 -1
  217. package/dist/services/pipelines/select-parsing.d.ts +7 -0
  218. package/dist/services/pipelines/select-parsing.d.ts.map +1 -0
  219. package/dist/services/pipelines/select-parsing.js +185 -0
  220. package/dist/services/pipelines/select-parsing.js.map +1 -0
  221. package/dist/services/pipelines/source-parsing.d.ts +8 -0
  222. package/dist/services/pipelines/source-parsing.d.ts.map +1 -0
  223. package/dist/services/pipelines/source-parsing.js +151 -0
  224. package/dist/services/pipelines/source-parsing.js.map +1 -0
  225. package/dist/services/pipelines/sql-parsing.d.ts +8 -0
  226. package/dist/services/pipelines/sql-parsing.d.ts.map +1 -0
  227. package/dist/services/pipelines/sql-parsing.js +9 -0
  228. package/dist/services/pipelines/sql-parsing.js.map +1 -0
  229. package/dist/services/pipelines/sql-tokenizer.d.ts +42 -0
  230. package/dist/services/pipelines/sql-tokenizer.d.ts.map +1 -0
  231. package/dist/services/pipelines/sql-tokenizer.js +493 -0
  232. package/dist/services/pipelines/sql-tokenizer.js.map +1 -0
  233. package/dist/services/pipelines/sql-utils.d.ts +30 -0
  234. package/dist/services/pipelines/sql-utils.d.ts.map +1 -0
  235. package/dist/services/pipelines/sql-utils.js +62 -0
  236. package/dist/services/pipelines/sql-utils.js.map +1 -0
  237. package/dist/services/pipelines/workshop.d.ts.map +1 -1
  238. package/dist/services/pipelines/workshop.js +53 -25
  239. package/dist/services/pipelines/workshop.js.map +1 -1
  240. package/dist/services/pipelines/workspace-resolution.d.ts +18 -0
  241. package/dist/services/pipelines/workspace-resolution.d.ts.map +1 -0
  242. package/dist/services/pipelines/workspace-resolution.js +279 -0
  243. package/dist/services/pipelines/workspace-resolution.js.map +1 -0
  244. package/dist/services/runs/diagnostics.d.ts.map +1 -1
  245. package/dist/services/runs/diagnostics.js +3 -8
  246. package/dist/services/runs/diagnostics.js.map +1 -1
  247. package/dist/services/shared/elicitation.d.ts +14 -0
  248. package/dist/services/shared/elicitation.d.ts.map +1 -0
  249. package/dist/services/shared/elicitation.js +56 -0
  250. package/dist/services/shared/elicitation.js.map +1 -0
  251. package/dist/services/workspace/node-creation.d.ts.map +1 -1
  252. package/dist/services/workspace/node-creation.js +5 -1
  253. package/dist/services/workspace/node-creation.js.map +1 -1
  254. package/dist/services/workspace/node-update-helpers.d.ts.map +1 -1
  255. package/dist/services/workspace/node-update-helpers.js +3 -8
  256. package/dist/services/workspace/node-update-helpers.js.map +1 -1
  257. package/dist/utils.d.ts +11 -0
  258. package/dist/utils.d.ts.map +1 -1
  259. package/dist/utils.js +20 -1
  260. package/dist/utils.js.map +1 -1
  261. package/dist/workflows/get-environment-health.d.ts +49 -0
  262. package/dist/workflows/get-environment-health.d.ts.map +1 -0
  263. package/dist/workflows/get-environment-health.js +310 -0
  264. package/dist/workflows/get-environment-health.js.map +1 -0
  265. package/dist/workflows/get-environment-overview.d.ts +2 -1
  266. package/dist/workflows/get-environment-overview.d.ts.map +1 -1
  267. package/dist/workflows/get-environment-overview.js +13 -19
  268. package/dist/workflows/get-environment-overview.js.map +1 -1
  269. package/dist/workflows/get-run-details.d.ts +2 -2
  270. package/dist/workflows/get-run-details.d.ts.map +1 -1
  271. package/dist/workflows/get-run-details.js +14 -19
  272. package/dist/workflows/get-run-details.js.map +1 -1
  273. package/dist/workflows/retry-and-wait.d.ts.map +1 -1
  274. package/dist/workflows/retry-and-wait.js +3 -2
  275. package/dist/workflows/retry-and-wait.js.map +1 -1
  276. package/dist/workflows/run-and-wait.d.ts.map +1 -1
  277. package/dist/workflows/run-and-wait.js +3 -2
  278. package/dist/workflows/run-and-wait.js.map +1 -1
  279. package/package.json +2 -2
@@ -4,11 +4,12 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFile
4
4
  import { join } from "node:path";
5
5
  import { CACHE_DIR_NAME } from "../cache-dir.js";
6
6
  import { buildPlanConfirmationToken, sortJsonValue } from "../services/pipelines/confirmation.js";
7
- import { PipelinePlanSchema, planPipeline, } from "../services/pipelines/planning.js";
7
+ import { PipelinePlanSchema, planPipeline, extractCtes, parseSqlSourceRefs, parseSqlSelectItems, getWorkspaceNodeTypeInventory, escapeRegExp, } from "../services/pipelines/planning.js";
8
+ import { selectPipelineNodeType, } from "../services/pipelines/node-type-selection.js";
8
9
  import { createPipelineFromPlan, } from "../services/pipelines/execution.js";
9
10
  import { NodeConfigInputSchema } from "../schemas/node-payloads.js";
10
11
  import { buildJsonToolResponse, getToolOutputSchema, handleToolError, READ_ONLY_ANNOTATIONS, WRITE_ANNOTATIONS, validatePathSegment, } from "../coalesce/types.js";
11
- import { isPlainObject } from "../utils.js";
12
+ import { isPlainObject, sanitizeForFilename } from "../utils.js";
12
13
  import { buildPipelinePlanFromIntent, } from "../services/pipelines/intent.js";
13
14
  import { reviewPipeline, } from "../services/pipelines/review.js";
14
15
  export { buildPlanConfirmationToken };
@@ -76,7 +77,7 @@ function findCachedPlanSummary(workspaceID, fingerprint) {
76
77
  const dir = getPlanSummaryDir();
77
78
  if (!existsSync(dir))
78
79
  return null;
79
- const safeID = workspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
80
+ const safeID = sanitizeForFilename(workspaceID);
80
81
  const prefix = `plan-${safeID}-`;
81
82
  const files = readdirSync(dir)
82
83
  .filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
@@ -95,8 +96,7 @@ function writePlanSummary(plan, fingerprint) {
95
96
  if (!isPlainObject(plan))
96
97
  return null;
97
98
  const rawWorkspaceID = typeof plan.workspaceID === "string" ? plan.workspaceID : "unknown";
98
- // Sanitize workspaceID for safe use in filenames
99
- const workspaceID = rawWorkspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
99
+ const workspaceID = sanitizeForFilename(rawWorkspaceID);
100
100
  const selection = isPlainObject(plan.nodeTypeSelection) ? plan.nodeTypeSelection : null;
101
101
  const consideredNodeTypes = Array.isArray(selection?.consideredNodeTypes)
102
102
  ? selection.consideredNodeTypes.filter(isPlainObject)
@@ -161,14 +161,24 @@ function writePlanSummary(plan, fingerprint) {
161
161
  }
162
162
  function cleanupOldPlanFiles(dir, workspaceID, maxToKeep) {
163
163
  try {
164
- const safeID = workspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
164
+ const safeID = sanitizeForFilename(workspaceID);
165
165
  const prefix = `plan-${safeID}-`;
166
166
  const files = readdirSync(dir)
167
167
  .filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
168
168
  .sort()
169
169
  .reverse(); // most recent first (timestamp in filename)
170
170
  for (const file of files.slice(maxToKeep)) {
171
- unlinkSync(join(dir, file));
171
+ try {
172
+ unlinkSync(join(dir, file));
173
+ }
174
+ catch (err) {
175
+ // Ignore ENOENT — another process may have already deleted this file.
176
+ // Log anything else so permission/disk errors don't go unnoticed.
177
+ if (err instanceof Error && "code" in err && err.code === "ENOENT")
178
+ continue;
179
+ const reason = err instanceof Error ? err.message : String(err);
180
+ process.stderr.write(`[coalesce-transform-mcp] plan file delete failed for ${file}: ${reason}\n`);
181
+ }
172
182
  }
173
183
  }
174
184
  catch (error) {
@@ -271,322 +281,512 @@ async function requirePipelineCreationApproval(server, toolName, plan, confirmed
271
281
  }
272
282
  return null;
273
283
  }
274
- export function registerPipelineTools(server, client) {
275
- server.registerTool("plan_pipeline", {
276
- title: "Plan Pipeline",
277
- description: "Plan a Coalesce pipeline by discovering and ranking all available node types from the repo. ALWAYS call this before creating nodes to get the correct node type.\n\nThe planner scans the repo for all committed node type definitions, scores them against your use case, and returns ranked candidates. When available, it also returns a cached `planSummaryUri` MCP resource for the ranked node type summary so you can reuse that guidance throughout the pipeline without calling the planner again.\n\nIMPORTANT — DO NOT WRITE SQL: The `sql` parameter is ONLY for converting SQL that the USER provided (pasted or typed). If you are building a pipeline yourself, provide `goal` + `sourceNodeIDs` instead.\n\nPREREQUISITE: Before calling this tool, use list_workspace_nodes to discover available source/upstream nodes and their IDs in the workspace.\n\nPreferred approach: Provide `goal` AND `sourceNodeIDs`. The planner selects the best node type and scaffolds the pipeline. Without sourceNodeIDs, the planner returns clarification questions.\n\nUser-provided SQL: When a user pastes SQL, pass it in `sql`. The planner parses refs and column projections.\n\nConsult coalesce://context/node-type-corpus for node type patterns and metadata structures.",
278
- inputSchema: z.object({
279
- workspaceID: z.string().describe("The workspace ID"),
280
- goal: z.string().optional().describe("Optional natural-language pipeline goal"),
281
- sql: z.string().optional().describe("The user's EXACT SQL, copied verbatim. It may use raw table names or existing Coalesce {{ ref() }} syntax. Do NOT rewrite between SQL styles or modify the query. If you are building a pipeline yourself, do NOT write SQL — use goal + sourceNodeIDs instead."),
282
- targetName: z.string().optional().describe("Optional target node name override"),
283
- targetNodeType: z
284
- .string()
285
- .optional()
286
- .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
287
- description: z.string().optional().describe("Optional node description"),
288
- configOverrides: NodeConfigInputSchema
289
- .optional()
290
- .describe("Optional config overrides to merge into the planned node body."),
291
- locationName: z.string().optional().describe("Optional target locationName"),
292
- database: z.string().optional().describe("Optional target database"),
293
- schema: z.string().optional().describe("Optional target schema"),
294
- repoPath: z
295
- .string()
296
- .optional()
297
- .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
298
- sourceNodeIDs: z
299
- .array(z.string())
300
- .optional()
301
- .describe("Optional upstream node IDs when planning from a non-SQL goal."),
302
- }),
303
- outputSchema: getToolOutputSchema("plan_pipeline"),
304
- annotations: READ_ONLY_ANNOTATIONS,
305
- }, async (params) => {
306
- try {
307
- const result = await planPipeline(client, params);
308
- // Build fingerprint from the actual ranked node-type output used in the summary.
309
- const selection = isPlainObject(result.nodeTypeSelection) ? result.nodeTypeSelection : null;
310
- const fingerprint = buildPlanFingerprint(params.workspaceID, selection, Array.isArray(result.supportedNodeTypes)
311
- ? result.supportedNodeTypes.filter((value) => typeof value === "string")
312
- : [], {
313
- goal: params.goal,
314
- sql: params.sql,
315
- sourceNodeIDs: params.sourceNodeIDs,
316
- targetNodeType: params.targetNodeType,
317
- });
318
- // Check for a cached plan with the same fingerprint
319
- const cached = findCachedPlanSummary(params.workspaceID, fingerprint);
320
- const summaryPath = cached?.path ?? writePlanSummary(result, fingerprint);
321
- // Extract the recommended nodeType and put it at the top level
322
- // so the agent can't miss it.
323
- const selectedNodeType = typeof selection?.selectedNodeType === "string"
324
- ? selection.selectedNodeType
325
- : null;
326
- const selectedDisplayName = typeof selection?.selectedDisplayName === "string"
327
- ? selection.selectedDisplayName
328
- : null;
329
- const response = summaryPath
330
- ? {
331
- // Put the recommended type FIRST so it's the most visible field
332
- ...(selectedNodeType ? {
333
- USE_THIS_NODE_TYPE: selectedNodeType,
334
- ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
335
- nodeTypeInstruction: `Use nodeType "${selectedNodeType}" when calling create_workspace_node_from_predecessor or create_workspace_node_from_scratch. Do NOT use "Source" or any other type unless the plan explicitly recommends it.`,
336
- } : {}),
337
- ...result,
338
- planSummaryUri: summaryPath,
339
- planCached: !!cached,
340
- instruction: cached
341
- ? `Cached node type rankings found at planSummaryUri (ranking fingerprint unchanged). Reference this resource for all subsequent node creations — no need to call plan_pipeline again unless repo-backed ranking inputs or workspace node types change enough to alter the planner's ranking.`
342
- : `Node type rankings saved to planSummaryUri. Reference this resource for all subsequent node creations in this pipeline. The cache auto-invalidates when repo-backed ranking inputs or workspace node types change enough to alter the planner's ranking.`,
284
+ export function definePipelineTools(server, client) {
285
+ return [
286
+ [
287
+ "plan_pipeline",
288
+ {
289
+ title: "Plan Pipeline",
290
+ description: "Plan a Coalesce pipeline by discovering and ranking all available node types from the repo. ALWAYS call this before creating nodes to get the correct node type.\n\nThe planner scans the repo for all committed node type definitions, scores them against your use case, and returns ranked candidates. When available, it also returns a cached `planSummaryUri` MCP resource for the ranked node type summary so you can reuse that guidance throughout the pipeline without calling the planner again.\n\nIMPORTANT — DO NOT WRITE SQL: The `sql` parameter is ONLY for converting SQL that the USER provided (pasted or typed). If you are building a pipeline yourself, provide `goal` + `sourceNodeIDs` instead.\n\nPREREQUISITE: Before calling this tool, use list_workspace_nodes to discover available source/upstream nodes and their IDs in the workspace.\n\nPreferred approach: Provide `goal` AND `sourceNodeIDs`. The planner selects the best node type and scaffolds the pipeline. Without sourceNodeIDs, the planner returns clarification questions.\n\nUser-provided SQL: When a user pastes SQL, pass it in `sql`. The planner parses refs and column projections.\n\nConsult coalesce://context/node-type-corpus for node type patterns and metadata structures.",
291
+ inputSchema: z.object({
292
+ workspaceID: z.string().describe("The workspace ID"),
293
+ goal: z.string().optional().describe("Optional natural-language pipeline goal"),
294
+ sql: z.string().optional().describe("The user's EXACT SQL, copied verbatim. It may use raw table names or existing Coalesce {{ ref() }} syntax. Do NOT rewrite between SQL styles or modify the query. If you are building a pipeline yourself, do NOT write SQL — use goal + sourceNodeIDs instead."),
295
+ targetName: z.string().optional().describe("Optional target node name override"),
296
+ targetNodeType: z
297
+ .string()
298
+ .optional()
299
+ .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
300
+ description: z.string().optional().describe("Optional node description"),
301
+ configOverrides: NodeConfigInputSchema
302
+ .optional()
303
+ .describe("Optional config overrides to merge into the planned node body."),
304
+ locationName: z.string().optional().describe("Optional target locationName"),
305
+ database: z.string().optional().describe("Optional target database"),
306
+ schema: z.string().optional().describe("Optional target schema"),
307
+ repoPath: z
308
+ .string()
309
+ .optional()
310
+ .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
311
+ sourceNodeIDs: z
312
+ .array(z.string())
313
+ .optional()
314
+ .describe("Optional upstream node IDs when planning from a non-SQL goal."),
315
+ }),
316
+ outputSchema: getToolOutputSchema("plan_pipeline"),
317
+ annotations: READ_ONLY_ANNOTATIONS,
318
+ },
319
+ async (params) => {
320
+ try {
321
+ const result = await planPipeline(client, params);
322
+ // Build fingerprint from the actual ranked node-type output used in the summary.
323
+ const selection = isPlainObject(result.nodeTypeSelection) ? result.nodeTypeSelection : null;
324
+ const fingerprint = buildPlanFingerprint(params.workspaceID, selection, Array.isArray(result.supportedNodeTypes)
325
+ ? result.supportedNodeTypes.filter((value) => typeof value === "string")
326
+ : [], {
327
+ goal: params.goal,
328
+ sql: params.sql,
329
+ sourceNodeIDs: params.sourceNodeIDs,
330
+ targetNodeType: params.targetNodeType,
331
+ });
332
+ // Check for a cached plan with the same fingerprint
333
+ const cached = findCachedPlanSummary(params.workspaceID, fingerprint);
334
+ const summaryPath = cached?.path ?? writePlanSummary(result, fingerprint);
335
+ // Extract the recommended nodeType and put it at the top level
336
+ // so the agent can't miss it.
337
+ const selectedNodeType = typeof selection?.selectedNodeType === "string"
338
+ ? selection.selectedNodeType
339
+ : null;
340
+ const selectedDisplayName = typeof selection?.selectedDisplayName === "string"
341
+ ? selection.selectedDisplayName
342
+ : null;
343
+ const response = summaryPath
344
+ ? {
345
+ // Put the recommended type FIRST so it's the most visible field
346
+ ...(selectedNodeType ? {
347
+ USE_THIS_NODE_TYPE: selectedNodeType,
348
+ ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
349
+ nodeTypeInstruction: `Use nodeType "${selectedNodeType}" when calling create_workspace_node_from_predecessor or create_workspace_node_from_scratch. Do NOT use "Source" or any other type unless the plan explicitly recommends it.`,
350
+ } : {}),
351
+ ...result,
352
+ planSummaryUri: summaryPath,
353
+ planCached: !!cached,
354
+ instruction: cached
355
+ ? `Cached node type rankings found at planSummaryUri (ranking fingerprint unchanged). Reference this resource for all subsequent node creations — no need to call plan_pipeline again unless repo-backed ranking inputs or workspace node types change enough to alter the planner's ranking.`
356
+ : `Node type rankings saved to planSummaryUri. Reference this resource for all subsequent node creations in this pipeline. The cache auto-invalidates when repo-backed ranking inputs or workspace node types change enough to alter the planner's ranking.`,
357
+ }
358
+ : {
359
+ ...(selectedNodeType ? {
360
+ USE_THIS_NODE_TYPE: selectedNodeType,
361
+ ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
362
+ nodeTypeInstruction: `Use nodeType "${selectedNodeType}" when calling create_workspace_node_from_predecessor or create_workspace_node_from_scratch. Do NOT use "Source" or any other type unless the plan explicitly recommends it.`,
363
+ } : {}),
364
+ ...result,
365
+ };
366
+ return buildJsonToolResponse("plan_pipeline", response);
343
367
  }
344
- : {
345
- ...(selectedNodeType ? {
346
- USE_THIS_NODE_TYPE: selectedNodeType,
347
- ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
348
- nodeTypeInstruction: `Use nodeType "${selectedNodeType}" when calling create_workspace_node_from_predecessor or create_workspace_node_from_scratch. Do NOT use "Source" or any other type unless the plan explicitly recommends it.`,
349
- } : {}),
350
- ...result,
351
- };
352
- return buildJsonToolResponse("plan_pipeline", response);
353
- }
354
- catch (error) {
355
- return handleToolError(error);
356
- }
357
- });
358
- server.registerTool("create_pipeline_from_plan", {
359
- title: "Create Pipeline from Plan",
360
- description: "Create a Coalesce pipeline from a previously approved plan. Pass the exact plan object returned by plan_pipeline. Projection-capable node types execute by creating predecessor-based nodes first and then persisting the final full node body via set_workspace_node.\n\nArgs:\n - workspaceID (string, required): The workspace ID\n - plan (object, required): The exact plan object returned by plan_pipeline\n - confirmed (boolean, optional): Set to true after user approves the plan. Must be paired with confirmationToken\n - confirmationToken (string, optional): Token from prior STOP_AND_CONFIRM response. Required when confirmed=true\n - dryRun (boolean, optional): When true, validate without creating nodes\n\nReturns:\n { created: boolean, nodes?: CreatedNode[], warnings?: string[] }",
361
- inputSchema: z.object({
362
- workspaceID: z.string().describe("The workspace ID"),
363
- plan: PipelinePlanSchema.describe("The plan object returned by plan_pipeline."),
364
- confirmed: z
365
- .boolean()
366
- .optional()
367
- .describe("Set to true only after presenting the plan to the user and receiving explicit approval. Must be paired with the confirmationToken returned by the prior STOP_AND_CONFIRM response."),
368
- confirmationToken: z
369
- .string()
370
- .optional()
371
- .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true to prove the plan was presented to the user."),
372
- dryRun: z
373
- .boolean()
374
- .optional()
375
- .describe("When true, validate the plan and return it without creating any nodes."),
376
- }),
377
- outputSchema: getToolOutputSchema("create_pipeline_from_plan"),
378
- annotations: WRITE_ANNOTATIONS,
379
- }, async (params) => {
380
- try {
381
- if (!params.dryRun) {
382
- const approvalResponse = await requirePipelineCreationApproval(server, "create_pipeline_from_plan", params.plan, params.confirmed, params.confirmationToken, { plan: params.plan });
383
- if (approvalResponse) {
384
- return approvalResponse;
368
+ catch (error) {
369
+ return handleToolError(error);
385
370
  }
386
371
  }
387
- const result = await createPipelineFromPlan(client, params);
388
- const response = buildJsonToolResponse("create_pipeline_from_plan", result);
389
- if (isPlainObject(result) && result.isError) {
390
- return { ...response, isError: true };
391
- }
392
- return response;
393
- }
394
- catch (error) {
395
- return handleToolError(error);
396
- }
397
- });
398
- server.registerTool("create_pipeline_from_sql", {
399
- title: "Create Pipeline from SQL",
400
- description: "Plan and create a Coalesce pipeline from user-provided SQL. Pass the user's EXACT SQL unchanged. The SQL may use raw table names or already contain Coalesce {{ ref() }} syntax if that is what the user provided. Do NOT rewrite between styles or otherwise modify the query. The planner resolves workspace sources automatically and generates a Coalesce-compatible joinCondition for the final node.\n\nIf you are building a pipeline yourself, use declarative tools directly: create_workspace_node_from_predecessor → convert_join_to_aggregation → replace_workspace_node_columns.\n\nThis tool validates candidate node types against currently observed workspace nodes. If a selected type is not observed, the plan will include a warning asking the user to confirm installation in Coalesce.\n\nConsult coalesce://context/node-type-corpus for node type patterns and metadata structures.",
401
- inputSchema: z.object({
402
- workspaceID: z.string().describe("The workspace ID"),
403
- sql: z.string().describe("The user's EXACT SQL, copied verbatim. It may use raw table names or existing Coalesce {{ ref() }} syntax. Do NOT rewrite between SQL styles or modify it in any way. Pass it exactly as the user provided it."),
404
- goal: z.string().optional().describe("Optional business goal or context for the SQL"),
405
- targetName: z.string().optional().describe("Optional target node name override"),
406
- targetNodeType: z
407
- .string()
408
- .optional()
409
- .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
410
- description: z.string().optional().describe("Optional node description"),
411
- configOverrides: NodeConfigInputSchema
412
- .optional()
413
- .describe("Optional config overrides to merge into the final node body."),
414
- locationName: z.string().optional().describe("Optional target locationName"),
415
- database: z.string().optional().describe("Optional target database"),
416
- schema: z.string().optional().describe("Optional target schema"),
417
- repoPath: z
418
- .string()
419
- .optional()
420
- .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
421
- confirmed: z
422
- .boolean()
423
- .optional()
424
- .describe("Set to true only after presenting the ready plan to the user and receiving explicit approval. Must be paired with the confirmationToken returned by the prior STOP_AND_CONFIRM response."),
425
- confirmationToken: z
426
- .string()
427
- .optional()
428
- .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true to prove the plan was presented to the user."),
429
- dryRun: z
430
- .boolean()
431
- .optional()
432
- .describe("When true, return the generated plan without creating nodes."),
433
- }),
434
- outputSchema: getToolOutputSchema("create_pipeline_from_sql"),
435
- annotations: WRITE_ANNOTATIONS,
436
- }, async (params) => {
437
- try {
438
- const plan = await planPipeline(client, params);
439
- if (params.dryRun || plan.status !== "ready") {
440
- return buildJsonToolResponse("create_pipeline_from_sql", {
441
- created: false,
442
- ...(params.dryRun ? { dryRun: true } : {}),
443
- plan,
444
- ...(plan.status !== "ready"
445
- ? {
446
- warning: "SQL was planned but still needs clarification before creation. Review openQuestions and warnings. Present the plan to the user and wait for approval.",
372
+ ],
373
+ [
374
+ "create_pipeline_from_plan",
375
+ {
376
+ title: "Create Pipeline from Plan",
377
+ description: "Create a Coalesce pipeline from a previously approved plan. Pass the exact plan object returned by plan_pipeline. Projection-capable node types execute by creating predecessor-based nodes first and then persisting the final full node body via set_workspace_node.\n\nArgs:\n - workspaceID (string, required): The workspace ID\n - plan (object, required): The exact plan object returned by plan_pipeline\n - confirmed (boolean, optional): Set to true after user approves the plan. Must be paired with confirmationToken\n - confirmationToken (string, optional): Token from prior STOP_AND_CONFIRM response. Required when confirmed=true\n - dryRun (boolean, optional): When true, validate without creating nodes\n\nReturns:\n { created: boolean, nodes?: CreatedNode[], warnings?: string[] }",
378
+ inputSchema: z.object({
379
+ workspaceID: z.string().describe("The workspace ID"),
380
+ plan: PipelinePlanSchema.describe("The plan object returned by plan_pipeline."),
381
+ confirmed: z
382
+ .boolean()
383
+ .optional()
384
+ .describe("Set to true only after presenting the plan to the user and receiving explicit approval. Must be paired with the confirmationToken returned by the prior STOP_AND_CONFIRM response."),
385
+ confirmationToken: z
386
+ .string()
387
+ .optional()
388
+ .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true to prove the plan was presented to the user."),
389
+ dryRun: z
390
+ .boolean()
391
+ .optional()
392
+ .describe("When true, validate the plan and return it without creating any nodes."),
393
+ }),
394
+ outputSchema: getToolOutputSchema("create_pipeline_from_plan"),
395
+ annotations: WRITE_ANNOTATIONS,
396
+ },
397
+ async (params) => {
398
+ try {
399
+ if (!params.dryRun) {
400
+ const approvalResponse = await requirePipelineCreationApproval(server, "create_pipeline_from_plan", params.plan, params.confirmed, params.confirmationToken, { plan: params.plan });
401
+ if (approvalResponse) {
402
+ return approvalResponse;
447
403
  }
448
- : {}),
449
- });
404
+ }
405
+ const result = await createPipelineFromPlan(client, params);
406
+ const response = buildJsonToolResponse("create_pipeline_from_plan", result);
407
+ if (isPlainObject(result) && result.isError) {
408
+ return { ...response, isError: true };
409
+ }
410
+ return response;
411
+ }
412
+ catch (error) {
413
+ return handleToolError(error);
414
+ }
450
415
  }
451
- const approvalResponse = await requirePipelineCreationApproval(server, "create_pipeline_from_sql", plan, params.confirmed, params.confirmationToken, { plan });
452
- if (approvalResponse) {
453
- return approvalResponse;
416
+ ],
417
+ [
418
+ "create_pipeline_from_sql",
419
+ {
420
+ title: "Create Pipeline from SQL",
421
+ description: "Plan and create a Coalesce pipeline from user-provided SQL. Pass the user's EXACT SQL unchanged. The SQL may use raw table names or already contain Coalesce {{ ref() }} syntax if that is what the user provided. Do NOT rewrite between styles or otherwise modify the query. The planner resolves workspace sources automatically and generates a Coalesce-compatible joinCondition for the final node.\n\nIf you are building a pipeline yourself, use declarative tools directly: create_workspace_node_from_predecessor → convert_join_to_aggregation → replace_workspace_node_columns.\n\nThis tool validates candidate node types against currently observed workspace nodes. If a selected type is not observed, the plan will include a warning asking the user to confirm installation in Coalesce.\n\nConsult coalesce://context/node-type-corpus for node type patterns and metadata structures.",
422
+ inputSchema: z.object({
423
+ workspaceID: z.string().describe("The workspace ID"),
424
+ sql: z.string().describe("The user's EXACT SQL, copied verbatim. It may use raw table names or existing Coalesce {{ ref() }} syntax. Do NOT rewrite between SQL styles or modify it in any way. Pass it exactly as the user provided it."),
425
+ goal: z.string().optional().describe("Optional business goal or context for the SQL"),
426
+ targetName: z.string().optional().describe("Optional target node name override"),
427
+ targetNodeType: z
428
+ .string()
429
+ .optional()
430
+ .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
431
+ description: z.string().optional().describe("Optional node description"),
432
+ configOverrides: NodeConfigInputSchema
433
+ .optional()
434
+ .describe("Optional config overrides to merge into the final node body."),
435
+ locationName: z.string().optional().describe("Optional target locationName"),
436
+ database: z.string().optional().describe("Optional target database"),
437
+ schema: z.string().optional().describe("Optional target schema"),
438
+ repoPath: z
439
+ .string()
440
+ .optional()
441
+ .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
442
+ confirmed: z
443
+ .boolean()
444
+ .optional()
445
+ .describe("Set to true only after presenting the ready plan to the user and receiving explicit approval. Must be paired with the confirmationToken returned by the prior STOP_AND_CONFIRM response."),
446
+ confirmationToken: z
447
+ .string()
448
+ .optional()
449
+ .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true to prove the plan was presented to the user."),
450
+ dryRun: z
451
+ .boolean()
452
+ .optional()
453
+ .describe("When true, return the generated plan without creating nodes."),
454
+ }),
455
+ outputSchema: getToolOutputSchema("create_pipeline_from_sql"),
456
+ annotations: WRITE_ANNOTATIONS,
457
+ },
458
+ async (params) => {
459
+ try {
460
+ const plan = await planPipeline(client, params);
461
+ if (params.dryRun || plan.status !== "ready") {
462
+ return buildJsonToolResponse("create_pipeline_from_sql", {
463
+ created: false,
464
+ ...(params.dryRun ? { dryRun: true } : {}),
465
+ plan,
466
+ ...(plan.status !== "ready"
467
+ ? {
468
+ warning: "SQL was planned but still needs clarification before creation. Review openQuestions and warnings. Present the plan to the user and wait for approval.",
469
+ }
470
+ : {}),
471
+ });
472
+ }
473
+ const approvalResponse = await requirePipelineCreationApproval(server, "create_pipeline_from_sql", plan, params.confirmed, params.confirmationToken, { plan });
474
+ if (approvalResponse) {
475
+ return approvalResponse;
476
+ }
477
+ const execution = await createPipelineFromPlan(client, {
478
+ workspaceID: params.workspaceID,
479
+ plan,
480
+ });
481
+ const result = {
482
+ plan,
483
+ ...(isPlainObject(execution) ? execution : { execution }),
484
+ };
485
+ const response = buildJsonToolResponse("create_pipeline_from_sql", result);
486
+ if (isPlainObject(execution) && execution.isError) {
487
+ return { ...response, isError: true };
488
+ }
489
+ return response;
490
+ }
491
+ catch (error) {
492
+ return handleToolError(error);
493
+ }
454
494
  }
455
- const execution = await createPipelineFromPlan(client, {
456
- workspaceID: params.workspaceID,
457
- plan,
458
- });
459
- const result = {
460
- plan,
461
- ...(isPlainObject(execution) ? execution : { execution }),
462
- };
463
- const response = buildJsonToolResponse("create_pipeline_from_sql", result);
464
- if (isPlainObject(execution) && execution.isError) {
465
- return { ...response, isError: true };
495
+ ],
496
+ [
497
+ "build_pipeline_from_intent",
498
+ {
499
+ title: "Build Pipeline from Intent",
500
+ description: "Build a Coalesce pipeline from a natural language description. Describe what you want in plain English and this tool resolves workspace nodes, selects node types, and creates the pipeline nodes.\n\nExamples:\n- \"combine customers and orders by customer_id, aggregate total revenue by region\"\n- \"stage the raw payments table\"\n- \"join products with inventory on product_id\"\n\nThe tool parses the intent, fuzzy-matches entity names to existing workspace nodes, and selects appropriate node types. When confirmed, it creates the pipeline nodes directly. Alternatively, set dryRun=true to get the plan without creating nodes, then pass it to create_pipeline_from_plan.\n\nIf entity names cannot be resolved or the intent is ambiguous, the tool returns clarification questions instead of a plan.",
501
+ inputSchema: z.object({
502
+ workspaceID: z.string().describe("The workspace ID"),
503
+ intent: z
504
+ .string()
505
+ .describe("Natural language description of the pipeline to build. Mention table/node names, join keys, aggregations, and filters."),
506
+ targetName: z.string().optional().describe("Optional target node name override"),
507
+ targetNodeType: z
508
+ .string()
509
+ .optional()
510
+ .describe("Optional node type override. When omitted, the planner selects the best type automatically."),
511
+ repoPath: z
512
+ .string()
513
+ .optional()
514
+ .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
515
+ locationName: z.string().optional().describe("Optional target locationName"),
516
+ database: z.string().optional().describe("Optional target database"),
517
+ schema: z.string().optional().describe("Optional target schema"),
518
+ confirmed: z
519
+ .boolean()
520
+ .optional()
521
+ .describe("Set to true only after presenting the plan to the user and receiving explicit approval."),
522
+ confirmationToken: z
523
+ .string()
524
+ .optional()
525
+ .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true."),
526
+ dryRun: z
527
+ .boolean()
528
+ .optional()
529
+ .describe("When true, return the generated plan without creating nodes."),
530
+ }),
531
+ outputSchema: getToolOutputSchema("build_pipeline_from_intent"),
532
+ annotations: WRITE_ANNOTATIONS,
533
+ },
534
+ async (params) => {
535
+ try {
536
+ const result = await buildPipelinePlanFromIntent(client, {
537
+ workspaceID: params.workspaceID,
538
+ intent: params.intent,
539
+ targetName: params.targetName,
540
+ targetNodeType: params.targetNodeType,
541
+ repoPath: params.repoPath,
542
+ locationName: params.locationName,
543
+ database: params.database,
544
+ schema: params.schema,
545
+ });
546
+ if (result.status !== "ready" ||
547
+ !result.plan ||
548
+ params.dryRun) {
549
+ return buildJsonToolResponse("build_pipeline_from_intent", {
550
+ created: false,
551
+ ...(params.dryRun ? { dryRun: true } : {}),
552
+ ...result,
553
+ });
554
+ }
555
+ // Plan is ready — require confirmation before creating
556
+ const approvalResponse = await requirePipelineCreationApproval(server, "build_pipeline_from_intent", result.plan, params.confirmed, params.confirmationToken, { ...result });
557
+ if (approvalResponse) {
558
+ return approvalResponse;
559
+ }
560
+ // Confirmed — execute the plan
561
+ const execution = await createPipelineFromPlan(client, {
562
+ workspaceID: params.workspaceID,
563
+ plan: result.plan,
564
+ });
565
+ const response = {
566
+ ...result,
567
+ ...(isPlainObject(execution) ? execution : { execution }),
568
+ };
569
+ const toolResponse = buildJsonToolResponse("build_pipeline_from_intent", response);
570
+ if (isPlainObject(execution) && execution.isError) {
571
+ return { ...toolResponse, isError: true };
572
+ }
573
+ return toolResponse;
574
+ }
575
+ catch (error) {
576
+ return handleToolError(error);
577
+ }
466
578
  }
467
- return response;
468
- }
469
- catch (error) {
470
- return handleToolError(error);
471
- }
472
- });
473
- server.registerTool("build_pipeline_from_intent", {
474
- title: "Build Pipeline from Intent",
475
- description: "Build a Coalesce pipeline from a natural language description. Describe what you want in plain English and this tool resolves workspace nodes, selects node types, and creates the pipeline nodes.\n\nExamples:\n- \"combine customers and orders by customer_id, aggregate total revenue by region\"\n- \"stage the raw payments table\"\n- \"join products with inventory on product_id\"\n\nThe tool parses the intent, fuzzy-matches entity names to existing workspace nodes, and selects appropriate node types. When confirmed, it creates the pipeline nodes directly. Alternatively, set dryRun=true to get the plan without creating nodes, then pass it to create_pipeline_from_plan.\n\nIf entity names cannot be resolved or the intent is ambiguous, the tool returns clarification questions instead of a plan.",
476
- inputSchema: z.object({
477
- workspaceID: z.string().describe("The workspace ID"),
478
- intent: z
479
- .string()
480
- .describe("Natural language description of the pipeline to build. Mention table/node names, join keys, aggregations, and filters."),
481
- targetName: z.string().optional().describe("Optional target node name override"),
482
- targetNodeType: z
483
- .string()
484
- .optional()
485
- .describe("Optional node type override. When omitted, the planner selects the best type automatically."),
486
- repoPath: z
487
- .string()
488
- .optional()
489
- .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
490
- locationName: z.string().optional().describe("Optional target locationName"),
491
- database: z.string().optional().describe("Optional target database"),
492
- schema: z.string().optional().describe("Optional target schema"),
493
- confirmed: z
494
- .boolean()
495
- .optional()
496
- .describe("Set to true only after presenting the plan to the user and receiving explicit approval."),
497
- confirmationToken: z
498
- .string()
499
- .optional()
500
- .describe("The token returned in the STOP_AND_CONFIRM response. Required when confirmed=true."),
501
- dryRun: z
502
- .boolean()
503
- .optional()
504
- .describe("When true, return the generated plan without creating nodes."),
505
- }),
506
- outputSchema: getToolOutputSchema("build_pipeline_from_intent"),
507
- annotations: WRITE_ANNOTATIONS,
508
- }, async (params) => {
509
- try {
510
- const result = await buildPipelinePlanFromIntent(client, {
511
- workspaceID: params.workspaceID,
512
- intent: params.intent,
513
- targetName: params.targetName,
514
- targetNodeType: params.targetNodeType,
515
- repoPath: params.repoPath,
516
- locationName: params.locationName,
517
- database: params.database,
518
- schema: params.schema,
519
- });
520
- if (result.status !== "ready" ||
521
- !result.plan ||
522
- params.dryRun) {
523
- return buildJsonToolResponse("build_pipeline_from_intent", {
524
- created: false,
525
- ...(params.dryRun ? { dryRun: true } : {}),
526
- ...result,
527
- });
579
+ ],
580
+ [
581
+ "review_pipeline",
582
+ {
583
+ title: "Review Pipeline",
584
+ description: "Analyze an existing pipeline in a Coalesce workspace and suggest improvements. " +
585
+ "Walks the node DAG, inspects column transforms, join conditions, node types, naming conventions, " +
586
+ "and layer architecture to identify issues and optimization opportunities.\n\n" +
587
+ "Returns findings sorted by severity (critical warning suggestion) with actionable fix suggestions.\n\n" +
588
+ "Checks for:\n" +
589
+ "- Redundant passthrough nodes (no transforms added)\n" +
590
+ "- Missing join conditions (multi-predecessor nodes without FROM/JOIN)\n" +
591
+ "- Layer violations (skipping staging/intermediate layers)\n" +
592
+ "- Node type mismatches (View used for joins, Dimension in staging layer)\n" +
593
+ "- Orphan nodes (disconnected from the pipeline)\n" +
594
+ "- Deep chains (8+ nodes deep)\n" +
595
+ "- High fan-out risk (10+ downstream dependents)\n" +
596
+ "- Naming inconsistencies\n" +
597
+ "- Unused columns (>50% not referenced downstream)\n\n" +
598
+ "Use nodeIDs to scope the review to a specific pipeline section (e.g., from a subgraph).",
599
+ inputSchema: z.object({
600
+ workspaceID: z.string().describe("The workspace ID to review"),
601
+ nodeIDs: z
602
+ .array(z.string())
603
+ .optional()
604
+ .describe("Optional list of node IDs to scope the review. If omitted, reviews the entire workspace (up to 50 nodes in detail)."),
605
+ }),
606
+ outputSchema: getToolOutputSchema("review_pipeline"),
607
+ annotations: READ_ONLY_ANNOTATIONS,
608
+ },
609
+ async (params) => {
610
+ try {
611
+ const result = await reviewPipeline(client, {
612
+ workspaceID: validatePathSegment(params.workspaceID, "workspaceID"),
613
+ nodeIDs: params.nodeIDs,
614
+ });
615
+ return buildJsonToolResponse("review_pipeline", result);
616
+ }
617
+ catch (error) {
618
+ return handleToolError(error);
619
+ }
528
620
  }
529
- // Plan is ready — require confirmation before creating
530
- const approvalResponse = await requirePipelineCreationApproval(server, "build_pipeline_from_intent", result.plan, params.confirmed, params.confirmationToken, { ...result });
531
- if (approvalResponse) {
532
- return approvalResponse;
621
+ ],
622
+ [
623
+ "parse_sql_structure",
624
+ {
625
+ title: "Parse SQL Structure",
626
+ description: "Parse a SQL statement into its structural components without touching the workspace. " +
627
+ "Returns CTEs (if any), source table references (FROM/JOIN), and projected columns (SELECT list). " +
628
+ "Use this as the first step in a multi-step pipeline creation workflow to inspect " +
629
+ "the SQL decomposition before choosing node types or building a plan.\n\n" +
630
+ "For CTE-based SQL, each CTE is returned with its columns (with transform detection), " +
631
+ "WHERE filters, source tables, structural flags (hasJoin, hasGroupBy), and inter-CTE dependency references.\n\n" +
632
+ "For non-CTE SQL, returns the parsed source refs and SELECT items with column/expression classification.\n\n" +
633
+ "This tool is pure parsing — no workspace reads, no node type selection, no mutations.",
634
+ inputSchema: z.object({
635
+ sql: z.string().describe("The SQL statement to parse. Pass it exactly as provided — do not rewrite or modify it."),
636
+ }),
637
+ outputSchema: getToolOutputSchema("parse_sql_structure"),
638
+ annotations: READ_ONLY_ANNOTATIONS,
639
+ },
640
+ async (params) => {
641
+ try {
642
+ const sql = params.sql.trim();
643
+ if (sql.length === 0) {
644
+ return {
645
+ ...buildJsonToolResponse("parse_sql_structure", {
646
+ error: "SQL parameter must not be empty.",
647
+ }),
648
+ isError: true,
649
+ };
650
+ }
651
+ const ctes = extractCtes(sql);
652
+ if (ctes.length > 0) {
653
+ // CTE-based SQL — return the decomposed CTEs plus the final SELECT
654
+ const cteNames = ctes.map((cte) => cte.name);
655
+ return buildJsonToolResponse("parse_sql_structure", {
656
+ hasCtes: true,
657
+ cteCount: ctes.length,
658
+ ctes: ctes.map((cte) => ({
659
+ name: cte.name,
660
+ sourceTable: cte.sourceTable,
661
+ hasJoin: cte.hasJoin,
662
+ hasGroupBy: cte.hasGroupBy,
663
+ whereClause: cte.whereClause,
664
+ columnCount: cte.columns.length,
665
+ columns: cte.columns.map((col) => ({
666
+ outputName: col.outputName,
667
+ expression: col.expression,
668
+ isTransform: col.isTransform,
669
+ })),
670
+ transformCount: cte.columns.filter((col) => col.isTransform).length,
671
+ passthroughCount: cte.columns.filter((col) => !col.isTransform).length,
672
+ dependsOnCtes: cteNames.filter((name) => name !== cte.name && new RegExp(`\\b${escapeRegExp(name)}\\b`, "iu").test(cte.body)),
673
+ })),
674
+ guidance: "Each CTE should become a separate Coalesce node — Coalesce does not support CTEs. " +
675
+ "Use select_pipeline_node_type for each CTE to determine the best node type based on its pattern " +
676
+ "(staging vs join vs aggregation). Then use plan_pipeline or create_pipeline_from_plan to build the pipeline.",
677
+ });
678
+ }
679
+ // Non-CTE SQL — parse source refs and SELECT items
680
+ const sourceResult = parseSqlSourceRefs(sql);
681
+ const parseResult = parseSqlSelectItems(sql, sourceResult.refs);
682
+ return buildJsonToolResponse("parse_sql_structure", {
683
+ hasCtes: false,
684
+ sourceRefs: parseResult.refs.map((ref) => ({
685
+ locationName: ref.locationName,
686
+ nodeName: ref.nodeName,
687
+ alias: ref.alias,
688
+ sourceStyle: ref.sourceStyle,
689
+ })),
690
+ sourceCount: parseResult.refs.length,
691
+ hasJoin: parseResult.refs.length > 1,
692
+ selectItems: parseResult.selectItems.map((item) => ({
693
+ expression: item.expression,
694
+ outputName: item.outputName,
695
+ sourceNodeAlias: item.sourceNodeAlias,
696
+ sourceColumnName: item.sourceColumnName,
697
+ kind: item.kind,
698
+ supported: item.supported,
699
+ ...(item.reason ? { reason: item.reason } : {}),
700
+ })),
701
+ selectItemCount: parseResult.selectItems.length,
702
+ supportedSelectCount: parseResult.selectItems.filter((item) => item.supported).length,
703
+ unsupportedSelectCount: parseResult.selectItems.filter((item) => !item.supported).length,
704
+ warnings: parseResult.warnings,
705
+ guidance: "Use select_pipeline_node_type with the structural hints from this result " +
706
+ "(sourceCount, hasJoin) to determine the best node type. " +
707
+ "Then use plan_pipeline or create_pipeline_from_plan to build the pipeline.",
708
+ });
709
+ }
710
+ catch (error) {
711
+ return handleToolError(error);
712
+ }
533
713
  }
534
- // Confirmed — execute the plan
535
- const execution = await createPipelineFromPlan(client, {
536
- workspaceID: params.workspaceID,
537
- plan: result.plan,
538
- });
539
- const response = {
540
- ...result,
541
- ...(isPlainObject(execution) ? execution : { execution }),
542
- };
543
- const toolResponse = buildJsonToolResponse("build_pipeline_from_intent", response);
544
- if (isPlainObject(execution) && execution.isError) {
545
- return { ...toolResponse, isError: true };
714
+ ],
715
+ [
716
+ "select_pipeline_node_type",
717
+ {
718
+ title: "Select Pipeline Node Type",
719
+ description: "Rank and select the best Coalesce node type for a specific pipeline step. " +
720
+ "When repoPath or COALESCE_REPO_PATH is configured, scans the local repo for committed node type definitions. " +
721
+ "Otherwise, falls back to workspace-observed node types. Scores candidates against the provided context " +
722
+ "and returns ranked candidates with confidence levels.\n\n" +
723
+ "Use this after parse_sql_structure to select node types for each structural piece " +
724
+ "(e.g., each CTE, or the main query). Provide structural hints like sourceCount, hasJoin, hasGroupBy " +
725
+ "for more accurate selection.\n\n" +
726
+ "The tool runs the full deliberative selection loop: score all candidates, challenge the top pick " +
727
+ "against the intent corpus, disqualify if challenged, re-rank, and challenge again (2 rounds max).\n\n" +
728
+ "Returns the selected node type, confidence level, full ranking with scores, and any warnings. " +
729
+ "Use the selectedNodeType in subsequent plan_pipeline, create_workspace_node_from_predecessor, or create_workspace_node_from_scratch calls.\n\n" +
730
+ "This tool reads workspace node type inventory for ranking but does not mutate anything.",
731
+ inputSchema: z.object({
732
+ workspaceID: z.string().describe("The workspace ID — used to fetch observed node types for ranking context."),
733
+ goal: z.string().optional().describe("Natural-language description of what this pipeline step does. Be specific — " +
734
+ "'staging layer for raw customer data' is better than 'stage'. " +
735
+ "Include the transform pattern (join, aggregate, filter, rename, etc.)."),
736
+ targetName: z.string().optional().describe("Optional target node name — used for naming-signal scoring."),
737
+ sql: z.string().optional().describe("Optional SQL for this step — used for structural analysis during scoring."),
738
+ sourceCount: z.number().describe("Number of source/predecessor nodes feeding into this step."),
739
+ hasJoin: z.boolean().optional().describe("Does this step involve a JOIN? Prevents view-family selection and provides structural context for ranking."),
740
+ hasGroupBy: z.boolean().optional().describe("Does this step involve a GROUP BY? Influences selection toward aggregation-capable types."),
741
+ hasBusinessKeys: z.boolean().optional().describe("Are business keys explicitly defined? Influences dimensional/data-vault type selection."),
742
+ targetNodeType: z.string().optional().describe("Optional explicit node type override — bypasses ranking and validates this specific type."),
743
+ repoPath: z.string().optional().describe("Optional local committed Coalesce repo path. Falls back to COALESCE_REPO_PATH when omitted."),
744
+ }),
745
+ outputSchema: getToolOutputSchema("select_pipeline_node_type"),
746
+ annotations: READ_ONLY_ANNOTATIONS,
747
+ },
748
+ async (params) => {
749
+ try {
750
+ const inventory = await getWorkspaceNodeTypeInventory(client, validatePathSegment(params.workspaceID, "workspaceID"));
751
+ const result = selectPipelineNodeType({
752
+ explicitNodeType: params.targetNodeType,
753
+ goal: params.goal,
754
+ targetName: params.targetName,
755
+ sql: params.sql,
756
+ sourceCount: params.sourceCount,
757
+ hasJoin: params.hasJoin,
758
+ hasGroupBy: params.hasGroupBy,
759
+ hasBusinessKeys: params.hasBusinessKeys,
760
+ workspaceNodeTypes: inventory.nodeTypes,
761
+ workspaceNodeTypeCounts: inventory.counts,
762
+ repoPath: params.repoPath,
763
+ });
764
+ const candidate = result.selectedCandidate;
765
+ const inventoryDegraded = inventory.nodeTypes.length === 0 && inventory.warnings.length > 0;
766
+ const response = {
767
+ ...(candidate ? {
768
+ selectedNodeType: candidate.nodeType,
769
+ selectedDisplayName: candidate.displayName,
770
+ selectedFamily: candidate.family,
771
+ autoExecutable: candidate.autoExecutable,
772
+ ...(candidate.semanticSignals.length > 0 ? { semanticSignals: candidate.semanticSignals } : {}),
773
+ ...(candidate.missingDefaultFields.length > 0 ? { missingDefaultFields: candidate.missingDefaultFields } : {}),
774
+ ...(candidate.templateWarnings.length > 0 ? { templateWarnings: candidate.templateWarnings } : {}),
775
+ } : {
776
+ selectedNodeType: null,
777
+ warning: "No suitable node type found. Review the ranked candidates or specify an explicit targetNodeType.",
778
+ }),
779
+ ...(inventoryDegraded ? { selectionDegraded: true } : {}),
780
+ selection: result.selection,
781
+ warnings: [...result.warnings, ...inventory.warnings],
782
+ };
783
+ return buildJsonToolResponse("select_pipeline_node_type", response);
784
+ }
785
+ catch (error) {
786
+ return handleToolError(error);
787
+ }
546
788
  }
547
- return toolResponse;
548
- }
549
- catch (error) {
550
- return handleToolError(error);
551
- }
552
- });
553
- server.registerTool("review_pipeline", {
554
- title: "Review Pipeline",
555
- description: "Analyze an existing pipeline in a Coalesce workspace and suggest improvements. " +
556
- "Walks the node DAG, inspects column transforms, join conditions, node types, naming conventions, " +
557
- "and layer architecture to identify issues and optimization opportunities.\n\n" +
558
- "Returns findings sorted by severity (critical → warning → suggestion) with actionable fix suggestions.\n\n" +
559
- "Checks for:\n" +
560
- "- Redundant passthrough nodes (no transforms added)\n" +
561
- "- Missing join conditions (multi-predecessor nodes without FROM/JOIN)\n" +
562
- "- Layer violations (skipping staging/intermediate layers)\n" +
563
- "- Node type mismatches (View used for joins, Dimension in staging layer)\n" +
564
- "- Orphan nodes (disconnected from the pipeline)\n" +
565
- "- Deep chains (8+ nodes deep)\n" +
566
- "- High fan-out risk (10+ downstream dependents)\n" +
567
- "- Naming inconsistencies\n" +
568
- "- Unused columns (>50% not referenced downstream)\n\n" +
569
- "Use nodeIDs to scope the review to a specific pipeline section (e.g., from a subgraph).",
570
- inputSchema: z.object({
571
- workspaceID: z.string().describe("The workspace ID to review"),
572
- nodeIDs: z
573
- .array(z.string())
574
- .optional()
575
- .describe("Optional list of node IDs to scope the review. If omitted, reviews the entire workspace (up to 50 nodes in detail)."),
576
- }),
577
- outputSchema: getToolOutputSchema("review_pipeline"),
578
- annotations: READ_ONLY_ANNOTATIONS,
579
- }, async (params) => {
580
- try {
581
- const result = await reviewPipeline(client, {
582
- workspaceID: validatePathSegment(params.workspaceID, "workspaceID"),
583
- nodeIDs: params.nodeIDs,
584
- });
585
- return buildJsonToolResponse("review_pipeline", result);
586
- }
587
- catch (error) {
588
- return handleToolError(error);
589
- }
590
- });
789
+ ],
790
+ ];
591
791
  }
592
792
  //# sourceMappingURL=pipelines.js.map