coalesce-transform-mcp 0.1.0

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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -0
  3. package/dist/cache-dir.d.ts +26 -0
  4. package/dist/cache-dir.js +106 -0
  5. package/dist/client.d.ts +25 -0
  6. package/dist/client.js +212 -0
  7. package/dist/coalesce/api/environments.d.ts +20 -0
  8. package/dist/coalesce/api/environments.js +15 -0
  9. package/dist/coalesce/api/git-accounts.d.ts +21 -0
  10. package/dist/coalesce/api/git-accounts.js +21 -0
  11. package/dist/coalesce/api/jobs.d.ts +25 -0
  12. package/dist/coalesce/api/jobs.js +21 -0
  13. package/dist/coalesce/api/nodes.d.ts +29 -0
  14. package/dist/coalesce/api/nodes.js +33 -0
  15. package/dist/coalesce/api/projects.d.ts +22 -0
  16. package/dist/coalesce/api/projects.js +25 -0
  17. package/dist/coalesce/api/runs.d.ts +19 -0
  18. package/dist/coalesce/api/runs.js +34 -0
  19. package/dist/coalesce/api/subgraphs.d.ts +20 -0
  20. package/dist/coalesce/api/subgraphs.js +17 -0
  21. package/dist/coalesce/api/users.d.ts +30 -0
  22. package/dist/coalesce/api/users.js +31 -0
  23. package/dist/coalesce/types.d.ts +298 -0
  24. package/dist/coalesce/types.js +746 -0
  25. package/dist/generated/.gitkeep +0 -0
  26. package/dist/generated/node-type-corpus.json +42656 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +10 -0
  29. package/dist/mcp/cache.d.ts +3 -0
  30. package/dist/mcp/cache.js +137 -0
  31. package/dist/mcp/environments.d.ts +3 -0
  32. package/dist/mcp/environments.js +61 -0
  33. package/dist/mcp/git-accounts.d.ts +3 -0
  34. package/dist/mcp/git-accounts.js +70 -0
  35. package/dist/mcp/jobs.d.ts +3 -0
  36. package/dist/mcp/jobs.js +77 -0
  37. package/dist/mcp/node-type-corpus.d.ts +3 -0
  38. package/dist/mcp/node-type-corpus.js +173 -0
  39. package/dist/mcp/nodes.d.ts +3 -0
  40. package/dist/mcp/nodes.js +341 -0
  41. package/dist/mcp/pipelines.d.ts +3 -0
  42. package/dist/mcp/pipelines.js +342 -0
  43. package/dist/mcp/projects.d.ts +3 -0
  44. package/dist/mcp/projects.js +70 -0
  45. package/dist/mcp/repo-node-types.d.ts +135 -0
  46. package/dist/mcp/repo-node-types.js +387 -0
  47. package/dist/mcp/runs.d.ts +3 -0
  48. package/dist/mcp/runs.js +92 -0
  49. package/dist/mcp/subgraphs.d.ts +3 -0
  50. package/dist/mcp/subgraphs.js +60 -0
  51. package/dist/mcp/users.d.ts +3 -0
  52. package/dist/mcp/users.js +107 -0
  53. package/dist/prompts/index.d.ts +2 -0
  54. package/dist/prompts/index.js +58 -0
  55. package/dist/resources/context/aggregation-patterns.md +145 -0
  56. package/dist/resources/context/data-engineering-principles.md +183 -0
  57. package/dist/resources/context/hydrated-metadata.md +92 -0
  58. package/dist/resources/context/id-discovery.md +64 -0
  59. package/dist/resources/context/intelligent-node-configuration.md +162 -0
  60. package/dist/resources/context/node-creation-decision-tree.md +156 -0
  61. package/dist/resources/context/node-operations.md +316 -0
  62. package/dist/resources/context/node-payloads.md +114 -0
  63. package/dist/resources/context/node-type-corpus.md +166 -0
  64. package/dist/resources/context/node-type-selection-guide.md +96 -0
  65. package/dist/resources/context/overview.md +135 -0
  66. package/dist/resources/context/pipeline-workflows.md +355 -0
  67. package/dist/resources/context/run-operations.md +55 -0
  68. package/dist/resources/context/sql-bigquery.md +41 -0
  69. package/dist/resources/context/sql-databricks.md +40 -0
  70. package/dist/resources/context/sql-platform-selection.md +70 -0
  71. package/dist/resources/context/sql-snowflake.md +43 -0
  72. package/dist/resources/context/storage-mappings.md +49 -0
  73. package/dist/resources/context/tool-usage.md +98 -0
  74. package/dist/resources/index.d.ts +5 -0
  75. package/dist/resources/index.js +254 -0
  76. package/dist/schemas/node-payloads.d.ts +5019 -0
  77. package/dist/schemas/node-payloads.js +147 -0
  78. package/dist/server.d.ts +7 -0
  79. package/dist/server.js +63 -0
  80. package/dist/services/cache/snapshots.d.ts +108 -0
  81. package/dist/services/cache/snapshots.js +275 -0
  82. package/dist/services/config/context-analyzer.d.ts +14 -0
  83. package/dist/services/config/context-analyzer.js +76 -0
  84. package/dist/services/config/field-classifier.d.ts +23 -0
  85. package/dist/services/config/field-classifier.js +47 -0
  86. package/dist/services/config/intelligent.d.ts +55 -0
  87. package/dist/services/config/intelligent.js +306 -0
  88. package/dist/services/config/rules.d.ts +6 -0
  89. package/dist/services/config/rules.js +44 -0
  90. package/dist/services/config/schema-resolver.d.ts +18 -0
  91. package/dist/services/config/schema-resolver.js +80 -0
  92. package/dist/services/corpus/loader.d.ts +56 -0
  93. package/dist/services/corpus/loader.js +25 -0
  94. package/dist/services/corpus/search.d.ts +49 -0
  95. package/dist/services/corpus/search.js +69 -0
  96. package/dist/services/corpus/templates.d.ts +4 -0
  97. package/dist/services/corpus/templates.js +11 -0
  98. package/dist/services/pipelines/execution.d.ts +20 -0
  99. package/dist/services/pipelines/execution.js +290 -0
  100. package/dist/services/pipelines/node-type-intent.d.ts +96 -0
  101. package/dist/services/pipelines/node-type-intent.js +356 -0
  102. package/dist/services/pipelines/node-type-selection.d.ts +66 -0
  103. package/dist/services/pipelines/node-type-selection.js +758 -0
  104. package/dist/services/pipelines/planning.d.ts +543 -0
  105. package/dist/services/pipelines/planning.js +1839 -0
  106. package/dist/services/policies/sql-override.d.ts +7 -0
  107. package/dist/services/policies/sql-override.js +109 -0
  108. package/dist/services/repo/operations.d.ts +6 -0
  109. package/dist/services/repo/operations.js +10 -0
  110. package/dist/services/repo/parser.d.ts +70 -0
  111. package/dist/services/repo/parser.js +365 -0
  112. package/dist/services/repo/path.d.ts +2 -0
  113. package/dist/services/repo/path.js +58 -0
  114. package/dist/services/templates/nodes.d.ts +50 -0
  115. package/dist/services/templates/nodes.js +336 -0
  116. package/dist/services/workspace/analysis.d.ts +56 -0
  117. package/dist/services/workspace/analysis.js +151 -0
  118. package/dist/services/workspace/mutations.d.ts +150 -0
  119. package/dist/services/workspace/mutations.js +1718 -0
  120. package/dist/utils.d.ts +5 -0
  121. package/dist/utils.js +7 -0
  122. package/dist/workflows/get-environment-overview.d.ts +9 -0
  123. package/dist/workflows/get-environment-overview.js +23 -0
  124. package/dist/workflows/get-run-details.d.ts +10 -0
  125. package/dist/workflows/get-run-details.js +28 -0
  126. package/dist/workflows/progress.d.ts +20 -0
  127. package/dist/workflows/progress.js +54 -0
  128. package/dist/workflows/retry-and-wait.d.ts +13 -0
  129. package/dist/workflows/retry-and-wait.js +139 -0
  130. package/dist/workflows/run-and-wait.d.ts +13 -0
  131. package/dist/workflows/run-and-wait.js +141 -0
  132. package/dist/workflows/run-status.d.ts +10 -0
  133. package/dist/workflows/run-status.js +27 -0
  134. package/package.json +34 -0
