@ffschrattenecker/tm1-mcp-server 7.0.1 → 8.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 +102 -1
- package/README.md +5 -5
- package/dist/config.js +34 -17
- package/dist/connections.d.ts +18 -1
- package/dist/connections.js +34 -18
- package/dist/http-transport.js +51 -4
- package/dist/lib/callgraph/referenceIndex.js +2 -1
- package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
- package/dist/lib/callgraph/rulesLinter.js +0 -590
- package/dist/lib/callgraph/tiParser.js +10 -5
- package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
- package/dist/lib/callgraph/tm1-adapter.js +41 -9
- package/dist/lib/callgraph/variableEnv.js +3 -2
- package/dist/lib/cell-address.d.ts +11 -1
- package/dist/lib/cell-address.js +22 -3
- package/dist/lib/complexity/antipatterns.js +3 -1
- package/dist/lib/complexity/comment-classifier.js +2 -1
- package/dist/lib/coordinate-error.js +5 -1
- package/dist/lib/feeders/element-type-cache.js +4 -6
- package/dist/lib/naming/odata-filter.js +2 -1
- package/dist/lib/pro-parser.js +13 -4
- package/dist/lib/safe-regex.d.ts +0 -13
- package/dist/lib/safe-regex.js +40 -13
- package/dist/lib/sample-cells.d.ts +6 -0
- package/dist/lib/sample-cells.js +20 -2
- package/dist/lib/ti-identifier.d.ts +11 -0
- package/dist/lib/ti-identifier.js +11 -0
- package/dist/lib/tm1-name.d.ts +9 -0
- package/dist/lib/tm1-name.js +9 -0
- package/dist/lib/v12-compat/deprecated-ti.js +2 -2
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +24 -0
- package/dist/tm1-client/connection/profile.js +2 -4
- package/dist/tm1-client/dispatcher.d.ts +1 -1
- package/dist/tm1-client/dispatcher.js +18 -6
- package/dist/tm1-client/http.d.ts +21 -0
- package/dist/tm1-client/http.js +39 -30
- package/dist/tm1-client/services/cell-service.d.ts +9 -7
- package/dist/tm1-client/services/cell-service.js +54 -25
- package/dist/tm1-client/services/chore-service.js +8 -9
- package/dist/tm1-client/services/cube-service.d.ts +14 -18
- package/dist/tm1-client/services/cube-service.js +51 -45
- package/dist/tm1-client/services/dimension-order.d.ts +13 -0
- package/dist/tm1-client/services/dimension-order.js +42 -0
- package/dist/tm1-client/services/dimension-service.js +4 -6
- package/dist/tm1-client/services/element-service.d.ts +35 -11
- package/dist/tm1-client/services/element-service.js +124 -60
- package/dist/tm1-client/services/file-service.d.ts +42 -6
- package/dist/tm1-client/services/file-service.js +225 -28
- package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
- package/dist/tm1-client/services/hierarchy-service.js +118 -12
- package/dist/tm1-client/services/monitoring-service.js +2 -5
- package/dist/tm1-client/services/odata-page.d.ts +6 -0
- package/dist/tm1-client/services/odata-page.js +8 -0
- package/dist/tm1-client/services/process-service.d.ts +1 -1
- package/dist/tm1-client/services/process-service.js +26 -26
- package/dist/tm1-client/services/security-service.js +8 -9
- package/dist/tm1-client/services/server-service.js +14 -15
- package/dist/tm1-client/services/subset-service.d.ts +23 -10
- package/dist/tm1-client/services/subset-service.js +82 -26
- package/dist/tm1-client/services/view-service.js +12 -13
- package/dist/tm1-client.js +5 -2
- package/dist/tools/analysis/check-v12-readiness.js +3 -3
- package/dist/tools/celldata/check-feeders.js +6 -6
- package/dist/tools/celldata/check-writable-coords.js +43 -25
- package/dist/tools/celldata/execute-mdx.js +19 -23
- package/dist/tools/celldata/get-cell-value.js +1 -1
- package/dist/tools/celldata/get-view.js +10 -9
- package/dist/tools/celldata/member-ref.d.ts +15 -0
- package/dist/tools/celldata/member-ref.js +67 -0
- package/dist/tools/celldata/sample-cells.js +3 -0
- package/dist/tools/celldata/trace-cell-calculation.js +6 -6
- package/dist/tools/celldata/trace-feeders.js +6 -6
- package/dist/tools/celldata/write-cells.js +62 -12
- package/dist/tools/dimension-management/create-element-attribute.js +1 -1
- package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
- package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
- package/dist/tools/dimension-management/list-element-attributes.js +1 -1
- package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
- package/dist/tools/dimension-management/update-element.js +14 -7
- package/dist/tools/fileops/container.d.ts +8 -0
- package/dist/tools/fileops/container.js +11 -0
- package/dist/tools/fileops/delete-file.js +4 -2
- package/dist/tools/fileops/get-file-content.js +45 -7
- package/dist/tools/fileops/list-files.js +5 -2
- package/dist/tools/fileops/search-files.js +4 -1
- package/dist/tools/fileops/upload-file.js +5 -2
- package/dist/tools/format.d.ts +1 -0
- package/dist/tools/format.js +7 -2
- package/dist/tools/index.js +0 -2
- package/dist/tools/model-building/check-cube-rule.js +3 -2
- package/dist/tools/model-building/clear-cube.js +35 -26
- package/dist/tools/model-building/set-cube-rules.js +13 -20
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-cube-stats.js +1 -1
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-error-logs.js +27 -2
- package/dist/tools/schemas/items-fileops.d.ts +4 -0
- package/dist/tools/schemas/items-fileops.js +1 -0
- package/dist/tools/security/list-clients.js +5 -5
- package/dist/tools/security/list-groups.js +1 -1
- package/dist/tools/subsets/create-subset.js +9 -5
- package/dist/tools/subsets/delete-subset.js +8 -3
- package/dist/tools/subsets/update-subset.js +9 -9
- package/dist/tools/ti-development/diff-process-with-file.js +4 -29
- package/dist/tools/ti-development/diff-processes.js +9 -0
- package/dist/tools/ti-development/export-process-to-git.js +9 -7
- package/dist/tools/ti-development/export-process-to-pro.js +15 -8
- package/dist/tools/ti-development/import-pro-file.js +10 -21
- package/dist/tools/ti-development/import-process-from-git.js +8 -18
- package/dist/tools/ti-development/install-pro-bundle.js +2 -2
- package/dist/tools/ti-development/upsert-process.js +26 -5
- package/npm-shrinkwrap.json +465 -500
- package/package.json +14 -10
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
2
3
|
import { actionResponse } from "../format.js";
|
|
3
4
|
import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
4
5
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
@@ -7,40 +8,48 @@ import { defineTool } from "../define-tool.js";
|
|
|
7
8
|
export const registerClearCube = defineTool({
|
|
8
9
|
name: "tm1_clear_cube",
|
|
9
10
|
description: [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
11
|
+
"Wipe every cell in a cube. There is no region or slice option — the server offers no selective clear, so this always empties the whole cube.",
|
|
12
|
+
"FINAL — treat the data as gone. TM1 has no undo for it, and there is no dependable way back: any SaveDataAll or CubeSaveData that follows writes the empty cube to disk. Cleared cells read as zero/empty.",
|
|
13
|
+
"Prefer TI for reproducible loads — this is for ad-hoc resets. To empty part of a cube, run a TI process with tm1_execute_process instead.",
|
|
14
|
+
"Safety: pass confirm=<cube name verbatim>. Mismatched confirm rejects the call.",
|
|
15
|
+
"Before: tm1_get_cube_stats to see how much data you are about to wipe.",
|
|
14
16
|
],
|
|
15
17
|
annotations: DESTRUCTIVE,
|
|
16
18
|
output: MutationResultSchema,
|
|
17
19
|
input: {
|
|
18
|
-
cubeName: z.string().describe("Cube to
|
|
19
|
-
|
|
20
|
+
cubeName: z.string().describe("Cube to empty completely"),
|
|
21
|
+
...CONFIRM_SCHEMA,
|
|
22
|
+
timeoutMs: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(1000)
|
|
26
|
+
.max(3600000)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Override the default 30s request timeout (ms, 1000–3600000). A clear that outlasts it keeps running on the server and still empties the cube; the call then reports a timeout instead of success."),
|
|
29
|
+
// Removed in 4.0.0, still DECLARED so a stored old call is refused instead
|
|
30
|
+
// of silently widened: the SDK strips properties the schema does not
|
|
31
|
+
// mention, so an undeclared field reaches the handler as if it was never
|
|
32
|
+
// sent — and a region clear would become a full wipe.
|
|
33
|
+
dimensions: z
|
|
34
|
+
.array(z.string())
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("REMOVED — passing this fails the call. Do not send it."),
|
|
20
37
|
tuples: z
|
|
21
38
|
.array(z.array(z.string()))
|
|
22
|
-
.
|
|
23
|
-
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("REMOVED — passing this fails the call. Do not send it."),
|
|
24
41
|
},
|
|
25
|
-
handler: async ({ cubeName, dimensions, tuples
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
type: "text",
|
|
33
|
-
text: `dimensions (${dimensions.length}) and tuples (${tuples.length}) must have the same length.`,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
};
|
|
42
|
+
handler: async ({ cubeName, confirm, timeoutMs, dimensions, tuples }, tm1Client) => {
|
|
43
|
+
if (dimensions !== undefined || tuples !== undefined) {
|
|
44
|
+
throw new TM1Error({
|
|
45
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
46
|
+
message: "tm1_clear_cube no longer takes `dimensions`/`tuples`, and this call was NOT executed. Sending them once meant a region clear, which the server never supported; ignoring them here would empty the whole cube instead.",
|
|
47
|
+
hint: "To empty the whole cube, repeat the call with cubeName and confirm only. To empty part of it, run a TI process with CubeClearData()/view-based logic via tm1_execute_process.",
|
|
48
|
+
});
|
|
37
49
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.map((d, i) => `${d}=${tuples[i].length === 0 ? "*" : tuples[i].join("|")}`)
|
|
42
|
-
.join(", ");
|
|
43
|
-
return actionResponse({ success: true, cubeName, summary });
|
|
50
|
+
requireConfirm(confirm, cubeName, "cube");
|
|
51
|
+
await tm1Client.cubes.clear(cubeName, timeoutMs !== undefined ? { timeoutMs } : undefined);
|
|
52
|
+
return actionResponse({ success: true, cubeName, summary: "all cells" });
|
|
44
53
|
},
|
|
45
54
|
});
|
|
46
55
|
//# sourceMappingURL=clear-cube.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { invalidateCallgraphCache } from "../../lib/callgraph/tm1-adapter.js";
|
|
3
|
-
import { TM1ErrorCode } from "../../types.js";
|
|
3
|
+
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
4
4
|
import { withToolHint } from "../error-format.js";
|
|
5
5
|
import { actionResponse } from "../format.js";
|
|
6
6
|
import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
@@ -14,7 +14,8 @@ export const registerSetCubeRules = defineTool({
|
|
|
14
14
|
"Create or replace the rules for a TM1 cube.",
|
|
15
15
|
"SKIPCHECK; belongs at the top and FEEDERS; before all feeder definitions — SKIPCHECK is what makes feeders take effect, so rules with feeders need it.",
|
|
16
16
|
"Pass exactly one source: rules (the full text — replaces everything), edits (find/replace patch against the current text; each find must match exactly once, else nothing is written), or filePath (full text from a host file under TM1_LOCAL_FILE_ROOT).",
|
|
17
|
-
"The full resulting text is syntax-checked before anything is written
|
|
17
|
+
"The full resulting text is syntax-checked with tm1.CheckRules before anything is written: TM1 itself stores broken rules without an error, and they then silently compute nothing. Any error aborts the call with VALIDATION_ERROR, the errors with their line numbers in details, and nothing is written; preflight:false skips the check.",
|
|
18
|
+
"The stored text is read back after writing (verified.textMatches), so no separate tm1_get_cube_rules is needed; the callgraph cache is dropped automatically.",
|
|
18
19
|
],
|
|
19
20
|
annotations: IDEMPOTENT_DESTRUCTIVE,
|
|
20
21
|
output: MutationResultSchema,
|
|
@@ -45,23 +46,15 @@ export const registerSetCubeRules = defineTool({
|
|
|
45
46
|
if (preflight) {
|
|
46
47
|
const errors = await tm1Client.cubes.checkRule(cubeName, text);
|
|
47
48
|
if (errors.length > 0) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: `Preflight rule check failed: ${errors.length} error(s). Nothing was written.`,
|
|
58
|
-
hint: "Fix the lines in errors[] (lineNumber is in the full resulting text, after edits are applied). preflight:false skips the check; TM1 would store the broken text.",
|
|
59
|
-
errors,
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
isError: true,
|
|
64
|
-
};
|
|
49
|
+
throw new TM1Error({
|
|
50
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
51
|
+
message: `Rules for '${cubeName}' have ${errors.length} syntax error(s): ${errors
|
|
52
|
+
.slice(0, 5)
|
|
53
|
+
.map((e) => `line ${e.lineNumber ?? "?"}: ${e.message.trim()}`)
|
|
54
|
+
.join("; ")}. Nothing was written.`,
|
|
55
|
+
hint: "Fix the reported lines and retry (line numbers are in the full resulting text, after edits are applied). preflight:false writes the text anyway; TM1 stores it, but the broken statements compute nothing.",
|
|
56
|
+
details: JSON.stringify({ stage: "preflight", errors }),
|
|
57
|
+
});
|
|
65
58
|
}
|
|
66
59
|
}
|
|
67
60
|
await withToolHint(tm1Client.cubes.updateRules(cubeName, text), `Inspect details for the offending line.`);
|
|
@@ -78,7 +71,7 @@ export const registerSetCubeRules = defineTool({
|
|
|
78
71
|
}
|
|
79
72
|
const lineCount = text.split("\n").length;
|
|
80
73
|
// Rule changes shift call edges (DB(), feeders) — drop callgraph TTL early.
|
|
81
|
-
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache();
|
|
74
|
+
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache(tm1Client.connectionId);
|
|
82
75
|
return actionResponse({
|
|
83
76
|
success: true,
|
|
84
77
|
cubeName,
|
|
@@ -5,7 +5,7 @@ import { MutationResultSchema } from "../schemas/items.js";
|
|
|
5
5
|
import { defineTool } from "../define-tool.js";
|
|
6
6
|
export const registerUnloadCube = defineTool({
|
|
7
7
|
name: "tm1_unload_cube",
|
|
8
|
-
description: "Unload a cube from memory. TM1 discards the in-memory fed-cell index and reloads from
|
|
8
|
+
description: "Unload a cube from memory. TM1 saves the cube to disk first, then discards the in-memory fed-cell index and reloads from .cub on next access. Two reasons to call it: after feeder corrections, since the fed-cell index is cumulative and changes to existing feeders only take effect once the cube reloads; and to give the memory the cube occupies back to the server. TM1 v11 only.",
|
|
9
9
|
annotations: DESTRUCTIVE,
|
|
10
10
|
// v11 only. v12 answers tm1.Unload with "Demand load, loading and unloading
|
|
11
11
|
// of cubes is no longer supported." — the feature is gone, with no successor
|
|
@@ -12,7 +12,7 @@ export const registerGetCubeStats = defineTool({
|
|
|
12
12
|
"Read }StatsByCube metrics for one or more cubes (memory, populated cells, fed cells, feeder efficiency).",
|
|
13
13
|
"Well-known metrics are mapped to typed fields; the full element-name → value map is also returned under `raw` so server-side renames don't break the tool.",
|
|
14
14
|
"Per-cube errors are reported as items[].error without failing the whole call.",
|
|
15
|
-
"
|
|
15
|
+
"The }Stats* control cubes are not always there: v12 has none at all, and v11 keeps them only while statistics collection is switched on. That case, and an account not allowed to read them, return `statsUnavailable` {reason: absent|denied} instead of a raw error.",
|
|
16
16
|
],
|
|
17
17
|
annotations: READ_ONLY,
|
|
18
18
|
output: CubeStatsResultSchema,
|
|
@@ -5,7 +5,7 @@ import { TransactionLogEntrySchema } from "../schemas/items.js";
|
|
|
5
5
|
import { defineTool } from "../define-tool.js";
|
|
6
6
|
export const registerGetTransactionLog = defineTool({
|
|
7
7
|
name: "tm1_get_transaction_log",
|
|
8
|
-
description: "Fetch recent TM1 transaction log entries (cell writes), newest first. Optional filters: cube, user, and a since/until time range. NOTE: the endpoint scans the log server-side and a full scan can take minutes-to-hours. A cheap preflight probe fails fast on unreachable/no-rights; without `since` the server walks expanding time windows backward (10min→1y) and stops once `top` rows are found, so
|
|
8
|
+
description: "Fetch recent TM1 transaction log entries (cell writes), newest first. Optional filters: cube, user, and a since/until time range. NOTE: the endpoint scans the log server-side and a full scan can take minutes-to-hours. A cheap preflight probe fails fast on unreachable/no-rights; without `since` the server walks expanding time windows backward (10min→1y) and stops once `top` rows are found, so the lookback is bounded at one year rather than open-ended — a sparse log still walks all nine windows before it gives up. Pass since/until (from-to) to bound it explicitly. (v11 only)",
|
|
9
9
|
annotations: withVersion(READ_ONLY, "v11"),
|
|
10
10
|
// v12 deprecated TransactionLogEntry/TransactionLogEntries (and the
|
|
11
11
|
// TransactionLog / TailTransactionLog functions) in 12.0.0; all of them serve
|
|
@@ -8,11 +8,16 @@ import { pageShapeFor } from "../schemas/common.js";
|
|
|
8
8
|
// Best-effort extraction of {process, ts} from an error-log filename.
|
|
9
9
|
// Two known patterns (see server-service.listErrorLogFiles):
|
|
10
10
|
// modern v11: TM1ProcessError_<ts>_<id>_<proc>(_<hash>)?.log
|
|
11
|
+
// v12: ProcessLog_<ts>_<id>_<proc>.jsonl
|
|
11
12
|
// legacy: <proc>_<ts>.log
|
|
12
13
|
// Process names may contain underscores, so the modern parse greedily captures
|
|
13
14
|
// the tail and strips a trailing session-hash token (_<hex6+>). This is a
|
|
14
15
|
// heuristic — good enough for audit triage, not a guaranteed exact split.
|
|
15
16
|
export function parseLogName(filename) {
|
|
17
|
+
// v12 names carry no session hash, so nothing to strip.
|
|
18
|
+
const v12 = filename.match(/^ProcessLog_(\d{14})_\d+_(.+)\.jsonl$/i);
|
|
19
|
+
if (v12)
|
|
20
|
+
return { ts: v12[1], process: v12[2] };
|
|
16
21
|
const modern = filename.match(/^TM1ProcessError_(\d{14})_\d+_(.+)\.log$/i);
|
|
17
22
|
if (modern) {
|
|
18
23
|
// Strip the trailing TM1 session-hash token. Real-world v11 hashes are
|
|
@@ -62,12 +67,32 @@ export const registerListErrorLogs = defineTool({
|
|
|
62
67
|
"groupBy='process' returns a per-process audit summary instead of individual files.",
|
|
63
68
|
],
|
|
64
69
|
annotations: READ_ONLY,
|
|
65
|
-
output:
|
|
70
|
+
output: {
|
|
71
|
+
// Set only on the groupBy='process' branch, which wraps the page in the
|
|
72
|
+
// filters it applied and the totals before paging.
|
|
73
|
+
groupBy: z.literal("process").optional().describe("Echoes groupBy"),
|
|
74
|
+
processName: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Echoes the processName filter"),
|
|
78
|
+
since: z.string().optional().describe("Echoes the since filter"),
|
|
79
|
+
totalFiles: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Log files aggregated into the groups"),
|
|
84
|
+
groupCount: z
|
|
85
|
+
.number()
|
|
86
|
+
.int()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Distinct processes before paging"),
|
|
89
|
+
...pageShapeFor(z.union([ErrorLogFileSchema, ErrorLogGroupSchema])),
|
|
90
|
+
},
|
|
66
91
|
input: {
|
|
67
92
|
processName: z
|
|
68
93
|
.string()
|
|
69
94
|
.optional()
|
|
70
|
-
.describe("Optional process-name filter — matches
|
|
95
|
+
.describe("Optional process-name filter — matches modern v11 'TM1ProcessError_<ts>_<id>_<processName>_<hash>.log', v12 'ProcessLog_<ts>_<id>_<processName>.jsonl' and legacy '<processName>_<ts>.log' filename patterns."),
|
|
71
96
|
since: z
|
|
72
97
|
.string()
|
|
73
98
|
.optional()
|
|
@@ -5,6 +5,10 @@ export declare const FileContentResultSchema: z.ZodObject<{
|
|
|
5
5
|
returnedBytes: z.ZodNumber;
|
|
6
6
|
truncated: z.ZodBoolean;
|
|
7
7
|
truncationReason: z.ZodOptional<z.ZodString>;
|
|
8
|
+
encoding: z.ZodEnum<{
|
|
9
|
+
base64: "base64";
|
|
10
|
+
text: "text";
|
|
11
|
+
}>;
|
|
8
12
|
content: z.ZodString;
|
|
9
13
|
}, z.core.$strip>;
|
|
10
14
|
export declare const FilenameItemSchema: z.ZodString;
|
|
@@ -6,6 +6,7 @@ export const FileContentResultSchema = z.object({
|
|
|
6
6
|
returnedBytes: z.number().int(),
|
|
7
7
|
truncated: z.boolean(),
|
|
8
8
|
truncationReason: z.string().optional(),
|
|
9
|
+
encoding: z.enum(["text", "base64"]),
|
|
9
10
|
content: z.string(),
|
|
10
11
|
});
|
|
11
12
|
// listFiles returns bare strings (file/folder names).
|
|
@@ -30,9 +30,9 @@ function project(clients, fields, groupCount) {
|
|
|
30
30
|
want.delete("groups");
|
|
31
31
|
}
|
|
32
32
|
return clients.map((c) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Name is the item key and required by ClientSchema, so every projection
|
|
34
|
+
// carries it — fields=['type'] without it failed the output schema.
|
|
35
|
+
const out = { Name: c.Name };
|
|
36
36
|
if (want.has("friendlyName") && c.FriendlyName !== undefined)
|
|
37
37
|
out.FriendlyName = c.FriendlyName;
|
|
38
38
|
if (want.has("type") && c.Type !== undefined)
|
|
@@ -57,7 +57,7 @@ export const registerListClients = defineTool({
|
|
|
57
57
|
fields: z
|
|
58
58
|
.array(z.enum(FIELD_KEYS))
|
|
59
59
|
.optional()
|
|
60
|
-
.describe("Projection: subset of ['name','friendlyName','type','enabled','groups','groupCount']. Omit for full default payload."),
|
|
60
|
+
.describe("Projection: subset of ['name','friendlyName','type','enabled','groups','groupCount']. Name is always included. Omit for full default payload."),
|
|
61
61
|
groupCount: z
|
|
62
62
|
.boolean()
|
|
63
63
|
.optional()
|
|
@@ -77,7 +77,7 @@ export const registerListClients = defineTool({
|
|
|
77
77
|
header: "Groups",
|
|
78
78
|
get: (c) => c.groupCount !== undefined
|
|
79
79
|
? `${c.groupCount} (count)`
|
|
80
|
-
: (c.Groups ?? []).join(", "),
|
|
80
|
+
: (c.Groups ?? []).map((g) => g.Name).join(", "),
|
|
81
81
|
},
|
|
82
82
|
]);
|
|
83
83
|
return pageResponse(projectedPage, format, { title: "Clients", columns });
|
|
@@ -34,7 +34,7 @@ export const registerListGroups = defineTool({
|
|
|
34
34
|
header: "Clients",
|
|
35
35
|
get: (g) => "clientCount" in g
|
|
36
36
|
? `${g.clientCount} (count)`
|
|
37
|
-
: (g.Clients ?? []).join(", "),
|
|
37
|
+
: (g.Clients ?? []).map((c) => c.Name).join(", "),
|
|
38
38
|
},
|
|
39
39
|
]);
|
|
40
40
|
return pageResponse(projectedPage, format, { title: "Groups", columns });
|
|
@@ -6,7 +6,7 @@ import { defineTool } from "../define-tool.js";
|
|
|
6
6
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
7
7
|
export const registerCreateSubset = defineTool({
|
|
8
8
|
name: "tm1_create_subset",
|
|
9
|
-
description: "Create a public
|
|
9
|
+
description: "Create a TM1 subset, public by default or private with isPrivate=true (private subsets belong to the signed-in user and are invisible to others; the same name may exist once public and once private). Provide either expression (MDX-based, dynamic) OR elements (static list) — not both. Optional alias attribute name controls the displayed alias.",
|
|
10
10
|
annotations: WRITE,
|
|
11
11
|
output: MutationResultSchema,
|
|
12
12
|
input: {
|
|
@@ -25,15 +25,19 @@ export const registerCreateSubset = defineTool({
|
|
|
25
25
|
.string()
|
|
26
26
|
.optional()
|
|
27
27
|
.describe("Alias attribute used as display name in the subset"),
|
|
28
|
+
isPrivate: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
28
33
|
},
|
|
29
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias }, tm1Client) => {
|
|
30
|
-
|
|
31
|
-
await tm1Client.subsets.create(dimensionName, hierarchy, {
|
|
34
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias, isPrivate, }, tm1Client) => {
|
|
35
|
+
await tm1Client.subsets.create(dimensionName, resolveHierarchy(dimensionName, hierarchyName), {
|
|
32
36
|
name: subsetName,
|
|
33
37
|
expression,
|
|
34
38
|
elements,
|
|
35
39
|
alias,
|
|
36
|
-
});
|
|
40
|
+
}, isPrivate ?? false);
|
|
37
41
|
return actionResponse({
|
|
38
42
|
success: true,
|
|
39
43
|
subsetName,
|
|
@@ -7,19 +7,24 @@ import { defineTool } from "../define-tool.js";
|
|
|
7
7
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
8
8
|
export const registerDeleteSubset = defineTool({
|
|
9
9
|
name: "tm1_delete_subset",
|
|
10
|
-
description: "Delete a public
|
|
10
|
+
description: "Delete a TM1 subset, public by default or private with isPrivate=true. TM1 refuses to delete a subset a view still uses (SubsetIsBeingUsedByView) — change it with tm1_update_subset instead. 404 if not found. Irreversible — pass confirm=<subset name verbatim>.",
|
|
11
11
|
annotations: DESTRUCTIVE,
|
|
12
12
|
output: MutationResultSchema,
|
|
13
13
|
input: {
|
|
14
14
|
dimensionName: z.string().describe("Dimension name"),
|
|
15
15
|
...HIERARCHY_NAME_OPTIONAL,
|
|
16
16
|
subsetName: z.string().describe("Subset to delete"),
|
|
17
|
+
isPrivate: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
17
22
|
...CONFIRM_SCHEMA,
|
|
18
23
|
},
|
|
19
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, confirm }, tm1Client) => {
|
|
24
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, isPrivate, confirm }, tm1Client) => {
|
|
20
25
|
const hierarchy = resolveHierarchy(dimensionName, hierarchyName);
|
|
21
26
|
requireConfirm(confirm, subsetName, "subset");
|
|
22
|
-
await tm1Client.subsets.delete(dimensionName, hierarchy, subsetName);
|
|
27
|
+
await tm1Client.subsets.delete(dimensionName, hierarchy, subsetName, isPrivate ?? false);
|
|
23
28
|
return actionResponse({ success: true, subsetName });
|
|
24
29
|
},
|
|
25
30
|
});
|
|
@@ -6,7 +6,7 @@ import { defineTool } from "../define-tool.js";
|
|
|
6
6
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
7
7
|
export const registerUpdateSubset = defineTool({
|
|
8
8
|
name: "tm1_update_subset",
|
|
9
|
-
description: "Update
|
|
9
|
+
description: "Update an existing TM1 subset in place, public by default or private with isPrivate=true. The way to change a subset a view uses, since TM1 will not delete that one. Pass expression to replace the MDX, OR elements to replace the static list (order kept; an MDX subset becomes static; [] empties it) — not both. Pass alias to change the alias attribute. If the new list names an unknown element, TM1 refuses it and the old definition is written back.",
|
|
10
10
|
annotations: IDEMPOTENT_WRITE,
|
|
11
11
|
output: MutationResultSchema,
|
|
12
12
|
input: {
|
|
@@ -17,16 +17,16 @@ export const registerUpdateSubset = defineTool({
|
|
|
17
17
|
elements: z
|
|
18
18
|
.array(z.string())
|
|
19
19
|
.optional()
|
|
20
|
-
.describe("New static element list
|
|
20
|
+
.describe("New static element list; replaces the old one and turns an MDX subset static. Mutually exclusive with expression."),
|
|
21
21
|
alias: z.string().optional().describe("New alias attribute"),
|
|
22
|
+
isPrivate: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
22
27
|
},
|
|
23
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias }, tm1Client) => {
|
|
24
|
-
|
|
25
|
-
await tm1Client.subsets.update(dimensionName, hierarchy, subsetName, {
|
|
26
|
-
expression,
|
|
27
|
-
elements,
|
|
28
|
-
alias,
|
|
29
|
-
});
|
|
28
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias, isPrivate, }, tm1Client) => {
|
|
29
|
+
await tm1Client.subsets.update(dimensionName, resolveHierarchy(dimensionName, hierarchyName), subsetName, { expression, elements, alias }, isPrivate ?? false);
|
|
30
30
|
return actionResponse({ success: true, subsetName });
|
|
31
31
|
},
|
|
32
32
|
});
|
|
@@ -5,8 +5,9 @@ import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
|
5
5
|
import { parseProFile } from "../../lib/pro-parser.js";
|
|
6
6
|
import { ignoredColumnsOf } from "../../lib/variables-ui-data.js";
|
|
7
7
|
import { maskCode, resolveMaskSecrets } from "../../lib/mask-secrets.js";
|
|
8
|
+
import { diffDs } from "./diff-processes.js";
|
|
8
9
|
import { DiffProcessResultSchema } from "../schemas/items.js";
|
|
9
|
-
import { READ_ONLY
|
|
10
|
+
import { READ_ONLY } from "../annotations.js";
|
|
10
11
|
import { defineTool } from "../define-tool.js";
|
|
11
12
|
function tabDiff(name, installed, file) {
|
|
12
13
|
const norm = (s) => s.replace(/\r\n/g, "\n").trimEnd();
|
|
@@ -93,36 +94,10 @@ function diffIgnoredColumns(installed, file) {
|
|
|
93
94
|
renamed,
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
function diffDataSource(installed, file) {
|
|
97
|
-
const diffs = [];
|
|
98
|
-
if (installed.type !== file.type)
|
|
99
|
-
diffs.push(`type: ${installed.type} → ${file.type}`);
|
|
100
|
-
const fields = [
|
|
101
|
-
"dataSourceNameForServer",
|
|
102
|
-
"dataSourceNameForClient",
|
|
103
|
-
"asciiDelimiterChar",
|
|
104
|
-
"asciiQuoteCharacter",
|
|
105
|
-
"asciiDecimalSeparator",
|
|
106
|
-
"asciiThousandSeparator",
|
|
107
|
-
"asciiHeaderRecords",
|
|
108
|
-
"view",
|
|
109
|
-
"subset",
|
|
110
|
-
"userName",
|
|
111
|
-
// See diff-processes.ts: the ODBC query is substance, the password is noise.
|
|
112
|
-
"query",
|
|
113
|
-
];
|
|
114
|
-
for (const f of fields) {
|
|
115
|
-
const a = installed[f];
|
|
116
|
-
const b = file[f];
|
|
117
|
-
if ((a ?? "") !== (b ?? ""))
|
|
118
|
-
diffs.push(`${String(f)}: ${JSON.stringify(a)} → ${JSON.stringify(b)}`);
|
|
119
|
-
}
|
|
120
|
-
return { identical: diffs.length === 0, differences: diffs };
|
|
121
|
-
}
|
|
122
97
|
export const registerDiffProcessWithFile = defineTool({
|
|
123
98
|
name: "tm1_diff_process_with_file",
|
|
124
99
|
description: "Compare an installed TI process on the server against a local .pro file. Returns per-tab identical flags + line counts, parameter diff (added/removed/changed), variable diff, and datasource diff. Use before tm1_import_pro_file to preview what will change.",
|
|
125
|
-
annotations:
|
|
100
|
+
annotations: READ_ONLY,
|
|
126
101
|
output: DiffProcessResultSchema,
|
|
127
102
|
input: {
|
|
128
103
|
filePath: z
|
|
@@ -177,7 +152,7 @@ export const registerDiffProcessWithFile = defineTool({
|
|
|
177
152
|
const params = diffParams(installedParams, parsed.parameters);
|
|
178
153
|
const variables = diffVars(installedLayout.variables, parsed.variables);
|
|
179
154
|
const ignoredColumns = diffIgnoredColumns(installedLayout.ignoredColumns, ignoredColumnsOf(parsed.variablesUIData));
|
|
180
|
-
const dataSource =
|
|
155
|
+
const dataSource = diffDs(installedDs, parsed.dataSource);
|
|
181
156
|
const allIdentical = tabs.every((t) => t.identical) &&
|
|
182
157
|
params.added.length === 0 &&
|
|
183
158
|
params.removed.length === 0 &&
|
|
@@ -204,6 +204,7 @@ function diffIgnoredColumns(a, b) {
|
|
|
204
204
|
renamed,
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
+
// Shared with tm1_diff_process_with_file so both diffs check the same fields.
|
|
207
208
|
export function diffDs(a, b) {
|
|
208
209
|
const diffs = [];
|
|
209
210
|
const fields = [
|
|
@@ -227,6 +228,14 @@ export function diffDs(a, b) {
|
|
|
227
228
|
if ((a[f] ?? "") !== (b[f] ?? ""))
|
|
228
229
|
diffs.push(`${String(f)}: ${JSON.stringify(a[f])} → ${JSON.stringify(b[f])}`);
|
|
229
230
|
}
|
|
231
|
+
// Compared with their defaults filled in: the .pro parser sets the
|
|
232
|
+
// delimiter type only on ASCII sources and v12 never returns usesUnicode,
|
|
233
|
+
// so a bare comparison would report a difference nobody made.
|
|
234
|
+
const delimiter = (d) => d.type === "ASCII" ? (d.asciiDelimiterType ?? "Character") : undefined;
|
|
235
|
+
if (delimiter(a) !== delimiter(b))
|
|
236
|
+
diffs.push(`asciiDelimiterType: ${JSON.stringify(delimiter(a))} → ${JSON.stringify(delimiter(b))}`);
|
|
237
|
+
if ((a.usesUnicode ?? false) !== (b.usesUnicode ?? false))
|
|
238
|
+
diffs.push(`usesUnicode: ${JSON.stringify(a.usesUnicode ?? false)} → ${JSON.stringify(b.usesUnicode ?? false)}`);
|
|
230
239
|
return { identical: diffs.length === 0, differences: diffs };
|
|
231
240
|
}
|
|
232
241
|
// ── tool ──────────────────────────────────────────────────────────────────────
|
|
@@ -14,8 +14,8 @@ export const registerExportProcessToGit = defineTool({
|
|
|
14
14
|
description: [
|
|
15
15
|
"Serialize a TM1 process to the tm1-git two-file layout: a '{name}.json' (parameters, variables, ignored datasource columns, datasource) plus a '{name}.ti' (Prolog/Metadata/Data/Epilog as plain code).",
|
|
16
16
|
"The .ti holds the code in TM1's native `Code` representation (#region <Tab> / #endregion, CRLF, empty tabs omitted); the .json holds the structure. Code lives outside the JSON so Git diffs stay readable.",
|
|
17
|
-
"Returns both file bodies (json + ti) inline by default. Pass writeToDir to persist them to disk instead: the code is then written to files and omitted from the response to avoid duplicating it into the context window; only metadata (filenames, counts, writtenTo paths) comes back.
|
|
18
|
-
"Security: the ODBC datasource password is stripped unless includeDataSourcePassword is set (which also requires writeToDir); credential literals in the TI code are masked when maskSecrets is on; credentialsOmitted=true flags when a password was stripped.",
|
|
17
|
+
"Returns both file bodies (json + ti) inline by default. Pass writeToDir to persist them to disk instead: the code is then written to files and omitted from the response to avoid duplicating it into the context window; only metadata (filenames, counts, writtenTo paths) comes back. Files written via writeToDir are round-trip safe with tm1_import_process_from_git; the inline .ti has credential literals masked by default and is for reading, not re-importing.",
|
|
18
|
+
"Security: the ODBC datasource password is stripped unless includeDataSourcePassword is set (which also requires writeToDir); credential literals in the TI code are masked in the inline response when maskSecrets is on, never in written files (those must re-import intact); credentialsOmitted=true flags when a password was stripped.",
|
|
19
19
|
"includeDataSourcePassword is v12-only: what v11 hands out expires with the server run, so exporting it would produce a file that looks complete and fails later.",
|
|
20
20
|
],
|
|
21
21
|
annotations: READ_ONLY,
|
|
@@ -30,8 +30,7 @@ export const registerExportProcessToGit = defineTool({
|
|
|
30
30
|
.boolean()
|
|
31
31
|
.optional()
|
|
32
32
|
.default(true)
|
|
33
|
-
.describe("Redact credential literals in the
|
|
34
|
-
"Masks the password arg of ODBCOpen() and quoted values assigned to credential-named identifiers (pPwd, sToken, …). Default: true. Set false only when explicitly auditing credentials."),
|
|
33
|
+
.describe("Redact credential literals in the .ti code returned INLINE (password arg of ODBCOpen(), quoted values assigned to credential-named identifiers such as pPwd or sToken). Default: true. Files written via writeToDir always hold the code unmasked, so they re-import intact — treat them as containing any password literal the code has."),
|
|
35
34
|
includeDataSourcePassword: z
|
|
36
35
|
.boolean()
|
|
37
36
|
.optional()
|
|
@@ -57,8 +56,11 @@ export const registerExportProcessToGit = defineTool({
|
|
|
57
56
|
const pair = await readProcessAsGit(tm1Client, processName, {
|
|
58
57
|
includePassword: includeDataSourcePassword === true,
|
|
59
58
|
});
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// The file is the deployable copy and gets the code as it is: a masked
|
|
60
|
+
// literal would re-import as broken code ('***' instead of the password).
|
|
61
|
+
// Masking applies to what goes back to the model — the inline body.
|
|
62
|
+
const ti = pair.ti;
|
|
63
|
+
const inlineTi = resolveMaskSecrets(maskSecrets) ? maskCode(ti) : ti;
|
|
62
64
|
const { json, credentialsOmitted } = pair;
|
|
63
65
|
const jsonFileName = `${processName}.json`;
|
|
64
66
|
const tiFileName = `${processName}.ti`;
|
|
@@ -103,7 +105,7 @@ export const registerExportProcessToGit = defineTool({
|
|
|
103
105
|
// Echo the file bodies inline only when NOT persisting to disk. With
|
|
104
106
|
// writeToDir the caller already has the files, so returning the code
|
|
105
107
|
// would just duplicate thousands of tokens into the context window.
|
|
106
|
-
...(writeToDir ? {} : { json, ti }),
|
|
108
|
+
...(writeToDir ? {} : { json, ti: inlineTi }),
|
|
107
109
|
}),
|
|
108
110
|
},
|
|
109
111
|
],
|
|
@@ -7,7 +7,7 @@ import { maskCode, resolveMaskSecrets } from "../../lib/mask-secrets.js";
|
|
|
7
7
|
import { supportsCredentialExport } from "../../lib/credential-format.js";
|
|
8
8
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
9
9
|
import { ExportProcessToProResultSchema } from "../schemas/items.js";
|
|
10
|
-
import { READ_ONLY
|
|
10
|
+
import { READ_ONLY } from "../annotations.js";
|
|
11
11
|
import { defineTool } from "../define-tool.js";
|
|
12
12
|
export const registerExportProcessToPro = defineTool({
|
|
13
13
|
name: "tm1_export_process_to_pro",
|
|
@@ -15,10 +15,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
15
15
|
"Reverse of tm1_import_pro_file: serialize a TM1 process back to a .pro file body.",
|
|
16
16
|
"Fetches code (Prolog/Metadata/Data/Epilog), parameters, variables (including the columns set to Ignore), and datasource in parallel.",
|
|
17
17
|
"Returns the .pro content inline by default; pass writeToFile to also persist to an absolute path on the MCP host.",
|
|
18
|
-
"
|
|
18
|
+
"A file written via writeToFile is round-trip safe with tm1_import_pro_file — useful for syncing live server state into a Git repo. The inline copy has credential literals masked by default and is for reading, not re-importing.",
|
|
19
19
|
"NOT a drop-in replacement for the .pro file in TM1's Datadir: the output omits TM1's BOM, its '601' version header and CRLF line endings. Measured on 11.8: TM1 does load such a file at startup and rewrites it in its own dialect, but it decodes slot 565 with its own scheme — a password written here becomes garbage that TM1 then persists, so the process looks configured and fails at runtime. Deploy via tm1_import_pro_file, not by copying into the Datadir.",
|
|
20
20
|
],
|
|
21
|
-
annotations:
|
|
21
|
+
annotations: READ_ONLY,
|
|
22
22
|
output: ExportProcessToProResultSchema,
|
|
23
23
|
input: {
|
|
24
24
|
processName: z.string().describe("Name of the TI process to export"),
|
|
@@ -30,8 +30,9 @@ export const registerExportProcessToPro = defineTool({
|
|
|
30
30
|
.boolean()
|
|
31
31
|
.optional()
|
|
32
32
|
.default(true)
|
|
33
|
-
.describe("Redact credential literals in the
|
|
34
|
-
"
|
|
33
|
+
.describe("Redact credential literals in the code returned INLINE (password arg of ODBCOpen(), quoted values assigned to credential-named " +
|
|
34
|
+
"identifiers such as pPwd or sToken). Default: true. A file written via writeToFile always holds the code unmasked, so it " +
|
|
35
|
+
"re-imports intact — treat it as containing any password literal the code has."),
|
|
35
36
|
includeDataSourcePassword: z
|
|
36
37
|
.boolean()
|
|
37
38
|
.optional()
|
|
@@ -70,8 +71,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
70
71
|
if (!includeDataSourcePassword)
|
|
71
72
|
delete dataSource.password;
|
|
72
73
|
const credentialsIncluded = Boolean(includeDataSourcePassword && dataSource.password);
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// The file is the deployable copy and gets the code as it is: a masked
|
|
75
|
+
// literal would re-import as broken code ('***' instead of the password).
|
|
76
|
+
// Masking applies to what goes back to the model — the inline body.
|
|
77
|
+
const build = (mask) => serializeToPro({
|
|
75
78
|
name: processName,
|
|
76
79
|
prolog: mask(code.prolog),
|
|
77
80
|
metadata: mask(code.metadata),
|
|
@@ -84,6 +87,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
84
87
|
: {}),
|
|
85
88
|
dataSource,
|
|
86
89
|
});
|
|
90
|
+
const proContent = build((s) => s);
|
|
91
|
+
const inlineContent = resolveMaskSecrets(maskSecrets)
|
|
92
|
+
? build(maskCode)
|
|
93
|
+
: proContent;
|
|
87
94
|
let writtenTo = null;
|
|
88
95
|
if (writeToFile) {
|
|
89
96
|
const target = resolveLocalPath(writeToFile, "writeToFile");
|
|
@@ -107,7 +114,7 @@ export const registerExportProcessToPro = defineTool({
|
|
|
107
114
|
credentialsIncluded,
|
|
108
115
|
// Written to disk only when credentials are in play — otherwise
|
|
109
116
|
// the caller already has the body and it stays out of the context.
|
|
110
|
-
...(includeDataSourcePassword ? {} : { content:
|
|
117
|
+
...(includeDataSourcePassword ? {} : { content: inlineContent }),
|
|
111
118
|
}),
|
|
112
119
|
},
|
|
113
120
|
],
|