llng-mcp 0.1.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.
Files changed (114) hide show
  1. package/.github/workflows/ci.yml +77 -0
  2. package/.prettierrc +7 -0
  3. package/LICENSE +661 -0
  4. package/README.md +502 -0
  5. package/dist/__tests__/api-transport.test.d.ts +1 -0
  6. package/dist/__tests__/api-transport.test.js +577 -0
  7. package/dist/__tests__/api-transport.test.js.map +1 -0
  8. package/dist/__tests__/config.test.d.ts +1 -0
  9. package/dist/__tests__/config.test.js +472 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/integration/api-mode.test.d.ts +1 -0
  12. package/dist/__tests__/integration/api-mode.test.js +199 -0
  13. package/dist/__tests__/integration/api-mode.test.js.map +1 -0
  14. package/dist/__tests__/integration/oidc-rp.test.d.ts +1 -0
  15. package/dist/__tests__/integration/oidc-rp.test.js +120 -0
  16. package/dist/__tests__/integration/oidc-rp.test.js.map +1 -0
  17. package/dist/__tests__/integration/ssh-mode.test.d.ts +1 -0
  18. package/dist/__tests__/integration/ssh-mode.test.js +101 -0
  19. package/dist/__tests__/integration/ssh-mode.test.js.map +1 -0
  20. package/dist/__tests__/k8s-transport.test.d.ts +1 -0
  21. package/dist/__tests__/k8s-transport.test.js +254 -0
  22. package/dist/__tests__/k8s-transport.test.js.map +1 -0
  23. package/dist/__tests__/oidc-tools.test.d.ts +1 -0
  24. package/dist/__tests__/oidc-tools.test.js +457 -0
  25. package/dist/__tests__/oidc-tools.test.js.map +1 -0
  26. package/dist/__tests__/registry.test.d.ts +1 -0
  27. package/dist/__tests__/registry.test.js +96 -0
  28. package/dist/__tests__/registry.test.js.map +1 -0
  29. package/dist/__tests__/ssh-transport.test.d.ts +1 -0
  30. package/dist/__tests__/ssh-transport.test.js +618 -0
  31. package/dist/__tests__/ssh-transport.test.js.map +1 -0
  32. package/dist/__tests__/tools.test.d.ts +1 -0
  33. package/dist/__tests__/tools.test.js +525 -0
  34. package/dist/__tests__/tools.test.js.map +1 -0
  35. package/dist/config.d.ts +65 -0
  36. package/dist/config.js +506 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +42 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/resources/documentation.d.ts +5 -0
  42. package/dist/resources/documentation.js +56 -0
  43. package/dist/resources/documentation.js.map +1 -0
  44. package/dist/tools/cli-utilities.d.ts +3 -0
  45. package/dist/tools/cli-utilities.js +187 -0
  46. package/dist/tools/cli-utilities.js.map +1 -0
  47. package/dist/tools/config.d.ts +6 -0
  48. package/dist/tools/config.js +326 -0
  49. package/dist/tools/config.js.map +1 -0
  50. package/dist/tools/consents.d.ts +3 -0
  51. package/dist/tools/consents.js +39 -0
  52. package/dist/tools/consents.js.map +1 -0
  53. package/dist/tools/instances.d.ts +3 -0
  54. package/dist/tools/instances.js +14 -0
  55. package/dist/tools/instances.js.map +1 -0
  56. package/dist/tools/oidc-rp.d.ts +6 -0
  57. package/dist/tools/oidc-rp.js +246 -0
  58. package/dist/tools/oidc-rp.js.map +1 -0
  59. package/dist/tools/oidc.d.ts +3 -0
  60. package/dist/tools/oidc.js +343 -0
  61. package/dist/tools/oidc.js.map +1 -0
  62. package/dist/tools/secondfactors.d.ts +3 -0
  63. package/dist/tools/secondfactors.js +62 -0
  64. package/dist/tools/secondfactors.js.map +1 -0
  65. package/dist/tools/sessions.d.ts +6 -0
  66. package/dist/tools/sessions.js +300 -0
  67. package/dist/tools/sessions.js.map +1 -0
  68. package/dist/transport/api.d.ts +35 -0
  69. package/dist/transport/api.js +327 -0
  70. package/dist/transport/api.js.map +1 -0
  71. package/dist/transport/interface.d.ts +50 -0
  72. package/dist/transport/interface.js +2 -0
  73. package/dist/transport/interface.js.map +1 -0
  74. package/dist/transport/k8s.d.ts +41 -0
  75. package/dist/transport/k8s.js +303 -0
  76. package/dist/transport/k8s.js.map +1 -0
  77. package/dist/transport/registry.d.ts +20 -0
  78. package/dist/transport/registry.js +91 -0
  79. package/dist/transport/registry.js.map +1 -0
  80. package/dist/transport/ssh.d.ts +37 -0
  81. package/dist/transport/ssh.js +353 -0
  82. package/dist/transport/ssh.js.map +1 -0
  83. package/docker-compose.test.yml +16 -0
  84. package/eslint.config.js +21 -0
  85. package/package.json +38 -0
  86. package/src/__tests__/api-transport.test.ts +746 -0
  87. package/src/__tests__/config.test.ts +587 -0
  88. package/src/__tests__/integration/api-mode.test.ts +229 -0
  89. package/src/__tests__/integration/oidc-rp.test.ts +138 -0
  90. package/src/__tests__/integration/ssh-mode.test.ts +113 -0
  91. package/src/__tests__/k8s-transport.test.ts +342 -0
  92. package/src/__tests__/oidc-tools.test.ts +554 -0
  93. package/src/__tests__/registry.test.ts +110 -0
  94. package/src/__tests__/ssh-transport.test.ts +805 -0
  95. package/src/__tests__/tools.test.ts +735 -0
  96. package/src/config.ts +605 -0
  97. package/src/index.ts +48 -0
  98. package/src/resources/documentation.ts +65 -0
  99. package/src/tools/cli-utilities.ts +207 -0
  100. package/src/tools/config.ts +382 -0
  101. package/src/tools/consents.ts +50 -0
  102. package/src/tools/instances.ts +21 -0
  103. package/src/tools/oidc-rp.ts +299 -0
  104. package/src/tools/oidc.ts +434 -0
  105. package/src/tools/secondfactors.ts +78 -0
  106. package/src/tools/sessions.ts +342 -0
  107. package/src/transport/api.ts +429 -0
  108. package/src/transport/interface.ts +58 -0
  109. package/src/transport/k8s.ts +367 -0
  110. package/src/transport/registry.ts +105 -0
  111. package/src/transport/ssh.ts +430 -0
  112. package/tsconfig.json +16 -0
  113. package/vitest.config.ts +8 -0
  114. package/vitest.integration.config.ts +9 -0
