@ffschrattenecker/tm1-mcp-server 6.0.0 → 7.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 +110 -1
- package/README.md +28 -7
- package/dist/config.d.ts +12 -1
- package/dist/config.js +120 -79
- package/dist/connections.d.ts +69 -0
- package/dist/connections.js +262 -0
- package/dist/http-transport.d.ts +2 -2
- package/dist/index.js +45 -37
- package/dist/lib/callgraph/tm1-adapter.d.ts +4 -1
- package/dist/lib/callgraph/tm1-adapter.js +5 -2
- package/dist/lib/slim-json-schema.d.ts +8 -0
- package/dist/lib/slim-json-schema.js +34 -0
- package/dist/lib/strip-comments.js +1 -1
- package/dist/lib/tm1-events.d.ts +2 -0
- package/dist/prompts/index.js +2 -2
- package/dist/resources/index.d.ts +4 -2
- package/dist/resources/index.js +93 -53
- package/dist/resources/subscriptions.d.ts +2 -1
- package/dist/resources/subscriptions.js +16 -7
- package/dist/server-instructions.d.ts +3 -0
- package/dist/server-instructions.js +12 -0
- package/dist/tm1-client/http.d.ts +1 -1
- package/dist/tm1-client/http.js +21 -4
- package/dist/tm1-client/services/process-service.d.ts +7 -1
- package/dist/tm1-client/services/process-service.js +7 -5
- package/dist/tm1-client.d.ts +7 -1
- package/dist/tm1-client.js +10 -11
- 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/audit-naming.js +1 -1
- package/dist/tools/analysis/delete-impact.d.ts +13 -0
- package/dist/tools/analysis/delete-impact.js +30 -0
- package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
- package/dist/tools/analysis/trace-data-flow.js +1 -1
- package/dist/tools/celldata/write-cells.js +37 -2
- package/dist/tools/confirm.d.ts +4 -1
- package/dist/tools/confirm.js +12 -2
- package/dist/tools/define-tool.d.ts +35 -11
- package/dist/tools/define-tool.js +51 -4
- package/dist/tools/dimension-management/delete-dimension.js +18 -4
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +6 -12
- package/dist/tools/metadata/list-cubes.js +3 -3
- package/dist/tools/metadata/list-processes.js +11 -7
- package/dist/tools/model-building/delete-cube.js +18 -4
- package/dist/tools/model-building/set-cube-rules.js +13 -1
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-audit-log.js +1 -1
- package/dist/tools/operations/get-jobs.js +2 -2
- package/dist/tools/operations/get-message-log.js +1 -1
- package/dist/tools/operations/get-server-info.js +10 -1
- package/dist/tools/operations/get-server-state.js +1 -1
- package/dist/tools/operations/get-threads.js +2 -2
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-connections.d.ts +2 -0
- package/dist/tools/operations/list-connections.js +35 -0
- package/dist/tools/operations/save-data.js +1 -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 +29 -4
- package/dist/tools/ti-development/import-pro-file.js +1 -1
- package/dist/tools/ti-development/preflight.d.ts +24 -0
- package/dist/tools/ti-development/preflight.js +32 -0
- package/dist/tools/ti-development/search-code.js +1 -1
- package/dist/tools/ti-development/upsert-process.js +100 -11
- package/dist/tools/with-annotations.d.ts +10 -0
- package/dist/tools/with-annotations.js +58 -3
- package/npm-shrinkwrap.json +4586 -0
- package/package.json +9 -7
- package/dist/tools/ti-development/get-process-code.d.ts +0 -2
- package/dist/tools/ti-development/get-process-code.js +0 -100
- package/dist/tools/ti-development/get-process-datasource.d.ts +0 -2
- package/dist/tools/ti-development/get-process-datasource.js +0 -20
- package/dist/tools/ti-development/get-process-parameters.d.ts +0 -2
- package/dist/tools/ti-development/get-process-parameters.js +0 -25
- package/dist/tools/ti-development/get-process-variables.d.ts +0 -2
- package/dist/tools/ti-development/get-process-variables.js +0 -38
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [7.0.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **One server for all TM1 connections.** Without `TM1_BASE_URL`, the server discovers every
|
|
15
|
+
`<name>/.env` under `~/.tm1/mcp-servers` (or `TM1_CONNECTIONS_DIR`) and every tool takes a
|
|
16
|
+
`connection` argument. Replace the per-instance MCP entries with a single one: the client then
|
|
17
|
+
carries one tool list instead of one per TM1 instance. Setups with `TM1_BASE_URL` keep working
|
|
18
|
+
unchanged as a single connection. See docs/CONFIGURATION.md, "Several TM1 connections".
|
|
19
|
+
- **`tm1_get_process_code`, `tm1_get_process_parameters`, `tm1_get_process_variables` and
|
|
20
|
+
`tm1_get_process_datasource` are removed.** `tm1_get_process` returns every part, each behind
|
|
21
|
+
an include-flag (`includeCode=false` for parameters/variables/datasource only). The datasource
|
|
22
|
+
now sits under `dataSource`.
|
|
23
|
+
- **`tm1_list_processes` returns names only by default.** Pass `fields=['name','parameters']`
|
|
24
|
+
for parameter lists; TM1 is then asked for the parameters too, instead of on every listing.
|
|
25
|
+
- **`tm1_list_cubes` defaults to `includeDimensions=false`.**
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `tm1_list_connections`: configured connections, their mode, environment, version and session state.
|
|
30
|
+
- `TM1_ENVIRONMENT` / `TM1_ALLOW_PROD_WRITES` are read per connection folder, never inherited from
|
|
31
|
+
the launching shell. A prod connection forced to readonly says so in `tm1_list_connections` and in
|
|
32
|
+
the refusal hint of a write tool.
|
|
33
|
+
- Oversized paginated results are cut to the items that fit, with `has_more`/`next_offset`
|
|
34
|
+
pointing at the rest, instead of failing with `RESPONSE_TOO_LARGE`.
|
|
35
|
+
- Server `instructions` (≤500 characters) tell the model how to pick a connection and keep
|
|
36
|
+
results small.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- The callgraph cache is keyed per connection, so an index built for one TM1 server is never
|
|
41
|
+
answered for another.
|
|
42
|
+
|
|
43
|
+
## [6.1.1] - 2026-09-26
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **Publishing.** The publish workflow handed npm the tarball as `pkg/<file>.tgz`, which npm reads
|
|
48
|
+
as a GitHub `user/repo` spec, so the 6.1.0 release failed at upload. Contents are identical to
|
|
49
|
+
6.1.0, which was tagged but never reached npm: install 6.1.1.
|
|
50
|
+
|
|
51
|
+
## [6.1.0] - 2026-09-26 [not published]
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **`tm1_delete_dimension` / `tm1_delete_cube` `dryRun`.** Returns every process and rule that
|
|
56
|
+
references the object, per source, plus the cubes a dimension is part of. Nothing is deleted,
|
|
57
|
+
and it needs no `confirm` (the field is now optional in the schema and still required on the real
|
|
58
|
+
call). Before, the check before a delete took `tm1_analyze_object_usage` plus
|
|
59
|
+
`tm1_find_orphan_dimensions` as separate calls. Elements are not covered: the usage index does
|
|
60
|
+
not resolve element references.
|
|
61
|
+
- **`TM1_ENVIRONMENT=dev|test|prod`, and `mode`/`environment` in `tm1_get_server_info`.**
|
|
62
|
+
`mcpServer` now carries `mode` (`readonly`/`readwrite`), `environment` (`unspecified` when
|
|
63
|
+
unset) and, when the mode was forced, `modeReason`. Before, a client could only guess which
|
|
64
|
+
environment it was on from free-text labels, and inferred readonly from missing tools.
|
|
65
|
+
`TM1_ENVIRONMENT=prod` forces readonly even under `TM1_MODE=readwrite`, unless
|
|
66
|
+
`TM1_ALLOW_PROD_WRITES=true` is set too. A readwrite `.env` copied from a dev instance is the
|
|
67
|
+
easy way to end up with write tools on production. An unknown value refuses to start, like
|
|
68
|
+
`TM1_MODE`.
|
|
69
|
+
- **`tm1_execute_process` attaches the run's error log.** When TM1 names an error log for the run
|
|
70
|
+
(a failure, or `HasMinorErrors` on a run that committed), the last 40 lines come back as
|
|
71
|
+
`errorLog`. A run that reports `CompletedWithMessages` can still have skipped every record, and
|
|
72
|
+
telling that apart used to need a `tm1_diagnose_process_error` call on every non-clean run. That
|
|
73
|
+
tool stays for cascade siblings and older logs.
|
|
74
|
+
- **`tm1_set_cube_rules` and `tm1_write_cells` read back what they wrote.** `set_cube_rules`
|
|
75
|
+
returns `verified.textMatches`; `write_cells` re-reads up to 20 of the written cells and lists
|
|
76
|
+
any whose stored value differs in `verified.mismatches` (a rule or spread overriding the value).
|
|
77
|
+
Each replaces a separate read-back call. A failed read-back is reported as `readBackError` and
|
|
78
|
+
never turns a landed write into an error.
|
|
79
|
+
- **`tm1_upsert_process` `dryRun`.** Runs the syntax and the reference check on the process as it
|
|
80
|
+
would be after the call, reports both (neither stops the other), and diffs it against the
|
|
81
|
+
installed version with credentials masked. Nothing is written. It needs no `confirm` and does not
|
|
82
|
+
count as one: the real overwrite still asks. Before, a caller that wanted the findings in front of
|
|
83
|
+
the user before the overwrite needed `tm1_check_process_code`, `tm1_validate_process_refs` and a
|
|
84
|
+
diff as three separate calls, because the install preflight only runs once the write is approved.
|
|
85
|
+
- **`tm1_upsert_process` reads the code back.** The result carries `verified: {codeMatches,
|
|
86
|
+
mismatchedTabs}` for the tabs it sent, so confirming the change landed no longer takes a
|
|
87
|
+
`tm1_get_process_code` call.
|
|
88
|
+
|
|
89
|
+
### ⚠️ Behavior change
|
|
90
|
+
|
|
91
|
+
- **Node.js 22.19 or newer is required.** Node 20 reached end of life in April 2026. CI now
|
|
92
|
+
tests Node 22 and 24.
|
|
93
|
+
|
|
94
|
+
### Security
|
|
95
|
+
|
|
96
|
+
- **Consumers now get the pinned dependency tree.** The package ships `npm-shrinkwrap.json`, so
|
|
97
|
+
`npx` installs the exact versions CI tested and audited. Before, the lockfile was never published
|
|
98
|
+
and dependencies resolved fresh on every install, so the 6.0.1 lockfile fixes did not reach users.
|
|
99
|
+
- **Hardened release pipeline.** npm publishing runs in a separate job that holds the OIDC
|
|
100
|
+
token and only uploads the tarball built and tested in the previous job.
|
|
101
|
+
|
|
102
|
+
### Note
|
|
103
|
+
|
|
104
|
+
- 6.0.1 was tagged but never published to npm (the tag push did not trigger the publish
|
|
105
|
+
workflow). Its fixes ship in the next release.
|
|
106
|
+
|
|
107
|
+
## [6.0.1] - 2026-09-25
|
|
108
|
+
|
|
109
|
+
### Security
|
|
110
|
+
|
|
111
|
+
- **Dependency advisories patched.** The lockfile now pins fixed versions of `fast-uri` (host
|
|
112
|
+
confusion / SSRF, high), `hono` and `@hono/node-server`, `qs` and `body-parser` (moderate / low),
|
|
113
|
+
all pulled in by the MCP SDK. No tool contract changes.
|
|
114
|
+
|
|
10
115
|
## [6.0.0] - 2026-09-25
|
|
11
116
|
|
|
12
117
|
### Breaking
|
|
@@ -1194,7 +1299,11 @@ Initial public release.
|
|
|
1194
1299
|
- Quality gates: strict typecheck, ESLint, `lint:no-flat-api`,
|
|
1195
1300
|
annotation-coverage, and tool-registration wiring.
|
|
1196
1301
|
|
|
1197
|
-
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/
|
|
1302
|
+
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.0...HEAD
|
|
1303
|
+
[7.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.1...v7.0.0
|
|
1304
|
+
[6.1.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.1.0...v6.1.1
|
|
1305
|
+
[6.1.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.0.1...v6.1.0
|
|
1306
|
+
[6.0.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v6.0.0...v6.0.1
|
|
1198
1307
|
[6.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v5.0.0...v6.0.0
|
|
1199
1308
|
[5.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v4.1.0...v5.0.0
|
|
1200
1309
|
[4.1.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v4.0.0...v4.1.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
|
+
112 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
|
|
|
@@ -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,27 @@ 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.
|
|
110
|
+
|
|
111
|
+
### Several TM1 servers — one process
|
|
112
|
+
|
|
113
|
+
Give each connection its own folder with a `.env` under `~/.tm1/mcp-servers/`
|
|
114
|
+
(or the folder named by `TM1_CONNECTIONS_DIR`) and leave `TM1_BASE_URL` unset:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
~/.tm1/mcp-servers/
|
|
118
|
+
dev/.env # TM1_BASE_URL=… TM1_MODE=readwrite
|
|
119
|
+
prod/.env # TM1_BASE_URL=… (readonly: the default)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
One server then serves them all: every tool takes a `connection` argument and
|
|
123
|
+
`tm1_list_connections` shows what is configured. Each folder sets its own
|
|
124
|
+
`TM1_MODE` — a write against a readonly connection is refused per call — and
|
|
125
|
+
no connection logs in before its first use. One entry replaces one MCP server
|
|
126
|
+
per TM1 instance, so the client carries one tool list instead of N.
|
|
127
|
+
[docs/CONFIGURATION.md](docs/CONFIGURATION.md#several-tm1-connections) has the details.
|
|
107
128
|
|
|
108
129
|
Host-disk file access is default-off in the same spirit: the `.pro` and git
|
|
109
130
|
tools accept inline content, and touch host paths only once
|
|
@@ -181,7 +202,7 @@ security notes and the `autoApprove` allowlist:
|
|
|
181
202
|
|
|
182
203
|
## Compatibility
|
|
183
204
|
|
|
184
|
-
- Node.js >=
|
|
205
|
+
- Node.js >= 22.19
|
|
185
206
|
- TM1 11.8 / Planning Analytics 2.0 — the primary target; some metadata-write
|
|
186
207
|
paths assume 11.x semantics, and v12-only fields (e.g.
|
|
187
208
|
`DataSource.usesUnicode`) are dropped when `TM1_VERSION` says `11.x`
|
|
@@ -197,7 +218,7 @@ security notes and the `autoApprove` allowlist:
|
|
|
197
218
|
|
|
198
219
|
<!-- TOOLS-AUTOGEN:START -->
|
|
199
220
|
|
|
200
|
-
## Tools (
|
|
221
|
+
## Tools (112)
|
|
201
222
|
|
|
202
223
|
Names and one-line descriptions: [docs/TOOLS.md](docs/TOOLS.md).
|
|
203
224
|
|
|
@@ -209,13 +230,13 @@ Names and one-line descriptions: [docs/TOOLS.md](docs/TOOLS.md).
|
|
|
209
230
|
| fileops | 5 |
|
|
210
231
|
| metadata | 9 |
|
|
211
232
|
| model-building | 9 |
|
|
212
|
-
| operations |
|
|
233
|
+
| operations | 16 |
|
|
213
234
|
| scheduling | 5 |
|
|
214
235
|
| security | 8 |
|
|
215
236
|
| subsets | 5 |
|
|
216
|
-
| ti-development |
|
|
237
|
+
| ti-development | 17 |
|
|
217
238
|
| views | 4 |
|
|
218
|
-
| **Total** | **
|
|
239
|
+
| **Total** | **112** |
|
|
219
240
|
|
|
220
241
|
<!-- TOOLS-AUTOGEN:END -->
|
|
221
242
|
|
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,14 @@ export interface TM1Config {
|
|
|
31
33
|
iamUrl?: string | undefined;
|
|
32
34
|
}
|
|
33
35
|
export declare const DEFAULT_MAX_RESPONSE_CHARS = 80000;
|
|
34
|
-
export
|
|
36
|
+
export type ServerSettings = Pick<TM1Config, "logLevel" | "logFile" | "transport" | "httpHost" | "httpPort" | "httpAllowedOrigins" | "httpToken" | "responseMode" | "maxResponseChars">;
|
|
37
|
+
export declare function loadServerSettings(env?: NodeJS.ProcessEnv): ServerSettings;
|
|
38
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): TM1Config;
|
|
39
|
+
/**
|
|
40
|
+
* Stable label for a connection: host and port, plus the v12
|
|
41
|
+
* instance/database. Keys per-connection state (process backups, the callgraph
|
|
42
|
+
* cache, mutation events) — two folders pointing at the same server share it,
|
|
43
|
+
* which is correct: they see the same objects.
|
|
44
|
+
*/
|
|
45
|
+
export declare function connectionIdOf(config: Pick<TM1Config, "baseUrl" | "instance" | "database">): string;
|
|
35
46
|
//# 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,59 +23,20 @@ function parseIntEnv(name, raw, def) {
|
|
|
22
23
|
}
|
|
23
24
|
return n;
|
|
24
25
|
}
|
|
25
|
-
export function
|
|
26
|
-
const
|
|
27
|
-
const user = process.env.TM1_USER;
|
|
28
|
-
const password = process.env.TM1_PASSWORD;
|
|
29
|
-
// CAM auth (mirrors TM1py's RestService._build_authorization_token):
|
|
30
|
-
// TM1_CAM_PASSPORT set → "CAMPassport <token>" (no user/password round-trip)
|
|
31
|
-
// TM1_NAMESPACE set → "CAMNamespace b64(u:p:ns)" (needs user + password + namespace)
|
|
32
|
-
// neither → "Basic b64(u:p)" (native TM1)
|
|
33
|
-
// SSO/gateway (Windows SSPI) is intentionally unsupported here: TM1py only does
|
|
34
|
-
// it via the Windows-only requests_negotiate_sspi package. Supply a passport
|
|
35
|
-
// obtained out-of-band via TM1_CAM_PASSPORT instead.
|
|
36
|
-
const namespace = process.env.TM1_NAMESPACE || undefined;
|
|
37
|
-
const camPassport = process.env.TM1_CAM_PASSPORT || undefined;
|
|
38
|
-
// Required: baseUrl always. user/password only when NOT using a passport — a
|
|
39
|
-
// passport carries the authenticated identity, so TM1 needs no credentials.
|
|
40
|
-
// Empty strings are rejected (treated as unset). Password may be empty — some
|
|
41
|
-
// TM1 setups allow a blank password for the admin account — so we warn but
|
|
42
|
-
// don't block, letting the real 401 (if any) surface with context.
|
|
43
|
-
const missing = [];
|
|
44
|
-
if (!baseUrl)
|
|
45
|
-
missing.push("TM1_BASE_URL");
|
|
46
|
-
if (!camPassport) {
|
|
47
|
-
if (!user)
|
|
48
|
-
missing.push("TM1_USER");
|
|
49
|
-
if (password === undefined)
|
|
50
|
-
missing.push("TM1_PASSWORD");
|
|
51
|
-
}
|
|
52
|
-
if (missing.length > 0) {
|
|
53
|
-
throw new Error(`Missing or empty required environment variables: ${missing.join(", ")}. ` +
|
|
54
|
-
`Set them in your shell or .env file before starting the server.`);
|
|
55
|
-
}
|
|
56
|
-
if (!camPassport && password === "") {
|
|
57
|
-
process.stderr.write("[tm1-mcp-server] WARNING: TM1_PASSWORD is empty. " +
|
|
58
|
-
"If TM1 rejects with 401, check whether the account actually allows blank passwords.\n");
|
|
59
|
-
}
|
|
60
|
-
const sslRaw = process.env.TM1_SSL_REJECT_UNAUTHORIZED;
|
|
61
|
-
const rejectUnauthorized = sslRaw === undefined ? true : sslRaw !== "false";
|
|
62
|
-
const keepAliveIntervalMs = parseIntEnv("TM1_KEEP_ALIVE_INTERVAL", process.env.TM1_KEEP_ALIVE_INTERVAL, 60000);
|
|
63
|
-
const requestTimeoutMs = parseIntEnv("TM1_REQUEST_TIMEOUT", process.env.TM1_REQUEST_TIMEOUT, 30000);
|
|
64
|
-
const logLevelRaw = process.env.TM1_LOG_LEVEL ?? "info";
|
|
26
|
+
export function loadServerSettings(env = process.env) {
|
|
27
|
+
const logLevelRaw = env.TM1_LOG_LEVEL ?? "info";
|
|
65
28
|
const logLevel = VALID_LOG_LEVELS.includes(logLevelRaw)
|
|
66
29
|
? logLevelRaw
|
|
67
30
|
: "info";
|
|
68
|
-
const logFile =
|
|
69
|
-
const
|
|
70
|
-
const transportRaw = process.env.TM1_MCP_TRANSPORT ?? "stdio";
|
|
31
|
+
const logFile = env.TM1_LOG_FILE || undefined;
|
|
32
|
+
const transportRaw = env.TM1_MCP_TRANSPORT ?? "stdio";
|
|
71
33
|
const transport = VALID_TRANSPORTS.includes(transportRaw)
|
|
72
34
|
? transportRaw
|
|
73
35
|
: "stdio";
|
|
74
36
|
// Default to loopback. Binding to 0.0.0.0 must be opt-in to avoid exposing
|
|
75
37
|
// a TM1-credentialed MCP server to the LAN by accident.
|
|
76
|
-
const httpHost =
|
|
77
|
-
const httpPort = parseIntEnv("TM1_MCP_HTTP_PORT",
|
|
38
|
+
const httpHost = env.TM1_MCP_HTTP_HOST || "127.0.0.1";
|
|
39
|
+
const httpPort = parseIntEnv("TM1_MCP_HTTP_PORT", env.TM1_MCP_HTTP_PORT, 3000);
|
|
78
40
|
// Origin allow-list for DNS-rebinding protection. Loopback origins are
|
|
79
41
|
// always included so localhost dev clients work out of the box; if the
|
|
80
42
|
// server binds to a non-loopback host we add http://<host>:<port> too.
|
|
@@ -89,7 +51,7 @@ export function loadConfig() {
|
|
|
89
51
|
httpHost !== "0.0.0.0") {
|
|
90
52
|
defaultOrigins.push(`http://${httpHost}:${httpPort}`);
|
|
91
53
|
}
|
|
92
|
-
const extraOriginsRaw =
|
|
54
|
+
const extraOriginsRaw = env.TM1_MCP_HTTP_ALLOWED_ORIGINS;
|
|
93
55
|
const extraOrigins = extraOriginsRaw
|
|
94
56
|
? extraOriginsRaw
|
|
95
57
|
.split(",")
|
|
@@ -97,7 +59,7 @@ export function loadConfig() {
|
|
|
97
59
|
.filter(Boolean)
|
|
98
60
|
: [];
|
|
99
61
|
const httpAllowedOrigins = Array.from(new Set([...defaultOrigins, ...extraOrigins]));
|
|
100
|
-
const httpToken =
|
|
62
|
+
const httpToken = env.TM1_MCP_HTTP_TOKEN || undefined;
|
|
101
63
|
// Refuse a non-loopback HTTP bind without transport auth: TM1_MCP_HTTP_HOST=0.0.0.0
|
|
102
64
|
// (or any LAN address) with no bearer token would expose an unauthenticated,
|
|
103
65
|
// TM1-credentialed /mcp endpoint to the network. Loopback binds keep the
|
|
@@ -111,31 +73,99 @@ export function loadConfig() {
|
|
|
111
73
|
`Refusing to expose an unauthenticated /mcp endpoint beyond localhost — set ` +
|
|
112
74
|
`TM1_MCP_HTTP_TOKEN, or bind to 127.0.0.1/localhost.`);
|
|
113
75
|
}
|
|
114
|
-
// Case-insensitive so a `TM1_MODE=ReadWrite` typo resolves to readwrite rather
|
|
115
|
-
// than silently falling back to readonly (dropping every write tool without a
|
|
116
|
-
// word). A genuinely-unknown value throws at startup — parity with the numeric
|
|
117
|
-
// env vars — instead of failing quietly.
|
|
118
|
-
const modeRaw = (process.env.TM1_MODE ?? "readonly").trim().toLowerCase();
|
|
119
|
-
if (!VALID_MODES.includes(modeRaw)) {
|
|
120
|
-
throw new Error(`Invalid TM1_MODE: "${process.env.TM1_MODE}". Expected "readwrite" or "readonly".`);
|
|
121
|
-
}
|
|
122
|
-
const mode = modeRaw;
|
|
123
76
|
// Same parse shape as TM1_MODE: case-insensitive, unknown value throws at
|
|
124
77
|
// startup rather than silently picking a wire format the operator did not ask
|
|
125
78
|
// for.
|
|
126
|
-
const responseModeRaw = (
|
|
79
|
+
const responseModeRaw = (env.TM1_RESPONSE_MODE ?? "legacy")
|
|
127
80
|
.trim()
|
|
128
81
|
.toLowerCase();
|
|
129
82
|
if (!VALID_RESPONSE_MODES.includes(responseModeRaw)) {
|
|
130
|
-
throw new Error(`Invalid TM1_RESPONSE_MODE: "${
|
|
83
|
+
throw new Error(`Invalid TM1_RESPONSE_MODE: "${env.TM1_RESPONSE_MODE}". Expected "legacy" or "structured".`);
|
|
131
84
|
}
|
|
132
85
|
const responseMode = responseModeRaw;
|
|
133
86
|
// ~80k characters sits under Claude Code's default MCP output cap (25k
|
|
134
87
|
// tokens) with room for the envelope; 23 recorded results overflowed it.
|
|
135
|
-
const maxResponseChars = parseIntEnv("TM1_MAX_RESPONSE_CHARS",
|
|
88
|
+
const maxResponseChars = parseIntEnv("TM1_MAX_RESPONSE_CHARS", env.TM1_MAX_RESPONSE_CHARS, DEFAULT_MAX_RESPONSE_CHARS);
|
|
89
|
+
return {
|
|
90
|
+
logLevel,
|
|
91
|
+
logFile,
|
|
92
|
+
transport,
|
|
93
|
+
httpHost,
|
|
94
|
+
httpPort,
|
|
95
|
+
httpAllowedOrigins,
|
|
96
|
+
httpToken,
|
|
97
|
+
responseMode,
|
|
98
|
+
maxResponseChars,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// `env` defaults to the process environment. The multi-connection registry
|
|
102
|
+
// passes one record per connection folder instead (see ./connections.ts).
|
|
103
|
+
export function loadConfig(env = process.env) {
|
|
104
|
+
const baseUrl = env.TM1_BASE_URL;
|
|
105
|
+
const user = env.TM1_USER;
|
|
106
|
+
const password = env.TM1_PASSWORD;
|
|
107
|
+
// CAM auth (mirrors TM1py's RestService._build_authorization_token):
|
|
108
|
+
// TM1_CAM_PASSPORT set → "CAMPassport <token>" (no user/password round-trip)
|
|
109
|
+
// TM1_NAMESPACE set → "CAMNamespace b64(u:p:ns)" (needs user + password + namespace)
|
|
110
|
+
// neither → "Basic b64(u:p)" (native TM1)
|
|
111
|
+
// SSO/gateway (Windows SSPI) is intentionally unsupported here: TM1py only does
|
|
112
|
+
// it via the Windows-only requests_negotiate_sspi package. Supply a passport
|
|
113
|
+
// obtained out-of-band via TM1_CAM_PASSPORT instead.
|
|
114
|
+
const namespace = env.TM1_NAMESPACE || undefined;
|
|
115
|
+
const camPassport = env.TM1_CAM_PASSPORT || undefined;
|
|
116
|
+
// Required: baseUrl always. user/password only when NOT using a passport — a
|
|
117
|
+
// passport carries the authenticated identity, so TM1 needs no credentials.
|
|
118
|
+
// Empty strings are rejected (treated as unset). Password may be empty — some
|
|
119
|
+
// TM1 setups allow a blank password for the admin account — so we warn but
|
|
120
|
+
// don't block, letting the real 401 (if any) surface with context.
|
|
121
|
+
const missing = [];
|
|
122
|
+
if (!baseUrl)
|
|
123
|
+
missing.push("TM1_BASE_URL");
|
|
124
|
+
if (!camPassport) {
|
|
125
|
+
if (!user)
|
|
126
|
+
missing.push("TM1_USER");
|
|
127
|
+
if (password === undefined)
|
|
128
|
+
missing.push("TM1_PASSWORD");
|
|
129
|
+
}
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
throw new Error(`Missing or empty required environment variables: ${missing.join(", ")}. ` +
|
|
132
|
+
`Set them in your shell or .env file before starting the server.`);
|
|
133
|
+
}
|
|
134
|
+
if (!camPassport && password === "") {
|
|
135
|
+
process.stderr.write("[tm1-mcp-server] WARNING: TM1_PASSWORD is empty. " +
|
|
136
|
+
"If TM1 rejects with 401, check whether the account actually allows blank passwords.\n");
|
|
137
|
+
}
|
|
138
|
+
const sslRaw = env.TM1_SSL_REJECT_UNAUTHORIZED;
|
|
139
|
+
const rejectUnauthorized = sslRaw === undefined ? true : sslRaw !== "false";
|
|
140
|
+
const keepAliveIntervalMs = parseIntEnv("TM1_KEEP_ALIVE_INTERVAL", env.TM1_KEEP_ALIVE_INTERVAL, 60000);
|
|
141
|
+
const requestTimeoutMs = parseIntEnv("TM1_REQUEST_TIMEOUT", env.TM1_REQUEST_TIMEOUT, 30000);
|
|
142
|
+
const server = loadServerSettings(env);
|
|
143
|
+
const tm1Version = env.TM1_VERSION || "11.8";
|
|
144
|
+
// Case-insensitive so a `TM1_MODE=ReadWrite` typo resolves to readwrite rather
|
|
145
|
+
// than silently falling back to readonly (dropping every write tool without a
|
|
146
|
+
// word). A genuinely-unknown value throws at startup — parity with the numeric
|
|
147
|
+
// env vars — instead of failing quietly.
|
|
148
|
+
const modeRaw = (env.TM1_MODE ?? "readonly").trim().toLowerCase();
|
|
149
|
+
if (!VALID_MODES.includes(modeRaw)) {
|
|
150
|
+
throw new Error(`Invalid TM1_MODE: "${env.TM1_MODE}". Expected "readwrite" or "readonly".`);
|
|
151
|
+
}
|
|
152
|
+
const envRaw = env.TM1_ENVIRONMENT?.trim().toLowerCase() || undefined;
|
|
153
|
+
if (envRaw !== undefined &&
|
|
154
|
+
!VALID_ENVIRONMENTS.includes(envRaw)) {
|
|
155
|
+
throw new Error(`Invalid TM1_ENVIRONMENT: "${env.TM1_ENVIRONMENT}". Expected "dev", "test" or "prod".`);
|
|
156
|
+
}
|
|
157
|
+
const environment = envRaw;
|
|
158
|
+
const allowProdWrites = env.TM1_ALLOW_PROD_WRITES?.trim().toLowerCase() === "true";
|
|
159
|
+
let mode = modeRaw;
|
|
160
|
+
let modeReason;
|
|
161
|
+
if (environment === "prod" && mode === "readwrite" && !allowProdWrites) {
|
|
162
|
+
mode = "readonly";
|
|
163
|
+
modeReason =
|
|
164
|
+
"TM1_ENVIRONMENT=prod forces readonly; set TM1_ALLOW_PROD_WRITES=true to allow writes.";
|
|
165
|
+
}
|
|
136
166
|
// --- v12 (Planning Analytics Engine) connection ---------------------------
|
|
137
|
-
const instance =
|
|
138
|
-
const database =
|
|
167
|
+
const instance = env.TM1_INSTANCE || undefined;
|
|
168
|
+
const database = env.TM1_DATABASE || undefined;
|
|
139
169
|
const versionMajor = Number.parseInt(tm1Version, 10);
|
|
140
170
|
const isV12 = Boolean(instance || database) || versionMajor === 12;
|
|
141
171
|
const version = isV12 ? 12 : 11;
|
|
@@ -159,19 +189,17 @@ export function loadConfig() {
|
|
|
159
189
|
if (!database) {
|
|
160
190
|
throw new Error("v12 connection requires TM1_DATABASE (set alongside TM1_INSTANCE).");
|
|
161
191
|
}
|
|
162
|
-
const authModeRaw = (
|
|
163
|
-
.trim()
|
|
164
|
-
.toLowerCase();
|
|
192
|
+
const authModeRaw = (env.TM1_AUTH_MODE ?? "s2s").trim().toLowerCase();
|
|
165
193
|
if (!VALID_AUTH_MODES.includes(authModeRaw)) {
|
|
166
|
-
throw new Error(`Invalid TM1_AUTH_MODE: "${
|
|
194
|
+
throw new Error(`Invalid TM1_AUTH_MODE: "${env.TM1_AUTH_MODE}". ` +
|
|
167
195
|
`Expected one of: ${VALID_AUTH_MODES.join(", ")}.`);
|
|
168
196
|
}
|
|
169
197
|
authMode = authModeRaw;
|
|
170
|
-
clientId =
|
|
171
|
-
clientSecret =
|
|
172
|
-
accessToken =
|
|
173
|
-
apiKey =
|
|
174
|
-
iamUrl =
|
|
198
|
+
clientId = env.TM1_CLIENT_ID || undefined;
|
|
199
|
+
clientSecret = env.TM1_CLIENT_SECRET || undefined;
|
|
200
|
+
accessToken = env.TM1_ACCESS_TOKEN || undefined;
|
|
201
|
+
apiKey = env.TM1_API_KEY || undefined;
|
|
202
|
+
iamUrl = env.TM1_IAM_URL || undefined;
|
|
175
203
|
const missingV12 = [];
|
|
176
204
|
// Every v12 mode — not just "basic" — sends `{ User: config.user }` in the
|
|
177
205
|
// session login body (profile.ts buildLoginRequest), so TM1_USER is required
|
|
@@ -203,6 +231,7 @@ export function loadConfig() {
|
|
|
203
231
|
}
|
|
204
232
|
}
|
|
205
233
|
return {
|
|
234
|
+
...server,
|
|
206
235
|
baseUrl: baseUrl,
|
|
207
236
|
// In passport mode user/password are unused; default to "" so the type stays
|
|
208
237
|
// a plain string and the Authorization header is built from the passport.
|
|
@@ -213,17 +242,10 @@ export function loadConfig() {
|
|
|
213
242
|
ssl: { rejectUnauthorized },
|
|
214
243
|
keepAliveIntervalMs,
|
|
215
244
|
requestTimeoutMs,
|
|
216
|
-
logLevel,
|
|
217
|
-
logFile,
|
|
218
245
|
tm1Version: effectiveTm1Version,
|
|
219
|
-
transport,
|
|
220
|
-
httpHost,
|
|
221
|
-
httpPort,
|
|
222
|
-
httpAllowedOrigins,
|
|
223
|
-
httpToken,
|
|
224
246
|
mode,
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
...(environment !== undefined ? { environment } : {}),
|
|
248
|
+
...(modeReason !== undefined ? { modeReason } : {}),
|
|
227
249
|
version,
|
|
228
250
|
instance,
|
|
229
251
|
database,
|
|
@@ -235,4 +257,23 @@ export function loadConfig() {
|
|
|
235
257
|
iamUrl,
|
|
236
258
|
};
|
|
237
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Stable label for a connection: host and port, plus the v12
|
|
262
|
+
* instance/database. Keys per-connection state (process backups, the callgraph
|
|
263
|
+
* cache, mutation events) — two folders pointing at the same server share it,
|
|
264
|
+
* which is correct: they see the same objects.
|
|
265
|
+
*/
|
|
266
|
+
export function connectionIdOf(config) {
|
|
267
|
+
let host;
|
|
268
|
+
try {
|
|
269
|
+
const url = new URL(config.baseUrl);
|
|
270
|
+
host = url.port ? `${url.hostname}_${url.port}` : url.hostname;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
host = config.baseUrl;
|
|
274
|
+
}
|
|
275
|
+
return [host, config.instance, config.database]
|
|
276
|
+
.filter((part) => Boolean(part))
|
|
277
|
+
.join("_");
|
|
278
|
+
}
|
|
238
279
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import { type TM1Config } from "./config.js";
|
|
3
|
+
import { TM1Client } from "./tm1-client.js";
|
|
4
|
+
export interface ConnectionInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
/** Folder the `.env` came from; undefined for the legacy single connection. */
|
|
7
|
+
dir?: string | undefined;
|
|
8
|
+
mode?: TM1Config["mode"] | undefined;
|
|
9
|
+
environment?: TM1Config["environment"];
|
|
10
|
+
/** Why mode differs from TM1_MODE (prod forces readonly). */
|
|
11
|
+
modeReason?: string | undefined;
|
|
12
|
+
version?: 11 | 12 | undefined;
|
|
13
|
+
tm1Version?: string | undefined;
|
|
14
|
+
baseUrl?: string | undefined;
|
|
15
|
+
/** connectionIdOf() — what mutation events and caches are keyed by. */
|
|
16
|
+
connectionId?: string | undefined;
|
|
17
|
+
/** Set when the folder's `.env` could not be turned into a config. */
|
|
18
|
+
configError?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
export interface ConnectionStatus extends ConnectionInfo {
|
|
21
|
+
connected: boolean;
|
|
22
|
+
lastError?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare class ConnectionRegistry {
|
|
25
|
+
private readonly logger;
|
|
26
|
+
private readonly entries;
|
|
27
|
+
private constructor();
|
|
28
|
+
/** Discover connections from the environment (see the header comment). */
|
|
29
|
+
static fromEnvironment(env: NodeJS.ProcessEnv, logger: pino.Logger): ConnectionRegistry;
|
|
30
|
+
/** Wrap one prebuilt client (tests, embedders). */
|
|
31
|
+
static single(client: TM1Client, logger?: pino.Logger): ConnectionRegistry;
|
|
32
|
+
/**
|
|
33
|
+
* Prebuilt clients under explicit names. The owner keeps the clients'
|
|
34
|
+
* lifecycle: disconnectAll() leaves them alone. Mode defaults to readwrite
|
|
35
|
+
* because the registration-time gate (TM1_MODE via withAnnotations) already
|
|
36
|
+
* decided what an embedder may call.
|
|
37
|
+
*/
|
|
38
|
+
static of(clients: ReadonlyArray<{
|
|
39
|
+
name: string;
|
|
40
|
+
client: TM1Client;
|
|
41
|
+
mode?: TM1Config["mode"];
|
|
42
|
+
}>, logger?: pino.Logger): ConnectionRegistry;
|
|
43
|
+
private addConfig;
|
|
44
|
+
private discover;
|
|
45
|
+
/** Every connection name, including ones whose config failed to load. */
|
|
46
|
+
get names(): string[];
|
|
47
|
+
/** Connections that can actually be used. */
|
|
48
|
+
get usableNames(): string[];
|
|
49
|
+
/** True when tools should not expose a `connection` argument at all. */
|
|
50
|
+
get isSingle(): boolean;
|
|
51
|
+
get anyReadwrite(): boolean;
|
|
52
|
+
hasVersion(version: 11 | 12): boolean;
|
|
53
|
+
/** A client exists and is not mid-login. Never triggers a login itself. */
|
|
54
|
+
isConnected(name: string): boolean;
|
|
55
|
+
info(name: string): ConnectionInfo | undefined;
|
|
56
|
+
status(): ConnectionStatus[];
|
|
57
|
+
/** Resolve a connection name (optional when there is only one). */
|
|
58
|
+
private entryFor;
|
|
59
|
+
/**
|
|
60
|
+
* The client for a connection, built and logged in on first use. A failed
|
|
61
|
+
* login does not poison the entry: the client's own request path retries
|
|
62
|
+
* authentication, so the next call gets a fresh attempt.
|
|
63
|
+
*/
|
|
64
|
+
get(name: string | undefined): Promise<TM1Client>;
|
|
65
|
+
/** Connection metadata for a resolved call (mode/version gates). */
|
|
66
|
+
resolveInfo(name: string | undefined): ConnectionInfo;
|
|
67
|
+
disconnectAll(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=connections.d.ts.map
|