@ffschrattenecker/tm1-mcp-server 7.0.2 → 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 +83 -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 +1 -1
- package/dist/lib/cell-address.js +2 -2
- package/dist/lib/complexity/antipatterns.js +3 -1
- package/dist/lib/complexity/comment-classifier.js +2 -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/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 +51 -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 +33 -22
- package/dist/tools/celldata/execute-mdx.js +10 -9
- 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/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/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 +10 -9
- package/package.json +9 -5
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,87 @@ 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
|
+
|
|
10
91
|
## [7.0.2] - 2026-09-27
|
|
11
92
|
|
|
12
93
|
### Fixed
|
|
@@ -1324,7 +1405,8 @@ Initial public release.
|
|
|
1324
1405
|
- Quality gates: strict typecheck, ESLint, `lint:no-flat-api`,
|
|
1325
1406
|
annotation-coverage, and tool-registration wiring.
|
|
1326
1407
|
|
|
1327
|
-
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/
|
|
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
|
|
1328
1410
|
[7.0.2]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.1...v7.0.2
|
|
1329
1411
|
[7.0.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.0...v7.0.1
|
|
1330
1412
|
[7.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.1...v7.0.0
|
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
|
-
|
|
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
|
|
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 (
|
|
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 |
|
|
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** | **
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 =
|
|
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(", ")}.`);
|
package/dist/connections.d.ts
CHANGED
|
@@ -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). */
|
package/dist/connections.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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);
|
package/dist/http-transport.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|