tdoms-mcp 0.6.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.
@@ -0,0 +1,126 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { dataDir, normalizeBaseUrl, redactConnection } from "./config.js";
5
+ import { SecretStore } from "./secret-store.js";
6
+ import { atomicWriteFile, withFileLock } from "./storage-utils.js";
7
+
8
+ const CONNECTIONS_FILE = "connections.json";
9
+
10
+ export class ConnectionStore {
11
+ constructor(dir = dataDir(), { env = process.env } = {}) {
12
+ this.dir = dir;
13
+ this.filePath = path.join(dir, CONNECTIONS_FILE);
14
+ this.secrets = new SecretStore(dir, { env });
15
+ }
16
+
17
+ async listConnections() {
18
+ const state = await this.#readState();
19
+ return state.connections.map(redactConnection);
20
+ }
21
+
22
+ async getConnection(id, { includeSecrets = false } = {}) {
23
+ const state = await this.#readState();
24
+ const connection = state.connections.find((item) => item.id === id);
25
+ if (!connection) {
26
+ return undefined;
27
+ }
28
+
29
+ if (!includeSecrets) {
30
+ return redactConnection(connection);
31
+ }
32
+
33
+ return {
34
+ ...connection,
35
+ password: await this.secrets.getSecret(`connection:${id}:password`),
36
+ token: await this.secrets.getSecret(`connection:${id}:token`)
37
+ };
38
+ }
39
+
40
+ async upsertConnection(input) {
41
+ const id = input.id || crypto.randomUUID();
42
+ let connection;
43
+ await withFileLock(this.filePath, async () => {
44
+ const state = await this.#readState();
45
+ const now = new Date().toISOString();
46
+ const existing = state.connections.find((item) => item.id === id);
47
+ connection = {
48
+ id,
49
+ name: String(input.name || input.host || "TD/OMS").trim(),
50
+ protocol: input.protocol || "https",
51
+ host: String(input.host || "").trim(),
52
+ port: String(input.port || "45011").trim(),
53
+ library: String(input.library || "OMS").trim(),
54
+ username: String(input.username || "").trim(),
55
+ verifyTls: input.verifyTls !== false,
56
+ caFile: String(input.caFile || "").trim() || undefined,
57
+ createdAt: existing?.createdAt || now,
58
+ updatedAt: now,
59
+ lastLoginAt: existing?.lastLoginAt,
60
+ lastSystemProbeAt: existing?.lastSystemProbeAt,
61
+ lastStatus: existing?.lastStatus || "never-tested"
62
+ };
63
+ connection.baseUrl = normalizeBaseUrl(connection);
64
+
65
+ const index = state.connections.findIndex((item) => item.id === id);
66
+ if (index >= 0) state.connections[index] = connection;
67
+ else state.connections.push(connection);
68
+ await this.#writeState(state);
69
+ });
70
+
71
+ if (input.password) {
72
+ await this.secrets.setSecret(`connection:${id}:password`, input.password);
73
+ }
74
+
75
+ return redactConnection(connection);
76
+ }
77
+
78
+ async updateStatus(id, patch) {
79
+ return withFileLock(this.filePath, async () => {
80
+ const state = await this.#readState();
81
+ const index = state.connections.findIndex((item) => item.id === id);
82
+ if (index < 0) throw new Error(`Connection not found: ${id}`);
83
+ state.connections[index] = {
84
+ ...state.connections[index],
85
+ ...patch,
86
+ updatedAt: new Date().toISOString()
87
+ };
88
+ await this.#writeState(state);
89
+ return redactConnection(state.connections[index]);
90
+ });
91
+ }
92
+
93
+ async saveToken(id, token) {
94
+ await this.secrets.setSecret(`connection:${id}:token`, token);
95
+ await this.updateStatus(id, {
96
+ lastStatus: "logged-in",
97
+ lastLoginAt: new Date().toISOString()
98
+ });
99
+ }
100
+
101
+ async deleteConnection(id) {
102
+ await withFileLock(this.filePath, async () => {
103
+ const state = await this.#readState();
104
+ state.connections = state.connections.filter((item) => item.id !== id);
105
+ await this.#writeState(state);
106
+ });
107
+ await this.secrets.deleteSecret(`connection:${id}:password`);
108
+ await this.secrets.deleteSecret(`connection:${id}:token`);
109
+ }
110
+
111
+ async #readState() {
112
+ try {
113
+ const parsed = JSON.parse(await fs.readFile(this.filePath, "utf8"));
114
+ return { connections: Array.isArray(parsed.connections) ? parsed.connections : [] };
115
+ } catch (error) {
116
+ if (error.code === "ENOENT") {
117
+ return { connections: [] };
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ async #writeState(state) {
124
+ await atomicWriteFile(this.filePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
125
+ }
126
+ }
package/src/index.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { DEFAULT_HOST, DEFAULT_PORT, dataDir } from "./config.js";
3
+ import { ConnectionStore } from "./connection-store.js";
4
+ import { ClientManager } from "./client-manager.js";
5
+ import { startMcpServer } from "./mcp-server.js";
6
+ import { startWebServer } from "./web-app.js";
7
+ import { hardenDataDirectory } from "./storage-utils.js";
8
+
9
+ const command = process.argv[2] || "mcp";
10
+ const profileFlagIndex = process.argv.indexOf("--profile");
11
+ const profile = profileFlagIndex >= 0 ? process.argv[profileFlagIndex + 1] : process.env.TDOMS_MCP_PROFILE;
12
+
13
+ async function main() {
14
+ const store = new ConnectionStore();
15
+ const clientManager = new ClientManager();
16
+
17
+ if (command === "ui") {
18
+ const { host, port } = await startWebServer({ store, clientManager });
19
+ console.log(`TD/OMS MCP UI: http://${host}:${port}`);
20
+ console.log(`Data directory: ${dataDir()}`);
21
+ return;
22
+ }
23
+
24
+ if (command === "dev") {
25
+ const { host, port } = await startWebServer({ store, clientManager });
26
+ console.error(`TD/OMS MCP UI: http://${host}:${port}`);
27
+ await startMcpServer({ store, profile });
28
+ return;
29
+ }
30
+
31
+ if (command === "smoke") {
32
+ console.log(JSON.stringify({
33
+ ok: true,
34
+ dataDir: dataDir(),
35
+ connections: await store.listConnections()
36
+ }, null, 2));
37
+ return;
38
+ }
39
+
40
+ if (command === "secure-storage") {
41
+ const security = await hardenDataDirectory(dataDir());
42
+ console.log(JSON.stringify({ ok: true, dataDir: dataDir(), security }, null, 2));
43
+ return;
44
+ }
45
+
46
+ if (command === "clients") {
47
+ console.log(JSON.stringify({ clients: await clientManager.listClients() }, null, 2));
48
+ return;
49
+ }
50
+
51
+ if (command === "install-client") {
52
+ const clientId = process.argv[3];
53
+ if (!clientId) throw new Error("Client id is required. Run `tdoms-mcp clients` to list detected clients.");
54
+ const result = await clientManager.installClient(clientId, { confirm: process.argv.includes("--confirm") });
55
+ console.log(JSON.stringify(result, null, 2));
56
+ return;
57
+ }
58
+
59
+ if (command === "install-detected") {
60
+ const result = await clientManager.installDetected({ confirm: process.argv.includes("--confirm") });
61
+ console.log(JSON.stringify(result, null, 2));
62
+ if (!result.ok) process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ if (command === "mcp") {
67
+ await startMcpServer({ store, profile });
68
+ return;
69
+ }
70
+
71
+ console.error(`Unknown command: ${command}`);
72
+ console.error("Expected one of: mcp, ui, dev, smoke, secure-storage, clients, install-client, install-detected");
73
+ process.exit(1);
74
+ }
75
+
76
+ main().catch((error) => {
77
+ console.error(error);
78
+ process.exit(1);
79
+ });