@ffschrattenecker/tm1-mcp-server 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -1
- package/README.md +28 -7
- package/dist/config.d.ts +12 -1
- package/dist/config.js +120 -79
- package/dist/connections.d.ts +69 -0
- package/dist/connections.js +262 -0
- package/dist/http-transport.d.ts +2 -2
- package/dist/index.js +45 -37
- package/dist/lib/callgraph/tm1-adapter.d.ts +4 -1
- package/dist/lib/callgraph/tm1-adapter.js +5 -2
- package/dist/lib/slim-json-schema.d.ts +8 -0
- package/dist/lib/slim-json-schema.js +34 -0
- package/dist/lib/strip-comments.js +1 -1
- package/dist/lib/tm1-events.d.ts +2 -0
- package/dist/prompts/index.js +2 -2
- package/dist/resources/index.d.ts +4 -2
- package/dist/resources/index.js +93 -53
- package/dist/resources/subscriptions.d.ts +2 -1
- package/dist/resources/subscriptions.js +16 -7
- package/dist/server-instructions.d.ts +3 -0
- package/dist/server-instructions.js +12 -0
- package/dist/tm1-client/http.d.ts +1 -1
- package/dist/tm1-client/http.js +21 -4
- package/dist/tm1-client/services/process-service.d.ts +7 -1
- package/dist/tm1-client/services/process-service.js +7 -5
- package/dist/tm1-client.d.ts +7 -1
- package/dist/tm1-client.js +10 -11
- package/dist/tools/analysis/analyze-object-usage.d.ts +10 -0
- package/dist/tools/analysis/analyze-object-usage.js +37 -33
- package/dist/tools/analysis/audit-naming.js +1 -1
- package/dist/tools/analysis/delete-impact.d.ts +13 -0
- package/dist/tools/analysis/delete-impact.js +30 -0
- package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
- package/dist/tools/analysis/trace-data-flow.js +1 -1
- package/dist/tools/celldata/write-cells.js +37 -2
- package/dist/tools/confirm.d.ts +4 -1
- package/dist/tools/confirm.js +12 -2
- package/dist/tools/define-tool.d.ts +35 -11
- package/dist/tools/define-tool.js +51 -4
- package/dist/tools/dimension-management/delete-dimension.js +18 -4
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +6 -12
- package/dist/tools/metadata/list-cubes.js +3 -3
- package/dist/tools/metadata/list-processes.js +11 -7
- package/dist/tools/model-building/delete-cube.js +18 -4
- package/dist/tools/model-building/set-cube-rules.js +13 -1
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-audit-log.js +1 -1
- package/dist/tools/operations/get-jobs.js +2 -2
- package/dist/tools/operations/get-message-log.js +1 -1
- package/dist/tools/operations/get-server-info.js +10 -1
- package/dist/tools/operations/get-server-state.js +1 -1
- package/dist/tools/operations/get-threads.js +2 -2
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-connections.d.ts +2 -0
- package/dist/tools/operations/list-connections.js +35 -0
- package/dist/tools/operations/save-data.js +1 -1
- package/dist/tools/schemas/items-monitoring.d.ts +11 -0
- package/dist/tools/schemas/items-monitoring.js +9 -1
- package/dist/tools/schemas/items-processes.d.ts +3 -0
- package/dist/tools/schemas/items-processes.js +2 -1
- package/dist/tools/ti-development/diff-processes.d.ts +42 -0
- package/dist/tools/ti-development/diff-processes.js +4 -4
- package/dist/tools/ti-development/execute-process.js +29 -4
- package/dist/tools/ti-development/import-pro-file.js +1 -1
- package/dist/tools/ti-development/preflight.d.ts +24 -0
- package/dist/tools/ti-development/preflight.js +32 -0
- package/dist/tools/ti-development/search-code.js +1 -1
- package/dist/tools/ti-development/upsert-process.js +100 -11
- package/dist/tools/with-annotations.d.ts +10 -0
- package/dist/tools/with-annotations.js +58 -3
- package/npm-shrinkwrap.json +4586 -0
- package/package.json +9 -7
- package/dist/tools/ti-development/get-process-code.d.ts +0 -2
- package/dist/tools/ti-development/get-process-code.js +0 -100
- package/dist/tools/ti-development/get-process-datasource.d.ts +0 -2
- package/dist/tools/ti-development/get-process-datasource.js +0 -20
- package/dist/tools/ti-development/get-process-parameters.d.ts +0 -2
- package/dist/tools/ti-development/get-process-parameters.js +0 -25
- package/dist/tools/ti-development/get-process-variables.d.ts +0 -2
- package/dist/tools/ti-development/get-process-variables.js +0 -38
package/dist/resources/index.js
CHANGED
|
@@ -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,
|
|
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(
|
|
30
|
-
title:
|
|
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
|
|
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:
|
|
53
|
-
name:
|
|
54
|
-
title:
|
|
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(
|
|
60
|
-
title:
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
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:
|
|
93
|
-
name:
|
|
94
|
-
title:
|
|
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(
|
|
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
|
-
|
|
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:
|
|
109
|
-
uri:
|
|
110
|
-
title:
|
|
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
|
|
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:
|
|
128
|
-
description:
|
|
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
|
|
166
|
+
const code = await (await ctx.client()).processes.getCode(name);
|
|
134
167
|
// Hard-mask credential literals unconditionally: resources take no
|
|
135
|
-
// parameters, so unlike
|
|
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:
|
|
149
|
-
description:
|
|
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
|
-
|
|
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:
|
|
159
|
-
uri:
|
|
160
|
-
title:
|
|
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(
|
|
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
|
|
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:
|
|
178
|
-
uri:
|
|
179
|
-
title:
|
|
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
|
|
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:
|
|
197
|
-
description:
|
|
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
|
|
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:
|
|
217
|
-
description:
|
|
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
|
-
|
|
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:
|
|
227
|
-
uri:
|
|
228
|
-
title:
|
|
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
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
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(
|
|
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
|
-
|
|
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,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
|
package/dist/tm1-client/http.js
CHANGED
|
@@ -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", {
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
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
|
/**
|
package/dist/tm1-client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type pino from "pino";
|
|
2
|
-
import type
|
|
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.
|
package/dist/tm1-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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")) {
|