package/src/config.ts ADDED
@@ -0,0 +1,605 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const DEFAULT_BIN_PREFIX = "/usr/share/lemonldap-ng/bin";
6
+ const VALID_MODES = ["ssh", "api", "k8s"] as const;
7
+ type LlngMode = (typeof VALID_MODES)[number];
8
+
9
+ function parseMode(value: string): LlngMode {
10
+ if (VALID_MODES.includes(value as LlngMode)) {
11
+ return value as LlngMode;
12
+ }
13
+ throw new Error(`Invalid LLNG mode '${value}'. Valid modes: ${VALID_MODES.join(", ")}`);
14
+ }
15
+
16
+ export interface SshConfig {
17
+ host?: string;
18
+ user?: string;
19
+ port?: number;
20
+ sudo?: string;
21
+ remoteCommand?: string;
22
+ binPrefix?: string;
23
+ cliPath?: string;
24
+ sessionsPath?: string;
25
+ configEditorPath?: string;
26
+ deleteSessionPath?: string;
27
+ }
28
+
29
+ export interface ApiConfig {
30
+ baseUrl: string;
31
+ basicAuth?: { username: string; password: string };
32
+ verifySsl?: boolean;
33
+ }
34
+
35
+ export interface OidcConfig {
36
+ issuer: string;
37
+ clientId: string;
38
+ clientSecret?: string;
39
+ redirectUri: string;
40
+ scope: string;
41
+ }
42
+
43
+ export interface K8sConfig {
44
+ context?: string;
45
+ namespace?: string;
46
+ deployment?: string;
47
+ container?: string;
48
+ podSelector?: string;
49
+ binPrefix?: string;
50
+ }
51
+
52
+ export interface ManagerOverride {
53
+ mode?: LlngMode;
54
+ ssh?: Partial<SshConfig>;
55
+ api?: ApiConfig;
56
+ k8s?: K8sConfig;
57
+ }
58
+
59
+ export interface LlngConfig {
60
+ mode: LlngMode;
61
+ ssh?: SshConfig;
62
+ api?: ApiConfig;
63
+ k8s?: K8sConfig;
64
+ oidc?: OidcConfig;
65
+ manager?: ManagerOverride;
66
+ }
67
+
68
+ export interface ResolvedPaths {
69
+ cliPath: string;
70
+ sessionsPath: string;
71
+ configEditorPath: string;
72
+ }
73
+
74
+ export function resolvePaths(
75
+ binPrefix?: string,
76
+ cliPath?: string,
77
+ sessionsPath?: string,
78
+ configEditorPath?: string,
79
+ ): ResolvedPaths {
80
+ const prefix = binPrefix || DEFAULT_BIN_PREFIX;
81
+ return {
82
+ cliPath: cliPath || `${prefix}/lemonldap-ng-cli`,
83
+ sessionsPath: sessionsPath || `${prefix}/lemonldap-ng-sessions`,
84
+ configEditorPath: configEditorPath || `${prefix}/lmConfigEditor`,
85
+ };
86
+ }
87
+
88
+ export function loadConfig(): LlngConfig {
89
+ // Start with defaults
90
+ const config: LlngConfig = {
91
+ mode: "ssh",
92
+ ssh: {},
93
+ };
94
+
95
+ // Try to load config file from ~/.llng-mcp.json
96
+ const configPath = join(homedir(), ".llng-mcp.json");
97
+ try {
98
+ // Check file permissions - warn if too open
99
+ try {
100
+ const stats = statSync(configPath);
101
+ const mode = stats.mode & 0o777;
102
+ if (mode & 0o077) {
103
+ console.error(
104
+ `WARNING: ${configPath} has permissions ${mode.toString(8)}. It may contain credentials and should be restricted to owner only (chmod 600).`,
105
+ );
106
+ }
107
+ } catch {
108
+ // stat failed, file may not exist - continue
109
+ }
110
+ const fileContent = readFileSync(configPath, "utf-8");
111
+ const fileConfig = JSON.parse(fileContent);
112
+
113
+ // Merge file config with defaults
114
+ if (fileConfig.mode) config.mode = parseMode(fileConfig.mode);
115
+ if (fileConfig.ssh) {
116
+ config.ssh = { ...config.ssh, ...fileConfig.ssh };
117
+ }
118
+ if (fileConfig.api) {
119
+ config.api = fileConfig.api;
120
+ }
121
+ if (fileConfig.k8s) {
122
+ config.k8s = fileConfig.k8s;
123
+ }
124
+ if (fileConfig.oidc) {
125
+ config.oidc = fileConfig.oidc;
126
+ }
127
+ } catch {
128
+ // File doesn't exist or invalid JSON - continue with defaults
129
+ }
130
+
131
+ // Overlay environment variables
132
+ if (process.env.LLNG_MODE) {
133
+ config.mode = parseMode(process.env.LLNG_MODE);
134
+ }
135
+
136
+ // SSH config - helper to ensure ssh config exists
137
+ const ensureSshConfig = () => {
138
+ if (!config.ssh) {
139
+ config.ssh = {};
140
+ }
141
+ return config.ssh;
142
+ };
143
+
144
+ if (process.env.LLNG_SSH_HOST) {
145
+ ensureSshConfig().host = process.env.LLNG_SSH_HOST;
146
+ }
147
+ if (process.env.LLNG_SSH_USER) {
148
+ ensureSshConfig().user = process.env.LLNG_SSH_USER;
149
+ }
150
+ if (process.env.LLNG_SSH_PORT) {
151
+ ensureSshConfig().port = parseInt(process.env.LLNG_SSH_PORT, 10);
152
+ }
153
+ if (process.env.LLNG_SSH_SUDO) {
154
+ ensureSshConfig().sudo = process.env.LLNG_SSH_SUDO;
155
+ }
156
+ if (process.env.LLNG_SSH_REMOTE_COMMAND) {
157
+ ensureSshConfig().remoteCommand = process.env.LLNG_SSH_REMOTE_COMMAND;
158
+ }
159
+ if (process.env.LLNG_SSH_BIN_PREFIX) {
160
+ ensureSshConfig().binPrefix = process.env.LLNG_SSH_BIN_PREFIX;
161
+ }
162
+ if (process.env.LLNG_SSH_CLI_PATH) {
163
+ ensureSshConfig().cliPath = process.env.LLNG_SSH_CLI_PATH;
164
+ }
165
+ if (process.env.LLNG_SSH_SESSIONS_PATH) {
166
+ ensureSshConfig().sessionsPath = process.env.LLNG_SSH_SESSIONS_PATH;
167
+ }
168
+ if (process.env.LLNG_SSH_CONFIG_EDITOR_PATH) {
169
+ ensureSshConfig().configEditorPath = process.env.LLNG_SSH_CONFIG_EDITOR_PATH;
170
+ }
171
+ if (process.env.LLNG_SSH_DELETE_SESSION_PATH) {
172
+ ensureSshConfig().deleteSessionPath = process.env.LLNG_SSH_DELETE_SESSION_PATH;
173
+ }
174
+
175
+ // API config
176
+ if (process.env.LLNG_API_URL) {
177
+ config.api = config.api || { baseUrl: process.env.LLNG_API_URL };
178
+ config.api.baseUrl = process.env.LLNG_API_URL;
179
+ }
180
+ if (process.env.LLNG_API_BASIC_USER || process.env.LLNG_API_BASIC_PASSWORD) {
181
+ config.api = config.api || { baseUrl: "" };
182
+ config.api.basicAuth = {
183
+ username: process.env.LLNG_API_BASIC_USER || "",
184
+ password: process.env.LLNG_API_BASIC_PASSWORD || "",
185
+ };
186
+ }
187
+ if (process.env.LLNG_API_VERIFY_SSL) {
188
+ config.api = config.api || { baseUrl: "" };
189
+ config.api.verifySsl = process.env.LLNG_API_VERIFY_SSL !== "false";
190
+ }
191
+
192
+ // K8s config
193
+ const ensureK8sConfig = () => {
194
+ if (!config.k8s) {
195
+ config.k8s = {};
196
+ }
197
+ return config.k8s;
198
+ };
199
+
200
+ if (process.env.LLNG_K8S_CONTEXT) {
201
+ ensureK8sConfig().context = process.env.LLNG_K8S_CONTEXT;
202
+ }
203
+ if (process.env.LLNG_K8S_NAMESPACE) {
204
+ ensureK8sConfig().namespace = process.env.LLNG_K8S_NAMESPACE;
205
+ }
206
+ if (process.env.LLNG_K8S_DEPLOYMENT) {
207
+ ensureK8sConfig().deployment = process.env.LLNG_K8S_DEPLOYMENT;
208
+ }
209
+ if (process.env.LLNG_K8S_CONTAINER) {
210
+ ensureK8sConfig().container = process.env.LLNG_K8S_CONTAINER;
211
+ }
212
+ if (process.env.LLNG_K8S_POD_SELECTOR) {
213
+ ensureK8sConfig().podSelector = process.env.LLNG_K8S_POD_SELECTOR;
214
+ }
215
+ if (process.env.LLNG_K8S_BIN_PREFIX) {
216
+ ensureK8sConfig().binPrefix = process.env.LLNG_K8S_BIN_PREFIX;
217
+ }
218
+
219
+ // OIDC config
220
+ if (process.env.LLNG_OIDC_ISSUER) {
221
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
222
+ config.oidc.issuer = process.env.LLNG_OIDC_ISSUER;
223
+ }
224
+ if (process.env.LLNG_OIDC_CLIENT_ID) {
225
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
226
+ config.oidc.clientId = process.env.LLNG_OIDC_CLIENT_ID;
227
+ }
228
+ if (process.env.LLNG_OIDC_CLIENT_SECRET) {
229
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
230
+ config.oidc.clientSecret = process.env.LLNG_OIDC_CLIENT_SECRET;
231
+ }
232
+ if (process.env.LLNG_OIDC_REDIRECT_URI) {
233
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
234
+ config.oidc.redirectUri = process.env.LLNG_OIDC_REDIRECT_URI;
235
+ }
236
+ if (process.env.LLNG_OIDC_SCOPE) {
237
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
238
+ config.oidc.scope = process.env.LLNG_OIDC_SCOPE;
239
+ }
240
+
241
+ // Manager override config
242
+ const ensureManager = () => {
243
+ if (!config.manager) {
244
+ config.manager = {};
245
+ }
246
+ return config.manager;
247
+ };
248
+
249
+ const ensureManagerSsh = () => {
250
+ const manager = ensureManager();
251
+ if (!manager.ssh) {
252
+ manager.ssh = {};
253
+ }
254
+ return manager.ssh;
255
+ };
256
+
257
+ const ensureManagerApi = () => {
258
+ const manager = ensureManager();
259
+ if (!manager.api) {
260
+ manager.api = { baseUrl: "" };
261
+ }
262
+ return manager.api;
263
+ };
264
+
265
+ const ensureManagerK8s = () => {
266
+ const manager = ensureManager();
267
+ if (!manager.k8s) {
268
+ manager.k8s = {};
269
+ }
270
+ return manager.k8s;
271
+ };
272
+
273
+ if (process.env.LLNG_MANAGER_MODE) {
274
+ ensureManager().mode = parseMode(process.env.LLNG_MANAGER_MODE);
275
+ }
276
+
277
+ if (process.env.LLNG_MANAGER_SSH_HOST) {
278
+ ensureManagerSsh().host = process.env.LLNG_MANAGER_SSH_HOST;
279
+ }
280
+ if (process.env.LLNG_MANAGER_SSH_USER) {
281
+ ensureManagerSsh().user = process.env.LLNG_MANAGER_SSH_USER;
282
+ }
283
+ if (process.env.LLNG_MANAGER_SSH_PORT) {
284
+ ensureManagerSsh().port = parseInt(process.env.LLNG_MANAGER_SSH_PORT, 10);
285
+ }
286
+ if (process.env.LLNG_MANAGER_SSH_SUDO) {
287
+ ensureManagerSsh().sudo = process.env.LLNG_MANAGER_SSH_SUDO;
288
+ }
289
+ if (process.env.LLNG_MANAGER_SSH_REMOTE_COMMAND) {
290
+ ensureManagerSsh().remoteCommand = process.env.LLNG_MANAGER_SSH_REMOTE_COMMAND;
291
+ }
292
+ if (process.env.LLNG_MANAGER_SSH_BIN_PREFIX) {
293
+ ensureManagerSsh().binPrefix = process.env.LLNG_MANAGER_SSH_BIN_PREFIX;
294
+ }
295
+ if (process.env.LLNG_MANAGER_SSH_CLI_PATH) {
296
+ ensureManagerSsh().cliPath = process.env.LLNG_MANAGER_SSH_CLI_PATH;
297
+ }
298
+ if (process.env.LLNG_MANAGER_SSH_SESSIONS_PATH) {
299
+ ensureManagerSsh().sessionsPath = process.env.LLNG_MANAGER_SSH_SESSIONS_PATH;
300
+ }
301
+ if (process.env.LLNG_MANAGER_SSH_CONFIG_EDITOR_PATH) {
302
+ ensureManagerSsh().configEditorPath = process.env.LLNG_MANAGER_SSH_CONFIG_EDITOR_PATH;
303
+ }
304
+ if (process.env.LLNG_MANAGER_SSH_DELETE_SESSION_PATH) {
305
+ ensureManagerSsh().deleteSessionPath = process.env.LLNG_MANAGER_SSH_DELETE_SESSION_PATH;
306
+ }
307
+
308
+ if (process.env.LLNG_MANAGER_API_URL) {
309
+ ensureManagerApi().baseUrl = process.env.LLNG_MANAGER_API_URL;
310
+ }
311
+ if (process.env.LLNG_MANAGER_API_BASIC_USER || process.env.LLNG_MANAGER_API_BASIC_PASSWORD) {
312
+ ensureManagerApi().basicAuth = {
313
+ username: process.env.LLNG_MANAGER_API_BASIC_USER || "",
314
+ password: process.env.LLNG_MANAGER_API_BASIC_PASSWORD || "",
315
+ };
316
+ }
317
+ if (process.env.LLNG_MANAGER_API_VERIFY_SSL) {
318
+ ensureManagerApi().verifySsl = process.env.LLNG_MANAGER_API_VERIFY_SSL !== "false";
319
+ }
320
+
321
+ if (process.env.LLNG_MANAGER_K8S_CONTEXT) {
322
+ ensureManagerK8s().context = process.env.LLNG_MANAGER_K8S_CONTEXT;
323
+ }
324
+ if (process.env.LLNG_MANAGER_K8S_NAMESPACE) {
325
+ ensureManagerK8s().namespace = process.env.LLNG_MANAGER_K8S_NAMESPACE;
326
+ }
327
+ if (process.env.LLNG_MANAGER_K8S_DEPLOYMENT) {
328
+ ensureManagerK8s().deployment = process.env.LLNG_MANAGER_K8S_DEPLOYMENT;
329
+ }
330
+ if (process.env.LLNG_MANAGER_K8S_CONTAINER) {
331
+ ensureManagerK8s().container = process.env.LLNG_MANAGER_K8S_CONTAINER;
332
+ }
333
+ if (process.env.LLNG_MANAGER_K8S_POD_SELECTOR) {
334
+ ensureManagerK8s().podSelector = process.env.LLNG_MANAGER_K8S_POD_SELECTOR;
335
+ }
336
+ if (process.env.LLNG_MANAGER_K8S_BIN_PREFIX) {
337
+ ensureManagerK8s().binPrefix = process.env.LLNG_MANAGER_K8S_BIN_PREFIX;
338
+ }
339
+
340
+ return config;
341
+ }
342
+
343
+ // Multi-instance support
344
+ export type LlngInstanceConfig = LlngConfig;
345
+
346
+ export interface LlngMultiConfig {
347
+ instances: Record<string, LlngInstanceConfig>;
348
+ default: string;
349
+ }
350
+
351
+ function applyInstanceDefaults(partial: Partial<LlngConfig>): LlngConfig {
352
+ const config: LlngConfig = {
353
+ mode: partial.mode ? parseMode(partial.mode) : "ssh",
354
+ };
355
+ if (partial.ssh) config.ssh = { ...partial.ssh };
356
+ if (partial.api) config.api = partial.api;
357
+ if (partial.k8s) config.k8s = partial.k8s;
358
+ if (partial.oidc) config.oidc = partial.oidc;
359
+ if (partial.manager) config.manager = partial.manager;
360
+
361
+ // For ssh mode without explicit ssh config, provide empty object
362
+ if (config.mode === "ssh" && !config.ssh) {
363
+ config.ssh = {};
364
+ }
365
+ // For k8s mode without explicit k8s config, provide empty object
366
+ if (config.mode === "k8s" && !config.k8s) {
367
+ config.k8s = {};
368
+ }
369
+
370
+ return config;
371
+ }
372
+
373
+ function applyEnvOverrides(config: LlngConfig): void {
374
+ if (process.env.LLNG_MODE) {
375
+ config.mode = parseMode(process.env.LLNG_MODE);
376
+ }
377
+
378
+ const ensureSsh = () => {
379
+ if (!config.ssh) {
380
+ config.ssh = {};
381
+ }
382
+ return config.ssh;
383
+ };
384
+
385
+ if (process.env.LLNG_SSH_HOST) ensureSsh().host = process.env.LLNG_SSH_HOST;
386
+ if (process.env.LLNG_SSH_USER) ensureSsh().user = process.env.LLNG_SSH_USER;
387
+ if (process.env.LLNG_SSH_PORT) ensureSsh().port = parseInt(process.env.LLNG_SSH_PORT, 10);
388
+ if (process.env.LLNG_SSH_SUDO) ensureSsh().sudo = process.env.LLNG_SSH_SUDO;
389
+ if (process.env.LLNG_SSH_REMOTE_COMMAND)
390
+ ensureSsh().remoteCommand = process.env.LLNG_SSH_REMOTE_COMMAND;
391
+ if (process.env.LLNG_SSH_BIN_PREFIX) ensureSsh().binPrefix = process.env.LLNG_SSH_BIN_PREFIX;
392
+ if (process.env.LLNG_SSH_CLI_PATH) ensureSsh().cliPath = process.env.LLNG_SSH_CLI_PATH;
393
+ if (process.env.LLNG_SSH_SESSIONS_PATH)
394
+ ensureSsh().sessionsPath = process.env.LLNG_SSH_SESSIONS_PATH;
395
+ if (process.env.LLNG_SSH_CONFIG_EDITOR_PATH)
396
+ ensureSsh().configEditorPath = process.env.LLNG_SSH_CONFIG_EDITOR_PATH;
397
+ if (process.env.LLNG_SSH_DELETE_SESSION_PATH)
398
+ ensureSsh().deleteSessionPath = process.env.LLNG_SSH_DELETE_SESSION_PATH;
399
+
400
+ if (process.env.LLNG_API_URL) {
401
+ config.api = config.api || { baseUrl: process.env.LLNG_API_URL };
402
+ config.api.baseUrl = process.env.LLNG_API_URL;
403
+ }
404
+ if (process.env.LLNG_API_BASIC_USER || process.env.LLNG_API_BASIC_PASSWORD) {
405
+ config.api = config.api || { baseUrl: "" };
406
+ config.api.basicAuth = {
407
+ username: process.env.LLNG_API_BASIC_USER || "",
408
+ password: process.env.LLNG_API_BASIC_PASSWORD || "",
409
+ };
410
+ }
411
+ if (process.env.LLNG_API_VERIFY_SSL) {
412
+ config.api = config.api || { baseUrl: "" };
413
+ config.api.verifySsl = process.env.LLNG_API_VERIFY_SSL !== "false";
414
+ }
415
+
416
+ // K8s env overrides
417
+ const ensureK8s = () => {
418
+ if (!config.k8s) {
419
+ config.k8s = {};
420
+ }
421
+ return config.k8s;
422
+ };
423
+
424
+ if (process.env.LLNG_K8S_CONTEXT) ensureK8s().context = process.env.LLNG_K8S_CONTEXT;
425
+ if (process.env.LLNG_K8S_NAMESPACE) ensureK8s().namespace = process.env.LLNG_K8S_NAMESPACE;
426
+ if (process.env.LLNG_K8S_DEPLOYMENT) ensureK8s().deployment = process.env.LLNG_K8S_DEPLOYMENT;
427
+ if (process.env.LLNG_K8S_CONTAINER) ensureK8s().container = process.env.LLNG_K8S_CONTAINER;
428
+ if (process.env.LLNG_K8S_POD_SELECTOR)
429
+ ensureK8s().podSelector = process.env.LLNG_K8S_POD_SELECTOR;
430
+ if (process.env.LLNG_K8S_BIN_PREFIX) ensureK8s().binPrefix = process.env.LLNG_K8S_BIN_PREFIX;
431
+
432
+ if (process.env.LLNG_OIDC_ISSUER) {
433
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
434
+ config.oidc.issuer = process.env.LLNG_OIDC_ISSUER;
435
+ }
436
+ if (process.env.LLNG_OIDC_CLIENT_ID) {
437
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
438
+ config.oidc.clientId = process.env.LLNG_OIDC_CLIENT_ID;
439
+ }
440
+ if (process.env.LLNG_OIDC_CLIENT_SECRET) {
441
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
442
+ config.oidc.clientSecret = process.env.LLNG_OIDC_CLIENT_SECRET;
443
+ }
444
+ if (process.env.LLNG_OIDC_REDIRECT_URI) {
445
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
446
+ config.oidc.redirectUri = process.env.LLNG_OIDC_REDIRECT_URI;
447
+ }
448
+ if (process.env.LLNG_OIDC_SCOPE) {
449
+ config.oidc = config.oidc || { issuer: "", clientId: "", redirectUri: "", scope: "" };
450
+ config.oidc.scope = process.env.LLNG_OIDC_SCOPE;
451
+ }
452
+
453
+ // Manager override env overrides
454
+ const ensureManager = () => {
455
+ if (!config.manager) {
456
+ config.manager = {};
457
+ }
458
+ return config.manager;
459
+ };
460
+
461
+ const ensureManagerSsh = () => {
462
+ const manager = ensureManager();
463
+ if (!manager.ssh) {
464
+ manager.ssh = {};
465
+ }
466
+ return manager.ssh;
467
+ };
468
+
469
+ const ensureManagerApi = () => {
470
+ const manager = ensureManager();
471
+ if (!manager.api) {
472
+ manager.api = { baseUrl: "" };
473
+ }
474
+ return manager.api;
475
+ };
476
+
477
+ const ensureManagerK8s = () => {
478
+ const manager = ensureManager();
479
+ if (!manager.k8s) {
480
+ manager.k8s = {};
481
+ }
482
+ return manager.k8s;
483
+ };
484
+
485
+ if (process.env.LLNG_MANAGER_MODE) {
486
+ ensureManager().mode = parseMode(process.env.LLNG_MANAGER_MODE);
487
+ }
488
+
489
+ if (process.env.LLNG_MANAGER_SSH_HOST) {
490
+ ensureManagerSsh().host = process.env.LLNG_MANAGER_SSH_HOST;
491
+ }
492
+ if (process.env.LLNG_MANAGER_SSH_USER) {
493
+ ensureManagerSsh().user = process.env.LLNG_MANAGER_SSH_USER;
494
+ }
495
+ if (process.env.LLNG_MANAGER_SSH_PORT) {
496
+ ensureManagerSsh().port = parseInt(process.env.LLNG_MANAGER_SSH_PORT, 10);
497
+ }
498
+ if (process.env.LLNG_MANAGER_SSH_SUDO) {
499
+ ensureManagerSsh().sudo = process.env.LLNG_MANAGER_SSH_SUDO;
500
+ }
501
+ if (process.env.LLNG_MANAGER_SSH_REMOTE_COMMAND) {
502
+ ensureManagerSsh().remoteCommand = process.env.LLNG_MANAGER_SSH_REMOTE_COMMAND;
503
+ }
504
+ if (process.env.LLNG_MANAGER_SSH_BIN_PREFIX) {
505
+ ensureManagerSsh().binPrefix = process.env.LLNG_MANAGER_SSH_BIN_PREFIX;
506
+ }
507
+ if (process.env.LLNG_MANAGER_SSH_CLI_PATH) {
508
+ ensureManagerSsh().cliPath = process.env.LLNG_MANAGER_SSH_CLI_PATH;
509
+ }
510
+ if (process.env.LLNG_MANAGER_SSH_SESSIONS_PATH) {
511
+ ensureManagerSsh().sessionsPath = process.env.LLNG_MANAGER_SSH_SESSIONS_PATH;
512
+ }
513
+ if (process.env.LLNG_MANAGER_SSH_CONFIG_EDITOR_PATH) {
514
+ ensureManagerSsh().configEditorPath = process.env.LLNG_MANAGER_SSH_CONFIG_EDITOR_PATH;
515
+ }
516
+ if (process.env.LLNG_MANAGER_SSH_DELETE_SESSION_PATH) {
517
+ ensureManagerSsh().deleteSessionPath = process.env.LLNG_MANAGER_SSH_DELETE_SESSION_PATH;
518
+ }
519
+
520
+ if (process.env.LLNG_MANAGER_API_URL) {
521
+ ensureManagerApi().baseUrl = process.env.LLNG_MANAGER_API_URL;
522
+ }
523
+ if (process.env.LLNG_MANAGER_API_BASIC_USER || process.env.LLNG_MANAGER_API_BASIC_PASSWORD) {
524
+ ensureManagerApi().basicAuth = {
525
+ username: process.env.LLNG_MANAGER_API_BASIC_USER || "",
526
+ password: process.env.LLNG_MANAGER_API_BASIC_PASSWORD || "",
527
+ };
528
+ }
529
+ if (process.env.LLNG_MANAGER_API_VERIFY_SSL) {
530
+ ensureManagerApi().verifySsl = process.env.LLNG_MANAGER_API_VERIFY_SSL !== "false";
531
+ }
532
+
533
+ if (process.env.LLNG_MANAGER_K8S_CONTEXT) {
534
+ ensureManagerK8s().context = process.env.LLNG_MANAGER_K8S_CONTEXT;
535
+ }
536
+ if (process.env.LLNG_MANAGER_K8S_NAMESPACE) {
537
+ ensureManagerK8s().namespace = process.env.LLNG_MANAGER_K8S_NAMESPACE;
538
+ }
539
+ if (process.env.LLNG_MANAGER_K8S_DEPLOYMENT) {
540
+ ensureManagerK8s().deployment = process.env.LLNG_MANAGER_K8S_DEPLOYMENT;
541
+ }
542
+ if (process.env.LLNG_MANAGER_K8S_CONTAINER) {
543
+ ensureManagerK8s().container = process.env.LLNG_MANAGER_K8S_CONTAINER;
544
+ }
545
+ if (process.env.LLNG_MANAGER_K8S_POD_SELECTOR) {
546
+ ensureManagerK8s().podSelector = process.env.LLNG_MANAGER_K8S_POD_SELECTOR;
547
+ }
548
+ if (process.env.LLNG_MANAGER_K8S_BIN_PREFIX) {
549
+ ensureManagerK8s().binPrefix = process.env.LLNG_MANAGER_K8S_BIN_PREFIX;
550
+ }
551
+ }
552
+
553
+ export function loadMultiConfig(): LlngMultiConfig {
554
+ const configPath = join(homedir(), ".llng-mcp.json");
555
+ let fileConfig: any = null;
556
+
557
+ // Check file permissions
558
+ try {
559
+ const stats = statSync(configPath);
560
+ const mode = stats.mode & 0o777;
561
+ if (mode & 0o077) {
562
+ console.error(
563
+ `WARNING: ${configPath} has permissions ${mode.toString(8)}. It may contain credentials and should be restricted to owner only (chmod 600).`,
564
+ );
565
+ }
566
+ } catch {
567
+ // stat failed, file may not exist
568
+ }
569
+
570
+ try {
571
+ const fileContent = readFileSync(configPath, "utf-8");
572
+ fileConfig = JSON.parse(fileContent);
573
+ } catch {
574
+ // File doesn't exist or invalid JSON
575
+ }
576
+
577
+ // Detect multi-instance format
578
+ if (fileConfig && fileConfig.instances) {
579
+ const instances: Record<string, LlngConfig> = {};
580
+ for (const [name, instanceConfig] of Object.entries(fileConfig.instances)) {
581
+ instances[name] = applyInstanceDefaults(instanceConfig as Partial<LlngConfig>);
582
+ }
583
+ const instanceNames = Object.keys(instances);
584
+ let defaultName: string | undefined = fileConfig.default;
585
+ if (!defaultName || !instances[defaultName]) {
586
+ defaultName = instanceNames[0] || "default";
587
+ }
588
+ const multi: LlngMultiConfig = {
589
+ instances,
590
+ default: defaultName,
591
+ };
592
+ // Apply env vars to the default instance, if it exists
593
+ if (multi.instances[multi.default]) {
594
+ applyEnvOverrides(multi.instances[multi.default]);
595
+ }
596
+ return multi;
597
+ }
598
+
599
+ // Legacy flat format: wrap as single "default" instance
600
+ const singleConfig = loadConfig();
601
+ return {
602
+ instances: { default: singleConfig },
603
+ default: "default",
604
+ };
605
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { loadMultiConfig } from "./config.js";
6
+ import { TransportRegistry } from "./transport/registry.js";
7
+ import { registerConfigTools } from "./tools/config.js";
8
+ import { registerSessionTools } from "./tools/sessions.js";
9
+ import { registerSecondFactorTools } from "./tools/secondfactors.js";
10
+ import { registerConsentTools } from "./tools/consents.js";
11
+ import { registerOidcTools } from "./tools/oidc.js";
12
+ import { registerOidcRpTools } from "./tools/oidc-rp.js";
13
+ import { registerInstanceTools } from "./tools/instances.js";
14
+ import { registerCliUtilityTools } from "./tools/cli-utilities.js";
15
+ import { registerDocumentationResource } from "./resources/documentation.js";
16
+
17
+ async function main() {
18
+ const multiConfig = loadMultiConfig();
19
+ const registry = new TransportRegistry(multiConfig);
20
+
21
+ // Create MCP server
22
+ const server = new McpServer({
23
+ name: "llng-mcp",
24
+ version: "0.1.0",
25
+ });
26
+
27
+ // Register all tools
28
+ registerConfigTools(server, registry);
29
+ registerSessionTools(server, registry);
30
+ registerSecondFactorTools(server, registry);
31
+ registerConsentTools(server, registry);
32
+ registerOidcTools(server, registry);
33
+ registerOidcRpTools(server, registry);
34
+ registerInstanceTools(server, registry);
35
+ registerCliUtilityTools(server, registry);
36
+
37
+ // Register resources
38
+ registerDocumentationResource(server);
39
+
40
+ // Start server
41
+ const stdioTransport = new StdioServerTransport();
42
+ await server.connect(stdioTransport);
43
+ }
44
+
45
+ main().catch((error) => {
46
+ console.error("Fatal error:", error instanceof Error ? error.message : String(error));
47
+ process.exit(1);
48
+ });