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.
- package/LICENSE +21 -0
- package/README.md +304 -0
- package/dist/cache-dir.d.ts +26 -0
- package/dist/cache-dir.js +106 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +212 -0
- package/dist/coalesce/api/environments.d.ts +20 -0
- package/dist/coalesce/api/environments.js +15 -0
- package/dist/coalesce/api/git-accounts.d.ts +21 -0
- package/dist/coalesce/api/git-accounts.js +21 -0
- package/dist/coalesce/api/jobs.d.ts +25 -0
- package/dist/coalesce/api/jobs.js +21 -0
- package/dist/coalesce/api/nodes.d.ts +29 -0
- package/dist/coalesce/api/nodes.js +33 -0
- package/dist/coalesce/api/projects.d.ts +22 -0
- package/dist/coalesce/api/projects.js +25 -0
- package/dist/coalesce/api/runs.d.ts +19 -0
- package/dist/coalesce/api/runs.js +34 -0
- package/dist/coalesce/api/subgraphs.d.ts +20 -0
- package/dist/coalesce/api/subgraphs.js +17 -0
- package/dist/coalesce/api/users.d.ts +30 -0
- package/dist/coalesce/api/users.js +31 -0
- package/dist/coalesce/types.d.ts +298 -0
- package/dist/coalesce/types.js +746 -0
- package/dist/generated/.gitkeep +0 -0
- package/dist/generated/node-type-corpus.json +42656 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -0
- package/dist/mcp/cache.d.ts +3 -0
- package/dist/mcp/cache.js +137 -0
- package/dist/mcp/environments.d.ts +3 -0
- package/dist/mcp/environments.js +61 -0
- package/dist/mcp/git-accounts.d.ts +3 -0
- package/dist/mcp/git-accounts.js +70 -0
- package/dist/mcp/jobs.d.ts +3 -0
- package/dist/mcp/jobs.js +77 -0
- package/dist/mcp/node-type-corpus.d.ts +3 -0
- package/dist/mcp/node-type-corpus.js +173 -0
- package/dist/mcp/nodes.d.ts +3 -0
- package/dist/mcp/nodes.js +341 -0
- package/dist/mcp/pipelines.d.ts +3 -0
- package/dist/mcp/pipelines.js +342 -0
- package/dist/mcp/projects.d.ts +3 -0
- package/dist/mcp/projects.js +70 -0
- package/dist/mcp/repo-node-types.d.ts +135 -0
- package/dist/mcp/repo-node-types.js +387 -0
- package/dist/mcp/runs.d.ts +3 -0
- package/dist/mcp/runs.js +92 -0
- package/dist/mcp/subgraphs.d.ts +3 -0
- package/dist/mcp/subgraphs.js +60 -0
- package/dist/mcp/users.d.ts +3 -0
- package/dist/mcp/users.js +107 -0
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.js +58 -0
- package/dist/resources/context/aggregation-patterns.md +145 -0
- package/dist/resources/context/data-engineering-principles.md +183 -0
- package/dist/resources/context/hydrated-metadata.md +92 -0
- package/dist/resources/context/id-discovery.md +64 -0
- package/dist/resources/context/intelligent-node-configuration.md +162 -0
- package/dist/resources/context/node-creation-decision-tree.md +156 -0
- package/dist/resources/context/node-operations.md +316 -0
- package/dist/resources/context/node-payloads.md +114 -0
- package/dist/resources/context/node-type-corpus.md +166 -0
- package/dist/resources/context/node-type-selection-guide.md +96 -0
- package/dist/resources/context/overview.md +135 -0
- package/dist/resources/context/pipeline-workflows.md +355 -0
- package/dist/resources/context/run-operations.md +55 -0
- package/dist/resources/context/sql-bigquery.md +41 -0
- package/dist/resources/context/sql-databricks.md +40 -0
- package/dist/resources/context/sql-platform-selection.md +70 -0
- package/dist/resources/context/sql-snowflake.md +43 -0
- package/dist/resources/context/storage-mappings.md +49 -0
- package/dist/resources/context/tool-usage.md +98 -0
- package/dist/resources/index.d.ts +5 -0
- package/dist/resources/index.js +254 -0
- package/dist/schemas/node-payloads.d.ts +5019 -0
- package/dist/schemas/node-payloads.js +147 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +63 -0
- package/dist/services/cache/snapshots.d.ts +108 -0
- package/dist/services/cache/snapshots.js +275 -0
- package/dist/services/config/context-analyzer.d.ts +14 -0
- package/dist/services/config/context-analyzer.js +76 -0
- package/dist/services/config/field-classifier.d.ts +23 -0
- package/dist/services/config/field-classifier.js +47 -0
- package/dist/services/config/intelligent.d.ts +55 -0
- package/dist/services/config/intelligent.js +306 -0
- package/dist/services/config/rules.d.ts +6 -0
- package/dist/services/config/rules.js +44 -0
- package/dist/services/config/schema-resolver.d.ts +18 -0
- package/dist/services/config/schema-resolver.js +80 -0
- package/dist/services/corpus/loader.d.ts +56 -0
- package/dist/services/corpus/loader.js +25 -0
- package/dist/services/corpus/search.d.ts +49 -0
- package/dist/services/corpus/search.js +69 -0
- package/dist/services/corpus/templates.d.ts +4 -0
- package/dist/services/corpus/templates.js +11 -0
- package/dist/services/pipelines/execution.d.ts +20 -0
- package/dist/services/pipelines/execution.js +290 -0
- package/dist/services/pipelines/node-type-intent.d.ts +96 -0
- package/dist/services/pipelines/node-type-intent.js +356 -0
- package/dist/services/pipelines/node-type-selection.d.ts +66 -0
- package/dist/services/pipelines/node-type-selection.js +758 -0
- package/dist/services/pipelines/planning.d.ts +543 -0
- package/dist/services/pipelines/planning.js +1839 -0
- package/dist/services/policies/sql-override.d.ts +7 -0
- package/dist/services/policies/sql-override.js +109 -0
- package/dist/services/repo/operations.d.ts +6 -0
- package/dist/services/repo/operations.js +10 -0
- package/dist/services/repo/parser.d.ts +70 -0
- package/dist/services/repo/parser.js +365 -0
- package/dist/services/repo/path.d.ts +2 -0
- package/dist/services/repo/path.js +58 -0
- package/dist/services/templates/nodes.d.ts +50 -0
- package/dist/services/templates/nodes.js +336 -0
- package/dist/services/workspace/analysis.d.ts +56 -0
- package/dist/services/workspace/analysis.js +151 -0
- package/dist/services/workspace/mutations.d.ts +150 -0
- package/dist/services/workspace/mutations.js +1718 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/dist/workflows/get-environment-overview.d.ts +9 -0
- package/dist/workflows/get-environment-overview.js +23 -0
- package/dist/workflows/get-run-details.d.ts +10 -0
- package/dist/workflows/get-run-details.js +28 -0
- package/dist/workflows/progress.d.ts +20 -0
- package/dist/workflows/progress.js +54 -0
- package/dist/workflows/retry-and-wait.d.ts +13 -0
- package/dist/workflows/retry-and-wait.js +139 -0
- package/dist/workflows/run-and-wait.d.ts +13 -0
- package/dist/workflows/run-and-wait.js +141 -0
- package/dist/workflows/run-status.d.ts +10 -0
- package/dist/workflows/run-status.js +27 -0
- package/package.json +34 -0
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard that narrows `unknown` to a plain key-value object.
|
|
3
|
+
* Used throughout the codebase to safely access dynamic API responses.
|
|
4
|
+
*/
|
|
5
|
+
export function isPlainObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CoalesceClient } from "../client.js";
|
|
3
|
+
export declare function getEnvironmentOverview(client: CoalesceClient, params: {
|
|
4
|
+
environmentID: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
environment: unknown;
|
|
7
|
+
nodes: unknown[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function registerGetEnvironmentOverview(server: McpServer, client: CoalesceClient): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchAllEnvironmentNodes } from "../services/cache/snapshots.js";
|
|
3
|
+
import { READ_ONLY_ANNOTATIONS, buildJsonToolResponse, validatePathSegment, handleToolError, } from "../coalesce/types.js";
|
|
4
|
+
export async function getEnvironmentOverview(client, params) {
|
|
5
|
+
const environmentID = validatePathSegment(params.environmentID, "environmentID");
|
|
6
|
+
const basePath = `/api/v1/environments/${environmentID}`;
|
|
7
|
+
const [environment, nodes] = await Promise.all([
|
|
8
|
+
client.get(basePath),
|
|
9
|
+
fetchAllEnvironmentNodes(client, { environmentID }),
|
|
10
|
+
]);
|
|
11
|
+
return { environment, nodes: nodes.items };
|
|
12
|
+
}
|
|
13
|
+
export function registerGetEnvironmentOverview(server, client) {
|
|
14
|
+
server.tool("get-environment-overview", "Get environment details and all its nodes in a single call", { environmentID: z.string().describe("The environment ID") }, READ_ONLY_ANNOTATIONS, async (params) => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await getEnvironmentOverview(client, params);
|
|
17
|
+
return buildJsonToolResponse("get-environment-overview", result);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return handleToolError(error);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CoalesceClient } from "../client.js";
|
|
3
|
+
export declare function getRunDetails(client: CoalesceClient, params: {
|
|
4
|
+
runID: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
run: unknown;
|
|
7
|
+
results: unknown;
|
|
8
|
+
resultsError?: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function registerGetRunDetails(server: McpServer, client: CoalesceClient): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { READ_ONLY_ANNOTATIONS, buildJsonToolResponse, sanitizeResponse, validatePathSegment, handleToolError, } from "../coalesce/types.js";
|
|
3
|
+
export async function getRunDetails(client, params) {
|
|
4
|
+
const validRunID = validatePathSegment(params.runID, "runID");
|
|
5
|
+
let run;
|
|
6
|
+
let results = null;
|
|
7
|
+
let resultsError;
|
|
8
|
+
const runPromise = client.get(`/api/v1/runs/${validRunID}`);
|
|
9
|
+
const resultsPromise = client.get(`/api/v1/runs/${validRunID}/results`);
|
|
10
|
+
[run] = await Promise.all([
|
|
11
|
+
runPromise,
|
|
12
|
+
resultsPromise.then((data) => { results = data; }, (error) => { resultsError = error instanceof Error ? error.message : String(error); }),
|
|
13
|
+
]);
|
|
14
|
+
return resultsError !== undefined
|
|
15
|
+
? { run, results: null, resultsError }
|
|
16
|
+
: { run, results };
|
|
17
|
+
}
|
|
18
|
+
export function registerGetRunDetails(server, client) {
|
|
19
|
+
server.tool("get-run-details", "Get run metadata and results in a single call", { runID: z.string().describe("The numeric run ID (integer, e.g. '401'). Use the runCounter value from start-run or run-status responses — not the UUID from run URLs.") }, READ_ONLY_ANNOTATIONS, async (params) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await getRunDetails(client, params);
|
|
22
|
+
return buildJsonToolResponse("get-run-details", sanitizeResponse(result));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return handleToolError(error);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type WorkflowProgressNotification = {
|
|
2
|
+
method: "notifications/progress";
|
|
3
|
+
params: {
|
|
4
|
+
progressToken: string | number;
|
|
5
|
+
progress: number;
|
|
6
|
+
total?: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type WorkflowProgressExtra = {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
_meta?: {
|
|
13
|
+
progressToken?: string | number;
|
|
14
|
+
};
|
|
15
|
+
sendNotification?: (notification: WorkflowProgressNotification) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export type WorkflowProgressReporter = (message: string, total?: number) => Promise<void>;
|
|
18
|
+
export declare function throwIfAborted(signal?: AbortSignal): void;
|
|
19
|
+
export declare function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void>;
|
|
20
|
+
export declare function createWorkflowProgressReporter(extra?: WorkflowProgressExtra): WorkflowProgressReporter | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function createAbortError() {
|
|
2
|
+
const error = new Error("Request was cancelled");
|
|
3
|
+
error.name = "AbortError";
|
|
4
|
+
return error;
|
|
5
|
+
}
|
|
6
|
+
export function throwIfAborted(signal) {
|
|
7
|
+
if (signal?.aborted) {
|
|
8
|
+
throw createAbortError();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export async function sleepWithAbort(ms, signal) {
|
|
12
|
+
throwIfAborted(signal);
|
|
13
|
+
if (!signal) {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await new Promise((resolve, reject) => {
|
|
18
|
+
const timeoutHandle = setTimeout(() => {
|
|
19
|
+
signal.removeEventListener("abort", onAbort);
|
|
20
|
+
resolve();
|
|
21
|
+
}, ms);
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
clearTimeout(timeoutHandle);
|
|
24
|
+
signal.removeEventListener("abort", onAbort);
|
|
25
|
+
reject(createAbortError());
|
|
26
|
+
};
|
|
27
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function createWorkflowProgressReporter(extra) {
|
|
31
|
+
const progressToken = extra?._meta?.progressToken;
|
|
32
|
+
const sendNotification = extra?.sendNotification;
|
|
33
|
+
if (progressToken === undefined || !sendNotification) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
let progress = 0;
|
|
37
|
+
return async (message, total) => {
|
|
38
|
+
progress += 1;
|
|
39
|
+
try {
|
|
40
|
+
await sendNotification({
|
|
41
|
+
method: "notifications/progress",
|
|
42
|
+
params: {
|
|
43
|
+
progressToken,
|
|
44
|
+
progress,
|
|
45
|
+
...(total !== undefined ? { total } : {}),
|
|
46
|
+
...(message ? { message } : {}),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Progress is best-effort and should not fail the workflow.
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { type CoalesceClient } from "../client.js";
|
|
4
|
+
import { RerunParams } from "../coalesce/types.js";
|
|
5
|
+
import { type WorkflowProgressReporter } from "./progress.js";
|
|
6
|
+
export declare function retryAndWait(client: CoalesceClient, params: z.infer<typeof RerunParams> & {
|
|
7
|
+
pollInterval?: number;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}, options?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
reportProgress?: WorkflowProgressReporter;
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
export declare function registerRetryAndWait(server: McpServer, client: CoalesceClient): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CoalesceApiError } from "../client.js";
|
|
3
|
+
import { RerunParams, buildJsonToolResponse, buildRerunBody, WRITE_ANNOTATIONS, sanitizeResponse, handleToolError, } from "../coalesce/types.js";
|
|
4
|
+
import { createWorkflowProgressReporter, sleepWithAbort, throwIfAborted, } from "./progress.js";
|
|
5
|
+
import { formatRunStatusForMessage, isTerminalRunStatus, validateRunStatus, } from "./run-status.js";
|
|
6
|
+
function remainingTimeMs(startedAt, totalTimeoutMs) {
|
|
7
|
+
return Math.max(0, totalTimeoutMs - (Date.now() - startedAt));
|
|
8
|
+
}
|
|
9
|
+
function serializeResultsError(error) {
|
|
10
|
+
if (error instanceof CoalesceApiError) {
|
|
11
|
+
return {
|
|
12
|
+
message: error.message,
|
|
13
|
+
status: error.status,
|
|
14
|
+
...(error.detail !== undefined ? { detail: error.detail } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return { message: error.message };
|
|
19
|
+
}
|
|
20
|
+
return { message: "Unable to fetch run results", detail: error };
|
|
21
|
+
}
|
|
22
|
+
export async function retryAndWait(client, params, options = {}) {
|
|
23
|
+
const pollInterval = Math.max(5, Math.min(params.pollInterval ?? 10, 300)) * 1000;
|
|
24
|
+
const timeout = Math.max(30, Math.min(params.timeout ?? 1800, 3600)) * 1000;
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
const { signal, reportProgress } = options;
|
|
27
|
+
throwIfAborted(signal);
|
|
28
|
+
// Retry the run — response is { runCounter: number }
|
|
29
|
+
const body = buildRerunBody(params);
|
|
30
|
+
const rerunResult = (await client.post("/scheduler/rerun", body, undefined, {
|
|
31
|
+
timeoutMs: remainingTimeMs(startedAt, timeout),
|
|
32
|
+
}));
|
|
33
|
+
if (typeof rerunResult.runCounter !== "number") {
|
|
34
|
+
throw new Error(`rerun response did not include a numeric runCounter (got ${typeof rerunResult.runCounter})`);
|
|
35
|
+
}
|
|
36
|
+
const runCounter = rerunResult.runCounter;
|
|
37
|
+
await reportProgress?.(`Started retry run ${runCounter}. Polling every ${pollInterval / 1000}s for up to ${timeout / 1000}s.`);
|
|
38
|
+
// Poll for status
|
|
39
|
+
let lastStatus = null;
|
|
40
|
+
let pollCount = 0;
|
|
41
|
+
while (remainingTimeMs(startedAt, timeout) > 0) {
|
|
42
|
+
const nextPollDelay = Math.min(pollInterval, remainingTimeMs(startedAt, timeout));
|
|
43
|
+
await sleepWithAbort(nextPollDelay, signal);
|
|
44
|
+
const statusTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
45
|
+
if (statusTimeoutMs <= 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
let status;
|
|
49
|
+
try {
|
|
50
|
+
status = (await client.get("/scheduler/runStatus", {
|
|
51
|
+
runCounter,
|
|
52
|
+
}, { timeoutMs: statusTimeoutMs }));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof CoalesceApiError && error.status === 408) {
|
|
56
|
+
pollCount += 1;
|
|
57
|
+
await reportProgress?.(`Status check ${pollCount} for retry run ${runCounter} timed out. Retrying while time remains.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
lastStatus = status;
|
|
63
|
+
pollCount += 1;
|
|
64
|
+
const runStatus = status.runStatus;
|
|
65
|
+
await reportProgress?.(`Status check ${pollCount} for retry run ${runCounter}: ${formatRunStatusForMessage(runStatus)}.`);
|
|
66
|
+
const validatedRunStatus = validateRunStatus(runCounter, runStatus);
|
|
67
|
+
if (isTerminalRunStatus(validatedRunStatus)) {
|
|
68
|
+
// Fetch run results — runCounter is the numeric run ID
|
|
69
|
+
await reportProgress?.(`Retry run ${runCounter} reached terminal status ${validatedRunStatus}. Fetching results.`);
|
|
70
|
+
const resultsTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
71
|
+
if (resultsTimeoutMs <= 0) {
|
|
72
|
+
await reportProgress?.(`Workflow deadline reached before results could be fetched for retry run ${runCounter}.`);
|
|
73
|
+
return {
|
|
74
|
+
status,
|
|
75
|
+
results: null,
|
|
76
|
+
resultsError: {
|
|
77
|
+
message: "Workflow timeout reached before run results could be fetched",
|
|
78
|
+
status: 408,
|
|
79
|
+
},
|
|
80
|
+
incomplete: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const results = await client.get(`/api/v1/runs/${runCounter}/results`, undefined, { timeoutMs: resultsTimeoutMs });
|
|
85
|
+
await reportProgress?.(`Fetched results for retry run ${runCounter}.`);
|
|
86
|
+
return { status, results };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const serializedError = serializeResultsError(error);
|
|
90
|
+
await reportProgress?.(`Retry run ${runCounter} finished, but fetching results failed: ${serializedError.message}.`);
|
|
91
|
+
return {
|
|
92
|
+
status,
|
|
93
|
+
results: null,
|
|
94
|
+
resultsError: serializedError,
|
|
95
|
+
incomplete: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Timeout — return last known status
|
|
101
|
+
let finalStatus = lastStatus;
|
|
102
|
+
const finalStatusTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
103
|
+
if (finalStatusTimeoutMs > 0) {
|
|
104
|
+
try {
|
|
105
|
+
finalStatus = await client.get("/scheduler/runStatus", {
|
|
106
|
+
runCounter: runCounter,
|
|
107
|
+
}, { timeoutMs: finalStatusTimeoutMs });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof CoalesceApiError && error.status === 408)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (finalStatus && typeof finalStatus === "object") {
|
|
116
|
+
validateRunStatus(runCounter, finalStatus.runStatus);
|
|
117
|
+
}
|
|
118
|
+
await reportProgress?.(`Timed out waiting for retry run ${runCounter}. Returning the last known status.`);
|
|
119
|
+
return { status: finalStatus, results: null, timedOut: true };
|
|
120
|
+
}
|
|
121
|
+
export function registerRetryAndWait(server, client) {
|
|
122
|
+
server.tool("retry-and-wait", "Retry a failed Coalesce run and wait for it to complete. Requires the runID from the original run. " +
|
|
123
|
+
"Requires Snowflake Key Pair auth; credentials are read from environment variables. Polls run status until finished or timeout.", RerunParams.extend({
|
|
124
|
+
pollInterval: z.number().optional().describe("Seconds between status checks (default: 10, min: 5, max: 300)"),
|
|
125
|
+
timeout: z.number().optional().describe("Max seconds to wait (default: 1800, min: 30, max: 3600)"),
|
|
126
|
+
}).shape, WRITE_ANNOTATIONS, async (params, extra) => {
|
|
127
|
+
try {
|
|
128
|
+
const progressReporter = createWorkflowProgressReporter(extra);
|
|
129
|
+
const result = await retryAndWait(client, params, {
|
|
130
|
+
signal: extra?.signal,
|
|
131
|
+
reportProgress: progressReporter,
|
|
132
|
+
});
|
|
133
|
+
return buildJsonToolResponse("retry-and-wait", sanitizeResponse(result));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return handleToolError(error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { type CoalesceClient } from "../client.js";
|
|
4
|
+
import { StartRunParams } from "../coalesce/types.js";
|
|
5
|
+
import { type WorkflowProgressReporter } from "./progress.js";
|
|
6
|
+
export declare function runAndWait(client: CoalesceClient, params: z.infer<typeof StartRunParams> & {
|
|
7
|
+
pollInterval?: number;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}, options?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
reportProgress?: WorkflowProgressReporter;
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
export declare function registerRunAndWait(server: McpServer, client: CoalesceClient): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CoalesceApiError } from "../client.js";
|
|
3
|
+
import { StartRunParams, buildJsonToolResponse, buildStartRunBody, WRITE_ANNOTATIONS, sanitizeResponse, handleToolError, } from "../coalesce/types.js";
|
|
4
|
+
import { createWorkflowProgressReporter, sleepWithAbort, throwIfAborted, } from "./progress.js";
|
|
5
|
+
import { formatRunStatusForMessage, isTerminalRunStatus, validateRunStatus, } from "./run-status.js";
|
|
6
|
+
function remainingTimeMs(startedAt, totalTimeoutMs) {
|
|
7
|
+
return Math.max(0, totalTimeoutMs - (Date.now() - startedAt));
|
|
8
|
+
}
|
|
9
|
+
function serializeResultsError(error) {
|
|
10
|
+
if (error instanceof CoalesceApiError) {
|
|
11
|
+
return {
|
|
12
|
+
message: error.message,
|
|
13
|
+
status: error.status,
|
|
14
|
+
...(error.detail !== undefined ? { detail: error.detail } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return { message: error.message };
|
|
19
|
+
}
|
|
20
|
+
return { message: "Unable to fetch run results", detail: error };
|
|
21
|
+
}
|
|
22
|
+
export async function runAndWait(client, params, options = {}) {
|
|
23
|
+
const pollInterval = Math.max(5, Math.min(params.pollInterval ?? 10, 300)) * 1000;
|
|
24
|
+
const timeout = Math.max(30, Math.min(params.timeout ?? 1800, 3600)) * 1000;
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
const { signal, reportProgress } = options;
|
|
27
|
+
throwIfAborted(signal);
|
|
28
|
+
// Start the run — response is { runCounter: number }
|
|
29
|
+
const body = buildStartRunBody(params);
|
|
30
|
+
const startResult = (await client.post("/scheduler/startRun", body, undefined, {
|
|
31
|
+
timeoutMs: remainingTimeMs(startedAt, timeout),
|
|
32
|
+
}));
|
|
33
|
+
if (typeof startResult.runCounter !== "number") {
|
|
34
|
+
throw new Error(`startRun response did not include a numeric runCounter (got ${typeof startResult.runCounter})`);
|
|
35
|
+
}
|
|
36
|
+
const runCounter = startResult.runCounter;
|
|
37
|
+
await reportProgress?.(`Started run ${runCounter}. Polling every ${pollInterval / 1000}s for up to ${timeout / 1000}s.`);
|
|
38
|
+
// Poll for status
|
|
39
|
+
let lastStatus = null;
|
|
40
|
+
let pollCount = 0;
|
|
41
|
+
while (remainingTimeMs(startedAt, timeout) > 0) {
|
|
42
|
+
const nextPollDelay = Math.min(pollInterval, remainingTimeMs(startedAt, timeout));
|
|
43
|
+
await sleepWithAbort(nextPollDelay, signal);
|
|
44
|
+
const statusTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
45
|
+
if (statusTimeoutMs <= 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
let status;
|
|
49
|
+
try {
|
|
50
|
+
status = (await client.get("/scheduler/runStatus", {
|
|
51
|
+
runCounter,
|
|
52
|
+
}, { timeoutMs: statusTimeoutMs }));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof CoalesceApiError && error.status === 408) {
|
|
56
|
+
pollCount += 1;
|
|
57
|
+
await reportProgress?.(`Status check ${pollCount} for run ${runCounter} timed out. Retrying while time remains.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
lastStatus = status;
|
|
63
|
+
pollCount += 1;
|
|
64
|
+
const runStatus = status.runStatus;
|
|
65
|
+
await reportProgress?.(`Status check ${pollCount} for run ${runCounter}: ${formatRunStatusForMessage(runStatus)}.`);
|
|
66
|
+
const validatedRunStatus = validateRunStatus(runCounter, runStatus);
|
|
67
|
+
if (isTerminalRunStatus(validatedRunStatus)) {
|
|
68
|
+
// Fetch run results — runCounter is the numeric run ID
|
|
69
|
+
await reportProgress?.(`Run ${runCounter} reached terminal status ${validatedRunStatus}. Fetching results.`);
|
|
70
|
+
const resultsTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
71
|
+
if (resultsTimeoutMs <= 0) {
|
|
72
|
+
await reportProgress?.(`Workflow deadline reached before results could be fetched for run ${runCounter}.`);
|
|
73
|
+
return {
|
|
74
|
+
status,
|
|
75
|
+
results: null,
|
|
76
|
+
resultsError: {
|
|
77
|
+
message: "Workflow timeout reached before run results could be fetched",
|
|
78
|
+
status: 408,
|
|
79
|
+
},
|
|
80
|
+
incomplete: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const results = await client.get(`/api/v1/runs/${runCounter}/results`, undefined, { timeoutMs: resultsTimeoutMs });
|
|
85
|
+
await reportProgress?.(`Fetched results for run ${runCounter}.`);
|
|
86
|
+
return { status, results };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const serializedError = serializeResultsError(error);
|
|
90
|
+
await reportProgress?.(`Run ${runCounter} finished, but fetching results failed: ${serializedError.message}.`);
|
|
91
|
+
return {
|
|
92
|
+
status,
|
|
93
|
+
results: null,
|
|
94
|
+
resultsError: serializedError,
|
|
95
|
+
incomplete: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Timeout — return last known status
|
|
101
|
+
let finalStatus = lastStatus;
|
|
102
|
+
const finalStatusTimeoutMs = remainingTimeMs(startedAt, timeout);
|
|
103
|
+
if (finalStatusTimeoutMs > 0) {
|
|
104
|
+
try {
|
|
105
|
+
finalStatus = await client.get("/scheduler/runStatus", {
|
|
106
|
+
runCounter,
|
|
107
|
+
}, { timeoutMs: finalStatusTimeoutMs });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof CoalesceApiError && error.status === 408)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (finalStatus && typeof finalStatus === "object") {
|
|
116
|
+
validateRunStatus(runCounter, finalStatus.runStatus);
|
|
117
|
+
}
|
|
118
|
+
await reportProgress?.(`Timed out waiting for run ${runCounter}. Returning the last known status.`);
|
|
119
|
+
return { status: finalStatus, results: null, timedOut: true };
|
|
120
|
+
}
|
|
121
|
+
export function registerRunAndWait(server, client) {
|
|
122
|
+
server.tool("run-and-wait", "Start a Coalesce run and wait for completion. Requires a numeric environmentID and optionally a jobID (not job name). " +
|
|
123
|
+
"If the user provides a job name instead of an ID, ask them for the job ID. " +
|
|
124
|
+
"If the user doesn't know the environment ID, use list-environments to look it up by name. " +
|
|
125
|
+
"Requires Snowflake Key Pair auth; credentials are read from environment variables. Polls run status until finished or timeout.", StartRunParams.extend({
|
|
126
|
+
pollInterval: z.number().optional().describe("Seconds between status checks (default: 10, min: 5, max: 300)"),
|
|
127
|
+
timeout: z.number().optional().describe("Max seconds to wait (default: 1800, min: 30, max: 3600)"),
|
|
128
|
+
}).shape, WRITE_ANNOTATIONS, async (params, extra) => {
|
|
129
|
+
try {
|
|
130
|
+
const progressReporter = createWorkflowProgressReporter(extra);
|
|
131
|
+
const result = await runAndWait(client, params, {
|
|
132
|
+
signal: extra?.signal,
|
|
133
|
+
reportProgress: progressReporter,
|
|
134
|
+
});
|
|
135
|
+
return buildJsonToolResponse("run-and-wait", sanitizeResponse(result));
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return handleToolError(error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const TERMINAL_RUN_STATUS_VALUES: readonly ["completed", "failed", "canceled"];
|
|
2
|
+
declare const NON_TERMINAL_RUN_STATUS_VALUES: readonly ["waitingToRun", "running"];
|
|
3
|
+
export type TerminalRunStatus = (typeof TERMINAL_RUN_STATUS_VALUES)[number];
|
|
4
|
+
export type NonTerminalRunStatus = (typeof NON_TERMINAL_RUN_STATUS_VALUES)[number];
|
|
5
|
+
export type KnownRunStatus = TerminalRunStatus | NonTerminalRunStatus;
|
|
6
|
+
export declare const DOCUMENTED_RUN_STATUSES: readonly ["waitingToRun", "running", "completed", "failed", "canceled"];
|
|
7
|
+
export declare function formatRunStatusForMessage(runStatus: unknown): string;
|
|
8
|
+
export declare function validateRunStatus(runCounter: number, runStatus: unknown): KnownRunStatus;
|
|
9
|
+
export declare function isTerminalRunStatus(runStatus: KnownRunStatus): runStatus is TerminalRunStatus;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const TERMINAL_RUN_STATUS_VALUES = ["completed", "failed", "canceled"];
|
|
2
|
+
const NON_TERMINAL_RUN_STATUS_VALUES = ["waitingToRun", "running"];
|
|
3
|
+
const TERMINAL_RUN_STATUSES = new Set(TERMINAL_RUN_STATUS_VALUES);
|
|
4
|
+
const KNOWN_RUN_STATUSES = new Set([
|
|
5
|
+
...TERMINAL_RUN_STATUS_VALUES,
|
|
6
|
+
...NON_TERMINAL_RUN_STATUS_VALUES,
|
|
7
|
+
]);
|
|
8
|
+
export const DOCUMENTED_RUN_STATUSES = [
|
|
9
|
+
...NON_TERMINAL_RUN_STATUS_VALUES,
|
|
10
|
+
...TERMINAL_RUN_STATUS_VALUES,
|
|
11
|
+
];
|
|
12
|
+
export function formatRunStatusForMessage(runStatus) {
|
|
13
|
+
return typeof runStatus === "string" ? runStatus : "unknown";
|
|
14
|
+
}
|
|
15
|
+
export function validateRunStatus(runCounter, runStatus) {
|
|
16
|
+
const expected = DOCUMENTED_RUN_STATUSES.join(", ");
|
|
17
|
+
if (typeof runStatus !== "string") {
|
|
18
|
+
throw new Error(`Run ${runCounter} returned a non-string runStatus (${typeof runStatus}). Expected one of: ${expected}.`);
|
|
19
|
+
}
|
|
20
|
+
if (!KNOWN_RUN_STATUSES.has(runStatus)) {
|
|
21
|
+
throw new Error(`Run ${runCounter} returned unexpected runStatus '${runStatus}'. Expected one of: ${expected}.`);
|
|
22
|
+
}
|
|
23
|
+
return runStatus;
|
|
24
|
+
}
|
|
25
|
+
export function isTerminalRunStatus(runStatus) {
|
|
26
|
+
return TERMINAL_RUN_STATUSES.has(runStatus);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coalesce-transform-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for the Coalesce Transform API; run tools support Snowflake Key Pair auth only",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coalesce-transform-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node scripts/build.mjs",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"import:node-type-corpus": "node scripts/import-node-type-corpus.mjs",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["mcp", "coalesce", "transform", "model-context-protocol"],
|
|
22
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
+
"yaml": "^2.8.3",
|
|
27
|
+
"zod": "^3.24.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"vitest": "^3.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|