@aveq-research/surfmeter-mcp 0.2.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/LICENSE +21 -0
- package/README.md +127 -0
- package/build/client.d.ts +25 -0
- package/build/client.js +102 -0
- package/build/client.js.map +1 -0
- package/build/config.d.ts +7 -0
- package/build/config.js +46 -0
- package/build/config.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +17 -0
- package/build/index.js.map +1 -0
- package/build/result.d.ts +3 -0
- package/build/result.js +27 -0
- package/build/result.js.map +1 -0
- package/build/server.d.ts +3 -0
- package/build/server.js +14 -0
- package/build/server.js.map +1 -0
- package/build/tools.d.ts +3 -0
- package/build/tools.js +988 -0
- package/build/tools.js.map +1 -0
- package/package.json +41 -0
package/build/tools.js
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { errorResult, successResult } from "./result.js";
|
|
3
|
+
const actionCatalogs = new WeakMap();
|
|
4
|
+
const promotedActions = new Set([
|
|
5
|
+
"surfmeter_search_measurements",
|
|
6
|
+
"surfmeter_search_anomalies",
|
|
7
|
+
"surfmeter_update_client",
|
|
8
|
+
]);
|
|
9
|
+
function registerActive(server, options) {
|
|
10
|
+
const callback = async (args) => {
|
|
11
|
+
try {
|
|
12
|
+
return successResult(await options.handler(args));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return errorResult(error);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
server.registerTool(options.name, {
|
|
19
|
+
description: options.description,
|
|
20
|
+
inputSchema: options.schema,
|
|
21
|
+
annotations: {
|
|
22
|
+
readOnlyHint: options.readOnly,
|
|
23
|
+
destructiveHint: options.destructive ?? false,
|
|
24
|
+
idempotentHint: options.readOnly,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
},
|
|
27
|
+
}, callback);
|
|
28
|
+
}
|
|
29
|
+
function register(server, options) {
|
|
30
|
+
const catalog = actionCatalogs.get(server);
|
|
31
|
+
if (!catalog) {
|
|
32
|
+
throw new Error("Surfmeter action catalog was not initialized");
|
|
33
|
+
}
|
|
34
|
+
catalog.set(options.name, {
|
|
35
|
+
category: options.readOnly
|
|
36
|
+
? "read"
|
|
37
|
+
: options.destructive
|
|
38
|
+
? "destructive"
|
|
39
|
+
: "write",
|
|
40
|
+
description: options.description,
|
|
41
|
+
handler: options.handler,
|
|
42
|
+
name: options.name,
|
|
43
|
+
schema: options.schema,
|
|
44
|
+
});
|
|
45
|
+
if (promotedActions.has(options.name)) {
|
|
46
|
+
registerActive(server, options);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const id = z
|
|
50
|
+
.union([z.string().min(1), z.number().int().nonnegative()])
|
|
51
|
+
.describe("Rails numeric ID or UUID, where the endpoint supports UUIDs");
|
|
52
|
+
const numericId = z.number().int().positive();
|
|
53
|
+
const nullableString = z.string().nullable().optional();
|
|
54
|
+
const role = z.enum(["org_admin", "admin", "editor", "viewer"]);
|
|
55
|
+
const keyKind = z.enum(["export", "client_analytics", "client_admin"]);
|
|
56
|
+
const jsonObject = z.record(z.string(), z.unknown());
|
|
57
|
+
const searchBody = z
|
|
58
|
+
.union([jsonObject, z.string().min(2)])
|
|
59
|
+
.describe("Elasticsearch Query DSL object (preferred) or JSON string. Use .keyword for exact text matching.");
|
|
60
|
+
const searchOptions = {
|
|
61
|
+
body: searchBody,
|
|
62
|
+
source_only: z.boolean().optional(),
|
|
63
|
+
size: z.number().int().nonnegative().optional(),
|
|
64
|
+
from: z.number().int().nonnegative().optional(),
|
|
65
|
+
search_after: z.array(z.unknown()).optional(),
|
|
66
|
+
};
|
|
67
|
+
const notificationConfig = z
|
|
68
|
+
.object({
|
|
69
|
+
anomaly_events: z.boolean().optional(),
|
|
70
|
+
client_offline: z.boolean().optional(),
|
|
71
|
+
anomaly_events_frequency: z.enum(["immediate", "digest"]).optional(),
|
|
72
|
+
anomaly_events_digest_hours: z.number().int().min(1).max(168).optional(),
|
|
73
|
+
anomaly_events_digest_interval_unit: z
|
|
74
|
+
.enum(["hour", "day", "week"])
|
|
75
|
+
.optional(),
|
|
76
|
+
anomaly_events_digest_interval_count: z
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.min(1)
|
|
80
|
+
.max(168)
|
|
81
|
+
.optional(),
|
|
82
|
+
anomaly_events_digest_at_time: z
|
|
83
|
+
.string()
|
|
84
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
|
85
|
+
.optional(),
|
|
86
|
+
anomaly_events_digest_day_of_week: z
|
|
87
|
+
.number()
|
|
88
|
+
.int()
|
|
89
|
+
.min(1)
|
|
90
|
+
.max(7)
|
|
91
|
+
.optional(),
|
|
92
|
+
})
|
|
93
|
+
.optional();
|
|
94
|
+
async function data(client, request) {
|
|
95
|
+
return (await client.request(request)).data;
|
|
96
|
+
}
|
|
97
|
+
function apiKeyPath(kind) {
|
|
98
|
+
return `${kind}_api_keys`;
|
|
99
|
+
}
|
|
100
|
+
export function registerSurfmeterTools(server, client) {
|
|
101
|
+
const catalog = new Map();
|
|
102
|
+
actionCatalogs.set(server, catalog);
|
|
103
|
+
register(server, {
|
|
104
|
+
name: "surfmeter_get_version",
|
|
105
|
+
description: "Get the Surfmeter server version.",
|
|
106
|
+
schema: {},
|
|
107
|
+
readOnly: true,
|
|
108
|
+
handler: () => data(client, { path: "version" }),
|
|
109
|
+
});
|
|
110
|
+
register(server, {
|
|
111
|
+
name: "surfmeter_get_measurement_options",
|
|
112
|
+
description: "Get allowed measurement types and subjects for capability configuration.",
|
|
113
|
+
schema: {},
|
|
114
|
+
readOnly: true,
|
|
115
|
+
handler: () => data(client, { path: "measurement_options" }),
|
|
116
|
+
});
|
|
117
|
+
register(server, {
|
|
118
|
+
name: "surfmeter_get_license_usage",
|
|
119
|
+
description: "Get monthly license usage. Use extended=true for license units and per-subject distribution.",
|
|
120
|
+
schema: {
|
|
121
|
+
start_time: z.string().optional(),
|
|
122
|
+
end_time: z.string().optional(),
|
|
123
|
+
extended: z.boolean().optional(),
|
|
124
|
+
},
|
|
125
|
+
readOnly: true,
|
|
126
|
+
handler: (args) => data(client, { path: "license_usage", query: args }),
|
|
127
|
+
});
|
|
128
|
+
register(server, {
|
|
129
|
+
name: "surfmeter_list_clients",
|
|
130
|
+
description: "List all owned and shared Surfmeter clients visible to the current API key.",
|
|
131
|
+
schema: {},
|
|
132
|
+
readOnly: true,
|
|
133
|
+
handler: () => data(client, { path: "clients" }),
|
|
134
|
+
});
|
|
135
|
+
register(server, {
|
|
136
|
+
name: "surfmeter_get_client",
|
|
137
|
+
description: "Get one Surfmeter client by numeric ID or UUID.",
|
|
138
|
+
schema: { id },
|
|
139
|
+
readOnly: true,
|
|
140
|
+
handler: ({ id }) => data(client, { path: `clients/${id}` }),
|
|
141
|
+
});
|
|
142
|
+
register(server, {
|
|
143
|
+
name: "surfmeter_check_client_label",
|
|
144
|
+
description: "Check whether a client label is available. Optionally exclude the client currently being edited.",
|
|
145
|
+
schema: { label: z.string(), exclude_id: id.optional() },
|
|
146
|
+
readOnly: true,
|
|
147
|
+
handler: (args) => data(client, {
|
|
148
|
+
path: "clients/check_label_availability",
|
|
149
|
+
query: args,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
register(server, {
|
|
153
|
+
name: "surfmeter_update_client",
|
|
154
|
+
description: "Update an owned client's labels, tags, or client-group assignments. Shared clients are read-only.",
|
|
155
|
+
schema: {
|
|
156
|
+
id,
|
|
157
|
+
label: nullableString,
|
|
158
|
+
extra_label: nullableString,
|
|
159
|
+
tags: z.array(z.string()).nullable().optional(),
|
|
160
|
+
client_group_ids: z.array(numericId).optional(),
|
|
161
|
+
},
|
|
162
|
+
readOnly: false,
|
|
163
|
+
handler: ({ id, ...body }) => data(client, { method: "PUT", path: `clients/${id}`, body }),
|
|
164
|
+
});
|
|
165
|
+
for (const action of ["disable", "mute", "unmute"]) {
|
|
166
|
+
register(server, {
|
|
167
|
+
name: `surfmeter_${action}_client`,
|
|
168
|
+
description: action === "disable"
|
|
169
|
+
? "Manually disable an owned Surfmeter client. This interrupts that probe and is not automatically reversible through this API."
|
|
170
|
+
: `${action === "mute" ? "Mute" : "Unmute"} anomaly detection for an owned client.`,
|
|
171
|
+
schema: { id },
|
|
172
|
+
readOnly: false,
|
|
173
|
+
destructive: action === "disable",
|
|
174
|
+
handler: ({ id }) => data(client, { method: "POST", path: `clients/${id}/${action}` }),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
register(server, {
|
|
178
|
+
name: "surfmeter_list_client_groups",
|
|
179
|
+
description: "List client groups and their assigned clients.",
|
|
180
|
+
schema: {},
|
|
181
|
+
readOnly: true,
|
|
182
|
+
handler: () => data(client, { path: "client_groups" }),
|
|
183
|
+
});
|
|
184
|
+
register(server, {
|
|
185
|
+
name: "surfmeter_get_client_group",
|
|
186
|
+
description: "Get one client group.",
|
|
187
|
+
schema: { id: numericId },
|
|
188
|
+
readOnly: true,
|
|
189
|
+
handler: ({ id }) => data(client, { path: `client_groups/${id}` }),
|
|
190
|
+
});
|
|
191
|
+
register(server, {
|
|
192
|
+
name: "surfmeter_create_client_group",
|
|
193
|
+
description: "Create a client group.",
|
|
194
|
+
schema: { name: z.string().min(1), description: nullableString },
|
|
195
|
+
readOnly: false,
|
|
196
|
+
handler: (body) => data(client, { method: "POST", path: "client_groups", body }),
|
|
197
|
+
});
|
|
198
|
+
register(server, {
|
|
199
|
+
name: "surfmeter_update_client_group",
|
|
200
|
+
description: "Update a client group's name, description, or complete client membership.",
|
|
201
|
+
schema: {
|
|
202
|
+
id: numericId,
|
|
203
|
+
name: z.string().min(1).optional(),
|
|
204
|
+
description: nullableString,
|
|
205
|
+
client_ids: z.array(numericId).optional(),
|
|
206
|
+
},
|
|
207
|
+
readOnly: false,
|
|
208
|
+
handler: ({ id, ...body }) => data(client, { method: "PUT", path: `client_groups/${id}`, body }),
|
|
209
|
+
});
|
|
210
|
+
register(server, {
|
|
211
|
+
name: "surfmeter_delete_client_group",
|
|
212
|
+
description: "Permanently delete a client group.",
|
|
213
|
+
schema: { id: numericId },
|
|
214
|
+
readOnly: false,
|
|
215
|
+
destructive: true,
|
|
216
|
+
handler: ({ id }) => data(client, { method: "DELETE", path: `client_groups/${id}` }),
|
|
217
|
+
});
|
|
218
|
+
register(server, {
|
|
219
|
+
name: "surfmeter_list_isp_contracts",
|
|
220
|
+
description: "List ISP contracts assigned to one client.",
|
|
221
|
+
schema: { client_id: id },
|
|
222
|
+
readOnly: true,
|
|
223
|
+
handler: ({ client_id }) => data(client, { path: `clients/${client_id}/isp_contracts` }),
|
|
224
|
+
});
|
|
225
|
+
const ispContractFields = {
|
|
226
|
+
isp_id: numericId,
|
|
227
|
+
name: z.string().min(1),
|
|
228
|
+
download_speed: z.number().int().nonnegative().nullable().optional(),
|
|
229
|
+
upload_speed: z.number().int().nonnegative().nullable().optional(),
|
|
230
|
+
connection_type: nullableString,
|
|
231
|
+
monthly_price: z.number().nonnegative().nullable().optional(),
|
|
232
|
+
currency: nullableString,
|
|
233
|
+
assumed_tariff: nullableString,
|
|
234
|
+
valid_from: nullableString,
|
|
235
|
+
valid_until: nullableString,
|
|
236
|
+
};
|
|
237
|
+
register(server, {
|
|
238
|
+
name: "surfmeter_create_isp_contract",
|
|
239
|
+
description: "Create an ISP contract for a client.",
|
|
240
|
+
schema: { client_id: id, ...ispContractFields },
|
|
241
|
+
readOnly: false,
|
|
242
|
+
handler: ({ client_id, ...isp_contract }) => data(client, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
path: `clients/${client_id}/isp_contracts`,
|
|
245
|
+
body: { isp_contract },
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
register(server, {
|
|
249
|
+
name: "surfmeter_update_isp_contract",
|
|
250
|
+
description: "Replace the editable details of a client's ISP contract.",
|
|
251
|
+
schema: {
|
|
252
|
+
client_id: id,
|
|
253
|
+
id: numericId,
|
|
254
|
+
...ispContractFields,
|
|
255
|
+
},
|
|
256
|
+
readOnly: false,
|
|
257
|
+
handler: ({ client_id, id, ...isp_contract }) => data(client, {
|
|
258
|
+
method: "PUT",
|
|
259
|
+
path: `clients/${client_id}/isp_contracts/${id}`,
|
|
260
|
+
body: { isp_contract },
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
register(server, {
|
|
264
|
+
name: "surfmeter_delete_isp_contract",
|
|
265
|
+
description: "Permanently remove an ISP contract from a client.",
|
|
266
|
+
schema: { client_id: id, id: numericId },
|
|
267
|
+
readOnly: false,
|
|
268
|
+
destructive: true,
|
|
269
|
+
handler: ({ client_id, id }) => data(client, {
|
|
270
|
+
method: "DELETE",
|
|
271
|
+
path: `clients/${client_id}/isp_contracts/${id}`,
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
register(server, {
|
|
275
|
+
name: "surfmeter_list_isps",
|
|
276
|
+
description: "List or search ISPs. Name search returns at most 20 compact results.",
|
|
277
|
+
schema: {
|
|
278
|
+
name: z.string().optional(),
|
|
279
|
+
country: z.string().length(2).optional(),
|
|
280
|
+
type: z.enum(["FixedIsp", "MobileIsp"]).optional(),
|
|
281
|
+
verified: z.boolean().optional(),
|
|
282
|
+
},
|
|
283
|
+
readOnly: true,
|
|
284
|
+
handler: (args) => data(client, { path: "isps", query: args }),
|
|
285
|
+
});
|
|
286
|
+
register(server, {
|
|
287
|
+
name: "surfmeter_get_isp",
|
|
288
|
+
description: "Get one ISP and its detailed metadata.",
|
|
289
|
+
schema: { id: numericId },
|
|
290
|
+
readOnly: true,
|
|
291
|
+
handler: ({ id }) => data(client, { path: `isps/${id}` }),
|
|
292
|
+
});
|
|
293
|
+
const ispFields = {
|
|
294
|
+
name: z.string().min(1),
|
|
295
|
+
country: z.string().length(2),
|
|
296
|
+
type: z.enum(["FixedIsp", "MobileIsp"]),
|
|
297
|
+
is_vpn: z.boolean().nullable().optional(),
|
|
298
|
+
};
|
|
299
|
+
register(server, {
|
|
300
|
+
name: "surfmeter_create_isp",
|
|
301
|
+
description: "Create a verified ISP record.",
|
|
302
|
+
schema: ispFields,
|
|
303
|
+
readOnly: false,
|
|
304
|
+
handler: (isp) => data(client, { method: "POST", path: "isps", body: { isp } }),
|
|
305
|
+
});
|
|
306
|
+
register(server, {
|
|
307
|
+
name: "surfmeter_update_isp",
|
|
308
|
+
description: "Update an ISP record.",
|
|
309
|
+
schema: {
|
|
310
|
+
id: numericId,
|
|
311
|
+
name: ispFields.name.optional(),
|
|
312
|
+
country: ispFields.country.optional(),
|
|
313
|
+
type: ispFields.type.optional(),
|
|
314
|
+
is_vpn: ispFields.is_vpn,
|
|
315
|
+
},
|
|
316
|
+
readOnly: false,
|
|
317
|
+
handler: ({ id, ...isp }) => data(client, { method: "PUT", path: `isps/${id}`, body: { isp } }),
|
|
318
|
+
});
|
|
319
|
+
register(server, {
|
|
320
|
+
name: "surfmeter_merge_isp",
|
|
321
|
+
description: "Permanently merge the loser ISP into the target ISP, repoint all references, and delete the loser.",
|
|
322
|
+
schema: { id: numericId, target_id: numericId },
|
|
323
|
+
readOnly: false,
|
|
324
|
+
destructive: true,
|
|
325
|
+
handler: ({ id, target_id }) => data(client, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
path: `isps/${id}/merge_into`,
|
|
328
|
+
body: { target_id },
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
register(server, {
|
|
332
|
+
name: "surfmeter_delete_isp",
|
|
333
|
+
description: "Permanently delete an ISP that is safe to remove.",
|
|
334
|
+
schema: { id: numericId },
|
|
335
|
+
readOnly: false,
|
|
336
|
+
destructive: true,
|
|
337
|
+
handler: ({ id }) => data(client, { method: "DELETE", path: `isps/${id}` }),
|
|
338
|
+
});
|
|
339
|
+
register(server, {
|
|
340
|
+
name: "surfmeter_list_users",
|
|
341
|
+
description: "List visible dashboard users in the current organization. Requires org_admin.",
|
|
342
|
+
schema: {},
|
|
343
|
+
readOnly: true,
|
|
344
|
+
handler: () => data(client, { path: "client_admin_users" }),
|
|
345
|
+
});
|
|
346
|
+
register(server, {
|
|
347
|
+
name: "surfmeter_get_user",
|
|
348
|
+
description: "Get a dashboard user profile. Users can read themselves; org_admin can read peers.",
|
|
349
|
+
schema: { id: numericId },
|
|
350
|
+
readOnly: true,
|
|
351
|
+
handler: ({ id }) => data(client, { path: `client_admin_users/${id}` }),
|
|
352
|
+
});
|
|
353
|
+
register(server, {
|
|
354
|
+
name: "surfmeter_create_user",
|
|
355
|
+
description: "Create a dashboard user. Requires org_admin. notify_user optionally sends account email.",
|
|
356
|
+
schema: {
|
|
357
|
+
email: z.email(),
|
|
358
|
+
username: z.string().min(1),
|
|
359
|
+
role,
|
|
360
|
+
first_name: nullableString,
|
|
361
|
+
last_name: nullableString,
|
|
362
|
+
notify_user: z.boolean().optional(),
|
|
363
|
+
notification_config: notificationConfig,
|
|
364
|
+
},
|
|
365
|
+
readOnly: false,
|
|
366
|
+
handler: (body) => data(client, { method: "POST", path: "client_admin_users", body }),
|
|
367
|
+
});
|
|
368
|
+
register(server, {
|
|
369
|
+
name: "surfmeter_update_user",
|
|
370
|
+
description: "Update a dashboard user. Only org_admin can change another user's role.",
|
|
371
|
+
schema: {
|
|
372
|
+
id: numericId,
|
|
373
|
+
first_name: nullableString,
|
|
374
|
+
last_name: nullableString,
|
|
375
|
+
role: role.optional(),
|
|
376
|
+
ai_usage_accepted: z.boolean().nullable().optional(),
|
|
377
|
+
ai_custom_prompt: nullableString,
|
|
378
|
+
notification_config: notificationConfig,
|
|
379
|
+
},
|
|
380
|
+
readOnly: false,
|
|
381
|
+
handler: ({ id, ...body }) => data(client, {
|
|
382
|
+
method: "PUT",
|
|
383
|
+
path: `client_admin_users/${id}`,
|
|
384
|
+
body,
|
|
385
|
+
}),
|
|
386
|
+
});
|
|
387
|
+
register(server, {
|
|
388
|
+
name: "surfmeter_request_user_email_change",
|
|
389
|
+
description: "Start the verified email-change flow for a dashboard user.",
|
|
390
|
+
schema: { id: numericId, email: z.email() },
|
|
391
|
+
readOnly: false,
|
|
392
|
+
handler: ({ id, email }) => data(client, {
|
|
393
|
+
method: "POST",
|
|
394
|
+
path: `client_admin_users/${id}/email_change`,
|
|
395
|
+
body: { email },
|
|
396
|
+
}),
|
|
397
|
+
});
|
|
398
|
+
register(server, {
|
|
399
|
+
name: "surfmeter_cancel_user_email_change",
|
|
400
|
+
description: "Cancel a dashboard user's pending email change.",
|
|
401
|
+
schema: { id: numericId },
|
|
402
|
+
readOnly: false,
|
|
403
|
+
handler: ({ id }) => data(client, {
|
|
404
|
+
method: "DELETE",
|
|
405
|
+
path: `client_admin_users/${id}/email_change`,
|
|
406
|
+
}),
|
|
407
|
+
});
|
|
408
|
+
register(server, {
|
|
409
|
+
name: "surfmeter_delete_user",
|
|
410
|
+
description: "Permanently delete a dashboard user. Requires org_admin and cannot target the current user.",
|
|
411
|
+
schema: { id: numericId },
|
|
412
|
+
readOnly: false,
|
|
413
|
+
destructive: true,
|
|
414
|
+
handler: ({ id }) => data(client, {
|
|
415
|
+
method: "DELETE",
|
|
416
|
+
path: `client_admin_users/${id}`,
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
register(server, {
|
|
420
|
+
name: "surfmeter_list_api_keys",
|
|
421
|
+
description: "List API keys of one kind: export, client analytics, or client admin.",
|
|
422
|
+
schema: { kind: keyKind },
|
|
423
|
+
readOnly: true,
|
|
424
|
+
handler: ({ kind }) => data(client, { path: apiKeyPath(kind) }),
|
|
425
|
+
});
|
|
426
|
+
register(server, {
|
|
427
|
+
name: "surfmeter_get_api_key",
|
|
428
|
+
description: "Get metadata for one API key. Secret key material is only returned at creation time.",
|
|
429
|
+
schema: { kind: keyKind, id: numericId },
|
|
430
|
+
readOnly: true,
|
|
431
|
+
handler: ({ kind, id }) => data(client, { path: `${apiKeyPath(kind)}/${id}` }),
|
|
432
|
+
});
|
|
433
|
+
register(server, {
|
|
434
|
+
name: "surfmeter_create_api_key",
|
|
435
|
+
description: "Create an API key. The returned secret is shown once; handle it as sensitive data. effective_role only applies to client_admin keys.",
|
|
436
|
+
schema: {
|
|
437
|
+
kind: keyKind,
|
|
438
|
+
name: z.string().min(1).optional(),
|
|
439
|
+
valid_until: z.string().optional(),
|
|
440
|
+
effective_role: role.optional(),
|
|
441
|
+
},
|
|
442
|
+
readOnly: false,
|
|
443
|
+
handler: ({ kind, ...body }) => data(client, { method: "POST", path: apiKeyPath(kind), body }),
|
|
444
|
+
});
|
|
445
|
+
register(server, {
|
|
446
|
+
name: "surfmeter_revoke_api_key",
|
|
447
|
+
description: "Permanently revoke an API key.",
|
|
448
|
+
schema: { kind: keyKind, id: numericId },
|
|
449
|
+
readOnly: false,
|
|
450
|
+
destructive: true,
|
|
451
|
+
handler: ({ kind, id }) => data(client, {
|
|
452
|
+
method: "DELETE",
|
|
453
|
+
path: `${apiKeyPath(kind)}/${id}`,
|
|
454
|
+
}),
|
|
455
|
+
});
|
|
456
|
+
const registrationKeyFields = {
|
|
457
|
+
comment: nullableString,
|
|
458
|
+
valid_until: nullableString,
|
|
459
|
+
num_registrations_left: z.number().int().nullable().optional(),
|
|
460
|
+
capability_id: numericId.nullable().optional(),
|
|
461
|
+
usage_limit_days: z.number().int().nonnegative().nullable().optional(),
|
|
462
|
+
testing: z.boolean().nullable().optional(),
|
|
463
|
+
};
|
|
464
|
+
register(server, {
|
|
465
|
+
name: "surfmeter_list_registration_keys",
|
|
466
|
+
description: "List client registration keys and their usage.",
|
|
467
|
+
schema: {},
|
|
468
|
+
readOnly: true,
|
|
469
|
+
handler: () => data(client, { path: "registration_keys" }),
|
|
470
|
+
});
|
|
471
|
+
register(server, {
|
|
472
|
+
name: "surfmeter_get_registration_key",
|
|
473
|
+
description: "Get one client registration key.",
|
|
474
|
+
schema: { id: numericId },
|
|
475
|
+
readOnly: true,
|
|
476
|
+
handler: ({ id }) => data(client, { path: `registration_keys/${id}` }),
|
|
477
|
+
});
|
|
478
|
+
register(server, {
|
|
479
|
+
name: "surfmeter_create_registration_key",
|
|
480
|
+
description: "Create a client registration key. The secret registration value is returned by the API.",
|
|
481
|
+
schema: registrationKeyFields,
|
|
482
|
+
readOnly: false,
|
|
483
|
+
handler: (body) => data(client, { method: "POST", path: "registration_keys", body }),
|
|
484
|
+
});
|
|
485
|
+
register(server, {
|
|
486
|
+
name: "surfmeter_update_registration_key",
|
|
487
|
+
description: "Update a client registration key's limits and metadata.",
|
|
488
|
+
schema: { id: numericId, ...registrationKeyFields },
|
|
489
|
+
readOnly: false,
|
|
490
|
+
handler: ({ id, ...body }) => data(client, {
|
|
491
|
+
method: "PUT",
|
|
492
|
+
path: `registration_keys/${id}`,
|
|
493
|
+
body,
|
|
494
|
+
}),
|
|
495
|
+
});
|
|
496
|
+
register(server, {
|
|
497
|
+
name: "surfmeter_delete_registration_key",
|
|
498
|
+
description: "Permanently revoke a client registration key.",
|
|
499
|
+
schema: { id: numericId },
|
|
500
|
+
readOnly: false,
|
|
501
|
+
destructive: true,
|
|
502
|
+
handler: ({ id }) => data(client, {
|
|
503
|
+
method: "DELETE",
|
|
504
|
+
path: `registration_keys/${id}`,
|
|
505
|
+
}),
|
|
506
|
+
});
|
|
507
|
+
const capabilityFields = {
|
|
508
|
+
name: z.string().min(1),
|
|
509
|
+
comment: nullableString,
|
|
510
|
+
allowed_measurement_types: z.array(z.string()).optional(),
|
|
511
|
+
allowed_measurement_subjects: z.array(z.string()).optional(),
|
|
512
|
+
forbidden_measurement_types: z.array(z.string()).optional(),
|
|
513
|
+
forbidden_measurement_subjects: z.array(z.string()).optional(),
|
|
514
|
+
};
|
|
515
|
+
register(server, {
|
|
516
|
+
name: "surfmeter_list_capabilities",
|
|
517
|
+
description: "List capability profiles and registration-key counts.",
|
|
518
|
+
schema: {},
|
|
519
|
+
readOnly: true,
|
|
520
|
+
handler: () => data(client, { path: "capabilities" }),
|
|
521
|
+
});
|
|
522
|
+
register(server, {
|
|
523
|
+
name: "surfmeter_get_capability",
|
|
524
|
+
description: "Get one capability profile and its associated registration keys.",
|
|
525
|
+
schema: { id: numericId },
|
|
526
|
+
readOnly: true,
|
|
527
|
+
handler: ({ id }) => data(client, { path: `capabilities/${id}` }),
|
|
528
|
+
});
|
|
529
|
+
register(server, {
|
|
530
|
+
name: "surfmeter_create_capability",
|
|
531
|
+
description: "Create a capability profile controlling permitted measurement types and subjects.",
|
|
532
|
+
schema: capabilityFields,
|
|
533
|
+
readOnly: false,
|
|
534
|
+
handler: (body) => data(client, { method: "POST", path: "capabilities", body }),
|
|
535
|
+
});
|
|
536
|
+
register(server, {
|
|
537
|
+
name: "surfmeter_update_capability",
|
|
538
|
+
description: "Update a capability profile.",
|
|
539
|
+
schema: {
|
|
540
|
+
id: numericId,
|
|
541
|
+
name: capabilityFields.name.optional(),
|
|
542
|
+
comment: capabilityFields.comment,
|
|
543
|
+
allowed_measurement_types: capabilityFields.allowed_measurement_types,
|
|
544
|
+
allowed_measurement_subjects: capabilityFields.allowed_measurement_subjects,
|
|
545
|
+
forbidden_measurement_types: capabilityFields.forbidden_measurement_types,
|
|
546
|
+
forbidden_measurement_subjects: capabilityFields.forbidden_measurement_subjects,
|
|
547
|
+
},
|
|
548
|
+
readOnly: false,
|
|
549
|
+
handler: ({ id, ...body }) => data(client, { method: "PUT", path: `capabilities/${id}`, body }),
|
|
550
|
+
});
|
|
551
|
+
register(server, {
|
|
552
|
+
name: "surfmeter_delete_capability",
|
|
553
|
+
description: "Permanently delete a capability profile. The API rejects unsafe deletion when dependencies prevent it.",
|
|
554
|
+
schema: { id: numericId },
|
|
555
|
+
readOnly: false,
|
|
556
|
+
destructive: true,
|
|
557
|
+
handler: ({ id }) => data(client, { method: "DELETE", path: `capabilities/${id}` }),
|
|
558
|
+
});
|
|
559
|
+
register(server, {
|
|
560
|
+
name: "surfmeter_search_measurements",
|
|
561
|
+
description: "Search and aggregate measurement documents with Elasticsearch Query DSL. Prefer filters and aggregations server-side; use .keyword for exact text fields.",
|
|
562
|
+
schema: {
|
|
563
|
+
...searchOptions,
|
|
564
|
+
scroll: z
|
|
565
|
+
.string()
|
|
566
|
+
.regex(/^\d+[smh]$/)
|
|
567
|
+
.optional()
|
|
568
|
+
.describe("Optional Elasticsearch scroll timeout such as 1m"),
|
|
569
|
+
},
|
|
570
|
+
readOnly: true,
|
|
571
|
+
handler: (body) => data(client, { method: "POST", path: "measurements/search", body }),
|
|
572
|
+
});
|
|
573
|
+
register(server, {
|
|
574
|
+
name: "surfmeter_get_measurement",
|
|
575
|
+
description: "Get one measurement document by Elasticsearch document ID.",
|
|
576
|
+
schema: { id: z.string().min(1) },
|
|
577
|
+
readOnly: true,
|
|
578
|
+
handler: ({ id }) => data(client, { path: `measurements/${id}` }),
|
|
579
|
+
});
|
|
580
|
+
register(server, {
|
|
581
|
+
name: "surfmeter_get_p1203_report",
|
|
582
|
+
description: "Get the ITU-T P.1203 input/client-report data for one video measurement.",
|
|
583
|
+
schema: { id: z.string().min(1) },
|
|
584
|
+
readOnly: true,
|
|
585
|
+
handler: ({ id }) => data(client, { path: `measurements/${id}/p1203_client_report` }),
|
|
586
|
+
});
|
|
587
|
+
register(server, {
|
|
588
|
+
name: "surfmeter_get_network_performance_report",
|
|
589
|
+
description: "Get the network performance client report attached to one web measurement.",
|
|
590
|
+
schema: { id: z.string().min(1) },
|
|
591
|
+
readOnly: true,
|
|
592
|
+
handler: ({ id }) => data(client, {
|
|
593
|
+
path: `measurements/${id}/network_performance_report`,
|
|
594
|
+
}),
|
|
595
|
+
});
|
|
596
|
+
register(server, {
|
|
597
|
+
name: "surfmeter_search_anomalies",
|
|
598
|
+
description: "Search and aggregate anomaly episode documents with Elasticsearch Query DSL. Use .keyword for exact text fields.",
|
|
599
|
+
schema: searchOptions,
|
|
600
|
+
readOnly: true,
|
|
601
|
+
handler: (body) => data(client, { method: "POST", path: "anomaly_events/search", body }),
|
|
602
|
+
});
|
|
603
|
+
register(server, {
|
|
604
|
+
name: "surfmeter_get_anomaly",
|
|
605
|
+
description: "Get one anomaly event by its public anomaly_id.",
|
|
606
|
+
schema: { anomaly_id: z.string().min(1) },
|
|
607
|
+
readOnly: true,
|
|
608
|
+
handler: ({ anomaly_id }) => data(client, {
|
|
609
|
+
path: `anomaly_events/${encodeURIComponent(anomaly_id)}`,
|
|
610
|
+
}),
|
|
611
|
+
});
|
|
612
|
+
register(server, {
|
|
613
|
+
name: "surfmeter_resolve_anomalies",
|
|
614
|
+
description: "Resolve up to 500 anomaly episodes by Elasticsearch document ID. Already-resolved and unknown IDs are harmless no-ops.",
|
|
615
|
+
schema: { anomaly_ids: z.array(z.string().min(1)).min(1).max(500) },
|
|
616
|
+
readOnly: false,
|
|
617
|
+
handler: (body) => data(client, {
|
|
618
|
+
method: "PATCH",
|
|
619
|
+
path: "anomaly_events/resolve",
|
|
620
|
+
body,
|
|
621
|
+
}),
|
|
622
|
+
});
|
|
623
|
+
const anomalyScope = z.object({
|
|
624
|
+
subject: z.string().optional(),
|
|
625
|
+
measurement_type: z.string().optional(),
|
|
626
|
+
metric: z.string().optional(),
|
|
627
|
+
detection_pass: z
|
|
628
|
+
.enum(["spike", "shift", "inter_client", "data_quality"])
|
|
629
|
+
.optional(),
|
|
630
|
+
});
|
|
631
|
+
const anomalyLevel = z.enum([
|
|
632
|
+
"way_more",
|
|
633
|
+
"more",
|
|
634
|
+
"normal",
|
|
635
|
+
"fewer",
|
|
636
|
+
"much_fewer",
|
|
637
|
+
"off",
|
|
638
|
+
]);
|
|
639
|
+
register(server, {
|
|
640
|
+
name: "surfmeter_list_anomaly_overrides",
|
|
641
|
+
description: "List all anomaly-detection tuning overrides.",
|
|
642
|
+
schema: {},
|
|
643
|
+
readOnly: true,
|
|
644
|
+
handler: () => data(client, { path: "anomaly_detection_overrides" }),
|
|
645
|
+
});
|
|
646
|
+
register(server, {
|
|
647
|
+
name: "surfmeter_get_anomaly_override",
|
|
648
|
+
description: "Get one anomaly-detection tuning override.",
|
|
649
|
+
schema: { id: numericId },
|
|
650
|
+
readOnly: true,
|
|
651
|
+
handler: ({ id }) => data(client, { path: `anomaly_detection_overrides/${id}` }),
|
|
652
|
+
});
|
|
653
|
+
register(server, {
|
|
654
|
+
name: "surfmeter_get_anomaly_override_dimensions",
|
|
655
|
+
description: "Get valid measurement types, subjects, metrics, detection passes, and tuning levels for authoring overrides.",
|
|
656
|
+
schema: {},
|
|
657
|
+
readOnly: true,
|
|
658
|
+
handler: () => data(client, { path: "anomaly_detection_overrides/dimensions" }),
|
|
659
|
+
});
|
|
660
|
+
register(server, {
|
|
661
|
+
name: "surfmeter_create_anomaly_override",
|
|
662
|
+
description: "Create a manual anomaly-detection tuning override.",
|
|
663
|
+
schema: { scope: anomalyScope, level: anomalyLevel },
|
|
664
|
+
readOnly: false,
|
|
665
|
+
handler: (body) => data(client, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
path: "anomaly_detection_overrides",
|
|
668
|
+
body,
|
|
669
|
+
}),
|
|
670
|
+
});
|
|
671
|
+
register(server, {
|
|
672
|
+
name: "surfmeter_update_anomaly_override",
|
|
673
|
+
description: "Change an anomaly override's level. Scope changes require delete and recreate.",
|
|
674
|
+
schema: { id: numericId, level: anomalyLevel },
|
|
675
|
+
readOnly: false,
|
|
676
|
+
handler: ({ id, level }) => data(client, {
|
|
677
|
+
method: "PUT",
|
|
678
|
+
path: `anomaly_detection_overrides/${id}`,
|
|
679
|
+
body: { level },
|
|
680
|
+
}),
|
|
681
|
+
});
|
|
682
|
+
register(server, {
|
|
683
|
+
name: "surfmeter_submit_anomaly_feedback",
|
|
684
|
+
description: "Apply thumbs-style feedback to an anomaly scope, stepping alert sensitivity up or down.",
|
|
685
|
+
schema: {
|
|
686
|
+
scope: anomalyScope,
|
|
687
|
+
direction: z.enum(["up", "down"]),
|
|
688
|
+
source_anomaly_id: z.string().nullable().optional(),
|
|
689
|
+
},
|
|
690
|
+
readOnly: false,
|
|
691
|
+
handler: (body) => data(client, {
|
|
692
|
+
method: "POST",
|
|
693
|
+
path: "anomaly_detection_overrides/feedback",
|
|
694
|
+
body,
|
|
695
|
+
}),
|
|
696
|
+
});
|
|
697
|
+
register(server, {
|
|
698
|
+
name: "surfmeter_delete_anomaly_override",
|
|
699
|
+
description: "Permanently delete one anomaly-detection override.",
|
|
700
|
+
schema: { id: numericId },
|
|
701
|
+
readOnly: false,
|
|
702
|
+
destructive: true,
|
|
703
|
+
handler: ({ id }) => data(client, {
|
|
704
|
+
method: "DELETE",
|
|
705
|
+
path: `anomaly_detection_overrides/${id}`,
|
|
706
|
+
}),
|
|
707
|
+
});
|
|
708
|
+
register(server, {
|
|
709
|
+
name: "surfmeter_delete_anomaly_overrides",
|
|
710
|
+
description: "Permanently delete selected anomaly overrides, or all overrides when ids is omitted or empty.",
|
|
711
|
+
schema: { ids: z.array(numericId).optional() },
|
|
712
|
+
readOnly: false,
|
|
713
|
+
destructive: true,
|
|
714
|
+
handler: (body) => data(client, {
|
|
715
|
+
method: "DELETE",
|
|
716
|
+
path: "anomaly_detection_overrides",
|
|
717
|
+
body,
|
|
718
|
+
}),
|
|
719
|
+
});
|
|
720
|
+
register(server, {
|
|
721
|
+
name: "surfmeter_get_settings",
|
|
722
|
+
description: "Get organization settings such as AI system prompt and offline threshold.",
|
|
723
|
+
schema: {},
|
|
724
|
+
readOnly: true,
|
|
725
|
+
handler: () => data(client, { path: "data_owner/settings" }),
|
|
726
|
+
});
|
|
727
|
+
register(server, {
|
|
728
|
+
name: "surfmeter_update_settings",
|
|
729
|
+
description: "Update organization settings. A null offline threshold clears the override and restores the server default.",
|
|
730
|
+
schema: {
|
|
731
|
+
ai_system_prompt: z.string().nullable().optional(),
|
|
732
|
+
offline_threshold_minutes: z
|
|
733
|
+
.number()
|
|
734
|
+
.int()
|
|
735
|
+
.positive()
|
|
736
|
+
.nullable()
|
|
737
|
+
.optional(),
|
|
738
|
+
},
|
|
739
|
+
readOnly: false,
|
|
740
|
+
handler: (body) => data(client, { method: "PUT", path: "data_owner/settings", body }),
|
|
741
|
+
});
|
|
742
|
+
register(server, {
|
|
743
|
+
name: "surfmeter_get_anomaly_config",
|
|
744
|
+
description: "Get organization-wide anomaly detection status and sensitivity.",
|
|
745
|
+
schema: {},
|
|
746
|
+
readOnly: true,
|
|
747
|
+
handler: () => data(client, { path: "data_owner/anomaly_detection_config" }),
|
|
748
|
+
});
|
|
749
|
+
register(server, {
|
|
750
|
+
name: "surfmeter_update_anomaly_config",
|
|
751
|
+
description: "Set organization-wide anomaly sensitivity to low, medium, or high.",
|
|
752
|
+
schema: { sensitivity: z.enum(["low", "medium", "high"]) },
|
|
753
|
+
readOnly: false,
|
|
754
|
+
handler: (body) => data(client, {
|
|
755
|
+
method: "PUT",
|
|
756
|
+
path: "data_owner/anomaly_detection_config",
|
|
757
|
+
body,
|
|
758
|
+
}),
|
|
759
|
+
});
|
|
760
|
+
register(server, {
|
|
761
|
+
name: "surfmeter_get_ai_status",
|
|
762
|
+
description: "Check whether Surfmeter's built-in AI assistant is enabled for the organization.",
|
|
763
|
+
schema: {},
|
|
764
|
+
readOnly: true,
|
|
765
|
+
handler: () => data(client, { path: "ai_assistant/status" }),
|
|
766
|
+
});
|
|
767
|
+
register(server, {
|
|
768
|
+
name: "surfmeter_get_ai_usage",
|
|
769
|
+
description: "Get the organization's Surfmeter AI usage summary. Requires org_admin.",
|
|
770
|
+
schema: {},
|
|
771
|
+
readOnly: true,
|
|
772
|
+
handler: () => data(client, { path: "ai_assistant/usage" }),
|
|
773
|
+
});
|
|
774
|
+
register(server, {
|
|
775
|
+
name: "surfmeter_explain_measurement",
|
|
776
|
+
description: "Ask Surfmeter's built-in AI assistant to explain a supplied measurement. May consume AI quota; force=true bypasses cached output.",
|
|
777
|
+
schema: {
|
|
778
|
+
measurement: jsonObject,
|
|
779
|
+
force: z.boolean().optional(),
|
|
780
|
+
user_timezone: z.string().optional(),
|
|
781
|
+
},
|
|
782
|
+
readOnly: false,
|
|
783
|
+
handler: (body) => data(client, {
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "ai_assistant/explain",
|
|
786
|
+
body,
|
|
787
|
+
}),
|
|
788
|
+
});
|
|
789
|
+
register(server, {
|
|
790
|
+
name: "surfmeter_explain_anomaly",
|
|
791
|
+
description: "Ask Surfmeter's built-in AI assistant to explain a supplied anomaly. May consume AI quota; force=true bypasses cached output.",
|
|
792
|
+
schema: {
|
|
793
|
+
anomaly: jsonObject,
|
|
794
|
+
force: z.boolean().optional(),
|
|
795
|
+
user_timezone: z.string().optional(),
|
|
796
|
+
},
|
|
797
|
+
readOnly: false,
|
|
798
|
+
handler: (body) => data(client, {
|
|
799
|
+
method: "POST",
|
|
800
|
+
path: "ai_assistant/explain_anomaly",
|
|
801
|
+
body,
|
|
802
|
+
}),
|
|
803
|
+
});
|
|
804
|
+
register(server, {
|
|
805
|
+
name: "surfmeter_ai_chat",
|
|
806
|
+
description: "Send a conversation to Surfmeter's built-in context-aware AI assistant. May consume AI quota.",
|
|
807
|
+
schema: {
|
|
808
|
+
messages: z.array(z.unknown()).min(1),
|
|
809
|
+
context: jsonObject.optional(),
|
|
810
|
+
},
|
|
811
|
+
readOnly: false,
|
|
812
|
+
handler: (body) => data(client, {
|
|
813
|
+
method: "POST",
|
|
814
|
+
path: "ai_assistant/chat",
|
|
815
|
+
body,
|
|
816
|
+
}),
|
|
817
|
+
});
|
|
818
|
+
registerCatalogTools(server, client, catalog);
|
|
819
|
+
}
|
|
820
|
+
function actionExecutor(category) {
|
|
821
|
+
switch (category) {
|
|
822
|
+
case "read":
|
|
823
|
+
return "surfmeter_execute_read_action";
|
|
824
|
+
case "write":
|
|
825
|
+
return "surfmeter_execute_write_action";
|
|
826
|
+
case "destructive":
|
|
827
|
+
return "surfmeter_execute_destructive_action";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function actionSchema(action) {
|
|
831
|
+
return z.toJSONSchema(z.object(action.schema));
|
|
832
|
+
}
|
|
833
|
+
function actionSummary(action, detailed = false) {
|
|
834
|
+
const schema = actionSchema(action);
|
|
835
|
+
const summary = {
|
|
836
|
+
id: action.name,
|
|
837
|
+
description: action.description,
|
|
838
|
+
category: action.category,
|
|
839
|
+
executor: actionExecutor(action.category),
|
|
840
|
+
parameters: Object.keys(schema.properties ?? {}),
|
|
841
|
+
required_parameters: schema.required ?? [],
|
|
842
|
+
};
|
|
843
|
+
return detailed ? { ...summary, input_schema: schema } : summary;
|
|
844
|
+
}
|
|
845
|
+
function searchScore(action, query) {
|
|
846
|
+
const normalizedQuery = query
|
|
847
|
+
.toLowerCase()
|
|
848
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
849
|
+
.trim();
|
|
850
|
+
const stopWords = new Set([
|
|
851
|
+
"a",
|
|
852
|
+
"an",
|
|
853
|
+
"and",
|
|
854
|
+
"for",
|
|
855
|
+
"i",
|
|
856
|
+
"of",
|
|
857
|
+
"the",
|
|
858
|
+
"to",
|
|
859
|
+
"want",
|
|
860
|
+
"with",
|
|
861
|
+
]);
|
|
862
|
+
const terms = normalizedQuery
|
|
863
|
+
.split(/\s+/)
|
|
864
|
+
.filter((term) => term.length > 1 && !stopWords.has(term));
|
|
865
|
+
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
866
|
+
const description = action.description.toLowerCase();
|
|
867
|
+
let score = 0;
|
|
868
|
+
if (name.includes(normalizedQuery)) {
|
|
869
|
+
score += 20;
|
|
870
|
+
}
|
|
871
|
+
if (description.includes(normalizedQuery)) {
|
|
872
|
+
score += 12;
|
|
873
|
+
}
|
|
874
|
+
for (const term of terms) {
|
|
875
|
+
if (name.includes(term)) {
|
|
876
|
+
score += 5;
|
|
877
|
+
}
|
|
878
|
+
if (description.includes(term)) {
|
|
879
|
+
score += 2;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return score;
|
|
883
|
+
}
|
|
884
|
+
async function executeCatalogAction(catalog, category, actionId, params) {
|
|
885
|
+
const action = catalog.get(actionId);
|
|
886
|
+
if (!action) {
|
|
887
|
+
throw new Error(`Unknown action '${actionId}'. Call surfmeter_search_actions first.`);
|
|
888
|
+
}
|
|
889
|
+
if (action.category !== category) {
|
|
890
|
+
throw new Error(`Action '${actionId}' is ${action.category}; use ${actionExecutor(action.category)}.`);
|
|
891
|
+
}
|
|
892
|
+
const validation = await z
|
|
893
|
+
.object(action.schema)
|
|
894
|
+
.safeParseAsync(params);
|
|
895
|
+
if (!validation.success) {
|
|
896
|
+
throw new Error(`Invalid parameters for '${actionId}': ${z.prettifyError(validation.error)}`);
|
|
897
|
+
}
|
|
898
|
+
return action.handler(validation.data);
|
|
899
|
+
}
|
|
900
|
+
function registerCatalogTools(server, client, catalog) {
|
|
901
|
+
registerActive(server, {
|
|
902
|
+
name: "surfmeter_get_clients",
|
|
903
|
+
description: "List all visible Surfmeter clients, or get one client when id is supplied.",
|
|
904
|
+
schema: { id: id.optional() },
|
|
905
|
+
readOnly: true,
|
|
906
|
+
handler: ({ id }) => data(client, { path: id === undefined ? "clients" : `clients/${id}` }),
|
|
907
|
+
});
|
|
908
|
+
registerActive(server, {
|
|
909
|
+
name: "surfmeter_get_system_status",
|
|
910
|
+
description: "Read server version, measurement options, license usage, or Surfmeter AI status.",
|
|
911
|
+
schema: {
|
|
912
|
+
resource: z.enum([
|
|
913
|
+
"version",
|
|
914
|
+
"measurement_options",
|
|
915
|
+
"license_usage",
|
|
916
|
+
"ai_status",
|
|
917
|
+
]),
|
|
918
|
+
start_time: z.string().optional(),
|
|
919
|
+
end_time: z.string().optional(),
|
|
920
|
+
extended: z.boolean().optional(),
|
|
921
|
+
},
|
|
922
|
+
readOnly: true,
|
|
923
|
+
handler: ({ resource, start_time, end_time, extended }) => {
|
|
924
|
+
if (resource === "license_usage") {
|
|
925
|
+
return data(client, {
|
|
926
|
+
path: "license_usage",
|
|
927
|
+
query: { start_time, end_time, extended },
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return data(client, {
|
|
931
|
+
path: resource === "ai_status" ? "ai_assistant/status" : resource,
|
|
932
|
+
});
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
registerActive(server, {
|
|
936
|
+
name: "surfmeter_search_actions",
|
|
937
|
+
description: "Find less-common Surfmeter operations by intent. Returns action IDs, safety categories, executors, and parameter names. Use surfmeter_describe_action for the exact schema.",
|
|
938
|
+
schema: {
|
|
939
|
+
query: z.string().min(1),
|
|
940
|
+
category: z.enum(["read", "write", "destructive"]).optional(),
|
|
941
|
+
limit: z.number().int().min(1).max(20).default(8),
|
|
942
|
+
},
|
|
943
|
+
readOnly: true,
|
|
944
|
+
handler: ({ query, category, limit }) => {
|
|
945
|
+
const matches = [...catalog.values()]
|
|
946
|
+
.filter((action) => category === undefined || action.category === category)
|
|
947
|
+
.map((action) => ({ action, score: searchScore(action, query) }))
|
|
948
|
+
.filter(({ score }) => score > 0)
|
|
949
|
+
.sort((left, right) => right.score - left.score ||
|
|
950
|
+
left.action.name.localeCompare(right.action.name))
|
|
951
|
+
.slice(0, limit)
|
|
952
|
+
.map(({ action }) => actionSummary(action));
|
|
953
|
+
return { actions: matches };
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
registerActive(server, {
|
|
957
|
+
name: "surfmeter_describe_action",
|
|
958
|
+
description: "Get the exact schema, safety category, and required executor for one discovered Surfmeter action.",
|
|
959
|
+
schema: { action_id: z.string().min(1) },
|
|
960
|
+
readOnly: true,
|
|
961
|
+
handler: ({ action_id }) => {
|
|
962
|
+
const action = catalog.get(action_id);
|
|
963
|
+
if (!action) {
|
|
964
|
+
throw new Error(`Unknown action '${action_id}'`);
|
|
965
|
+
}
|
|
966
|
+
return actionSummary(action, true);
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
const executorSchema = {
|
|
970
|
+
action_id: z.string().min(1),
|
|
971
|
+
params: jsonObject.default({}),
|
|
972
|
+
};
|
|
973
|
+
for (const category of ["read", "write", "destructive"]) {
|
|
974
|
+
registerActive(server, {
|
|
975
|
+
name: actionExecutor(category),
|
|
976
|
+
description: category === "read"
|
|
977
|
+
? "Execute a read-only Surfmeter action returned by surfmeter_search_actions."
|
|
978
|
+
: category === "write"
|
|
979
|
+
? "Execute a non-destructive Surfmeter mutation returned by surfmeter_search_actions."
|
|
980
|
+
: "Execute a destructive Surfmeter action returned by surfmeter_search_actions. Confirm exact targets and consequences first.",
|
|
981
|
+
schema: executorSchema,
|
|
982
|
+
readOnly: category === "read",
|
|
983
|
+
destructive: category === "destructive",
|
|
984
|
+
handler: ({ action_id, params }) => executeCatalogAction(catalog, category, action_id, params),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
//# sourceMappingURL=tools.js.map
|