@ffschrattenecker/tm1-mcp-server 6.1.1 → 7.0.1

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 (63) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +23 -5
  3. package/dist/config.d.ts +9 -0
  4. package/dist/config.js +89 -64
  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 -40
  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 +1 -1
  27. package/dist/tm1-client.js +2 -11
  28. package/dist/tools/analysis/audit-naming.js +1 -1
  29. package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
  30. package/dist/tools/analysis/trace-data-flow.js +1 -1
  31. package/dist/tools/confirm.js +1 -1
  32. package/dist/tools/define-tool.d.ts +35 -11
  33. package/dist/tools/define-tool.js +51 -4
  34. package/dist/tools/index.d.ts +2 -2
  35. package/dist/tools/index.js +6 -12
  36. package/dist/tools/metadata/list-cubes.js +3 -3
  37. package/dist/tools/metadata/list-processes.js +11 -7
  38. package/dist/tools/model-building/unload-cube.js +1 -1
  39. package/dist/tools/operations/get-audit-log.js +1 -1
  40. package/dist/tools/operations/get-jobs.js +2 -2
  41. package/dist/tools/operations/get-message-log.js +1 -1
  42. package/dist/tools/operations/get-server-state.js +1 -1
  43. package/dist/tools/operations/get-threads.js +2 -2
  44. package/dist/tools/operations/get-transaction-log.js +1 -1
  45. package/dist/tools/operations/list-connections.d.ts +2 -0
  46. package/dist/tools/operations/list-connections.js +39 -0
  47. package/dist/tools/operations/save-data.js +1 -1
  48. package/dist/tools/ti-development/execute-process.js +2 -2
  49. package/dist/tools/ti-development/import-pro-file.js +1 -1
  50. package/dist/tools/ti-development/search-code.js +1 -1
  51. package/dist/tools/ti-development/upsert-process.js +2 -2
  52. package/dist/tools/with-annotations.d.ts +10 -0
  53. package/dist/tools/with-annotations.js +58 -3
  54. package/npm-shrinkwrap.json +2 -2
  55. package/package.json +4 -3
  56. package/dist/tools/ti-development/get-process-code.d.ts +0 -2
  57. package/dist/tools/ti-development/get-process-code.js +0 -100
  58. package/dist/tools/ti-development/get-process-datasource.d.ts +0 -2
  59. package/dist/tools/ti-development/get-process-datasource.js +0 -20
  60. package/dist/tools/ti-development/get-process-parameters.d.ts +0 -2
  61. package/dist/tools/ti-development/get-process-parameters.js +0 -25
  62. package/dist/tools/ti-development/get-process-variables.d.ts +0 -2
  63. package/dist/tools/ti-development/get-process-variables.js +0 -38
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,27 +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");
79
- }
80
- if (config.modeReason) {
81
- logger.warn(`${config.modeReason} Write and destructive tools will not be registered.`);
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
+ }
82
89
  }
83
- else if (config.mode === "readonly") {
84
- 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);
85
95
  }
86
96
  // Branch on transport. stdio is the default for local MCP-client setups
87
97
  // (Claude Code, Claude Desktop) and gets one long-lived server. http
@@ -92,14 +102,14 @@ async function main() {
92
102
  // runs in shutdown(). http builds one per request and disposes each in the
93
103
  // transport's own teardown — nothing process-scoped is left for us here.
94
104
  let stdioDispose;
95
- const httpCloser = config.transport === "http"
96
- ? await startHttpTransport(() => buildMcpServer(tm1Client, config, logger), config, logger)
105
+ const httpCloser = settings.transport === "http"
106
+ ? await startHttpTransport(() => buildMcpServer(registry, settings, logger), settings, logger)
97
107
  : await (async () => {
98
- const built = buildMcpServer(tm1Client, config, logger);
108
+ const built = buildMcpServer(registry, settings, logger);
99
109
  stdioDispose = built.dispose;
100
110
  return startStdioTransport(built.server, logger);
101
111
  })();
102
- logger.info(`MCP server configured (mode: ${config.mode}, transport: ${config.transport})`);
112
+ logger.info(`MCP server configured (connections: ${registry.names.join(", ")}, transport: ${settings.transport})`);
103
113
  // Graceful shutdown — ensure TM1 session is always cleaned up.
104
114
  let shuttingDown = false;
105
115
  const shutdown = async (signal, exitCode = 0) => {
@@ -119,12 +129,7 @@ async function main() {
119
129
  catch (err) {
120
130
  logger.error({ err }, "Error closing transport");
121
131
  }
122
- try {
123
- await tm1Client.disconnect();
124
- }
125
- catch (err) {
126
- logger.error({ err }, "Error during disconnect");
127
- }
132
+ await registry.disconnectAll();
128
133
  process.exit(exitCode);
129
134
  };
130
135
  process.on("SIGINT", () => {
@@ -139,7 +144,7 @@ async function main() {
139
144
  // Parent process death — only meaningful on stdio (Claude spawns us as a
140
145
  // child). For http we ignore stdin events since the process lifecycle is
141
146
  // independent.
142
- if (config.transport === "stdio") {
147
+ if (settings.transport === "stdio") {
143
148
  process.stdin.on("end", () => {
144
149
  logger.info("stdin ended (parent process gone), shutting down");
145
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
@@ -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. */