@ffschrattenecker/tm1-mcp-server 6.0.0 → 7.0.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/CHANGELOG.md +110 -1
- package/README.md +28 -7
- package/dist/config.d.ts +12 -1
- package/dist/config.js +120 -79
- package/dist/connections.d.ts +69 -0
- package/dist/connections.js +262 -0
- package/dist/http-transport.d.ts +2 -2
- package/dist/index.js +45 -37
- package/dist/lib/callgraph/tm1-adapter.d.ts +4 -1
- package/dist/lib/callgraph/tm1-adapter.js +5 -2
- package/dist/lib/slim-json-schema.d.ts +8 -0
- package/dist/lib/slim-json-schema.js +34 -0
- package/dist/lib/strip-comments.js +1 -1
- package/dist/lib/tm1-events.d.ts +2 -0
- package/dist/prompts/index.js +2 -2
- package/dist/resources/index.d.ts +4 -2
- package/dist/resources/index.js +93 -53
- package/dist/resources/subscriptions.d.ts +2 -1
- package/dist/resources/subscriptions.js +16 -7
- package/dist/server-instructions.d.ts +3 -0
- package/dist/server-instructions.js +12 -0
- package/dist/tm1-client/http.d.ts +1 -1
- package/dist/tm1-client/http.js +21 -4
- package/dist/tm1-client/services/process-service.d.ts +7 -1
- package/dist/tm1-client/services/process-service.js +7 -5
- package/dist/tm1-client.d.ts +7 -1
- package/dist/tm1-client.js +10 -11
- package/dist/tools/analysis/analyze-object-usage.d.ts +10 -0
- package/dist/tools/analysis/analyze-object-usage.js +37 -33
- package/dist/tools/analysis/audit-naming.js +1 -1
- package/dist/tools/analysis/delete-impact.d.ts +13 -0
- package/dist/tools/analysis/delete-impact.js +30 -0
- package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
- package/dist/tools/analysis/trace-data-flow.js +1 -1
- package/dist/tools/celldata/write-cells.js +37 -2
- package/dist/tools/confirm.d.ts +4 -1
- package/dist/tools/confirm.js +12 -2
- package/dist/tools/define-tool.d.ts +35 -11
- package/dist/tools/define-tool.js +51 -4
- package/dist/tools/dimension-management/delete-dimension.js +18 -4
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +6 -12
- package/dist/tools/metadata/list-cubes.js +3 -3
- package/dist/tools/metadata/list-processes.js +11 -7
- package/dist/tools/model-building/delete-cube.js +18 -4
- package/dist/tools/model-building/set-cube-rules.js +13 -1
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-audit-log.js +1 -1
- package/dist/tools/operations/get-jobs.js +2 -2
- package/dist/tools/operations/get-message-log.js +1 -1
- package/dist/tools/operations/get-server-info.js +10 -1
- package/dist/tools/operations/get-server-state.js +1 -1
- package/dist/tools/operations/get-threads.js +2 -2
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-connections.d.ts +2 -0
- package/dist/tools/operations/list-connections.js +35 -0
- package/dist/tools/operations/save-data.js +1 -1
- package/dist/tools/schemas/items-monitoring.d.ts +11 -0
- package/dist/tools/schemas/items-monitoring.js +9 -1
- package/dist/tools/schemas/items-processes.d.ts +3 -0
- package/dist/tools/schemas/items-processes.js +2 -1
- package/dist/tools/ti-development/diff-processes.d.ts +42 -0
- package/dist/tools/ti-development/diff-processes.js +4 -4
- package/dist/tools/ti-development/execute-process.js +29 -4
- package/dist/tools/ti-development/import-pro-file.js +1 -1
- package/dist/tools/ti-development/preflight.d.ts +24 -0
- package/dist/tools/ti-development/preflight.js +32 -0
- package/dist/tools/ti-development/search-code.js +1 -1
- package/dist/tools/ti-development/upsert-process.js +100 -11
- package/dist/tools/with-annotations.d.ts +10 -0
- package/dist/tools/with-annotations.js +58 -3
- package/npm-shrinkwrap.json +4586 -0
- package/package.json +9 -7
- package/dist/tools/ti-development/get-process-code.d.ts +0 -2
- package/dist/tools/ti-development/get-process-code.js +0 -100
- package/dist/tools/ti-development/get-process-datasource.d.ts +0 -2
- package/dist/tools/ti-development/get-process-datasource.js +0 -20
- package/dist/tools/ti-development/get-process-parameters.d.ts +0 -2
- package/dist/tools/ti-development/get-process-parameters.js +0 -25
- package/dist/tools/ti-development/get-process-variables.d.ts +0 -2
- package/dist/tools/ti-development/get-process-variables.js +0 -38
|
@@ -47,7 +47,7 @@ export const registerGetServerState = defineTool({
|
|
|
47
47
|
tm1Client.server.getInfo(),
|
|
48
48
|
tm1Client.cubes.list(),
|
|
49
49
|
tm1Client.dimensions.list(),
|
|
50
|
-
tm1Client.processes.list(),
|
|
50
|
+
tm1Client.processes.list({ namesOnly: true }),
|
|
51
51
|
tm1Client.chores.list(),
|
|
52
52
|
tm1Client.security.listClients(),
|
|
53
53
|
]);
|
|
@@ -9,7 +9,7 @@ const registerListThreads = defineTool({
|
|
|
9
9
|
name: "tm1_list_threads",
|
|
10
10
|
description: "List active threads on the TM1 server (running processes, chores, MDX queries, etc.). Paginated (default 50/page). (v11 only)",
|
|
11
11
|
annotations: READ_ONLY,
|
|
12
|
-
|
|
12
|
+
version: 11,
|
|
13
13
|
output: pageShapeFor(ThreadSchema),
|
|
14
14
|
input: { ...PAGINATION_SCHEMA, ...FORMAT_SCHEMA },
|
|
15
15
|
handler: async ({ limit, offset, fetchAll, format }, tm1Client) => {
|
|
@@ -33,7 +33,7 @@ const registerCancelThread = defineTool({
|
|
|
33
33
|
"(v11 only)",
|
|
34
34
|
],
|
|
35
35
|
annotations: DESTRUCTIVE,
|
|
36
|
-
|
|
36
|
+
version: 11,
|
|
37
37
|
output: MutationResultSchema,
|
|
38
38
|
input: {
|
|
39
39
|
id: z.number().int().describe("Thread ID to cancel"),
|
|
@@ -10,7 +10,7 @@ export const registerGetTransactionLog = defineTool({
|
|
|
10
10
|
// v12 deprecated TransactionLogEntry/TransactionLogEntries (and the
|
|
11
11
|
// TransactionLog / TailTransactionLog functions) in 12.0.0; all of them serve
|
|
12
12
|
// empty with no successor endpoint.
|
|
13
|
-
|
|
13
|
+
version: 11,
|
|
14
14
|
output: {
|
|
15
15
|
count: z.number().int(),
|
|
16
16
|
coverage: z
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "../define-tool.js";
|
|
3
|
+
import { READ_ONLY } from "../annotations.js";
|
|
4
|
+
const ConnectionSchema = z.object({
|
|
5
|
+
name: z.string(),
|
|
6
|
+
mode: z.enum(["readwrite", "readonly"]).optional(),
|
|
7
|
+
tm1Version: z.string().optional(),
|
|
8
|
+
connected: z.boolean(),
|
|
9
|
+
error: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const registerListConnections = defineTool({
|
|
12
|
+
name: "tm1_list_connections",
|
|
13
|
+
description: "List the TM1 connections this server can reach: name (the `connection` argument of every other tool), readonly/readwrite mode, TM1 version, and whether a session is open. Makes no TM1 call.",
|
|
14
|
+
annotations: READ_ONLY,
|
|
15
|
+
output: z.object({ connections: z.array(ConnectionSchema) }),
|
|
16
|
+
input: {},
|
|
17
|
+
connectionless: true,
|
|
18
|
+
handler: (_args, registry) => {
|
|
19
|
+
const connections = registry.status().map((c) => ({
|
|
20
|
+
name: c.name,
|
|
21
|
+
...(c.mode && { mode: c.mode }),
|
|
22
|
+
...(c.tm1Version && { tm1Version: c.tm1Version }),
|
|
23
|
+
connected: c.connected,
|
|
24
|
+
...((c.configError ?? c.lastError) && {
|
|
25
|
+
error: c.configError ?? c.lastError,
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{ type: "text", text: JSON.stringify({ connections }) },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=list-connections.js.map
|
|
@@ -13,7 +13,7 @@ export const registerSaveData = defineTool({
|
|
|
13
13
|
"Executes as an unbound TI process via ExecuteProcessWithReturn; no process object is created on the server.",
|
|
14
14
|
],
|
|
15
15
|
annotations: withVersion(IDEMPOTENT_WRITE, "v11"),
|
|
16
|
-
|
|
16
|
+
version: 11,
|
|
17
17
|
output: MutationResultSchema,
|
|
18
18
|
input: {
|
|
19
19
|
cube: z
|
|
@@ -4,6 +4,17 @@ export declare const ServerInfoSchema: z.ZodObject<{
|
|
|
4
4
|
mcpServer: z.ZodOptional<z.ZodObject<{
|
|
5
5
|
name: z.ZodString;
|
|
6
6
|
version: z.ZodString;
|
|
7
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
readwrite: "readwrite";
|
|
9
|
+
readonly: "readonly";
|
|
10
|
+
}>>;
|
|
11
|
+
environment: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
dev: "dev";
|
|
13
|
+
test: "test";
|
|
14
|
+
prod: "prod";
|
|
15
|
+
unspecified: "unspecified";
|
|
16
|
+
}>>;
|
|
17
|
+
modeReason: z.ZodOptional<z.ZodString>;
|
|
7
18
|
}, z.core.$strip>>;
|
|
8
19
|
serverName: z.ZodString;
|
|
9
20
|
productVersion: z.ZodString;
|
|
@@ -7,7 +7,15 @@ export { AuditLogDetailSchema, ErrorLogFileSchema, JobSchema, MessageLogEntrySch
|
|
|
7
7
|
import { AuditLogDetailSchema } from "../../schemas/monitoring.js";
|
|
8
8
|
export const ServerInfoSchema = z
|
|
9
9
|
.object({
|
|
10
|
-
mcpServer: z
|
|
10
|
+
mcpServer: z
|
|
11
|
+
.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
version: z.string(),
|
|
14
|
+
mode: z.enum(["readwrite", "readonly"]).optional(),
|
|
15
|
+
environment: z.enum(["dev", "test", "prod", "unspecified"]).optional(),
|
|
16
|
+
modeReason: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.optional(),
|
|
11
19
|
serverName: z.string(),
|
|
12
20
|
productVersion: z.string(),
|
|
13
21
|
productEdition: z.string().optional(),
|
|
@@ -154,12 +154,15 @@ export declare const UpsertProcessResultSchema: z.ZodObject<{
|
|
|
154
154
|
action: z.ZodEnum<{
|
|
155
155
|
created: "created";
|
|
156
156
|
updated: "updated";
|
|
157
|
+
wouldCreate: "wouldCreate";
|
|
158
|
+
wouldUpdate: "wouldUpdate";
|
|
157
159
|
}>;
|
|
158
160
|
appliedSteps: z.ZodArray<z.ZodString>;
|
|
159
161
|
backup: z.ZodOptional<z.ZodObject<{
|
|
160
162
|
json: z.ZodString;
|
|
161
163
|
ti: z.ZodString;
|
|
162
164
|
}, z.core.$strip>>;
|
|
165
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
163
166
|
}, z.core.$loose>;
|
|
164
167
|
export declare const InstallProBundleResultSchema: z.ZodObject<{
|
|
165
168
|
directory: z.ZodString;
|
|
@@ -102,9 +102,10 @@ const ProcessBackupSchema = z.object({ json: z.string(), ti: z.string() });
|
|
|
102
102
|
export const UpsertProcessResultSchema = z
|
|
103
103
|
.object({
|
|
104
104
|
processName: z.string(),
|
|
105
|
-
action: z.enum(["created", "updated"]),
|
|
105
|
+
action: z.enum(["created", "updated", "wouldCreate", "wouldUpdate"]),
|
|
106
106
|
appliedSteps: z.array(z.string()),
|
|
107
107
|
backup: ProcessBackupSchema.optional(),
|
|
108
|
+
dryRun: z.boolean().optional(),
|
|
108
109
|
})
|
|
109
110
|
.passthrough();
|
|
110
111
|
const InstallBundleEntrySchema = z
|
|
@@ -1,2 +1,44 @@
|
|
|
1
|
+
import type { ProcessParameter, ProcessVariable, DataSource } from "../../types.js";
|
|
2
|
+
interface DiffHunk {
|
|
3
|
+
startA: number;
|
|
4
|
+
countA: number;
|
|
5
|
+
startB: number;
|
|
6
|
+
countB: number;
|
|
7
|
+
lines: Array<{
|
|
8
|
+
type: " " | "+" | "-";
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function tabCodeDiff(codeA: string, codeB: string, contextLines: number): {
|
|
13
|
+
identical: boolean;
|
|
14
|
+
linesA: number;
|
|
15
|
+
linesB: number;
|
|
16
|
+
hunks: DiffHunk[];
|
|
17
|
+
};
|
|
18
|
+
export declare function diffParams(a: ProcessParameter[], b: ProcessParameter[]): {
|
|
19
|
+
identical: boolean;
|
|
20
|
+
added: string[];
|
|
21
|
+
removed: string[];
|
|
22
|
+
changed: {
|
|
23
|
+
name: string;
|
|
24
|
+
a: ProcessParameter;
|
|
25
|
+
b: ProcessParameter;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
export declare function diffVars(a: ProcessVariable[], b: ProcessVariable[]): {
|
|
29
|
+
identical: boolean;
|
|
30
|
+
added: string[];
|
|
31
|
+
removed: string[];
|
|
32
|
+
changed: {
|
|
33
|
+
name: string;
|
|
34
|
+
a: ProcessVariable;
|
|
35
|
+
b: ProcessVariable;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
export declare function diffDs(a: DataSource, b: DataSource): {
|
|
39
|
+
identical: boolean;
|
|
40
|
+
differences: string[];
|
|
41
|
+
};
|
|
1
42
|
export declare const registerDiffProcesses: import("../define-tool.js").ToolRegistrar;
|
|
43
|
+
export {};
|
|
2
44
|
//# sourceMappingURL=diff-processes.d.ts.map
|
|
@@ -105,7 +105,7 @@ function buildHunks(ops, contextLines) {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
const NORM = (s) => s.replace(/\r\n/g, "\n").trimEnd();
|
|
108
|
-
function tabCodeDiff(codeA, codeB, contextLines) {
|
|
108
|
+
export function tabCodeDiff(codeA, codeB, contextLines) {
|
|
109
109
|
const na = NORM(codeA);
|
|
110
110
|
const nb = NORM(codeB);
|
|
111
111
|
if (na === nb) {
|
|
@@ -119,7 +119,7 @@ function tabCodeDiff(codeA, codeB, contextLines) {
|
|
|
119
119
|
return { identical: false, linesA: a.length, linesB: b.length, hunks };
|
|
120
120
|
}
|
|
121
121
|
// ── param / var / datasource diff ────────────────────────────────────────────
|
|
122
|
-
function diffParams(a, b) {
|
|
122
|
+
export function diffParams(a, b) {
|
|
123
123
|
const ma = new Map(a.map((p) => [p.name, p]));
|
|
124
124
|
const mb = new Map(b.map((p) => [p.name, p]));
|
|
125
125
|
const added = [];
|
|
@@ -147,7 +147,7 @@ function diffParams(a, b) {
|
|
|
147
147
|
changed,
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
|
-
function diffVars(a, b) {
|
|
150
|
+
export function diffVars(a, b) {
|
|
151
151
|
const ma = new Map(a.map((v) => [v.name, v]));
|
|
152
152
|
const mb = new Map(b.map((v) => [v.name, v]));
|
|
153
153
|
const added = [];
|
|
@@ -204,7 +204,7 @@ function diffIgnoredColumns(a, b) {
|
|
|
204
204
|
renamed,
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
-
function diffDs(a, b) {
|
|
207
|
+
export function diffDs(a, b) {
|
|
208
208
|
const diffs = [];
|
|
209
209
|
const fields = [
|
|
210
210
|
"type",
|
|
@@ -5,6 +5,7 @@ import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
|
5
5
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
6
6
|
import { ProcessResultSchema } from "../schemas/items.js";
|
|
7
7
|
import { defineTool } from "../define-tool.js";
|
|
8
|
+
import { tailLines } from "../operations/error-log-helpers.js";
|
|
8
9
|
/**
|
|
9
10
|
* Client-abort recovery hint, branched by TM1 major version: v11 exposes
|
|
10
11
|
* server threads (tm1_list_threads / tm1_cancel_thread), v12 exposes jobs
|
|
@@ -15,13 +16,24 @@ export function abortHint(version) {
|
|
|
15
16
|
const cancel = version === 12 ? "tm1_cancel_job" : "tm1_cancel_thread";
|
|
16
17
|
return `Request aborted by the client — the process was NOT confirmed failed and may still be executing. Use ${monitor} to check for it and ${cancel} to stop it. Do NOT blindly re-run: that risks a duplicate execution.`;
|
|
17
18
|
}
|
|
19
|
+
const ERROR_LOG_TAIL_LINES = 40;
|
|
20
|
+
async function fetchErrorLog(tm1Client, filename) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await tm1Client.server.getErrorLogContent(filename);
|
|
23
|
+
const { body, truncated, totalLines } = tailLines(raw, ERROR_LOG_TAIL_LINES);
|
|
24
|
+
return { filename, totalLines, truncated, content: body };
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return { filename, fetchError: e.message };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
export const registerExecuteProcess = defineTool({
|
|
19
31
|
name: "tm1_execute_process",
|
|
20
32
|
description: [
|
|
21
33
|
"Execute a TurboIntegrator process on the TM1 server with optional parameters.",
|
|
22
34
|
"Non-idempotent: each call re-runs the process — do not retry blindly on transport errors without checking server state.",
|
|
23
|
-
"Before: tm1_check_process_code (syntax) and/or tm1_compile_process (full compile). Discover required params with
|
|
24
|
-
"
|
|
35
|
+
"Before: tm1_check_process_code (syntax) and/or tm1_compile_process (full compile). Discover required params with tm1_get_process.",
|
|
36
|
+
"When TM1 wrote an error log for the run (failure or minor errors), its last 40 lines come back as errorLog. For cascade siblings and older logs use tm1_diagnose_process_error.",
|
|
25
37
|
],
|
|
26
38
|
annotations: DESTRUCTIVE,
|
|
27
39
|
output: ProcessResultSchema,
|
|
@@ -74,9 +86,22 @@ export const registerExecuteProcess = defineTool({
|
|
|
74
86
|
const result = await withToolHint(tm1Client.processes.execute(processName, parameters, {
|
|
75
87
|
signal: extra?.signal,
|
|
76
88
|
...(timeoutMs ? { timeoutMs } : {}),
|
|
77
|
-
}), `Process '${processName}' failed at runtime. Inspect cascade with tm1_diagnose_process_error(processName='${processName}', includeRelated=true). Verify parameter shape via
|
|
89
|
+
}), `Process '${processName}' failed at runtime. Inspect cascade with tm1_diagnose_process_error(processName='${processName}', includeRelated=true). Verify parameter shape via tm1_get_process; check syntax with tm1_compile_process before re-running.`);
|
|
90
|
+
// TM1 names the run's own error log when it wrote one (minor errors
|
|
91
|
+
// included). Attach its tail so judging the run takes no extra call.
|
|
92
|
+
const errorLog = result.errorLogFile
|
|
93
|
+
? await fetchErrorLog(tm1Client, result.errorLogFile)
|
|
94
|
+
: undefined;
|
|
78
95
|
return {
|
|
79
|
-
content: [
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
...result,
|
|
101
|
+
...(errorLog ? { errorLog } : {}),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
80
105
|
// A TI process that ran but reported failure is a tool failure, not a
|
|
81
106
|
// successful call carrying success:false. Flag isError so agents that
|
|
82
107
|
// branch on the MCP error signal don't silently treat it as success;
|
|
@@ -79,7 +79,7 @@ export const registerImportProFile = defineTool({
|
|
|
79
79
|
if (failure)
|
|
80
80
|
return preflightResult(failure);
|
|
81
81
|
}
|
|
82
|
-
const allProcs = await tm1Client.processes.list();
|
|
82
|
+
const allProcs = await tm1Client.processes.list({ namesOnly: true });
|
|
83
83
|
const exists = allProcs.some((p) => p.name === processName);
|
|
84
84
|
if (mode === "create" && exists) {
|
|
85
85
|
throw new TM1Error({
|
|
@@ -36,4 +36,28 @@ export declare function preflightResult(failure: PreflightFailure): {
|
|
|
36
36
|
}[];
|
|
37
37
|
isError: true;
|
|
38
38
|
};
|
|
39
|
+
export interface PreflightChecks {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
syntax: {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
errors: Array<{
|
|
44
|
+
procedure?: string | undefined;
|
|
45
|
+
lineNumber?: number | undefined;
|
|
46
|
+
message: string;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
references: {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
unresolved: number;
|
|
52
|
+
issues: RefIssue[];
|
|
53
|
+
partial: boolean;
|
|
54
|
+
unresolvableArgs: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Both checks, neither short-circuiting the other: the dryRun report the
|
|
59
|
+
* user approves from. runPreflight stops at the first failure because the
|
|
60
|
+
* install is refused either way; a pre-approval report needs every finding.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runChecks(tm1Client: TM1Client, p: PreflightPayload): Promise<PreflightChecks>;
|
|
39
63
|
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -53,4 +53,36 @@ export function preflightResult(failure) {
|
|
|
53
53
|
isError: true,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Both checks, neither short-circuiting the other: the dryRun report the
|
|
58
|
+
* user approves from. runPreflight stops at the first failure because the
|
|
59
|
+
* install is refused either way; a pre-approval report needs every finding.
|
|
60
|
+
*/
|
|
61
|
+
export async function runChecks(tm1Client, p) {
|
|
62
|
+
const [check, refs] = await Promise.all([
|
|
63
|
+
tm1Client.processes.check({
|
|
64
|
+
name: p.name,
|
|
65
|
+
prolog: p.prolog,
|
|
66
|
+
metadata: p.metadata,
|
|
67
|
+
data: p.data,
|
|
68
|
+
epilog: p.epilog,
|
|
69
|
+
...(p.parameters !== undefined ? { parameters: p.parameters } : {}),
|
|
70
|
+
...(p.variables !== undefined ? { variables: p.variables } : {}),
|
|
71
|
+
...(p.dataSource !== undefined ? { dataSource: p.dataSource } : {}),
|
|
72
|
+
}),
|
|
73
|
+
checkProcessRefs(tm1Client, p),
|
|
74
|
+
]);
|
|
75
|
+
const references = {
|
|
76
|
+
ok: refs.unresolved === 0,
|
|
77
|
+
unresolved: refs.unresolved,
|
|
78
|
+
issues: refs.issues,
|
|
79
|
+
partial: refs.partial,
|
|
80
|
+
unresolvableArgs: refs.unresolvableArgs,
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
ok: check.success && references.ok,
|
|
84
|
+
syntax: { ok: check.success, errors: check.errors },
|
|
85
|
+
references,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
56
88
|
//# sourceMappingURL=preflight.js.map
|
|
@@ -14,7 +14,7 @@ export const registerSearchCode = defineTool({
|
|
|
14
14
|
"Regex search across all TI process code (Prolog/Metadata/Data/Epilog).",
|
|
15
15
|
"Returns matches paginated (default 50/page) with process name, tab, line number, and trimmed line text.",
|
|
16
16
|
"Wrapper over tm1_get_all_processes_code that avoids dumping ~MB of code through the channel.",
|
|
17
|
-
"Use
|
|
17
|
+
"Use tm1_get_process on a hit to inspect the surrounding context.",
|
|
18
18
|
"Set groupBy='process' (or 'tab') for a sorted count aggregation instead of individual match lines.",
|
|
19
19
|
],
|
|
20
20
|
annotations: READ_ONLY,
|
|
@@ -6,12 +6,14 @@ import { IDEMPOTENT_DESTRUCTIVE } from "../annotations.js";
|
|
|
6
6
|
import { UpsertProcessResultSchema } from "../schemas/items.js";
|
|
7
7
|
import { defineTool } from "../define-tool.js";
|
|
8
8
|
import { OVERWRITE_CONFIRM_SCHEMA, requireOverwriteConfirm, } from "../confirm.js";
|
|
9
|
-
import { preflightResult, runPreflight } from "./preflight.js";
|
|
9
|
+
import { preflightResult, runChecks, runPreflight, } from "./preflight.js";
|
|
10
|
+
import { diffDs, diffParams, diffVars, tabCodeDiff } from "./diff-processes.js";
|
|
11
|
+
import { maskCode } from "../../lib/mask-secrets.js";
|
|
10
12
|
import { backupProcess } from "./process-backup.js";
|
|
11
13
|
// The same data source shape the git round-trip and check_process_code use.
|
|
12
14
|
// This tool used to carry its own copy, which had drifted: it was missing
|
|
13
15
|
// `query`, so an ODBC source could be created here but its SQL could not —
|
|
14
|
-
// while
|
|
16
|
+
// while tm1_get_process reads it back.
|
|
15
17
|
// Strict, so a misspelled field is rejected instead of silently dropped.
|
|
16
18
|
const dataSourceSchema = sharedDataSourceSchema.strict();
|
|
17
19
|
const parameterSchema = z.object({
|
|
@@ -20,6 +22,7 @@ const parameterSchema = z.object({
|
|
|
20
22
|
defaultValue: z.union([z.string(), z.number()]),
|
|
21
23
|
prompt: z.string().optional(),
|
|
22
24
|
});
|
|
25
|
+
const TABS = ["prolog", "metadata", "data", "epilog"];
|
|
23
26
|
const variableSchema = z.object({
|
|
24
27
|
name: z.string(),
|
|
25
28
|
type: z.enum(["String", "Numeric"]),
|
|
@@ -57,8 +60,13 @@ export const registerUpsertProcess = defineTool({
|
|
|
57
60
|
.optional()
|
|
58
61
|
.default(false)
|
|
59
62
|
.describe("After deploy, run tm1.Compile and include the result in the response (compile: {ok, errorCount, errors}). Off by default — compile holds a brief lock on the process and serializes badly under bulk-deploy."),
|
|
63
|
+
dryRun: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.optional()
|
|
66
|
+
.default(false)
|
|
67
|
+
.describe("Write nothing. Run the syntax AND reference check on the process as it would be after this call (both reported, neither stops the other) and diff it against the installed version (credentials masked). Needs no confirm and is not one: the real call still needs it. Replaces a separate check_process_code + validate_process_refs + diff before the risk assessment."),
|
|
60
68
|
},
|
|
61
|
-
handler: async ({ processName, prolog, metadata, data, epilog, parameters, variables, dataSource, hasSecurityAccess, mode, preflight, autoCompile, confirm, }, tm1Client) => {
|
|
69
|
+
handler: async ({ processName, prolog, metadata, data, epilog, parameters, variables, dataSource, hasSecurityAccess, mode, preflight, autoCompile, dryRun, confirm, }, tm1Client) => {
|
|
62
70
|
const trail = [];
|
|
63
71
|
const exists = await tm1Client.processes.exists(processName);
|
|
64
72
|
if (mode === "create" && exists) {
|
|
@@ -73,16 +81,13 @@ export const registerUpsertProcess = defineTool({
|
|
|
73
81
|
message: `Process '${processName}' does not exist; mode=update`,
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (preflight) {
|
|
80
|
-
// Check the process as it will stand after this call, not the fields
|
|
81
|
-
// the caller happened to send: an omitted tab keeps its installed code.
|
|
84
|
+
// The process as it will stand after this call, not the fields the
|
|
85
|
+
// caller happened to send: an omitted tab keeps its installed code.
|
|
86
|
+
const resolve = async () => {
|
|
82
87
|
const current = exists
|
|
83
88
|
? await tm1Client.processes.getCode(processName)
|
|
84
89
|
: { prolog: "", metadata: "", data: "", epilog: "" };
|
|
85
|
-
|
|
90
|
+
return {
|
|
86
91
|
name: processName,
|
|
87
92
|
prolog: prolog ?? current.prolog,
|
|
88
93
|
metadata: metadata ?? current.metadata,
|
|
@@ -96,7 +101,79 @@ export const registerUpsertProcess = defineTool({
|
|
|
96
101
|
? await tm1Client.processes.getVariables(processName)
|
|
97
102
|
: [],
|
|
98
103
|
...(dataSource !== undefined ? { dataSource } : {}),
|
|
99
|
-
}
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
// Deliberately before the confirm gate: a dry run writes nothing, and
|
|
108
|
+
// it is what the risk assessment is built from.
|
|
109
|
+
const payload = await resolve();
|
|
110
|
+
const [checks, installed, installedParams] = await Promise.all([
|
|
111
|
+
runChecks(tm1Client, payload),
|
|
112
|
+
exists
|
|
113
|
+
? tm1Client.processes.getCode(processName)
|
|
114
|
+
: Promise.resolve({ prolog: "", metadata: "", data: "", epilog: "" }),
|
|
115
|
+
exists
|
|
116
|
+
? tm1Client.processes.getParameters(processName)
|
|
117
|
+
: Promise.resolve([]),
|
|
118
|
+
]);
|
|
119
|
+
const tabs = Object.fromEntries(TABS.map((t) => [
|
|
120
|
+
t,
|
|
121
|
+
tabCodeDiff(maskCode(installed[t] ?? ""), maskCode(payload[t]), 3),
|
|
122
|
+
]));
|
|
123
|
+
const params = diffParams(installedParams, payload.parameters ?? []);
|
|
124
|
+
// Variables and datasource change only when the caller sends them; a
|
|
125
|
+
// datasource swap must not come back as "identical".
|
|
126
|
+
const sendsVars = variables !== undefined && variables.length > 0;
|
|
127
|
+
const [installedVars, installedDs] = await Promise.all([
|
|
128
|
+
sendsVars && exists
|
|
129
|
+
? tm1Client.processes.getVariableLayout(processName)
|
|
130
|
+
: Promise.resolve({ variables: [] }),
|
|
131
|
+
dataSource !== undefined && exists
|
|
132
|
+
? tm1Client.processes.getDataSource(processName)
|
|
133
|
+
: Promise.resolve({}),
|
|
134
|
+
]);
|
|
135
|
+
const vars = sendsVars
|
|
136
|
+
? diffVars(installedVars.variables, variables)
|
|
137
|
+
: undefined;
|
|
138
|
+
const ds = dataSource !== undefined
|
|
139
|
+
? // updateDataSource PATCHes only the fields sent; the rest stay.
|
|
140
|
+
diffDs(installedDs, {
|
|
141
|
+
...installedDs,
|
|
142
|
+
...dataSource,
|
|
143
|
+
})
|
|
144
|
+
: undefined;
|
|
145
|
+
const identical = Object.values(tabs).every((t) => t.identical) &&
|
|
146
|
+
params.identical &&
|
|
147
|
+
(vars?.identical ?? true) &&
|
|
148
|
+
(ds?.identical ?? true);
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: JSON.stringify({
|
|
154
|
+
processName,
|
|
155
|
+
dryRun: true,
|
|
156
|
+
action: exists ? "wouldUpdate" : "wouldCreate",
|
|
157
|
+
appliedSteps: [],
|
|
158
|
+
overwritesExisting: exists,
|
|
159
|
+
checks,
|
|
160
|
+
diff: {
|
|
161
|
+
identical,
|
|
162
|
+
tabs,
|
|
163
|
+
parameters: params,
|
|
164
|
+
...(vars ? { variables: vars } : {}),
|
|
165
|
+
...(ds ? { dataSource: ds } : {}),
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Replacing an installed process is not undoable through the API.
|
|
173
|
+
if (exists)
|
|
174
|
+
requireOverwriteConfirm(confirm, processName, "process");
|
|
175
|
+
if (preflight) {
|
|
176
|
+
const failure = await runPreflight(tm1Client, await resolve());
|
|
100
177
|
if (failure)
|
|
101
178
|
return preflightResult(failure);
|
|
102
179
|
}
|
|
@@ -137,6 +214,17 @@ export const registerUpsertProcess = defineTool({
|
|
|
137
214
|
// Process body/parameters/datasource may have changed call sites — drop the
|
|
138
215
|
// 60s callgraph TTL so the next analysis sees fresh references instead of stale graph.
|
|
139
216
|
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache();
|
|
217
|
+
// Read the code back: TM1 stores what REST sent, so a mismatch means the
|
|
218
|
+
// write did not land as sent. Saves the caller a get_process turn.
|
|
219
|
+
const sent = { prolog, metadata, data, epilog };
|
|
220
|
+
const sentTabs = TABS.filter((t) => sent[t] !== undefined);
|
|
221
|
+
let verified;
|
|
222
|
+
if (sentTabs.length > 0) {
|
|
223
|
+
const stored = await tm1Client.processes.getCode(processName);
|
|
224
|
+
const norm = (x) => (x ?? "").replace(/\r\n/g, "\n").trimEnd();
|
|
225
|
+
const mismatchedTabs = sentTabs.filter((t) => norm(stored[t]) !== norm(sent[t]));
|
|
226
|
+
verified = { codeMatches: mismatchedTabs.length === 0, mismatchedTabs };
|
|
227
|
+
}
|
|
140
228
|
let compile;
|
|
141
229
|
if (autoCompile) {
|
|
142
230
|
const result = await tm1Client.processes.compile(processName);
|
|
@@ -157,6 +245,7 @@ export const registerUpsertProcess = defineTool({
|
|
|
157
245
|
appliedSteps: trail,
|
|
158
246
|
callgraphEntriesCleared,
|
|
159
247
|
...(backup ? { backup } : {}),
|
|
248
|
+
...(verified !== undefined ? { verified } : {}),
|
|
160
249
|
...(compile !== undefined ? { compile } : {}),
|
|
161
250
|
}, null, 2),
|
|
162
251
|
},
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type pino from "pino";
|
|
2
2
|
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { type McpToolResult } from "./error-format.js";
|
|
3
4
|
export declare function deriveTitle(toolName: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Cut an oversized page down to the longest prefix of its items that fits,
|
|
7
|
+
* turning it into an ordinary shorter page: count, has_more and next_offset
|
|
8
|
+
* are rewritten so the caller walks on from exactly where it stopped. The
|
|
9
|
+
* result is still valid JSON of the declared shape — unlike a text cut — and
|
|
10
|
+
* the call is not wasted. Returns undefined when not even one item fits (or
|
|
11
|
+
* the payload is not a page), leaving the caller to report the overflow.
|
|
12
|
+
*/
|
|
13
|
+
export declare function trimPageToFit(result: McpToolResult, maxChars: number): McpToolResult | undefined;
|
|
4
14
|
export declare function narrowingHint(inputKeys: ReadonlySet<string>): string;
|
|
5
15
|
export declare function withAnnotations(server: McpServer, logger: pino.Logger, mode: "readwrite" | "readonly", responseMode?: "legacy" | "structured", maxResponseChars?: number): McpServer;
|
|
6
16
|
//# sourceMappingURL=with-annotations.d.ts.map
|
|
@@ -14,7 +14,7 @@ const TITLE_CASING = {
|
|
|
14
14
|
v11: "v11",
|
|
15
15
|
};
|
|
16
16
|
// Derive a human-readable title from a snake_case tool name:
|
|
17
|
-
// "
|
|
17
|
+
// "tm1_get_process" → "Get Process". Single point of derivation —
|
|
18
18
|
// no per-tool overrides in ANNOTATION_MAP.
|
|
19
19
|
export function deriveTitle(toolName) {
|
|
20
20
|
return toolName
|
|
@@ -49,6 +49,55 @@ function responseChars(result) {
|
|
|
49
49
|
return text;
|
|
50
50
|
return JSON.stringify(result.structuredContent).length;
|
|
51
51
|
}
|
|
52
|
+
function isPagePayload(value) {
|
|
53
|
+
if (typeof value !== "object" || value === null)
|
|
54
|
+
return false;
|
|
55
|
+
const v = value;
|
|
56
|
+
return (Array.isArray(v.items) &&
|
|
57
|
+
typeof v.offset === "number" &&
|
|
58
|
+
typeof v.total === "number");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cut an oversized page down to the longest prefix of its items that fits,
|
|
62
|
+
* turning it into an ordinary shorter page: count, has_more and next_offset
|
|
63
|
+
* are rewritten so the caller walks on from exactly where it stopped. The
|
|
64
|
+
* result is still valid JSON of the declared shape — unlike a text cut — and
|
|
65
|
+
* the call is not wasted. Returns undefined when not even one item fits (or
|
|
66
|
+
* the payload is not a page), leaving the caller to report the overflow.
|
|
67
|
+
*/
|
|
68
|
+
export function trimPageToFit(result, maxChars) {
|
|
69
|
+
const page = result.structuredContent;
|
|
70
|
+
if (!isPagePayload(page))
|
|
71
|
+
return undefined;
|
|
72
|
+
const withText = (result.content ?? []).some((c) => c.type === "text");
|
|
73
|
+
const build = (n) => {
|
|
74
|
+
const trimmed = {
|
|
75
|
+
...page,
|
|
76
|
+
count: n,
|
|
77
|
+
has_more: true, // n < items.length: at least one item was dropped
|
|
78
|
+
next_offset: page.offset + n,
|
|
79
|
+
items: page.items.slice(0, n),
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
...result,
|
|
83
|
+
content: withText
|
|
84
|
+
? [{ type: "text", text: JSON.stringify(trimmed) }]
|
|
85
|
+
: [],
|
|
86
|
+
structuredContent: trimmed,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
// Largest n in [1, items.length) whose serialization fits.
|
|
90
|
+
let lo = 0;
|
|
91
|
+
let hi = page.items.length - 1;
|
|
92
|
+
while (lo < hi) {
|
|
93
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
94
|
+
if (responseChars(build(mid)) <= maxChars)
|
|
95
|
+
lo = mid;
|
|
96
|
+
else
|
|
97
|
+
hi = mid - 1;
|
|
98
|
+
}
|
|
99
|
+
return lo > 0 ? build(lo) : undefined;
|
|
100
|
+
}
|
|
52
101
|
// How to narrow a call, derived from the tool's own input keys so a new tool
|
|
53
102
|
// gets a correct hint without a per-tool table to keep in step.
|
|
54
103
|
export function narrowingHint(inputKeys) {
|
|
@@ -199,12 +248,18 @@ responseMode = "legacy", maxResponseChars = DEFAULT_MAX_RESPONSE_CHARS) {
|
|
|
199
248
|
}
|
|
200
249
|
};
|
|
201
250
|
const SLOW_TOOL_MS = 5000;
|
|
202
|
-
//
|
|
203
|
-
//
|
|
251
|
+
// An oversized page is cut to the items that fit (see trimPageToFit).
|
|
252
|
+
// Anything else becomes an error that says how to narrow the call — never a
|
|
253
|
+
// truncated text: a cut JSON payload is worse than none.
|
|
204
254
|
const guardSize = (toolName, result, hint) => {
|
|
205
255
|
const size = responseChars(result);
|
|
206
256
|
if (size <= maxResponseChars)
|
|
207
257
|
return result;
|
|
258
|
+
const trimmed = trimPageToFit(result, maxResponseChars);
|
|
259
|
+
if (trimmed) {
|
|
260
|
+
logger.info({ tool: toolName, size, maxResponseChars }, "page trimmed to fit the response limit");
|
|
261
|
+
return trimmed;
|
|
262
|
+
}
|
|
208
263
|
logger.warn({ tool: toolName, size, maxResponseChars }, "response too large");
|
|
209
264
|
return formatTm1ErrorResult(new TM1Error({
|
|
210
265
|
code: TM1ErrorCode.RESPONSE_TOO_LARGE,
|