@ffschrattenecker/tm1-mcp-server 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -2
- package/README.md +4 -1
- package/dist/config.d.ts +10 -1
- package/dist/config.js +7 -6
- package/dist/connections.d.ts +12 -1
- package/dist/connections.js +62 -5
- package/dist/index.js +6 -0
- package/dist/lib/sample-cells.js +1 -1
- package/dist/secrets-cli.d.ts +10 -0
- package/dist/secrets-cli.js +218 -0
- package/dist/secrets.d.ts +39 -0
- package/dist/secrets.js +128 -0
- package/dist/tools/model-building/clear-cube.js +1 -21
- package/dist/tools/schemas/items-fileops.d.ts +1 -1
- package/npm-shrinkwrap.json +237 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [8.1.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Connection secrets can live in the OS keychain instead of the `.env`: set `TM1_SECRETS=keychain`
|
|
15
|
+
in a connection folder and store the secrets with `npx tm1-mcp-server secrets set|list|delete`,
|
|
16
|
+
or move an existing `.env` over with `secrets migrate <connection>`. The keychain is read the
|
|
17
|
+
first time a connection is used, never when connections are listed. At startup the server warns
|
|
18
|
+
about connections that still keep plaintext secrets. See
|
|
19
|
+
[docs/CONFIGURATION.md](docs/CONFIGURATION.md#secrets-in-the-os-keychain--tm1_secretskeychain).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`tm1_clear_cube` no longer declares `dimensions`/`tuples`.** They are dropped like any
|
|
24
|
+
unknown input, so an old call that still sends them empties the whole cube once `confirm`
|
|
25
|
+
matches. *Action:* remove them from stored calls; a partial clear is a TI process.
|
|
26
|
+
|
|
10
27
|
## [8.0.0] - 2026-09-28
|
|
11
28
|
|
|
12
29
|
Merged upstream tm1-mcp-server 5.0.0 (flameY3T1, 2026-09-27). Where both lines had
|
|
@@ -86,7 +103,7 @@ response-size guard, the per-connection caches) are unchanged.
|
|
|
86
103
|
- **Corrected descriptions** of `tm1_unload_cube`, `tm1_clear_cube`, `tm1_delete_hierarchy`,
|
|
87
104
|
`tm1_get_cube_stats` and `tm1_get_transaction_log`.
|
|
88
105
|
- **The wire contracts were re-recorded against 11.8** upstream and merged with the fork's
|
|
89
|
-
|
|
106
|
+
own recordings; the recorder now merges by default (`--replace` starts over).
|
|
90
107
|
|
|
91
108
|
## [7.0.2] - 2026-09-27
|
|
92
109
|
|
|
@@ -1405,7 +1422,8 @@ Initial public release.
|
|
|
1405
1422
|
- Quality gates: strict typecheck, ESLint, `lint:no-flat-api`,
|
|
1406
1423
|
annotation-coverage, and tool-registration wiring.
|
|
1407
1424
|
|
|
1408
|
-
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v8.
|
|
1425
|
+
[Unreleased]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v8.1.0...HEAD
|
|
1426
|
+
[8.1.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v8.0.0...v8.1.0
|
|
1409
1427
|
[8.0.0]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.2...v8.0.0
|
|
1410
1428
|
[7.0.2]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.1...v7.0.2
|
|
1411
1429
|
[7.0.1]: https://github.com/ffschrattenecker/tm1-mcp-server/compare/v7.0.0...v7.0.1
|
package/README.md
CHANGED
|
@@ -171,7 +171,10 @@ its report, just redacted.
|
|
|
171
171
|
Copy `mcp.json.example` to `.mcp.json` (project-local) or merge it into
|
|
172
172
|
`~/.claude/settings.json`. **Do not put `TM1_PASSWORD` in either file** — those
|
|
173
173
|
are routinely shared or committed; keep it in a gitignored `.env`
|
|
174
|
-
([where the server looks for one](docs/CONFIGURATION.md#where-credentials-are-read-from))
|
|
174
|
+
([where the server looks for one](docs/CONFIGURATION.md#where-credentials-are-read-from)),
|
|
175
|
+
or out of files altogether in the OS keychain with
|
|
176
|
+
`npx tm1-mcp-server secrets migrate <connection>`
|
|
177
|
+
([details](docs/CONFIGURATION.md#secrets-in-the-os-keychain--tm1_secretskeychain)).
|
|
175
178
|
|
|
176
179
|
For the `npx` install:
|
|
177
180
|
|
package/dist/config.d.ts
CHANGED
|
@@ -35,7 +35,16 @@ export interface TM1Config {
|
|
|
35
35
|
export declare const DEFAULT_MAX_RESPONSE_CHARS = 80000;
|
|
36
36
|
export type ServerSettings = Pick<TM1Config, "logLevel" | "logFile" | "transport" | "httpHost" | "httpPort" | "httpAllowedOrigins" | "httpToken" | "responseMode" | "maxResponseChars">;
|
|
37
37
|
export declare function loadServerSettings(env?: NodeJS.ProcessEnv): ServerSettings;
|
|
38
|
-
export
|
|
38
|
+
export interface LoadConfigOptions {
|
|
39
|
+
/**
|
|
40
|
+
* The secrets are not in `env` yet: they are read from the OS keychain when
|
|
41
|
+
* the connection is first used (see ./secrets.ts), and loadConfig runs again
|
|
42
|
+
* then, with them. Until then a missing secret is not an error, and neither
|
|
43
|
+
* is a missing TM1_USER — the keychain may hold a TM1_CAM_PASSPORT.
|
|
44
|
+
*/
|
|
45
|
+
deferSecrets?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv, options?: LoadConfigOptions): TM1Config;
|
|
39
48
|
/**
|
|
40
49
|
* Stable label for a connection: host and port, plus the v12
|
|
41
50
|
* instance/database. Keys per-connection state (process backups, the callgraph
|
package/dist/config.js
CHANGED
|
@@ -106,7 +106,8 @@ export function loadServerSettings(env = process.env) {
|
|
|
106
106
|
}
|
|
107
107
|
// `env` defaults to the process environment. The multi-connection registry
|
|
108
108
|
// passes one record per connection folder instead (see ./connections.ts).
|
|
109
|
-
export function loadConfig(env = process.env) {
|
|
109
|
+
export function loadConfig(env = process.env, options = {}) {
|
|
110
|
+
const deferred = options.deferSecrets === true;
|
|
110
111
|
const baseUrl = env.TM1_BASE_URL;
|
|
111
112
|
const user = env.TM1_USER;
|
|
112
113
|
const password = env.TM1_PASSWORD;
|
|
@@ -143,7 +144,7 @@ export function loadConfig(env = process.env) {
|
|
|
143
144
|
const missing = [];
|
|
144
145
|
if (!baseUrl)
|
|
145
146
|
missing.push("TM1_BASE_URL");
|
|
146
|
-
if (!camPassport) {
|
|
147
|
+
if (!camPassport && !deferred) {
|
|
147
148
|
if (!user)
|
|
148
149
|
missing.push("TM1_USER");
|
|
149
150
|
if (password === undefined && !passwordlessV12) {
|
|
@@ -154,7 +155,7 @@ export function loadConfig(env = process.env) {
|
|
|
154
155
|
throw new Error(`Missing or empty required environment variables: ${missing.join(", ")}. ` +
|
|
155
156
|
`Set them in your shell or .env file before starting the server.`);
|
|
156
157
|
}
|
|
157
|
-
if (!camPassport && !passwordlessV12 && password === "") {
|
|
158
|
+
if (!camPassport && !passwordlessV12 && !deferred && password === "") {
|
|
158
159
|
process.stderr.write("[tm1-mcp-server] WARNING: TM1_PASSWORD is empty. " +
|
|
159
160
|
"If TM1 rejects with 401, check whether the account actually allows blank passwords.\n");
|
|
160
161
|
}
|
|
@@ -229,15 +230,15 @@ export function loadConfig(env = process.env) {
|
|
|
229
230
|
if (authMode === "s2s") {
|
|
230
231
|
if (!clientId)
|
|
231
232
|
missingV12.push("TM1_CLIENT_ID");
|
|
232
|
-
if (!clientSecret)
|
|
233
|
+
if (!clientSecret && !deferred)
|
|
233
234
|
missingV12.push("TM1_CLIENT_SECRET");
|
|
234
235
|
}
|
|
235
236
|
else if (authMode === "access_token" || authMode === "oidc") {
|
|
236
|
-
if (!accessToken)
|
|
237
|
+
if (!accessToken && !deferred)
|
|
237
238
|
missingV12.push("TM1_ACCESS_TOKEN");
|
|
238
239
|
}
|
|
239
240
|
else if (authMode === "iam") {
|
|
240
|
-
if (!apiKey)
|
|
241
|
+
if (!apiKey && !deferred)
|
|
241
242
|
missingV12.push("TM1_API_KEY");
|
|
242
243
|
if (!iamUrl)
|
|
243
244
|
missingV12.push("TM1_IAM_URL");
|
package/dist/connections.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
2
|
import { type TM1Config } from "./config.js";
|
|
3
|
+
import { type SecretStore } from "./secrets.js";
|
|
3
4
|
import { TM1Client } from "./tm1-client.js";
|
|
4
5
|
/** Folder holding one `<name>/.env` per connection. */
|
|
5
6
|
export declare function connectionsDir(env: NodeJS.ProcessEnv): string;
|
|
@@ -30,6 +31,8 @@ export interface ConnectionInfo {
|
|
|
30
31
|
connectionId?: string | undefined;
|
|
31
32
|
/** Set when the folder's `.env` could not be turned into a config. */
|
|
32
33
|
configError?: string | undefined;
|
|
34
|
+
/** "keychain" when the secrets are read from the OS keychain on first use. */
|
|
35
|
+
secrets?: "keychain" | undefined;
|
|
33
36
|
}
|
|
34
37
|
export interface ConnectionStatus extends ConnectionInfo {
|
|
35
38
|
connected: boolean;
|
|
@@ -37,10 +40,14 @@ export interface ConnectionStatus extends ConnectionInfo {
|
|
|
37
40
|
}
|
|
38
41
|
export declare class ConnectionRegistry {
|
|
39
42
|
private readonly logger;
|
|
43
|
+
/** Defaults to the OS keychain; tests inject a fake. */
|
|
44
|
+
private readonly secretStore?;
|
|
40
45
|
private readonly entries;
|
|
41
46
|
private constructor();
|
|
42
47
|
/** Discover connections from the environment (see the header comment). */
|
|
43
|
-
static fromEnvironment(env: NodeJS.ProcessEnv, logger: pino.Logger
|
|
48
|
+
static fromEnvironment(env: NodeJS.ProcessEnv, logger: pino.Logger, options?: {
|
|
49
|
+
secretStore?: SecretStore;
|
|
50
|
+
}): ConnectionRegistry;
|
|
44
51
|
/** Wrap one prebuilt client (tests, embedders). */
|
|
45
52
|
static single(client: TM1Client, logger?: pino.Logger): ConnectionRegistry;
|
|
46
53
|
/**
|
|
@@ -54,6 +61,8 @@ export declare class ConnectionRegistry {
|
|
|
54
61
|
client: TM1Client;
|
|
55
62
|
mode?: TM1Config["mode"];
|
|
56
63
|
}>, logger?: pino.Logger): ConnectionRegistry;
|
|
64
|
+
/** Validate one connection's env; keychain secrets are read later. */
|
|
65
|
+
private addEnv;
|
|
57
66
|
private addConfig;
|
|
58
67
|
private discover;
|
|
59
68
|
/** Every connection name, including ones whose config failed to load. */
|
|
@@ -79,6 +88,8 @@ export declare class ConnectionRegistry {
|
|
|
79
88
|
* until the server restarts.
|
|
80
89
|
*/
|
|
81
90
|
get(name: string | undefined): Promise<TM1Client>;
|
|
91
|
+
/** The full config of a keychain connection, secrets included. */
|
|
92
|
+
private resolveSecrets;
|
|
82
93
|
/** Connection metadata for a resolved call (mode/version gates). */
|
|
83
94
|
resolveInfo(name: string | undefined): ConnectionInfo;
|
|
84
95
|
disconnectAll(): Promise<void>;
|
package/dist/connections.js
CHANGED
|
@@ -18,12 +18,17 @@
|
|
|
18
18
|
// defaults to readonly: a connection-level key (TM1_MODE, TM1_BASE_URL, …) in
|
|
19
19
|
// the server's own environment is NOT inherited, so a TM1_MODE=readwrite in
|
|
20
20
|
// the launching shell cannot silently arm every connection.
|
|
21
|
+
//
|
|
22
|
+
// A folder with TM1_SECRETS=keychain keeps its secrets in the OS keychain
|
|
23
|
+
// (see ./secrets.ts). Discovery validates it without them; get() reads them
|
|
24
|
+
// and builds the real config on first use.
|
|
21
25
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
22
26
|
import { homedir } from "node:os";
|
|
23
27
|
import { join } from "node:path";
|
|
24
28
|
import { parse as parseDotenv } from "dotenv";
|
|
25
29
|
import pino from "pino";
|
|
26
30
|
import { connectionIdOf, loadConfig } from "./config.js";
|
|
31
|
+
import { keychainHint, plaintextSecretKeys, usesKeychain, withKeychainSecrets, } from "./secrets.js";
|
|
27
32
|
import { SessionManager } from "./session-manager.js";
|
|
28
33
|
import { TM1Client } from "./tm1-client.js";
|
|
29
34
|
import { TM1Error, TM1ErrorCode } from "./types.js";
|
|
@@ -69,16 +74,20 @@ export function connectionEnv(folder, env) {
|
|
|
69
74
|
}
|
|
70
75
|
export class ConnectionRegistry {
|
|
71
76
|
logger;
|
|
77
|
+
secretStore;
|
|
72
78
|
entries = new Map();
|
|
73
|
-
constructor(logger
|
|
79
|
+
constructor(logger,
|
|
80
|
+
/** Defaults to the OS keychain; tests inject a fake. */
|
|
81
|
+
secretStore) {
|
|
74
82
|
this.logger = logger;
|
|
83
|
+
this.secretStore = secretStore;
|
|
75
84
|
}
|
|
76
85
|
/** Discover connections from the environment (see the header comment). */
|
|
77
|
-
static fromEnvironment(env, logger) {
|
|
78
|
-
const registry = new ConnectionRegistry(logger);
|
|
86
|
+
static fromEnvironment(env, logger, options = {}) {
|
|
87
|
+
const registry = new ConnectionRegistry(logger, options.secretStore);
|
|
79
88
|
const explicitDir = env.TM1_CONNECTIONS_DIR;
|
|
80
89
|
if (!explicitDir && env.TM1_BASE_URL) {
|
|
81
|
-
registry.
|
|
90
|
+
registry.addEnv("default", env);
|
|
82
91
|
return registry;
|
|
83
92
|
}
|
|
84
93
|
const dir = connectionsDir(env);
|
|
@@ -123,6 +132,17 @@ export class ConnectionRegistry {
|
|
|
123
132
|
}
|
|
124
133
|
return registry;
|
|
125
134
|
}
|
|
135
|
+
/** Validate one connection's env; keychain secrets are read later. */
|
|
136
|
+
addEnv(name, env, dir) {
|
|
137
|
+
const keychain = usesKeychain(env);
|
|
138
|
+
const config = loadConfig(env, { deferSecrets: keychain });
|
|
139
|
+
this.addConfig(name, config, dir);
|
|
140
|
+
if (keychain) {
|
|
141
|
+
const entry = this.entries.get(name);
|
|
142
|
+
entry.env = env;
|
|
143
|
+
entry.info.secrets = "keychain";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
126
146
|
addConfig(name, config, dir) {
|
|
127
147
|
this.entries.set(name, {
|
|
128
148
|
info: {
|
|
@@ -152,10 +172,16 @@ export class ConnectionRegistry {
|
|
|
152
172
|
.map((d) => d.name)
|
|
153
173
|
.filter((name) => !only || only.has(name))
|
|
154
174
|
.sort((a, b) => a.localeCompare(b));
|
|
175
|
+
const plaintext = [];
|
|
155
176
|
for (const name of names) {
|
|
156
177
|
const folder = join(dir, name);
|
|
157
178
|
try {
|
|
158
|
-
|
|
179
|
+
const connEnv = connectionEnv(folder, env);
|
|
180
|
+
this.addEnv(name, connEnv, folder);
|
|
181
|
+
if (!this.entries.get(name).env &&
|
|
182
|
+
plaintextSecretKeys(connEnv).length) {
|
|
183
|
+
plaintext.push(name);
|
|
184
|
+
}
|
|
159
185
|
}
|
|
160
186
|
catch (err) {
|
|
161
187
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -165,6 +191,10 @@ export class ConnectionRegistry {
|
|
|
165
191
|
});
|
|
166
192
|
}
|
|
167
193
|
}
|
|
194
|
+
if (plaintext.length > 0) {
|
|
195
|
+
this.logger.warn({ connections: plaintext }, "plaintext secrets in .env — move them to the OS keychain with " +
|
|
196
|
+
"`npx tm1-mcp-server secrets migrate <connection>`");
|
|
197
|
+
}
|
|
168
198
|
}
|
|
169
199
|
/** Every connection name, including ones whose config failed to load. */
|
|
170
200
|
get names() {
|
|
@@ -236,6 +266,19 @@ export class ConnectionRegistry {
|
|
|
236
266
|
*/
|
|
237
267
|
async get(name) {
|
|
238
268
|
const entry = this.entryFor(name);
|
|
269
|
+
if (!entry.client && entry.env) {
|
|
270
|
+
// Concurrent first calls share one keychain read. A failure is not
|
|
271
|
+
// cached: the next call reads again, after the user stored the entry.
|
|
272
|
+
entry.resolving ??= this.resolveSecrets(entry)
|
|
273
|
+
.catch((err) => {
|
|
274
|
+
entry.lastError = err instanceof Error ? err.message : String(err);
|
|
275
|
+
throw err;
|
|
276
|
+
})
|
|
277
|
+
.finally(() => {
|
|
278
|
+
entry.resolving = undefined;
|
|
279
|
+
});
|
|
280
|
+
entry.config = await entry.resolving;
|
|
281
|
+
}
|
|
239
282
|
if (!entry.client) {
|
|
240
283
|
const config = entry.config;
|
|
241
284
|
const logger = this.logger.child({ connection: entry.info.name });
|
|
@@ -258,6 +301,20 @@ export class ConnectionRegistry {
|
|
|
258
301
|
await entry.connecting;
|
|
259
302
|
return entry.client;
|
|
260
303
|
}
|
|
304
|
+
/** The full config of a keychain connection, secrets included. */
|
|
305
|
+
async resolveSecrets(entry) {
|
|
306
|
+
const { name } = entry.info;
|
|
307
|
+
const env = await withKeychainSecrets(name, entry.env, this.secretStore);
|
|
308
|
+
try {
|
|
309
|
+
return loadConfig(env);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
throw new TM1Error({
|
|
313
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
314
|
+
message: `Connection "${name}": ${err instanceof Error ? err.message : String(err)}${keychainHint(name)}`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
261
318
|
/** Connection metadata for a resolved call (mode/version gates). */
|
|
262
319
|
resolveInfo(name) {
|
|
263
320
|
return this.entryFor(name).info;
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,12 @@ function buildMcpServer(registry, settings, logger) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
async function main() {
|
|
68
|
+
// `tm1-mcp-server secrets …` manages keychain entries and exits; it never
|
|
69
|
+
// starts the server.
|
|
70
|
+
if (process.argv[2] === "secrets") {
|
|
71
|
+
const { runSecretsCli } = await import("./secrets-cli.js");
|
|
72
|
+
process.exit(await runSecretsCli(process.argv.slice(3), process.env));
|
|
73
|
+
}
|
|
68
74
|
const settings = loadServerSettings();
|
|
69
75
|
const logger = createLogger(settings);
|
|
70
76
|
logger.info("Starting TM1 MCP Server");
|
package/dist/lib/sample-cells.js
CHANGED
|
@@ -40,7 +40,7 @@ export function buildSampleCellsMdx(args) {
|
|
|
40
40
|
// An unfiltered Sandboxes is pinned to Base instead of crossjoined: TM1's
|
|
41
41
|
// NONEMPTY() returns an empty set whenever Sandboxes is part of the row set,
|
|
42
42
|
// even as {[Sandboxes].[Base]} alone, so every sample of a sandboxed cube
|
|
43
|
-
// came back empty. Measured on
|
|
43
|
+
// came back empty. Measured on a server with EnableSandboxDimension=true.
|
|
44
44
|
// Base is also what every other read through this server sees.
|
|
45
45
|
let sandboxPin;
|
|
46
46
|
for (const dim of dimensions) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SecretStore } from "./secrets.js";
|
|
2
|
+
export interface CliIo {
|
|
3
|
+
out: (line: string) => void;
|
|
4
|
+
err: (line: string) => void;
|
|
5
|
+
/** Reads one secret without echoing it. */
|
|
6
|
+
readSecret: (prompt: string) => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function runSecretsCli(args: string[], env: NodeJS.ProcessEnv, io?: CliIo, store?: SecretStore): Promise<number>;
|
|
9
|
+
export declare function stripPipedInput(raw: string): string;
|
|
10
|
+
//# sourceMappingURL=secrets-cli.d.ts.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// `tm1-mcp-server secrets <command>` — manage the keychain entries of a
|
|
2
|
+
// connection (see ./secrets.ts). No command ever prints a secret value.
|
|
3
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parse as parseDotenv } from "dotenv";
|
|
6
|
+
import { connectionsDir } from "./connections.js";
|
|
7
|
+
import { isSecretKey, keychainAccount, keychainStore, plaintextSecretKeys, SECRET_KEYS, } from "./secrets.js";
|
|
8
|
+
const USAGE = `Usage: tm1-mcp-server secrets <command> <connection> [KEY]
|
|
9
|
+
|
|
10
|
+
set <connection> [KEY] store a secret (prompted, or read from stdin)
|
|
11
|
+
list <connection> show which secrets are stored (never the values)
|
|
12
|
+
delete <connection> [KEY] remove one secret, or all of the connection's
|
|
13
|
+
migrate <connection> move the plaintext secrets of <connection>/.env
|
|
14
|
+
into the keychain and set TM1_SECRETS=keychain
|
|
15
|
+
|
|
16
|
+
KEY is one of ${SECRET_KEYS.join(", ")} (default TM1_PASSWORD).
|
|
17
|
+
<connection> is a folder under ~/.tm1/mcp-servers (or $TM1_CONNECTIONS_DIR).`;
|
|
18
|
+
export async function runSecretsCli(args, env, io = processIo, store) {
|
|
19
|
+
const [command, connection, keyArg, ...extra] = args;
|
|
20
|
+
if (!command || command === "help" || command === "--help") {
|
|
21
|
+
io.out(USAGE);
|
|
22
|
+
return command ? 0 : 2;
|
|
23
|
+
}
|
|
24
|
+
if (!connection || extra.length > 0) {
|
|
25
|
+
io.err(USAGE);
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
if (keyArg !== undefined && !isSecretKey(keyArg)) {
|
|
29
|
+
io.err(`Unknown KEY "${keyArg}". One of: ${SECRET_KEYS.join(", ")}.`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
const folder = join(connectionsDir(env), connection);
|
|
33
|
+
const envFile = join(folder, ".env");
|
|
34
|
+
if (!existsSync(envFile)) {
|
|
35
|
+
io.err(`No connection "${connection}": ${envFile} does not exist.`);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
let keychain;
|
|
39
|
+
try {
|
|
40
|
+
keychain = store ?? (await keychainStore());
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
switch (command) {
|
|
47
|
+
case "set": {
|
|
48
|
+
const key = keyArg ?? "TM1_PASSWORD";
|
|
49
|
+
const value = await io.readSecret(`${key} for ${connection}: `);
|
|
50
|
+
if (value === "") {
|
|
51
|
+
io.err("Empty value — nothing stored.");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
// Pasted or piped junk becomes a wrong password, and wrong passwords
|
|
55
|
+
// lock TM1 accounts.
|
|
56
|
+
if (/[\r\n]/.test(value)) {
|
|
57
|
+
io.err("The value spans several lines — nothing stored.");
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
keychain.set(connection, key, value);
|
|
61
|
+
io.out(`Stored ${keychainAccount(connection, key)}.`);
|
|
62
|
+
const marker = parseDotenv(readFileSync(envFile)).TM1_SECRETS;
|
|
63
|
+
if (marker?.trim().toLowerCase() !== "keychain") {
|
|
64
|
+
io.out(`The .env does not set TM1_SECRETS=keychain yet, so the server ` +
|
|
65
|
+
`ignores this entry. Run \`secrets migrate ${connection}\` or add it.`);
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
case "list": {
|
|
70
|
+
for (const key of SECRET_KEYS) {
|
|
71
|
+
const stored = keychain.get(connection, key) !== undefined;
|
|
72
|
+
io.out(`${key.padEnd(18)} ${stored ? "stored" : "-"}`);
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
case "delete": {
|
|
77
|
+
const keys = keyArg ? [keyArg] : SECRET_KEYS;
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
if (keychain.delete(connection, key)) {
|
|
80
|
+
io.out(`Deleted ${keychainAccount(connection, key)}.`);
|
|
81
|
+
}
|
|
82
|
+
else if (keyArg) {
|
|
83
|
+
io.out(`No entry ${keychainAccount(connection, key)}.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
case "migrate":
|
|
89
|
+
return migrate(connection, envFile, keychain, io);
|
|
90
|
+
default:
|
|
91
|
+
io.err(`Unknown command "${command}".\n${USAGE}`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Move the plaintext secrets of one `.env` into the keychain. Order matters:
|
|
97
|
+
* every secret is stored and read back BEFORE the file changes, and the new
|
|
98
|
+
* file is checked to parse to exactly the old settings minus the secrets. No
|
|
99
|
+
* plaintext backup is kept — that would defeat the point.
|
|
100
|
+
*/
|
|
101
|
+
function migrate(connection, envFile, keychain, io) {
|
|
102
|
+
const text = readFileSync(envFile, "utf8");
|
|
103
|
+
const before = parseDotenv(text);
|
|
104
|
+
const keys = plaintextSecretKeys(before);
|
|
105
|
+
if (keys.length === 0) {
|
|
106
|
+
io.out(`${envFile} holds no plaintext secret.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
110
|
+
const kept = [];
|
|
111
|
+
for (const line of text.split(/\r?\n/)) {
|
|
112
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
|
|
113
|
+
const name = match?.[1];
|
|
114
|
+
if (name && (keys.includes(name) || name === "TM1_SECRETS")) {
|
|
115
|
+
const value = match[2].trim();
|
|
116
|
+
const quote = value[0];
|
|
117
|
+
if ((quote === '"' || quote === "'" || quote === "`") &&
|
|
118
|
+
value.indexOf(quote, 1) === -1) {
|
|
119
|
+
io.err(`${name} in ${envFile} is a multi-line value; move it by hand.`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
kept.push(line);
|
|
125
|
+
}
|
|
126
|
+
while (kept.length > 0 && kept[kept.length - 1] === "")
|
|
127
|
+
kept.pop();
|
|
128
|
+
kept.push("TM1_SECRETS=keychain", "");
|
|
129
|
+
const next = kept.join(eol);
|
|
130
|
+
const after = parseDotenv(next);
|
|
131
|
+
// A blank value (TM1_PASSWORD= for a blank-password account) is no secret
|
|
132
|
+
// and stays in the file.
|
|
133
|
+
const expected = {
|
|
134
|
+
...before,
|
|
135
|
+
TM1_SECRETS: "keychain",
|
|
136
|
+
};
|
|
137
|
+
for (const key of keys)
|
|
138
|
+
delete expected[key];
|
|
139
|
+
if (JSON.stringify(sorted(after)) !== JSON.stringify(sorted(expected))) {
|
|
140
|
+
io.err(`Could not rewrite ${envFile} safely; nothing changed.`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
const value = before[key];
|
|
145
|
+
keychain.set(connection, key, value);
|
|
146
|
+
if (keychain.get(connection, key) !== value) {
|
|
147
|
+
io.err(`Reading ${keychainAccount(connection, key)} back returned a different ` +
|
|
148
|
+
`value; ${envFile} left unchanged.`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
io.out(`Stored ${keychainAccount(connection, key)}.`);
|
|
152
|
+
}
|
|
153
|
+
const tmp = `${envFile}.tmp`;
|
|
154
|
+
writeFileSync(tmp, next);
|
|
155
|
+
renameSync(tmp, envFile);
|
|
156
|
+
io.out(`Removed ${keys.join(", ")} from ${envFile} and set TM1_SECRETS=keychain. ` +
|
|
157
|
+
`Restart the MCP client to pick up the change.`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
function sorted(obj) {
|
|
161
|
+
return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
|
|
162
|
+
}
|
|
163
|
+
const processIo = {
|
|
164
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
165
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
166
|
+
readSecret: (prompt) => process.stdin.isTTY ? promptHidden(prompt) : readPipedStdin(),
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* `echo secret | tm1-mcp-server secrets set …` — a leading BOM (PowerShell
|
|
170
|
+
* pipes) and one trailing newline dropped. Git Bash's mintty is no TTY to
|
|
171
|
+
* Node either, so typed input lands here too, echoed: say so.
|
|
172
|
+
*/
|
|
173
|
+
async function readPipedStdin() {
|
|
174
|
+
process.stderr.write("Reading the secret from stdin until EOF. In a terminal that is no TTY to " +
|
|
175
|
+
"Node (Git Bash/mintty) the input is echoed; pipe the value or use " +
|
|
176
|
+
"PowerShell/cmd instead.\n");
|
|
177
|
+
const chunks = [];
|
|
178
|
+
for await (const chunk of process.stdin)
|
|
179
|
+
chunks.push(chunk);
|
|
180
|
+
return stripPipedInput(Buffer.concat(chunks).toString("utf8"));
|
|
181
|
+
}
|
|
182
|
+
export function stripPipedInput(raw) {
|
|
183
|
+
return raw.replace(/^\uFEFF/, "").replace(/\r?\n$/, "");
|
|
184
|
+
}
|
|
185
|
+
function promptHidden(prompt) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const stdin = process.stdin;
|
|
188
|
+
process.stderr.write(prompt);
|
|
189
|
+
stdin.setRawMode(true);
|
|
190
|
+
stdin.setEncoding("utf8");
|
|
191
|
+
stdin.resume();
|
|
192
|
+
let value = "";
|
|
193
|
+
const done = (err) => {
|
|
194
|
+
stdin.setRawMode(false);
|
|
195
|
+
stdin.pause();
|
|
196
|
+
stdin.off("data", onData);
|
|
197
|
+
process.stderr.write("\n");
|
|
198
|
+
if (err)
|
|
199
|
+
reject(err);
|
|
200
|
+
else
|
|
201
|
+
resolve(value);
|
|
202
|
+
};
|
|
203
|
+
const onData = (chunk) => {
|
|
204
|
+
for (const ch of chunk) {
|
|
205
|
+
if (ch === "\r" || ch === "\n")
|
|
206
|
+
return done();
|
|
207
|
+
if (ch === "\u0003")
|
|
208
|
+
return done(new Error("Aborted."));
|
|
209
|
+
if (ch === "\u007f" || ch === "\b")
|
|
210
|
+
value = value.slice(0, -1);
|
|
211
|
+
else
|
|
212
|
+
value += ch;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
stdin.on("data", onData);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=secrets-cli.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Every `.env` key that holds a credential. */
|
|
2
|
+
export declare const SECRET_KEYS: readonly ["TM1_PASSWORD", "TM1_CLIENT_SECRET", "TM1_ACCESS_TOKEN", "TM1_API_KEY", "TM1_CAM_PASSPORT"];
|
|
3
|
+
export type SecretKey = (typeof SECRET_KEYS)[number];
|
|
4
|
+
export declare const KEYCHAIN_SERVICE = "tm1-mcp-server";
|
|
5
|
+
export declare function isSecretKey(key: string): key is SecretKey;
|
|
6
|
+
export declare function keychainAccount(connection: string, key: SecretKey): string;
|
|
7
|
+
/** The Windows Credential Manager target name of one entry. */
|
|
8
|
+
export declare function keychainTarget(connection: string, key: SecretKey): string;
|
|
9
|
+
/** Secret keys that carry a non-empty plaintext value in `env`. */
|
|
10
|
+
export declare function plaintextSecretKeys(env: NodeJS.ProcessEnv): SecretKey[];
|
|
11
|
+
/**
|
|
12
|
+
* True when the connection keeps its secrets in the keychain. Throws on an
|
|
13
|
+
* unknown TM1_SECRETS value, and when a keychain connection still carries a
|
|
14
|
+
* plaintext secret — which of the two should win is not guessable, and a
|
|
15
|
+
* stale one is exactly the kind of wrong password that locks an account.
|
|
16
|
+
* An empty value (`TM1_PASSWORD=`, a blank-password account) is no conflict.
|
|
17
|
+
*/
|
|
18
|
+
export declare function usesKeychain(env: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
/** The OS store, narrowed to what the server and the CLI need. */
|
|
20
|
+
export interface SecretStore {
|
|
21
|
+
get(connection: string, key: SecretKey): string | undefined;
|
|
22
|
+
set(connection: string, key: SecretKey, value: string): void;
|
|
23
|
+
/** True when an entry existed. */
|
|
24
|
+
delete(connection: string, key: SecretKey): boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The OS keychain. The native binding is imported on first use, so a
|
|
28
|
+
* platform without a prebuilt binary only breaks connections that ask for it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function keychainStore(): Promise<SecretStore>;
|
|
31
|
+
/**
|
|
32
|
+
* `env` with every secret the keychain holds for `connection` filled in. The
|
|
33
|
+
* result goes straight into loadConfig and is never written to process.env,
|
|
34
|
+
* so child processes do not inherit it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withKeychainSecrets(connection: string, env: NodeJS.ProcessEnv, store?: SecretStore): Promise<NodeJS.ProcessEnv>;
|
|
37
|
+
/** Appended to a config error on a keychain connection: how to fix it. */
|
|
38
|
+
export declare function keychainHint(connection: string): string;
|
|
39
|
+
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Connection secrets kept in the OS keychain instead of the `.env`.
|
|
2
|
+
//
|
|
3
|
+
// A connection folder opts in with `TM1_SECRETS=keychain`. Its `.env` then
|
|
4
|
+
// carries no secret; each one lives in the OS store (Windows Credential
|
|
5
|
+
// Manager, macOS Keychain, Linux Secret Service) under
|
|
6
|
+
//
|
|
7
|
+
// service "tm1-mcp-server", account "<connection>/<KEY>"
|
|
8
|
+
// Windows target name "<connection>/<KEY>.tm1-mcp-server"
|
|
9
|
+
//
|
|
10
|
+
// The Windows target name is the library default ("<account>.<service>"),
|
|
11
|
+
// pinned by keychainTarget() because a stdlib-only reader — the tm1-api
|
|
12
|
+
// skill's tm1.py calls CredReadW via ctypes — looks the entry up by it. The
|
|
13
|
+
// blob is UTF-16LE. Entry.withTarget() is deliberately not used: in
|
|
14
|
+
// @napi-rs/keyring 2.1.0 on Windows a fresh Entry reads back "" for it.
|
|
15
|
+
//
|
|
16
|
+
// Discovery never reads the store: loadConfig(env, { deferSecrets: true })
|
|
17
|
+
// accepts the folder, and the secrets are read when the connection is first
|
|
18
|
+
// used (ConnectionRegistry.get). Listing connections therefore never touches
|
|
19
|
+
// the keychain, and a missing entry fails that one connection, not startup.
|
|
20
|
+
//
|
|
21
|
+
// What this protects: the secret no longer sits in a file that gets synced,
|
|
22
|
+
// backed up, committed, pasted or read into an agent's context. What it does
|
|
23
|
+
// not: any process running as the same OS user can read the store too.
|
|
24
|
+
import { TM1Error, TM1ErrorCode } from "./types.js";
|
|
25
|
+
/** Every `.env` key that holds a credential. */
|
|
26
|
+
export const SECRET_KEYS = [
|
|
27
|
+
"TM1_PASSWORD",
|
|
28
|
+
"TM1_CLIENT_SECRET",
|
|
29
|
+
"TM1_ACCESS_TOKEN",
|
|
30
|
+
"TM1_API_KEY",
|
|
31
|
+
"TM1_CAM_PASSPORT",
|
|
32
|
+
];
|
|
33
|
+
export const KEYCHAIN_SERVICE = "tm1-mcp-server";
|
|
34
|
+
export function isSecretKey(key) {
|
|
35
|
+
return SECRET_KEYS.includes(key);
|
|
36
|
+
}
|
|
37
|
+
export function keychainAccount(connection, key) {
|
|
38
|
+
return `${connection}/${key}`;
|
|
39
|
+
}
|
|
40
|
+
/** The Windows Credential Manager target name of one entry. */
|
|
41
|
+
export function keychainTarget(connection, key) {
|
|
42
|
+
return `${keychainAccount(connection, key)}.${KEYCHAIN_SERVICE}`;
|
|
43
|
+
}
|
|
44
|
+
/** Secret keys that carry a non-empty plaintext value in `env`. */
|
|
45
|
+
export function plaintextSecretKeys(env) {
|
|
46
|
+
return SECRET_KEYS.filter((key) => Boolean(env[key]));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when the connection keeps its secrets in the keychain. Throws on an
|
|
50
|
+
* unknown TM1_SECRETS value, and when a keychain connection still carries a
|
|
51
|
+
* plaintext secret — which of the two should win is not guessable, and a
|
|
52
|
+
* stale one is exactly the kind of wrong password that locks an account.
|
|
53
|
+
* An empty value (`TM1_PASSWORD=`, a blank-password account) is no conflict.
|
|
54
|
+
*/
|
|
55
|
+
export function usesKeychain(env) {
|
|
56
|
+
const raw = env.TM1_SECRETS?.trim().toLowerCase();
|
|
57
|
+
if (!raw)
|
|
58
|
+
return false;
|
|
59
|
+
if (raw !== "keychain") {
|
|
60
|
+
throw new Error(`Invalid TM1_SECRETS: "${env.TM1_SECRETS}". Expected "keychain" or unset.`);
|
|
61
|
+
}
|
|
62
|
+
const plain = plaintextSecretKeys(env);
|
|
63
|
+
if (plain.length > 0) {
|
|
64
|
+
throw new Error(`TM1_SECRETS=keychain, but the .env still sets ${plain.join(", ")}. ` +
|
|
65
|
+
`Remove ${plain.length === 1 ? "it" : "them"} from the .env ` +
|
|
66
|
+
`(\`tm1-mcp-server secrets migrate <connection>\` does both steps).`);
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
let osStore;
|
|
71
|
+
/**
|
|
72
|
+
* The OS keychain. The native binding is imported on first use, so a
|
|
73
|
+
* platform without a prebuilt binary only breaks connections that ask for it.
|
|
74
|
+
*/
|
|
75
|
+
export function keychainStore() {
|
|
76
|
+
osStore ??= import("@napi-rs/keyring").then(({ Entry }) => {
|
|
77
|
+
const entry = (connection, key) => new Entry(KEYCHAIN_SERVICE, keychainAccount(connection, key));
|
|
78
|
+
return {
|
|
79
|
+
get: (connection, key) => entry(connection, key).getPassword() ?? undefined,
|
|
80
|
+
set: (connection, key, value) => entry(connection, key).setPassword(value),
|
|
81
|
+
delete: (connection, key) => entry(connection, key).deletePassword(),
|
|
82
|
+
};
|
|
83
|
+
}, (err) => {
|
|
84
|
+
osStore = undefined;
|
|
85
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
86
|
+
throw new Error(`The OS keychain is not available on this platform (${reason}).`);
|
|
87
|
+
});
|
|
88
|
+
return osStore;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `env` with every secret the keychain holds for `connection` filled in. The
|
|
92
|
+
* result goes straight into loadConfig and is never written to process.env,
|
|
93
|
+
* so child processes do not inherit it.
|
|
94
|
+
*/
|
|
95
|
+
export async function withKeychainSecrets(connection, env, store) {
|
|
96
|
+
let resolved;
|
|
97
|
+
try {
|
|
98
|
+
resolved = store ?? (await keychainStore());
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw new TM1Error({
|
|
102
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
103
|
+
message: `Connection "${connection}" keeps its secrets in the OS keychain: ${err instanceof Error ? err.message : String(err)}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const out = { ...env };
|
|
107
|
+
for (const key of SECRET_KEYS) {
|
|
108
|
+
let value;
|
|
109
|
+
try {
|
|
110
|
+
value = resolved.get(connection, key);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new TM1Error({
|
|
114
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
115
|
+
message: `Reading ${keychainAccount(connection, key)} from the OS keychain failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (value !== undefined)
|
|
119
|
+
out[key] = value;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/** Appended to a config error on a keychain connection: how to fix it. */
|
|
124
|
+
export function keychainHint(connection) {
|
|
125
|
+
return (` The secrets of "${connection}" are read from the OS keychain; store the ` +
|
|
126
|
+
`missing one with \`npx tm1-mcp-server secrets set ${connection} <KEY>\`.`);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
3
2
|
import { actionResponse } from "../format.js";
|
|
4
3
|
import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
5
4
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
@@ -26,27 +25,8 @@ export const registerClearCube = defineTool({
|
|
|
26
25
|
.max(3600000)
|
|
27
26
|
.optional()
|
|
28
27
|
.describe("Override the default 30s request timeout (ms, 1000–3600000). A clear that outlasts it keeps running on the server and still empties the cube; the call then reports a timeout instead of success."),
|
|
29
|
-
// Removed in 4.0.0, still DECLARED so a stored old call is refused instead
|
|
30
|
-
// of silently widened: the SDK strips properties the schema does not
|
|
31
|
-
// mention, so an undeclared field reaches the handler as if it was never
|
|
32
|
-
// sent — and a region clear would become a full wipe.
|
|
33
|
-
dimensions: z
|
|
34
|
-
.array(z.string())
|
|
35
|
-
.optional()
|
|
36
|
-
.describe("REMOVED — passing this fails the call. Do not send it."),
|
|
37
|
-
tuples: z
|
|
38
|
-
.array(z.array(z.string()))
|
|
39
|
-
.optional()
|
|
40
|
-
.describe("REMOVED — passing this fails the call. Do not send it."),
|
|
41
28
|
},
|
|
42
|
-
handler: async ({ cubeName, confirm, timeoutMs
|
|
43
|
-
if (dimensions !== undefined || tuples !== undefined) {
|
|
44
|
-
throw new TM1Error({
|
|
45
|
-
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
46
|
-
message: "tm1_clear_cube no longer takes `dimensions`/`tuples`, and this call was NOT executed. Sending them once meant a region clear, which the server never supported; ignoring them here would empty the whole cube instead.",
|
|
47
|
-
hint: "To empty the whole cube, repeat the call with cubeName and confirm only. To empty part of it, run a TI process with CubeClearData()/view-based logic via tm1_execute_process.",
|
|
48
|
-
});
|
|
49
|
-
}
|
|
29
|
+
handler: async ({ cubeName, confirm, timeoutMs }, tm1Client) => {
|
|
50
30
|
requireConfirm(confirm, cubeName, "cube");
|
|
51
31
|
await tm1Client.cubes.clear(cubeName, timeoutMs !== undefined ? { timeoutMs } : undefined);
|
|
52
32
|
return actionResponse({ success: true, cubeName, summary: "all cells" });
|
|
@@ -6,8 +6,8 @@ export declare const FileContentResultSchema: z.ZodObject<{
|
|
|
6
6
|
truncated: z.ZodBoolean;
|
|
7
7
|
truncationReason: z.ZodOptional<z.ZodString>;
|
|
8
8
|
encoding: z.ZodEnum<{
|
|
9
|
-
base64: "base64";
|
|
10
9
|
text: "text";
|
|
10
|
+
base64: "base64";
|
|
11
11
|
}>;
|
|
12
12
|
content: z.ZodString;
|
|
13
13
|
}, z.core.$strip>;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ffschrattenecker/tm1-mcp-server",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ffschrattenecker/tm1-mcp-server",
|
|
9
|
-
"version": "8.
|
|
9
|
+
"version": "8.1.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
13
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
13
14
|
"dotenv": "^18.0.0",
|
|
14
15
|
"pino": "^10.3.1",
|
|
15
16
|
"regexp-tree": "^0.1.27",
|
|
@@ -862,6 +863,240 @@
|
|
|
862
863
|
}
|
|
863
864
|
}
|
|
864
865
|
},
|
|
866
|
+
"node_modules/@napi-rs/keyring": {
|
|
867
|
+
"version": "2.1.0",
|
|
868
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-2.1.0.tgz",
|
|
869
|
+
"integrity": "sha512-km9J3fomkGSLpImpelq0cMvLBrJ5eVlTzuWdG3Iy0laP6gU90MmHYjqhUhG32aYq0/0++r2TLuAA9xlnh4XU1g==",
|
|
870
|
+
"license": "MIT",
|
|
871
|
+
"engines": {
|
|
872
|
+
"node": ">= 10"
|
|
873
|
+
},
|
|
874
|
+
"funding": {
|
|
875
|
+
"type": "github",
|
|
876
|
+
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
877
|
+
},
|
|
878
|
+
"optionalDependencies": {
|
|
879
|
+
"@napi-rs/keyring-darwin-arm64": "2.1.0",
|
|
880
|
+
"@napi-rs/keyring-darwin-x64": "2.1.0",
|
|
881
|
+
"@napi-rs/keyring-freebsd-x64": "2.1.0",
|
|
882
|
+
"@napi-rs/keyring-linux-arm-gnueabihf": "2.1.0",
|
|
883
|
+
"@napi-rs/keyring-linux-arm64-gnu": "2.1.0",
|
|
884
|
+
"@napi-rs/keyring-linux-arm64-musl": "2.1.0",
|
|
885
|
+
"@napi-rs/keyring-linux-riscv64-gnu": "2.1.0",
|
|
886
|
+
"@napi-rs/keyring-linux-x64-gnu": "2.1.0",
|
|
887
|
+
"@napi-rs/keyring-linux-x64-musl": "2.1.0",
|
|
888
|
+
"@napi-rs/keyring-win32-arm64-msvc": "2.1.0",
|
|
889
|
+
"@napi-rs/keyring-win32-ia32-msvc": "2.1.0",
|
|
890
|
+
"@napi-rs/keyring-win32-x64-msvc": "2.1.0"
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
"node_modules/@napi-rs/keyring-darwin-arm64": {
|
|
894
|
+
"version": "2.1.0",
|
|
895
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-2.1.0.tgz",
|
|
896
|
+
"integrity": "sha512-j6ATTOhmPW+N5EKwi8Yzn6P4Bz/uqnxzyYvqOL73/dgzDSYG5pS5eprqpVmOLse68WUTf79ZMqz97HnfvirJ9g==",
|
|
897
|
+
"cpu": [
|
|
898
|
+
"arm64"
|
|
899
|
+
],
|
|
900
|
+
"license": "MIT",
|
|
901
|
+
"optional": true,
|
|
902
|
+
"os": [
|
|
903
|
+
"darwin"
|
|
904
|
+
],
|
|
905
|
+
"engines": {
|
|
906
|
+
"node": ">= 10"
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
"node_modules/@napi-rs/keyring-darwin-x64": {
|
|
910
|
+
"version": "2.1.0",
|
|
911
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-2.1.0.tgz",
|
|
912
|
+
"integrity": "sha512-54Q803nguiOaQzBG2h4uIkacI0eLGAlzzdpxPNi6IZtXbjqbPI4AQ86GrQy25tw3czBqRPBoGu0lHP2U+/f5iA==",
|
|
913
|
+
"cpu": [
|
|
914
|
+
"x64"
|
|
915
|
+
],
|
|
916
|
+
"license": "MIT",
|
|
917
|
+
"optional": true,
|
|
918
|
+
"os": [
|
|
919
|
+
"darwin"
|
|
920
|
+
],
|
|
921
|
+
"engines": {
|
|
922
|
+
"node": ">= 10"
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
"node_modules/@napi-rs/keyring-freebsd-x64": {
|
|
926
|
+
"version": "2.1.0",
|
|
927
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-2.1.0.tgz",
|
|
928
|
+
"integrity": "sha512-BL7ngJAYQBrUp1PTF8enOXOIVfxN51RbwVOEQ+8eAjWypF/5Ipqs8oQoMqkuc0wAbk+vatnNVjdKCIFa/HPglg==",
|
|
929
|
+
"cpu": [
|
|
930
|
+
"x64"
|
|
931
|
+
],
|
|
932
|
+
"license": "MIT",
|
|
933
|
+
"optional": true,
|
|
934
|
+
"os": [
|
|
935
|
+
"freebsd"
|
|
936
|
+
],
|
|
937
|
+
"engines": {
|
|
938
|
+
"node": ">= 10"
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
"node_modules/@napi-rs/keyring-linux-arm-gnueabihf": {
|
|
942
|
+
"version": "2.1.0",
|
|
943
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-2.1.0.tgz",
|
|
944
|
+
"integrity": "sha512-JYgbCDk+giss7lKla8NTXhM/fLj+Hrfgr3xwKQ8jKn8qMD6vXLwYcfK8HJ9VoAcjIeYEUpCL6dRszJC5M4zFNg==",
|
|
945
|
+
"cpu": [
|
|
946
|
+
"arm"
|
|
947
|
+
],
|
|
948
|
+
"license": "MIT",
|
|
949
|
+
"optional": true,
|
|
950
|
+
"os": [
|
|
951
|
+
"linux"
|
|
952
|
+
],
|
|
953
|
+
"engines": {
|
|
954
|
+
"node": ">= 10"
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
"node_modules/@napi-rs/keyring-linux-arm64-gnu": {
|
|
958
|
+
"version": "2.1.0",
|
|
959
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-2.1.0.tgz",
|
|
960
|
+
"integrity": "sha512-+PMONFQ+2GkBXG7vRG2spgR73bNCMeVpNopHoSmhj2E+5Gu6zJooRBjoaIZKj6hCyrOU8pXAhoZ22EEFQ/4YeQ==",
|
|
961
|
+
"cpu": [
|
|
962
|
+
"arm64"
|
|
963
|
+
],
|
|
964
|
+
"libc": [
|
|
965
|
+
"glibc"
|
|
966
|
+
],
|
|
967
|
+
"license": "MIT",
|
|
968
|
+
"optional": true,
|
|
969
|
+
"os": [
|
|
970
|
+
"linux"
|
|
971
|
+
],
|
|
972
|
+
"engines": {
|
|
973
|
+
"node": ">= 10"
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
"node_modules/@napi-rs/keyring-linux-arm64-musl": {
|
|
977
|
+
"version": "2.1.0",
|
|
978
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-2.1.0.tgz",
|
|
979
|
+
"integrity": "sha512-dKjiXKTHQjAS57hL0HVzLpW9XYNGFpfYkr/51FzIIvbZR08PAnzIDYKwShoNYGvjQP6+rDFf1a5Utl0aWwcFrA==",
|
|
980
|
+
"cpu": [
|
|
981
|
+
"arm64"
|
|
982
|
+
],
|
|
983
|
+
"libc": [
|
|
984
|
+
"musl"
|
|
985
|
+
],
|
|
986
|
+
"license": "MIT",
|
|
987
|
+
"optional": true,
|
|
988
|
+
"os": [
|
|
989
|
+
"linux"
|
|
990
|
+
],
|
|
991
|
+
"engines": {
|
|
992
|
+
"node": ">= 10"
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
"node_modules/@napi-rs/keyring-linux-riscv64-gnu": {
|
|
996
|
+
"version": "2.1.0",
|
|
997
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-2.1.0.tgz",
|
|
998
|
+
"integrity": "sha512-tqhb0ofhtEJ2+ZfmlKTDHbmIoNSz+SSTPWRgMfSVCgI4JVkqDdfcmIOcThZfRffJ3QI9C3Tng+rZL24acmD+GA==",
|
|
999
|
+
"cpu": [
|
|
1000
|
+
"riscv64"
|
|
1001
|
+
],
|
|
1002
|
+
"libc": [
|
|
1003
|
+
"glibc"
|
|
1004
|
+
],
|
|
1005
|
+
"license": "MIT",
|
|
1006
|
+
"optional": true,
|
|
1007
|
+
"os": [
|
|
1008
|
+
"linux"
|
|
1009
|
+
],
|
|
1010
|
+
"engines": {
|
|
1011
|
+
"node": ">= 10"
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
"node_modules/@napi-rs/keyring-linux-x64-gnu": {
|
|
1015
|
+
"version": "2.1.0",
|
|
1016
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-2.1.0.tgz",
|
|
1017
|
+
"integrity": "sha512-7ZA0ssxfpuGXV8S5Ct1ZJoEL3sKg/8u7mIL4SK9/PwKBhxIq7K+FVcjnggNsuFnEhHTX+HYl9hHDrlX0odZnHg==",
|
|
1018
|
+
"cpu": [
|
|
1019
|
+
"x64"
|
|
1020
|
+
],
|
|
1021
|
+
"libc": [
|
|
1022
|
+
"glibc"
|
|
1023
|
+
],
|
|
1024
|
+
"license": "MIT",
|
|
1025
|
+
"optional": true,
|
|
1026
|
+
"os": [
|
|
1027
|
+
"linux"
|
|
1028
|
+
],
|
|
1029
|
+
"engines": {
|
|
1030
|
+
"node": ">= 10"
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
"node_modules/@napi-rs/keyring-linux-x64-musl": {
|
|
1034
|
+
"version": "2.1.0",
|
|
1035
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-2.1.0.tgz",
|
|
1036
|
+
"integrity": "sha512-dI74Fzl03CrN2TEP7YVBtUBFgyRTRPyK1LMcIgAUfgW3o6f5SD1a8EsfT82+brpFOltjOhacIlFiYOkYtSI5Fg==",
|
|
1037
|
+
"cpu": [
|
|
1038
|
+
"x64"
|
|
1039
|
+
],
|
|
1040
|
+
"libc": [
|
|
1041
|
+
"musl"
|
|
1042
|
+
],
|
|
1043
|
+
"license": "MIT",
|
|
1044
|
+
"optional": true,
|
|
1045
|
+
"os": [
|
|
1046
|
+
"linux"
|
|
1047
|
+
],
|
|
1048
|
+
"engines": {
|
|
1049
|
+
"node": ">= 10"
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
"node_modules/@napi-rs/keyring-win32-arm64-msvc": {
|
|
1053
|
+
"version": "2.1.0",
|
|
1054
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-2.1.0.tgz",
|
|
1055
|
+
"integrity": "sha512-SltrXXkEe9a+Jv8ConDkZW2Mdr0srpWurS7oYkYP1Mp07DNBpFyqawRnyo4p7ZvQU3e+R6irj/Fq0epCCvVUXA==",
|
|
1056
|
+
"cpu": [
|
|
1057
|
+
"arm64"
|
|
1058
|
+
],
|
|
1059
|
+
"license": "MIT",
|
|
1060
|
+
"optional": true,
|
|
1061
|
+
"os": [
|
|
1062
|
+
"win32"
|
|
1063
|
+
],
|
|
1064
|
+
"engines": {
|
|
1065
|
+
"node": ">= 10"
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
"node_modules/@napi-rs/keyring-win32-ia32-msvc": {
|
|
1069
|
+
"version": "2.1.0",
|
|
1070
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-2.1.0.tgz",
|
|
1071
|
+
"integrity": "sha512-l5wJQhqXkAfQ4FGxoRh0wMpL9VxVmfiYI5TTeOGNupuVsQvbHmysh8gfDQBBWVhyMENHJS2OqOSYIuqVjumAtA==",
|
|
1072
|
+
"cpu": [
|
|
1073
|
+
"ia32"
|
|
1074
|
+
],
|
|
1075
|
+
"license": "MIT",
|
|
1076
|
+
"optional": true,
|
|
1077
|
+
"os": [
|
|
1078
|
+
"win32"
|
|
1079
|
+
],
|
|
1080
|
+
"engines": {
|
|
1081
|
+
"node": ">= 10"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
"node_modules/@napi-rs/keyring-win32-x64-msvc": {
|
|
1085
|
+
"version": "2.1.0",
|
|
1086
|
+
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-2.1.0.tgz",
|
|
1087
|
+
"integrity": "sha512-3zYwO7dhJiUNyv2te81ujXm2vGxHwgx+z2eBm0dTYe3aWpvKQVzpfxCp/7cViJs8duVYE04wVOgXiRhRCMEq2w==",
|
|
1088
|
+
"cpu": [
|
|
1089
|
+
"x64"
|
|
1090
|
+
],
|
|
1091
|
+
"license": "MIT",
|
|
1092
|
+
"optional": true,
|
|
1093
|
+
"os": [
|
|
1094
|
+
"win32"
|
|
1095
|
+
],
|
|
1096
|
+
"engines": {
|
|
1097
|
+
"node": ">= 10"
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
865
1100
|
"node_modules/@oxc-project/types": {
|
|
866
1101
|
"version": "0.150.0",
|
|
867
1102
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ffschrattenecker/tm1-mcp-server",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "MCP server for IBM Planning Analytics / TM1 — full model lifecycle via REST API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
89
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
89
90
|
"dotenv": "^18.0.0",
|
|
90
91
|
"pino": "^10.3.1",
|
|
91
92
|
"regexp-tree": "^0.1.27",
|