@alfe.ai/connectwise-screenconnect-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/README.md +104 -0
- package/dist/bin.cjs +30 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +31 -0
- package/dist/screenconnect-client.cjs +5 -0
- package/dist/screenconnect-client.d.cts +92 -0
- package/dist/screenconnect-client.d.ts +92 -0
- package/dist/screenconnect-client.js +2 -0
- package/dist/screenconnect-client2.cjs +357 -0
- package/dist/screenconnect-client2.d.cts +2 -0
- package/dist/screenconnect-client2.d.ts +2 -0
- package/dist/screenconnect-client2.js +280 -0
- package/dist/server.cjs +6 -0
- package/dist/server.d.cts +24 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +2 -0
- package/dist/server2.cjs +286 -0
- package/dist/server2.js +263 -0
- package/package.json +50 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
let node_dns_promises = require("node:dns/promises");
|
|
2
|
+
let node_net = require("node:net");
|
|
3
|
+
let zod = require("zod");
|
|
4
|
+
let node_https = require("node:https");
|
|
5
|
+
//#region src/boundary.ts
|
|
6
|
+
const PROVIDER = "connectwise-screenconnect";
|
|
7
|
+
const connectionIdField = zod.z.string().regex(/^con_[A-Za-z0-9_-]{1,128}$/u).describe("Exact connectionId from screenconnect_list_connections. Selects one authorized ScreenConnect instance.");
|
|
8
|
+
const sessionIdField = zod.z.uuid();
|
|
9
|
+
const textField = (max = 4096) => zod.z.string().min(1).max(max);
|
|
10
|
+
const customPropertiesField = zod.z.array(zod.z.string().max(4096)).max(8);
|
|
11
|
+
var ScreenConnectError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ScreenConnectError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Only errors created at this boundary are safe to display or capture. */
|
|
18
|
+
function safeErrorMessage(error) {
|
|
19
|
+
return error instanceof ScreenConnectError ? error.message : "ScreenConnect operation failed. Check the connection and retry reads; reconcile mutations before retrying.";
|
|
20
|
+
}
|
|
21
|
+
function normalizeSiteUrl(value) {
|
|
22
|
+
if (typeof value !== "string" || value.length > 2048) throw new ScreenConnectError("ScreenConnect site URL is invalid.");
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(value);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ScreenConnectError("ScreenConnect site URL is invalid.");
|
|
28
|
+
}
|
|
29
|
+
const host = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
30
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || (0, node_net.isIP)(host.replace(/^\[|\]$/gu, "")) || !host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host === "metadata.google.internal" || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(host)) throw new ScreenConnectError("ScreenConnect requires a public HTTPS origin without a path or credentials.");
|
|
31
|
+
url.hostname = host;
|
|
32
|
+
return url.origin;
|
|
33
|
+
}
|
|
34
|
+
function parseCredentials(value, connectionId) {
|
|
35
|
+
const parsed = zod.z.object({
|
|
36
|
+
provider: zod.z.literal(PROVIDER),
|
|
37
|
+
connectionId: connectionIdField,
|
|
38
|
+
siteUrl: zod.z.string(),
|
|
39
|
+
authenticationSecret: zod.z.string().min(1).max(16384).regex(/^[\x21-\x7e]+$/u),
|
|
40
|
+
apiMode: zod.z.literal("restful-api-manager")
|
|
41
|
+
}).safeParse(value);
|
|
42
|
+
if (!parsed.success || parsed.data.connectionId !== connectionId) throw new ScreenConnectError("The selected connection does not contain valid ScreenConnect credentials. Reconnect it in Alfe.");
|
|
43
|
+
return {
|
|
44
|
+
siteUrl: normalizeSiteUrl(parsed.data.siteUrl),
|
|
45
|
+
authenticationSecret: parsed.data.authenticationSecret
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const blocked = new node_net.BlockList();
|
|
49
|
+
for (const [address, prefix] of [
|
|
50
|
+
["0.0.0.0", 8],
|
|
51
|
+
["10.0.0.0", 8],
|
|
52
|
+
["100.64.0.0", 10],
|
|
53
|
+
["127.0.0.0", 8],
|
|
54
|
+
["169.254.0.0", 16],
|
|
55
|
+
["172.16.0.0", 12],
|
|
56
|
+
["192.0.0.0", 24],
|
|
57
|
+
["192.0.2.0", 24],
|
|
58
|
+
["192.168.0.0", 16],
|
|
59
|
+
["198.18.0.0", 15],
|
|
60
|
+
["198.51.100.0", 24],
|
|
61
|
+
["203.0.113.0", 24],
|
|
62
|
+
["224.0.0.0", 4],
|
|
63
|
+
["240.0.0.0", 4]
|
|
64
|
+
]) blocked.addSubnet(address, prefix, "ipv4");
|
|
65
|
+
for (const [address, prefix] of [
|
|
66
|
+
["::", 96],
|
|
67
|
+
["64:ff9b::", 96],
|
|
68
|
+
["64:ff9b:1::", 48],
|
|
69
|
+
["100::", 64],
|
|
70
|
+
["2001::", 32],
|
|
71
|
+
["2001:db8::", 32],
|
|
72
|
+
["2002::", 16],
|
|
73
|
+
["fc00::", 7],
|
|
74
|
+
["fe80::", 10],
|
|
75
|
+
["ff00::", 8]
|
|
76
|
+
]) blocked.addSubnet(address, prefix, "ipv6");
|
|
77
|
+
async function resolvePublicAddresses(hostname, signal, resolver = (host) => (0, node_dns_promises.lookup)(host, { all: true })) {
|
|
78
|
+
let onAbort;
|
|
79
|
+
try {
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
const addresses = await Promise.race([resolver(hostname), new Promise((_resolve, reject) => {
|
|
82
|
+
onAbort = () => {
|
|
83
|
+
reject(new ScreenConnectError("ScreenConnect request timed out."));
|
|
84
|
+
};
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
})]);
|
|
87
|
+
if (addresses.length === 0 || addresses.length > 64 || addresses.some(({ address, family }) => {
|
|
88
|
+
const actualFamily = (0, node_net.isIP)(address);
|
|
89
|
+
return actualFamily === 0 || actualFamily !== family || family === 6 && address.toLowerCase().startsWith("::ffff:") || blocked.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
90
|
+
})) throw new ScreenConnectError("ScreenConnect hostname must resolve only to public addresses.");
|
|
91
|
+
return addresses;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof ScreenConnectError) throw error;
|
|
94
|
+
throw new ScreenConnectError("ScreenConnect hostname could not be safely resolved.");
|
|
95
|
+
} finally {
|
|
96
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Avoid provider error envelopes being reported as successful session data. */
|
|
100
|
+
function parseResponse(text, secret) {
|
|
101
|
+
if (!text.trim()) return null;
|
|
102
|
+
const redacted = text.split(secret).join("[REDACTED]");
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = JSON.parse(redacted);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new ScreenConnectError("ScreenConnect returned invalid JSON.");
|
|
108
|
+
}
|
|
109
|
+
if (isRecord(result) && ("error" in result || "Error" in result || "ExceptionType" in result || "StackTrace" in result)) throw new ScreenConnectError("ScreenConnect rejected the operation. Check extension settings and permissions.");
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function isRecord(value) {
|
|
113
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/screenconnect-client.ts
|
|
117
|
+
const EXTENSION_PATH = "/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/";
|
|
118
|
+
/** Positional contracts from the official RESTful API Manager reference. */
|
|
119
|
+
const methods = {
|
|
120
|
+
GetSessionsByFilter: {
|
|
121
|
+
method: "GET",
|
|
122
|
+
parameters: zod.z.tuple([textField(2048)])
|
|
123
|
+
},
|
|
124
|
+
GetSessionsByName: {
|
|
125
|
+
method: "GET",
|
|
126
|
+
parameters: zod.z.tuple([textField(256)])
|
|
127
|
+
},
|
|
128
|
+
GetSessionBySessionID: {
|
|
129
|
+
method: "GET",
|
|
130
|
+
parameters: zod.z.tuple([sessionIdField])
|
|
131
|
+
},
|
|
132
|
+
GetSessionDetailsBySessionID: {
|
|
133
|
+
method: "GET",
|
|
134
|
+
parameters: zod.z.tuple([sessionIdField])
|
|
135
|
+
},
|
|
136
|
+
CreateSession: {
|
|
137
|
+
method: "POST",
|
|
138
|
+
parameters: zod.z.tuple([
|
|
139
|
+
zod.z.enum(["Support", "Meeting"]),
|
|
140
|
+
textField(256),
|
|
141
|
+
zod.z.boolean(),
|
|
142
|
+
zod.z.string().max(128),
|
|
143
|
+
customPropertiesField
|
|
144
|
+
])
|
|
145
|
+
},
|
|
146
|
+
UpdateSessionName: {
|
|
147
|
+
method: "POST",
|
|
148
|
+
parameters: zod.z.tuple([sessionIdField, textField(256)])
|
|
149
|
+
},
|
|
150
|
+
UpdateSessionCustomProperties: {
|
|
151
|
+
method: "POST",
|
|
152
|
+
parameters: zod.z.tuple([sessionIdField, customPropertiesField])
|
|
153
|
+
},
|
|
154
|
+
AddNoteToSession: {
|
|
155
|
+
method: "POST",
|
|
156
|
+
parameters: zod.z.tuple([sessionIdField, textField(16384)])
|
|
157
|
+
},
|
|
158
|
+
SendMessageToSession: {
|
|
159
|
+
method: "POST",
|
|
160
|
+
parameters: zod.z.tuple([
|
|
161
|
+
sessionIdField,
|
|
162
|
+
textField(256),
|
|
163
|
+
textField(16384)
|
|
164
|
+
])
|
|
165
|
+
},
|
|
166
|
+
SendCommandToSession: {
|
|
167
|
+
method: "POST",
|
|
168
|
+
parameters: zod.z.tuple([sessionIdField, textField(32768)])
|
|
169
|
+
},
|
|
170
|
+
SendToolboxItemToSession: {
|
|
171
|
+
method: "POST",
|
|
172
|
+
parameters: zod.z.tuple([sessionIdField, textField(512)])
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var ScreenConnectClient = class {
|
|
176
|
+
siteUrl;
|
|
177
|
+
secret;
|
|
178
|
+
resolveHost;
|
|
179
|
+
requestFactory;
|
|
180
|
+
timeoutMs;
|
|
181
|
+
constructor(options) {
|
|
182
|
+
this.siteUrl = normalizeSiteUrl(options.siteUrl);
|
|
183
|
+
if (typeof options.authenticationSecret !== "string" || !/^[\x21-\x7e]{1,16384}$/u.test(options.authenticationSecret)) throw new ScreenConnectError("ScreenConnect authentication secret is invalid.");
|
|
184
|
+
this.secret = options.authenticationSecret;
|
|
185
|
+
this.resolveHost = options.resolveHost;
|
|
186
|
+
this.requestFactory = options.requestFactory ?? node_https.request;
|
|
187
|
+
this.timeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
188
|
+
if (!Number.isInteger(this.timeoutMs) || this.timeoutMs < 1 || this.timeoutMs > 6e4) throw new ScreenConnectError("ScreenConnect request timeout must be between 1 and 60000 milliseconds.");
|
|
189
|
+
}
|
|
190
|
+
async call(method, parameters) {
|
|
191
|
+
if (!Object.hasOwn(methods, method)) throw new ScreenConnectError("Unsupported ScreenConnect operation.");
|
|
192
|
+
const operation = methods[method];
|
|
193
|
+
const validated = operation.parameters.safeParse(parameters);
|
|
194
|
+
if (!validated.success) throw new ScreenConnectError("Invalid parameters for the ScreenConnect operation.");
|
|
195
|
+
const body = JSON.stringify(validated.data);
|
|
196
|
+
if (Buffer.byteLength(body) > 131072) throw new ScreenConnectError("ScreenConnect request is too large.");
|
|
197
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
198
|
+
const url = new URL(`${EXTENSION_PATH}${method}`, this.siteUrl);
|
|
199
|
+
const addresses = await resolvePublicAddresses(url.hostname, signal, this.resolveHost);
|
|
200
|
+
const result = parseResponse(await this.send(url, operation.method, body, addresses, signal), this.secret);
|
|
201
|
+
if (method === "GetSessionsByFilter" || method === "GetSessionsByName") {
|
|
202
|
+
if (!Array.isArray(result) || !result.every(isRecord)) throw new ScreenConnectError("ScreenConnect returned a malformed session list.");
|
|
203
|
+
} else if (operation.method === "POST" && method !== "CreateSession") {
|
|
204
|
+
if (result !== null) throw new ScreenConnectError("ScreenConnect returned an unexpected action response. Check session details before retrying.");
|
|
205
|
+
} else if (result !== null && !isRecord(result) && !(Array.isArray(result) && result.every(isRecord))) throw new ScreenConnectError("ScreenConnect returned malformed session data.");
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
send(url, method, body, addresses, signal) {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const fail = (error) => {
|
|
211
|
+
reject(error instanceof ScreenConnectError ? error : new ScreenConnectError(signal.aborted ? "ScreenConnect request timed out." : "ScreenConnect request failed. Verify extension settings and connectivity."));
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const request = this.requestFactory(url, {
|
|
215
|
+
method,
|
|
216
|
+
agent: false,
|
|
217
|
+
lookup: pinnedLookup(addresses),
|
|
218
|
+
signal,
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
Accept: "application/json",
|
|
222
|
+
"Accept-Encoding": "identity",
|
|
223
|
+
"Content-Length": Buffer.byteLength(body),
|
|
224
|
+
CTRLAuthHeader: this.secret,
|
|
225
|
+
Origin: this.siteUrl
|
|
226
|
+
}
|
|
227
|
+
}, (response) => {
|
|
228
|
+
readResponse(response).then(resolve, fail);
|
|
229
|
+
});
|
|
230
|
+
request.once("error", fail);
|
|
231
|
+
request.end(body);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
fail(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
/** No second DNS resolution is allowed between validation and connection. */
|
|
239
|
+
function pinnedLookup(addresses) {
|
|
240
|
+
return (_hostname, options, callback) => {
|
|
241
|
+
if (options.all) callback(null, addresses.map((entry) => ({ ...entry })));
|
|
242
|
+
else {
|
|
243
|
+
const address = addresses.find((entry) => !options.family || entry.family === options.family);
|
|
244
|
+
if (!address) callback(/* @__PURE__ */ new Error("No validated address for requested family"), "", 4);
|
|
245
|
+
else callback(null, address.address, address.family);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function readResponse(response) {
|
|
250
|
+
const status = response.statusCode ?? 0;
|
|
251
|
+
if (status < 200 || status >= 300) {
|
|
252
|
+
response.destroy();
|
|
253
|
+
if (status === 401 || status === 403) throw new ScreenConnectError("ScreenConnect rejected authentication. Check the secret, RESTfulAllowedOrigin, and extension user permissions.");
|
|
254
|
+
if (status === 404) throw new ScreenConnectError("ScreenConnect RESTful API Manager endpoint was not found. Install or update the extension.");
|
|
255
|
+
throw new ScreenConnectError(`ScreenConnect returned HTTP ${String(status)}. The operation was not retried.`);
|
|
256
|
+
}
|
|
257
|
+
const length = response.headers["content-length"];
|
|
258
|
+
if (length !== void 0 && (!/^\d+$/u.test(length) || Number(length) > 5242880)) {
|
|
259
|
+
response.destroy();
|
|
260
|
+
throw new ScreenConnectError("ScreenConnect response exceeded the 5 MiB limit. Narrow the session filter.");
|
|
261
|
+
}
|
|
262
|
+
const chunks = [];
|
|
263
|
+
let bytes = 0;
|
|
264
|
+
for await (const chunk of response) {
|
|
265
|
+
if (!(chunk instanceof Uint8Array) && typeof chunk !== "string") {
|
|
266
|
+
response.destroy();
|
|
267
|
+
throw new ScreenConnectError("ScreenConnect returned an invalid response stream.");
|
|
268
|
+
}
|
|
269
|
+
const buffer = Buffer.from(chunk);
|
|
270
|
+
bytes += buffer.byteLength;
|
|
271
|
+
if (bytes > 5242880) {
|
|
272
|
+
response.destroy();
|
|
273
|
+
throw new ScreenConnectError("ScreenConnect response exceeded the 5 MiB limit. Narrow the session filter.");
|
|
274
|
+
}
|
|
275
|
+
chunks.push(buffer);
|
|
276
|
+
}
|
|
277
|
+
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
Object.defineProperty(exports, "EXTENSION_PATH", {
|
|
281
|
+
enumerable: true,
|
|
282
|
+
get: function() {
|
|
283
|
+
return EXTENSION_PATH;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
Object.defineProperty(exports, "PROVIDER", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: function() {
|
|
289
|
+
return PROVIDER;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
Object.defineProperty(exports, "ScreenConnectClient", {
|
|
293
|
+
enumerable: true,
|
|
294
|
+
get: function() {
|
|
295
|
+
return ScreenConnectClient;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
Object.defineProperty(exports, "ScreenConnectError", {
|
|
299
|
+
enumerable: true,
|
|
300
|
+
get: function() {
|
|
301
|
+
return ScreenConnectError;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
Object.defineProperty(exports, "connectionIdField", {
|
|
305
|
+
enumerable: true,
|
|
306
|
+
get: function() {
|
|
307
|
+
return connectionIdField;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
Object.defineProperty(exports, "customPropertiesField", {
|
|
311
|
+
enumerable: true,
|
|
312
|
+
get: function() {
|
|
313
|
+
return customPropertiesField;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
Object.defineProperty(exports, "isRecord", {
|
|
317
|
+
enumerable: true,
|
|
318
|
+
get: function() {
|
|
319
|
+
return isRecord;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
Object.defineProperty(exports, "normalizeSiteUrl", {
|
|
323
|
+
enumerable: true,
|
|
324
|
+
get: function() {
|
|
325
|
+
return normalizeSiteUrl;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
Object.defineProperty(exports, "parseCredentials", {
|
|
329
|
+
enumerable: true,
|
|
330
|
+
get: function() {
|
|
331
|
+
return parseCredentials;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
Object.defineProperty(exports, "pinnedLookup", {
|
|
335
|
+
enumerable: true,
|
|
336
|
+
get: function() {
|
|
337
|
+
return pinnedLookup;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
Object.defineProperty(exports, "safeErrorMessage", {
|
|
341
|
+
enumerable: true,
|
|
342
|
+
get: function() {
|
|
343
|
+
return safeErrorMessage;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
Object.defineProperty(exports, "sessionIdField", {
|
|
347
|
+
enumerable: true,
|
|
348
|
+
get: function() {
|
|
349
|
+
return sessionIdField;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
Object.defineProperty(exports, "textField", {
|
|
353
|
+
enumerable: true,
|
|
354
|
+
get: function() {
|
|
355
|
+
return textField;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ScreenConnectClientOptions, i as ScreenConnectClient, n as RequestFactory, o as ScreenConnectMethod, r as ScreenConnectApi, s as pinnedLookup, t as EXTENSION_PATH } from "./screenconnect-client.cjs";
|
|
2
|
+
export { EXTENSION_PATH, RequestFactory, ScreenConnectApi, ScreenConnectClient, ScreenConnectClientOptions, ScreenConnectMethod, pinnedLookup };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ScreenConnectClientOptions, i as ScreenConnectClient, n as RequestFactory, o as ScreenConnectMethod, r as ScreenConnectApi, s as pinnedLookup, t as EXTENSION_PATH } from "./screenconnect-client.js";
|
|
2
|
+
export { EXTENSION_PATH, RequestFactory, ScreenConnectApi, ScreenConnectClient, ScreenConnectClientOptions, ScreenConnectMethod, pinnedLookup };
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { BlockList, isIP } from "node:net";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { request } from "node:https";
|
|
5
|
+
//#region src/boundary.ts
|
|
6
|
+
const PROVIDER = "connectwise-screenconnect";
|
|
7
|
+
const connectionIdField = z.string().regex(/^con_[A-Za-z0-9_-]{1,128}$/u).describe("Exact connectionId from screenconnect_list_connections. Selects one authorized ScreenConnect instance.");
|
|
8
|
+
const sessionIdField = z.uuid();
|
|
9
|
+
const textField = (max = 4096) => z.string().min(1).max(max);
|
|
10
|
+
const customPropertiesField = z.array(z.string().max(4096)).max(8);
|
|
11
|
+
var ScreenConnectError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ScreenConnectError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Only errors created at this boundary are safe to display or capture. */
|
|
18
|
+
function safeErrorMessage(error) {
|
|
19
|
+
return error instanceof ScreenConnectError ? error.message : "ScreenConnect operation failed. Check the connection and retry reads; reconcile mutations before retrying.";
|
|
20
|
+
}
|
|
21
|
+
function normalizeSiteUrl(value) {
|
|
22
|
+
if (typeof value !== "string" || value.length > 2048) throw new ScreenConnectError("ScreenConnect site URL is invalid.");
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(value);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ScreenConnectError("ScreenConnect site URL is invalid.");
|
|
28
|
+
}
|
|
29
|
+
const host = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
30
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || isIP(host.replace(/^\[|\]$/gu, "")) || !host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host === "metadata.google.internal" || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(host)) throw new ScreenConnectError("ScreenConnect requires a public HTTPS origin without a path or credentials.");
|
|
31
|
+
url.hostname = host;
|
|
32
|
+
return url.origin;
|
|
33
|
+
}
|
|
34
|
+
function parseCredentials(value, connectionId) {
|
|
35
|
+
const parsed = z.object({
|
|
36
|
+
provider: z.literal(PROVIDER),
|
|
37
|
+
connectionId: connectionIdField,
|
|
38
|
+
siteUrl: z.string(),
|
|
39
|
+
authenticationSecret: z.string().min(1).max(16384).regex(/^[\x21-\x7e]+$/u),
|
|
40
|
+
apiMode: z.literal("restful-api-manager")
|
|
41
|
+
}).safeParse(value);
|
|
42
|
+
if (!parsed.success || parsed.data.connectionId !== connectionId) throw new ScreenConnectError("The selected connection does not contain valid ScreenConnect credentials. Reconnect it in Alfe.");
|
|
43
|
+
return {
|
|
44
|
+
siteUrl: normalizeSiteUrl(parsed.data.siteUrl),
|
|
45
|
+
authenticationSecret: parsed.data.authenticationSecret
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const blocked = new BlockList();
|
|
49
|
+
for (const [address, prefix] of [
|
|
50
|
+
["0.0.0.0", 8],
|
|
51
|
+
["10.0.0.0", 8],
|
|
52
|
+
["100.64.0.0", 10],
|
|
53
|
+
["127.0.0.0", 8],
|
|
54
|
+
["169.254.0.0", 16],
|
|
55
|
+
["172.16.0.0", 12],
|
|
56
|
+
["192.0.0.0", 24],
|
|
57
|
+
["192.0.2.0", 24],
|
|
58
|
+
["192.168.0.0", 16],
|
|
59
|
+
["198.18.0.0", 15],
|
|
60
|
+
["198.51.100.0", 24],
|
|
61
|
+
["203.0.113.0", 24],
|
|
62
|
+
["224.0.0.0", 4],
|
|
63
|
+
["240.0.0.0", 4]
|
|
64
|
+
]) blocked.addSubnet(address, prefix, "ipv4");
|
|
65
|
+
for (const [address, prefix] of [
|
|
66
|
+
["::", 96],
|
|
67
|
+
["64:ff9b::", 96],
|
|
68
|
+
["64:ff9b:1::", 48],
|
|
69
|
+
["100::", 64],
|
|
70
|
+
["2001::", 32],
|
|
71
|
+
["2001:db8::", 32],
|
|
72
|
+
["2002::", 16],
|
|
73
|
+
["fc00::", 7],
|
|
74
|
+
["fe80::", 10],
|
|
75
|
+
["ff00::", 8]
|
|
76
|
+
]) blocked.addSubnet(address, prefix, "ipv6");
|
|
77
|
+
async function resolvePublicAddresses(hostname, signal, resolver = (host) => lookup(host, { all: true })) {
|
|
78
|
+
let onAbort;
|
|
79
|
+
try {
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
const addresses = await Promise.race([resolver(hostname), new Promise((_resolve, reject) => {
|
|
82
|
+
onAbort = () => {
|
|
83
|
+
reject(new ScreenConnectError("ScreenConnect request timed out."));
|
|
84
|
+
};
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
})]);
|
|
87
|
+
if (addresses.length === 0 || addresses.length > 64 || addresses.some(({ address, family }) => {
|
|
88
|
+
const actualFamily = isIP(address);
|
|
89
|
+
return actualFamily === 0 || actualFamily !== family || family === 6 && address.toLowerCase().startsWith("::ffff:") || blocked.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
90
|
+
})) throw new ScreenConnectError("ScreenConnect hostname must resolve only to public addresses.");
|
|
91
|
+
return addresses;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof ScreenConnectError) throw error;
|
|
94
|
+
throw new ScreenConnectError("ScreenConnect hostname could not be safely resolved.");
|
|
95
|
+
} finally {
|
|
96
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Avoid provider error envelopes being reported as successful session data. */
|
|
100
|
+
function parseResponse(text, secret) {
|
|
101
|
+
if (!text.trim()) return null;
|
|
102
|
+
const redacted = text.split(secret).join("[REDACTED]");
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = JSON.parse(redacted);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new ScreenConnectError("ScreenConnect returned invalid JSON.");
|
|
108
|
+
}
|
|
109
|
+
if (isRecord(result) && ("error" in result || "Error" in result || "ExceptionType" in result || "StackTrace" in result)) throw new ScreenConnectError("ScreenConnect rejected the operation. Check extension settings and permissions.");
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function isRecord(value) {
|
|
113
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/screenconnect-client.ts
|
|
117
|
+
const EXTENSION_PATH = "/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/";
|
|
118
|
+
/** Positional contracts from the official RESTful API Manager reference. */
|
|
119
|
+
const methods = {
|
|
120
|
+
GetSessionsByFilter: {
|
|
121
|
+
method: "GET",
|
|
122
|
+
parameters: z.tuple([textField(2048)])
|
|
123
|
+
},
|
|
124
|
+
GetSessionsByName: {
|
|
125
|
+
method: "GET",
|
|
126
|
+
parameters: z.tuple([textField(256)])
|
|
127
|
+
},
|
|
128
|
+
GetSessionBySessionID: {
|
|
129
|
+
method: "GET",
|
|
130
|
+
parameters: z.tuple([sessionIdField])
|
|
131
|
+
},
|
|
132
|
+
GetSessionDetailsBySessionID: {
|
|
133
|
+
method: "GET",
|
|
134
|
+
parameters: z.tuple([sessionIdField])
|
|
135
|
+
},
|
|
136
|
+
CreateSession: {
|
|
137
|
+
method: "POST",
|
|
138
|
+
parameters: z.tuple([
|
|
139
|
+
z.enum(["Support", "Meeting"]),
|
|
140
|
+
textField(256),
|
|
141
|
+
z.boolean(),
|
|
142
|
+
z.string().max(128),
|
|
143
|
+
customPropertiesField
|
|
144
|
+
])
|
|
145
|
+
},
|
|
146
|
+
UpdateSessionName: {
|
|
147
|
+
method: "POST",
|
|
148
|
+
parameters: z.tuple([sessionIdField, textField(256)])
|
|
149
|
+
},
|
|
150
|
+
UpdateSessionCustomProperties: {
|
|
151
|
+
method: "POST",
|
|
152
|
+
parameters: z.tuple([sessionIdField, customPropertiesField])
|
|
153
|
+
},
|
|
154
|
+
AddNoteToSession: {
|
|
155
|
+
method: "POST",
|
|
156
|
+
parameters: z.tuple([sessionIdField, textField(16384)])
|
|
157
|
+
},
|
|
158
|
+
SendMessageToSession: {
|
|
159
|
+
method: "POST",
|
|
160
|
+
parameters: z.tuple([
|
|
161
|
+
sessionIdField,
|
|
162
|
+
textField(256),
|
|
163
|
+
textField(16384)
|
|
164
|
+
])
|
|
165
|
+
},
|
|
166
|
+
SendCommandToSession: {
|
|
167
|
+
method: "POST",
|
|
168
|
+
parameters: z.tuple([sessionIdField, textField(32768)])
|
|
169
|
+
},
|
|
170
|
+
SendToolboxItemToSession: {
|
|
171
|
+
method: "POST",
|
|
172
|
+
parameters: z.tuple([sessionIdField, textField(512)])
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var ScreenConnectClient = class {
|
|
176
|
+
siteUrl;
|
|
177
|
+
secret;
|
|
178
|
+
resolveHost;
|
|
179
|
+
requestFactory;
|
|
180
|
+
timeoutMs;
|
|
181
|
+
constructor(options) {
|
|
182
|
+
this.siteUrl = normalizeSiteUrl(options.siteUrl);
|
|
183
|
+
if (typeof options.authenticationSecret !== "string" || !/^[\x21-\x7e]{1,16384}$/u.test(options.authenticationSecret)) throw new ScreenConnectError("ScreenConnect authentication secret is invalid.");
|
|
184
|
+
this.secret = options.authenticationSecret;
|
|
185
|
+
this.resolveHost = options.resolveHost;
|
|
186
|
+
this.requestFactory = options.requestFactory ?? request;
|
|
187
|
+
this.timeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
188
|
+
if (!Number.isInteger(this.timeoutMs) || this.timeoutMs < 1 || this.timeoutMs > 6e4) throw new ScreenConnectError("ScreenConnect request timeout must be between 1 and 60000 milliseconds.");
|
|
189
|
+
}
|
|
190
|
+
async call(method, parameters) {
|
|
191
|
+
if (!Object.hasOwn(methods, method)) throw new ScreenConnectError("Unsupported ScreenConnect operation.");
|
|
192
|
+
const operation = methods[method];
|
|
193
|
+
const validated = operation.parameters.safeParse(parameters);
|
|
194
|
+
if (!validated.success) throw new ScreenConnectError("Invalid parameters for the ScreenConnect operation.");
|
|
195
|
+
const body = JSON.stringify(validated.data);
|
|
196
|
+
if (Buffer.byteLength(body) > 131072) throw new ScreenConnectError("ScreenConnect request is too large.");
|
|
197
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
198
|
+
const url = new URL(`${EXTENSION_PATH}${method}`, this.siteUrl);
|
|
199
|
+
const addresses = await resolvePublicAddresses(url.hostname, signal, this.resolveHost);
|
|
200
|
+
const result = parseResponse(await this.send(url, operation.method, body, addresses, signal), this.secret);
|
|
201
|
+
if (method === "GetSessionsByFilter" || method === "GetSessionsByName") {
|
|
202
|
+
if (!Array.isArray(result) || !result.every(isRecord)) throw new ScreenConnectError("ScreenConnect returned a malformed session list.");
|
|
203
|
+
} else if (operation.method === "POST" && method !== "CreateSession") {
|
|
204
|
+
if (result !== null) throw new ScreenConnectError("ScreenConnect returned an unexpected action response. Check session details before retrying.");
|
|
205
|
+
} else if (result !== null && !isRecord(result) && !(Array.isArray(result) && result.every(isRecord))) throw new ScreenConnectError("ScreenConnect returned malformed session data.");
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
send(url, method, body, addresses, signal) {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const fail = (error) => {
|
|
211
|
+
reject(error instanceof ScreenConnectError ? error : new ScreenConnectError(signal.aborted ? "ScreenConnect request timed out." : "ScreenConnect request failed. Verify extension settings and connectivity."));
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const request = this.requestFactory(url, {
|
|
215
|
+
method,
|
|
216
|
+
agent: false,
|
|
217
|
+
lookup: pinnedLookup(addresses),
|
|
218
|
+
signal,
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
Accept: "application/json",
|
|
222
|
+
"Accept-Encoding": "identity",
|
|
223
|
+
"Content-Length": Buffer.byteLength(body),
|
|
224
|
+
CTRLAuthHeader: this.secret,
|
|
225
|
+
Origin: this.siteUrl
|
|
226
|
+
}
|
|
227
|
+
}, (response) => {
|
|
228
|
+
readResponse(response).then(resolve, fail);
|
|
229
|
+
});
|
|
230
|
+
request.once("error", fail);
|
|
231
|
+
request.end(body);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
fail(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
/** No second DNS resolution is allowed between validation and connection. */
|
|
239
|
+
function pinnedLookup(addresses) {
|
|
240
|
+
return (_hostname, options, callback) => {
|
|
241
|
+
if (options.all) callback(null, addresses.map((entry) => ({ ...entry })));
|
|
242
|
+
else {
|
|
243
|
+
const address = addresses.find((entry) => !options.family || entry.family === options.family);
|
|
244
|
+
if (!address) callback(/* @__PURE__ */ new Error("No validated address for requested family"), "", 4);
|
|
245
|
+
else callback(null, address.address, address.family);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function readResponse(response) {
|
|
250
|
+
const status = response.statusCode ?? 0;
|
|
251
|
+
if (status < 200 || status >= 300) {
|
|
252
|
+
response.destroy();
|
|
253
|
+
if (status === 401 || status === 403) throw new ScreenConnectError("ScreenConnect rejected authentication. Check the secret, RESTfulAllowedOrigin, and extension user permissions.");
|
|
254
|
+
if (status === 404) throw new ScreenConnectError("ScreenConnect RESTful API Manager endpoint was not found. Install or update the extension.");
|
|
255
|
+
throw new ScreenConnectError(`ScreenConnect returned HTTP ${String(status)}. The operation was not retried.`);
|
|
256
|
+
}
|
|
257
|
+
const length = response.headers["content-length"];
|
|
258
|
+
if (length !== void 0 && (!/^\d+$/u.test(length) || Number(length) > 5242880)) {
|
|
259
|
+
response.destroy();
|
|
260
|
+
throw new ScreenConnectError("ScreenConnect response exceeded the 5 MiB limit. Narrow the session filter.");
|
|
261
|
+
}
|
|
262
|
+
const chunks = [];
|
|
263
|
+
let bytes = 0;
|
|
264
|
+
for await (const chunk of response) {
|
|
265
|
+
if (!(chunk instanceof Uint8Array) && typeof chunk !== "string") {
|
|
266
|
+
response.destroy();
|
|
267
|
+
throw new ScreenConnectError("ScreenConnect returned an invalid response stream.");
|
|
268
|
+
}
|
|
269
|
+
const buffer = Buffer.from(chunk);
|
|
270
|
+
bytes += buffer.byteLength;
|
|
271
|
+
if (bytes > 5242880) {
|
|
272
|
+
response.destroy();
|
|
273
|
+
throw new ScreenConnectError("ScreenConnect response exceeded the 5 MiB limit. Narrow the session filter.");
|
|
274
|
+
}
|
|
275
|
+
chunks.push(buffer);
|
|
276
|
+
}
|
|
277
|
+
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { ScreenConnectError as a, isRecord as c, safeErrorMessage as d, sessionIdField as f, PROVIDER as i, normalizeSiteUrl as l, ScreenConnectClient as n, connectionIdField as o, textField as p, pinnedLookup as r, customPropertiesField as s, EXTENSION_PATH as t, parseCredentials as u };
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_server = require("./server2.cjs");
|
|
3
|
+
exports.SERVER_NAME = require_server.SERVER_NAME;
|
|
4
|
+
exports.SERVER_VERSION = require_server.SERVER_VERSION;
|
|
5
|
+
exports.createServer = require_server.createServer;
|
|
6
|
+
exports.startServer = require_server.startServer;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { r as ScreenConnectApi } from "./screenconnect-client.cjs";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
//#region src/tools.d.ts
|
|
5
|
+
interface ConnectApi {
|
|
6
|
+
getConnectProviderAccounts(provider: string): Promise<unknown>;
|
|
7
|
+
getConnectionCredentials(connectionId: string): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface ToolDependencies {
|
|
10
|
+
createClient?: (credentials: {
|
|
11
|
+
siteUrl: string;
|
|
12
|
+
authenticationSecret: string;
|
|
13
|
+
}) => ScreenConnectApi;
|
|
14
|
+
/** Receives only a static sanitized error and operation name, never credentials or provider bodies. */
|
|
15
|
+
captureError?: (error: Error, operation: string) => void;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/server.d.ts
|
|
19
|
+
declare const SERVER_VERSION: string;
|
|
20
|
+
declare const SERVER_NAME = "connectwise-screenconnect-mcp";
|
|
21
|
+
declare function createServer(api: ConnectApi, dependencies?: ToolDependencies): McpServer;
|
|
22
|
+
declare function startServer(api?: ConnectApi, dependencies?: ToolDependencies): Promise<McpServer>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { type ConnectApi, SERVER_NAME, SERVER_VERSION, type ToolDependencies, createServer, startServer };
|