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.
- package/.github/workflows/ci.yml +77 -0
- package/.prettierrc +7 -0
- package/LICENSE +661 -0
- package/README.md +502 -0
- package/dist/__tests__/api-transport.test.d.ts +1 -0
- package/dist/__tests__/api-transport.test.js +577 -0
- package/dist/__tests__/api-transport.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +472 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration/api-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/api-mode.test.js +199 -0
- package/dist/__tests__/integration/api-mode.test.js.map +1 -0
- package/dist/__tests__/integration/oidc-rp.test.d.ts +1 -0
- package/dist/__tests__/integration/oidc-rp.test.js +120 -0
- package/dist/__tests__/integration/oidc-rp.test.js.map +1 -0
- package/dist/__tests__/integration/ssh-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/ssh-mode.test.js +101 -0
- package/dist/__tests__/integration/ssh-mode.test.js.map +1 -0
- package/dist/__tests__/k8s-transport.test.d.ts +1 -0
- package/dist/__tests__/k8s-transport.test.js +254 -0
- package/dist/__tests__/k8s-transport.test.js.map +1 -0
- package/dist/__tests__/oidc-tools.test.d.ts +1 -0
- package/dist/__tests__/oidc-tools.test.js +457 -0
- package/dist/__tests__/oidc-tools.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +96 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/ssh-transport.test.d.ts +1 -0
- package/dist/__tests__/ssh-transport.test.js +618 -0
- package/dist/__tests__/ssh-transport.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +525 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.js +506 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/documentation.d.ts +5 -0
- package/dist/resources/documentation.js +56 -0
- package/dist/resources/documentation.js.map +1 -0
- package/dist/tools/cli-utilities.d.ts +3 -0
- package/dist/tools/cli-utilities.js +187 -0
- package/dist/tools/cli-utilities.js.map +1 -0
- package/dist/tools/config.d.ts +6 -0
- package/dist/tools/config.js +326 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/consents.d.ts +3 -0
- package/dist/tools/consents.js +39 -0
- package/dist/tools/consents.js.map +1 -0
- package/dist/tools/instances.d.ts +3 -0
- package/dist/tools/instances.js +14 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/oidc-rp.d.ts +6 -0
- package/dist/tools/oidc-rp.js +246 -0
- package/dist/tools/oidc-rp.js.map +1 -0
- package/dist/tools/oidc.d.ts +3 -0
- package/dist/tools/oidc.js +343 -0
- package/dist/tools/oidc.js.map +1 -0
- package/dist/tools/secondfactors.d.ts +3 -0
- package/dist/tools/secondfactors.js +62 -0
- package/dist/tools/secondfactors.js.map +1 -0
- package/dist/tools/sessions.d.ts +6 -0
- package/dist/tools/sessions.js +300 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/transport/api.d.ts +35 -0
- package/dist/transport/api.js +327 -0
- package/dist/transport/api.js.map +1 -0
- package/dist/transport/interface.d.ts +50 -0
- package/dist/transport/interface.js +2 -0
- package/dist/transport/interface.js.map +1 -0
- package/dist/transport/k8s.d.ts +41 -0
- package/dist/transport/k8s.js +303 -0
- package/dist/transport/k8s.js.map +1 -0
- package/dist/transport/registry.d.ts +20 -0
- package/dist/transport/registry.js +91 -0
- package/dist/transport/registry.js.map +1 -0
- package/dist/transport/ssh.d.ts +37 -0
- package/dist/transport/ssh.js +353 -0
- package/dist/transport/ssh.js.map +1 -0
- package/docker-compose.test.yml +16 -0
- package/eslint.config.js +21 -0
- package/package.json +38 -0
- package/src/__tests__/api-transport.test.ts +746 -0
- package/src/__tests__/config.test.ts +587 -0
- package/src/__tests__/integration/api-mode.test.ts +229 -0
- package/src/__tests__/integration/oidc-rp.test.ts +138 -0
- package/src/__tests__/integration/ssh-mode.test.ts +113 -0
- package/src/__tests__/k8s-transport.test.ts +342 -0
- package/src/__tests__/oidc-tools.test.ts +554 -0
- package/src/__tests__/registry.test.ts +110 -0
- package/src/__tests__/ssh-transport.test.ts +805 -0
- package/src/__tests__/tools.test.ts +735 -0
- package/src/config.ts +605 -0
- package/src/index.ts +48 -0
- package/src/resources/documentation.ts +65 -0
- package/src/tools/cli-utilities.ts +207 -0
- package/src/tools/config.ts +382 -0
- package/src/tools/consents.ts +50 -0
- package/src/tools/instances.ts +21 -0
- package/src/tools/oidc-rp.ts +299 -0
- package/src/tools/oidc.ts +434 -0
- package/src/tools/secondfactors.ts +78 -0
- package/src/tools/sessions.ts +342 -0
- package/src/transport/api.ts +429 -0
- package/src/transport/interface.ts +58 -0
- package/src/transport/k8s.ts +367 -0
- package/src/transport/registry.ts +105 -0
- package/src/transport/ssh.ts +430 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { randomBytes, createHash } from "crypto";
|
|
4
|
+
import { OidcConfig } from "../config.js";
|
|
5
|
+
import { TransportRegistry } from "../transport/registry.js";
|
|
6
|
+
|
|
7
|
+
// Cache for OIDC discovery documents, keyed by issuer with TTL
|
|
8
|
+
const discoveryCache = new Map<string, { metadata: any; fetchedAt: number }>();
|
|
9
|
+
const DISCOVERY_CACHE_TTL_MS = 3600_000; // 1 hour
|
|
10
|
+
|
|
11
|
+
async function getDiscoveryMetadata(config: OidcConfig): Promise<any> {
|
|
12
|
+
const cached = discoveryCache.get(config.issuer);
|
|
13
|
+
if (cached && Date.now() - cached.fetchedAt < DISCOVERY_CACHE_TTL_MS) {
|
|
14
|
+
return cached.metadata;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const url = `${config.issuer}/.well-known/openid-configuration`;
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Failed to fetch discovery metadata: ${response.status} ${response.statusText}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const metadata = await response.json();
|
|
27
|
+
discoveryCache.set(config.issuer, { metadata, fetchedAt: Date.now() });
|
|
28
|
+
return metadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateCodeVerifier(): string {
|
|
32
|
+
return randomBytes(32).toString("base64url");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generateCodeChallenge(verifier: string): string {
|
|
36
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateState(): string {
|
|
40
|
+
return randomBytes(16).toString("base64url");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isUrlSafe(url: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(url);
|
|
46
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const hostname = parsed.hostname;
|
|
50
|
+
// Block private/link-local/loopback IPs
|
|
51
|
+
if (
|
|
52
|
+
hostname === "localhost" ||
|
|
53
|
+
hostname.startsWith("127.") ||
|
|
54
|
+
hostname === "[::1]" ||
|
|
55
|
+
hostname.startsWith("10.") ||
|
|
56
|
+
hostname.startsWith("192.168.") ||
|
|
57
|
+
hostname.startsWith("169.254.") ||
|
|
58
|
+
hostname.startsWith("0.") ||
|
|
59
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname)
|
|
60
|
+
) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function base64UrlDecode(str: string): string {
|
|
70
|
+
// Convert base64url to base64
|
|
71
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
72
|
+
// Add padding if needed
|
|
73
|
+
base64 += "=".repeat((4 - (base64.length % 4)) % 4);
|
|
74
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerOidcTools(server: McpServer, registry: TransportRegistry): void {
|
|
78
|
+
server.tool(
|
|
79
|
+
"llng_oidc_metadata",
|
|
80
|
+
"Fetch OIDC discovery metadata",
|
|
81
|
+
{
|
|
82
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
83
|
+
},
|
|
84
|
+
async (params) => {
|
|
85
|
+
try {
|
|
86
|
+
const config = registry.getOidcConfig(params.instance);
|
|
87
|
+
if (!config) {
|
|
88
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
92
|
+
return { content: [{ type: "text", text: JSON.stringify(metadata, null, 2) }] };
|
|
93
|
+
} catch (e: unknown) {
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
server.tool(
|
|
103
|
+
"llng_oidc_authorize",
|
|
104
|
+
"Get authorization URL with PKCE",
|
|
105
|
+
{
|
|
106
|
+
scope: z.string().optional(),
|
|
107
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
108
|
+
},
|
|
109
|
+
async (params) => {
|
|
110
|
+
try {
|
|
111
|
+
const config = registry.getOidcConfig(params.instance);
|
|
112
|
+
if (!config) {
|
|
113
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
117
|
+
const codeVerifier = generateCodeVerifier();
|
|
118
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
119
|
+
const state = generateState();
|
|
120
|
+
const scope = params.scope || config.scope;
|
|
121
|
+
|
|
122
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
123
|
+
authUrl.searchParams.set("response_type", "code");
|
|
124
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
125
|
+
authUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
126
|
+
authUrl.searchParams.set("scope", scope);
|
|
127
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
128
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
129
|
+
authUrl.searchParams.set("state", state);
|
|
130
|
+
|
|
131
|
+
const result = {
|
|
132
|
+
url: authUrl.toString(),
|
|
133
|
+
code_verifier: codeVerifier,
|
|
134
|
+
state: state,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
138
|
+
} catch (e: unknown) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
"llng_oidc_tokens",
|
|
149
|
+
"Exchange authorization code for tokens",
|
|
150
|
+
{
|
|
151
|
+
code: z.string(),
|
|
152
|
+
code_verifier: z.string(),
|
|
153
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
154
|
+
},
|
|
155
|
+
async (params) => {
|
|
156
|
+
try {
|
|
157
|
+
const config = registry.getOidcConfig(params.instance);
|
|
158
|
+
if (!config) {
|
|
159
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
163
|
+
const body = new URLSearchParams();
|
|
164
|
+
body.set("grant_type", "authorization_code");
|
|
165
|
+
body.set("code", params.code);
|
|
166
|
+
body.set("redirect_uri", config.redirectUri);
|
|
167
|
+
body.set("client_id", config.clientId);
|
|
168
|
+
body.set("code_verifier", params.code_verifier);
|
|
169
|
+
|
|
170
|
+
if (config.clientSecret) {
|
|
171
|
+
body.set("client_secret", config.clientSecret);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
178
|
+
},
|
|
179
|
+
body: body.toString(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await response.json();
|
|
183
|
+
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: `Error: ${JSON.stringify(result)}` }],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
192
|
+
} catch (e: unknown) {
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
195
|
+
isError: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
server.tool(
|
|
202
|
+
"llng_oidc_userinfo",
|
|
203
|
+
"Get user info from OIDC provider",
|
|
204
|
+
{
|
|
205
|
+
access_token: z.string(),
|
|
206
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
207
|
+
},
|
|
208
|
+
async (params) => {
|
|
209
|
+
try {
|
|
210
|
+
const config = registry.getOidcConfig(params.instance);
|
|
211
|
+
if (!config) {
|
|
212
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
216
|
+
const response = await fetch(metadata.userinfo_endpoint, {
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${params.access_token}`,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const result = await response.json();
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text", text: `Error: ${JSON.stringify(result)}` }],
|
|
227
|
+
isError: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
232
|
+
} catch (e: unknown) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
server.tool(
|
|
242
|
+
"llng_oidc_introspect",
|
|
243
|
+
"Introspect an access token",
|
|
244
|
+
{
|
|
245
|
+
token: z.string(),
|
|
246
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
247
|
+
},
|
|
248
|
+
async (params) => {
|
|
249
|
+
try {
|
|
250
|
+
const config = registry.getOidcConfig(params.instance);
|
|
251
|
+
if (!config) {
|
|
252
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
256
|
+
|
|
257
|
+
if (!metadata.introspection_endpoint) {
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: "Error: Introspection endpoint not supported" }],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const body = new URLSearchParams();
|
|
265
|
+
body.set("token", params.token);
|
|
266
|
+
body.set("client_id", config.clientId);
|
|
267
|
+
|
|
268
|
+
if (config.clientSecret) {
|
|
269
|
+
body.set("client_secret", config.clientSecret);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const response = await fetch(metadata.introspection_endpoint, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: {
|
|
275
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
276
|
+
},
|
|
277
|
+
body: body.toString(),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const result = await response.json();
|
|
281
|
+
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: `Error: ${JSON.stringify(result)}` }],
|
|
285
|
+
isError: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
290
|
+
} catch (e: unknown) {
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
293
|
+
isError: true,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
server.tool(
|
|
300
|
+
"llng_oidc_refresh",
|
|
301
|
+
"Refresh an access token",
|
|
302
|
+
{
|
|
303
|
+
refresh_token: z.string(),
|
|
304
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
305
|
+
},
|
|
306
|
+
async (params) => {
|
|
307
|
+
try {
|
|
308
|
+
const config = registry.getOidcConfig(params.instance);
|
|
309
|
+
if (!config) {
|
|
310
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const metadata = await getDiscoveryMetadata(config);
|
|
314
|
+
const body = new URLSearchParams();
|
|
315
|
+
body.set("grant_type", "refresh_token");
|
|
316
|
+
body.set("refresh_token", params.refresh_token);
|
|
317
|
+
body.set("client_id", config.clientId);
|
|
318
|
+
|
|
319
|
+
if (config.clientSecret) {
|
|
320
|
+
body.set("client_secret", config.clientSecret);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: {
|
|
326
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
327
|
+
},
|
|
328
|
+
body: body.toString(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const result = await response.json();
|
|
332
|
+
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: "text", text: `Error: ${JSON.stringify(result)}` }],
|
|
336
|
+
isError: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
341
|
+
} catch (e: unknown) {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
344
|
+
isError: true,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
server.tool(
|
|
351
|
+
"llng_oidc_whoami",
|
|
352
|
+
"Decode ID token to show identity (WARNING: signature is NOT verified, for debugging only)",
|
|
353
|
+
{
|
|
354
|
+
id_token: z.string(),
|
|
355
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
356
|
+
},
|
|
357
|
+
async (params) => {
|
|
358
|
+
try {
|
|
359
|
+
const config = registry.getOidcConfig(params.instance);
|
|
360
|
+
if (!config) {
|
|
361
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const parts = params.id_token.split(".");
|
|
365
|
+
if (parts.length !== 3) {
|
|
366
|
+
return { content: [{ type: "text", text: "Error: Invalid JWT format" }], isError: true };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
370
|
+
const result = {
|
|
371
|
+
_warning:
|
|
372
|
+
"UNVERIFIED - JWT signature was NOT checked. Do not trust these claims for authorization decisions.",
|
|
373
|
+
...payload,
|
|
374
|
+
};
|
|
375
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
376
|
+
} catch (e: unknown) {
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
379
|
+
isError: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
server.tool(
|
|
386
|
+
"llng_oidc_check_auth",
|
|
387
|
+
"Check if a URL requires authentication (only public URLs allowed, private/internal IPs blocked)",
|
|
388
|
+
{
|
|
389
|
+
url: z.string().url(),
|
|
390
|
+
access_token: z.string(),
|
|
391
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
392
|
+
},
|
|
393
|
+
async (params) => {
|
|
394
|
+
try {
|
|
395
|
+
const config = registry.getOidcConfig(params.instance);
|
|
396
|
+
if (!config) {
|
|
397
|
+
return { content: [{ type: "text", text: "Error: OIDC not configured" }], isError: true };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!isUrlSafe(params.url)) {
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: "Error: URL must be a public HTTP(S) URL. Private, loopback, and link-local addresses are blocked.",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
isError: true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const response = await fetch(params.url, {
|
|
413
|
+
headers: {
|
|
414
|
+
Authorization: `Bearer ${params.access_token}`,
|
|
415
|
+
},
|
|
416
|
+
redirect: "manual",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const result = {
|
|
420
|
+
status: response.status,
|
|
421
|
+
statusText: response.statusText,
|
|
422
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
426
|
+
} catch (e: unknown) {
|
|
427
|
+
return {
|
|
428
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
429
|
+
isError: true,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 registerSecondFactorTools(server: McpServer, registry: TransportRegistry): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
"llng_2fa_list",
|
|
8
|
+
"List user's 2FA devices",
|
|
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.secondFactorsGet(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_2fa_delete",
|
|
29
|
+
"Delete specific 2FA device(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.secondFactorsDelete(params.user, params.ids);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{ type: "text", text: `Successfully deleted ${params.ids.length} 2FA device(s)` },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
} catch (e: unknown) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
"llng_2fa_delType",
|
|
55
|
+
"Delete all 2FA devices of a given type",
|
|
56
|
+
{
|
|
57
|
+
user: z.string(),
|
|
58
|
+
type: z.string(),
|
|
59
|
+
instance: z.string().optional().describe("LLNG instance name (uses default if omitted)"),
|
|
60
|
+
},
|
|
61
|
+
async (params) => {
|
|
62
|
+
try {
|
|
63
|
+
const transport = registry.getTransport(params.instance);
|
|
64
|
+
await transport.secondFactorsDelType(params.user, params.type);
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{ type: "text", text: `Successfully deleted all '${params.type}' 2FA devices` },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
} catch (e: unknown) {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|