@ffschrattenecker/tm1-mcp-server 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +110 -1
  2. package/README.md +28 -7
  3. package/dist/config.d.ts +12 -1
  4. package/dist/config.js +120 -79
  5. package/dist/connections.d.ts +69 -0
  6. package/dist/connections.js +262 -0
  7. package/dist/http-transport.d.ts +2 -2
  8. package/dist/index.js +45 -37
  9. package/dist/lib/callgraph/tm1-adapter.d.ts +4 -1
  10. package/dist/lib/callgraph/tm1-adapter.js +5 -2
  11. package/dist/lib/slim-json-schema.d.ts +8 -0
  12. package/dist/lib/slim-json-schema.js +34 -0
  13. package/dist/lib/strip-comments.js +1 -1
  14. package/dist/lib/tm1-events.d.ts +2 -0
  15. package/dist/prompts/index.js +2 -2
  16. package/dist/resources/index.d.ts +4 -2
  17. package/dist/resources/index.js +93 -53
  18. package/dist/resources/subscriptions.d.ts +2 -1
  19. package/dist/resources/subscriptions.js +16 -7
  20. package/dist/server-instructions.d.ts +3 -0
  21. package/dist/server-instructions.js +12 -0
  22. package/dist/tm1-client/http.d.ts +1 -1
  23. package/dist/tm1-client/http.js +21 -4
  24. package/dist/tm1-client/services/process-service.d.ts +7 -1
  25. package/dist/tm1-client/services/process-service.js +7 -5
  26. package/dist/tm1-client.d.ts +7 -1
  27. package/dist/tm1-client.js +10 -11
  28. package/dist/tools/analysis/analyze-object-usage.d.ts +10 -0
  29. package/dist/tools/analysis/analyze-object-usage.js +37 -33
  30. package/dist/tools/analysis/audit-naming.js +1 -1
  31. package/dist/tools/analysis/delete-impact.d.ts +13 -0
  32. package/dist/tools/analysis/delete-impact.js +30 -0
  33. package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
  34. package/dist/tools/analysis/trace-data-flow.js +1 -1
  35. package/dist/tools/celldata/write-cells.js +37 -2
  36. package/dist/tools/confirm.d.ts +4 -1
  37. package/dist/tools/confirm.js +12 -2
  38. package/dist/tools/define-tool.d.ts +35 -11
  39. package/dist/tools/define-tool.js +51 -4
  40. package/dist/tools/dimension-management/delete-dimension.js +18 -4
  41. package/dist/tools/index.d.ts +2 -2
  42. package/dist/tools/index.js +6 -12
  43. package/dist/tools/metadata/list-cubes.js +3 -3
  44. package/dist/tools/metadata/list-processes.js +11 -7
  45. package/dist/tools/model-building/delete-cube.js +18 -4
  46. package/dist/tools/model-building/set-cube-rules.js +13 -1
  47. package/dist/tools/model-building/unload-cube.js +1 -1
  48. package/dist/tools/operations/get-audit-log.js +1 -1
  49. package/dist/tools/operations/get-jobs.js +2 -2
  50. package/dist/tools/operations/get-message-log.js +1 -1
  51. package/dist/tools/operations/get-server-info.js +10 -1
  52. package/dist/tools/operations/get-server-state.js +1 -1
  53. package/dist/tools/operations/get-threads.js +2 -2
  54. package/dist/tools/operations/get-transaction-log.js +1 -1
  55. package/dist/tools/operations/list-connections.d.ts +2 -0
  56. package/dist/tools/operations/list-connections.js +35 -0
  57. package/dist/tools/operations/save-data.js +1 -1
  58. package/dist/tools/schemas/items-monitoring.d.ts +11 -0
  59. package/dist/tools/schemas/items-monitoring.js +9 -1
  60. package/dist/tools/schemas/items-processes.d.ts +3 -0
  61. package/dist/tools/schemas/items-processes.js +2 -1
  62. package/dist/tools/ti-development/diff-processes.d.ts +42 -0
  63. package/dist/tools/ti-development/diff-processes.js +4 -4
  64. package/dist/tools/ti-development/execute-process.js +29 -4
  65. package/dist/tools/ti-development/import-pro-file.js +1 -1
  66. package/dist/tools/ti-development/preflight.d.ts +24 -0
  67. package/dist/tools/ti-development/preflight.js +32 -0
  68. package/dist/tools/ti-development/search-code.js +1 -1
  69. package/dist/tools/ti-development/upsert-process.js +100 -11
  70. package/dist/tools/with-annotations.d.ts +10 -0
  71. package/dist/tools/with-annotations.js +58 -3
  72. package/npm-shrinkwrap.json +4586 -0
  73. package/package.json +9 -7
  74. package/dist/tools/ti-development/get-process-code.d.ts +0 -2
  75. package/dist/tools/ti-development/get-process-code.js +0 -100
  76. package/dist/tools/ti-development/get-process-datasource.d.ts +0 -2
  77. package/dist/tools/ti-development/get-process-datasource.js +0 -20
  78. package/dist/tools/ti-development/get-process-parameters.d.ts +0 -2
  79. package/dist/tools/ti-development/get-process-parameters.js +0 -25
  80. package/dist/tools/ti-development/get-process-variables.d.ts +0 -2
  81. package/dist/tools/ti-development/get-process-variables.js +0 -38
