@ffschrattenecker/tm1-mcp-server 6.0.0 → 6.1.1
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 +76 -1
- package/README.md +5 -2
- package/dist/config.d.ts +3 -1
- package/dist/config.js +50 -34
- package/dist/index.js +4 -1
- package/dist/tm1-client.d.ts +6 -0
- package/dist/tm1-client.js +8 -0
- package/dist/tools/analysis/analyze-object-usage.d.ts +10 -0
- package/dist/tools/analysis/analyze-object-usage.js +37 -33
- package/dist/tools/analysis/delete-impact.d.ts +13 -0
- package/dist/tools/analysis/delete-impact.js +30 -0
- package/dist/tools/celldata/write-cells.js +37 -2
- package/dist/tools/confirm.d.ts +4 -1
- package/dist/tools/confirm.js +11 -1
- package/dist/tools/dimension-management/delete-dimension.js +18 -4
- package/dist/tools/model-building/delete-cube.js +18 -4
- package/dist/tools/model-building/set-cube-rules.js +13 -1
- package/dist/tools/operations/get-server-info.js +10 -1
- package/dist/tools/schemas/items-monitoring.d.ts +11 -0
- package/dist/tools/schemas/items-monitoring.js +9 -1
- package/dist/tools/schemas/items-processes.d.ts +3 -0
- package/dist/tools/schemas/items-processes.js +2 -1
- package/dist/tools/ti-development/diff-processes.d.ts +42 -0
- package/dist/tools/ti-development/diff-processes.js +4 -4
- package/dist/tools/ti-development/execute-process.js +27 -2
- package/dist/tools/ti-development/preflight.d.ts +24 -0
- package/dist/tools/ti-development/preflight.js +32 -0
- package/dist/tools/ti-development/upsert-process.js +99 -10
- package/npm-shrinkwrap.json +4586 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [6.1.1] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Publishing.** The publish workflow handed npm the tarball as `pkg/<file>.tgz`, which npm reads
|
|
15
|
+
as a GitHub `user/repo` spec, so the 6.1.0 release failed at upload. Contents are identical to
|
|
16
|
+
6.1.0, which was tagged but never reached npm: install 6.1.1.
|
|
17
|
+
|
|
18
|
+
## [6.1.0] - 2026-09-26 [not published]
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`tm1_delete_dimension` / `tm1_delete_cube` `dryRun`.** Returns every process and rule that
|
|
23
|
+
references the object, per source, plus the cubes a dimension is part of. Nothing is deleted,
|
|
24
|
+
and it needs no `confirm` (the field is now optional in the schema and still required on the real
|
|
25
|
+
call). Before, the check before a delete took `tm1_analyze_object_usage` plus
|
|
26
|
+
`tm1_find_orphan_dimensions` as separate calls. Elements are not covered: the usage index does
|
|
27
|
+
not resolve element references.
|
|
28
|
+
- **`TM1_ENVIRONMENT=dev|test|prod`, and `mode`/`environment` in `tm1_get_server_info`.**
|
|
29
|
+
`mcpServer` now carries `mode` (`readonly`/`readwrite`), `environment` (`unspecified` when
|
|
30
|
+
unset) and, when the mode was forced, `modeReason`. Before, a client could only guess which
|
|
31
|
+
environment it was on from free-text labels, and inferred readonly from missing tools.
|
|
32
|
+
`TM1_ENVIRONMENT=prod` forces readonly even under `TM1_MODE=readwrite`, unless
|
|
33
|
+
`TM1_ALLOW_PROD_WRITES=true` is set too. A readwrite `.env` copied from a dev instance is the
|
|
34
|
+
easy way to end up with write tools on production. An unknown value refuses to start, like
|
|
35
|
+
`TM1_MODE`.
|
|
36
|
+
- **`tm1_execute_process` attaches the run's error log.** When TM1 names an error log for the run
|
|
37
|
+
(a failure, or `HasMinorErrors` on a run that committed), the last 40 lines come back as
|
|
38
|
+
`errorLog`. A run that reports `CompletedWithMessages` can still have skipped every record, and
|
|
39
|
+
telling that apart used to need a `tm1_diagnose_process_error` call on every non-clean run. That
|
|
40
|
+
tool stays for cascade siblings and older logs.
|
|
41
|
+
- **`tm1_set_cube_rules` and `tm1_write_cells` read back what they wrote.** `set_cube_rules`
|
|
42
|
+
returns `verified.textMatches`; `write_cells` re-reads up to 20 of the written cells and lists
|
|
43
|
+
any whose stored value differs in `verified.mismatches` (a rule or spread overriding the value).
|
|
44
|
+
Each replaces a separate read-back call. A failed read-back is reported as `readBackError` and
|
|
45
|
+
never turns a landed write into an error.
|
|
46
|
+
- **`tm1_upsert_process` `dryRun`.** Runs the syntax and the reference check on the process as it
|
|
47
|
+
would be after the call, reports both (neither stops the other), and diffs it against the
|
|
48
|
+
installed version with credentials masked. Nothing is written. It needs no `confirm` and does not
|
|
49
|
+
count as one: the real overwrite still asks. Before, a caller that wanted the findings in front of
|
|
50
|
+
the user before the overwrite needed `tm1_check_process_code`, `tm1_validate_process_refs` and a
|
|
51
|
+
diff as three separate calls, because the install preflight only runs once the write is approved.
|
|
52
|
+
- **`tm1_upsert_process` reads the code back.** The result carries `verified: {codeMatches,
|
|
53
|
+
mismatchedTabs}` for the tabs it sent, so confirming the change landed no longer takes a
|
|
54
|
+
`tm1_get_process_code` call.
|
|
55
|
+
|
|
56
|
+
### ⚠️ Behavior change
|
|
57
|
+
|
|
58
|
+
- **Node.js 22.19 or newer is required.** Node 20 reached end of life in April 2026. CI now
|
|
59
|
+
tests Node 22 and 24.
|
|
60
|
+
|
|
61
|
+
### Security
|
|
62
|
+
|
|
63
|
+
- **Consumers now get the pinned dependency tree.** The package ships `npm-shrinkwrap.json`, so
|
|
64
|
+
`npx` installs the exact versions CI tested and audited. Before, the lockfile was never published
|
|
65
|
+
and dependencies resolved fresh on every install, so the 6.0.1 lockfile fixes did not reach users.
|
|
66
|
+
- **Hardened release pipeline.** npm publishing runs in a separate job that holds the OIDC
|
|
67
|
+
token and only uploads the tarball built and tested in the previous job.
|
|
68
|
+
|
|
69
|
+
### Note
|
|
70
|
+
|
|
71
|
+
- 6.0.1 was tagged but never published to npm (the tag push did not trigger the publish
|
|
72
|
+
workflow). Its fixes ship in the next release.
|
|
73
|
+
|
|
74
|
+
## [6.0.1] - 2026-09-25
|
|
75
|
+
|
|
76
|
+
### Security
|
|
77
|
+
|
|
78
|
+
- **Dependency advisories patched.** The lockfile now pins fixed versions of `fast-uri` (host
|
|
79
|
+
confusion / SSRF, high), `hono` and `@hono/node-server`, `qs` and `body-parser` (moderate / low),
|
|
80
|
+
all pulled in by the MCP SDK. No tool contract changes.
|
|
81
|
+
|
|
10
82
|
## [6.0.0] - 2026-09-25
|
|
11
83
|
|
|
12
84
|
### Breaking
|
|
@@ -1194,7 +1266,10 @@ Initial public release.
|
|
|
1194
1266
|
- Quality gates: strict typecheck, ESLint, `lint:no-flat-api`,
|
|
1195
1267
|
annotation-coverage, and tool-registration wiring.
|
|
1196
1268
|
|
|
1197
|
-
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.
|
|
1269
|
+
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.1...HEAD
|
|
1270
|
+
[6.1.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.0...v6.1.1
|
|
1271
|
+
[6.1.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.0.1...v6.1.0
|
|
1272
|
+
[6.0.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.0.0...v6.0.1
|
|
1198
1273
|
[6.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v5.0.0...v6.0.0
|
|
1199
1274
|
[5.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v4.1.0...v5.0.0
|
|
1200
1275
|
[4.1.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v4.0.0...v4.1.0
|
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ TM1_PASSWORD=your-password
|
|
|
86
86
|
TM1_SSL_REJECT_UNAUTHORIZED=false
|
|
87
87
|
TM1_VERSION=11.8
|
|
88
88
|
TM1_MODE=readonly # readonly (default) | readwrite
|
|
89
|
+
# TM1_ENVIRONMENT=prod # dev | test | prod; prod forces readonly
|
|
89
90
|
# TM1_RESPONSE_MODE=structured # legacy (default) | structured
|
|
90
91
|
# TM1_MAX_RESPONSE_CHARS=80000 # larger results fail with RESPONSE_TOO_LARGE
|
|
91
92
|
# TM1_LOCAL_FILE_ROOT=/srv/tm1-git # optional; enables host-disk file params
|
|
@@ -103,7 +104,9 @@ Analytics Engine) connection and its auth modes, host-disk file access.
|
|
|
103
104
|
> tools are registered, so it cannot mutate or delete anything. Set
|
|
104
105
|
> `TM1_MODE=readwrite` explicitly to enable the full lifecycle (cell writes,
|
|
105
106
|
> cube/dimension/process deletion, TI execution), and never point a `readwrite`
|
|
106
|
-
> server at production without reviewing the write path first.
|
|
107
|
+
> server at production without reviewing the write path first. Label production
|
|
108
|
+
> with `TM1_ENVIRONMENT=prod`: it then stays readonly even under
|
|
109
|
+
> `TM1_MODE=readwrite`, unless `TM1_ALLOW_PROD_WRITES=true` is set as well.
|
|
107
110
|
|
|
108
111
|
Host-disk file access is default-off in the same spirit: the `.pro` and git
|
|
109
112
|
tools accept inline content, and touch host paths only once
|
|
@@ -181,7 +184,7 @@ security notes and the `autoApprove` allowlist:
|
|
|
181
184
|
|
|
182
185
|
## Compatibility
|
|
183
186
|
|
|
184
|
-
- Node.js >=
|
|
187
|
+
- Node.js >= 22.19
|
|
185
188
|
- TM1 11.8 / Planning Analytics 2.0 — the primary target; some metadata-write
|
|
186
189
|
paths assume 11.x semantics, and v12-only fields (e.g.
|
|
187
190
|
`DataSource.usesUnicode`) are dropped when `TM1_VERSION` says `11.x`
|
package/dist/config.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface TM1Config {
|
|
|
18
18
|
httpAllowedOrigins: string[];
|
|
19
19
|
httpToken?: string | undefined;
|
|
20
20
|
mode: "readwrite" | "readonly";
|
|
21
|
+
environment?: "dev" | "test" | "prod" | undefined;
|
|
22
|
+
modeReason?: string | undefined;
|
|
21
23
|
responseMode: "legacy" | "structured";
|
|
22
24
|
maxResponseChars: number;
|
|
23
25
|
version: 11 | 12;
|
|
@@ -31,5 +33,5 @@ export interface TM1Config {
|
|
|
31
33
|
iamUrl?: string | undefined;
|
|
32
34
|
}
|
|
33
35
|
export declare const DEFAULT_MAX_RESPONSE_CHARS = 80000;
|
|
34
|
-
export declare function loadConfig(): TM1Config;
|
|
36
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): TM1Config;
|
|
35
37
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const VALID_LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
2
2
|
const VALID_TRANSPORTS = ["stdio", "http"];
|
|
3
3
|
const VALID_MODES = ["readwrite", "readonly"];
|
|
4
|
+
const VALID_ENVIRONMENTS = ["dev", "test", "prod"];
|
|
4
5
|
const VALID_RESPONSE_MODES = ["legacy", "structured"];
|
|
5
6
|
const VALID_AUTH_MODES = [
|
|
6
7
|
"s2s",
|
|
@@ -22,10 +23,12 @@ function parseIntEnv(name, raw, def) {
|
|
|
22
23
|
}
|
|
23
24
|
return n;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
26
|
+
// `env` defaults to the process environment. The multi-connection registry
|
|
27
|
+
// passes one record per connection folder instead (see ./connections.ts).
|
|
28
|
+
export function loadConfig(env = process.env) {
|
|
29
|
+
const baseUrl = env.TM1_BASE_URL;
|
|
30
|
+
const user = env.TM1_USER;
|
|
31
|
+
const password = env.TM1_PASSWORD;
|
|
29
32
|
// CAM auth (mirrors TM1py's RestService._build_authorization_token):
|
|
30
33
|
// TM1_CAM_PASSPORT set → "CAMPassport <token>" (no user/password round-trip)
|
|
31
34
|
// TM1_NAMESPACE set → "CAMNamespace b64(u:p:ns)" (needs user + password + namespace)
|
|
@@ -33,8 +36,8 @@ export function loadConfig() {
|
|
|
33
36
|
// SSO/gateway (Windows SSPI) is intentionally unsupported here: TM1py only does
|
|
34
37
|
// it via the Windows-only requests_negotiate_sspi package. Supply a passport
|
|
35
38
|
// obtained out-of-band via TM1_CAM_PASSPORT instead.
|
|
36
|
-
const namespace =
|
|
37
|
-
const camPassport =
|
|
39
|
+
const namespace = env.TM1_NAMESPACE || undefined;
|
|
40
|
+
const camPassport = env.TM1_CAM_PASSPORT || undefined;
|
|
38
41
|
// Required: baseUrl always. user/password only when NOT using a passport — a
|
|
39
42
|
// passport carries the authenticated identity, so TM1 needs no credentials.
|
|
40
43
|
// Empty strings are rejected (treated as unset). Password may be empty — some
|
|
@@ -57,24 +60,24 @@ export function loadConfig() {
|
|
|
57
60
|
process.stderr.write("[tm1-mcp-server] WARNING: TM1_PASSWORD is empty. " +
|
|
58
61
|
"If TM1 rejects with 401, check whether the account actually allows blank passwords.\n");
|
|
59
62
|
}
|
|
60
|
-
const sslRaw =
|
|
63
|
+
const sslRaw = env.TM1_SSL_REJECT_UNAUTHORIZED;
|
|
61
64
|
const rejectUnauthorized = sslRaw === undefined ? true : sslRaw !== "false";
|
|
62
|
-
const keepAliveIntervalMs = parseIntEnv("TM1_KEEP_ALIVE_INTERVAL",
|
|
63
|
-
const requestTimeoutMs = parseIntEnv("TM1_REQUEST_TIMEOUT",
|
|
64
|
-
const logLevelRaw =
|
|
65
|
+
const keepAliveIntervalMs = parseIntEnv("TM1_KEEP_ALIVE_INTERVAL", env.TM1_KEEP_ALIVE_INTERVAL, 60000);
|
|
66
|
+
const requestTimeoutMs = parseIntEnv("TM1_REQUEST_TIMEOUT", env.TM1_REQUEST_TIMEOUT, 30000);
|
|
67
|
+
const logLevelRaw = env.TM1_LOG_LEVEL ?? "info";
|
|
65
68
|
const logLevel = VALID_LOG_LEVELS.includes(logLevelRaw)
|
|
66
69
|
? logLevelRaw
|
|
67
70
|
: "info";
|
|
68
|
-
const logFile =
|
|
69
|
-
const tm1Version =
|
|
70
|
-
const transportRaw =
|
|
71
|
+
const logFile = env.TM1_LOG_FILE || undefined;
|
|
72
|
+
const tm1Version = env.TM1_VERSION || "11.8";
|
|
73
|
+
const transportRaw = env.TM1_MCP_TRANSPORT ?? "stdio";
|
|
71
74
|
const transport = VALID_TRANSPORTS.includes(transportRaw)
|
|
72
75
|
? transportRaw
|
|
73
76
|
: "stdio";
|
|
74
77
|
// Default to loopback. Binding to 0.0.0.0 must be opt-in to avoid exposing
|
|
75
78
|
// a TM1-credentialed MCP server to the LAN by accident.
|
|
76
|
-
const httpHost =
|
|
77
|
-
const httpPort = parseIntEnv("TM1_MCP_HTTP_PORT",
|
|
79
|
+
const httpHost = env.TM1_MCP_HTTP_HOST || "127.0.0.1";
|
|
80
|
+
const httpPort = parseIntEnv("TM1_MCP_HTTP_PORT", env.TM1_MCP_HTTP_PORT, 3000);
|
|
78
81
|
// Origin allow-list for DNS-rebinding protection. Loopback origins are
|
|
79
82
|
// always included so localhost dev clients work out of the box; if the
|
|
80
83
|
// server binds to a non-loopback host we add http://<host>:<port> too.
|
|
@@ -89,7 +92,7 @@ export function loadConfig() {
|
|
|
89
92
|
httpHost !== "0.0.0.0") {
|
|
90
93
|
defaultOrigins.push(`http://${httpHost}:${httpPort}`);
|
|
91
94
|
}
|
|
92
|
-
const extraOriginsRaw =
|
|
95
|
+
const extraOriginsRaw = env.TM1_MCP_HTTP_ALLOWED_ORIGINS;
|
|
93
96
|
const extraOrigins = extraOriginsRaw
|
|
94
97
|
? extraOriginsRaw
|
|
95
98
|
.split(",")
|
|
@@ -97,7 +100,7 @@ export function loadConfig() {
|
|
|
97
100
|
.filter(Boolean)
|
|
98
101
|
: [];
|
|
99
102
|
const httpAllowedOrigins = Array.from(new Set([...defaultOrigins, ...extraOrigins]));
|
|
100
|
-
const httpToken =
|
|
103
|
+
const httpToken = env.TM1_MCP_HTTP_TOKEN || undefined;
|
|
101
104
|
// Refuse a non-loopback HTTP bind without transport auth: TM1_MCP_HTTP_HOST=0.0.0.0
|
|
102
105
|
// (or any LAN address) with no bearer token would expose an unauthenticated,
|
|
103
106
|
// TM1-credentialed /mcp endpoint to the network. Loopback binds keep the
|
|
@@ -115,27 +118,40 @@ export function loadConfig() {
|
|
|
115
118
|
// than silently falling back to readonly (dropping every write tool without a
|
|
116
119
|
// word). A genuinely-unknown value throws at startup — parity with the numeric
|
|
117
120
|
// env vars — instead of failing quietly.
|
|
118
|
-
const modeRaw = (
|
|
121
|
+
const modeRaw = (env.TM1_MODE ?? "readonly").trim().toLowerCase();
|
|
119
122
|
if (!VALID_MODES.includes(modeRaw)) {
|
|
120
|
-
throw new Error(`Invalid TM1_MODE: "${
|
|
123
|
+
throw new Error(`Invalid TM1_MODE: "${env.TM1_MODE}". Expected "readwrite" or "readonly".`);
|
|
124
|
+
}
|
|
125
|
+
const envRaw = env.TM1_ENVIRONMENT?.trim().toLowerCase() || undefined;
|
|
126
|
+
if (envRaw !== undefined &&
|
|
127
|
+
!VALID_ENVIRONMENTS.includes(envRaw)) {
|
|
128
|
+
throw new Error(`Invalid TM1_ENVIRONMENT: "${env.TM1_ENVIRONMENT}". Expected "dev", "test" or "prod".`);
|
|
129
|
+
}
|
|
130
|
+
const environment = envRaw;
|
|
131
|
+
const allowProdWrites = env.TM1_ALLOW_PROD_WRITES?.trim().toLowerCase() === "true";
|
|
132
|
+
let mode = modeRaw;
|
|
133
|
+
let modeReason;
|
|
134
|
+
if (environment === "prod" && mode === "readwrite" && !allowProdWrites) {
|
|
135
|
+
mode = "readonly";
|
|
136
|
+
modeReason =
|
|
137
|
+
"TM1_ENVIRONMENT=prod forces readonly; set TM1_ALLOW_PROD_WRITES=true to allow writes.";
|
|
121
138
|
}
|
|
122
|
-
const mode = modeRaw;
|
|
123
139
|
// Same parse shape as TM1_MODE: case-insensitive, unknown value throws at
|
|
124
140
|
// startup rather than silently picking a wire format the operator did not ask
|
|
125
141
|
// for.
|
|
126
|
-
const responseModeRaw = (
|
|
142
|
+
const responseModeRaw = (env.TM1_RESPONSE_MODE ?? "legacy")
|
|
127
143
|
.trim()
|
|
128
144
|
.toLowerCase();
|
|
129
145
|
if (!VALID_RESPONSE_MODES.includes(responseModeRaw)) {
|
|
130
|
-
throw new Error(`Invalid TM1_RESPONSE_MODE: "${
|
|
146
|
+
throw new Error(`Invalid TM1_RESPONSE_MODE: "${env.TM1_RESPONSE_MODE}". Expected "legacy" or "structured".`);
|
|
131
147
|
}
|
|
132
148
|
const responseMode = responseModeRaw;
|
|
133
149
|
// ~80k characters sits under Claude Code's default MCP output cap (25k
|
|
134
150
|
// tokens) with room for the envelope; 23 recorded results overflowed it.
|
|
135
|
-
const maxResponseChars = parseIntEnv("TM1_MAX_RESPONSE_CHARS",
|
|
151
|
+
const maxResponseChars = parseIntEnv("TM1_MAX_RESPONSE_CHARS", env.TM1_MAX_RESPONSE_CHARS, DEFAULT_MAX_RESPONSE_CHARS);
|
|
136
152
|
// --- v12 (Planning Analytics Engine) connection ---------------------------
|
|
137
|
-
const instance =
|
|
138
|
-
const database =
|
|
153
|
+
const instance = env.TM1_INSTANCE || undefined;
|
|
154
|
+
const database = env.TM1_DATABASE || undefined;
|
|
139
155
|
const versionMajor = Number.parseInt(tm1Version, 10);
|
|
140
156
|
const isV12 = Boolean(instance || database) || versionMajor === 12;
|
|
141
157
|
const version = isV12 ? 12 : 11;
|
|
@@ -159,19 +175,17 @@ export function loadConfig() {
|
|
|
159
175
|
if (!database) {
|
|
160
176
|
throw new Error("v12 connection requires TM1_DATABASE (set alongside TM1_INSTANCE).");
|
|
161
177
|
}
|
|
162
|
-
const authModeRaw = (
|
|
163
|
-
.trim()
|
|
164
|
-
.toLowerCase();
|
|
178
|
+
const authModeRaw = (env.TM1_AUTH_MODE ?? "s2s").trim().toLowerCase();
|
|
165
179
|
if (!VALID_AUTH_MODES.includes(authModeRaw)) {
|
|
166
|
-
throw new Error(`Invalid TM1_AUTH_MODE: "${
|
|
180
|
+
throw new Error(`Invalid TM1_AUTH_MODE: "${env.TM1_AUTH_MODE}". ` +
|
|
167
181
|
`Expected one of: ${VALID_AUTH_MODES.join(", ")}.`);
|
|
168
182
|
}
|
|
169
183
|
authMode = authModeRaw;
|
|
170
|
-
clientId =
|
|
171
|
-
clientSecret =
|
|
172
|
-
accessToken =
|
|
173
|
-
apiKey =
|
|
174
|
-
iamUrl =
|
|
184
|
+
clientId = env.TM1_CLIENT_ID || undefined;
|
|
185
|
+
clientSecret = env.TM1_CLIENT_SECRET || undefined;
|
|
186
|
+
accessToken = env.TM1_ACCESS_TOKEN || undefined;
|
|
187
|
+
apiKey = env.TM1_API_KEY || undefined;
|
|
188
|
+
iamUrl = env.TM1_IAM_URL || undefined;
|
|
175
189
|
const missingV12 = [];
|
|
176
190
|
// Every v12 mode — not just "basic" — sends `{ User: config.user }` in the
|
|
177
191
|
// session login body (profile.ts buildLoginRequest), so TM1_USER is required
|
|
@@ -222,6 +236,8 @@ export function loadConfig() {
|
|
|
222
236
|
httpAllowedOrigins,
|
|
223
237
|
httpToken,
|
|
224
238
|
mode,
|
|
239
|
+
...(environment !== undefined ? { environment } : {}),
|
|
240
|
+
...(modeReason !== undefined ? { modeReason } : {}),
|
|
225
241
|
responseMode,
|
|
226
242
|
maxResponseChars,
|
|
227
243
|
version,
|
package/dist/index.js
CHANGED
|
@@ -77,7 +77,10 @@ async function main() {
|
|
|
77
77
|
catch (err) {
|
|
78
78
|
logger.warn({ err }, "Initial TM1 connection failed — server will retry on first tool call");
|
|
79
79
|
}
|
|
80
|
-
if (config.
|
|
80
|
+
if (config.modeReason) {
|
|
81
|
+
logger.warn(`${config.modeReason} Write and destructive tools will not be registered.`);
|
|
82
|
+
}
|
|
83
|
+
else if (config.mode === "readonly") {
|
|
81
84
|
logger.info("TM1_MODE=readonly — write and destructive tools will not be registered");
|
|
82
85
|
}
|
|
83
86
|
// Branch on transport. stdio is the default for local MCP-client setups
|
package/dist/tm1-client.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export declare class TM1Client {
|
|
|
38
38
|
constructor(config: TM1Config, sessionManager: SessionManager, logger: pino.Logger);
|
|
39
39
|
/** The configured TM1 major version (11 or 12). Fixed for this connection. */
|
|
40
40
|
get version(): 11 | 12;
|
|
41
|
+
/** Effective tool mode, the environment label, and why mode was forced. */
|
|
42
|
+
get access(): {
|
|
43
|
+
mode: TM1Config["mode"];
|
|
44
|
+
environment: TM1Config["environment"];
|
|
45
|
+
modeReason: string | undefined;
|
|
46
|
+
};
|
|
41
47
|
/**
|
|
42
48
|
* Stable label for this connection, used to keep per-connection files (the
|
|
43
49
|
* process backups) apart: host and port, plus the v12 instance/database.
|
package/dist/tm1-client.js
CHANGED
|
@@ -82,6 +82,14 @@ export class TM1Client {
|
|
|
82
82
|
get version() {
|
|
83
83
|
return this.config.version;
|
|
84
84
|
}
|
|
85
|
+
/** Effective tool mode, the environment label, and why mode was forced. */
|
|
86
|
+
get access() {
|
|
87
|
+
return {
|
|
88
|
+
mode: this.config.mode,
|
|
89
|
+
environment: this.config.environment,
|
|
90
|
+
modeReason: this.config.modeReason,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
85
93
|
/**
|
|
86
94
|
* Stable label for this connection, used to keep per-connection files (the
|
|
87
95
|
* process backups) apart: host and port, plus the v12 instance/database.
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
import { buildCubeOrDimUsages } from "../../lib/callgraph/callGraph.js";
|
|
1
2
|
export declare const registerAnalyzeObjectUsage: import("../define-tool.js").ToolRegistrar;
|
|
3
|
+
/** One row per referencing process or rule, most references first. */
|
|
4
|
+
export declare function summarizeBySource(all: ReturnType<typeof buildCubeOrDimUsages>): {
|
|
5
|
+
sourceKind: "process" | "rule";
|
|
6
|
+
sourceName: string;
|
|
7
|
+
accessTypes: string[];
|
|
8
|
+
sections: string[];
|
|
9
|
+
funcNames: string[];
|
|
10
|
+
count: number;
|
|
11
|
+
}[];
|
|
2
12
|
//# sourceMappingURL=analyze-object-usage.d.ts.map
|
|
@@ -64,39 +64,7 @@ export const registerAnalyzeObjectUsage = defineTool({
|
|
|
64
64
|
accessMode,
|
|
65
65
|
});
|
|
66
66
|
if (mode === "summary") {
|
|
67
|
-
|
|
68
|
-
// and a cube-rule sharing a name don't collapse into one row.
|
|
69
|
-
const bySource = new Map();
|
|
70
|
-
for (const u of all) {
|
|
71
|
-
const key = `${u.sourceKind}\x00${u.sourceName}`;
|
|
72
|
-
let s = bySource.get(key);
|
|
73
|
-
if (!s) {
|
|
74
|
-
s = {
|
|
75
|
-
sourceKind: u.sourceKind,
|
|
76
|
-
sourceName: u.sourceName,
|
|
77
|
-
accessTypes: new Set(),
|
|
78
|
-
sections: new Set(),
|
|
79
|
-
funcNames: new Set(),
|
|
80
|
-
count: 0,
|
|
81
|
-
};
|
|
82
|
-
bySource.set(key, s);
|
|
83
|
-
}
|
|
84
|
-
s.accessTypes.add(u.accessType);
|
|
85
|
-
s.sections.add(u.section);
|
|
86
|
-
if (u.funcName)
|
|
87
|
-
s.funcNames.add(u.funcName);
|
|
88
|
-
s.count++;
|
|
89
|
-
}
|
|
90
|
-
const allSources = [...bySource.values()]
|
|
91
|
-
.map((s) => ({
|
|
92
|
-
sourceKind: s.sourceKind,
|
|
93
|
-
sourceName: s.sourceName,
|
|
94
|
-
accessTypes: [...s.accessTypes].sort(),
|
|
95
|
-
sections: [...s.sections].sort(),
|
|
96
|
-
funcNames: [...s.funcNames].sort(),
|
|
97
|
-
count: s.count,
|
|
98
|
-
}))
|
|
99
|
-
.sort((a, b) => b.count - a.count || a.sourceName.localeCompare(b.sourceName));
|
|
67
|
+
const allSources = summarizeBySource(all);
|
|
100
68
|
const sumTruncated = allSources.length > effectiveLimit;
|
|
101
69
|
const sources = sumTruncated
|
|
102
70
|
? allSources.slice(0, effectiveLimit)
|
|
@@ -140,4 +108,40 @@ export const registerAnalyzeObjectUsage = defineTool({
|
|
|
140
108
|
};
|
|
141
109
|
},
|
|
142
110
|
});
|
|
111
|
+
/** One row per referencing process or rule, most references first. */
|
|
112
|
+
export function summarizeBySource(all) {
|
|
113
|
+
// Aggregate per source (process or rule). Key by kind+name so a process
|
|
114
|
+
// and a cube-rule sharing a name don't collapse into one row.
|
|
115
|
+
const bySource = new Map();
|
|
116
|
+
for (const u of all) {
|
|
117
|
+
const key = `${u.sourceKind}\x00${u.sourceName}`;
|
|
118
|
+
let s = bySource.get(key);
|
|
119
|
+
if (!s) {
|
|
120
|
+
s = {
|
|
121
|
+
sourceKind: u.sourceKind,
|
|
122
|
+
sourceName: u.sourceName,
|
|
123
|
+
accessTypes: new Set(),
|
|
124
|
+
sections: new Set(),
|
|
125
|
+
funcNames: new Set(),
|
|
126
|
+
count: 0,
|
|
127
|
+
};
|
|
128
|
+
bySource.set(key, s);
|
|
129
|
+
}
|
|
130
|
+
s.accessTypes.add(u.accessType);
|
|
131
|
+
s.sections.add(u.section);
|
|
132
|
+
if (u.funcName)
|
|
133
|
+
s.funcNames.add(u.funcName);
|
|
134
|
+
s.count++;
|
|
135
|
+
}
|
|
136
|
+
return [...bySource.values()]
|
|
137
|
+
.map((s) => ({
|
|
138
|
+
sourceKind: s.sourceKind,
|
|
139
|
+
sourceName: s.sourceName,
|
|
140
|
+
accessTypes: [...s.accessTypes].sort(),
|
|
141
|
+
sections: [...s.sections].sort(),
|
|
142
|
+
funcNames: [...s.funcNames].sort(),
|
|
143
|
+
count: s.count,
|
|
144
|
+
}))
|
|
145
|
+
.sort((a, b) => b.count - a.count || a.sourceName.localeCompare(b.sourceName));
|
|
146
|
+
}
|
|
143
147
|
//# sourceMappingURL=analyze-object-usage.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TM1Client } from "../../tm1-client.js";
|
|
2
|
+
export declare function deleteImpact(tm1Client: TM1Client, kind: "cube" | "dimension", name: string): Promise<{
|
|
3
|
+
sources: {
|
|
4
|
+
sourceKind: "process" | "rule";
|
|
5
|
+
sourceName: string;
|
|
6
|
+
accessTypes: string[];
|
|
7
|
+
count: number;
|
|
8
|
+
}[];
|
|
9
|
+
truncated?: boolean;
|
|
10
|
+
referencingSources: number;
|
|
11
|
+
usedInCubes?: string[];
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=delete-impact.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildIndexFromTM1 } from "../../lib/callgraph/tm1-adapter.js";
|
|
2
|
+
import { buildCubeOrDimUsages } from "../../lib/callgraph/callGraph.js";
|
|
3
|
+
import { summarizeBySource } from "./analyze-object-usage.js";
|
|
4
|
+
const MAX_SOURCES = 50;
|
|
5
|
+
export async function deleteImpact(tm1Client, kind, name) {
|
|
6
|
+
const [index, cubes] = await Promise.all([
|
|
7
|
+
buildIndexFromTM1(tm1Client, { includeControl: false }),
|
|
8
|
+
kind === "dimension" ? tm1Client.cubes.list() : Promise.resolve([]),
|
|
9
|
+
]);
|
|
10
|
+
const sources = summarizeBySource(buildCubeOrDimUsages(index, kind, name, {
|
|
11
|
+
includeSystem: false,
|
|
12
|
+
accessMode: "all",
|
|
13
|
+
}));
|
|
14
|
+
const key = name.toLowerCase();
|
|
15
|
+
const usedInCubes = cubes
|
|
16
|
+
.filter((c) => (c.dimensions ?? []).some((d) => d.toLowerCase() === key))
|
|
17
|
+
.map((c) => c.name);
|
|
18
|
+
return {
|
|
19
|
+
...(kind === "dimension" ? { usedInCubes } : {}),
|
|
20
|
+
referencingSources: sources.length,
|
|
21
|
+
...(sources.length > MAX_SOURCES ? { truncated: true } : {}),
|
|
22
|
+
sources: sources.slice(0, MAX_SOURCES).map((s) => ({
|
|
23
|
+
sourceKind: s.sourceKind,
|
|
24
|
+
sourceName: s.sourceName,
|
|
25
|
+
accessTypes: s.accessTypes,
|
|
26
|
+
count: s.count,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=delete-impact.js.map
|
|
@@ -6,6 +6,12 @@ import { MutationResultSchema } from "../schemas/items.js";
|
|
|
6
6
|
import { defineTool } from "../define-tool.js";
|
|
7
7
|
import { dimensionCountMismatch } from "../../lib/coordinate-error.js";
|
|
8
8
|
import { resolveCellAddress } from "../../lib/cell-address.js";
|
|
9
|
+
const READ_BACK_LIMIT = 20;
|
|
10
|
+
function sameValue(sent, stored) {
|
|
11
|
+
if (typeof sent === "number")
|
|
12
|
+
return Number(stored) === sent;
|
|
13
|
+
return String(stored ?? "") === sent;
|
|
14
|
+
}
|
|
9
15
|
export const registerWriteCells = defineTool({
|
|
10
16
|
name: "tm1_write_cells",
|
|
11
17
|
description: [
|
|
@@ -14,7 +20,8 @@ export const registerWriteCells = defineTool({
|
|
|
14
20
|
"Writes to consolidated cells are rejected by TM1.",
|
|
15
21
|
"Every cube dimension must be named (any order) — a dimension left out would land on its default member, so the call is refused. Only Sandboxes may be left out: it is bound to Base and echoed as sandboxDefaulted. A named sandbox is addressable only once its IncludeInSandboxDimension is true.",
|
|
16
22
|
"Before: tm1_check_writable_coords to validate that target coordinates are leaf-level and addressable.",
|
|
17
|
-
"
|
|
23
|
+
"The written cells are read back (up to 20; verified.mismatches lists any that differ, e.g. a rule or spread overriding the value), so no separate tm1_get_cell_value is needed.",
|
|
24
|
+
"Related: tm1_clear_cube for bulk wipe, tm1_execute_process for production data loads.",
|
|
18
25
|
],
|
|
19
26
|
annotations: DESTRUCTIVE,
|
|
20
27
|
output: MutationResultSchema,
|
|
@@ -52,10 +59,38 @@ export const registerWriteCells = defineTool({
|
|
|
52
59
|
// propagate to the index.ts Proxy unwrapped — wrapping it here would
|
|
53
60
|
// clobber that hint with a generic one.
|
|
54
61
|
const address = resolveCellAddress(cubeName, await tm1Client.cubes.getDimensionNames(cubeName), dimensions);
|
|
55
|
-
|
|
62
|
+
const ordered = cells.map((c) => ({
|
|
63
|
+
...c,
|
|
64
|
+
elements: address.toCubeOrder(c.elements),
|
|
65
|
+
}));
|
|
66
|
+
await tm1Client.cells.writeCells(cubeName, address.dimensions, ordered);
|
|
67
|
+
// Read back a bounded sample: each read is its own MDX round trip. The
|
|
68
|
+
// write has landed by now, so a failed read-back is reported, not thrown.
|
|
69
|
+
const sample = ordered.slice(0, READ_BACK_LIMIT);
|
|
70
|
+
let verified;
|
|
71
|
+
try {
|
|
72
|
+
const readBack = await Promise.all(sample.map((c) => tm1Client.cells.getValue(cubeName, c.elements)));
|
|
73
|
+
verified = {
|
|
74
|
+
checked: sample.length,
|
|
75
|
+
...(cells.length > sample.length
|
|
76
|
+
? { unchecked: cells.length - sample.length }
|
|
77
|
+
: {}),
|
|
78
|
+
mismatches: sample
|
|
79
|
+
.map((c, i) => ({
|
|
80
|
+
elements: c.elements,
|
|
81
|
+
sent: c.value,
|
|
82
|
+
stored: readBack[i],
|
|
83
|
+
}))
|
|
84
|
+
.filter((m) => !sameValue(m.sent, m.stored)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
verified = { checked: 0, readBackError: e.message };
|
|
89
|
+
}
|
|
56
90
|
return actionResponse({
|
|
57
91
|
success: true,
|
|
58
92
|
cellsWritten: cells.length,
|
|
93
|
+
verified,
|
|
59
94
|
...(address.sandboxDefaulted
|
|
60
95
|
? { sandboxDefaulted: address.sandboxDefaulted }
|
|
61
96
|
: {}),
|
package/dist/tools/confirm.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const CONFIRM_SCHEMA: {
|
|
3
3
|
confirm: z.ZodString;
|
|
4
4
|
};
|
|
5
|
-
export declare
|
|
5
|
+
export declare const DRY_RUN_CONFIRM_SCHEMA: {
|
|
6
|
+
confirm: z.ZodOptional<z.ZodString>;
|
|
7
|
+
};
|
|
8
|
+
export declare function requireConfirm(provided: string | undefined, target: string, kind: string): void;
|
|
6
9
|
export declare const OVERWRITE_CONFIRM_SCHEMA: {
|
|
7
10
|
confirm: z.ZodOptional<z.ZodString>;
|
|
8
11
|
};
|
package/dist/tools/confirm.js
CHANGED
|
@@ -29,11 +29,21 @@ export const CONFIRM_SCHEMA = {
|
|
|
29
29
|
.string()
|
|
30
30
|
.describe("Safety check: repeat the target identifier verbatim (e.g. cube/dimension/process name) to confirm this irreversible action. Mismatched values reject the call."),
|
|
31
31
|
};
|
|
32
|
+
// For a destructive tool with a dryRun: a dry run needs no confirm, so the
|
|
33
|
+
// field is optional in the schema and required at runtime on the real call.
|
|
34
|
+
export const DRY_RUN_CONFIRM_SCHEMA = {
|
|
35
|
+
confirm: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Required unless dryRun=true: repeat the target identifier verbatim to confirm this irreversible action. A dry run is not a confirmation."),
|
|
39
|
+
};
|
|
32
40
|
export function requireConfirm(provided, target, kind) {
|
|
33
41
|
if (provided !== target) {
|
|
34
42
|
throw new TM1Error({
|
|
35
43
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
36
|
-
message:
|
|
44
|
+
message: provided === undefined
|
|
45
|
+
? `This call is irreversible for ${kind} "${target}" and needs confirm="${target}". Nothing was changed.`
|
|
46
|
+
: `confirm mismatch — expected ${kind} name "${target}", got "${provided}".`,
|
|
37
47
|
hint: `Re-issue the call with confirm="${target}" verbatim. This safety check prevents accidental destructive operations.`,
|
|
38
48
|
});
|
|
39
49
|
}
|
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { actionResponse } from "../format.js";
|
|
3
|
-
import {
|
|
3
|
+
import { DRY_RUN_CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
4
4
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
5
5
|
import { MutationResultSchema } from "../schemas/items.js";
|
|
6
6
|
import { defineTool } from "../define-tool.js";
|
|
7
|
+
import { deleteImpact } from "../analysis/delete-impact.js";
|
|
7
8
|
export const registerDeleteDimension = defineTool({
|
|
8
9
|
name: "tm1_delete_dimension",
|
|
9
10
|
description: [
|
|
10
11
|
"Delete a TM1 dimension and all its hierarchies. Warning: fails if the dimension is used in a cube.",
|
|
11
|
-
"Before:
|
|
12
|
+
"Before: dryRun=true lists the cubes that contain it and every process or rule that references it, in one call.",
|
|
12
13
|
],
|
|
13
14
|
annotations: DESTRUCTIVE,
|
|
14
15
|
output: MutationResultSchema,
|
|
15
16
|
input: {
|
|
16
17
|
dimensionName: z.string().describe("Dimension name (case-sensitive)"),
|
|
17
|
-
|
|
18
|
+
dryRun: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe("Delete nothing: return what references the dimension (processes and rules, per source) and the cubes it is part of. Needs no confirm and is not one."),
|
|
23
|
+
...DRY_RUN_CONFIRM_SCHEMA,
|
|
18
24
|
},
|
|
19
|
-
handler: async ({ dimensionName, confirm }, tm1Client) => {
|
|
25
|
+
handler: async ({ dimensionName, dryRun, confirm }, tm1Client) => {
|
|
26
|
+
if (dryRun) {
|
|
27
|
+
return actionResponse({
|
|
28
|
+
success: true,
|
|
29
|
+
dryRun: true,
|
|
30
|
+
dimensionName,
|
|
31
|
+
impact: await deleteImpact(tm1Client, "dimension", dimensionName),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
20
34
|
requireConfirm(confirm, dimensionName, "dimension");
|
|
21
35
|
await tm1Client.dimensions.delete(dimensionName);
|
|
22
36
|
return actionResponse({ success: true, dimensionName });
|