@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/README.md +5 -5
  3. package/dist/config.js +34 -17
  4. package/dist/connections.d.ts +18 -1
  5. package/dist/connections.js +34 -18
  6. package/dist/http-transport.js +51 -4
  7. package/dist/lib/callgraph/referenceIndex.js +2 -1
  8. package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
  9. package/dist/lib/callgraph/rulesLinter.js +0 -590
  10. package/dist/lib/callgraph/tiParser.js +10 -5
  11. package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
  12. package/dist/lib/callgraph/tm1-adapter.js +41 -9
  13. package/dist/lib/callgraph/variableEnv.js +3 -2
  14. package/dist/lib/cell-address.d.ts +11 -1
  15. package/dist/lib/cell-address.js +22 -3
  16. package/dist/lib/complexity/antipatterns.js +3 -1
  17. package/dist/lib/complexity/comment-classifier.js +2 -1
  18. package/dist/lib/coordinate-error.js +5 -1
  19. package/dist/lib/feeders/element-type-cache.js +4 -6
  20. package/dist/lib/naming/odata-filter.js +2 -1
  21. package/dist/lib/pro-parser.js +13 -4
  22. package/dist/lib/safe-regex.d.ts +0 -13
  23. package/dist/lib/safe-regex.js +40 -13
  24. package/dist/lib/sample-cells.d.ts +6 -0
  25. package/dist/lib/sample-cells.js +20 -2
  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/session-manager.d.ts +1 -0
  32. package/dist/session-manager.js +24 -0
  33. package/dist/tm1-client/connection/profile.js +2 -4
  34. package/dist/tm1-client/dispatcher.d.ts +1 -1
  35. package/dist/tm1-client/dispatcher.js +18 -6
  36. package/dist/tm1-client/http.d.ts +21 -0
  37. package/dist/tm1-client/http.js +39 -30
  38. package/dist/tm1-client/services/cell-service.d.ts +9 -7
  39. package/dist/tm1-client/services/cell-service.js +54 -25
  40. package/dist/tm1-client/services/chore-service.js +8 -9
  41. package/dist/tm1-client/services/cube-service.d.ts +14 -18
  42. package/dist/tm1-client/services/cube-service.js +51 -45
  43. package/dist/tm1-client/services/dimension-order.d.ts +13 -0
  44. package/dist/tm1-client/services/dimension-order.js +42 -0
  45. package/dist/tm1-client/services/dimension-service.js +4 -6
  46. package/dist/tm1-client/services/element-service.d.ts +35 -11
  47. package/dist/tm1-client/services/element-service.js +124 -60
  48. package/dist/tm1-client/services/file-service.d.ts +42 -6
  49. package/dist/tm1-client/services/file-service.js +225 -28
  50. package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
  51. package/dist/tm1-client/services/hierarchy-service.js +118 -12
  52. package/dist/tm1-client/services/monitoring-service.js +2 -5
  53. package/dist/tm1-client/services/odata-page.d.ts +6 -0
  54. package/dist/tm1-client/services/odata-page.js +8 -0
  55. package/dist/tm1-client/services/process-service.d.ts +1 -1
  56. package/dist/tm1-client/services/process-service.js +26 -26
  57. package/dist/tm1-client/services/security-service.js +8 -9
  58. package/dist/tm1-client/services/server-service.js +14 -15
  59. package/dist/tm1-client/services/subset-service.d.ts +23 -10
  60. package/dist/tm1-client/services/subset-service.js +82 -26
  61. package/dist/tm1-client/services/view-service.js +12 -13
  62. package/dist/tm1-client.js +5 -2
  63. package/dist/tools/analysis/check-v12-readiness.js +3 -3
  64. package/dist/tools/celldata/check-feeders.js +6 -6
  65. package/dist/tools/celldata/check-writable-coords.js +43 -25
  66. package/dist/tools/celldata/execute-mdx.js +19 -23
  67. package/dist/tools/celldata/get-cell-value.js +1 -1
  68. package/dist/tools/celldata/get-view.js +10 -9
  69. package/dist/tools/celldata/member-ref.d.ts +15 -0
  70. package/dist/tools/celldata/member-ref.js +67 -0
  71. package/dist/tools/celldata/sample-cells.js +3 -0
  72. package/dist/tools/celldata/trace-cell-calculation.js +6 -6
  73. package/dist/tools/celldata/trace-feeders.js +6 -6
  74. package/dist/tools/celldata/write-cells.js +62 -12
  75. package/dist/tools/dimension-management/create-element-attribute.js +1 -1
  76. package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
  77. package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
  78. package/dist/tools/dimension-management/list-element-attributes.js +1 -1
  79. package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
  80. package/dist/tools/dimension-management/update-element.js +14 -7
  81. package/dist/tools/fileops/container.d.ts +8 -0
  82. package/dist/tools/fileops/container.js +11 -0
  83. package/dist/tools/fileops/delete-file.js +4 -2
  84. package/dist/tools/fileops/get-file-content.js +45 -7
  85. package/dist/tools/fileops/list-files.js +5 -2
  86. package/dist/tools/fileops/search-files.js +4 -1
  87. package/dist/tools/fileops/upload-file.js +5 -2
  88. package/dist/tools/format.d.ts +1 -0
  89. package/dist/tools/format.js +7 -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 +35 -26
  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 +465 -500
  114. package/package.json +14 -10
  115. package/dist/tools/dimension-management/move-element.d.ts +0 -2
  116. package/dist/tools/dimension-management/move-element.js +0 -28