@@ -0,0 +1,262 @@
1
+ // Named TM1 connections served by ONE server process.
2
+ //
3
+ // Before v7 every TM1 connection was its own server: one `.env` per folder
4
+ // under ~/.tm1/mcp-servers/, one process, one full copy of the tool list. With
5
+ // N connections a client carried N × ~113 tool names in context every turn,
6
+ // spawned N processes and held N sessions + keepalives open. The registry keeps
7
+ // the same folders as the source of truth (they are also what the tm1-api
8
+ // skill reads) but serves them all from one tool list: every tool takes a
9
+ // `connection` argument, and each connection's TM1Client is built on first use.
10
+ //
11
+ // Selection:
12
+ // TM1_CONNECTIONS_DIR set → discover that folder
13
+ // TM1_BASE_URL set (and no dir) → legacy single connection "default"
14
+ // neither → discover ~/.tm1/mcp-servers
15
+ // TM1_CONNECTIONS (comma-separated) narrows discovery to the named folders.
16
+ //
17
+ // Per connection, only the folder's own `.env` decides mode and version. Mode
18
+ // defaults to readonly: a connection-level key (TM1_MODE, TM1_BASE_URL, …) in
19
+ // the server's own environment is NOT inherited, so a TM1_MODE=readwrite in
20
+ // the launching shell cannot silently arm every connection.
21
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { parse as parseDotenv } from "dotenv";
25
+ import pino from "pino";
26
+ import { connectionIdOf, loadConfig } from "./config.js";
27
+ import { SessionManager } from "./session-manager.js";
28
+ import { TM1Client } from "./tm1-client.js";
29
+ import { TM1Error, TM1ErrorCode } from "./types.js";
30
+ /** Settings that belong to the server process, not to one TM1 connection. */
31
+ const SERVER_LEVEL_KEYS = new Set([
32
+ "TM1_LOG_LEVEL",
33
+ "TM1_LOG_FILE",
34
+ "TM1_RESPONSE_MODE",
35
+ "TM1_MAX_RESPONSE_CHARS",
36
+ "TM1_CONNECTIONS_DIR",
37
+ "TM1_CONNECTIONS",
38
+ ]);
39
+ function isServerLevelKey(key) {
40
+ return SERVER_LEVEL_KEYS.has(key) || key.startsWith("TM1_MCP_");
41
+ }
42
+ export class ConnectionRegistry {
43
+ logger;
44
+ entries = new Map();
45
+ constructor(logger) {
46
+ this.logger = logger;
47
+ }
48
+ /** Discover connections from the environment (see the header comment). */
49
+ static fromEnvironment(env, logger) {
50
+ const registry = new ConnectionRegistry(logger);
51
+ const explicitDir = env.TM1_CONNECTIONS_DIR;
52
+ if (!explicitDir && env.TM1_BASE_URL) {
53
+ registry.addConfig("default", loadConfig(env));
54
+ return registry;
55
+ }
56
+ const dir = explicitDir || join(homedir(), ".tm1", "mcp-servers");
57
+ registry.discover(dir, env);
58
+ if (registry.entries.size === 0) {
59
+ throw new Error(`No TM1 connections found. Set TM1_BASE_URL (single connection) or ` +
60
+ `create <name>/.env folders under ${dir} (or point TM1_CONNECTIONS_DIR at them).`);
61
+ }
62
+ // Folders exist but none is usable: fail at startup with every reason,
63
+ // rather than serve tools whose `connection` enum is empty.
64
+ if (registry.usableNames.length === 0) {
65
+ const reasons = registry
66
+ .status()
67
+ .map((c) => ` ${c.name}: ${c.configError}`)
68
+ .join("\n");
69
+ throw new Error(`No usable TM1 connection under ${dir}:\n${reasons}`);
70
+ }
71
+ return registry;
72
+ }
73
+ /** Wrap one prebuilt client (tests, embedders). */
74
+ static single(client, logger = pino({ level: "silent" })) {
75
+ return ConnectionRegistry.of([{ name: "default", client }], logger);
76
+ }
77
+ /**
78
+ * Prebuilt clients under explicit names. The owner keeps the clients'
79
+ * lifecycle: disconnectAll() leaves them alone. Mode defaults to readwrite
80
+ * because the registration-time gate (TM1_MODE via withAnnotations) already
81
+ * decided what an embedder may call.
82
+ */
83
+ static of(clients, logger = pino({ level: "silent" })) {
84
+ const registry = new ConnectionRegistry(logger);
85
+ for (const { name, client, mode } of clients) {
86
+ registry.entries.set(name, {
87
+ info: {
88
+ name,
89
+ version: client.version,
90
+ mode: mode ?? "readwrite",
91
+ connectionId: client.connectionId,
92
+ },
93
+ client,
94
+ });
95
+ }
96
+ return registry;
97
+ }
98
+ addConfig(name, config, dir) {
99
+ this.entries.set(name, {
100
+ info: {
101
+ name,
102
+ dir,
103
+ mode: config.mode,
104
+ environment: config.environment,
105
+ modeReason: config.modeReason,
106
+ version: config.version,
107
+ tm1Version: config.tm1Version,
108
+ baseUrl: config.baseUrl,
109
+ connectionId: connectionIdOf(config),
110
+ },
111
+ config,
112
+ });
113
+ }
114
+ discover(dir, env) {
115
+ if (!existsSync(dir))
116
+ return;
117
+ const only = env.TM1_CONNECTIONS
118
+ ? new Set(env.TM1_CONNECTIONS.split(",")
119
+ .map((s) => s.trim())
120
+ .filter(Boolean))
121
+ : undefined;
122
+ // Server-level settings and non-TM1 variables (PATH, proxies) carry over;
123
+ // connection-level TM1_* keys come from the folder only.
124
+ const base = {};
125
+ for (const [key, value] of Object.entries(env)) {
126
+ if (!key.startsWith("TM1_") || isServerLevelKey(key))
127
+ base[key] = value;
128
+ }
129
+ const names = readdirSync(dir, { withFileTypes: true })
130
+ .filter((d) => d.isDirectory() && existsSync(join(dir, d.name, ".env")))
131
+ .map((d) => d.name)
132
+ .filter((name) => !only || only.has(name))
133
+ .sort((a, b) => a.localeCompare(b));
134
+ for (const name of names) {
135
+ const folder = join(dir, name);
136
+ try {
137
+ const fileEnv = parseDotenv(readFileSync(join(folder, ".env")));
138
+ const connEnv = { ...base };
139
+ for (const [key, value] of Object.entries(fileEnv)) {
140
+ // The folder may not override server-level settings: one folder's
141
+ // TM1_MCP_TRANSPORT must not reconfigure the whole process.
142
+ if (!isServerLevelKey(key))
143
+ connEnv[key] = value;
144
+ }
145
+ this.addConfig(name, loadConfig(connEnv), folder);
146
+ }
147
+ catch (err) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ this.logger.warn({ connection: name, err: message }, "skipping connection");
150
+ this.entries.set(name, {
151
+ info: { name, dir: folder, configError: message },
152
+ });
153
+ }
154
+ }
155
+ }
156
+ /** Every connection name, including ones whose config failed to load. */
157
+ get names() {
158
+ return [...this.entries.keys()];
159
+ }
160
+ /** Connections that can actually be used. */
161
+ get usableNames() {
162
+ return [...this.entries.values()]
163
+ .filter((e) => e.config || e.client)
164
+ .map((e) => e.info.name);
165
+ }
166
+ /** True when tools should not expose a `connection` argument at all. */
167
+ get isSingle() {
168
+ return this.usableNames.length === 1;
169
+ }
170
+ get anyReadwrite() {
171
+ return [...this.entries.values()].some((e) => e.info.mode === "readwrite");
172
+ }
173
+ hasVersion(version) {
174
+ return [...this.entries.values()].some((e) => e.info.version === version);
175
+ }
176
+ /** A client exists and is not mid-login. Never triggers a login itself. */
177
+ isConnected(name) {
178
+ const entry = this.entries.get(name);
179
+ return entry?.client !== undefined && entry.connecting === undefined;
180
+ }
181
+ info(name) {
182
+ return this.entries.get(name)?.info;
183
+ }
184
+ status() {
185
+ return [...this.entries.values()].map((e) => ({
186
+ ...e.info,
187
+ connected: e.client !== undefined && e.connecting === undefined && !e.lastError,
188
+ lastError: e.lastError,
189
+ }));
190
+ }
191
+ /** Resolve a connection name (optional when there is only one). */
192
+ entryFor(name) {
193
+ if (name === undefined) {
194
+ if (this.isSingle)
195
+ return this.entries.get(this.usableNames[0]);
196
+ throw new TM1Error({
197
+ code: TM1ErrorCode.VALIDATION_ERROR,
198
+ message: `connection is required. One of: ${this.names.join(", ")}.`,
199
+ });
200
+ }
201
+ const entry = this.entries.get(name);
202
+ if (!entry) {
203
+ throw new TM1Error({
204
+ code: TM1ErrorCode.VALIDATION_ERROR,
205
+ message: `Unknown connection "${name}". One of: ${this.names.join(", ")}.`,
206
+ });
207
+ }
208
+ if (entry.info.configError) {
209
+ throw new TM1Error({
210
+ code: TM1ErrorCode.VALIDATION_ERROR,
211
+ message: `Connection "${name}" is misconfigured: ${entry.info.configError}`,
212
+ });
213
+ }
214
+ return entry;
215
+ }
216
+ /**
217
+ * The client for a connection, built and logged in on first use. A failed
218
+ * login does not poison the entry: the client's own request path retries
219
+ * authentication, so the next call gets a fresh attempt.
220
+ */
221
+ async get(name) {
222
+ const entry = this.entryFor(name);
223
+ if (!entry.client) {
224
+ const config = entry.config;
225
+ const logger = this.logger.child({ connection: entry.info.name });
226
+ const sessionManager = new SessionManager(config, logger);
227
+ entry.client = new TM1Client(config, sessionManager, logger);
228
+ entry.connecting = entry.client
229
+ .connect()
230
+ .then(() => {
231
+ entry.lastError = undefined;
232
+ })
233
+ .catch((err) => {
234
+ entry.lastError = err instanceof Error ? err.message : String(err);
235
+ logger.warn({ err }, "initial TM1 connection failed — will retry on request");
236
+ })
237
+ .finally(() => {
238
+ entry.connecting = undefined;
239
+ });
240
+ }
241
+ if (entry.connecting)
242
+ await entry.connecting;
243
+ return entry.client;
244
+ }
245
+ /** Connection metadata for a resolved call (mode/version gates). */
246
+ resolveInfo(name) {
247
+ return this.entryFor(name).info;
248
+ }
249
+ async disconnectAll() {
250
+ await Promise.all([...this.entries.values()].map(async (e) => {
251
+ if (!e.client || !e.config)
252
+ return; // prebuilt clients are the owner's
253
+ try {
254
+ await e.client.disconnect();
255
+ }
256
+ catch (err) {
257
+ this.logger.error({ err, connection: e.info.name }, "disconnect failed");
258
+ }
259
+ }));
260
+ }
261
+ }
262
+ //# sourceMappingURL=connections.js.map
@@ -1,8 +1,8 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type pino from "pino";
3
- import type { TM1Config } from "./config.js";
3
+ import type { ServerSettings } from "./config.js";
4
4
  export declare function startHttpTransport(buildServer: () => {
5
5
  server: McpServer;
6
6
  dispose: () => void;
7
- }, config: TM1Config, logger: pino.Logger): Promise<() => Promise<void>>;
7
+ }, config: ServerSettings, logger: pino.Logger): Promise<() => Promise<void>>;
8
8
  //# sourceMappingURL=http-transport.d.ts.map
