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,746 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { buildCacheResourceLink, CACHE_DIR_NAME, } from "../cache-dir.js";
5
+ import { z } from "zod";
6
+ import { CoalesceApiError } from "../client.js";
7
+ const SESSION_START_TIME = new Date();
8
+ // Workspace node body schema — validates known structural fields while allowing
9
+ // node-type-specific extras through. Used by set-workspace-node.
10
+ export const WorkspaceNodeBodySchema = z
11
+ .object({
12
+ name: z.string().optional(),
13
+ description: z.string().optional(),
14
+ nodeType: z.string().optional(),
15
+ database: z.string().optional(),
16
+ schema: z.string().optional(),
17
+ locationName: z.string().optional(),
18
+ storageLocations: z.array(z.unknown()).optional(),
19
+ config: z.record(z.unknown()).optional(),
20
+ metadata: z
21
+ .object({
22
+ columns: z.array(z.unknown()).optional(),
23
+ })
24
+ .passthrough()
25
+ .optional(),
26
+ })
27
+ .passthrough();
28
+ // Pagination params — only used by endpoints that support it
29
+ export const PaginationParams = z.object({
30
+ limit: z.number().optional().describe("Number of results to return"),
31
+ startingFrom: z
32
+ .string()
33
+ .optional()
34
+ .describe("Cursor from previous response's next field"),
35
+ orderBy: z
36
+ .string()
37
+ .optional()
38
+ .describe("Field to sort by (required with startingFrom)"),
39
+ orderByDirection: z
40
+ .enum(["asc", "desc"])
41
+ .optional()
42
+ .describe("Sort direction"),
43
+ });
44
+ // Common annotations
45
+ export const READ_ONLY_ANNOTATIONS = {
46
+ readOnlyHint: true,
47
+ idempotentHint: true,
48
+ destructiveHint: false,
49
+ };
50
+ export const WRITE_ANNOTATIONS = {
51
+ readOnlyHint: false,
52
+ idempotentHint: false,
53
+ destructiveHint: false,
54
+ };
55
+ export const IDEMPOTENT_WRITE_ANNOTATIONS = {
56
+ readOnlyHint: false,
57
+ idempotentHint: true,
58
+ destructiveHint: false,
59
+ };
60
+ export const DESTRUCTIVE_ANNOTATIONS = {
61
+ readOnlyHint: false,
62
+ idempotentHint: false,
63
+ destructiveHint: true,
64
+ };
65
+ const DEFAULT_AUTO_CACHE_MAX_BYTES = 32 * 1024;
66
+ const JSON_TOOL_OUTPUT_SCHEMA_PATCHED = Symbol("jsonToolOutputSchemaPatched");
67
+ const JsonObjectSchema = z.object({}).passthrough();
68
+ const JsonToolErrorSchema = z.object({
69
+ message: z.string(),
70
+ status: z.number().optional(),
71
+ detail: z.unknown().optional(),
72
+ }).passthrough();
73
+ const ListToolOutputSchema = z.object({
74
+ data: z.array(z.unknown()).optional(),
75
+ next: z.string().optional(),
76
+ total: z.number().optional(),
77
+ }).passthrough();
78
+ const EntityToolOutputSchema = z.object({
79
+ id: z.union([z.string(), z.number()]).optional(),
80
+ name: z.string().optional(),
81
+ message: z.string().optional(),
82
+ }).passthrough();
83
+ const WorkspaceNodeMutationOutputSchema = z.object({
84
+ nodeID: z.string().optional(),
85
+ created: z.boolean().optional(),
86
+ warning: z.string().optional(),
87
+ validation: JsonObjectSchema.optional(),
88
+ nextSteps: z.array(z.string()).optional(),
89
+ joinSuggestions: z.array(z.unknown()).optional(),
90
+ configCompletion: JsonObjectSchema.optional(),
91
+ configCompletionSkipped: z.string().optional(),
92
+ nodeTypeValidation: JsonObjectSchema.optional(),
93
+ }).passthrough();
94
+ const WorkspaceAnalysisOutputSchema = z.object({
95
+ workspaceID: z.string(),
96
+ analyzedAt: z.string(),
97
+ nodeCount: z.number(),
98
+ packageAdoption: JsonObjectSchema,
99
+ layerPatterns: JsonObjectSchema,
100
+ methodology: z.string(),
101
+ recommendations: JsonObjectSchema,
102
+ }).passthrough();
103
+ const WorkspaceNodeTypesOutputSchema = z.object({
104
+ workspaceID: z.string(),
105
+ basis: z.literal("observed_nodes"),
106
+ nodeTypes: z.array(z.string()),
107
+ counts: z.record(z.number()),
108
+ total: z.number(),
109
+ }).passthrough();
110
+ const RunSchedulerOutputSchema = z.object({
111
+ runCounter: z.number().optional(),
112
+ runStatus: z.string().optional(),
113
+ message: z.string().optional(),
114
+ }).passthrough();
115
+ const RunDetailsOutputSchema = z.object({
116
+ run: z.unknown(),
117
+ results: z.unknown(),
118
+ resultsError: z.string().optional(),
119
+ }).passthrough();
120
+ const RunWaitOutputSchema = z.object({
121
+ status: z.unknown().optional(),
122
+ results: z.unknown().optional(),
123
+ resultsError: JsonToolErrorSchema.optional(),
124
+ incomplete: z.boolean().optional(),
125
+ timedOut: z.boolean().optional(),
126
+ }).passthrough();
127
+ const EnvironmentOverviewOutputSchema = z.object({
128
+ environment: z.unknown(),
129
+ nodes: z.array(z.unknown()),
130
+ }).passthrough();
131
+ const CacheArtifactOutputSchema = z.object({
132
+ workspaceID: z.string().optional(),
133
+ environmentID: z.string().optional(),
134
+ runType: z.enum(["deploy", "refresh"]).optional(),
135
+ runStatus: z
136
+ .enum(["completed", "failed", "canceled", "running", "waitingToRun"])
137
+ .optional(),
138
+ detail: z.boolean().optional(),
139
+ totalNodes: z.number().optional(),
140
+ totalRuns: z.number().optional(),
141
+ totalUsers: z.number().optional(),
142
+ pageCount: z.number().optional(),
143
+ pageSize: z.number().optional(),
144
+ orderBy: z.string().optional(),
145
+ orderByDirection: z.enum(["asc", "desc"]).optional(),
146
+ fileUri: z.string().optional(),
147
+ metaUri: z.string().optional(),
148
+ cachedAt: z.string().optional(),
149
+ autoCached: z.boolean().optional(),
150
+ resourceUri: z.string().optional(),
151
+ toolName: z.string().optional(),
152
+ message: z.string().optional(),
153
+ sizeBytes: z.number().optional(),
154
+ maxInlineBytes: z.number().optional(),
155
+ }).passthrough();
156
+ const ClearCacheOutputSchema = z.object({
157
+ deleted: z.boolean(),
158
+ fileCount: z.number().optional(),
159
+ totalBytes: z.number().optional(),
160
+ sizeMB: z.string().optional(),
161
+ message: z.string(),
162
+ }).passthrough();
163
+ const RepoPackagesOutputSchema = z.object({
164
+ summary: JsonObjectSchema,
165
+ packages: z.array(JsonObjectSchema),
166
+ }).passthrough();
167
+ const RepoNodeTypesOutputSchema = z.object({
168
+ summary: JsonObjectSchema,
169
+ nodeTypes: z.array(JsonObjectSchema),
170
+ }).passthrough();
171
+ const RepoNodeTypeDefinitionOutputSchema = z.object({
172
+ repoPath: z.string(),
173
+ resolvedRepoPath: z.string(),
174
+ repoWarnings: z.array(z.string()),
175
+ requestedNodeType: z.string(),
176
+ resolvedNodeType: z.string(),
177
+ resolution: JsonObjectSchema,
178
+ outerDefinition: JsonObjectSchema,
179
+ nodeMetadataSpecYaml: z.string().nullable().optional(),
180
+ nodeDefinition: z.unknown(),
181
+ parseError: z.string().nullable().optional(),
182
+ filePaths: JsonObjectSchema,
183
+ usageSummary: JsonObjectSchema,
184
+ warnings: z.array(z.string()),
185
+ }).passthrough();
186
+ const WorkspaceNodeTemplateOutputSchema = z.object({
187
+ warnings: z.array(z.string()).optional(),
188
+ setWorkspaceNodeBodyTemplate: JsonObjectSchema.optional(),
189
+ setWorkspaceNodeBodyTemplateYaml: z.string().optional(),
190
+ nodeDefinition: z.unknown().optional(),
191
+ nodeMetadataSpecYaml: z.string().nullable().optional(),
192
+ comparison: JsonObjectSchema.optional(),
193
+ }).passthrough();
194
+ const CorpusSearchOutputSchema = z.object({
195
+ summary: JsonObjectSchema,
196
+ matchedCount: z.number().optional(),
197
+ returnedCount: z.number().optional(),
198
+ matches: z.array(JsonObjectSchema).optional(),
199
+ totalMatches: z.number().optional(),
200
+ }).passthrough();
201
+ const CorpusVariantOutputSchema = z.object({
202
+ variantKey: z.string().optional(),
203
+ supportStatus: z.string().optional(),
204
+ nodeDefinition: z.unknown().optional(),
205
+ nodeMetadataSpec: z.string().optional(),
206
+ warnings: z.array(z.string()).optional(),
207
+ }).passthrough();
208
+ const PipelinePlanOutputSchema = z.object({
209
+ version: z.number().optional(),
210
+ intent: z.string().optional(),
211
+ status: z.string().optional(),
212
+ workspaceID: z.string().optional(),
213
+ platform: z.string().nullable().optional(),
214
+ goal: z.string().nullable().optional(),
215
+ sql: z.string().nullable().optional(),
216
+ warning: z.string().optional(),
217
+ warnings: z.array(z.string()).optional(),
218
+ assumptions: z.array(z.string()).optional(),
219
+ openQuestions: z.array(z.unknown()).optional(),
220
+ nodes: z.array(z.unknown()).optional(),
221
+ cteNodeSummary: z.array(z.unknown()).optional(),
222
+ supportedNodeTypes: z.array(z.string()).optional(),
223
+ nodeTypeSelection: JsonObjectSchema.optional(),
224
+ STOP_AND_CONFIRM: z.string().optional(),
225
+ USE_THIS_NODE_TYPE: z.string().optional(),
226
+ nodeTypeDisplayName: z.string().optional(),
227
+ nodeTypeInstruction: z.string().optional(),
228
+ planSummaryUri: z.string().optional(),
229
+ planCached: z.boolean().optional(),
230
+ instruction: z.string().optional(),
231
+ }).passthrough();
232
+ const PipelineCreateOutputSchema = z.object({
233
+ created: z.boolean().optional(),
234
+ cancelled: z.boolean().optional(),
235
+ dryRun: z.boolean().optional(),
236
+ STOP_AND_CONFIRM: z.string().optional(),
237
+ reason: z.string().optional(),
238
+ warning: z.string().optional(),
239
+ workspaceID: z.string().optional(),
240
+ nodeCount: z.number().optional(),
241
+ incomplete: z.boolean().optional(),
242
+ failedPlanNodeID: z.string().optional(),
243
+ plan: z.unknown().optional(),
244
+ createdNodes: z.array(z.unknown()).optional(),
245
+ cleanupFailedNodeIDs: z.array(z.string()).optional(),
246
+ cleanupFailures: z.array(z.object({
247
+ nodeID: z.string(),
248
+ message: z.string(),
249
+ status: z.number().optional(),
250
+ detail: z.unknown().optional(),
251
+ }).passthrough()).optional(),
252
+ error: JsonToolErrorSchema.optional(),
253
+ }).passthrough();
254
+ const LIST_TOOL_NAMES = new Set([
255
+ "list-environments",
256
+ "list-projects",
257
+ "list-jobs",
258
+ "list-runs",
259
+ "list-environment-nodes",
260
+ "list-workspace-nodes",
261
+ "list-org-users",
262
+ "list-user-roles",
263
+ "list-git-accounts",
264
+ ]);
265
+ const ENTITY_TOOL_NAMES = new Set([
266
+ "get-environment",
267
+ "create-environment",
268
+ "delete-environment",
269
+ "get-project",
270
+ "create-project",
271
+ "update-project",
272
+ "delete-project",
273
+ "get-job",
274
+ "create-workspace-job",
275
+ "update-workspace-job",
276
+ "delete-workspace-job",
277
+ "get-run",
278
+ "get-run-results",
279
+ "get-environment-node",
280
+ "get-workspace-node",
281
+ "get-user-roles",
282
+ "set-org-role",
283
+ "set-project-role",
284
+ "delete-project-role",
285
+ "set-env-role",
286
+ "delete-env-role",
287
+ "get-git-account",
288
+ "create-git-account",
289
+ "update-git-account",
290
+ "delete-git-account",
291
+ "get-workspace-subgraph",
292
+ "create-workspace-subgraph",
293
+ "update-workspace-subgraph",
294
+ "delete-workspace-subgraph",
295
+ ]);
296
+ const WORKSPACE_NODE_MUTATION_TOOL_NAMES = new Set([
297
+ "create-workspace-node-from-scratch",
298
+ "set-workspace-node",
299
+ "update-workspace-node",
300
+ "replace-workspace-node-columns",
301
+ "convert-join-to-aggregation",
302
+ "apply-join-condition",
303
+ "create-workspace-node-from-predecessor",
304
+ "delete-workspace-node",
305
+ "complete-node-configuration",
306
+ ]);
307
+ const CACHE_TOOL_NAMES = new Set([
308
+ "cache-workspace-nodes",
309
+ "cache-environment-nodes",
310
+ "cache-runs",
311
+ "cache-org-users",
312
+ ]);
313
+ export const JsonToolOutputSchema = JsonObjectSchema.describe("Tool-specific JSON object output. Oversized responses may be replaced with cache metadata including resourceUri.");
314
+ function getToolOutputSchema(toolName) {
315
+ if (LIST_TOOL_NAMES.has(toolName)) {
316
+ return ListToolOutputSchema;
317
+ }
318
+ if (ENTITY_TOOL_NAMES.has(toolName)) {
319
+ return EntityToolOutputSchema;
320
+ }
321
+ if (WORKSPACE_NODE_MUTATION_TOOL_NAMES.has(toolName)) {
322
+ return WorkspaceNodeMutationOutputSchema;
323
+ }
324
+ if (CACHE_TOOL_NAMES.has(toolName)) {
325
+ return CacheArtifactOutputSchema;
326
+ }
327
+ switch (toolName) {
328
+ case "clear_coalesce_transform_mcp_data_cache":
329
+ return ClearCacheOutputSchema;
330
+ case "analyze-workspace-patterns":
331
+ return WorkspaceAnalysisOutputSchema;
332
+ case "list-workspace-node-types":
333
+ return WorkspaceNodeTypesOutputSchema;
334
+ case "run-status":
335
+ case "start-run":
336
+ case "retry-run":
337
+ case "cancel-run":
338
+ return RunSchedulerOutputSchema;
339
+ case "get-run-details":
340
+ return RunDetailsOutputSchema;
341
+ case "run-and-wait":
342
+ case "retry-and-wait":
343
+ return RunWaitOutputSchema;
344
+ case "get-environment-overview":
345
+ return EnvironmentOverviewOutputSchema;
346
+ case "list-repo-packages":
347
+ return RepoPackagesOutputSchema;
348
+ case "list-repo-node-types":
349
+ return RepoNodeTypesOutputSchema;
350
+ case "get-repo-node-type-definition":
351
+ return RepoNodeTypeDefinitionOutputSchema;
352
+ case "generate-set-workspace-node-template":
353
+ case "generate-set-workspace-node-template-from-variant":
354
+ return WorkspaceNodeTemplateOutputSchema;
355
+ case "search-node-type-variants":
356
+ return CorpusSearchOutputSchema;
357
+ case "get-node-type-variant":
358
+ return CorpusVariantOutputSchema;
359
+ case "plan-pipeline":
360
+ return PipelinePlanOutputSchema;
361
+ case "create-pipeline-from-plan":
362
+ case "create-pipeline-from-sql":
363
+ return PipelineCreateOutputSchema;
364
+ default:
365
+ return JsonToolOutputSchema;
366
+ }
367
+ }
368
+ // --- startRun / run-and-wait schemas ---
369
+ export const RunDetailsSchema = z.object({
370
+ environmentID: z.string().describe("The environment being refreshed"),
371
+ includeNodesSelector: z
372
+ .string()
373
+ .optional()
374
+ .describe("Nodes included for an ad-hoc job"),
375
+ excludeNodesSelector: z
376
+ .string()
377
+ .optional()
378
+ .describe("Nodes excluded for an ad-hoc job"),
379
+ jobID: z.string().optional().describe("The ID of a job being run"),
380
+ parallelism: z
381
+ .number()
382
+ .int()
383
+ .optional()
384
+ .describe("Max parallel nodes to run (API default: 16)"),
385
+ forceIgnoreWorkspaceStatus: z
386
+ .boolean()
387
+ .optional()
388
+ .describe("Allow refresh even if last deploy failed (API default: false). Use with caution."),
389
+ });
390
+ export const UserCredentialsSchema = z.object({
391
+ snowflakeUsername: z.string().describe("Snowflake account username"),
392
+ snowflakeKeyPairKey: z
393
+ .string()
394
+ .describe("PEM-encoded private key for Snowflake auth. Use \\n for line breaks in JSON."),
395
+ snowflakeKeyPairPass: z
396
+ .string()
397
+ .optional()
398
+ .describe("Password to decrypt an encrypted private key. Only required when the private key is encrypted."),
399
+ snowflakeWarehouse: z.string().describe("Snowflake compute warehouse"),
400
+ snowflakeRole: z.string().describe("Snowflake user role"),
401
+ });
402
+ export const StartRunParams = z.object({
403
+ runDetails: RunDetailsSchema,
404
+ parameters: z
405
+ .record(z.string())
406
+ .optional()
407
+ .describe("Arbitrary key-value parameters to pass to the run"),
408
+ confirmRunAllNodes: z
409
+ .boolean()
410
+ .optional()
411
+ .describe("Must be set to true when no jobID, includeNodesSelector, or excludeNodesSelector is provided. " +
412
+ "This confirms you intend to run ALL nodes in the environment."),
413
+ });
414
+ // --- rerun / retry-and-wait schemas ---
415
+ export const RerunDetailsSchema = z.object({
416
+ runID: z.string().describe("The run ID to retry"),
417
+ forceIgnoreWorkspaceStatus: z
418
+ .boolean()
419
+ .optional()
420
+ .describe("Allow refresh even if last deploy failed (API default: false). Use with caution."),
421
+ });
422
+ export const RerunParams = z.object({
423
+ runDetails: RerunDetailsSchema,
424
+ parameters: z
425
+ .record(z.string())
426
+ .optional()
427
+ .describe("Arbitrary key-value parameters to pass to the rerun"),
428
+ });
429
+ export function buildRerunBody(params) {
430
+ const userCredentials = getSnowflakeCredentials();
431
+ return {
432
+ runDetails: params.runDetails,
433
+ userCredentials,
434
+ ...(params.parameters ? { parameters: params.parameters } : {}),
435
+ };
436
+ }
437
+ const ALLOWED_PEM_HEADERS = [
438
+ "-----BEGIN PRIVATE KEY-----",
439
+ "-----BEGIN RSA PRIVATE KEY-----",
440
+ "-----BEGIN ENCRYPTED PRIVATE KEY-----",
441
+ ];
442
+ function readKeyPairFile(filePath) {
443
+ if (!existsSync(filePath)) {
444
+ throw new Error("SNOWFLAKE_KEY_PAIR_KEY file not found at the configured path. " +
445
+ "Check that the environment variable points to an existing PEM private key file.");
446
+ }
447
+ const content = readFileSync(filePath, "utf-8").trim();
448
+ const hasValidHeader = ALLOWED_PEM_HEADERS.some((header) => content.includes(header));
449
+ if (!hasValidHeader) {
450
+ throw new Error("SNOWFLAKE_KEY_PAIR_KEY file is not a valid PEM private key. " +
451
+ "Expected a file containing one of: PRIVATE KEY, RSA PRIVATE KEY, or ENCRYPTED PRIVATE KEY.");
452
+ }
453
+ return content;
454
+ }
455
+ export function getSnowflakeCredentials() {
456
+ const snowflakeUsername = process.env.SNOWFLAKE_USERNAME;
457
+ const snowflakeKeyPairKeyRaw = process.env.SNOWFLAKE_KEY_PAIR_KEY;
458
+ const snowflakeKeyPairPass = process.env.SNOWFLAKE_KEY_PAIR_PASS;
459
+ const snowflakeWarehouse = process.env.SNOWFLAKE_WAREHOUSE;
460
+ const snowflakeRole = process.env.SNOWFLAKE_ROLE;
461
+ if (!snowflakeUsername) {
462
+ throw new Error("SNOWFLAKE_USERNAME environment variable is required for Snowflake Key Pair run tools.");
463
+ }
464
+ if (!snowflakeKeyPairKeyRaw) {
465
+ throw new Error("SNOWFLAKE_KEY_PAIR_KEY environment variable is required for Snowflake Key Pair run tools.");
466
+ }
467
+ const snowflakeKeyPairKey = readKeyPairFile(snowflakeKeyPairKeyRaw);
468
+ if (!snowflakeWarehouse) {
469
+ throw new Error("SNOWFLAKE_WAREHOUSE environment variable is required for Snowflake Key Pair run tools.");
470
+ }
471
+ if (!snowflakeRole) {
472
+ throw new Error("SNOWFLAKE_ROLE environment variable is required for Snowflake Key Pair run tools.");
473
+ }
474
+ return {
475
+ snowflakeUsername,
476
+ snowflakeKeyPairKey,
477
+ ...(snowflakeKeyPairPass ? { snowflakeKeyPairPass } : {}),
478
+ snowflakeWarehouse,
479
+ snowflakeRole,
480
+ snowflakeAuthType: "KeyPair",
481
+ };
482
+ }
483
+ const SANITIZED_KEYS = new Set([
484
+ "userCredentials",
485
+ "snowflakeKeyPairKey",
486
+ "snowflakeKeyPairPass",
487
+ ]);
488
+ export function sanitizeResponse(data) {
489
+ if (Array.isArray(data)) {
490
+ return data.map(sanitizeResponse);
491
+ }
492
+ if (data && typeof data === "object") {
493
+ const obj = data;
494
+ const result = {};
495
+ for (const [key, value] of Object.entries(obj)) {
496
+ if (SANITIZED_KEYS.has(key))
497
+ continue;
498
+ result[key] = sanitizeResponse(value);
499
+ }
500
+ return result;
501
+ }
502
+ return data;
503
+ }
504
+ function slugifyFileComponent(value) {
505
+ return value
506
+ .trim()
507
+ .toLowerCase()
508
+ .replace(/[^a-z0-9]+/g, "-")
509
+ .replace(/^-+|-+$/g, "")
510
+ .slice(0, 80);
511
+ }
512
+ function getAutoCacheMaxBytes() {
513
+ const raw = process.env.COALESCE_MCP_AUTO_CACHE_MAX_BYTES;
514
+ if (raw === undefined) {
515
+ return DEFAULT_AUTO_CACHE_MAX_BYTES;
516
+ }
517
+ const parsed = Number.parseInt(raw, 10);
518
+ if (!Number.isFinite(parsed) || parsed < 0) {
519
+ return DEFAULT_AUTO_CACHE_MAX_BYTES;
520
+ }
521
+ return parsed;
522
+ }
523
+ function cleanupStaleAutoCacheFiles(autoCacheDir) {
524
+ try {
525
+ const sessionTimestamp = SESSION_START_TIME.toISOString().replace(/[:.]/g, "-");
526
+ const files = readdirSync(autoCacheDir)
527
+ .filter((f) => f.endsWith(".json"))
528
+ .sort();
529
+ for (const file of files) {
530
+ // Filenames are: {ISO_timestamp}-{tool-name}-{uuid}.json
531
+ // Compare the timestamp prefix against session start
532
+ if (file < sessionTimestamp) {
533
+ try {
534
+ unlinkSync(join(autoCacheDir, file));
535
+ }
536
+ catch {
537
+ // Best-effort — skip files that can't be deleted
538
+ }
539
+ }
540
+ }
541
+ }
542
+ catch {
543
+ // Best-effort cleanup — don't fail the write
544
+ }
545
+ }
546
+ function buildAutoCacheFilePath(toolName, cachedAt, baseDir) {
547
+ const directory = join(baseDir, CACHE_DIR_NAME, "auto-cache");
548
+ mkdirSync(directory, { recursive: true });
549
+ const timestamp = cachedAt.replace(/[:.]/g, "-");
550
+ const safeToolName = slugifyFileComponent(toolName) || "tool-response";
551
+ return join(directory, `${timestamp}-${safeToolName}-${randomUUID()}.json`);
552
+ }
553
+ function isPlainRecord(value) {
554
+ return typeof value === "object" && value !== null && !Array.isArray(value);
555
+ }
556
+ function humanizeFieldName(fieldName) {
557
+ const stripped = fieldName.replace(/(Path|Uri)$/, "");
558
+ const humanized = stripped
559
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
560
+ .replace(/[_-]+/g, " ")
561
+ .trim();
562
+ return humanized.length > 0
563
+ ? humanized.charAt(0).toUpperCase() + humanized.slice(1)
564
+ : "Cached artifact";
565
+ }
566
+ function buildCacheLinkForField(fieldName, filePath, baseDir) {
567
+ return buildCacheResourceLink(filePath, {
568
+ baseDir,
569
+ name: humanizeFieldName(fieldName),
570
+ description: "Read this cached artifact through the MCP resource URI.",
571
+ });
572
+ }
573
+ function externalizeCachePaths(value, baseDir, resourceLinks) {
574
+ if (Array.isArray(value)) {
575
+ return value.map((item) => externalizeCachePaths(item, baseDir, resourceLinks));
576
+ }
577
+ if (!isPlainRecord(value)) {
578
+ return value;
579
+ }
580
+ const output = {};
581
+ for (const [key, child] of Object.entries(value)) {
582
+ if (typeof child === "string") {
583
+ const link = buildCacheLinkForField(key, child, baseDir);
584
+ if (link) {
585
+ const renamedKey = key.endsWith("Path") && !Object.prototype.hasOwnProperty.call(value, `${key.slice(0, -4)}Uri`)
586
+ ? `${key.slice(0, -4)}Uri`
587
+ : key;
588
+ output[renamedKey] = link.uri;
589
+ resourceLinks.set(link.uri, link);
590
+ continue;
591
+ }
592
+ }
593
+ output[key] = externalizeCachePaths(child, baseDir, resourceLinks);
594
+ }
595
+ return output;
596
+ }
597
+ function buildInlineJsonResponse(result, resourceLinks) {
598
+ const text = JSON.stringify(result, null, 2);
599
+ return {
600
+ content: [{ type: "text", text }, ...resourceLinks],
601
+ structuredContent: normalizeStructuredContent(result),
602
+ };
603
+ }
604
+ function normalizeStructuredContent(result) {
605
+ if (isPlainRecord(result)) {
606
+ return result;
607
+ }
608
+ return { value: result ?? null };
609
+ }
610
+ export function buildJsonToolResponse(toolName, result, options = {}) {
611
+ const baseDir = options.baseDir ?? process.cwd();
612
+ const resourceLinks = new Map();
613
+ const externalizedResult = externalizeCachePaths(result, baseDir, resourceLinks);
614
+ const text = JSON.stringify(externalizedResult, null, 2);
615
+ const maxInlineBytes = options.maxInlineBytes ?? getAutoCacheMaxBytes();
616
+ const sizeBytes = Buffer.byteLength(text, "utf8");
617
+ if (sizeBytes <= maxInlineBytes) {
618
+ return buildInlineJsonResponse(externalizedResult, [...resourceLinks.values()]);
619
+ }
620
+ const cachedAt = new Date().toISOString();
621
+ const filePath = buildAutoCacheFilePath(toolName, cachedAt, baseDir);
622
+ try {
623
+ writeFileSync(filePath, `${text}\n`, "utf8");
624
+ cleanupStaleAutoCacheFiles(dirname(filePath));
625
+ }
626
+ catch {
627
+ return buildInlineJsonResponse(externalizedResult, [...resourceLinks.values()]);
628
+ }
629
+ const cacheLink = buildCacheResourceLink(filePath, {
630
+ baseDir,
631
+ name: `${toolName} cached response`,
632
+ description: "Full tool response cached on the MCP server because it exceeded the inline response threshold.",
633
+ }) ?? null;
634
+ const metadata = {
635
+ autoCached: true,
636
+ toolName,
637
+ cachedAt,
638
+ sizeBytes,
639
+ maxInlineBytes,
640
+ ...(cacheLink ? { resourceUri: cacheLink.uri } : {}),
641
+ message: "Full response was automatically cached to disk because it exceeded the inline response threshold.",
642
+ };
643
+ // Omit structuredContent for auto-cached responses: the cache metadata shape
644
+ // does not match the tool's declared output schema, so including it would
645
+ // violate the MCP output contract. Clients still receive the cache metadata
646
+ // as text content and can follow the resourceUri to fetch the full payload.
647
+ return {
648
+ content: [
649
+ {
650
+ type: "text",
651
+ text: JSON.stringify(metadata, null, 2),
652
+ },
653
+ ...(cacheLink ? [cacheLink] : []),
654
+ ],
655
+ };
656
+ }
657
+ export function validatePathSegment(value, name) {
658
+ if (value.length === 0) {
659
+ throw new Error(`Invalid ${name}: must not be empty`);
660
+ }
661
+ if (/[\/\\]|\.\./.test(value)) {
662
+ throw new Error(`Invalid ${name}: must not contain path separators or '..'`);
663
+ }
664
+ return value;
665
+ }
666
+ export function handleToolError(error) {
667
+ const normalized = error instanceof CoalesceApiError
668
+ ? {
669
+ message: error.message,
670
+ status: error.status,
671
+ ...(error.detail !== undefined ? { detail: error.detail } : {}),
672
+ }
673
+ : error instanceof Error
674
+ ? { message: error.message }
675
+ : { message: String(error) };
676
+ return {
677
+ isError: true,
678
+ content: [{ type: "text", text: normalized.message }],
679
+ structuredContent: {
680
+ error: normalized,
681
+ },
682
+ };
683
+ }
684
+ export function ensureJsonToolOutputSchemas(server) {
685
+ const looksLikeToolAnnotations = (value) => typeof value === "object" &&
686
+ value !== null &&
687
+ ("readOnlyHint" in value ||
688
+ "idempotentHint" in value ||
689
+ "destructiveHint" in value);
690
+ const patchedServer = server;
691
+ if (patchedServer[JSON_TOOL_OUTPUT_SCHEMA_PATCHED]) {
692
+ return;
693
+ }
694
+ const originalRegisterTool = patchedServer.registerTool.bind(server);
695
+ patchedServer.tool = ((...args) => {
696
+ const [name, ...rest] = args;
697
+ if (typeof name !== "string") {
698
+ throw new Error("Tool name must be a string");
699
+ }
700
+ const callback = rest.at(-1);
701
+ if (typeof callback !== "function") {
702
+ throw new Error("Tool callback must be a function");
703
+ }
704
+ const configArgs = [...rest.slice(0, -1)];
705
+ const config = {
706
+ outputSchema: getToolOutputSchema(name),
707
+ };
708
+ if (typeof configArgs[0] === "string") {
709
+ config.description = configArgs.shift();
710
+ }
711
+ if (configArgs.length === 1) {
712
+ if (looksLikeToolAnnotations(configArgs[0])) {
713
+ config.annotations = configArgs[0];
714
+ }
715
+ else {
716
+ config.inputSchema = configArgs[0];
717
+ }
718
+ }
719
+ if (configArgs.length === 2) {
720
+ config.inputSchema = configArgs[0];
721
+ config.annotations = configArgs[1];
722
+ }
723
+ if (configArgs.length > 2) {
724
+ throw new Error("Unsupported tool registration signature while applying output schemas");
725
+ }
726
+ return originalRegisterTool(name, config, callback);
727
+ });
728
+ patchedServer[JSON_TOOL_OUTPUT_SCHEMA_PATCHED] = true;
729
+ }
730
+ export function buildStartRunBody(params) {
731
+ const { runDetails } = params;
732
+ const hasNodeScope = runDetails.jobID ||
733
+ runDetails.includeNodesSelector ||
734
+ runDetails.excludeNodesSelector;
735
+ if (!hasNodeScope && !params.confirmRunAllNodes) {
736
+ throw new Error("No jobID, includeNodesSelector, or excludeNodesSelector was provided. " +
737
+ "This will run ALL nodes in the environment. " +
738
+ "Set confirmRunAllNodes to true to confirm this is intentional.");
739
+ }
740
+ const userCredentials = getSnowflakeCredentials();
741
+ return {
742
+ runDetails,
743
+ userCredentials,
744
+ ...(params.parameters ? { parameters: params.parameters } : {}),
745
+ };
746
+ }