@ffschrattenecker/tm1-mcp-server 6.1.1 → 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 +35 -1
- package/README.md +23 -5
- package/dist/config.d.ts +9 -0
- package/dist/config.js +89 -64
- 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 -40
- 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 +1 -1
- package/dist/tm1-client.js +2 -11
- package/dist/tools/analysis/audit-naming.js +1 -1
- package/dist/tools/analysis/invalidate-callgraph-cache.js +2 -1
- package/dist/tools/analysis/trace-data-flow.js +1 -1
- package/dist/tools/confirm.js +1 -1
- package/dist/tools/define-tool.d.ts +35 -11
- package/dist/tools/define-tool.js +51 -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/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-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/ti-development/execute-process.js +2 -2
- package/dist/tools/ti-development/import-pro-file.js +1 -1
- package/dist/tools/ti-development/search-code.js +1 -1
- package/dist/tools/ti-development/upsert-process.js +2 -2
- package/dist/tools/with-annotations.d.ts +10 -0
- package/dist/tools/with-annotations.js +58 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -3
- 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/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,27 +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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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 =
|
|
96
|
-
? await startHttpTransport(() => buildMcpServer(
|
|
105
|
+
const httpCloser = settings.transport === "http"
|
|
106
|
+
? await startHttpTransport(() => buildMcpServer(registry, settings, logger), settings, logger)
|
|
97
107
|
: await (async () => {
|
|
98
|
-
const built = buildMcpServer(
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
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. */
|