package/dist/index.js CHANGED
@@ -2,12 +2,12 @@
2
2
  import "./load-env.js";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { loadConfig } from "./config.js";
5
+ import { loadServerSettings } from "./config.js";
6
+ import { ConnectionRegistry } from "./connections.js";
7
+ import { serverInstructions } from "./server-instructions.js";
6
8
  import { createLogger } from "./logger.js";
7
- import { SessionManager } from "./session-manager.js";
8
- import { TM1Client } from "./tm1-client.js";
9
9
  import { registerAllPrompts } from "./prompts/index.js";
10
- import { registerAllResources } from "./resources/index.js";
10
+ import { registerAllResources, stateResourceUris } from "./resources/index.js";
11
11
  import { SubscriptionRegistry } from "./resources/subscriptions.js";
12
12
  import { installPaginatedListHandler } from "./resources/list-handler.js";
13
13
  import { registerAllTools } from "./tools/index.js";
@@ -23,7 +23,7 @@ async function startStdioTransport(server, logger) {
23
23
  await transport.close();
24
24
  };
25
25
  }
26
- function buildMcpServer(tm1Client, config, logger) {
26
+ function buildMcpServer(registry, settings, logger) {
27
27
  const server = new McpServer({
28
28
  name: NAME,
29
29
  version: VERSION,
@@ -34,21 +34,25 @@ function buildMcpServer(tm1Client, config, logger) {
34
34
  prompts: { listChanged: true },
35
35
  logging: {},
36
36
  },
37
+ instructions: serverInstructions(registry),
37
38
  });
38
39
  // Register all tools — wrap the server so each registration receives the
39
40
  // annotation hint from ANNOTATION_MAP without editing call sites. In readonly
40
41
  // mode the proxy silently drops write/destructive tools so they never appear
41
- // in the tool listing — no autoApprove lists needed.
42
- registerAllTools(withAnnotations(server, logger, config.mode, config.responseMode, config.maxResponseChars), tm1Client);
43
- logger.debug(`All MCP tools registered (mode: ${config.mode})`);
42
+ // in the tool listing — no autoApprove lists needed. With several
43
+ // connections, write tools are listed when ANY connection is readwrite and
44
+ // refused per call against a readonly one (see defineTool).
45
+ const mode = registry.anyReadwrite ? "readwrite" : "readonly";
46
+ registerAllTools(withAnnotations(server, logger, mode, settings.responseMode, settings.maxResponseChars), registry);
47
+ logger.debug(`All MCP tools registered (mode: ${mode})`);
44
48
  // Register MCP Resources (URI-addressable read-only views over TM1 objects).
45
- const resourceCatalog = registerAllResources(server, tm1Client);
49
+ const resourceCatalog = registerAllResources(server, registry);
46
50
  // R2-07: replace the SDK's default ListResourcesRequestSchema handler with a
47
51
  // cursor-aware override that paginates the combined static + template list.
48
52
  installPaginatedListHandler(server, resourceCatalog, logger);
49
53
  // R2-05: install subscribe/unsubscribe handlers and bridge HTTP-layer mutation
50
54
  // events to notifications/resources/updated for subscribers of tm1://server/state.
51
- const subscriptions = new SubscriptionRegistry(server, logger);
55
+ const subscriptions = new SubscriptionRegistry(server, logger, stateResourceUris(registry));
52
56
  subscriptions.install();
53
57
  // Register MCP Prompts (parameterised workflow templates surfaced as slash-commands).
54
58
  registerAllPrompts(server);
@@ -61,24 +65,33 @@ function buildMcpServer(tm1Client, config, logger) {
61
65
  };
62
66
  }
63
67
  async function main() {
64
- const config = loadConfig();
65
- const logger = createLogger(config);
68
+ const settings = loadServerSettings();
69
+ const logger = createLogger(settings);
66
70
  logger.info("Starting TM1 MCP Server");
67
- const sessionManager = new SessionManager(config, logger);
68
- const tm1Client = new TM1Client(config, sessionManager, logger);
69
- // Try to connect to TM1, but don't block MCP server startup.
70
- // request() calls ensureSession() on every tool invocation, so a tool call
71
- // will retry auth if the initial connect failed. Keeps Claude able to list
72
- // tools and report a meaningful error instead of crashing the whole process.
73
- try {
74
- await tm1Client.connect();
75
- logger.info("TM1 client connected");
76
- }
77
- catch (err) {
78
- logger.warn({ err }, "Initial TM1 connection failed — server will retry on first tool call");
71
+ // One registry for every configured connection (see ./connections.ts).
72
+ // Clients are built lazily on first use, so startup costs no TM1 logins.
73
+ const registry = ConnectionRegistry.fromEnvironment(process.env, logger);
74
+ for (const c of registry.status()) {
75
+ if (c.configError) {
76
+ logger.warn({ connection: c.name, err: c.configError }, "connection unusable");
77
+ }
78
+ else {
79
+ logger.info({
80
+ connection: c.name,
81
+ mode: c.mode,
82
+ environment: c.environment,
83
+ version: c.tm1Version,
84
+ }, "connection configured");
85
+ if (c.modeReason) {
86
+ logger.warn({ connection: c.name }, `${c.modeReason} Write and destructive tools are refused on this connection.`);
87
+ }
88
+ }
79
89
  }
80
- if (config.mode === "readonly") {
81
- logger.info("TM1_MODE=readonly — write and destructive tools will not be registered");
90
+ // A single connection keeps the old behavior of logging in at startup, in
91
+ // the background: request() re-authenticates on every call anyway, so a
92
+ // failed login here only delays the error to the first tool call.
93
+ if (registry.isSingle) {
94
+ void registry.get(undefined);
82
95
  }
83
96
  // Branch on transport. stdio is the default for local MCP-client setups
84
97
  // (Claude Code, Claude Desktop) and gets one long-lived server. http
@@ -89,14 +102,14 @@ async function main() {
89
102
  // runs in shutdown(). http builds one per request and disposes each in the
90
103
  // transport's own teardown — nothing process-scoped is left for us here.
91
104
  let stdioDispose;
92
- const httpCloser = config.transport === "http"
93
- ? await startHttpTransport(() => buildMcpServer(tm1Client, config, logger), config, logger)
105
+ const httpCloser = settings.transport === "http"
106
+ ? await startHttpTransport(() => buildMcpServer(registry, settings, logger), settings, logger)
94
107
  : await (async () => {
95
- const built = buildMcpServer(tm1Client, config, logger);
108
+ const built = buildMcpServer(registry, settings, logger);
96
109
  stdioDispose = built.dispose;
97
110
  return startStdioTransport(built.server, logger);
98
111
  })();
99
- logger.info(`MCP server configured (mode: ${config.mode}, transport: ${config.transport})`);
112
+ logger.info(`MCP server configured (connections: ${registry.names.join(", ")}, transport: ${settings.transport})`);
100
113
  // Graceful shutdown — ensure TM1 session is always cleaned up.
101
114
  let shuttingDown = false;
102
115
  const shutdown = async (signal, exitCode = 0) => {
@@ -116,12 +129,7 @@ async function main() {
116
129
  catch (err) {
117
130
  logger.error({ err }, "Error closing transport");
118
131
  }
119
- try {
120
- await tm1Client.disconnect();
121
- }
122
- catch (err) {
123
- logger.error({ err }, "Error during disconnect");
124
- }
132
+ await registry.disconnectAll();
125
133
  process.exit(exitCode);
126
134
  };
127
135
  process.on("SIGINT", () => {
@@ -136,7 +144,7 @@ async function main() {
136
144
  // Parent process death — only meaningful on stdio (Claude spawns us as a
137
145
  // child). For http we ignore stdin events since the process lifecycle is
138
146
  // independent.
139
- if (config.transport === "stdio") {
147
+ if (settings.transport === "stdio") {
140
148
  process.stdin.on("end", () => {
141
149
  logger.info("stdin ended (parent process gone), shutting down");
142
150
  void shutdown("stdin-end");
@@ -26,7 +26,10 @@ export declare function getCallgraphCacheStats(): Array<{
26
26
  }>;
27
27
  /**
28
28
  * Build a full ReferenceIndex from a connected TM1 server.
29
- * TTL-cached (60 s) per `includeControl` flag. Concurrent calls share one inflight promise.
29
+ * TTL-cached (60 s) per connection and `includeControl` flag — one server
30
+ * process serves several TM1 connections, so the key must name the connection
31
+ * or one server's index would be answered for another. Concurrent calls share
32
+ * one inflight promise.
30
33
  */
31
34
  export declare function buildIndexFromTM1(tm1Client: TM1Client, opts?: BuildIndexOpts): Promise<ReferenceIndex>;
32
35
  //# sourceMappingURL=tm1-adapter.d.ts.map
@@ -71,11 +71,14 @@ export function getCallgraphCacheStats() {
71
71
  }
72
72
  /**
73
73
  * Build a full ReferenceIndex from a connected TM1 server.
74
- * TTL-cached (60 s) per `includeControl` flag. Concurrent calls share one inflight promise.
74
+ * TTL-cached (60 s) per connection and `includeControl` flag — one server
75
+ * process serves several TM1 connections, so the key must name the connection
76
+ * or one server's index would be answered for another. Concurrent calls share
77
+ * one inflight promise.
75
78
  */
76
79
  export async function buildIndexFromTM1(tm1Client, opts = {}) {
77
80
  const includeControl = opts.includeControl ?? false;
78
- const key = `inc=${includeControl}`;
81
+ const key = `${tm1Client.connectionId}|inc=${includeControl}`;
79
82
  if (!opts.bypassCache) {
80
83
  const hit = cache.get(key);
81
84
  if (hit && Date.now() - hit.ts < CACHE_TTL_MS)
@@ -14,7 +14,15 @@
14
14
  * (`properties` & friends), where the keys are user-chosen field names — a tool
15
15
  * with a field literally called `$schema` keeps its subschema.
16
16
  *
17
+ * Descriptions that restate the node's own `default` ("(default: false)",
18
+ * "Default: 50.") lose that phrase: the `default` keyword right next to it
19
+ * already says so. Only a phrase whose value equals the emitted default is
20
+ * removed, so a description documenting a default the schema does not carry
21
+ * (an optional field resolved in the handler) keeps it.
22
+ *
17
23
  * Pure: the input is never mutated, the result is a fresh deep clone.
18
24
  */
25
+ /** Remove "(default: X)" / "Default: X." where X is the emitted default. */
26
+ export declare function stripDefaultPhrase(description: string, defaultValue: unknown): string;
19
27
  export declare function slimJsonSchema(schema: unknown): unknown;
20
28
  //# sourceMappingURL=slim-json-schema.d.ts.map
@@ -14,6 +14,12 @@
14
14
  * (`properties` & friends), where the keys are user-chosen field names — a tool
15
15
  * with a field literally called `$schema` keeps its subschema.
16
16
  *
17
+ * Descriptions that restate the node's own `default` ("(default: false)",
18
+ * "Default: 50.") lose that phrase: the `default` keyword right next to it
19
+ * already says so. Only a phrase whose value equals the emitted default is
20
+ * removed, so a description documenting a default the schema does not carry
21
+ * (an optional field resolved in the handler) keeps it.
22
+ *
17
23
  * Pure: the input is never mutated, the result is a fresh deep clone.
18
24
  */
19
25
  // Zod serializes `.int()` to the JS safe-integer range; these two exact values
@@ -29,6 +35,25 @@ const NAME_KEYED_CONTAINERS = new Set([
29
35
  "$defs",
30
36
  "dependentSchemas",
31
37
  ]);
38
+ function escapeRegExp(s) {
39
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ }
41
+ /** Remove "(default: X)" / "Default: X." where X is the emitted default. */
42
+ export function stripDefaultPhrase(description, defaultValue) {
43
+ if (typeof defaultValue !== "boolean" &&
44
+ typeof defaultValue !== "number" &&
45
+ typeof defaultValue !== "string") {
46
+ return description;
47
+ }
48
+ const value = escapeRegExp(String(defaultValue));
49
+ return (description
50
+ .replace(new RegExp(String.raw `\s*\(default:?\s*${value}\)`, "gi"), "")
51
+ .replace(new RegExp(String.raw `\s*\bDefault:?\s*${value}(?:[.;]|(?=\s)|$)`, "gi"), "")
52
+ .replace(/\s+([.,;])/g, "$1")
53
+ .trim()
54
+ // A phrase that opened the sentence leaves its trailing dash behind.
55
+ .replace(/^[—–-]\s*/, ""));
56
+ }
32
57
  export function slimJsonSchema(schema) {
33
58
  return slimNode(schema, false);
34
59
  }
@@ -40,10 +65,19 @@ function slimNode(node, nameKeyed) {
40
65
  if (node === null || typeof node !== "object")
41
66
  return node;
42
67
  const out = {};
68
+ const defaultValue = nameKeyed
69
+ ? undefined
70
+ : node.default;
43
71
  for (const [key, value] of Object.entries(node)) {
44
72
  if (!nameKeyed) {
45
73
  if (key === "$schema")
46
74
  continue;
75
+ if (key === "description" && typeof value === "string") {
76
+ const text = stripDefaultPhrase(value, defaultValue);
77
+ if (text)
78
+ out[key] = text;
79
+ continue;
80
+ }
47
81
  if (key === "minimum" && value === SAFE_INT_MIN)
48
82
  continue;
49
83
  if (key === "maximum" && value === SAFE_INT_MAX)
@@ -1,4 +1,4 @@
1
- // Pure helpers for tm1_get_process_code's stripComments mode. No I/O.
1
+ // Pure helpers for tm1_get_process's stripComments mode. No I/O.
2
2
  //
3
3
  // TM1 TI code uses only `#` line comments (no block comments); `#` starts a
4
4
  // comment that runs to end of line. Grown models accumulate large blocks of
@@ -2,6 +2,8 @@ import { EventEmitter } from "node:events";
2
2
  export interface Tm1MutationEvent {
3
3
  method: string;
4
4
  path: string;
5
+ /** connectionIdOf() of the connection that mutated; see config.ts. */
6
+ connectionId?: string;
5
7
  }
6
8
  interface Tm1Events {
7
9
  mutation: (e: Tm1MutationEvent) => void;
@@ -72,8 +72,8 @@ export function registerAllPrompts(server) {
72
72
  `Diagnose why TI process **${processName}** failed. Follow this sequence:`,
73
73
  "",
74
74
  `1. **Error logs**: call \`tm1_diagnose_process_error(processName="${processName}", includeRelated=true, tail=80)\` — pulls newest logs + cascade siblings (sub-processes failing seconds apart).`,
75
- `2. **Parameters**: call \`tm1_get_process_parameters(processName="${processName}")\` — verify the call site passed the right shape.`,
76
- `3. **Source code**: call \`tm1_get_process_code(processName="${processName}")\` to inspect Prolog/Metadata/Data/Epilog.`,
75
+ `2. **Parameters**: call \`tm1_get_process(processName="${processName}", includeCode=false)\` — verify the call site passed the right shape.`,
76
+ `3. **Source code**: call \`tm1_get_process(processName="${processName}")\` to inspect Prolog/Metadata/Data/Epilog.`,
77
77
  `4. **Static refs**: call \`tm1_validate_process_refs(processName="${processName}")\` — surfaces unresolved cube/dimension references.`,
78
78
  `5. **Callgraph**: call \`tm1_analyze_callgraph(processName="${processName}", summary=true)\` — what does this process call, what calls it.`,
79
79
  `6. **Server log** (v11 only): \`tm1_get_message_log(filter="${processName}", top=50)\` for server-side correlation. Skip on v12 — the message log endpoint is deprecated there and the tool is not registered.`,
@@ -1,5 +1,7 @@
1
1
  import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { TM1Client } from "../tm1-client.js";
2
+ import { type ClientSource } from "../tools/define-tool.js";
3
3
  import type { ResourceCatalog } from "./list-handler.js";
4
- export declare function registerAllResources(server: McpServer, tm1: TM1Client): ResourceCatalog;
4
+ export declare function registerAllResources(server: McpServer, source: ClientSource): ResourceCatalog;
5
+ /** URIs of every connection's server-state resource, keyed by connectionId. */
6
+ export declare function stateResourceUris(source: ClientSource): Map<string, string>;
5
7
  //# sourceMappingURL=index.d.ts.map