package/CHANGELOG.md CHANGED
@@ -7,6 +7,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [8.0.0] - 2026-09-28
11
+
12
+ Merged upstream tm1-mcp-server 5.0.0 (flameY3T1, 2026-09-27). Where both lines had
13
+ built the same thing, upstream's behaviour was taken; fork-only features (connection
14
+ registry, `tm1_delete_elements`, rules patch mode, batch attribute writes, the
15
+ response-size guard, the per-connection caches) are unchanged.
16
+
17
+ ### Breaking
18
+
19
+ - **`tm1_move_element` is removed.** It attached the element to the new parent but left it
20
+ under the old one. *Action:* use `tm1_update_element` on both parents' `components`.
21
+ - **`tm1_clear_cube` takes only `cubeName` and `confirm`** (plus `timeoutMs`). The
22
+ `dimensions` and `tuples` inputs advertised a region clear the server cannot do: the only
23
+ route that works is a TI with `CubeClearData`, which empties the whole cube. A call that
24
+ still carries them is refused and nothing is cleared. *Action:* use a TI process for a
25
+ partial clear.
26
+ - **`tm1_write_cells` refuses a consolidated coordinate.** Every coordinate is checked before
27
+ anything is sent; write the leaves or run a TI process.
28
+ - **`tm1_set_cube_rules` fails with `VALIDATION_ERROR` when the preflight finds syntax
29
+ errors** (the errors are in `details`), instead of returning an `isError` payload. The check
30
+ still runs on the full resulting text, so an `edits` patch is checked as installed.
31
+ *Action:* `preflight: false` writes the text as is.
32
+ - **Exported files hold the process code unmasked.** `'***'` in written files broke the
33
+ round-trip; `maskSecrets` now affects only the inline response of
34
+ `tm1_export_process_to_git`. Keep such files out of version control if the code contains
35
+ password literals.
36
+ - **An unknown `TM1_MCP_TRANSPORT` or `TM1_LOG_LEVEL` stops the server at startup** instead of
37
+ falling back silently. Both are server-wide; a bad value in one connection's `.env` cannot
38
+ occur because folders may not set them.
39
+ - **Unknown attributes are `NOT_FOUND`** in `tm1_update_element_attribute_value` (was
40
+ `VALIDATION_ERROR`), and attribute names match ignoring case and spaces, as TM1 does.
41
+
42
+ ### Added
43
+
44
+ - **The five file tools reach the Applications tree** via `container: "applications"`.
45
+ - **`tm1_get_file_content` takes `encoding: "base64"`** for binary files. Its `maxBytes` default
46
+ is 48 KB there (64 KB for text), so a default read fits the 80k response limit.
47
+ - **Subset tools take `isPrivate`.**
48
+ - **`tm1_upsert_process` takes `variablesUIData`** and warns when the kept column layout no
49
+ longer fits the variables.
50
+ - **`tm1_clear_cube` takes `timeoutMs`** (1 s to 1 h).
51
+ - **Feeder and cell-trace tools address alternate hierarchies** with `Hierarchy:Element`.
52
+ - **The attribute value tools take `hierarchyName`** for alternate hierarchies (single element
53
+ and `updates[]`).
54
+
55
+ ### Fixed
56
+
57
+ - **A rejected login is never retried.** A 401/403 on login latches for that connection, so
58
+ wrong credentials no longer lock the account; fix the `.env` and restart.
59
+ - **MCP SDK 1.30.1**; `fast-uri` out of the vulnerable range; `npm run verify` runs
60
+ `npm audit` first.
61
+ - **The ReDoS guard rejects repeated alternations** such as `^(\w|\w)*!$`.
62
+ - **The HTTP transport caps a request body at 64 MB** (413) and accepts `Host: localhost:<port>`
63
+ and `[::1]:<port>`.
64
+ - **A v12 connection no longer demands `TM1_PASSWORD`** for auth modes that never send one.
65
+ - **A TM1 request can wait longer than five minutes**; `timeoutMs` is the only limit.
66
+ - **`tm1_write_cells` no longer writes when its own pre-check failed.**
67
+ - **`tm1_check_writable_coords` understands `[Dimension].[Hierarchy].[Element]`** and looks in
68
+ the hierarchy the write would hit.
69
+ - **`fetchAll` and `limit: 0` on `tm1_execute_mdx` and `tm1_get_view` stop at 5000 cells.**
70
+ - **`tm1_check_cube_rule` returns syntax errors as a normal result**, not as a tool error.
71
+ - **`tm1_clear_cube` tells the truth on a timeout**: the clear finishes on the server.
72
+ - **`tm1_update_subset` can change the element list** (replaces, in order).
73
+ - **`tm1_get_descendants` and `tm1_get_ancestors` load only the subtree**, and `leavesOnly`
74
+ no longer returns empty consolidations.
75
+ - **`tm1_update_element` reports type conversions.**
76
+ - **Process imports remove what the source no longer has** (parameters, variables, data
77
+ source); a `.pro` round trip keeps trailing whitespace.
78
+ - **Process analysis reads variable names with `.`, `$`, `%` and backtick, and `;;` inside a
79
+ string literal.**
80
+ - **Both diff tools compare the delimiter type and the ODBC unicode flag.**
81
+ - **`tm1_list_error_logs` reads v12 log names**, and `groupBy: "process"` passes schema checks.
82
+ - **`tm1_list_clients` and `tm1_list_groups` show names in markdown.**
83
+ - **`tm1_check_v12_readiness` gives the right reason for `SetODBCUnicodeInterface`.**
84
+ - **Seven tools no longer claim to be v11-only**, among them `tm1_import_pro_file` and
85
+ `tm1_install_pro_bundle`.
86
+ - **Corrected descriptions** of `tm1_unload_cube`, `tm1_clear_cube`, `tm1_delete_hierarchy`,
87
+ `tm1_get_cube_stats` and `tm1_get_transaction_log`.
88
+ - **The wire contracts were re-recorded against 11.8** upstream and merged with the fork's
89
+ plapp-franz recordings; the recorder now merges by default (`--replace` starts over).
90
+
91
+ ## [7.0.2] - 2026-09-27
92
+
93
+ ### Fixed
94
+
95
+ - On servers with `EnableSandboxDimension=true`, `tm1_get_cell_value` and `tm1_check_writable_coords`
96
+ (without `dimensions`) accept a coordinate that leaves out `Sandboxes` and bind it to `Base`, as
97
+ `tm1_write_cells` already did. Before, they refused it as one element short.
98
+ - `tm1_sample_cells` found no cells in any cube with a `Sandboxes` dimension: TM1's `NONEMPTY()`
99
+ returns an empty set whenever Sandboxes is part of the row set. An unfiltered Sandboxes is now
100
+ pinned to `Base` in WHERE.
101
+ - Markdown tables (`format: "markdown"`) now escape backslashes, so a value ending in `\` no
102
+ longer breaks the table at the next `|`. In `tm1_execute_mdx`, raw cell values are no longer
103
+ escaped twice.
104
+
105
+ ### Changed
106
+
107
+ - Dependencies: undici 8, dotenv 18, zod 4.6, and minor/patch bumps. Dev: vitest 5.
108
+
10
109
  ## [7.0.1] - 2026-09-27
11
110
 
12
111
  ### Fixed
@@ -1306,7 +1405,9 @@ Initial public release.
1306
1405
  - Quality gates: strict typecheck, ESLint, `lint:no-flat-api`,
1307
1406
  annotation-coverage, and tool-registration wiring.
1308
1407
 
1309
- [Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.1...HEAD
1408
+ [Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v8.0.0...HEAD
1409
+ [8.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.2...v8.0.0
1410
+ [7.0.2]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.1...v7.0.2
1310
1411
  [7.0.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.0...v7.0.1
1311
1412
  [7.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.1...v7.0.0
1312
1413
  [6.1.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.0...v6.1.1
package/README.md CHANGED
@@ -18,7 +18,7 @@ published as `@ffschrattenecker/tm1-mcp-server`. The command is still `tm1-mcp-s
18
18
 
