@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 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
- plapp-franz recordings; the recorder now merges by default (`--replace` starts over).
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.0.0...HEAD
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 declare function loadConfig(env?: NodeJS.ProcessEnv): TM1Config;
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");
@@ -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): ConnectionRegistry;
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>;
@@ -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.addConfig("default", loadConfig(env));
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
- this.addConfig(name, loadConfig(connectionEnv(folder, env)), folder);
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");
@@ -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 plapp-franz (EnableSandboxDimension=true).
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
@@ -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, dimensions, tuples }, tm1Client) => {
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>;
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@ffschrattenecker/tm1-mcp-server",
3
- "version": "8.0.0",
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.0.0",
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.0.0",
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",