@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
@@ -7,6 +7,7 @@
7
7
  // Each resource maps to an existing TM1 service call — same backend logic
8
8
  // as the get_* tools, different MCP entry point.
9
9
  import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { asRegistry } from "../tools/define-tool.js";
10
11
  import { maskCode } from "../lib/mask-secrets.js";
11
12
  function asJsonContent(uri, payload) {
12
13
  return {
@@ -19,15 +20,41 @@ function asJsonContent(uri, payload) {
19
20
  ],
20
21
  };
21
22
  }
22
- export function registerAllResources(server, tm1) {
23
+ export function registerAllResources(server, source) {
23
24
  // Build a parallel catalog as we go so installPaginatedListHandler can
24
25
  // override SDK's default ListResourcesRequestSchema with cursor support.
25
26
  // registerResource still wires the read callbacks; we just keep our own
26
27
  // listing source of truth.
27
28
  const entries = [];
29
+ const registry = asRegistry(source);
30
+ for (const name of registry.usableNames) {
31
+ const single = registry.isSingle;
32
+ registerConnectionResources(server, entries, {
33
+ base: single ? "tm1://" : `tm1://${name}/`,
34
+ key: single ? "" : `${name}:`,
35
+ titlePrefix: single ? "" : `${name}: `,
36
+ client: () => registry.get(name),
37
+ enumerate: () => single || registry.isConnected(name),
38
+ });
39
+ }
40
+ return { entries };
41
+ }
42
+ /** URIs of every connection's server-state resource, keyed by connectionId. */
43
+ export function stateResourceUris(source) {
44
+ const registry = asRegistry(source);
45
+ const uris = new Map();
46
+ for (const name of registry.usableNames) {
47
+ const uri = registry.isSingle
48
+ ? "tm1://server/state"
49
+ : `tm1://${name}/server/state`;
50
+ uris.set(uri, registry.info(name)?.connectionId ?? "");
51
+ }
52
+ return uris;
53
+ }
54
+ function registerConnectionResources(server, entries, ctx) {
28
55
  // ── Static endpoints ────────────────────────────────────────────────
29
- server.registerResource("server-info", "tm1://server/info", {
30
- title: "TM1 Server Info",
56
+ server.registerResource(`${ctx.key}server-info`, `${ctx.base}server/info`, {
57
+ title: `${ctx.titlePrefix}TM1 Server Info`,
31
58
  description: "TM1 server configuration snapshot: name, version, data directory, timezone, integrated security mode.",
32
59
  mimeType: "application/json",
33
60
  }, async (uri) => {
@@ -35,7 +62,7 @@ export function registerAllResources(server, tm1) {
35
62
  // carries the full merged /Configuration body, which can include
36
63
  // sensitive settings — resources have no params, so unlike the
37
64
  // curated tm1_get_server_info tool there is no place to opt in.
38
- const info = await tm1.server.getInfo();
65
+ const info = await (await ctx.client()).server.getInfo();
39
66
  return asJsonContent(uri, {
40
67
  serverName: info.serverName,
41
68
  productVersion: info.productVersion,
@@ -49,28 +76,28 @@ export function registerAllResources(server, tm1) {
49
76
  entries.push({
50
77
  kind: "static",
51
78
  resource: {
52
- uri: "tm1://server/info",
53
- name: "server-info",
54
- title: "TM1 Server Info",
79
+ uri: `${ctx.base}server/info`,
80
+ name: `${ctx.key}server-info`,
81
+ title: `${ctx.titlePrefix}TM1 Server Info`,
55
82
  description: "TM1 server configuration snapshot: name, version, data directory, timezone, integrated security mode.",
56
83
  mimeType: "application/json",
57
84
  },
58
85
  });
59
- server.registerResource("server-state", "tm1://server/state", {
60
- title: "TM1 Server State",
86
+ server.registerResource(`${ctx.key}server-state`, `${ctx.base}server/state`, {
87
+ title: `${ctx.titlePrefix}TM1 Server State`,
61
88
  description: "Health-check snapshot: connection state, version, capability flags, object counts (cubes/dimensions/processes/chores/clients).",
62
89
  mimeType: "application/json",
63
90
  }, async (uri) => {
64
91
  const [info, cubes, dims, procs, chores, clients] = await Promise.all([
65
- tm1.server.getInfo(),
66
- tm1.cubes.list(),
67
- tm1.dimensions.list(),
68
- tm1.processes.list(),
69
- tm1.chores.list(),
70
- tm1.security.listClients(),
92
+ (await ctx.client()).server.getInfo(),
93
+ (await ctx.client()).cubes.list(),
94
+ (await ctx.client()).dimensions.list(),
95
+ (await ctx.client()).processes.list({ namesOnly: true }),
96
+ (await ctx.client()).chores.list(),
97
+ (await ctx.client()).security.listClients(),
71
98
  ]);
72
99
  return asJsonContent(uri, {
73
- connected: tm1.isConnected(),
100
+ connected: (await ctx.client()).isConnected(),
74
101
  server: {
75
102
  name: info.serverName,
76
103
  productVersion: info.productVersion,
@@ -89,25 +116,29 @@ export function registerAllResources(server, tm1) {
89
116
  entries.push({
90
117
  kind: "static",
91
118
  resource: {
92
- uri: "tm1://server/state",
93
- name: "server-state",
94
- title: "TM1 Server State",
119
+ uri: `${ctx.base}server/state`,
120
+ name: `${ctx.key}server-state`,
121
+ title: `${ctx.titlePrefix}TM1 Server State`,
95
122
  description: "Health-check snapshot: connection state, version, capability flags, object counts (cubes/dimensions/processes/chores/clients).",
96
123
  mimeType: "application/json",
97
124
  },
98
125
  });
99
126
  // ── Resource templates ──────────────────────────────────────────────
100
- // Process source code — `tm1://process/{name}/code`
101
- server.registerResource("process-code", new ResourceTemplate("tm1://process/{name}/code", {
127
+ // Process source code — `tm1://[<connection>/]process/{name}/code`
128
+ server.registerResource(`${ctx.key}process-code`, new ResourceTemplate(`${ctx.base}process/{name}/code`, {
102
129
  list: async () => {
103
- const procs = await tm1.processes.list();
130
+ if (!ctx.enumerate())
131
+ return { resources: [] };
132
+ const procs = await (await ctx.client()).processes.list({
133
+ namesOnly: true,
134
+ });
104
135
  return {
105
136
  resources: procs
106
137
  .filter((p) => !p.name.startsWith("}"))
107
138
  .map((p) => ({
108
- name: `process-code-${p.name}`,
109
- uri: `tm1://process/${encodeURIComponent(p.name)}/code`,
110
- title: `TI: ${p.name}`,
139
+ name: `${ctx.key}process-code-${p.name}`,
140
+ uri: `${ctx.base}process/${encodeURIComponent(p.name)}/code`,
141
+ title: `${ctx.titlePrefix}TI: ${p.name}`,
111
142
  description: `Source code (Prolog/Metadata/Data/Epilog) of TI process '${p.name}'.`,
112
143
  mimeType: "application/json",
113
144
  })),
@@ -115,7 +146,9 @@ export function registerAllResources(server, tm1) {
115
146
  },
116
147
  complete: {
117
148
  name: async (value) => {
118
- const procs = await tm1.processes.list();
149
+ const procs = await (await ctx.client()).processes.list({
150
+ namesOnly: true,
151
+ });
119
152
  const lower = value.toLowerCase();
120
153
  return procs
121
154
  .filter((p) => !p.name.startsWith("}") && p.name.toLowerCase().includes(lower))
@@ -124,15 +157,15 @@ export function registerAllResources(server, tm1) {
124
157
  },
125
158
  },
126
159
  }), {
127
- title: "TI Process Source Code",
128
- description: "Source code of any TurboIntegrator process by name. URI: tm1://process/{name}/code.",
160
+ title: `${ctx.titlePrefix}TI Process Source Code`,
161
+ description: `Source code of any TurboIntegrator process by name. URI: ${ctx.base}process/{name}/code.`,
129
162
  mimeType: "application/json",
130
163
  }, async (uri, vars) => {
131
164
  const raw = vars.name;
132
165
  const name = decodeURIComponent(Array.isArray(raw) ? raw[0] : (raw ?? ""));
133
- const code = await tm1.processes.getCode(name);
166
+ const code = await (await ctx.client()).processes.getCode(name);
134
167
  // Hard-mask credential literals unconditionally: resources take no
135
- // parameters, so unlike tm1_get_process_code there is no maskSecrets
168
+ // parameters, so unlike tm1_get_process there is no maskSecrets
136
169
  // opt-out — returning the code verbatim would bypass the tool-path
137
170
  // redaction (ODBCOpen passwords, credential assignments).
138
171
  return asJsonContent(uri, {
@@ -145,38 +178,44 @@ export function registerAllResources(server, tm1) {
145
178
  entries.push({
146
179
  kind: "template",
147
180
  templateMetadata: {
148
- title: "TI Process Source Code",
149
- description: "Source code of any TurboIntegrator process by name. URI: tm1://process/{name}/code.",
181
+ title: `${ctx.titlePrefix}TI Process Source Code`,
182
+ description: `Source code of any TurboIntegrator process by name. URI: ${ctx.base}process/{name}/code.`,
150
183
  mimeType: "application/json",
151
184
  },
152
185
  list: async () => {
153
- const procs = await tm1.processes.list();
186
+ if (!ctx.enumerate())
187
+ return { resources: [] };
188
+ const procs = await (await ctx.client()).processes.list({
189
+ namesOnly: true,
190
+ });
154
191
  return {
155
192
  resources: procs
156
193
  .filter((p) => !p.name.startsWith("}"))
157
194
  .map((p) => ({
158
- name: `process-code-${p.name}`,
159
- uri: `tm1://process/${encodeURIComponent(p.name)}/code`,
160
- title: `TI: ${p.name}`,
195
+ name: `${ctx.key}process-code-${p.name}`,
196
+ uri: `${ctx.base}process/${encodeURIComponent(p.name)}/code`,
197
+ title: `${ctx.titlePrefix}TI: ${p.name}`,
161
198
  description: `Source code (Prolog/Metadata/Data/Epilog) of TI process '${p.name}'.`,
162
199
  mimeType: "application/json",
163
200
  })),
164
201
  };
165
202
  },
166
203
  });
167
- // Cube rules — `tm1://cube/{name}/rules`
168
- server.registerResource("cube-rules", new ResourceTemplate("tm1://cube/{name}/rules", {
204
+ // Cube rules — `tm1://[<connection>/]cube/{name}/rules`
205
+ server.registerResource(`${ctx.key}cube-rules`, new ResourceTemplate(`${ctx.base}cube/{name}/rules`, {
169
206
  list: async () => {
207
+ if (!ctx.enumerate())
208
+ return { resources: [] };
170
209
  // Filter to cubes that actually carry rules — avoids cluttering
171
210
  // the resource tree with rule-less cubes whose body is "".
172
- const cubes = await tm1.cubes.list({ includeRules: true });
211
+ const cubes = await (await ctx.client()).cubes.list({ includeRules: true });
173
212
  return {
174
213
  resources: cubes
175
214
  .filter((c) => !c.name.startsWith("}") && c.hasRules)
176
215
  .map((c) => ({
177
- name: `cube-rules-${c.name}`,
178
- uri: `tm1://cube/${encodeURIComponent(c.name)}/rules`,
179
- title: `Rules: ${c.name}`,
216
+ name: `${ctx.key}cube-rules-${c.name}`,
217
+ uri: `${ctx.base}cube/${encodeURIComponent(c.name)}/rules`,
218
+ title: `${ctx.titlePrefix}Rules: ${c.name}`,
180
219
  description: `Rules text of cube '${c.name}' (SKIPCHECK + FEEDERS sections).`,
181
220
  mimeType: "text/plain",
182
221
  })),
@@ -184,7 +223,7 @@ export function registerAllResources(server, tm1) {
184
223
  },
185
224
  complete: {
186
225
  name: async (value) => {
187
- const cubes = await tm1.cubes.list();
226
+ const cubes = await (await ctx.client()).cubes.list();
188
227
  const lower = value.toLowerCase();
189
228
  return cubes
190
229
  .filter((c) => !c.name.startsWith("}") && c.name.toLowerCase().includes(lower))
@@ -193,13 +232,13 @@ export function registerAllResources(server, tm1) {
193
232
  },
194
233
  },
195
234
  }), {
196
- title: "Cube Rules Text",
197
- description: "Rules text of any TM1 cube by name. URI: tm1://cube/{name}/rules. Returns plain text.",
235
+ title: `${ctx.titlePrefix}Cube Rules Text`,
236
+ description: `Rules text of any TM1 cube by name. URI: ${ctx.base}cube/{name}/rules. Returns plain text.`,
198
237
  mimeType: "text/plain",
199
238
  }, async (uri, vars) => {
200
239
  const raw = vars.name;
201
240
  const name = decodeURIComponent(Array.isArray(raw) ? raw[0] : (raw ?? ""));
202
- const rules = await tm1.cubes.getRules(name);
241
+ const rules = await (await ctx.client()).cubes.getRules(name);
203
242
  return {
204
243
  contents: [
205
244
  {
@@ -213,25 +252,26 @@ export function registerAllResources(server, tm1) {
213
252
  entries.push({
214
253
  kind: "template",
215
254
  templateMetadata: {
216
- title: "Cube Rules Text",
217
- description: "Rules text of any TM1 cube by name. URI: tm1://cube/{name}/rules. Returns plain text.",
255
+ title: `${ctx.titlePrefix}Cube Rules Text`,
256
+ description: `Rules text of any TM1 cube by name. URI: ${ctx.base}cube/{name}/rules. Returns plain text.`,
218
257
  mimeType: "text/plain",
219
258
  },
220
259
  list: async () => {
221
- const cubes = await tm1.cubes.list({ includeRules: true });
260
+ if (!ctx.enumerate())
261
+ return { resources: [] };
262
+ const cubes = await (await ctx.client()).cubes.list({ includeRules: true });
222
263
  return {
223
264
  resources: cubes
224
265
  .filter((c) => !c.name.startsWith("}") && c.hasRules)
225
266
  .map((c) => ({
226
- name: `cube-rules-${c.name}`,
227
- uri: `tm1://cube/${encodeURIComponent(c.name)}/rules`,
228
- title: `Rules: ${c.name}`,
267
+ name: `${ctx.key}cube-rules-${c.name}`,
268
+ uri: `${ctx.base}cube/${encodeURIComponent(c.name)}/rules`,
269
+ title: `${ctx.titlePrefix}Rules: ${c.name}`,
229
270
  description: `Rules text of cube '${c.name}' (SKIPCHECK + FEEDERS sections).`,
230
271
  mimeType: "text/plain",
231
272
  })),
232
273
  };
233
274
  },
234
275
  });
235
- return { entries };
236
276
  }
237
277
  //# sourceMappingURL=index.js.map
@@ -3,9 +3,10 @@ import type pino from "pino";
3
3
  export declare class SubscriptionRegistry {
4
4
  private readonly server;
5
5
  private readonly logger;
6
+ private readonly stateUris;
6
7
  private readonly subscribed;
7
8
  private mutationListener?;
8
- constructor(server: McpServer, logger: pino.Logger);
9
+ constructor(server: McpServer, logger: pino.Logger, stateUris?: ReadonlyMap<string, string>);
9
10
  /** Install subscribe/unsubscribe request handlers + mutation listener. */
10
11
  install(): void;
11
12
  /** Detach listener — used for shutdown / tests. */
@@ -14,19 +14,24 @@
14
14
  // resources/unsubscribe, notifications/resources/updated).
15
15
  import { SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { tm1Events } from "../lib/tm1-events.js";
17
- // URIs whose contents are sensitive to *any* mutation. The server-state
18
- // resource aggregates counts across cubes/dims/processes/chores/clients —
19
- // almost any non-safe call can change it.
20
- const STATE_SENSITIVE_URIS = new Set(["tm1://server/state"]);
17
+ // The server-state resource aggregates counts across cubes/dims/processes/
18
+ // chores/clients — almost any non-safe call can change it. With several
19
+ // connections there is one such resource per connection; stateUris maps each
20
+ // to the connectionId whose mutations affect it.
21
+ const SINGLE_STATE_URIS = new Map([
22
+ ["tm1://server/state", ""],
23
+ ]);
21
24
  export class SubscriptionRegistry {
22
25
  server;
23
26
  logger;
27
+ stateUris;
24
28
  // URIs the client has asked us to push updates for.
25
29
  subscribed = new Set();
26
30
  mutationListener;
27
- constructor(server, logger) {
31
+ constructor(server, logger, stateUris = SINGLE_STATE_URIS) {
28
32
  this.server = server;
29
33
  this.logger = logger;
34
+ this.stateUris = stateUris;
30
35
  }
31
36
  /** Install subscribe/unsubscribe request handlers + mutation listener. */
32
37
  install() {
@@ -62,13 +67,17 @@ export class SubscriptionRegistry {
62
67
  size() {
63
68
  return this.subscribed.size;
64
69
  }
65
- onMutation(_e) {
70
+ onMutation(e) {
66
71
  // Iterate subscribed URIs and notify those affected by this mutation.
67
72
  // Currently only the aggregate state resource is mutation-sensitive;
68
73
  // future entries can branch on e.path / e.method for finer-grained
69
74
  // notifications (e.g. tm1://process/{name}/code on /Processes mutations).
70
75
  for (const uri of this.subscribed) {
71
- if (!STATE_SENSITIVE_URIS.has(uri))
76
+ const target = this.stateUris.get(uri);
77
+ if (target === undefined)
78
+ continue;
79
+ // An empty target or an untagged event matches any connection.
80
+ if (target && e.connectionId && target !== e.connectionId)
72
81
  continue;
73
82
  // Fire-and-forget; missing transport (server not connected) throws.
74
83
  this.server.server.sendResourceUpdated({ uri }).catch((err) => {
@@ -0,0 +1,3 @@
1
+ import type { ConnectionRegistry } from "./connections.js";
2
+ export declare function serverInstructions(registry: ConnectionRegistry): string;
3
+ //# sourceMappingURL=server-instructions.d.ts.map
@@ -0,0 +1,12 @@
1
+ // Sent once per session (MCP `instructions`), so every sentence here is paid
2
+ // for by every conversation — keep it to what a model cannot infer from the
3
+ // tool list itself.
4
+ export function serverInstructions(registry) {
5
+ const lines = ["TM1 / IBM Planning Analytics server."];
6
+ if (!registry.isSingle) {
7
+ lines.push("Every tool except tm1_list_connections takes a `connection`; if the target is unclear, call tm1_list_connections and ask. Never write to a connection the user did not name.");
8
+ }
9
+ lines.push("Keep results small: filter by name, use projections (fields, include* flags, compact) and countOnly before paging.");
10
+ return lines.join(" ");
11
+ }
12
+ //# sourceMappingURL=server-instructions.js.map
@@ -1,5 +1,5 @@
1
1
  import type pino from "pino";
2
- import type { TM1Config } from "../config.js";
2
+ import { type TM1Config } from "../config.js";
3
3
  import type { SessionManager } from "../session-manager.js";
4
4
  import { TM1Error } from "../types.js";
5
5
  export interface RequestOptions {
@@ -1,3 +1,4 @@
1
+ import { connectionIdOf } from "../config.js";
1
2
  import { createConnectionProfile, } from "./connection/profile.js";
2
3
  import { TM1Error, TM1ErrorCode } from "../types.js";
3
4
  import { PRODUCT, VERSION } from "../version.js";
@@ -110,13 +111,21 @@ export class TM1HttpClient {
110
111
  }
111
112
  const retryResult = await this.handleResponse(retryResponse, path);
112
113
  if (!isSafeMethod) {
113
- tm1Events.emit("mutation", { method, path });
114
+ tm1Events.emit("mutation", {
115
+ method,
116
+ path,
117
+ connectionId: connectionIdOf(this.config),
118
+ });
114
119
  }
115
120
  return retryResult;
116
121
  }
117
122
  const result = await this.handleResponse(response, path);
118
123
  if (!isSafeMethod) {
119
- tm1Events.emit("mutation", { method, path });
124
+ tm1Events.emit("mutation", {
125
+ method,
126
+ path,
127
+ connectionId: connectionIdOf(this.config),
128
+ });
120
129
  }
121
130
  return result;
122
131
  }
@@ -191,7 +200,11 @@ export class TM1HttpClient {
191
200
  }
192
201
  const text = await response.text();
193
202
  if (!isSafeHttpMethod(method)) {
194
- tm1Events.emit("mutation", { method, path });
203
+ tm1Events.emit("mutation", {
204
+ method,
205
+ path,
206
+ connectionId: connectionIdOf(this.config),
207
+ });
195
208
  }
196
209
  return text;
197
210
  }
@@ -225,7 +238,11 @@ export class TM1HttpClient {
225
238
  throw this.classifyHttpError(response.status, path, errBody || undefined);
226
239
  }
227
240
  if (!isSafeHttpMethod(method)) {
228
- tm1Events.emit("mutation", { method, path });
241
+ tm1Events.emit("mutation", {
242
+ method,
243
+ path,
244
+ connectionId: connectionIdOf(this.config),
245
+ });
229
246
  }
230
247
  }
231
248
  /**
@@ -12,6 +12,12 @@ export interface ProcessVariableLayout {
12
12
  export interface ProcessListOpts extends NameFilterOpts {
13
13
  /** Set to slice server-side. Only legal when every active filter is in NameFilterOpts. */
14
14
  page?: PageOpts;
15
+ /**
16
+ * Select Name only; every process comes back with `parameters: []`. TM1
17
+ * then serializes no parameter blocks at all — for callers that only need
18
+ * names (counts, existence checks, listings) this is the cheap query.
19
+ */
20
+ namesOnly?: boolean;
15
21
  }
16
22
  export declare class ProcessService {
17
23
  private readonly http;
@@ -29,7 +35,7 @@ export declare class ProcessService {
29
35
  * Name-only when v11 rejects the inline Parameters select.
30
36
  * GET /api/v1/Processes?$select=Name,Parameters
31
37
  */
32
- list(): Promise<Process[]>;
38
+ list(opts?: Pick<ProcessListOpts, "namesOnly">): Promise<Process[]>;
33
39
  /**
34
40
  * Paged variant: with `page` set the slice happens server-side
35
41
  * (`$orderby=Name&$top&$skip&$count=true`) and the return carries the
@@ -85,6 +85,12 @@ export class ProcessService {
85
85
  // — TM1 v11 rejects $expand=Parameters with a syntax error. Use $select
86
86
  // instead, which returns Parameters inline. Param.Type comes back as the
87
87
  // already-decoded string "Numeric" / "String" (not the legacy int code).
88
+ const namesOnly = async () => {
89
+ const response = await this.http.request("GET", `/api/v1/Processes?$select=Name${query}`);
90
+ return wrap(response.value.map((p) => ({ name: p.Name, parameters: [] })), readCount(response));
91
+ };
92
+ if (opts.namesOnly)
93
+ return namesOnly();
88
94
  try {
89
95
  const response = await this.http.request("GET", `/api/v1/Processes?$select=Name,Parameters${query}`);
90
96
  return wrap(response.value.map((p) => ({
@@ -99,11 +105,7 @@ export class ProcessService {
99
105
  }
100
106
  catch (e) {
101
107
  rethrowIfSystemic(e);
102
- const response = await this.http.request("GET", `/api/v1/Processes?$select=Name${query}`);
103
- return wrap(response.value.map((p) => ({
104
- name: p.Name,
105
- parameters: [],
106
- })), readCount(response));
108
+ return namesOnly();
107
109
  }
108
110
  }
109
111
  /**
@@ -1,5 +1,5 @@
1
1
  import type pino from "pino";
2
- import type { TM1Config } from "./config.js";
2
+ import { type TM1Config } from "./config.js";
3
3
  import type { SessionManager } from "./session-manager.js";
4
4
  import { BatchService } from "./tm1-client/services/batch-service.js";
5
5
  import { CubeService } from "./tm1-client/services/cube-service.js";
@@ -38,6 +38,12 @@ export declare class TM1Client {
38
38
  constructor(config: TM1Config, sessionManager: SessionManager, logger: pino.Logger);
39
39
  /** The configured TM1 major version (11 or 12). Fixed for this connection. */
40
40
  get version(): 11 | 12;
41
+ /** Effective tool mode, the environment label, and why mode was forced. */
42
+ get access(): {
43
+ mode: TM1Config["mode"];
44
+ environment: TM1Config["environment"];
45
+ modeReason: string | undefined;
46
+ };
41
47
  /**
42
48
  * Stable label for this connection, used to keep per-connection files (the
43
49
  * process backups) apart: host and port, plus the v12 instance/database.
@@ -1,3 +1,4 @@
1
+ import { connectionIdOf } from "./config.js";
1
2
  import { TM1HttpClient } from "./tm1-client/http.js";
2
3
  import { BatchService } from "./tm1-client/services/batch-service.js";
3
4
  import { CubeService } from "./tm1-client/services/cube-service.js";
@@ -82,22 +83,20 @@ export class TM1Client {
82
83
  get version() {
83
84
  return this.config.version;
84
85
  }
86
+ /** Effective tool mode, the environment label, and why mode was forced. */
87
+ get access() {
88
+ return {
89
+ mode: this.config.mode,
90
+ environment: this.config.environment,
91
+ modeReason: this.config.modeReason,
92
+ };
93
+ }
85
94
  /**
86
95
  * Stable label for this connection, used to keep per-connection files (the
87
96
  * process backups) apart: host and port, plus the v12 instance/database.
88
97
  */
89
98
  get connectionId() {
90
- let host;
91
- try {
92
- const url = new URL(this.config.baseUrl);
93
- host = url.port ? `${url.hostname}_${url.port}` : url.hostname;
94
- }
95
- catch {
96
- host = this.config.baseUrl;
97
- }
98
- return [host, this.config.instance, this.config.database]
99
- .filter((part) => Boolean(part))
100
- .join("_");
99
+ return connectionIdOf(this.config);
101
100
  }
102
101
  /**
103
102
  * Authenticate and start the keep-alive timer.
@@ -1,2 +1,12 @@
1
+ import { buildCubeOrDimUsages } from "../../lib/callgraph/callGraph.js";
1
2
  export declare const registerAnalyzeObjectUsage: import("../define-tool.js").ToolRegistrar;
3
+ /** One row per referencing process or rule, most references first. */
4
+ export declare function summarizeBySource(all: ReturnType<typeof buildCubeOrDimUsages>): {
5
+ sourceKind: "process" | "rule";
6
+ sourceName: string;
7
+ accessTypes: string[];
8
+ sections: string[];
9
+ funcNames: string[];
10
+ count: number;
11
+ }[];
2
12
  //# sourceMappingURL=analyze-object-usage.d.ts.map
@@ -64,39 +64,7 @@ export const registerAnalyzeObjectUsage = defineTool({
64
64
  accessMode,
65
65
  });
66
66
  if (mode === "summary") {
67
- // Aggregate per source (process or rule). Key by kind+name so a process
68
- // and a cube-rule sharing a name don't collapse into one row.
69
- const bySource = new Map();
70
- for (const u of all) {
71
- const key = `${u.sourceKind}\x00${u.sourceName}`;
72
- let s = bySource.get(key);
73
- if (!s) {
74
- s = {
75
- sourceKind: u.sourceKind,
76
- sourceName: u.sourceName,
77
- accessTypes: new Set(),
78
- sections: new Set(),
79
- funcNames: new Set(),
80
- count: 0,
81
- };
82
- bySource.set(key, s);
83
- }
84
- s.accessTypes.add(u.accessType);
85
- s.sections.add(u.section);
86
- if (u.funcName)
87
- s.funcNames.add(u.funcName);
88
- s.count++;
89
- }
90
- const allSources = [...bySource.values()]
91
- .map((s) => ({
92
- sourceKind: s.sourceKind,
93
- sourceName: s.sourceName,
94
- accessTypes: [...s.accessTypes].sort(),
95
- sections: [...s.sections].sort(),
96
- funcNames: [...s.funcNames].sort(),
97
- count: s.count,
98
- }))
99
- .sort((a, b) => b.count - a.count || a.sourceName.localeCompare(b.sourceName));
67
+ const allSources = summarizeBySource(all);
100
68
  const sumTruncated = allSources.length > effectiveLimit;
101
69
  const sources = sumTruncated
102
70
  ? allSources.slice(0, effectiveLimit)
@@ -140,4 +108,40 @@ export const registerAnalyzeObjectUsage = defineTool({
140
108
  };
141
109
  },
142
110
  });
111
+ /** One row per referencing process or rule, most references first. */
112
+ export function summarizeBySource(all) {
113
+ // Aggregate per source (process or rule). Key by kind+name so a process
114
+ // and a cube-rule sharing a name don't collapse into one row.
115
+ const bySource = new Map();
116
+ for (const u of all) {
117
+ const key = `${u.sourceKind}\x00${u.sourceName}`;
118
+ let s = bySource.get(key);
119
+ if (!s) {
120
+ s = {
121
+ sourceKind: u.sourceKind,
122
+ sourceName: u.sourceName,
123
+ accessTypes: new Set(),
124
+ sections: new Set(),
125
+ funcNames: new Set(),
126
+ count: 0,
127
+ };
128
+ bySource.set(key, s);
129
+ }
130
+ s.accessTypes.add(u.accessType);
131
+ s.sections.add(u.section);
132
+ if (u.funcName)
133
+ s.funcNames.add(u.funcName);
134
+ s.count++;
135
+ }
136
+ return [...bySource.values()]
137
+ .map((s) => ({
138
+ sourceKind: s.sourceKind,
139
+ sourceName: s.sourceName,
140
+ accessTypes: [...s.accessTypes].sort(),
141
+ sections: [...s.sections].sort(),
142
+ funcNames: [...s.funcNames].sort(),
143
+ count: s.count,
144
+ }))
145
+ .sort((a, b) => b.count - a.count || a.sourceName.localeCompare(b.sourceName));
146
+ }
143
147
  //# sourceMappingURL=analyze-object-usage.js.map
@@ -240,7 +240,7 @@ export const registerAuditNaming = defineTool({
240
240
  // ── Processes + (optional) variables ───────────────────────────────
241
241
  let processNames = [];
242
242
  if (want("processes") || want("processVariables")) {
243
- const procs = await tm1Client.processes.list();
243
+ const procs = await tm1Client.processes.list({ namesOnly: true });
244
244
  processNames = (includeControl ? procs : procs.filter((p) => !isControlName(p.name))).map((p) => p.name);
245
245
  }
246
246
  if (want("processes")) {