@alfe.ai/connectwise-psa-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 +61 -0
- package/dist/bin.cjs +17 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +18 -0
- package/dist/server.cjs +7 -0
- package/dist/server.d.cts +102 -0
- package/dist/server.d.ts +102 -0
- package/dist/server.js +2 -0
- package/dist/server2.cjs +524 -0
- package/dist/server2.js +489 -0
- package/package.json +39 -0
package/dist/server2.cjs
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
let node_module = require("node:module");
|
|
2
|
+
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
3
|
+
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
4
|
+
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
5
|
+
let _alfe_ai_config = require("@alfe.ai/config");
|
|
6
|
+
let zod = require("zod");
|
|
7
|
+
let node_dns_promises = require("node:dns/promises");
|
|
8
|
+
let node_https = require("node:https");
|
|
9
|
+
let node_net = require("node:net");
|
|
10
|
+
//#region src/boundary.ts
|
|
11
|
+
/** Only errors constructed by this package may cross the MCP boundary. */
|
|
12
|
+
var PsaError = class extends Error {
|
|
13
|
+
name = "PsaError";
|
|
14
|
+
};
|
|
15
|
+
function safeErrorMessage(error) {
|
|
16
|
+
return error instanceof PsaError ? error.message : "ConnectWise PSA request failed. Check the selected connection and retry a read before repeating any write.";
|
|
17
|
+
}
|
|
18
|
+
function boundedString(value, max, field) {
|
|
19
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max || Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) throw new PsaError(`Invalid ${field}.`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function record(value) {
|
|
23
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new PsaError("ConnectWise PSA returned an invalid response.");
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
async function withDeadline(operation, milliseconds = 2e4) {
|
|
27
|
+
let timer;
|
|
28
|
+
try {
|
|
29
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
reject(new PsaError("ConnectWise PSA request timed out. Check whether a write succeeded before repeating it."));
|
|
32
|
+
}, milliseconds);
|
|
33
|
+
})]);
|
|
34
|
+
} finally {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const MAX_TIMEOUT_MS = 2e4;
|
|
39
|
+
const blocked = new node_net.BlockList();
|
|
40
|
+
for (const [network, prefix] of [
|
|
41
|
+
["0.0.0.0", 8],
|
|
42
|
+
["10.0.0.0", 8],
|
|
43
|
+
["100.64.0.0", 10],
|
|
44
|
+
["127.0.0.0", 8],
|
|
45
|
+
["169.254.0.0", 16],
|
|
46
|
+
["172.16.0.0", 12],
|
|
47
|
+
["192.0.0.0", 24],
|
|
48
|
+
["192.0.2.0", 24],
|
|
49
|
+
["192.168.0.0", 16],
|
|
50
|
+
["198.18.0.0", 15],
|
|
51
|
+
["198.51.100.0", 24],
|
|
52
|
+
["203.0.113.0", 24],
|
|
53
|
+
["224.0.0.0", 4],
|
|
54
|
+
["240.0.0.0", 4]
|
|
55
|
+
]) blocked.addSubnet(network, prefix, "ipv4");
|
|
56
|
+
for (const [network, prefix] of [
|
|
57
|
+
["::", 96],
|
|
58
|
+
["64:ff9b::", 96],
|
|
59
|
+
["64:ff9b:1::", 48],
|
|
60
|
+
["100::", 64],
|
|
61
|
+
["2001::", 32],
|
|
62
|
+
["2001:db8::", 32],
|
|
63
|
+
["2002::", 16],
|
|
64
|
+
["fc00::", 7],
|
|
65
|
+
["fe80::", 10],
|
|
66
|
+
["ff00::", 8]
|
|
67
|
+
]) blocked.addSubnet(network, prefix, "ipv6");
|
|
68
|
+
function normalizeSiteUrl(value) {
|
|
69
|
+
const raw = boundedString(value, 2048, "PSA site URL");
|
|
70
|
+
let url;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new PsaError("Invalid PSA site URL.");
|
|
75
|
+
}
|
|
76
|
+
const host = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
77
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || (0, node_net.isIP)(host.replace(/^\[|\]$/gu, "")) !== 0 || !host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host === "metadata.google.internal") throw new PsaError("PSA requires a public HTTPS origin without a path, userinfo, query, or fragment.");
|
|
78
|
+
url.hostname = host;
|
|
79
|
+
return url;
|
|
80
|
+
}
|
|
81
|
+
function isPublicAddress(value) {
|
|
82
|
+
const family = (0, node_net.isIP)(value.address);
|
|
83
|
+
return family !== 0 && family === value.family && !blocked.check(value.address, family === 4 ? "ipv4" : "ipv6");
|
|
84
|
+
}
|
|
85
|
+
/** Re-resolve on every call and pin the socket to an address from that answer. */
|
|
86
|
+
async function resolvePublicAddress(url, resolve = (host) => (0, node_dns_promises.lookup)(host, { all: true })) {
|
|
87
|
+
let addresses;
|
|
88
|
+
try {
|
|
89
|
+
addresses = await withDeadline(resolve(url.hostname), 5e3);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new PsaError("Unable to resolve a public PSA server address.");
|
|
92
|
+
}
|
|
93
|
+
if (addresses.length === 0 || addresses.length > 64 || addresses.some((address) => !isPublicAddress(address))) throw new PsaError("The PSA server must resolve only to public IP addresses.");
|
|
94
|
+
return addresses[0];
|
|
95
|
+
}
|
|
96
|
+
/** Node HTTPS never follows redirects. TLS is verified against the original hostname. */
|
|
97
|
+
const sendRequest = (input) => new Promise((resolve, reject) => {
|
|
98
|
+
const req = (0, node_https.request)(input.url, {
|
|
99
|
+
method: input.method,
|
|
100
|
+
headers: input.headers,
|
|
101
|
+
agent: false,
|
|
102
|
+
family: input.address.family,
|
|
103
|
+
lookup: (_hostname, options, callback) => {
|
|
104
|
+
if (options.all) callback(null, [input.address]);
|
|
105
|
+
else callback(null, input.address.address, input.address.family);
|
|
106
|
+
}
|
|
107
|
+
}, (response) => {
|
|
108
|
+
const status = response.statusCode ?? 0;
|
|
109
|
+
if (status < 200 || status >= 300) {
|
|
110
|
+
response.destroy();
|
|
111
|
+
reject(new PsaError(`ConnectWise PSA returned HTTP ${String(status)}. ${status === 401 || status === 403 ? "Verify the API member keys, security role, company ID, and Alfe ClientID." : status === 429 ? "The PSA rate limit was reached; retry later." : status >= 300 && status < 400 ? "Redirects are refused; use the final PSA API origin." : "Check the record, service board, and required fields."}`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (Number(response.headers["content-length"] ?? 0) > 2097152) {
|
|
115
|
+
response.destroy();
|
|
116
|
+
reject(new PsaError("PSA response is too large; narrow the query or reduce pageSize."));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let bytes = 0;
|
|
121
|
+
response.on("data", (chunk) => {
|
|
122
|
+
bytes += chunk.length;
|
|
123
|
+
if (bytes > 2097152) response.destroy(new PsaError("PSA response is too large; narrow the query or reduce pageSize."));
|
|
124
|
+
else chunks.push(chunk);
|
|
125
|
+
});
|
|
126
|
+
response.once("error", () => {
|
|
127
|
+
reject(new PsaError("Unable to read the PSA response. Check whether a write succeeded before repeating it."));
|
|
128
|
+
});
|
|
129
|
+
response.once("end", () => {
|
|
130
|
+
try {
|
|
131
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
132
|
+
resolve(text.length === 0 && status === 204 ? null : JSON.parse(text));
|
|
133
|
+
} catch {
|
|
134
|
+
reject(new PsaError("ConnectWise PSA returned invalid JSON."));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
req.destroy(new PsaError("PSA request timed out. Check whether a write succeeded before repeating it."));
|
|
140
|
+
}, MAX_TIMEOUT_MS);
|
|
141
|
+
req.once("close", () => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
});
|
|
144
|
+
req.once("error", () => {
|
|
145
|
+
reject(new PsaError("Unable to reach ConnectWise PSA. Check whether a write succeeded before repeating it."));
|
|
146
|
+
});
|
|
147
|
+
req.end(input.body);
|
|
148
|
+
});
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/client.ts
|
|
151
|
+
const PROVIDER_ID = "connectwise-psa";
|
|
152
|
+
const API_PATH = "/v4_6_release/apis/3.0";
|
|
153
|
+
const connectionIdSchema = zod.z.string().min(1).max(256).regex(/^con_[A-Za-z0-9_-]+$/u);
|
|
154
|
+
const idSchema = zod.z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
155
|
+
const listSchema = zod.z.object({
|
|
156
|
+
page: zod.z.coerce.number().int().min(1).max(1e5).default(1),
|
|
157
|
+
pageSize: zod.z.coerce.number().int().min(1).max(100).default(25),
|
|
158
|
+
conditions: zod.z.string().trim().min(1).max(2048).optional(),
|
|
159
|
+
orderBy: zod.z.string().trim().min(1).max(256).optional()
|
|
160
|
+
}).strict();
|
|
161
|
+
const createTicketSchema = zod.z.object({
|
|
162
|
+
summary: zod.z.string().trim().min(1).max(100),
|
|
163
|
+
companyId: idSchema,
|
|
164
|
+
boardId: idSchema,
|
|
165
|
+
contactId: idSchema.optional(),
|
|
166
|
+
statusId: idSchema.optional(),
|
|
167
|
+
priorityId: idSchema.optional(),
|
|
168
|
+
initialDescription: zod.z.string().min(1).max(6e4).optional()
|
|
169
|
+
}).strict();
|
|
170
|
+
const updateTicketSchema = zod.z.object({
|
|
171
|
+
summary: zod.z.string().trim().min(1).max(100).optional(),
|
|
172
|
+
statusId: idSchema.optional(),
|
|
173
|
+
priorityId: idSchema.optional(),
|
|
174
|
+
ownerId: idSchema.optional(),
|
|
175
|
+
contactId: idSchema.optional()
|
|
176
|
+
}).strict().refine((value) => Object.values(value).some((field) => field !== void 0), "At least one ticket field is required.");
|
|
177
|
+
const noteSchema = zod.z.object({
|
|
178
|
+
text: zod.z.string().min(1).max(6e4),
|
|
179
|
+
visibility: zod.z.enum([
|
|
180
|
+
"internal",
|
|
181
|
+
"discussion",
|
|
182
|
+
"resolution"
|
|
183
|
+
]).default("internal")
|
|
184
|
+
}).strict();
|
|
185
|
+
const LIST_PATHS = {
|
|
186
|
+
companies: "/company/companies",
|
|
187
|
+
contacts: "/company/contacts",
|
|
188
|
+
boards: "/service/boards",
|
|
189
|
+
priorities: "/service/priorities",
|
|
190
|
+
members: "/system/members",
|
|
191
|
+
tickets: "/service/tickets"
|
|
192
|
+
};
|
|
193
|
+
function parseCredentials(input) {
|
|
194
|
+
const value = record(input);
|
|
195
|
+
if (value.provider !== "connectwise-psa") throw new PsaError("The selected connection is not ConnectWise PSA.");
|
|
196
|
+
const companyId = boundedString(value.companyId, 256, "PSA company ID");
|
|
197
|
+
const publicKey = boundedString(value.publicKey, 1024, "PSA public key");
|
|
198
|
+
const privateKey = boundedString(value.privateKey, 4096, "PSA private key");
|
|
199
|
+
const clientId = boundedString(value.clientId, 256, "Alfe PSA ClientID");
|
|
200
|
+
if (/[+:]/u.test(companyId) || publicKey.includes(":") || clientId.trim().toLowerCase() === "placeholder") throw new PsaError("ConnectWise PSA credentials or the Alfe ClientID are invalid.");
|
|
201
|
+
return {
|
|
202
|
+
provider: PROVIDER_ID,
|
|
203
|
+
connectionId: connectionIdSchema.parse(value.connectionId),
|
|
204
|
+
siteUrl: normalizeSiteUrl(value.siteUrl).origin,
|
|
205
|
+
companyId,
|
|
206
|
+
publicKey,
|
|
207
|
+
privateKey,
|
|
208
|
+
clientId
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Direct, finite REST operations; no arbitrary URL tool and no mutation retries. */
|
|
212
|
+
var PsaClient = class {
|
|
213
|
+
credentials;
|
|
214
|
+
options;
|
|
215
|
+
constructor(credentials, options = {}) {
|
|
216
|
+
this.credentials = parseCredentials(credentials);
|
|
217
|
+
this.options = options;
|
|
218
|
+
}
|
|
219
|
+
async list(resource, input = {}) {
|
|
220
|
+
if (!Object.hasOwn(LIST_PATHS, resource)) throw new PsaError("Unknown PSA list resource.");
|
|
221
|
+
const path = LIST_PATHS[resource];
|
|
222
|
+
return this.listPath(path, input);
|
|
223
|
+
}
|
|
224
|
+
async get(resource, id) {
|
|
225
|
+
if (![
|
|
226
|
+
"companies",
|
|
227
|
+
"contacts",
|
|
228
|
+
"tickets"
|
|
229
|
+
].includes(resource)) throw new PsaError("Unknown PSA record resource.");
|
|
230
|
+
return record(await this.call(`${LIST_PATHS[resource]}/${String(idSchema.parse(id))}`));
|
|
231
|
+
}
|
|
232
|
+
async listBoardStatuses(boardId, input = {}) {
|
|
233
|
+
return this.listPath(`/service/boards/${String(idSchema.parse(boardId))}/statuses`, input);
|
|
234
|
+
}
|
|
235
|
+
async listTicketNotes(ticketId, input = {}) {
|
|
236
|
+
return this.listPath(`/service/tickets/${String(idSchema.parse(ticketId))}/notes`, input);
|
|
237
|
+
}
|
|
238
|
+
async createTicket(input) {
|
|
239
|
+
const data = createTicketSchema.parse(input);
|
|
240
|
+
return record(await this.call("/service/tickets", "POST", {
|
|
241
|
+
summary: data.summary,
|
|
242
|
+
company: { id: data.companyId },
|
|
243
|
+
board: { id: data.boardId },
|
|
244
|
+
...data.contactId === void 0 ? {} : { contact: { id: data.contactId } },
|
|
245
|
+
...data.statusId === void 0 ? {} : { status: { id: data.statusId } },
|
|
246
|
+
...data.priorityId === void 0 ? {} : { priority: { id: data.priorityId } },
|
|
247
|
+
...data.initialDescription === void 0 ? {} : { initialDescription: data.initialDescription }
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
async updateTicket(ticketId, input) {
|
|
251
|
+
const data = updateTicketSchema.parse(input);
|
|
252
|
+
const operations = [];
|
|
253
|
+
if (data.summary !== void 0) operations.push({
|
|
254
|
+
op: "replace",
|
|
255
|
+
path: "summary",
|
|
256
|
+
value: data.summary
|
|
257
|
+
});
|
|
258
|
+
for (const [key, path] of [
|
|
259
|
+
["statusId", "status"],
|
|
260
|
+
["priorityId", "priority"],
|
|
261
|
+
["ownerId", "owner"],
|
|
262
|
+
["contactId", "contact"]
|
|
263
|
+
]) if (data[key] !== void 0) operations.push({
|
|
264
|
+
op: "replace",
|
|
265
|
+
path,
|
|
266
|
+
value: { id: data[key] }
|
|
267
|
+
});
|
|
268
|
+
return record(await this.call(`/service/tickets/${String(idSchema.parse(ticketId))}`, "PATCH", operations));
|
|
269
|
+
}
|
|
270
|
+
async addTicketNote(ticketId, input) {
|
|
271
|
+
const data = noteSchema.parse(input);
|
|
272
|
+
return record(await this.call(`/service/tickets/${String(idSchema.parse(ticketId))}/notes`, "POST", {
|
|
273
|
+
text: data.text,
|
|
274
|
+
internalAnalysisFlag: data.visibility === "internal",
|
|
275
|
+
detailDescriptionFlag: data.visibility === "discussion",
|
|
276
|
+
resolutionFlag: data.visibility === "resolution"
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
async listPath(path, input) {
|
|
280
|
+
const query = listSchema.parse(input);
|
|
281
|
+
const result = await this.call(path, "GET", void 0, query);
|
|
282
|
+
if (!Array.isArray(result) || result.length > query.pageSize) throw new PsaError("PSA returned an invalid or oversized page.");
|
|
283
|
+
for (const item of result) record(item);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
async call(path, method = "GET", data, query) {
|
|
287
|
+
const url = new URL(`${API_PATH}${path}`, this.credentials.siteUrl);
|
|
288
|
+
if (query) {
|
|
289
|
+
for (const [key, value] of Object.entries(query)) if (typeof value === "string" || typeof value === "number") url.searchParams.set(key, String(value));
|
|
290
|
+
}
|
|
291
|
+
const body = data === void 0 ? void 0 : JSON.stringify(data);
|
|
292
|
+
if (body && Buffer.byteLength(body) > 131072) throw new PsaError("PSA request is too large.");
|
|
293
|
+
const address = await resolvePublicAddress(url, this.options.resolve);
|
|
294
|
+
const { companyId, publicKey, privateKey, clientId } = this.credentials;
|
|
295
|
+
const authorization = `Basic ${Buffer.from(`${companyId}+${publicKey}:${privateKey}`).toString("base64")}`;
|
|
296
|
+
const result = await withDeadline((this.options.send ?? sendRequest)({
|
|
297
|
+
url,
|
|
298
|
+
address,
|
|
299
|
+
method,
|
|
300
|
+
body,
|
|
301
|
+
headers: {
|
|
302
|
+
Authorization: authorization,
|
|
303
|
+
clientId,
|
|
304
|
+
Accept: "application/json",
|
|
305
|
+
"Content-Type": "application/json"
|
|
306
|
+
}
|
|
307
|
+
}));
|
|
308
|
+
let text = JSON.stringify(result);
|
|
309
|
+
if (Buffer.byteLength(text) > 2097152) throw new PsaError("PSA response is too large.");
|
|
310
|
+
for (const secret of [
|
|
311
|
+
authorization,
|
|
312
|
+
authorization.slice(6),
|
|
313
|
+
publicKey,
|
|
314
|
+
privateKey,
|
|
315
|
+
clientId
|
|
316
|
+
]) text = text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]");
|
|
317
|
+
return JSON.parse(text);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/tools.ts
|
|
322
|
+
const LIST_CONNECTIONS_TOOL = "connectwise_psa_list_connections";
|
|
323
|
+
const connectionIdField = connectionIdSchema.describe("Exact connectionId from connectwise_psa_list_connections. Required even when only one account is connected.");
|
|
324
|
+
const readAnnotations = {
|
|
325
|
+
readOnlyHint: true,
|
|
326
|
+
destructiveHint: false,
|
|
327
|
+
idempotentHint: true,
|
|
328
|
+
openWorldHint: true
|
|
329
|
+
};
|
|
330
|
+
const writeAnnotations = {
|
|
331
|
+
readOnlyHint: false,
|
|
332
|
+
destructiveHint: false,
|
|
333
|
+
idempotentHint: false,
|
|
334
|
+
openWorldHint: true
|
|
335
|
+
};
|
|
336
|
+
async function guarded(operation) {
|
|
337
|
+
try {
|
|
338
|
+
return { content: [{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: JSON.stringify(await operation())
|
|
341
|
+
}] };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return {
|
|
344
|
+
isError: true,
|
|
345
|
+
content: [{
|
|
346
|
+
type: "text",
|
|
347
|
+
text: safeErrorMessage(error)
|
|
348
|
+
}]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function registerTools(server, connect, options = {}) {
|
|
353
|
+
const resolve = async (connectionId) => {
|
|
354
|
+
connectionIdSchema.parse(connectionId);
|
|
355
|
+
const credentials = record(await withDeadline(connect.getConnectionCredentials(connectionId)));
|
|
356
|
+
if (credentials.connectionId !== connectionId || credentials.provider !== "connectwise-psa") throw new PsaError("The selected connection is not an accessible ConnectWise PSA connection.");
|
|
357
|
+
return new PsaClient(credentials, options);
|
|
358
|
+
};
|
|
359
|
+
server.registerTool(LIST_CONNECTIONS_TOOL, {
|
|
360
|
+
description: "List accessible ConnectWise PSA connections. Choose the exact connectionId for every subsequent tool call.",
|
|
361
|
+
inputSchema: {},
|
|
362
|
+
annotations: readAnnotations
|
|
363
|
+
}, () => guarded(async () => {
|
|
364
|
+
const result = record(await withDeadline(connect.getConnectProviderAccounts(PROVIDER_ID)));
|
|
365
|
+
if (result.provider !== "connectwise-psa" || !Array.isArray(result.accounts) || result.accounts.length > 200) throw new PsaError("Connect returned an invalid PSA connection list.");
|
|
366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
367
|
+
return { connections: result.accounts.map((raw) => {
|
|
368
|
+
const account = record(raw);
|
|
369
|
+
const connectionId = connectionIdSchema.parse(account.connectionId);
|
|
370
|
+
if (seen.has(connectionId)) throw new PsaError("Connect returned duplicate PSA connections.");
|
|
371
|
+
seen.add(connectionId);
|
|
372
|
+
return {
|
|
373
|
+
connectionId,
|
|
374
|
+
accountIdentifier: boundedString(account.accountIdentifier, 2048, "PSA account identifier"),
|
|
375
|
+
displayName: typeof account.displayName === "string" ? boundedString(account.displayName, 512, "PSA display name") : null
|
|
376
|
+
};
|
|
377
|
+
}) };
|
|
378
|
+
}));
|
|
379
|
+
for (const resource of [
|
|
380
|
+
"companies",
|
|
381
|
+
"contacts",
|
|
382
|
+
"boards",
|
|
383
|
+
"priorities",
|
|
384
|
+
"members",
|
|
385
|
+
"tickets"
|
|
386
|
+
]) server.registerTool(`connectwise_psa_list_${resource}`, {
|
|
387
|
+
description: `List one page of PSA ${resource}. Optional conditions use PSA query syntax; page starts at 1. Continue with page + 1 when a full page is returned.`,
|
|
388
|
+
inputSchema: {
|
|
389
|
+
connectionId: connectionIdField,
|
|
390
|
+
...listSchema.shape
|
|
391
|
+
},
|
|
392
|
+
annotations: readAnnotations
|
|
393
|
+
}, ({ connectionId, ...query }) => guarded(async () => (await resolve(connectionId)).list(resource, query)));
|
|
394
|
+
for (const [resource, singular] of [
|
|
395
|
+
["companies", "company"],
|
|
396
|
+
["contacts", "contact"],
|
|
397
|
+
["tickets", "ticket"]
|
|
398
|
+
]) server.registerTool(`connectwise_psa_get_${singular}`, {
|
|
399
|
+
description: `Read a PSA ${singular} by its numeric ID.`,
|
|
400
|
+
inputSchema: {
|
|
401
|
+
connectionId: connectionIdField,
|
|
402
|
+
id: idSchema
|
|
403
|
+
},
|
|
404
|
+
annotations: readAnnotations
|
|
405
|
+
}, ({ connectionId, id }) => guarded(async () => (await resolve(connectionId)).get(resource, id)));
|
|
406
|
+
server.registerTool("connectwise_psa_list_board_statuses", {
|
|
407
|
+
description: "List statuses for a specific service board before creating or changing a ticket status.",
|
|
408
|
+
inputSchema: {
|
|
409
|
+
connectionId: connectionIdField,
|
|
410
|
+
boardId: idSchema,
|
|
411
|
+
...listSchema.shape
|
|
412
|
+
},
|
|
413
|
+
annotations: readAnnotations
|
|
414
|
+
}, ({ connectionId, boardId, ...query }) => guarded(async () => (await resolve(connectionId)).listBoardStatuses(boardId, query)));
|
|
415
|
+
server.registerTool("connectwise_psa_list_ticket_notes", {
|
|
416
|
+
description: "List direct notes on a service ticket. This endpoint excludes separate time-entry and child-ticket notes.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
connectionId: connectionIdField,
|
|
419
|
+
ticketId: idSchema,
|
|
420
|
+
...listSchema.shape
|
|
421
|
+
},
|
|
422
|
+
annotations: readAnnotations
|
|
423
|
+
}, ({ connectionId, ticketId, ...query }) => guarded(async () => (await resolve(connectionId)).listTicketNotes(ticketId, query)));
|
|
424
|
+
server.registerTool("connectwise_psa_create_ticket", {
|
|
425
|
+
description: "Create a service ticket for the chosen PSA company and board. Get company/board/status IDs from list tools. A timeout can occur after creation: search for the ticket before repeating.",
|
|
426
|
+
inputSchema: {
|
|
427
|
+
connectionId: connectionIdField,
|
|
428
|
+
...createTicketSchema.shape
|
|
429
|
+
},
|
|
430
|
+
annotations: writeAnnotations
|
|
431
|
+
}, ({ connectionId, ...input }) => guarded(async () => (await resolve(connectionId)).createTicket(input)));
|
|
432
|
+
server.registerTool("connectwise_psa_update_ticket", {
|
|
433
|
+
description: "Update supplied fields of a service ticket using PSA JSON Patch. Get valid status IDs from its board. A status change may close/reopen the ticket; only apply the requested status.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
connectionId: connectionIdField,
|
|
436
|
+
ticketId: idSchema,
|
|
437
|
+
...updateTicketSchema.shape
|
|
438
|
+
},
|
|
439
|
+
annotations: {
|
|
440
|
+
...writeAnnotations,
|
|
441
|
+
idempotentHint: true,
|
|
442
|
+
destructiveHint: true
|
|
443
|
+
}
|
|
444
|
+
}, ({ connectionId, ticketId, ...input }) => guarded(async () => (await resolve(connectionId)).updateTicket(ticketId, input)));
|
|
445
|
+
server.registerTool("connectwise_psa_add_ticket_note", {
|
|
446
|
+
description: "Append a note to a service ticket. Internal is the default. Discussion/resolution notes may be visible to customers; choose that visibility only when requested. Check notes before retrying an ambiguous failure.",
|
|
447
|
+
inputSchema: {
|
|
448
|
+
connectionId: connectionIdField,
|
|
449
|
+
ticketId: idSchema,
|
|
450
|
+
...noteSchema.shape
|
|
451
|
+
},
|
|
452
|
+
annotations: writeAnnotations
|
|
453
|
+
}, ({ connectionId, ticketId, ...input }) => guarded(async () => (await resolve(connectionId)).addTicketNote(ticketId, input)));
|
|
454
|
+
}
|
|
455
|
+
const SERVER_VERSION = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json").version;
|
|
456
|
+
const SERVER_NAME = "connectwise-psa";
|
|
457
|
+
function createServer(connect, options = {}) {
|
|
458
|
+
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
459
|
+
name: SERVER_NAME,
|
|
460
|
+
version: SERVER_VERSION
|
|
461
|
+
});
|
|
462
|
+
registerTools(server, connect, options);
|
|
463
|
+
return server;
|
|
464
|
+
}
|
|
465
|
+
function configuredClient() {
|
|
466
|
+
const config = (0, _alfe_ai_config.resolveConfig)();
|
|
467
|
+
const url = new URL(config.apiUrl);
|
|
468
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && [
|
|
469
|
+
"localhost",
|
|
470
|
+
"127.0.0.1",
|
|
471
|
+
"[::1]"
|
|
472
|
+
].includes(url.hostname)) || url.username || url.password || url.search || url.hash) throw new PsaError("Configure a valid Alfe API URL with alfe login.");
|
|
473
|
+
return new _alfe_ai_agent_api_client.AgentApiClient({
|
|
474
|
+
apiUrl: config.apiUrl,
|
|
475
|
+
apiKey: config.apiKey
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function main(connect = configuredClient(), transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport()) {
|
|
479
|
+
const server = createServer(connect);
|
|
480
|
+
try {
|
|
481
|
+
await server.connect(transport);
|
|
482
|
+
return server;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
await server.close().catch(() => void 0);
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
Object.defineProperty(exports, "PsaClient", {
|
|
490
|
+
enumerable: true,
|
|
491
|
+
get: function() {
|
|
492
|
+
return PsaClient;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
Object.defineProperty(exports, "SERVER_NAME", {
|
|
496
|
+
enumerable: true,
|
|
497
|
+
get: function() {
|
|
498
|
+
return SERVER_NAME;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
Object.defineProperty(exports, "SERVER_VERSION", {
|
|
502
|
+
enumerable: true,
|
|
503
|
+
get: function() {
|
|
504
|
+
return SERVER_VERSION;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
Object.defineProperty(exports, "createServer", {
|
|
508
|
+
enumerable: true,
|
|
509
|
+
get: function() {
|
|
510
|
+
return createServer;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
Object.defineProperty(exports, "main", {
|
|
514
|
+
enumerable: true,
|
|
515
|
+
get: function() {
|
|
516
|
+
return main;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
Object.defineProperty(exports, "safeErrorMessage", {
|
|
520
|
+
enumerable: true,
|
|
521
|
+
get: function() {
|
|
522
|
+
return safeErrorMessage;
|
|
523
|
+
}
|
|
524
|
+
});
|