@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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Named TM1 connections served by ONE server process.
|
|
2
|
+
//
|
|
3
|
+
// Before v7 every TM1 connection was its own server: one `.env` per folder
|
|
4
|
+
// under ~/.tm1/mcp-servers/, one process, one full copy of the tool list. With
|
|
5
|
+
// N connections a client carried N × ~113 tool names in context every turn,
|
|
6
|
+
// spawned N processes and held N sessions + keepalives open. The registry keeps
|
|
7
|
+
// the same folders as the source of truth (they are also what the tm1-api
|
|
8
|
+
// skill reads) but serves them all from one tool list: every tool takes a
|
|
9
|
+
// `connection` argument, and each connection's TM1Client is built on first use.
|
|
10
|
+
//
|
|
11
|
+
// Selection:
|
|
12
|
+
// TM1_CONNECTIONS_DIR set → discover that folder
|
|
13
|
+
// TM1_BASE_URL set (and no dir) → legacy single connection "default"
|
|
14
|
+
// neither → discover ~/.tm1/mcp-servers
|
|
15
|
+
// TM1_CONNECTIONS (comma-separated) narrows discovery to the named folders.
|
|
16
|
+
//
|
|
17
|
+
// Per connection, only the folder's own `.env` decides mode and version. Mode
|
|
18
|
+
// defaults to readonly: a connection-level key (TM1_MODE, TM1_BASE_URL, …) in
|
|
19
|
+
// the server's own environment is NOT inherited, so a TM1_MODE=readwrite in
|
|
20
|
+
// the launching shell cannot silently arm every connection.
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { parse as parseDotenv } from "dotenv";
|
|
25
|
+
import pino from "pino";
|
|
26
|
+
import { connectionIdOf, loadConfig } from "./config.js";
|
|
27
|
+
import { SessionManager } from "./session-manager.js";
|
|
28
|
+
import { TM1Client } from "./tm1-client.js";
|
|
29
|
+
import { TM1Error, TM1ErrorCode } from "./types.js";
|
|
30
|
+
/** Settings that belong to the server process, not to one TM1 connection. */
|
|
31
|
+
const SERVER_LEVEL_KEYS = new Set([
|
|
32
|
+
"TM1_LOG_LEVEL",
|
|
33
|
+
"TM1_LOG_FILE",
|
|
34
|
+
"TM1_RESPONSE_MODE",
|
|
35
|
+
"TM1_MAX_RESPONSE_CHARS",
|
|
36
|
+
"TM1_CONNECTIONS_DIR",
|
|
37
|
+
"TM1_CONNECTIONS",
|
|
38
|
+
]);
|
|
39
|
+
function isServerLevelKey(key) {
|
|
40
|
+
return SERVER_LEVEL_KEYS.has(key) || key.startsWith("TM1_MCP_");
|
|
41
|
+
}
|
|
42
|
+
export class ConnectionRegistry {
|
|
43
|
+
logger;
|
|
44
|
+
entries = new Map();
|
|
45
|
+
constructor(logger) {
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
}
|
|
48
|
+
/** Discover connections from the environment (see the header comment). */
|
|
49
|
+
static fromEnvironment(env, logger) {
|
|
50
|
+
const registry = new ConnectionRegistry(logger);
|
|
51
|
+
const explicitDir = env.TM1_CONNECTIONS_DIR;
|
|
52
|
+
if (!explicitDir && env.TM1_BASE_URL) {
|
|
53
|
+
registry.addConfig("default", loadConfig(env));
|
|
54
|
+
return registry;
|
|
55
|
+
}
|
|
56
|
+
const dir = explicitDir || join(homedir(), ".tm1", "mcp-servers");
|
|
57
|
+
registry.discover(dir, env);
|
|
58
|
+
if (registry.entries.size === 0) {
|
|
59
|
+
throw new Error(`No TM1 connections found. Set TM1_BASE_URL (single connection) or ` +
|
|
60
|
+
`create <name>/.env folders under ${dir} (or point TM1_CONNECTIONS_DIR at them).`);
|
|
61
|
+
}
|
|
62
|
+
// Folders exist but none is usable: fail at startup with every reason,
|
|
63
|
+
// rather than serve tools whose `connection` enum is empty.
|
|
64
|
+
if (registry.usableNames.length === 0) {
|
|
65
|
+
const reasons = registry
|
|
66
|
+
.status()
|
|
67
|
+
.map((c) => ` ${c.name}: ${c.configError}`)
|
|
68
|
+
.join("\n");
|
|
69
|
+
throw new Error(`No usable TM1 connection under ${dir}:\n${reasons}`);
|
|
70
|
+
}
|
|
71
|
+
return registry;
|
|
72
|
+
}
|
|
73
|
+
/** Wrap one prebuilt client (tests, embedders). */
|
|
74
|
+
static single(client, logger = pino({ level: "silent" })) {
|
|
75
|
+
return ConnectionRegistry.of([{ name: "default", client }], logger);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Prebuilt clients under explicit names. The owner keeps the clients'
|
|
79
|
+
* lifecycle: disconnectAll() leaves them alone. Mode defaults to readwrite
|
|
80
|
+
* because the registration-time gate (TM1_MODE via withAnnotations) already
|
|
81
|
+
* decided what an embedder may call.
|
|
82
|
+
*/
|
|
83
|
+
static of(clients, logger = pino({ level: "silent" })) {
|
|
84
|
+
const registry = new ConnectionRegistry(logger);
|
|
85
|
+
for (const { name, client, mode } of clients) {
|
|
86
|
+
registry.entries.set(name, {
|
|
87
|
+
info: {
|
|
88
|
+
name,
|
|
89
|
+
version: client.version,
|
|
90
|
+
mode: mode ?? "readwrite",
|
|
91
|
+
connectionId: client.connectionId,
|
|
92
|
+
},
|
|
93
|
+
client,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return registry;
|
|
97
|
+
}
|
|
98
|
+
addConfig(name, config, dir) {
|
|
99
|
+
this.entries.set(name, {
|
|
100
|
+
info: {
|
|
101
|
+
name,
|
|
102
|
+
dir,
|
|
103
|
+
mode: config.mode,
|
|
104
|
+
environment: config.environment,
|
|
105
|
+
modeReason: config.modeReason,
|
|
106
|
+
version: config.version,
|
|
107
|
+
tm1Version: config.tm1Version,
|
|
108
|
+
baseUrl: config.baseUrl,
|
|
109
|
+
connectionId: connectionIdOf(config),
|
|
110
|
+
},
|
|
111
|
+
config,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
discover(dir, env) {
|
|
115
|
+
if (!existsSync(dir))
|
|
116
|
+
return;
|
|
117
|
+
const only = env.TM1_CONNECTIONS
|
|
118
|
+
? new Set(env.TM1_CONNECTIONS.split(",")
|
|
119
|
+
.map((s) => s.trim())
|
|
120
|
+
.filter(Boolean))
|
|
121
|
+
: undefined;
|
|
122
|
+
// Server-level settings and non-TM1 variables (PATH, proxies) carry over;
|
|
123
|
+
// connection-level TM1_* keys come from the folder only.
|
|
124
|
+
const base = {};
|
|
125
|
+
for (const [key, value] of Object.entries(env)) {
|
|
126
|
+
if (!key.startsWith("TM1_") || isServerLevelKey(key))
|
|
127
|
+
base[key] = value;
|
|
128
|
+
}
|
|
129
|
+
const names = readdirSync(dir, { withFileTypes: true })
|
|
130
|
+
.filter((d) => d.isDirectory() && existsSync(join(dir, d.name, ".env")))
|
|
131
|
+
.map((d) => d.name)
|
|
132
|
+
.filter((name) => !only || only.has(name))
|
|
133
|
+
.sort((a, b) => a.localeCompare(b));
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
const folder = join(dir, name);
|
|
136
|
+
try {
|
|
137
|
+
const fileEnv = parseDotenv(readFileSync(join(folder, ".env")));
|
|
138
|
+
const connEnv = { ...base };
|
|
139
|
+
for (const [key, value] of Object.entries(fileEnv)) {
|
|
140
|
+
// The folder may not override server-level settings: one folder's
|
|
141
|
+
// TM1_MCP_TRANSPORT must not reconfigure the whole process.
|
|
142
|
+
if (!isServerLevelKey(key))
|
|
143
|
+
connEnv[key] = value;
|
|
144
|
+
}
|
|
145
|
+
this.addConfig(name, loadConfig(connEnv), folder);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
this.logger.warn({ connection: name, err: message }, "skipping connection");
|
|
150
|
+
this.entries.set(name, {
|
|
151
|
+
info: { name, dir: folder, configError: message },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Every connection name, including ones whose config failed to load. */
|
|
157
|
+
get names() {
|
|
158
|
+
return [...this.entries.keys()];
|
|
159
|
+
}
|
|
160
|
+
/** Connections that can actually be used. */
|
|
161
|
+
get usableNames() {
|
|
162
|
+
return [...this.entries.values()]
|
|
163
|
+
.filter((e) => e.config || e.client)
|
|
164
|
+
.map((e) => e.info.name);
|
|
165
|
+
}
|
|
166
|
+
/** True when tools should not expose a `connection` argument at all. */
|
|
167
|
+
get isSingle() {
|
|
168
|
+
return this.usableNames.length === 1;
|
|
169
|
+
}
|
|
170
|
+
get anyReadwrite() {
|
|
171
|
+
return [...this.entries.values()].some((e) => e.info.mode === "readwrite");
|
|
172
|
+
}
|
|
173
|
+
hasVersion(version) {
|
|
174
|
+
return [...this.entries.values()].some((e) => e.info.version === version);
|
|
175
|
+
}
|
|
176
|
+
/** A client exists and is not mid-login. Never triggers a login itself. */
|
|
177
|
+
isConnected(name) {
|
|
178
|
+
const entry = this.entries.get(name);
|
|
179
|
+
return entry?.client !== undefined && entry.connecting === undefined;
|
|
180
|
+
}
|
|
181
|
+
info(name) {
|
|
182
|
+
return this.entries.get(name)?.info;
|
|
183
|
+
}
|
|
184
|
+
status() {
|
|
185
|
+
return [...this.entries.values()].map((e) => ({
|
|
186
|
+
...e.info,
|
|
187
|
+
connected: e.client !== undefined && e.connecting === undefined && !e.lastError,
|
|
188
|
+
lastError: e.lastError,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/** Resolve a connection name (optional when there is only one). */
|
|
192
|
+
entryFor(name) {
|
|
193
|
+
if (name === undefined) {
|
|
194
|
+
if (this.isSingle)
|
|
195
|
+
return this.entries.get(this.usableNames[0]);
|
|
196
|
+
throw new TM1Error({
|
|
197
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
198
|
+
message: `connection is required. One of: ${this.names.join(", ")}.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const entry = this.entries.get(name);
|
|
202
|
+
if (!entry) {
|
|
203
|
+
throw new TM1Error({
|
|
204
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
205
|
+
message: `Unknown connection "${name}". One of: ${this.names.join(", ")}.`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (entry.info.configError) {
|
|
209
|
+
throw new TM1Error({
|
|
210
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
211
|
+
message: `Connection "${name}" is misconfigured: ${entry.info.configError}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The client for a connection, built and logged in on first use. A failed
|
|
218
|
+
* login does not poison the entry: the client's own request path retries
|
|
219
|
+
* authentication, so the next call gets a fresh attempt.
|
|
220
|
+
*/
|
|
221
|
+
async get(name) {
|
|
222
|
+
const entry = this.entryFor(name);
|
|
223
|
+
if (!entry.client) {
|
|
224
|
+
const config = entry.config;
|
|
225
|
+
const logger = this.logger.child({ connection: entry.info.name });
|
|
226
|
+
const sessionManager = new SessionManager(config, logger);
|
|
227
|
+
entry.client = new TM1Client(config, sessionManager, logger);
|
|
228
|
+
entry.connecting = entry.client
|
|
229
|
+
.connect()
|
|
230
|
+
.then(() => {
|
|
231
|
+
entry.lastError = undefined;
|
|
232
|
+
})
|
|
233
|
+
.catch((err) => {
|
|
234
|
+
entry.lastError = err instanceof Error ? err.message : String(err);
|
|
235
|
+
logger.warn({ err }, "initial TM1 connection failed — will retry on request");
|
|
236
|
+
})
|
|
237
|
+
.finally(() => {
|
|
238
|
+
entry.connecting = undefined;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (entry.connecting)
|
|
242
|
+
await entry.connecting;
|
|
243
|
+
return entry.client;
|
|
244
|
+
}
|
|
245
|
+
/** Connection metadata for a resolved call (mode/version gates). */
|
|
246
|
+
resolveInfo(name) {
|
|
247
|
+
return this.entryFor(name).info;
|
|
248
|
+
}
|
|
249
|
+
async disconnectAll() {
|
|
250
|
+
await Promise.all([...this.entries.values()].map(async (e) => {
|
|
251
|
+
if (!e.client || !e.config)
|
|
252
|
+
return; // prebuilt clients are the owner's
|
|
253
|
+
try {
|
|
254
|
+
await e.client.disconnect();
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
this.logger.error({ err, connection: e.info.name }, "disconnect failed");
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=connections.js.map
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type pino from "pino";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ServerSettings } from "./config.js";
|
|
4
4
|
export declare function startHttpTransport(buildServer: () => {
|
|
5
5
|
server: McpServer;
|
|
6
6
|
dispose: () => void;
|
|
7
|
-
}, config:
|
|
7
|
+
}, config: ServerSettings, logger: pino.Logger): Promise<() => Promise<void>>;
|
|
8
8
|
//# sourceMappingURL=http-transport.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import "./load-env.js";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
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(
|
|
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
|
-
|
|
43
|
-
|
|
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,
|
|
49
|
+
const resourceCatalog = registerAllResources(server, registry);
|
|
46
50
|
// R2-07: replace the SDK's default ListResourcesRequestSchema handler with a
|
|
47
51
|
// cursor-aware override that paginates the combined static + template list.
|
|
48
52
|
installPaginatedListHandler(server, resourceCatalog, logger);
|
|
49
53
|
// R2-05: install subscribe/unsubscribe handlers and bridge HTTP-layer mutation
|
|
50
54
|
// events to notifications/resources/updated for subscribers of tm1://server/state.
|
|
51
|
-
const subscriptions = new SubscriptionRegistry(server, logger);
|
|
55
|
+
const subscriptions = new SubscriptionRegistry(server, logger, stateResourceUris(registry));
|
|
52
56
|
subscriptions.install();
|
|
53
57
|
// Register MCP Prompts (parameterised workflow templates surfaced as slash-commands).
|
|
54
58
|
registerAllPrompts(server);
|
|
@@ -61,24 +65,33 @@ function buildMcpServer(tm1Client, config, logger) {
|
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
async function main() {
|
|
64
|
-
const
|
|
65
|
-
const logger = createLogger(
|
|
68
|
+
const settings = loadServerSettings();
|
|
69
|
+
const logger = createLogger(settings);
|
|
66
70
|
logger.info("Starting TM1 MCP Server");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
// One registry for every configured connection (see ./connections.ts).
|
|
72
|
+
// Clients are built lazily on first use, so startup costs no TM1 logins.
|
|
73
|
+
const registry = ConnectionRegistry.fromEnvironment(process.env, logger);
|
|
74
|
+
for (const c of registry.status()) {
|
|
75
|
+
if (c.configError) {
|
|
76
|
+
logger.warn({ connection: c.name, err: c.configError }, "connection unusable");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
logger.info({
|
|
80
|
+
connection: c.name,
|
|
81
|
+
mode: c.mode,
|
|
82
|
+
environment: c.environment,
|
|
83
|
+
version: c.tm1Version,
|
|
84
|
+
}, "connection configured");
|
|
85
|
+
if (c.modeReason) {
|
|
86
|
+
logger.warn({ connection: c.name }, `${c.modeReason} Write and destructive tools are refused on this connection.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
// A single connection keeps the old behavior of logging in at startup, in
|
|
91
|
+
// the background: request() re-authenticates on every call anyway, so a
|
|
92
|
+
// failed login here only delays the error to the first tool call.
|
|
93
|
+
if (registry.isSingle) {
|
|
94
|
+
void registry.get(undefined);
|
|
82
95
|
}
|
|
83
96
|
// Branch on transport. stdio is the default for local MCP-client setups
|
|
84
97
|
// (Claude Code, Claude Desktop) and gets one long-lived server. http
|
|
@@ -89,14 +102,14 @@ async function main() {
|
|
|
89
102
|
// runs in shutdown(). http builds one per request and disposes each in the
|
|
90
103
|
// transport's own teardown — nothing process-scoped is left for us here.
|
|
91
104
|
let stdioDispose;
|
|
92
|
-
const httpCloser =
|
|
93
|
-
? await startHttpTransport(() => buildMcpServer(
|
|
105
|
+
const httpCloser = settings.transport === "http"
|
|
106
|
+
? await startHttpTransport(() => buildMcpServer(registry, settings, logger), settings, logger)
|
|
94
107
|
: await (async () => {
|
|
95
|
-
const built = buildMcpServer(
|
|
108
|
+
const built = buildMcpServer(registry, settings, logger);
|
|
96
109
|
stdioDispose = built.dispose;
|
|
97
110
|
return startStdioTransport(built.server, logger);
|
|
98
111
|
})();
|
|
99
|
-
logger.info(`MCP server configured (
|
|
112
|
+
logger.info(`MCP server configured (connections: ${registry.names.join(", ")}, transport: ${settings.transport})`);
|
|
100
113
|
// Graceful shutdown — ensure TM1 session is always cleaned up.
|
|
101
114
|
let shuttingDown = false;
|
|
102
115
|
const shutdown = async (signal, exitCode = 0) => {
|
|
@@ -116,12 +129,7 @@ async function main() {
|
|
|
116
129
|
catch (err) {
|
|
117
130
|
logger.error({ err }, "Error closing transport");
|
|
118
131
|
}
|
|
119
|
-
|
|
120
|
-
await tm1Client.disconnect();
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
logger.error({ err }, "Error during disconnect");
|
|
124
|
-
}
|
|
132
|
+
await registry.disconnectAll();
|
|
125
133
|
process.exit(exitCode);
|
|
126
134
|
};
|
|
127
135
|
process.on("SIGINT", () => {
|
|
@@ -136,7 +144,7 @@ async function main() {
|
|
|
136
144
|
// Parent process death — only meaningful on stdio (Claude spawns us as a
|
|
137
145
|
// child). For http we ignore stdin events since the process lifecycle is
|
|
138
146
|
// independent.
|
|
139
|
-
if (
|
|
147
|
+
if (settings.transport === "stdio") {
|
|
140
148
|
process.stdin.on("end", () => {
|
|
141
149
|
logger.info("stdin ended (parent process gone), shutting down");
|
|
142
150
|
void shutdown("stdin-end");
|
|
@@ -26,7 +26,10 @@ export declare function getCallgraphCacheStats(): Array<{
|
|
|
26
26
|
}>;
|
|
27
27
|
/**
|
|
28
28
|
* Build a full ReferenceIndex from a connected TM1 server.
|
|
29
|
-
* TTL-cached (60 s) per `includeControl` flag
|
|
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
|
|
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 =
|
|
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
|
|
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
|
package/dist/lib/tm1-events.d.ts
CHANGED
|
@@ -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;
|
package/dist/prompts/index.js
CHANGED
|
@@ -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 \`
|
|
76
|
-
`3. **Source code**: call \`
|
|
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
|
|
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,
|
|
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
|