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
@@ -0,0 +1,65 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ /**
4
+ * Register LLNG documentation resource template
5
+ */
6
+ export function registerDocumentationResource(server: McpServer): void {
7
+ const template = new ResourceTemplate("llng://documentation/{page}", {
8
+ list: undefined, // No dynamic listing of documentation pages
9
+ });
10
+
11
+ server.resource(
12
+ "llng-documentation",
13
+ template,
14
+ { description: "Fetch Lemonldap-NG documentation page" },
15
+ async (uri, variables) => {
16
+ const page = variables.page as string;
17
+ const url = `https://lemonldap-ng.org/documentation/latest/${page}`;
18
+
19
+ try {
20
+ const resp = await fetch(url);
21
+ if (!resp.ok) {
22
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
23
+ }
24
+
25
+ const html = await resp.text();
26
+
27
+ // Strip HTML tags for simple text extraction
28
+ const text = html
29
+ .replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "")
30
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
31
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
32
+ .replace(/<[^>]*>/g, " ")
33
+ .replace(/&amp;/g, "&")
34
+ .replace(/&lt;/g, "<")
35
+ .replace(/&gt;/g, ">")
36
+ .replace(/&quot;/g, '"')
37
+ .replace(/&#39;/g, "'")
38
+ .replace(/&nbsp;/g, " ")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+
42
+ return {
43
+ contents: [
44
+ {
45
+ uri: uri.href,
46
+ mimeType: "text/plain",
47
+ text,
48
+ },
49
+ ],
50
+ };
51
+ } catch (e: unknown) {
52
+ const errorMessage = e instanceof Error ? e.message : String(e);
53
+ return {
54
+ contents: [
55
+ {
56
+ uri: uri.href,
57
+ mimeType: "text/plain",
58
+ text: `Error fetching documentation: ${errorMessage}`,
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ },
64
+ );
65
+ }
@@ -0,0 +1,207 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { TransportRegistry } from "../transport/registry.js";
4
+
5
+ export function registerCliUtilityTools(server: McpServer, registry: TransportRegistry): void {
6
+ // llng_download_saml_metadata
7
+ server.tool(
8
+ "llng_download_saml_metadata",
9
+ "Download SAML metadata from a remote IdP",
10
+ {
11
+ url: z.string().describe("URL of the remote SAML metadata"),
12
+ outputFile: z.string().optional().describe("Output file path for downloaded metadata"),
13
+ noCheck: z.boolean().optional().describe("Disable SSL certificate verification"),
14
+ verbose: z.boolean().optional().describe("Enable verbose output"),
15
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
16
+ },
17
+ async (params) => {
18
+ try {
19
+ const transport = registry.getTransport(params.instance);
20
+ const args: string[] = ["--url", params.url];
21
+ if (params.outputFile) args.push("--output-file", params.outputFile);
22
+ if (params.noCheck) args.push("--no-check");
23
+ if (params.verbose) args.push("--verbose");
24
+ const result = await transport.execScript("downloadSamlMetadata", args);
25
+ return { content: [{ type: "text", text: result }] };
26
+ } catch (e: unknown) {
27
+ return {
28
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
29
+ isError: true,
30
+ };
31
+ }
32
+ },
33
+ );
34
+
35
+ // llng_import_metadata
36
+ server.tool(
37
+ "llng_import_metadata",
38
+ "Import a SAML federation into LLNG config",
39
+ {
40
+ url: z.string().describe("URL of the SAML federation metadata"),
41
+ spPrefix: z.string().optional().describe("Prefix for SP entity IDs"),
42
+ idpPrefix: z.string().optional().describe("Prefix for IdP entity IDs"),
43
+ ignoreSp: z.array(z.string()).optional().describe("SP entity IDs to ignore"),
44
+ ignoreIdp: z.array(z.string()).optional().describe("IdP entity IDs to ignore"),
45
+ remove: z.boolean().optional().describe("Remove entities not in metadata"),
46
+ noCheck: z.boolean().optional().describe("Disable SSL certificate verification"),
47
+ verbose: z.boolean().optional().describe("Enable verbose output"),
48
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
49
+ },
50
+ async (params) => {
51
+ try {
52
+ const transport = registry.getTransport(params.instance);
53
+ const args: string[] = ["--url", params.url];
54
+ if (params.spPrefix) args.push("--sp-prefix", params.spPrefix);
55
+ if (params.idpPrefix) args.push("--idp-prefix", params.idpPrefix);
56
+ if (params.ignoreSp) {
57
+ for (const sp of params.ignoreSp) {
58
+ args.push("--ignore-sp", sp);
59
+ }
60
+ }
61
+ if (params.ignoreIdp) {
62
+ for (const idp of params.ignoreIdp) {
63
+ args.push("--ignore-idp", idp);
64
+ }
65
+ }
66
+ if (params.remove) args.push("--remove");
67
+ if (params.noCheck) args.push("--no-check");
68
+ if (params.verbose) args.push("--verbose");
69
+ const result = await transport.execScript("importMetadata", args);
70
+ return { content: [{ type: "text", text: result }] };
71
+ } catch (e: unknown) {
72
+ return {
73
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
74
+ isError: true,
75
+ };
76
+ }
77
+ },
78
+ );
79
+
80
+ // llng_delete_session
81
+ server.tool(
82
+ "llng_delete_session",
83
+ "Delete user sessions by UID pattern",
84
+ {
85
+ uid: z.string().describe("UID pattern to match for session deletion"),
86
+ force: z.boolean().optional().describe("Force deletion without confirmation"),
87
+ debug: z.boolean().optional().describe("Enable debug output"),
88
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
89
+ },
90
+ async (params) => {
91
+ try {
92
+ const transport = registry.getTransport(params.instance);
93
+ const args: string[] = ["--uid", params.uid];
94
+ if (params.force) args.push("--force");
95
+ if (params.debug) args.push("--debug");
96
+ const result = await transport.execScript("llngDeleteSession", args);
97
+ return { content: [{ type: "text", text: result }] };
98
+ } catch (e: unknown) {
99
+ return {
100
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
101
+ isError: true,
102
+ };
103
+ }
104
+ },
105
+ );
106
+
107
+ // llng_user_attributes
108
+ server.tool(
109
+ "llng_user_attributes",
110
+ "Look up user attributes",
111
+ {
112
+ username: z.string().describe("Username to look up"),
113
+ field: z.string().optional().describe("Specific field to return"),
114
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
115
+ },
116
+ async (params) => {
117
+ try {
118
+ const transport = registry.getTransport(params.instance);
119
+ const args: string[] = ["--username", params.username];
120
+ if (params.field) args.push("--field", params.field);
121
+ const result = await transport.execScript("llngUserAttributes", args);
122
+ return { content: [{ type: "text", text: result }] };
123
+ } catch (e: unknown) {
124
+ return {
125
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
126
+ isError: true,
127
+ };
128
+ }
129
+ },
130
+ );
131
+
132
+ // llng_purge_central_cache
133
+ server.tool(
134
+ "llng_purge_central_cache",
135
+ "Purge expired sessions from central cache",
136
+ {
137
+ debug: z.boolean().optional().describe("Enable debug output"),
138
+ force: z.boolean().optional().describe("Force purge without confirmation"),
139
+ json: z.boolean().optional().describe("Output in JSON format"),
140
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
141
+ },
142
+ async (params) => {
143
+ try {
144
+ const transport = registry.getTransport(params.instance);
145
+ const args: string[] = [];
146
+ if (params.debug) args.push("--debug");
147
+ if (params.force) args.push("--force");
148
+ if (params.json) args.push("--json");
149
+ const result = await transport.execScript("purgeCentralCache", args);
150
+ return { content: [{ type: "text", text: result }] };
151
+ } catch (e: unknown) {
152
+ return {
153
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
154
+ isError: true,
155
+ };
156
+ }
157
+ },
158
+ );
159
+
160
+ // llng_purge_local_cache
161
+ server.tool(
162
+ "llng_purge_local_cache",
163
+ "Purge local handler cache",
164
+ {
165
+ debug: z.boolean().optional().describe("Enable debug output"),
166
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
167
+ },
168
+ async (params) => {
169
+ try {
170
+ const transport = registry.getTransport(params.instance);
171
+ const args: string[] = [];
172
+ if (params.debug) args.push("--debug");
173
+ const result = await transport.execScript("purgeLocalCache", args);
174
+ return { content: [{ type: "text", text: result }] };
175
+ } catch (e: unknown) {
176
+ return {
177
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
178
+ isError: true,
179
+ };
180
+ }
181
+ },
182
+ );
183
+
184
+ // llng_rotate_oidc_keys
185
+ server.tool(
186
+ "llng_rotate_oidc_keys",
187
+ "Rotate OIDC signing keys",
188
+ {
189
+ debug: z.boolean().optional().describe("Enable debug output"),
190
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
191
+ },
192
+ async (params) => {
193
+ try {
194
+ const transport = registry.getTransport(params.instance);
195
+ const args: string[] = [];
196
+ if (params.debug) args.push("--debug");
197
+ const result = await transport.execScript("rotateOidcKeys", args);
198
+ return { content: [{ type: "text", text: result }] };
199
+ } catch (e: unknown) {
200
+ return {
201
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
202
+ isError: true,
203
+ };
204
+ }
205
+ },
206
+ );
207
+ }
@@ -0,0 +1,382 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { TransportRegistry } from "../transport/registry.js";
4
+
5
+ /**
6
+ * Register LLNG configuration management tools
7
+ */
8
+ export function registerConfigTools(server: McpServer, registry: TransportRegistry): void {
9
+ // 1. llng_config_info - Get current LLNG config metadata
10
+ server.tool(
11
+ "llng_config_info",
12
+ "Get current LLNG config metadata (number, author, date)",
13
+ {
14
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
15
+ },
16
+ async (args) => {
17
+ try {
18
+ const transport = registry.getTransport(args.instance, "manager");
19
+ const result = await transport.configInfo();
20
+ return {
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: JSON.stringify(result, null, 2),
25
+ },
26
+ ],
27
+ };
28
+ } catch (error) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
34
+ },
35
+ ],
36
+ isError: true,
37
+ };
38
+ }
39
+ },
40
+ );
41
+
42
+ // 2. llng_config_get - Get LLNG config value(s) by key
43
+ server.tool(
44
+ "llng_config_get",
45
+ "Get LLNG config value(s) by key",
46
+ {
47
+ keys: z.array(z.string()).describe("Array of config keys to retrieve"),
48
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
49
+ },
50
+ async (args) => {
51
+ try {
52
+ const transport = registry.getTransport(args.instance, "manager");
53
+ const result = await transport.configGet(args.keys);
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: JSON.stringify(result, null, 2),
59
+ },
60
+ ],
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
68
+ },
69
+ ],
70
+ isError: true,
71
+ };
72
+ }
73
+ },
74
+ );
75
+
76
+ // 3. llng_config_set - Set LLNG config value(s)
77
+ server.tool(
78
+ "llng_config_set",
79
+ "Set LLNG config value(s)",
80
+ {
81
+ keys: z.record(z.string(), z.any()).describe("Key-value pairs to set in config"),
82
+ log: z.string().optional().describe("Optional log message for this change"),
83
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
84
+ },
85
+ async (args) => {
86
+ try {
87
+ const transport = registry.getTransport(args.instance, "manager");
88
+ await transport.configSet(args.keys, args.log);
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: "Config values updated successfully",
94
+ },
95
+ ],
96
+ };
97
+ } catch (error) {
98
+ return {
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
103
+ },
104
+ ],
105
+ isError: true,
106
+ };
107
+ }
108
+ },
109
+ );
110
+
111
+ // 4. llng_config_addKey - Add subkey to a composite LLNG config parameter
112
+ server.tool(
113
+ "llng_config_addKey",
114
+ "Add subkey to a composite LLNG config parameter",
115
+ {
116
+ key: z.string().describe("The composite config key"),
117
+ subkey: z.string().describe("The subkey to add"),
118
+ value: z.string().describe("The value for the subkey"),
119
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
120
+ },
121
+ async (args) => {
122
+ try {
123
+ const transport = registry.getTransport(args.instance, "manager");
124
+ await transport.configAddKey(args.key, args.subkey, args.value);
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `Subkey '${args.subkey}' added to '${args.key}' successfully`,
130
+ },
131
+ ],
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
139
+ },
140
+ ],
141
+ isError: true,
142
+ };
143
+ }
144
+ },
145
+ );
146
+
147
+ // 5. llng_config_delKey - Delete subkey from a composite LLNG config parameter
148
+ server.tool(
149
+ "llng_config_delKey",
150
+ "Delete subkey from a composite LLNG config parameter",
151
+ {
152
+ key: z.string().describe("The composite config key"),
153
+ subkey: z.string().describe("The subkey to delete"),
154
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
155
+ },
156
+ async (args) => {
157
+ try {
158
+ const transport = registry.getTransport(args.instance, "manager");
159
+ await transport.configDelKey(args.key, args.subkey);
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `Subkey '${args.subkey}' deleted from '${args.key}' successfully`,
165
+ },
166
+ ],
167
+ };
168
+ } catch (error) {
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
174
+ },
175
+ ],
176
+ isError: true,
177
+ };
178
+ }
179
+ },
180
+ );
181
+
182
+ // 6. llng_config_export - Export full LLNG config as JSON
183
+ server.tool(
184
+ "llng_config_export",
185
+ "Export full LLNG config as JSON",
186
+ {
187
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
188
+ },
189
+ async (args) => {
190
+ try {
191
+ const transport = registry.getTransport(args.instance, "manager");
192
+ const result = await transport.configSave();
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: result,
198
+ },
199
+ ],
200
+ };
201
+ } catch (error) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+ },
213
+ );
214
+
215
+ // 7. llng_config_import - Import LLNG config from JSON
216
+ server.tool(
217
+ "llng_config_import",
218
+ "Import LLNG config from JSON",
219
+ {
220
+ json: z.string().describe("JSON string of the config to import"),
221
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
222
+ },
223
+ async (args) => {
224
+ try {
225
+ const transport = registry.getTransport(args.instance, "manager");
226
+ await transport.configRestore(args.json);
227
+ return {
228
+ content: [
229
+ {
230
+ type: "text",
231
+ text: "Config imported successfully",
232
+ },
233
+ ],
234
+ };
235
+ } catch (error) {
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
241
+ },
242
+ ],
243
+ isError: true,
244
+ };
245
+ }
246
+ },
247
+ );
248
+
249
+ // 8. llng_config_merge - Merge JSON snippet into LLNG config
250
+ server.tool(
251
+ "llng_config_merge",
252
+ "Merge JSON snippet into LLNG config",
253
+ {
254
+ json: z.string().describe("JSON string to merge into config"),
255
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
256
+ },
257
+ async (args) => {
258
+ try {
259
+ const transport = registry.getTransport(args.instance, "manager");
260
+ await transport.configMerge(args.json);
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: "Config merged successfully",
266
+ },
267
+ ],
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
275
+ },
276
+ ],
277
+ isError: true,
278
+ };
279
+ }
280
+ },
281
+ );
282
+
283
+ // 9. llng_config_rollback - Rollback LLNG config to previous version
284
+ server.tool(
285
+ "llng_config_rollback",
286
+ "Rollback LLNG config to previous version",
287
+ {
288
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
289
+ },
290
+ async (args) => {
291
+ try {
292
+ const transport = registry.getTransport(args.instance, "manager");
293
+ await transport.configRollback();
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: "Config rolled back successfully",
299
+ },
300
+ ],
301
+ };
302
+ } catch (error) {
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
308
+ },
309
+ ],
310
+ isError: true,
311
+ };
312
+ }
313
+ },
314
+ );
315
+
316
+ // 10. llng_config_update_cache - Force LLNG config cache update
317
+ server.tool(
318
+ "llng_config_update_cache",
319
+ "Force LLNG config cache update",
320
+ {
321
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
322
+ },
323
+ async (args) => {
324
+ try {
325
+ const transport = registry.getTransport(args.instance, "manager");
326
+ await transport.configUpdateCache();
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: "Config cache updated successfully",
332
+ },
333
+ ],
334
+ };
335
+ } catch (error) {
336
+ return {
337
+ content: [
338
+ {
339
+ type: "text",
340
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
341
+ },
342
+ ],
343
+ isError: true,
344
+ };
345
+ }
346
+ },
347
+ );
348
+
349
+ // 11. llng_config_test_email - Send a test email to verify SMTP settings
350
+ server.tool(
351
+ "llng_config_test_email",
352
+ "Send a test email to verify SMTP settings",
353
+ {
354
+ destination: z.string().describe("Email address to send the test email to"),
355
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
356
+ },
357
+ async (args) => {
358
+ try {
359
+ const transport = registry.getTransport(args.instance, "manager");
360
+ await transport.configTestEmail(args.destination);
361
+ return {
362
+ content: [
363
+ {
364
+ type: "text",
365
+ text: `Test email sent successfully to ${args.destination}`,
366
+ },
367
+ ],
368
+ };
369
+ } catch (error) {
370
+ return {
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
375
+ },
376
+ ],
377
+ isError: true,
378
+ };
379
+ }
380
+ },
381
+ );
382
+ }
@@ -0,0 +1,50 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { TransportRegistry } from "../transport/registry.js";
4
+
5
+ export function registerConsentTools(server: McpServer, registry: TransportRegistry): void {
6
+ server.tool(
7
+ "llng_consent_list",
8
+ "List user's OIDC consents",
9
+ {
10
+ user: z.string(),
11
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
12
+ },
13
+ async (params) => {
14
+ try {
15
+ const transport = registry.getTransport(params.instance);
16
+ const result = await transport.consentsGet(params.user);
17
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
18
+ } catch (e: unknown) {
19
+ return {
20
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
21
+ isError: true,
22
+ };
23
+ }
24
+ },
25
+ );
26
+
27
+ server.tool(
28
+ "llng_consent_delete",
29
+ "Delete user's OIDC consent(s)",
30
+ {
31
+ user: z.string(),
32
+ ids: z.array(z.string()),
33
+ instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
34
+ },
35
+ async (params) => {
36
+ try {
37
+ const transport = registry.getTransport(params.instance);
38
+ await transport.consentsDelete(params.user, params.ids);
39
+ return {
40
+ content: [{ type: "text", text: `Successfully deleted ${params.ids.length} consent(s)` }],
41
+ };
42
+ } catch (e: unknown) {
43
+ return {
44
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
45
+ isError: true,
46
+ };
47
+ }
48
+ },
49
+ );
50
+ }