@ffschrattenecker/tm1-mcp-server 7.0.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/README.md +9 -6
  3. package/dist/config.d.ts +10 -1
  4. package/dist/config.js +40 -22
  5. package/dist/connections.d.ts +30 -2
  6. package/dist/connections.js +94 -21
  7. package/dist/http-transport.js +51 -4
  8. package/dist/index.js +6 -0
  9. package/dist/lib/callgraph/referenceIndex.js +2 -1
  10. package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
  11. package/dist/lib/callgraph/rulesLinter.js +0 -590
  12. package/dist/lib/callgraph/tiParser.js +10 -5
  13. package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
  14. package/dist/lib/callgraph/tm1-adapter.js +41 -9
  15. package/dist/lib/callgraph/variableEnv.js +3 -2
  16. package/dist/lib/cell-address.d.ts +1 -1
  17. package/dist/lib/cell-address.js +2 -2
  18. package/dist/lib/complexity/antipatterns.js +3 -1
  19. package/dist/lib/complexity/comment-classifier.js +2 -1
  20. package/dist/lib/feeders/element-type-cache.js +4 -6
  21. package/dist/lib/naming/odata-filter.js +2 -1
  22. package/dist/lib/pro-parser.js +13 -4
  23. package/dist/lib/safe-regex.d.ts +0 -13
  24. package/dist/lib/safe-regex.js +40 -13
  25. package/dist/lib/sample-cells.js +1 -1
  26. package/dist/lib/ti-identifier.d.ts +11 -0
  27. package/dist/lib/ti-identifier.js +11 -0
  28. package/dist/lib/tm1-name.d.ts +9 -0
  29. package/dist/lib/tm1-name.js +9 -0
  30. package/dist/lib/v12-compat/deprecated-ti.js +2 -2
  31. package/dist/secrets-cli.d.ts +10 -0
  32. package/dist/secrets-cli.js +218 -0
  33. package/dist/secrets.d.ts +39 -0
  34. package/dist/secrets.js +128 -0
  35. package/dist/session-manager.d.ts +1 -0
  36. package/dist/session-manager.js +24 -0
  37. package/dist/tm1-client/connection/profile.js +2 -4
  38. package/dist/tm1-client/dispatcher.d.ts +1 -1
  39. package/dist/tm1-client/dispatcher.js +18 -6
  40. package/dist/tm1-client/http.d.ts +21 -0
  41. package/dist/tm1-client/http.js +39 -30
  42. package/dist/tm1-client/services/cell-service.d.ts +9 -7
  43. package/dist/tm1-client/services/cell-service.js +51 -25
  44. package/dist/tm1-client/services/chore-service.js +8 -9
  45. package/dist/tm1-client/services/cube-service.d.ts +14 -18
  46. package/dist/tm1-client/services/cube-service.js +51 -45
  47. package/dist/tm1-client/services/dimension-order.d.ts +13 -0
  48. package/dist/tm1-client/services/dimension-order.js +42 -0
  49. package/dist/tm1-client/services/dimension-service.js +4 -6
  50. package/dist/tm1-client/services/element-service.d.ts +35 -11
  51. package/dist/tm1-client/services/element-service.js +124 -60
  52. package/dist/tm1-client/services/file-service.d.ts +42 -6
  53. package/dist/tm1-client/services/file-service.js +225 -28
  54. package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
  55. package/dist/tm1-client/services/hierarchy-service.js +118 -12
  56. package/dist/tm1-client/services/monitoring-service.js +2 -5
  57. package/dist/tm1-client/services/odata-page.d.ts +6 -0
  58. package/dist/tm1-client/services/odata-page.js +8 -0
  59. package/dist/tm1-client/services/process-service.d.ts +1 -1
  60. package/dist/tm1-client/services/process-service.js +26 -26
  61. package/dist/tm1-client/services/security-service.js +8 -9
  62. package/dist/tm1-client/services/server-service.js +14 -15
  63. package/dist/tm1-client/services/subset-service.d.ts +23 -10
  64. package/dist/tm1-client/services/subset-service.js +82 -26
  65. package/dist/tm1-client/services/view-service.js +12 -13
  66. package/dist/tm1-client.js +5 -2
  67. package/dist/tools/analysis/check-v12-readiness.js +3 -3
  68. package/dist/tools/celldata/check-feeders.js +6 -6
  69. package/dist/tools/celldata/check-writable-coords.js +33 -22
  70. package/dist/tools/celldata/execute-mdx.js +10 -9
  71. package/dist/tools/celldata/get-view.js +10 -9
  72. package/dist/tools/celldata/member-ref.d.ts +15 -0
  73. package/dist/tools/celldata/member-ref.js +67 -0
  74. package/dist/tools/celldata/trace-cell-calculation.js +6 -6
  75. package/dist/tools/celldata/trace-feeders.js +6 -6
  76. package/dist/tools/celldata/write-cells.js +62 -12
  77. package/dist/tools/dimension-management/create-element-attribute.js +1 -1
  78. package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
  79. package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
  80. package/dist/tools/dimension-management/list-element-attributes.js +1 -1
  81. package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
  82. package/dist/tools/dimension-management/update-element.js +14 -7
  83. package/dist/tools/fileops/container.d.ts +8 -0
  84. package/dist/tools/fileops/container.js +11 -0
  85. package/dist/tools/fileops/delete-file.js +4 -2
  86. package/dist/tools/fileops/get-file-content.js +45 -7
  87. package/dist/tools/fileops/list-files.js +5 -2
  88. package/dist/tools/fileops/search-files.js +4 -1
  89. package/dist/tools/fileops/upload-file.js +5 -2
  90. package/dist/tools/index.js +0 -2
  91. package/dist/tools/model-building/check-cube-rule.js +3 -2
  92. package/dist/tools/model-building/clear-cube.js +16 -27
  93. package/dist/tools/model-building/set-cube-rules.js +13 -20
  94. package/dist/tools/model-building/unload-cube.js +1 -1
  95. package/dist/tools/operations/get-cube-stats.js +1 -1
  96. package/dist/tools/operations/get-transaction-log.js +1 -1
  97. package/dist/tools/operations/list-error-logs.js +27 -2
  98. package/dist/tools/schemas/items-fileops.d.ts +4 -0
  99. package/dist/tools/schemas/items-fileops.js +1 -0
  100. package/dist/tools/security/list-clients.js +5 -5
  101. package/dist/tools/security/list-groups.js +1 -1
  102. package/dist/tools/subsets/create-subset.js +9 -5
  103. package/dist/tools/subsets/delete-subset.js +8 -3
  104. package/dist/tools/subsets/update-subset.js +9 -9
  105. package/dist/tools/ti-development/diff-process-with-file.js +4 -29
  106. package/dist/tools/ti-development/diff-processes.js +9 -0
  107. package/dist/tools/ti-development/export-process-to-git.js +9 -7
  108. package/dist/tools/ti-development/export-process-to-pro.js +15 -8
  109. package/dist/tools/ti-development/import-pro-file.js +10 -21
  110. package/dist/tools/ti-development/import-process-from-git.js +8 -18
  111. package/dist/tools/ti-development/install-pro-bundle.js +2 -2
  112. package/dist/tools/ti-development/upsert-process.js +26 -5
  113. package/npm-shrinkwrap.json +245 -9
  114. package/package.json +10 -5
  115. package/dist/tools/dimension-management/move-element.d.ts +0 -2
  116. package/dist/tools/dimension-management/move-element.js +0 -28