@@ -0,0 +1,342 @@
1
+ import { z } from "zod";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { CACHE_DIR_NAME } from "../cache-dir.js";
6
+ import { PipelinePlanSchema, planPipeline, } from "../services/pipelines/planning.js";
7
+ import { createPipelineFromPlan, createPipelineFromSql, } from "../services/pipelines/execution.js";
8
+ import { NodeConfigInputSchema } from "../schemas/node-payloads.js";
9
+ import { buildJsonToolResponse, handleToolError, READ_ONLY_ANNOTATIONS, WRITE_ANNOTATIONS, } from "../coalesce/types.js";
10
+ import { isPlainObject } from "../utils.js";
11
+ const REWRITTEN_SQL_ERROR_MESSAGE = "The sql parameter contains {{ ref() }} syntax, which means you rewrote the user's SQL. " +
12
+ "Pass the user's EXACT SQL unchanged — the planner resolves source references automatically. " +
13
+ "Do NOT replace table names with {{ ref() }}.";
14
+ function buildPlanFingerprint(workspaceID, repoPath, workspaceNodeTypes, requestInputs) {
15
+ const input = [
16
+ `workspace:${workspaceID}`,
17
+ `repo:${repoPath ?? "none"}`,
18
+ `types:${[...workspaceNodeTypes].sort().join(",")}`,
19
+ `goal:${requestInputs?.goal ?? ""}`,
20
+ `sql:${requestInputs?.sql ?? ""}`,
21
+ `sources:${requestInputs?.sourceNodeIDs ? [...requestInputs.sourceNodeIDs].sort().join(",") : ""}`,
22
+ `targetType:${requestInputs?.targetNodeType ?? ""}`,
23
+ ].join("|");
24
+ return createHash("sha256").update(input).digest("hex").slice(0, 16);
25
+ }
26
+ function getPlanSummaryDir() {
27
+ return join(process.cwd(), CACHE_DIR_NAME, "plans");
28
+ }
29
+ function findCachedPlanSummary(workspaceID, fingerprint) {
30
+ const dir = getPlanSummaryDir();
31
+ if (!existsSync(dir))
32
+ return null;
33
+ const safeID = workspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
34
+ const prefix = `plan-${safeID}-`;
35
+ const files = readdirSync(dir)
36
+ .filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
37
+ .sort()
38
+ .reverse(); // most recent first
39
+ for (const file of files) {
40
+ const filePath = join(dir, file);
41
+ const content = readFileSync(filePath, "utf8");
42
+ if (content.includes(`Fingerprint: ${fingerprint}`)) {
43
+ return { path: filePath, content };
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function writePlanSummary(plan, fingerprint) {
49
+ if (!isPlainObject(plan))
50
+ return null;
51
+ const rawWorkspaceID = typeof plan.workspaceID === "string" ? plan.workspaceID : "unknown";
52
+ // Sanitize workspaceID for safe use in filenames
53
+ const workspaceID = rawWorkspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
54
+ const selection = isPlainObject(plan.nodeTypeSelection) ? plan.nodeTypeSelection : null;
55
+ const consideredNodeTypes = Array.isArray(selection?.consideredNodeTypes)
56
+ ? selection.consideredNodeTypes.filter(isPlainObject)
57
+ : [];
58
+ const supportedNodeTypes = Array.isArray(plan.supportedNodeTypes) ? plan.supportedNodeTypes : [];
59
+ if (consideredNodeTypes.length === 0 && supportedNodeTypes.length === 0) {
60
+ return null;
61
+ }
62
+ const lines = [
63
+ `# Pipeline Plan — Node Type Reference`,
64
+ ``,
65
+ `Workspace: ${workspaceID}`,
66
+ `Strategy: ${selection?.strategy ?? "unknown"}`,
67
+ `Selected: ${selection?.selectedNodeType ?? "none"}`,
68
+ `Confidence: ${selection?.confidence ?? "unknown"}`,
69
+ `Fingerprint: ${fingerprint}`,
70
+ `Generated: ${new Date().toISOString()}`,
71
+ ``,
72
+ `This file is automatically invalidated when the repo node types or workspace`,
73
+ `node types change. If you install new packages or commit new node type`,
74
+ `definitions, call plan-pipeline again to refresh.`,
75
+ ``,
76
+ `## Ranked Node Types`,
77
+ ``,
78
+ `Use these node types when calling create-workspace-node-from-predecessor.`,
79
+ `Pick the type whose family matches your pipeline layer (stage, dimension, fact, etc.).`,
80
+ ``,
81
+ ];
82
+ for (const candidate of consideredNodeTypes) {
83
+ const nodeType = typeof candidate.nodeType === "string" ? candidate.nodeType : "?";
84
+ const family = typeof candidate.family === "string" ? candidate.family : "unknown";
85
+ const score = typeof candidate.score === "number" ? candidate.score : 0;
86
+ const displayName = typeof candidate.displayName === "string" ? candidate.displayName : null;
87
+ const reasons = Array.isArray(candidate.reasons) ? candidate.reasons.filter((r) => typeof r === "string") : [];
88
+ lines.push(`### ${nodeType}${displayName ? ` (${displayName})` : ""}`);
89
+ lines.push(`- Family: ${family}`);
90
+ lines.push(`- Score: ${score}`);
91
+ if (reasons.length > 0) {
92
+ lines.push(`- Reasons: ${reasons.join("; ")}`);
93
+ }
94
+ lines.push(``);
95
+ }
96
+ if (supportedNodeTypes.length > 0) {
97
+ lines.push(`## Auto-Executable Types`);
98
+ lines.push(``);
99
+ lines.push(`These types support automatic creation via create-workspace-node-from-predecessor:`);
100
+ lines.push(``);
101
+ for (const nodeType of supportedNodeTypes) {
102
+ lines.push(`- ${nodeType}`);
103
+ }
104
+ lines.push(``);
105
+ }
106
+ const dir = getPlanSummaryDir();
107
+ mkdirSync(dir, { recursive: true });
108
+ const fileName = `plan-${workspaceID}-${Date.now()}.md`;
109
+ const filePath = join(dir, fileName);
110
+ writeFileSync(filePath, lines.join("\n"), "utf8");
111
+ // Clean up old plan files — keep only the 10 most recent per workspace
112
+ cleanupOldPlanFiles(dir, workspaceID, 10);
113
+ return filePath;
114
+ }
115
+ function cleanupOldPlanFiles(dir, workspaceID, maxToKeep) {
116
+ try {
117
+ const safeID = workspaceID.replace(/[^a-zA-Z0-9_\-]/g, "_");
118
+ const prefix = `plan-${safeID}-`;
119
+ const files = readdirSync(dir)
120
+ .filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
121
+ .sort()
122
+ .reverse(); // most recent first (timestamp in filename)
123
+ for (const file of files.slice(maxToKeep)) {
124
+ unlinkSync(join(dir, file));
125
+ }
126
+ }
127
+ catch {
128
+ // Best-effort cleanup — don't fail the write
129
+ }
130
+ }
131
+ function buildPlanSummaryForElicitation(plan) {
132
+ const lines = ["Pipeline plan ready. Review the nodes to be created:"];
133
+ lines.push("");
134
+ if (isPlainObject(plan)) {
135
+ // cteNodeSummary is populated for SQL-sourced plans; nodes for goal-based plans
136
+ const cteNodes = Array.isArray(plan.cteNodeSummary) ? plan.cteNodeSummary.filter(isPlainObject) : [];
137
+ const planNodes = Array.isArray(plan.nodes) ? plan.nodes.filter(isPlainObject) : [];
138
+ const nodesToShow = cteNodes.length > 0 ? cteNodes : planNodes;
139
+ if (nodesToShow.length === 0) {
140
+ lines.push(" (No node details available in plan)");
141
+ }
142
+ else {
143
+ for (const node of nodesToShow) {
144
+ const name = typeof node.name === "string" ? node.name : "(unnamed)";
145
+ const nodeType = typeof node.nodeType === "string" ? node.nodeType : "(unknown type)";
146
+ lines.push(` • ${name} [${nodeType}]`);
147
+ }
148
+ }
149
+ const warnings = Array.isArray(plan.warnings)
150
+ ? plan.warnings.filter((w) => typeof w === "string")
151
+ : [];
152
+ if (warnings.length > 0) {
153
+ lines.push("");
154
+ lines.push("Warnings:");
155
+ for (const w of warnings) {
156
+ lines.push(` ⚠ ${w}`);
157
+ }
158
+ }
159
+ }
160
+ lines.push("");
161
+ lines.push("Confirm to proceed with node creation, or cancel to abort.");
162
+ return lines.join("\n");
163
+ }
164
+ export function registerPipelineTools(server, client) {
165
+ server.tool("plan-pipeline", "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.", {
166
+ workspaceID: z.string().describe("The workspace ID"),
167
+ goal: z.string().optional().describe("Optional natural-language pipeline goal"),
168
+ sql: z.string().optional().describe("The user's EXACT SQL, copied verbatim. Do NOT rewrite table names, do NOT add {{ ref() }} syntax, do NOT modify it. Pass it exactly as the user provided it. If you are building a pipeline yourself, do NOT write SQL — use goal + sourceNodeIDs instead."),
169
+ targetName: z.string().optional().describe("Optional target node name override"),
170
+ targetNodeType: z
171
+ .string()
172
+ .optional()
173
+ .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
174
+ description: z.string().optional().describe("Optional node description"),
175
+ configOverrides: NodeConfigInputSchema
176
+ .optional()
177
+ .describe("Optional config overrides to merge into the planned node body."),
178
+ locationName: z.string().optional().describe("Optional target locationName"),
179
+ database: z.string().optional().describe("Optional target database"),
180
+ schema: z.string().optional().describe("Optional target schema"),
181
+ repoPath: z
182
+ .string()
183
+ .optional()
184
+ .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
185
+ sourceNodeIDs: z
186
+ .array(z.string())
187
+ .optional()
188
+ .describe("Optional upstream node IDs when planning from a non-SQL goal."),
189
+ }, READ_ONLY_ANNOTATIONS, async (params) => {
190
+ try {
191
+ // Reject SQL that the agent rewrote with {{ ref() }}
192
+ if (params.sql && /\{\{\s*ref\s*\(/.test(params.sql)) {
193
+ return handleToolError(new Error(REWRITTEN_SQL_ERROR_MESSAGE));
194
+ }
195
+ const result = await planPipeline(client, params);
196
+ // Build fingerprint from workspace + repo + observed types
197
+ const selection = isPlainObject(result.nodeTypeSelection) ? result.nodeTypeSelection : null;
198
+ const workspaceNodeTypes = Array.isArray(selection?.workspaceObservedNodeTypes)
199
+ ? selection.workspaceObservedNodeTypes
200
+ : [];
201
+ const repoPath = typeof selection?.resolvedRepoPath === "string"
202
+ ? selection.resolvedRepoPath
203
+ : null;
204
+ const fingerprint = buildPlanFingerprint(params.workspaceID, repoPath, workspaceNodeTypes, {
205
+ goal: params.goal,
206
+ sql: params.sql,
207
+ sourceNodeIDs: params.sourceNodeIDs,
208
+ targetNodeType: params.targetNodeType,
209
+ });
210
+ // Check for a cached plan with the same fingerprint
211
+ const cached = findCachedPlanSummary(params.workspaceID, fingerprint);
212
+ const summaryPath = cached?.path ?? writePlanSummary(result, fingerprint);
213
+ // Extract the recommended nodeType and put it at the top level
214
+ // so the agent can't miss it.
215
+ const selectedNodeType = typeof selection?.selectedNodeType === "string"
216
+ ? selection.selectedNodeType
217
+ : null;
218
+ const selectedDisplayName = typeof selection?.selectedDisplayName === "string"
219
+ ? selection.selectedDisplayName
220
+ : null;
221
+ const response = summaryPath
222
+ ? {
223
+ // Put the recommended type FIRST so it's the most visible field
224
+ ...(selectedNodeType ? {
225
+ USE_THIS_NODE_TYPE: selectedNodeType,
226
+ ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
227
+ 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.`,
228
+ } : {}),
229
+ ...result,
230
+ planSummaryUri: summaryPath,
231
+ planCached: !!cached,
232
+ instruction: cached
233
+ ? `Cached node type rankings found at planSummaryUri (fingerprint unchanged). Reference this resource for all subsequent node creations — no need to call plan-pipeline again unless you install new packages or commit new node type definitions.`
234
+ : `Node type rankings saved to planSummaryUri. Reference this resource for all subsequent node creations in this pipeline. The cache auto-invalidates when repo or workspace node types change.`,
235
+ }
236
+ : {
237
+ ...(selectedNodeType ? {
238
+ USE_THIS_NODE_TYPE: selectedNodeType,
239
+ ...(selectedDisplayName ? { nodeTypeDisplayName: selectedDisplayName } : {}),
240
+ 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.`,
241
+ } : {}),
242
+ ...result,
243
+ };
244
+ return buildJsonToolResponse("plan-pipeline", response);
245
+ }
246
+ catch (error) {
247
+ return handleToolError(error);
248
+ }
249
+ });
250
+ server.tool("create-pipeline-from-plan", "Create a Coalesce pipeline from a previously approved plan. Projection-capable node types execute by creating predecessor-based nodes first and then persisting the final full node body with set-workspace-node.", {
251
+ workspaceID: z.string().describe("The workspace ID"),
252
+ plan: PipelinePlanSchema.describe("The plan object returned by plan-pipeline."),
253
+ dryRun: z
254
+ .boolean()
255
+ .optional()
256
+ .describe("When true, validate the plan and return it without creating any nodes."),
257
+ }, WRITE_ANNOTATIONS, async (params) => {
258
+ try {
259
+ if (!params.dryRun) {
260
+ const planSummary = buildPlanSummaryForElicitation(params.plan);
261
+ try {
262
+ const elicitation = await server.server.elicitInput({
263
+ message: planSummary,
264
+ requestedSchema: {
265
+ type: "object",
266
+ properties: {
267
+ confirmed: {
268
+ type: "boolean",
269
+ title: "Create these pipeline nodes?",
270
+ description: "Select true to proceed with node creation, false to cancel.",
271
+ },
272
+ },
273
+ required: ["confirmed"],
274
+ },
275
+ });
276
+ if (elicitation.action !== "accept" || elicitation.content?.confirmed !== true) {
277
+ return buildJsonToolResponse("create-pipeline-from-plan", {
278
+ created: false,
279
+ cancelled: true,
280
+ reason: elicitation.action === "accept"
281
+ ? "User declined pipeline creation."
282
+ : `Pipeline creation ${elicitation.action}d by user.`,
283
+ });
284
+ }
285
+ }
286
+ catch (elicitError) {
287
+ // Client does not support elicitation — fall back to STOP_AND_CONFIRM convention
288
+ if (elicitError instanceof Error && elicitError.message.includes("does not support")) {
289
+ return buildJsonToolResponse("create-pipeline-from-plan", {
290
+ created: false,
291
+ STOP_AND_CONFIRM: "STOP. Present the pipeline plan to the user in a table showing each node name and nodeType. Ask for explicit approval BEFORE creating any nodes. Once the user approves, call create-pipeline-from-plan again.",
292
+ plan: params.plan,
293
+ });
294
+ }
295
+ throw elicitError;
296
+ }
297
+ }
298
+ const result = await createPipelineFromPlan(client, params);
299
+ return buildJsonToolResponse("create-pipeline-from-plan", result);
300
+ }
301
+ catch (error) {
302
+ return handleToolError(error);
303
+ }
304
+ });
305
+ server.tool("create-pipeline-from-sql", "Plan and create a Coalesce pipeline from user-provided SQL. Pass the user's EXACT SQL unchanged — do NOT rewrite it, do NOT replace table references with {{ ref() }}, do NOT modify the SQL in any way. The planner handles source resolution automatically.\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.", {
306
+ workspaceID: z.string().describe("The workspace ID"),
307
+ sql: z.string().describe("The user's EXACT SQL, copied verbatim. Do NOT rewrite table names, do NOT add {{ ref() }} syntax, do NOT modify it in any way. Pass it exactly as the user provided it."),
308
+ goal: z.string().optional().describe("Optional business goal or context for the SQL"),
309
+ targetName: z.string().optional().describe("Optional target node name override"),
310
+ targetNodeType: z
311
+ .string()
312
+ .optional()
313
+ .describe("Optional node type override. When omitted, the planner ranks repo-backed and observed workspace node types for the use case."),
314
+ description: z.string().optional().describe("Optional node description"),
315
+ configOverrides: NodeConfigInputSchema
316
+ .optional()
317
+ .describe("Optional config overrides to merge into the final node body."),
318
+ locationName: z.string().optional().describe("Optional target locationName"),
319
+ database: z.string().optional().describe("Optional target database"),
320
+ schema: z.string().optional().describe("Optional target schema"),
321
+ repoPath: z
322
+ .string()
323
+ .optional()
324
+ .describe("Optional local committed Coalesce repo path for repo-first node-type ranking. Falls back to COALESCE_REPO_PATH when omitted."),
325
+ dryRun: z
326
+ .boolean()
327
+ .optional()
328
+ .describe("When true, return the generated plan without creating nodes."),
329
+ }, WRITE_ANNOTATIONS, async (params) => {
330
+ try {
331
+ // Reject SQL that the agent rewrote with {{ ref() }} — the user's original SQL won't contain these
332
+ if (/\{\{\s*ref\s*\(/.test(params.sql)) {
333
+ return handleToolError(new Error(REWRITTEN_SQL_ERROR_MESSAGE));
334
+ }
335
+ const result = await createPipelineFromSql(client, params);
336
+ return buildJsonToolResponse("create-pipeline-from-sql", result);
337
+ }
338
+ catch (error) {
339
+ return handleToolError(error);
340
+ }
341
+ });
342
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CoalesceClient } from "../client.js";
3
+ export declare function registerProjectTools(server: McpServer, client: CoalesceClient): void;
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { listProjects, getProject, createProject, updateProject, deleteProject, } from "../coalesce/api/projects.js";
3
+ import { buildJsonToolResponse, handleToolError, READ_ONLY_ANNOTATIONS, WRITE_ANNOTATIONS, IDEMPOTENT_WRITE_ANNOTATIONS, DESTRUCTIVE_ANNOTATIONS, } from "../coalesce/types.js";
4
+ export function registerProjectTools(server, client) {
5
+ server.tool("list-projects", "List all Coalesce projects. Use includeWorkspaces to get workspace IDs (needed for workspace-node tools).", {
6
+ includeWorkspaces: z.boolean().optional().describe("Include nested workspace data with workspace IDs"),
7
+ includeJobs: z.boolean().optional().describe("Include nested job data for all workspaces"),
8
+ }, READ_ONLY_ANNOTATIONS, async (params) => {
9
+ try {
10
+ const result = await listProjects(client, params);
11
+ return buildJsonToolResponse("list-projects", result);
12
+ }
13
+ catch (error) {
14
+ return handleToolError(error);
15
+ }
16
+ });
17
+ server.tool("get-project", "Get details of a specific Coalesce project. Use includeWorkspaces to get workspace IDs (needed for workspace-node tools).", {
18
+ projectID: z.string().describe("The project ID"),
19
+ includeWorkspaces: z.boolean().optional().describe("Include nested workspace data with workspace IDs"),
20
+ includeJobs: z.boolean().optional().describe("Include nested job data for all workspaces"),
21
+ }, READ_ONLY_ANNOTATIONS, async (params) => {
22
+ try {
23
+ const result = await getProject(client, params);
24
+ return buildJsonToolResponse("get-project", result);
25
+ }
26
+ catch (error) {
27
+ return handleToolError(error);
28
+ }
29
+ });
30
+ server.tool("create-project", "Create a new Coalesce project", {
31
+ body: z
32
+ .record(z.unknown())
33
+ .describe("The project creation request body"),
34
+ }, WRITE_ANNOTATIONS, async (params) => {
35
+ try {
36
+ const result = await createProject(client, params);
37
+ return buildJsonToolResponse("create-project", result);
38
+ }
39
+ catch (error) {
40
+ return handleToolError(error);
41
+ }
42
+ });
43
+ server.tool("update-project", "Update an existing Coalesce project (partial update — only provided fields are changed)", {
44
+ projectID: z.string().describe("The project ID"),
45
+ body: z
46
+ .record(z.unknown())
47
+ .describe("The project update request body (partial — only include fields to change)"),
48
+ includeWorkspaces: z.boolean().optional().describe("Include nested workspace data in the response"),
49
+ includeJobs: z.boolean().optional().describe("Include nested job data in the response"),
50
+ }, IDEMPOTENT_WRITE_ANNOTATIONS, async (params) => {
51
+ try {
52
+ const result = await updateProject(client, params);
53
+ return buildJsonToolResponse("update-project", result);
54
+ }
55
+ catch (error) {
56
+ return handleToolError(error);
57
+ }
58
+ });
59
+ server.tool("delete-project", "Delete a Coalesce project", {
60
+ projectID: z.string().describe("The project ID"),
61
+ }, DESTRUCTIVE_ANNOTATIONS, async (params) => {
62
+ try {
63
+ const result = await deleteProject(client, params);
64
+ return buildJsonToolResponse("delete-project", result);
65
+ }
66
+ catch (error) {
67
+ return handleToolError(error);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,135 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CoalesceClient } from "../client.js";
3
+ export declare function listRepoPackages(params: {
4
+ repoPath?: string;
5
+ }): {
6
+ summary: {
7
+ repoPath: string;
8
+ resolvedRepoPath: string;
9
+ packageCount: number;
10
+ uniquePackageAliasCount: number;
11
+ nodeTypeDefinitionCount: number;
12
+ uniqueNodeTypeIDCount: number;
13
+ nodeCount: number;
14
+ warnings: string[];
15
+ };
16
+ packages: {
17
+ alias: string;
18
+ aliasSource: "name" | "filename";
19
+ packageFilePath: string;
20
+ packageID: string | null;
21
+ releaseID: string | null;
22
+ packageVariables: string | null;
23
+ enabledNodeTypeIDs: string[];
24
+ resolvedDefinitionIDs: string[];
25
+ missingDefinitionIDs: string[];
26
+ ambiguousDefinitionIDs: string[];
27
+ usageByNodeTypeID: Record<string, number>;
28
+ usageCount: number;
29
+ warnings: string[];
30
+ }[];
31
+ };
32
+ export declare function listRepoNodeTypes(params: {
33
+ repoPath?: string;
34
+ packageAlias?: string;
35
+ inUseOnly?: boolean;
36
+ }): {
37
+ summary: {
38
+ warnings: string[];
39
+ packageAlias: string | null;
40
+ inUseOnly: boolean;
41
+ matchedCount: number;
42
+ repoPath: string;
43
+ resolvedRepoPath: string;
44
+ packageCount: number;
45
+ uniquePackageAliasCount: number;
46
+ nodeTypeDefinitionCount: number;
47
+ uniqueNodeTypeIDCount: number;
48
+ nodeCount: number;
49
+ };
50
+ nodeTypes: {
51
+ nodeType: string;
52
+ outerID: string | null;
53
+ displayName: string | null;
54
+ resolutionKind: "direct" | "package";
55
+ packageAlias?: string;
56
+ definitionPath: string;
57
+ createPath: string | null;
58
+ runPath: string | null;
59
+ usageCount: number;
60
+ parseError: string | null;
61
+ warnings: string[];
62
+ }[];
63
+ };
64
+ export declare function getRepoNodeTypeDefinition(params: {
65
+ repoPath?: string;
66
+ nodeType: string;
67
+ }): {
68
+ outerDefinition: import("../services/repo/parser.js").RepoOuterDefinition;
69
+ nodeMetadataSpecYaml: string | null;
70
+ nodeDefinition: Record<string, unknown> | null;
71
+ parseError: string | null;
72
+ filePaths: {
73
+ definitionPath: string;
74
+ createPath: string | null;
75
+ runPath: string | null;
76
+ };
77
+ usageSummary: {
78
+ exactNodeType: string;
79
+ usageCount: number;
80
+ };
81
+ warnings: string[];
82
+ package?: {
83
+ alias: string;
84
+ aliasSource: "name" | "filename";
85
+ packageFilePath: string;
86
+ packageID: string | null;
87
+ releaseID: string | null;
88
+ packageVariables: string | null;
89
+ enabledNodeTypeIDs: string[];
90
+ resolvedDefinitionIDs: string[];
91
+ missingDefinitionIDs: string[];
92
+ ambiguousDefinitionIDs: string[];
93
+ usageByNodeTypeID: Record<string, number>;
94
+ usageCount: number;
95
+ warnings: string[];
96
+ } | undefined;
97
+ repoPath: string;
98
+ resolvedRepoPath: string;
99
+ repoWarnings: string[];
100
+ requestedNodeType: string;
101
+ resolvedNodeType: string;
102
+ resolution: {
103
+ resolutionKind: "package";
104
+ requestedNodeType: string;
105
+ resolvedNodeType: string;
106
+ packageAlias: string;
107
+ packageFilePath: string;
108
+ packageID: string | null;
109
+ releaseID: string | null;
110
+ enabledNodeTypeID: string | null;
111
+ usageCount: number;
112
+ } | {
113
+ resolutionKind: "direct";
114
+ requestedNodeType: string;
115
+ resolvedNodeType: string;
116
+ usageCount: number;
117
+ packageAlias?: undefined;
118
+ packageFilePath?: undefined;
119
+ packageID?: undefined;
120
+ releaseID?: undefined;
121
+ enabledNodeTypeID?: undefined;
122
+ };
123
+ };
124
+ export declare function generateSetWorkspaceNodeTemplate(client: CoalesceClient, params: {
125
+ definition?: Record<string, unknown>;
126
+ repoPath?: string;
127
+ nodeType?: string;
128
+ nodeName?: string;
129
+ locationName?: string;
130
+ database?: string;
131
+ schema?: string;
132
+ workspaceID?: string;
133
+ nodeID?: string;
134
+ }): Promise<Record<string, unknown>>;
135
+ export declare function registerRepoNodeTypeTools(server: McpServer, client: CoalesceClient): void;