19
19
  ## Features
20
20
 
21
- 112 tools across 12 categories — every one listed in
21
+ 111 tools across 12 categories — every one listed in
22
22
  [docs/TOOLS.md](docs/TOOLS.md), with working JSON payloads in
23
23
  [docs/EXAMPLES.md](docs/EXAMPLES.md). Past plain CRUD over the REST API:
24
24
 
@@ -45,7 +45,7 @@ published as `@ffschrattenecker/tm1-mcp-server`. The command is still `tm1-mcp-s
45
45
  in chat, plus five starting-point prompts.
46
46
 
47
47
  > **Token tip:** every registered tool's name + input schema costs context on
48
- > *each* turn, so exposing all 114 is wasteful when a session needs a slice.
48
+ > *each* turn, so exposing all 113 is wasteful when a session needs a slice.
49
49
  > Narrow the surface with your client's tool filter; `TM1_MODE=readonly` already
50
50
  > trims it to read-only tools.
51
51
 
@@ -218,7 +218,7 @@ security notes and the `autoApprove` allowlist:
218
218
 
219
219
  <!-- TOOLS-AUTOGEN:START -->
220
220
 
221
- ## Tools (112)
221
+ ## Tools (111)
222
222
 
223
223
  Names and one-line descriptions: [docs/TOOLS.md](docs/TOOLS.md).
224
224
 
@@ -226,7 +226,7 @@ Names and one-line descriptions: [docs/TOOLS.md](docs/TOOLS.md).
226
226
  |---|---|
227
227
  | analysis | 10 |
228
228
  | celldata | 10 |
229
- | dimension-management | 14 |
229
+ | dimension-management | 13 |
230
230
  | fileops | 5 |
231
231
  | metadata | 9 |
232
232
  | model-building | 9 |
@@ -236,7 +236,7 @@ Names and one-line descriptions: [docs/TOOLS.md](docs/TOOLS.md).
236
236
  | subsets | 5 |
237
237
  | ti-development | 17 |
238
238
  | views | 4 |
239
- | **Total** | **112** |
239
+ | **Total** | **111** |
240
240
 
241
241
  <!-- TOOLS-AUTOGEN:END -->
242
242
 
package/dist/config.js CHANGED
@@ -24,15 +24,21 @@ function parseIntEnv(name, raw, def) {
24
24
  return n;
25
25
  }
26
26
  export function loadServerSettings(env = process.env) {
27
- const logLevelRaw = env.TM1_LOG_LEVEL ?? "info";
28
- const logLevel = VALID_LOG_LEVELS.includes(logLevelRaw)
29
- ? logLevelRaw
30
- : "info";
27
+ // Same parse shape as TM1_MODE: case-insensitive, an unknown value throws at
28
+ // startup instead of silently falling back to the default.
29
+ const logLevelRaw = (env.TM1_LOG_LEVEL ?? "info").trim().toLowerCase();
30
+ if (!VALID_LOG_LEVELS.includes(logLevelRaw)) {
31
+ throw new Error(`Invalid TM1_LOG_LEVEL: "${env.TM1_LOG_LEVEL}". Expected one of ${VALID_LOG_LEVELS.join(", ")}.`);
32
+ }
33
+ const logLevel = logLevelRaw;
31
34
  const logFile = env.TM1_LOG_FILE || undefined;
32
- const transportRaw = env.TM1_MCP_TRANSPORT ?? "stdio";
33
- const transport = VALID_TRANSPORTS.includes(transportRaw)
34
- ? transportRaw
35
- : "stdio";
35
+ // A typo here used to start on stdio without a word: the expected /mcp port
36
+ // never bound and the operator saw only a client that could not connect.
37
+ const transportRaw = (env.TM1_MCP_TRANSPORT ?? "stdio").trim().toLowerCase();
38
+ if (!VALID_TRANSPORTS.includes(transportRaw)) {
39
+ throw new Error(`Invalid TM1_MCP_TRANSPORT: "${env.TM1_MCP_TRANSPORT}". Expected "stdio" or "http".`);
40
+ }
41
+ const transport = transportRaw;
36
42
  // Default to loopback. Binding to 0.0.0.0 must be opt-in to avoid exposing
37
43
  // a TM1-credentialed MCP server to the LAN by accident.
38
44
  const httpHost = env.TM1_MCP_HTTP_HOST || "127.0.0.1";
@@ -113,6 +119,22 @@ export function loadConfig(env = process.env) {
113
119
  // obtained out-of-band via TM1_CAM_PASSPORT instead.
114
120
  const namespace = env.TM1_NAMESPACE || undefined;
115
121
  const camPassport = env.TM1_CAM_PASSPORT || undefined;
122
+ // Which connection this is has to be known BEFORE the required-variable
123
+ // check below, because the answer decides which credential is required.
124
+ const tm1Version = env.TM1_VERSION || "11.8";
125
+ const instance = env.TM1_INSTANCE || undefined;
126
+ const database = env.TM1_DATABASE || undefined;
127
+ const versionMajor = Number.parseInt(tm1Version, 10);
128
+ const isV12 = Boolean(instance || database) || versionMajor === 12;
129
+ const version = isV12 ? 12 : 11;
130
+ // Every v12 auth mode except "basic" authenticates with a client secret, a
131
+ // bearer token or an API key, and the session login sends no password at all
132
+ // (connection/profile.ts buildV12Authorization). Demanding TM1_PASSWORD there
133
+ // rejects exactly the configuration docs/CONFIGURATION.md prescribes, and it
134
+ // does so at startup, before the server can say anything more useful. The v12
135
+ // block below still requires whichever credential the chosen mode does need.
136
+ const v12AuthMode = (env.TM1_AUTH_MODE ?? "s2s").trim().toLowerCase();
137
+ const passwordlessV12 = version === 12 && v12AuthMode !== "basic";
116
138
  // Required: baseUrl always. user/password only when NOT using a passport — a
117
139
  // passport carries the authenticated identity, so TM1 needs no credentials.
118
140
  // Empty strings are rejected (treated as unset). Password may be empty — some
@@ -124,14 +146,15 @@ export function loadConfig(env = process.env) {
124
146
  if (!camPassport) {
125
147
  if (!user)
126
148
  missing.push("TM1_USER");
127
- if (password === undefined)
149
+ if (password === undefined && !passwordlessV12) {
128
150
  missing.push("TM1_PASSWORD");
151
+ }
129
152
  }
130
153
  if (missing.length > 0) {
131
154
  throw new Error(`Missing or empty required environment variables: ${missing.join(", ")}. ` +
132
155
  `Set them in your shell or .env file before starting the server.`);
133
156
  }
134
- if (!camPassport && password === "") {
157
+ if (!camPassport && !passwordlessV12 && password === "") {
135
158
  process.stderr.write("[tm1-mcp-server] WARNING: TM1_PASSWORD is empty. " +
136
159
  "If TM1 rejects with 401, check whether the account actually allows blank passwords.\n");
137
160
  }
@@ -140,7 +163,6 @@ export function loadConfig(env = process.env) {
140
163
  const keepAliveIntervalMs = parseIntEnv("TM1_KEEP_ALIVE_INTERVAL", env.TM1_KEEP_ALIVE_INTERVAL, 60000);
141
164
  const requestTimeoutMs = parseIntEnv("TM1_REQUEST_TIMEOUT", env.TM1_REQUEST_TIMEOUT, 30000);
142
165
  const server = loadServerSettings(env);
143
- const tm1Version = env.TM1_VERSION || "11.8";
144
166
  // Case-insensitive so a `TM1_MODE=ReadWrite` typo resolves to readwrite rather
145
167
  // than silently falling back to readonly (dropping every write tool without a
146
168
  // word). A genuinely-unknown value throws at startup — parity with the numeric
@@ -164,11 +186,6 @@ export function loadConfig(env = process.env) {
164
186
  "TM1_ENVIRONMENT=prod forces readonly; set TM1_ALLOW_PROD_WRITES=true to allow writes.";
165
187
  }
166
188
  // --- v12 (Planning Analytics Engine) connection ---------------------------
167
- const instance = env.TM1_INSTANCE || undefined;
168
- const database = env.TM1_DATABASE || undefined;
169
- const versionMajor = Number.parseInt(tm1Version, 10);
170
- const isV12 = Boolean(instance || database) || versionMajor === 12;
171
- const version = isV12 ? 12 : 11;
172
189
  // Keep the DISPLAY string (server_info, logs) consistent with the numeric
173
190
  // `version`: a v12 connection (isV12, via TM1_INSTANCE/TM1_DATABASE) declared
174
191
  // with a v11-looking TM1_VERSION="11.8" would otherwise report "11.8" to users
@@ -189,7 +206,7 @@ export function loadConfig(env = process.env) {
189
206
  if (!database) {
190
207
  throw new Error("v12 connection requires TM1_DATABASE (set alongside TM1_INSTANCE).");
191
208
  }
192
- const authModeRaw = (env.TM1_AUTH_MODE ?? "s2s").trim().toLowerCase();
209
+ const authModeRaw = v12AuthMode;
193
210
  if (!VALID_AUTH_MODES.includes(authModeRaw)) {
194
211
  throw new Error(`Invalid TM1_AUTH_MODE: "${env.TM1_AUTH_MODE}". ` +
195
212
  `Expected one of: ${VALID_AUTH_MODES.join(", ")}.`);
@@ -1,6 +1,20 @@
1
1
  import pino from "pino";
2
2
  import { type TM1Config } from "./config.js";
3
3
  import { TM1Client } from "./tm1-client.js";
4
+ /** Folder holding one `<name>/.env` per connection. */
5
+ export declare function connectionsDir(env: NodeJS.ProcessEnv): string;
6
+ /**
7
+ * The environment one connection folder resolves to — the only way a
8
+ * connection's `.env` is read, by the server and by scripts/run-live-for.ts
9
+ * alike, so a script can never log in with a differently parsed password.
10
+ *
11
+ * Server-level settings and non-TM1 variables (PATH, proxies) carry over from
12
+ * `env`; connection-level TM1_* keys come from the folder only, so a stray
13
+ * TM1_INSTANCE in the shell cannot reroute a v11 connection. The folder may
14
+ * not override server-level settings: one folder's TM1_MCP_TRANSPORT must not
15
+ * reconfigure the whole process.
16
+ */
17
+ export declare function connectionEnv(folder: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
4
18
  export interface ConnectionInfo {
5
19
  name: string;
6
20
  /** Folder the `.env` came from; undefined for the legacy single connection. */
@@ -59,7 +73,10 @@ export declare class ConnectionRegistry {
59
73
  /**
60
74
  * The client for a connection, built and logged in on first use. A failed
61
75
  * login does not poison the entry: the client's own request path retries
62
- * authentication, so the next call gets a fresh attempt.
76
+ * authentication, so the next call gets a fresh attempt — except when TM1
77
+ * rejected the credentials (401/403). The client is kept, so its
78
+ * SessionManager's latch answers every later call without another login
79
+ * until the server restarts.
63
80
  */
64
81
  get(name: string | undefined): Promise<TM1Client>;
65
82
  /** Connection metadata for a resolved call (mode/version gates). */
@@ -39,6 +39,34 @@ const SERVER_LEVEL_KEYS = new Set([
39
39
  function isServerLevelKey(key) {
40
40
  return SERVER_LEVEL_KEYS.has(key) || key.startsWith("TM1_MCP_");
41
41
  }
42
+ /** Folder holding one `<name>/.env` per connection. */
43
+ export function connectionsDir(env) {
44
+ return env.TM1_CONNECTIONS_DIR || join(homedir(), ".tm1", "mcp-servers");
45
+ }
46
+ /**
47
+ * The environment one connection folder resolves to — the only way a
48
+ * connection's `.env` is read, by the server and by scripts/run-live-for.ts
49
+ * alike, so a script can never log in with a differently parsed password.
50
+ *
51
+ * Server-level settings and non-TM1 variables (PATH, proxies) carry over from
52
+ * `env`; connection-level TM1_* keys come from the folder only, so a stray
53
+ * TM1_INSTANCE in the shell cannot reroute a v11 connection. The folder may
54
+ * not override server-level settings: one folder's TM1_MCP_TRANSPORT must not
55
+ * reconfigure the whole process.
56
+ */
57
+ export function connectionEnv(folder, env) {
58
+ const connEnv = {};
59
+ for (const [key, value] of Object.entries(env)) {
60
+ if (!key.startsWith("TM1_") || isServerLevelKey(key))
61
+ connEnv[key] = value;
62
+ }
63
+ const fileEnv = parseDotenv(readFileSync(join(folder, ".env")));
64
+ for (const [key, value] of Object.entries(fileEnv)) {
65
+ if (!isServerLevelKey(key))
66
+ connEnv[key] = value;
67
+ }
68
+ return connEnv;
69
+ }
42
70
  export class ConnectionRegistry {
43
71
  logger;
44
72
  entries = new Map();
@@ -53,7 +81,7 @@ export class ConnectionRegistry {
53
81
  registry.addConfig("default", loadConfig(env));
54
82
  return registry;
55
83
  }
56
- const dir = explicitDir || join(homedir(), ".tm1", "mcp-servers");
84
+ const dir = connectionsDir(env);
57
85
  registry.discover(dir, env);
58
86
  if (registry.entries.size === 0) {
59
87
  throw new Error(`No TM1 connections found. Set TM1_BASE_URL (single connection) or ` +
@@ -119,13 +147,6 @@ export class ConnectionRegistry {
119
147
  .map((s) => s.trim())
120
148
  .filter(Boolean))
121
149
  : undefined;
122
- // Server-level settings and non-TM1 variables (PATH, proxies) carry over;
123
- // connection-level TM1_* keys come from the folder only.
124
- const base = {};
125
- for (const [key, value] of Object.entries(env)) {
126
- if (!key.startsWith("TM1_") || isServerLevelKey(key))
127
- base[key] = value;
128
- }
129
150
  const names = readdirSync(dir, { withFileTypes: true })
130
151
  .filter((d) => d.isDirectory() && existsSync(join(dir, d.name, ".env")))
131
152
  .map((d) => d.name)
@@ -134,15 +155,7 @@ export class ConnectionRegistry {
134
155
  for (const name of names) {
135
156
  const folder = join(dir, name);
136
157
  try {
137
- const fileEnv = parseDotenv(readFileSync(join(folder, ".env")));
138
- const connEnv = { ...base };
139
- for (const [key, value] of Object.entries(fileEnv)) {
140
- // The folder may not override server-level settings: one folder's
141
- // TM1_MCP_TRANSPORT must not reconfigure the whole process.
142
- if (!isServerLevelKey(key))
143
- connEnv[key] = value;
144
- }
145
- this.addConfig(name, loadConfig(connEnv), folder);
158
+ this.addConfig(name, loadConfig(connectionEnv(folder, env)), folder);
146
159
  }
147
160
  catch (err) {
148
161
  const message = err instanceof Error ? err.message : String(err);
@@ -216,7 +229,10 @@ export class ConnectionRegistry {
216
229
  /**
217
230
  * The client for a connection, built and logged in on first use. A failed
218
231
  * login does not poison the entry: the client's own request path retries
219
- * authentication, so the next call gets a fresh attempt.
232
+ * authentication, so the next call gets a fresh attempt — except when TM1
233
+ * rejected the credentials (401/403). The client is kept, so its
234
+ * SessionManager's latch answers every later call without another login
235
+ * until the server restarts.
220
236
  */
221
237
  async get(name) {
222
238
  const entry = this.entryFor(name);
@@ -16,8 +16,25 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
16
16
  //
17
17
  // Per MCP best practices: bind 127.0.0.1 by default and enable DNS-rebinding
18
18
  // protection. allowedHosts/Origins narrow what the transport accepts.
19
+ // Cap on a single /mcp request body. Without one the whole request is buffered
20
+ // and then copied again by Buffer.concat().toString(), so a large enough POST
21
+ // can take the process down before any tool-level limit applies. Sized above
22
+ // the biggest legitimate payload: tm1_upload_file accepts 32 MB of bytes, which
23
+ // is ~43 MB base64 plus JSON framing.
24
+ // ponytail: fixed constant, make it configurable if a deployment needs more.
25
+ const MAX_BODY_BYTES = 64 * 1024 * 1024;
19
26
  export async function startHttpTransport(buildServer, config, logger) {
20
- const allowedHost = `${config.httpHost}:${config.httpPort}`;
27
+ // The SDK compares the Host header as one string, port included, so every
28
+ // entry needs the port. Loopback names are safe to always allow: a rebinding
29
+ // attacker's page sends its own hostname, never one of these.
30
+ const allowedHosts = [
31
+ ...new Set([
32
+ `${config.httpHost}:${config.httpPort}`,
33
+ `127.0.0.1:${config.httpPort}`,
34
+ `localhost:${config.httpPort}`,
35
+ `[::1]:${config.httpPort}`,
36
+ ]),
37
+ ];
21
38
  if (!config.httpToken) {
22
39
  logger.warn("HTTP transport has no TM1_MCP_HTTP_TOKEN set — /mcp requests are unauthenticated. " +
23
40
  "Bind loopback only, or front the server with an authenticating reverse proxy.");
@@ -55,12 +72,42 @@ export async function startHttpTransport(buildServer, config, logger) {
55
72
  }
56
73
  let body;
57
74
  if (req.method === "POST") {
75
+ // Refuse on the declared length before a single byte is read; the
76
+ // counter below is the backstop for a chunked body that declares none.
77
+ const declared = Number(req.headers["content-length"]);
78
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
79
+ logger.warn({ declared, limit: MAX_BODY_BYTES }, "Request body too large");
80
+ res.statusCode = 413;
81
+ res.setHeader("Content-Type", "application/json");
82
+ // Close the connection: the client is still holding a body this
83
+ // server will never read, and keep-alive would leave it queued.
84
+ res.setHeader("Connection", "close");
85
+ res.end(JSON.stringify({
86
+ error: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
87
+ }));
88
+ return;
89
+ }
58
90
  try {
59
91
  const chunks = [];
92
+ let size = 0;
60
93
  for await (const chunk of req) {
61
- chunks.push(typeof chunk === "string"
94
+ const buf = typeof chunk === "string"
62
95
  ? Buffer.from(chunk)
63
- : chunk);
96
+ : chunk;
97
+ size += buf.length;
98
+ if (size > MAX_BODY_BYTES) {
99
+ logger.warn({ limit: MAX_BODY_BYTES }, "Request body too large");
100
+ res.statusCode = 413;
101
+ res.setHeader("Content-Type", "application/json");
102
+ res.end(JSON.stringify({
103
+ error: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
104
+ }));
105
+ // Answer first, then stop reading: the rest of the upload is
106
+ // dropped instead of being buffered behind an already-sent reply.
107
+ req.destroy();
108
+ return;
109
+ }
110
+ chunks.push(buf);
64
111
  }
65
112
  const raw = Buffer.concat(chunks).toString("utf8");
66
113
  body = raw ? JSON.parse(raw) : undefined;
@@ -80,7 +127,7 @@ export async function startHttpTransport(buildServer, config, logger) {
80
127
  // sessionIdGenerator omitted → stateless mode (single-use per request)
81
128
  enableJsonResponse: true,
82
129
  enableDnsRebindingProtection: true,
83
- allowedHosts: [allowedHost, "127.0.0.1", "localhost"],
130
+ allowedHosts,
84
131
  allowedOrigins: config.httpAllowedOrigins,
85
132
  });
86
133
  res.on("close", () => {
@@ -4,6 +4,7 @@ import { joinContinuationLines } from "./tiParser.js";
4
4
  import { buildProcessEnv, resolveExpression, } from "./variableEnv.js";
5
5
  import { rethrowIfSystemic } from "../../tm1-client/services/fallback.js";
6
6
  import { extractSubsetUsage } from "./subsetUsage.js";
7
+ import { TI_VAR } from "../ti-identifier.js";
7
8
  function buildArgIdxMap(paramName) {
8
9
  const map = new Map();
9
10
  for (const [key, sig] of KNOWN_SIGNATURES.entries()) {
@@ -168,7 +169,7 @@ export function extractTiReferences(text, env, sharedLiveVars, unresolvedOut, un
168
169
  const refs = [];
169
170
  // Multi-line-Calls auf ihre Startzeile zusammenfassen — Continuation-Zeilen werden leer.
170
171
  const lines = joinContinuationLines(text.split("\n").map((l) => l.replace(/\r$/, "")));
171
- const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?)\s*;?\s*(?:#.*)?$/;
172
+ const assignRe = new RegExp(`^\\s*(${TI_VAR})\\s*=\\s*(.+?)\\s*;?\\s*(?:#.*)?$`);
172
173
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
173
174
  const line = lines[lineIdx];
174
175
  if (SECTION_MARKER_RE.test(line.trim()) || line.trim() === "") {
@@ -1,13 +1,3 @@
1
- export interface RulesLintDiagnose {
2
- line: number;
3
- message: string;
4
- severity: "error" | "warning" | "hint";
5
- ruleId: string;
6
- }
7
- export type ApiRequestFn = (method: string, path: string) => Promise<{
8
- statusCode: number;
9
- body: string;
10
- }>;
11
1
  /**
12
2
  * Extracts all [...] block contents from a line (from original, not neutralized).
13
3
  * Uses neutralized positions to avoid false positives inside strings/comments.
@@ -42,14 +32,5 @@ interface DbCall {
42
32
  * Uses neutralized positions to skip DB() inside strings/comments.
43
33
  */
44
34
  export declare function extractDbCalls(line: string): DbCall[];
45
- export declare function lintRules(text: string): RulesLintDiagnose[];
46
- /**
47
- * Runs server-side lint checks against an active TM1 connection:
48
- * - DB() argument count vs. actual cube dimension count
49
- * - Element existence within the referenced dimension
50
- *
51
- * Silently returns [] if the server is unreachable or no relevant refs are found.
52
- */
53
- export declare function lintRulesServer(text: string, api: ApiRequestFn): Promise<RulesLintDiagnose[]>;
54
35
  export {};
55
36
  //# sourceMappingURL=rulesLinter.d.ts.map