@@ -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 (preflight). 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.",
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
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: JSON.stringify({
53
- stage: "preflight",
54
- check: "syntax",
55
- cubeName,
56
- code: TM1ErrorCode.VALIDATION_ERROR,
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 disk on next access. Required after feeder corrections, since the fed-cell index is cumulative — changes to existing feeders only take effect after an unload. Safe to call: data is preserved (read from .cub on next access). TM1 v11 only.",
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
- "Servers with no }Stats* control cubes (TM1 v12), or accounts not allowed to read them, return `statsUnavailable` {reason: absent|denied} instead of a raw error.",
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 it never triggers a full scan. Pass since/until (from-to) to bound it explicitly. (v11 only)",
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: pageShapeFor(z.union([ErrorLogFileSchema, ErrorLogGroupSchema])),
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 both modern v11 'TM1ProcessError_<ts>_<id>_<processName>_<hash>.log' and legacy '<processName>_<ts>.log' filename patterns."),
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
+ text: "text";
10
+ base64: "base64";
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
- const out = {};
34
- if (want.has("name"))
35
- out.Name = c.Name;
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 TM1 subset. Provide either expression (MDX-based, dynamic) OR elements (static list) — not both. Optional alias attribute name controls the displayed alias.",
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
- const hierarchy = resolveHierarchy(dimensionName, hierarchyName);
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 TM1 subset. Fails if the subset is referenced by views/processes (404 if not found). Irreversible — pass confirm=<subset name verbatim>.",
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 a public TM1 subset (partial). Pass expression to replace the MDX, or elements to switch the subset to a static list (resets Expression to ''). Pass alias to change the alias attribute.",
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 (clears MDX)"),
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
- const hierarchy = resolveHierarchy(dimensionName, hierarchyName);
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, withVersion } from "../annotations.js";
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: withVersion(READ_ONLY, "v11"),
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 = diffDataSource(installedDs, parsed.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. Round-trip safe with tm1_import_process_from_git.",
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 exported .ti code (inline and written file) and credential pairs (PWD=, UID=) in the datasource's ODBC connection string in the .json. " +
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
- const mask = resolveMaskSecrets(maskSecrets) ? maskCode : (s) => s;
61
- const ti = mask(pair.ti);
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, withVersion } from "../annotations.js";
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
- "Round-trip safe with tm1_import_pro_file — useful for syncing live server state into a Git repo.",
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: withVersion(READ_ONLY, "v11"),
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 exported code (inline and written file). Masks the password arg of ODBCOpen() and quoted values " +
34
- "assigned to credential-named identifiers (pPwd, sToken, …). Default: true. Set false only when explicitly auditing credentials."),
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
- const mask = resolveMaskSecrets(maskSecrets) ? maskCode : (s) => s;
74
- const proContent = serializeToPro({
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: proContent }),
117
+ ...(includeDataSourcePassword ? {} : { content: inlineContent }),
111
118
  }),
112
119
  },
113
120
  ],
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import { TM1Error, TM1ErrorCode } from "../../types.js";
5
5
  import { parseProFile } from "../../lib/pro-parser.js";
6
6
  import { withToolHint } from "../error-format.js";
7
- import { IDEMPOTENT_DESTRUCTIVE, withVersion } from "../annotations.js";
7
+ import { IDEMPOTENT_DESTRUCTIVE } from "../annotations.js";
8
8
  import { ImportProFileResultSchema } from "../schemas/items.js";
9
9
  import { defineTool } from "../define-tool.js";
10
10
  import { OVERWRITE_CONFIRM_SCHEMA, requireOverwriteConfirm, } from "../confirm.js";
@@ -13,7 +13,7 @@ import { backupProcess } from "./process-backup.js";
13
13
  export const registerImportProFile = defineTool({
14
14
  name: "tm1_import_pro_file",
15
15
  description: "Parse a TM1 .pro file (Tabs / Parameters / Variables / DataSource) and deploy the process in one call. Provide either filePath (absolute path on the MCP host) or content (the .pro file body as string). Modes: 'create' (fail if exists), 'update' (fail if missing), 'upsert' (default — create or update). A .pro carries an ODBC password only when it came from tm1_export_process_to_pro against a v12 database, which writes it in clear; TM1's own Datadir .pro encodes slot 565 in a form that cannot be replayed over REST, and v11 exports never contain a password at all. Pass dataSourcePassword in every other case.",
16
- annotations: withVersion(IDEMPOTENT_DESTRUCTIVE, "v11"),
16
+ annotations: IDEMPOTENT_DESTRUCTIVE,
17
17
  output: ImportProFileResultSchema,
18
18
  input: {
19
19
  filePath: z
@@ -108,25 +108,14 @@ export const registerImportProFile = defineTool({
108
108
  data: parsed.data,
109
109
  epilog: parsed.epilog,
110
110
  }), `Code update failed after process '${processName}' was ${exists ? "located" : "created"}. PARTIAL APPLY: the process shell exists but tabs are stale/empty. Re-run tm1_import_pro_file with mode=update once root cause fixed, or tm1_delete_process to roll back.`);
111
- // On an update the file replaces the whole definition, as the code tabs
112
- // already do: an empty parameter list, variable layout or a None
113
- // datasource is applied, not skipped. Skipping left the newer values in
114
- // place, so importing an older version (a backup) did not restore it.
115
- if (exists || parsed.parameters.length > 0) {
116
- await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. Inspect parsed parameters and re-run tm1_upsert_process with mode=update + parameters=[...] to recover.`);
117
- }
118
- // Ignored columns live only in the UI data, so a file can carry column
119
- // layout with an empty variable list — patch on either.
120
- if (exists ||
121
- parsed.variables.length > 0 ||
122
- parsed.variablesUIData.length > 0) {
123
- await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData.length > 0 || parsed.variables.length === 0
124
- ? parsed.variablesUIData
125
- : undefined), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
126
- }
127
- if (exists || parsed.dataSource.type !== "None") {
128
- await withToolHint(tm1Client.processes.updateDataSource(processName, parsed.dataSource), `Datasource update failed for '${processName}' (type=${parsed.dataSource.type}). Code+params+vars applied. Verify datasource credentials/path (ASCII file existence, ODBC DSN, view name) and re-run tm1_upsert_process with mode=update + dataSource={...} to recover.`);
129
- }
111
+ // The file is the whole truth: an empty list or a None source is sent as
112
+ // well, because TM1 applies both (measured on 11.8 and 12.5) and skipping
113
+ // them left the removed parameters, variables and source on the server.
114
+ await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. Inspect parsed parameters and re-run tm1_upsert_process with mode=update + parameters=[...] to recover.`);
115
+ // A file without a column-layout block leaves the server's layout as it
116
+ // is; one that carries it replaces it.
117
+ await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData.length > 0 ? parsed.variablesUIData : undefined), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
118
+ await withToolHint(tm1Client.processes.updateDataSource(processName, parsed.dataSource), `Datasource update failed for '${processName}' (type=${parsed.dataSource.type}). Code+params+vars applied. Verify datasource credentials/path (ASCII file existence, ODBC DSN, view name) and re-run tm1_upsert_process with mode=update + dataSource={...} to recover.`);
130
119
  return {
131
120
  content: [
132
121
  {
@@ -148,24 +148,14 @@ export const registerImportProcessFromGit = defineTool({
148
148
  // The parsed tabs were preflighted above; unplacedBlobContent() made
149
149
  // sure they cover this blob exactly, so what was checked is what deploys.
150
150
  await withToolHint(tm1Client.processes.updateCodeBlob(processName, ti.replace(/\r?\n/g, "\r\n")), `Code update failed after process '${processName}' was ${exists ? "located" : "created"}. PARTIAL APPLY: shell exists but tabs are stale/empty. Re-run with mode=update once root cause fixed, or tm1_delete_process to roll back.`);
151
- // On an update the file replaces the whole definition, as the code tabs
152
- // already do: an empty parameter list, variable layout or a None
153
- // datasource is applied, not skipped. Skipping left the newer values in
154
- // place, so importing an older version (a backup) did not restore it.
155
- if (exists || parsed.parameters.length > 0) {
156
- await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. tm1_upsert_process with mode=update + parameters=[...] to recover.`);
157
- }
158
- // Ignored columns live only in the UI data, so a .json can carry column
159
- // layout with an empty variable list — patch on either.
160
- if (exists ||
161
- parsed.variables.length > 0 ||
162
- (parsed.variablesUIData?.length ?? 0) > 0) {
163
- await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData ??
164
- (parsed.variables.length === 0 ? [] : undefined)), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
165
- }
166
- if (exists || dataSource.type !== "None") {
167
- await withToolHint(tm1Client.processes.updateDataSource(processName, dataSource), `Datasource update failed for '${processName}' (type=${dataSource.type}). Code+params+vars applied. For ODBC verify dataSourcePassword/DSN and re-run with mode=update.`);
168
- }
151
+ // The file is the whole truth: an empty list or a None source is sent as
152
+ // well, because TM1 applies both (measured on 11.8 and 12.5) and skipping
153
+ // them left the removed parameters, variables and source on the server.
154
+ await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. tm1_upsert_process with mode=update + parameters=[...] to recover.`);
155
+ // An export without variablesUIData (older format) leaves the server's
156
+ // column layout as it is; one that carries it replaces it.
157
+ await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
158
+ await withToolHint(tm1Client.processes.updateDataSource(processName, dataSource), `Datasource update failed for '${processName}' (type=${dataSource.type}). Code+params+vars applied. For ODBC verify dataSourcePassword/DSN and re-run with mode=update.`);
169
159
  if (parsed.hasSecurityAccess !== undefined) {
170
160
  await withToolHint(tm1Client.processes.updateSecurityAccess(processName, parsed.hasSecurityAccess), `HasSecurityAccess update failed for '${processName}'. Code+params+vars+datasource applied. Re-run with mode=update once root cause fixed.`);
171
161
  }