@impart-security/impart-mcp 0.1.2 → 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/dist/index.js CHANGED
@@ -18,6 +18,46 @@ const server = new mcp_js_1.McpServer({
18
18
  function validatePage(page) {
19
19
  return Math.max(1, page); // Ensure page is at least 1
20
20
  }
21
+ // Helper function to convert date strings to Date objects recursively
22
+ function convertDatesToObjects(obj) {
23
+ if (obj === null || obj === undefined) {
24
+ return obj;
25
+ }
26
+ if (Array.isArray(obj)) {
27
+ return obj.map(convertDatesToObjects);
28
+ }
29
+ if (typeof obj === "object") {
30
+ const converted = {};
31
+ for (const [key, value] of Object.entries(obj)) {
32
+ // Convert RFC3339 date strings to Date objects
33
+ if (typeof value === "string" && isDateField(key)) {
34
+ try {
35
+ // Check if it looks like an ISO date string
36
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.exec(value)) {
37
+ converted[key] = new Date(value);
38
+ }
39
+ else {
40
+ // For debugging: if it's a date field but doesn't match pattern, keep as null
41
+ converted[key] = null;
42
+ }
43
+ }
44
+ catch {
45
+ converted[key] = null; // Set to null if date parsing fails
46
+ }
47
+ }
48
+ else {
49
+ converted[key] = convertDatesToObjects(value);
50
+ }
51
+ }
52
+ return converted;
53
+ }
54
+ return obj;
55
+ }
56
+ // Helper function to identify date fields
57
+ function isDateField(key) {
58
+ const dateFieldPatterns = ["createdAt", "updatedAt", "lastSeen", "observedAt", "from", "until", "expiration"];
59
+ return dateFieldPatterns.includes(key) || key.endsWith("At");
60
+ }
21
61
  // Helper function for making Impart API requests
22
62
  async function makeImpartRequest(endpoint, queryParams, method = "GET", body) {
23
63
  const headers = {
@@ -42,56 +82,186 @@ async function makeImpartRequest(endpoint, queryParams, method = "GET", body) {
42
82
  });
43
83
  }
44
84
  try {
45
- const response = await fetch(url.toString(), {
85
+ const requestOptions = {
46
86
  method,
47
87
  headers,
48
- body: body ? JSON.stringify(body) : "",
49
- });
88
+ };
89
+ if (body && method !== "GET" && method !== "HEAD") {
90
+ requestOptions.body = JSON.stringify(body);
91
+ }
92
+ // Log request details for debugging
93
+ console.error(`[makeImpartRequest] ${method} ${url.toString()}`);
94
+ console.error(`[makeImpartRequest] Headers:`, JSON.stringify(headers, null, 2));
95
+ if (body) {
96
+ console.error(`[makeImpartRequest] Body:`, JSON.stringify(body, null, 2));
97
+ }
98
+ const response = await fetch(url.toString(), requestOptions);
99
+ console.error(`[makeImpartRequest] Response status: ${response.status} ${response.statusText}`);
50
100
  if (!response.ok) {
51
- throw new Error(`HTTP error! status: ${response.status}`);
101
+ let errorMessage = `HTTP error! status: ${response.status}`;
102
+ try {
103
+ const errorBody = await response.text();
104
+ if (errorBody)
105
+ errorMessage += ` - ${errorBody}`;
106
+ }
107
+ catch (e) {
108
+ // Ignore error reading response body
109
+ }
110
+ throw new Error(errorMessage);
52
111
  }
53
- return (await response.json());
112
+ const jsonData = await response.json();
113
+ const converted = convertDatesToObjects(jsonData);
114
+ return converted;
54
115
  }
55
116
  catch (error) {
56
117
  console.error("Error making Impart request:", error);
118
+ // Parse API error for better AI-friendly messages
119
+ let errorMessage = "Unknown error";
120
+ if (error instanceof Error) {
121
+ const message = error.message;
122
+ // Extract HTTP status and body for better error messages
123
+ if (message.includes("status: 400")) {
124
+ // Extract error details from response body
125
+ const bodyMatch = message.match(/status: 400 - (.+)/);
126
+ if (bodyMatch) {
127
+ errorMessage = `Bad Request - ${bodyMatch[1]}`;
128
+ }
129
+ else {
130
+ errorMessage =
131
+ "Bad Request - Invalid parameters or format. Check parameter types, ranges, and formats (e.g., time strings should be '-1d', '-24h', or ISO 8601).";
132
+ }
133
+ }
134
+ else if (message.includes("status: 401")) {
135
+ errorMessage = "Unauthorized - Invalid or expired API token. Please check your IMPART_AUTH_TOKEN.";
136
+ }
137
+ else if (message.includes("status: 403")) {
138
+ const scopeMatch = message.match(/Insufficient scope|permissions/i);
139
+ if (scopeMatch) {
140
+ errorMessage = "Forbidden - Insufficient permissions. Your API token does not have the required scope for this operation.";
141
+ }
142
+ else {
143
+ errorMessage = "Forbidden - Access denied. Please check your API token has the correct permissions.";
144
+ }
145
+ }
146
+ else if (message.includes("status: 404")) {
147
+ errorMessage = "Not Found - The requested resource does not exist or may have been deleted.";
148
+ }
149
+ else if (message.includes("status: 422")) {
150
+ const bodyMatch = message.match(/status: 422 - (.+)/);
151
+ if (bodyMatch) {
152
+ errorMessage = `Validation Error - ${bodyMatch[1]}`;
153
+ }
154
+ else {
155
+ errorMessage = "Validation Error - One or more parameters failed validation.";
156
+ }
157
+ }
158
+ else if (message.includes("status: 429")) {
159
+ errorMessage = "Rate Limit Exceeded - Too many requests. Please wait before retrying.";
160
+ }
161
+ else if (message.includes("status: 500")) {
162
+ errorMessage = "Server Error - Internal server error. Please try again later.";
163
+ }
164
+ else if (message.includes("status: 503")) {
165
+ errorMessage = "Service Unavailable - API is temporarily unavailable. Please try again later.";
166
+ }
167
+ else {
168
+ errorMessage = message;
169
+ }
170
+ }
171
+ makeImpartRequest.lastError = errorMessage;
57
172
  return null;
58
173
  }
59
174
  }
175
+ // Helper function to generate tool names with orgId
176
+ function getToolName(baseName) {
177
+ return `${baseName}_${config_js_1.config.orgId.slice(-4)}`;
178
+ }
179
+ // Helper function to validate pagination parameters
180
+ function validatePagination(page, max_results) {
181
+ if (page !== null && page !== undefined && page < 1) {
182
+ return {
183
+ valid: false,
184
+ error: "Invalid pagination: 'page' must be >= 1. Example: page=1",
185
+ };
186
+ }
187
+ if (max_results !== null && max_results !== undefined) {
188
+ if (max_results < 1) {
189
+ return {
190
+ valid: false,
191
+ error: "Invalid pagination: 'max_results' must be >= 1. Example: max_results=10",
192
+ };
193
+ }
194
+ if (max_results > 1000) {
195
+ return {
196
+ valid: false,
197
+ error: "Invalid pagination: 'max_results' must be <= 1000. Maximum allowed is 1000.",
198
+ };
199
+ }
200
+ }
201
+ return { valid: true };
202
+ }
203
+ // Helper function to validate time format
204
+ function validateTimeFormat(time) {
205
+ // Relative time format: -1d, -24h, -7d, etc.
206
+ const relativeTimeRegex = /^-\d+[dhms]$/;
207
+ if (relativeTimeRegex.test(time)) {
208
+ return { valid: true };
209
+ }
210
+ // ISO 8601 timestamp
211
+ try {
212
+ const date = new Date(time);
213
+ if (!isNaN(date.getTime())) {
214
+ return { valid: true };
215
+ }
216
+ }
217
+ catch {
218
+ // Fall through to error
219
+ }
220
+ return {
221
+ valid: false,
222
+ error: `Invalid time format '${time}'. Use relative time (e.g., '-1d', '-24h', '-7d') or ISO 8601 timestamp (e.g., '2025-10-14T12:00:00Z').`,
223
+ };
224
+ }
60
225
  // Register tools with consistent naming patterns
61
- server.tool("list_inspectors", "Get a list of inspectors for the organization", {
62
- page: zod_1.z
63
- .number()
64
- .default(1)
65
- .describe("The page of results to return (minimum value: 1)"),
226
+ server.tool(getToolName("list_inspectors"), "Get a list of inspectors for the organization", {
227
+ page: zod_1.z.number().default(1).optional().describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
66
228
  max_results: zod_1.z
67
229
  .number()
68
230
  .default(100)
69
- .describe("The maximum number of results to return"),
231
+ .optional()
232
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
70
233
  }, async ({ page, max_results }) => {
71
- const queryParams = {
72
- page: validatePage(page),
73
- max_results,
74
- };
234
+ // Validate pagination parameters
235
+ const paginationCheck = validatePagination(page, max_results);
236
+ if (!paginationCheck.valid) {
237
+ return {
238
+ content: [{ type: "text", text: paginationCheck.error }],
239
+ };
240
+ }
241
+ const queryParams = {};
242
+ if (page !== null && page !== undefined) {
243
+ queryParams["page"] = page;
244
+ }
245
+ if (max_results !== null && max_results !== undefined) {
246
+ queryParams["max_results"] = max_results;
247
+ }
75
248
  const data = await makeImpartRequest("inspectors", queryParams);
76
249
  if (!data) {
250
+ const lastError = makeImpartRequest.lastError || "Unknown error";
77
251
  return {
78
- content: [
79
- { type: "text", text: "Unable to fetch inspectors: Unknown error" },
80
- ],
252
+ content: [{ type: "text", text: `Unable to fetch inspectors: ${lastError}` }],
81
253
  };
82
254
  }
83
255
  const inspectors = data.items || [];
84
256
  if (inspectors.length === 0) {
85
257
  return {
86
- content: [
87
- { type: "text", text: "No inspectors found for this organization." },
88
- ],
258
+ content: [{ type: "text", text: "No inspectors found for this organization." }],
89
259
  };
90
260
  }
91
261
  const formattedInspectors = inspectors.map((inspector) => [
92
262
  `ID: ${inspector.id}`,
93
263
  `Version: ${inspector.version}`,
94
- `Last Seen: ${inspector.last_seen}`,
264
+ `Last Seen: ${inspector.lastSeen ? inspector.lastSeen.toISOString() : "N/A"}`,
95
265
  "---",
96
266
  ].join("\n"));
97
267
  return {
@@ -99,54 +269,71 @@ server.tool("list_inspectors", "Get a list of inspectors for the organization",
99
269
  };
100
270
  });
101
271
  // Rules Script Tools
102
- server.tool("list_rules_scripts", "Get a list of rules scripts for your Impart organization", {
272
+ server.tool(getToolName("list_rules_scripts"), "Get a list of rules scripts for your Impart organization", {
103
273
  page: zod_1.z
104
274
  .number()
275
+ .optional()
105
276
  .default(1)
106
- .describe("The page of results to return (minimum value: 1)"),
277
+ .nullable()
278
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
107
279
  max_results: zod_1.z
108
280
  .number()
281
+ .optional()
109
282
  .default(100)
110
- .describe("The maximum number of results to return"),
283
+ .nullable()
284
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
111
285
  type: zod_1.z
112
- .string()
286
+ .enum(["custom", "core", "recipe"])
113
287
  .optional()
114
- .describe("Filter by type of rule script ('custom' or 'core')"),
115
- exclude_src: zod_1.z
116
- .boolean()
117
- .default(false)
118
- .describe("Whether to exclude the rule script source"),
288
+ .describe("Filter by type of rule script ('custom', 'core' or 'recipe')"),
289
+ exclude_src: zod_1.z.boolean().optional().default(false).nullable().describe("Whether to exclude the rule script source"),
119
290
  exclude_revisions: zod_1.z
120
291
  .boolean()
292
+ .optional()
121
293
  .default(false)
294
+ .nullable()
122
295
  .describe("Whether to exclude rule script revisions"),
123
- is_disabled: zod_1.z
124
- .string()
296
+ is_disabled: zod_1.z.string().optional().describe("Filter by disabled status ('true' or 'false')"),
297
+ label: zod_1.z
298
+ .array(zod_1.z.string())
125
299
  .optional()
126
- .describe("Filter by disabled status ('true' or 'false')"),
127
- label: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by label slugs"),
128
- }, async ({ page, max_results, type, exclude_src, exclude_revisions, is_disabled, label, }) => {
129
- const queryParams = {
130
- page: validatePage(page),
131
- max_results,
132
- type,
133
- exclude_src,
134
- exclude_revisions,
135
- is_disabled,
136
- label,
137
- };
300
+ .nullable()
301
+ .describe('Filter by label slugs (e.g., ["llm-protection", "waf"])'),
302
+ }, async ({ page, max_results, type, exclude_src, exclude_revisions, is_disabled, label }) => {
303
+ const queryParams = {};
304
+ if (page !== null && page !== undefined) {
305
+ queryParams["page"] = validatePage(page);
306
+ }
307
+ if (max_results !== null && max_results !== undefined) {
308
+ queryParams["max_results"] = max_results;
309
+ }
310
+ if (type !== null && type !== undefined) {
311
+ queryParams["type"] = type;
312
+ }
313
+ if (exclude_src !== null && exclude_src !== undefined) {
314
+ queryParams["exclude_src"] = exclude_src;
315
+ }
316
+ if (exclude_revisions !== null && exclude_revisions !== undefined) {
317
+ queryParams["exclude_revisions"] = exclude_revisions;
318
+ }
319
+ if (is_disabled !== null && is_disabled !== undefined) {
320
+ queryParams["is_disabled"] = is_disabled;
321
+ }
322
+ if (label !== null && label !== undefined) {
323
+ queryParams["label"] = label;
324
+ }
138
325
  const data = await makeImpartRequest("rules_scripts", queryParams);
139
326
  if (!data) {
140
327
  return {
141
328
  content: [
142
329
  {
143
330
  type: "text",
144
- text: "Unable to fetch rules scripts: Unknown error",
331
+ text: `Unable to fetch rules scripts: ${makeImpartRequest.lastError || "Unknown error"}`,
145
332
  },
146
333
  ],
147
334
  };
148
335
  }
149
- const scripts = data.items || [];
336
+ const scripts = data.items;
150
337
  if (scripts.length === 0) {
151
338
  return {
152
339
  content: [
@@ -157,46 +344,40 @@ server.tool("list_rules_scripts", "Get a list of rules scripts for your Impart o
157
344
  ],
158
345
  };
159
346
  }
160
- const formattedScripts = scripts.map((script) => [
161
- `ID: ${script.id}`,
162
- `Name: ${script.name}`,
163
- `Description: ${script.description ?? "N/A"}`,
164
- `Type: ${script.type}`,
165
- `Disabled: ${script.disabled}`,
166
- `Blocking Effect: ${script.blocking_effect}`,
167
- `Language: ${script.lang}`,
168
- `Revision: ${script.revision}`,
169
- `Labels: ${script.labels.join(", ")}`,
170
- `Created By: ${script.created_by}`,
171
- `Created At: ${script.created_at}`,
172
- `Updated By: ${script.updated_by}`,
173
- `Updated At: ${script.updated_at}`,
174
- "---",
175
- ].join("\n"));
347
+ // Limit display for performance
348
+ const maxDisplay = Math.min(scripts.length, 15);
349
+ const showingLimited = scripts.length > maxDisplay;
350
+ const result = [
351
+ `Found ${scripts.length} rules scripts`,
352
+ showingLimited ? `Showing first ${maxDisplay} for performance. Use filters to narrow results.` : "",
353
+ "=".repeat(50),
354
+ "",
355
+ ];
356
+ scripts.slice(0, maxDisplay).forEach((script, index) => {
357
+ result.push(`${index + 1}. ${script.name} (${script.type})`, ` ID: ${script.id} | Rev: ${script.revision} | ${script.disabled ? "DISABLED" : "ENABLED"}`, ` Labels: ${script.labels.length > 0 ? script.labels.join(", ") : "None"}`, ` Created: ${script.createdAt ? script.createdAt.toISOString() : "N/A"}`, "");
358
+ });
176
359
  return {
177
- content: [{ type: "text", text: formattedScripts.join("\n") }],
360
+ content: [{ type: "text", text: result.join("\n") }],
178
361
  };
179
362
  });
180
- server.tool("get_rules_script", "Get details of a specific rules script", {
181
- rules_script_id: zod_1.z
182
- .string()
183
- .describe("The unique identifier of the rules script"),
184
- revision: zod_1.z.number().optional().describe("The specific revision to fetch"),
363
+ server.tool(getToolName("get_rules_script"), "Get details of a specific rules script", {
364
+ rules_script_id: zod_1.z.string().describe("The unique identifier of the rules script"),
365
+ revision: zod_1.z.number().optional().nullable().describe("The specific revision to fetch"),
185
366
  }, async ({ rules_script_id, revision }) => {
186
367
  if (!rules_script_id) {
187
368
  return {
188
- content: [
189
- { type: "text", text: "Error: rules_script_id is required." },
190
- ],
369
+ content: [{ type: "text", text: "Error: rules_script_id is required." }],
191
370
  };
192
371
  }
193
- const queryParams = revision ? { revision } : undefined;
372
+ const queryParams = {};
373
+ if (revision !== null && revision !== undefined) {
374
+ queryParams["revision"] = revision;
375
+ }
194
376
  const data = await makeImpartRequest(`rules_scripts/${rules_script_id}`, queryParams);
195
377
  if (!data) {
378
+ const lastError = makeImpartRequest.lastError || "Unknown error";
196
379
  return {
197
- content: [
198
- { type: "text", text: "Unable to fetch rules script: Unknown error" },
199
- ],
380
+ content: [{ type: "text", text: `Unable to fetch rules script: ${lastError}` }],
200
381
  };
201
382
  }
202
383
  const formattedScript = [
@@ -205,257 +386,316 @@ server.tool("get_rules_script", "Get details of a specific rules script", {
205
386
  `Description: ${data.description ?? "N/A"}`,
206
387
  `Type: ${data.type}`,
207
388
  `Disabled: ${data.disabled}`,
208
- `Blocking Effect: ${data.blocking_effect}`,
389
+ `Blocking Effect: ${data.blockingEffect}`,
209
390
  `Language: ${data.lang}`,
210
391
  `Source Code: ${data.src ?? "N/A"}`,
211
- `Autogenerated Explanation: ${data.autogenerated_explanation ?? "N/A"}`,
392
+ `Autogenerated Explanation: ${data.autogeneratedExplanation}`,
212
393
  `Revision: ${data.revision}`,
213
394
  `Dependencies: ${(data.dependencies ?? []).join(", ")}`,
214
395
  `Labels: ${data.labels.join(", ")}`,
215
- `Created By: ${data.created_by}`,
216
- `Created At: ${data.created_at}`,
217
- `Updated By: ${data.updated_by}`,
218
- `Updated At: ${data.updated_at}`,
396
+ `Created By: ${data.createdBy}`,
397
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
398
+ `Updated By: ${data.updatedBy}`,
399
+ `Updated At: ${data.updatedAt?.toISOString() ?? "N/A"}`,
219
400
  "---",
220
401
  "Revisions:",
221
402
  ];
222
403
  data.revisions.forEach((revision) => {
223
- formattedScript.push(` Revision: ${revision.revision}`, ` Created By: ${revision.created_by}`, ` Created At: ${revision.created_at}`, "---");
404
+ formattedScript.push(` Revision: ${revision.revision}`, ` Created By: ${revision.createdBy}`, ` Created At: ${revision.createdAt ? revision.createdAt.toISOString() : "N/A"}`, "---");
224
405
  });
225
406
  return {
226
407
  content: [{ type: "text", text: formattedScript.join("\n") }],
227
408
  };
228
409
  });
229
- server.tool("create_rules_script", "Creates a new rules script for the organization", {
230
- name: zod_1.z.string().describe("The name of the rules script"),
231
- src: zod_1.z
232
- .string()
233
- .describe("The base64-encoded source code of the rules script"),
234
- lang: zod_1.z
235
- .string()
236
- .default("assemblyscript")
237
- .describe("The language of the script"),
238
- description: zod_1.z
239
- .string()
410
+ // Rule Recipe Tools
411
+ // -----------------
412
+ server.tool(getToolName("list_rule_recipes"), "Get a list of rule recipes for your Impart organization", {
413
+ page: zod_1.z
414
+ .number()
415
+ .default(1)
240
416
  .optional()
241
- .describe("Optional description of the script"),
242
- blocking_effect: zod_1.z
243
- .string()
244
- .default("block")
245
- .describe("Effect when rule blocks"),
246
- disabled: zod_1.z
247
- .boolean()
248
- .default(false)
249
- .describe("Whether the script is disabled"),
250
- labels: zod_1.z
251
- .array(zod_1.z.string())
417
+ .nullable()
418
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
419
+ max_results: zod_1.z
420
+ .number()
421
+ .default(100)
252
422
  .optional()
253
- .describe("Optional list of label slugs"),
254
- }, async ({ name, src, lang, description, blocking_effect, disabled, labels, }) => {
255
- const payload = {
256
- name,
257
- src,
258
- lang,
259
- description,
260
- blocking_effect,
261
- disabled,
262
- labels: labels ?? [],
263
- };
264
- const data = await makeImpartRequest("rules_scripts", undefined, "POST", payload);
423
+ .nullable()
424
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
425
+ search: zod_1.z.string().optional().describe("Search string to filter rule recipes"),
426
+ }, async ({ page, max_results, search }) => {
427
+ // Validate pagination parameters
428
+ const paginationCheck = validatePagination(page, max_results);
429
+ if (!paginationCheck.valid) {
430
+ return { content: [{ type: "text", text: paginationCheck.error }] };
431
+ }
432
+ const queryParams = {};
433
+ if (page !== null && page !== undefined) {
434
+ queryParams["page"] = page;
435
+ }
436
+ if (max_results !== null && max_results !== undefined) {
437
+ queryParams["max_results"] = max_results;
438
+ }
439
+ if (search !== null && search !== undefined) {
440
+ queryParams["search"] = search;
441
+ }
442
+ const data = await makeImpartRequest("rule_recipes", queryParams);
265
443
  if (!data) {
444
+ const lastError = makeImpartRequest.lastError || "Unknown error";
266
445
  return {
267
- content: [{ type: "text", text: "Failed to create rules script" }],
446
+ content: [{ type: "text", text: `Unable to fetch rule recipes: ${lastError}` }],
268
447
  };
269
448
  }
270
- const formattedScript = [
271
- "Rules script created successfully:",
272
- `ID: ${data.id}`,
273
- `Name: ${data.name}`,
274
- `Description: ${data.description ?? "N/A"}`,
275
- `Language: ${data.lang}`,
276
- `Blocking Effect: ${data.blocking_effect}`,
277
- `Disabled: ${data.disabled}`,
278
- `Labels: ${data.labels.join(", ")}`,
279
- `Created At: ${data.created_at}`,
449
+ const recipes = data.items ?? [];
450
+ if (recipes.length === 0) {
451
+ return {
452
+ content: [{ type: "text", text: "No rule recipes found." }],
453
+ };
454
+ }
455
+ const formattedRecipes = recipes.map((recipe) => [
456
+ `ID: ${recipe.id}`,
457
+ `Name: ${recipe.name}`,
458
+ `Description: ${recipe.description}`,
459
+ `Disabled: ${recipe.disabled}`,
460
+ `Blocking Effect: ${recipe.blockingEffect}`,
461
+ `Labels: ${recipe.labels.join(", ")}`,
462
+ `Dependencies: ${recipe.dependencies.join(", ")}`,
463
+ `Created By: ${recipe.createdBy}`,
464
+ `Created At: ${recipe.createdAt ? recipe.createdAt.toISOString() : "N/A"}`,
465
+ `Updated At: ${recipe.updatedAt?.toISOString() ?? "N/A"}`,
280
466
  "---",
281
- ].join("\n");
467
+ ].join("\n"));
282
468
  return {
283
- content: [{ type: "text", text: formattedScript }],
469
+ content: [{ type: "text", text: formattedRecipes.join("\n") }],
284
470
  };
285
471
  });
286
- server.tool("update_rules_script", "Updates an existing rules script", {
287
- rules_script_id: zod_1.z
288
- .string()
289
- .describe("The ID of the rules script to update"),
290
- name: zod_1.z.string().describe("The name of the rules script"),
291
- src: zod_1.z.string().describe("The source code of the rules script"),
292
- lang: zod_1.z
293
- .string()
294
- .default("assemblyscript")
295
- .describe("The language of the script"),
296
- description: zod_1.z
297
- .string()
298
- .optional()
299
- .describe("Optional description of the script"),
300
- blocking_effect: zod_1.z
301
- .string()
302
- .default("block")
303
- .describe("Effect when rule blocks"),
304
- disabled: zod_1.z
305
- .boolean()
306
- .default(false)
307
- .describe("Whether the script is disabled"),
308
- labels: zod_1.z
309
- .array(zod_1.z.string())
310
- .optional()
311
- .describe("Optional list of label slugs"),
312
- }, async ({ rules_script_id, name, src, lang, description, blocking_effect, disabled, labels, }) => {
313
- if (!rules_script_id) {
472
+ server.tool(getToolName("get_rule_recipe"), "Get details of a specific rule recipe", {
473
+ rule_recipe_id: zod_1.z.string().describe("The unique identifier of the rule recipe"),
474
+ }, async ({ rule_recipe_id }) => {
475
+ if (!rule_recipe_id) {
314
476
  return {
315
- content: [
316
- { type: "text", text: "Error: rules_script_id is required." },
317
- ],
477
+ content: [{ type: "text", text: "Error: rule_recipe_id is required." }],
318
478
  };
319
479
  }
320
- const payload = {
321
- name,
322
- src,
323
- lang,
324
- description,
325
- blocking_effect,
326
- disabled,
327
- labels: labels ?? [],
328
- };
329
- const data = await makeImpartRequest(`rules_scripts/${rules_script_id}`, undefined, "PUT", payload);
480
+ const data = await makeImpartRequest(`rule_recipes/${rule_recipe_id}`, undefined);
330
481
  if (!data) {
482
+ const lastError = makeImpartRequest.lastError || "Unknown error";
331
483
  return {
332
- content: [{ type: "text", text: "Failed to update rules script" }],
484
+ content: [{ type: "text", text: `Unable to fetch rule recipe: ${lastError}` }],
333
485
  };
334
486
  }
335
- const formattedScript = [
336
- "Rules script updated successfully:",
487
+ const formattedRecipe = [
337
488
  `ID: ${data.id}`,
338
489
  `Name: ${data.name}`,
339
- `Description: ${data.description ?? "N/A"}`,
340
- `Language: ${data.lang}`,
341
- `Blocking Effect: ${data.blocking_effect}`,
490
+ `Description: ${data.description}`,
342
491
  `Disabled: ${data.disabled}`,
492
+ `Blocking Effect: ${data.blockingEffect}`,
343
493
  `Labels: ${data.labels.join(", ")}`,
344
- `Updated At: ${data.updated_at}`,
345
- "---",
494
+ `Dependencies: ${data.dependencies.join(", ")}`,
495
+ `Components:`,
496
+ ` Rate Limit: ${JSON.stringify(data.components.rate_limit, null, 2)}`,
497
+ ` Inspect Request: ${JSON.stringify(data.components.inspect_request, null, 2)}`,
498
+ ` Enforce: ${JSON.stringify(data.components.enforce, null, 2)}`,
499
+ ` Inspect Response: ${JSON.stringify(data.components.inspect_response, null, 2)}`,
500
+ `Created By: ${data.createdBy}`,
501
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
502
+ `Updated By: ${data.updatedBy ?? "N/A"}`,
503
+ `Updated At: ${data.updatedAt?.toISOString() ?? "N/A"}`,
346
504
  ].join("\n");
347
505
  return {
348
- content: [{ type: "text", text: formattedScript }],
506
+ content: [{ type: "text", text: formattedRecipe }],
349
507
  };
350
508
  });
351
- server.tool("write_rule_script", "Generate a rule script using Impart's LLM", {
352
- query: zod_1.z.string().describe("Description of the rule you want to generate"),
509
+ server.tool(getToolName("generate_rule_recipe"), "Generate a rule recipe configuration using Impart's AI (Sparkle). Returns a JSON configuration that can be saved as a rule recipe.", {
510
+ query: zod_1.z
511
+ .string()
512
+ .describe('Natural language description of the rule recipe to generate (e.g., "Create a rate limit rule that blocks requests after 100 requests per minute")'),
353
513
  }, async ({ query }) => {
514
+ if (!query) {
515
+ return {
516
+ content: [{ type: "text", text: "Error: query is required." }],
517
+ };
518
+ }
519
+ // The Sparkle API expects the query field to be a JSON-encoded string
520
+ // containing the agent-specific input format: {"prompt": "..."}
521
+ const agentQuery = JSON.stringify({
522
+ prompt: query,
523
+ });
354
524
  const payload = {
355
- agent: "rule_generator",
356
- query,
525
+ agent: "rule_recipe_generator",
526
+ query: agentQuery,
357
527
  };
528
+ console.error(`[generate_rule_recipe] Calling Sparkle API with payload:`, JSON.stringify(payload, null, 2));
358
529
  const data = await makeImpartRequest("sparkle", undefined, "POST", payload);
530
+ console.error(`[generate_rule_recipe] Response received:`, data ? "Success" : "Failure");
359
531
  if (!data) {
532
+ const lastError = makeImpartRequest.lastError || "Unknown error";
360
533
  return {
361
- content: [
362
- { type: "text", text: "Error: Failed to generate rule script" },
363
- ],
534
+ content: [{ type: "text", text: `Unable to generate rule recipe: ${lastError}` }],
364
535
  };
365
536
  }
366
537
  if (!data.output) {
367
538
  return {
368
- content: [
369
- {
370
- type: "text",
371
- text: "Error: No output received from the Sparkle API",
372
- },
373
- ],
539
+ content: [{ type: "text", text: "Error: No output received from the Sparkle API" }],
374
540
  };
375
541
  }
376
542
  return {
377
543
  content: [
378
- { type: "text", text: `Generated Rule Script:\n\n${data.output}` },
544
+ {
545
+ type: "text",
546
+ text: `Generated Rule Recipe Configuration:\n\n${data.output}\n\nYou can save this configuration using the create_rule_recipe tool.`,
547
+ },
379
548
  ],
380
549
  };
381
550
  });
382
- server.tool("validate_rules_script", "Validate a rules script", {
383
- src: zod_1.z
384
- .string()
385
- .describe("The base64-encoded source code of the rules script to validate"),
386
- lang: zod_1.z
551
+ server.tool(getToolName("create_rule_recipe"), "Create a new rule recipe from a configuration. IMPORTANT: The 'components' field requires a complex nested structure with clauses, thresholds, conditions, and actions. You MUST use the output from generate_rule_recipe (Sparkle AI) or copy from an existing rule recipe - do not manually create the components structure as it has a very specific schema.", {
552
+ name: zod_1.z.string().min(1).max(64).describe("The name of the rule recipe"),
553
+ description: zod_1.z.string().min(1).max(1024).optional().describe("Optional description of the rule recipe"),
554
+ disabled: zod_1.z.boolean().default(false).describe("Whether the rule recipe is disabled"),
555
+ blocking_effect: zod_1.z
556
+ .enum(["block", "simulate"])
557
+ .default("block")
558
+ .optional()
559
+ .describe("Effect when rule blocks (block or simulate)"),
560
+ labels: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of label slugs"),
561
+ components: zod_1.z
387
562
  .string()
388
- .default("assemblyscript")
389
- .describe("The language of the rules script"),
390
- }, async ({ src, lang }) => {
391
- if (!src) {
563
+ .describe("JSON string containing the rule recipe components (rate_limit, inspect_request, enforce, inspect_response)"),
564
+ }, async ({ name, description, disabled, blocking_effect, labels, components }) => {
565
+ if (!name || !components) {
392
566
  return {
393
- content: [
394
- {
395
- type: "text",
396
- text: "Error: The source code (src) is required and must be base64 encoded.",
397
- },
398
- ],
567
+ content: [{ type: "text", text: "Error: name and components are required." }],
399
568
  };
400
569
  }
401
- const payload = {
402
- src,
403
- lang,
404
- };
405
- const data = await makeImpartRequest("rules_script_validate", undefined, "POST", payload);
406
- if (!data) {
570
+ let parsedComponents;
571
+ try {
572
+ parsedComponents = JSON.parse(components);
573
+ }
574
+ catch (e) {
407
575
  return {
408
- content: [{ type: "text", text: "Failed to validate rules script" }],
576
+ content: [{ type: "text", text: "Error: components must be valid JSON" }],
409
577
  };
410
578
  }
411
- const diagnostics = data.diagnostics || [];
412
- if (diagnostics.length === 0) {
579
+ // Remove blocking_effect from components if present - it should be a top-level field
580
+ if (parsedComponents.blocking_effect) {
581
+ delete parsedComponents.blocking_effect;
582
+ }
583
+ if (parsedComponents.blockingEffect) {
584
+ delete parsedComponents.blockingEffect;
585
+ }
586
+ const payload = {
587
+ name,
588
+ disabled,
589
+ components: parsedComponents,
590
+ };
591
+ if (description !== null && description !== undefined) {
592
+ payload["description"] = description;
593
+ }
594
+ if (blocking_effect !== null && blocking_effect !== undefined) {
595
+ payload["blocking_effect"] = blocking_effect;
596
+ }
597
+ if (labels !== null && labels !== undefined) {
598
+ payload["labels"] = labels;
599
+ }
600
+ console.error(`[create_rule_recipe] Payload:`, JSON.stringify(payload, null, 2));
601
+ const data = await makeImpartRequest("rule_recipes", undefined, "POST", payload);
602
+ if (!data) {
603
+ const lastError = makeImpartRequest.lastError || "Unknown error";
413
604
  return {
414
- content: [
415
- {
416
- type: "text",
417
- text: "Validation successful! No errors or warnings found.",
418
- },
419
- ],
605
+ content: [{ type: "text", text: `Unable to create rule recipe: ${lastError}` }],
420
606
  };
421
607
  }
422
- const formattedDiagnostics = diagnostics.map((diagnostic) => `${diagnostic.severity.toUpperCase()} at line ${diagnostic.line}, column ${diagnostic.column}: ${diagnostic.message}`);
608
+ const formattedRecipe = [
609
+ "Rule recipe created successfully:",
610
+ `ID: ${data.id}`,
611
+ `Name: ${data.name}`,
612
+ `Description: ${data.description ?? "N/A"}`,
613
+ `Disabled: ${data.disabled}`,
614
+ `Blocking Effect: ${data.blockingEffect}`,
615
+ `Labels: ${data.labels.join(", ")}`,
616
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
617
+ ].join("\n");
423
618
  return {
424
- content: [
425
- {
426
- type: "text",
427
- text: "Validation completed with the following issues:\n" +
428
- formattedDiagnostics.join("\n"),
429
- },
430
- ],
619
+ content: [{ type: "text", text: formattedRecipe }],
431
620
  };
432
621
  });
433
- server.tool("list_endpoints", "Get an inventory of endpoints for the organization", {
622
+ server.tool(getToolName("list_endpoints"), "Get an inventory of endpoints for the organization", {
623
+ page: zod_1.z
624
+ .number()
625
+ .default(1)
626
+ .optional()
627
+ .nullable()
628
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
434
629
  max_results: zod_1.z
435
630
  .number()
436
631
  .default(100)
437
- .describe("The maximum number of results to return"),
438
- }, async ({ max_results }) => {
439
- const queryParams = { max_results };
632
+ .optional()
633
+ .nullable()
634
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
635
+ is_deprecated: zod_1.z.enum(["true", "false"]).optional().describe("Filter by deprecated status"),
636
+ is_orphaned: zod_1.z.enum(["true", "false"]).optional().describe("Filter by orphaned status"),
637
+ source: zod_1.z.enum(["learned", "provided"]).optional().describe("Filter by source"),
638
+ search: zod_1.z.string().optional().describe("Filter by search string"),
639
+ spec_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Filter by spec IDs"),
640
+ collection_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Filter by collection IDs"),
641
+ tag: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Filter by tags"),
642
+ method: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Filter by methods"),
643
+ label: zod_1.z
644
+ .array(zod_1.z.string())
645
+ .optional()
646
+ .nullable()
647
+ .describe('Filter by label slugs (e.g., ["llm-protection", "waf"])'),
648
+ }, async ({ page, max_results, is_deprecated, is_orphaned, source, search, spec_id, collection_id, tag, method, label, }) => {
649
+ const queryParams = {};
650
+ if (page !== null && page !== undefined) {
651
+ queryParams["page"] = page;
652
+ }
653
+ if (max_results !== null && max_results !== undefined) {
654
+ queryParams["max_results"] = max_results;
655
+ }
656
+ if (is_deprecated !== null && is_deprecated !== undefined) {
657
+ queryParams["is_deprecated"] = is_deprecated;
658
+ }
659
+ if (is_orphaned !== null && is_orphaned !== undefined) {
660
+ queryParams["is_orphaned"] = is_orphaned;
661
+ }
662
+ if (source !== null && source !== undefined) {
663
+ queryParams["source"] = source;
664
+ }
665
+ if (search !== null && search !== undefined) {
666
+ queryParams["search"] = search;
667
+ }
668
+ if (spec_id !== null && spec_id !== undefined) {
669
+ queryParams["spec_id"] = spec_id;
670
+ }
671
+ if (collection_id !== null && collection_id !== undefined) {
672
+ queryParams["collection_id"] = collection_id;
673
+ }
674
+ if (tag !== null && tag !== undefined) {
675
+ queryParams["tag"] = tag;
676
+ }
677
+ if (method !== null && method !== undefined) {
678
+ queryParams["method"] = method;
679
+ }
680
+ if (label !== null && label !== undefined) {
681
+ queryParams["label"] = label;
682
+ }
440
683
  const data = await makeImpartRequest("endpoints", queryParams);
441
684
  if (!data) {
685
+ const lastError = makeImpartRequest.lastError || "Unknown error";
442
686
  return {
443
- content: [
444
- { type: "text", text: "Unable to fetch endpoints: Unknown error" },
445
- ],
687
+ content: [{ type: "text", text: `Unable to fetch endpoints: ${lastError}` }],
446
688
  };
447
689
  }
448
690
  const endpoints = data.items || [];
449
691
  if (endpoints.length === 0) {
450
692
  return {
451
- content: [
452
- { type: "text", text: "No endpoints found for this organization." },
453
- ],
693
+ content: [{ type: "text", text: "No endpoints found for this organization." }],
454
694
  };
455
695
  }
456
696
  const formattedEndpoints = endpoints.map((endpoint) => [
457
697
  `ID: ${endpoint.id}`,
458
- `Spec ID: ${endpoint.spec_id}`,
698
+ `Spec ID: ${endpoint.specId}`,
459
699
  `Path: ${endpoint.path}`,
460
700
  `Method: ${endpoint.method}`,
461
701
  `Source: ${endpoint.source}`,
@@ -466,36 +706,55 @@ server.tool("list_endpoints", "Get an inventory of endpoints for the organizatio
466
706
  content: [{ type: "text", text: formattedEndpoints.join("\n") }],
467
707
  };
468
708
  });
469
- server.tool("list_tags", "Get a list of tags for your Impart organization", {
709
+ server.tool(getToolName("list_tags"), "Get a list of tags for your Impart organization", {
710
+ page: zod_1.z
711
+ .number()
712
+ .default(1)
713
+ .optional()
714
+ .nullable()
715
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
470
716
  max_results: zod_1.z
471
717
  .number()
472
718
  .default(100)
473
- .describe("The maximum number of results to return"),
474
- }, async ({ max_results }) => {
475
- const queryParams = { max_results };
719
+ .optional()
720
+ .nullable()
721
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
722
+ label: zod_1.z
723
+ .array(zod_1.z.string())
724
+ .optional()
725
+ .nullable()
726
+ .describe('Filter by label slugs (e.g., ["llm-protection", "waf"])'),
727
+ }, async ({ page, max_results, label }) => {
728
+ const queryParams = {};
729
+ if (page !== null && page !== undefined) {
730
+ queryParams["page"] = page;
731
+ }
732
+ if (max_results !== null && max_results !== undefined) {
733
+ queryParams["max_results"] = max_results;
734
+ }
735
+ if (label !== null && label !== undefined) {
736
+ queryParams["label"] = label;
737
+ }
476
738
  const data = await makeImpartRequest("tags", queryParams);
477
739
  if (!data) {
740
+ const lastError = makeImpartRequest.lastError || "Unknown error";
478
741
  return {
479
- content: [
480
- { type: "text", text: "Unable to fetch tags: Unknown error" },
481
- ],
742
+ content: [{ type: "text", text: `Unable to fetch tags: ${lastError}` }],
482
743
  };
483
744
  }
484
745
  const tags = data.items || [];
485
746
  if (tags.length === 0) {
486
747
  return {
487
- content: [
488
- { type: "text", text: "No tags found for this organization." },
489
- ],
748
+ content: [{ type: "text", text: "No tags found for this organization." }],
490
749
  };
491
750
  }
492
751
  const formattedTags = tags.map((tag) => [
493
752
  `Name: ${tag.name}`,
494
- `Display Name: ${tag.display_name}`,
753
+ `Display Name: ${tag.displayName}`,
495
754
  `Type: ${tag.type}`,
496
755
  `Description: ${tag.description ?? "N/A"}`,
497
- `Risk Statement: ${tag.risk_statement ?? "N/A"}`,
498
- `External URL: ${tag.external_url ?? "N/A"}`,
756
+ `Risk Statement: ${tag.riskStatement ?? "N/A"}`,
757
+ `External URL: ${tag.externalUrl ?? "N/A"}`,
499
758
  `Category: ${tag.category}`,
500
759
  "---",
501
760
  ].join("\n"));
@@ -503,56 +762,98 @@ server.tool("list_tags", "Get a list of tags for your Impart organization", {
503
762
  content: [{ type: "text", text: formattedTags.join("\n") }],
504
763
  };
505
764
  });
506
- server.tool("get_tag_metrics", "Get timeseries metrics for one or more tags", {
765
+ server.tool(getToolName("get_tag_metrics"), "Get timeseries metrics for one or more tags. Returns multiple time series datasets when multiple metrics are requested. Examples: \n\n" +
766
+ '• Single metric: {"metric": ["http-request.count"]}\n' +
767
+ '• Multiple metrics: {"metric": ["http-request.count", "attack.count", "blocked.count"]}\n' +
768
+ '• With time range: {"metric": ["http-request.count"], "from": "-7d", "until": "-0h"}\n' +
769
+ '• With filtering: {"metric": ["attack.count"], "spec_id": ["spec-123"], "rollup": "1h"}\n' +
770
+ '• Complex query: {"metric": ["http-request.count", "blocked.count"], "from": "-24h", "rollup": "1h", "unlearned": "exclude"}', {
771
+ max_results: zod_1.z
772
+ .number()
773
+ .default(100)
774
+ .nullable()
775
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
507
776
  metric: zod_1.z
508
777
  .array(zod_1.z.string())
509
778
  .default(["http-request.count"])
510
- .describe("List of metric names"),
511
- from_time: zod_1.z.string().default("-1d").describe("Start time"),
512
- until_time: zod_1.z.string().default("-0h").describe("End time"),
513
- spec_id: zod_1.z
514
- .array(zod_1.z.string())
515
- .optional()
516
- .describe("Optional list of spec IDs"),
517
- collection_id: zod_1.z
518
- .array(zod_1.z.string())
519
779
  .optional()
520
- .describe("Optional list of collection IDs"),
521
- endpoint_id: zod_1.z
522
- .array(zod_1.z.string())
780
+ .nullable()
781
+ .describe("List of metric names. Common metrics: 'http-request.count', 'attack.count', 'blocked.count', 'allowed.count'"),
782
+ from: zod_1.z
783
+ .string()
784
+ .default("-1d")
523
785
  .optional()
524
- .describe("Optional list of endpoint IDs"),
525
- rollup: zod_1.z
786
+ .nullable()
787
+ .describe("Start time (relative like '-1d', '-24h', '-7d' or absolute timestamp)"),
788
+ until: zod_1.z
526
789
  .string()
790
+ .default("-0h")
527
791
  .optional()
528
- .describe("Optional time window rollup resolution"),
792
+ .nullable()
793
+ .describe("End time (relative like '-0h', '-1h' or absolute timestamp)"),
794
+ spec_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of spec IDs to filter by"),
795
+ collection_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of collection IDs to filter by"),
796
+ endpoint_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of endpoint IDs to filter by"),
797
+ rollup: zod_1.z.string().optional().describe("Time window rollup resolution (e.g., '1h', '15m', '1d')"),
529
798
  unlearned: zod_1.z
530
- .string()
799
+ .enum(["exclude", "include", "only"])
531
800
  .default("include")
532
- .describe("How to handle unlearned endpoints"),
533
- max_results: zod_1.z
534
- .number()
535
- .default(100)
536
- .describe("The maximum number of results to return"),
537
- }, async ({ metric, from_time, until_time, spec_id, collection_id, endpoint_id, rollup, unlearned, max_results, }) => {
538
- const queryParams = {
539
- metric,
540
- from: from_time,
541
- until: until_time,
542
- spec_id,
543
- collection_id,
544
- endpoint_id,
545
- rollup,
546
- unlearned,
547
- max_results,
548
- };
801
+ .optional()
802
+ .describe("How to handle unlearned endpoints: 'exclude' (ignore), 'include' (default), 'only' (only unlearned)"),
803
+ }, async ({ metric, from, until, spec_id, collection_id, endpoint_id, rollup, unlearned, max_results }) => {
804
+ // Validate time format parameters
805
+ if (from) {
806
+ const fromCheck = validateTimeFormat(from);
807
+ if (!fromCheck.valid) {
808
+ return {
809
+ content: [{ type: "text", text: fromCheck.error }],
810
+ };
811
+ }
812
+ }
813
+ if (until) {
814
+ const untilCheck = validateTimeFormat(until);
815
+ if (!untilCheck.valid) {
816
+ return {
817
+ content: [{ type: "text", text: untilCheck.error }],
818
+ };
819
+ }
820
+ }
821
+ const queryParams = {};
822
+ if (metric !== null && metric !== undefined) {
823
+ // Convert array to comma-separated string for metrics API
824
+ queryParams["metric"] = Array.isArray(metric) ? metric.join(",") : metric;
825
+ }
826
+ if (from !== null && from !== undefined) {
827
+ queryParams["from"] = from;
828
+ }
829
+ if (until !== null && until !== undefined) {
830
+ queryParams["until"] = until;
831
+ }
832
+ if (spec_id !== null && spec_id !== undefined) {
833
+ queryParams["spec_id"] = spec_id;
834
+ }
835
+ if (collection_id !== null && collection_id !== undefined) {
836
+ queryParams["collection_id"] = collection_id;
837
+ }
838
+ if (endpoint_id !== null && endpoint_id !== undefined) {
839
+ queryParams["endpoint_id"] = endpoint_id;
840
+ }
841
+ if (rollup !== null && rollup !== undefined) {
842
+ queryParams["rollup"] = rollup;
843
+ }
844
+ if (unlearned !== null && unlearned !== undefined) {
845
+ queryParams["unlearned"] = unlearned;
846
+ }
847
+ if (max_results !== null && max_results !== undefined) {
848
+ queryParams["max_results"] = max_results;
849
+ }
549
850
  const data = await makeImpartRequest("timeseries/tags", queryParams);
550
851
  if (!data) {
551
852
  return {
552
853
  content: [
553
854
  {
554
855
  type: "text",
555
- text: "Unable to fetch timeseries metrics: Unknown error",
856
+ text: `Unable to fetch timeseries metrics: ${makeImpartRequest.lastError || "Unknown error"}`,
556
857
  },
557
858
  ],
558
859
  };
@@ -570,10 +871,10 @@ server.tool("get_tag_metrics", "Get timeseries metrics for one or more tags", {
570
871
  }
571
872
  const formattedMetrics = metrics.map((metric) => [
572
873
  `Metric: ${metric.metric}`,
573
- `From: ${metric.from}`,
574
- `Until: ${metric.until}`,
874
+ `From: ${metric.from ? metric.from.toISOString() : "N/A"}`,
875
+ `Until: ${metric.until ? metric.until.toISOString() : "N/A"}`,
575
876
  `Increment: ${metric.inc} seconds`,
576
- `Total Points: ${metric.total_points}`,
877
+ `Total Points: ${metric.totalPoints}`,
577
878
  `Data: ${metric.data.join(", ")}`,
578
879
  "---",
579
880
  ].join("\n"));
@@ -581,27 +882,34 @@ server.tool("get_tag_metrics", "Get timeseries metrics for one or more tags", {
581
882
  content: [{ type: "text", text: formattedMetrics.join("\n") }],
582
883
  };
583
884
  });
584
- server.tool("list_observed_hosts", "Get a list of observed hosts for your Impart organization", {
885
+ server.tool(getToolName("list_observed_hosts"), "Get a list of observed hosts for your Impart organization", {
585
886
  page: zod_1.z
586
887
  .number()
587
888
  .default(1)
588
- .describe("The page of results to return (minimum value: 1)"),
889
+ .optional()
890
+ .nullable()
891
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
589
892
  max_results: zod_1.z
590
893
  .number()
591
894
  .default(100)
592
- .describe("The maximum number of results to return"),
895
+ .optional()
896
+ .nullable()
897
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
593
898
  }, async ({ page, max_results }) => {
594
- const queryParams = {
595
- page: validatePage(page),
596
- max_results,
597
- };
899
+ const queryParams = {};
900
+ if (page !== null && page !== undefined) {
901
+ queryParams["page"] = validatePage(page);
902
+ }
903
+ if (max_results !== null && max_results !== undefined) {
904
+ queryParams["max_results"] = max_results;
905
+ }
598
906
  const data = await makeImpartRequest("observed_hosts", queryParams);
599
907
  if (!data) {
600
908
  return {
601
909
  content: [
602
910
  {
603
911
  type: "text",
604
- text: "Unable to fetch observed hosts: Unknown error",
912
+ text: `Unable to fetch observed hosts: ${makeImpartRequest.lastError || "Unknown error"}`,
605
913
  },
606
914
  ],
607
915
  };
@@ -619,25 +927,24 @@ server.tool("list_observed_hosts", "Get a list of observed hosts for your Impart
619
927
  }
620
928
  const formattedHosts = hosts.map((host) => [
621
929
  `ID: ${host.id}`,
622
- `Name: ${host.name || "N/A"}`,
623
- `IP Address: ${host.ip_address || "N/A"}`,
624
- `Last Seen: ${host.last_seen || "N/A"}`,
930
+ `Hostname: ${host.hostname}`,
931
+ `Port: ${host.port}`,
932
+ `Created At: ${host.createdAt ? host.createdAt.toISOString() : "N/A"}`,
933
+ `Observed At: ${host.observedAt ? host.observedAt.toISOString() : "N/A"}`,
934
+ `Suspected: ${host.suspected}`,
935
+ `Env: ${host.env}`,
625
936
  "---",
626
937
  ].join("\n"));
627
938
  return {
628
939
  content: [{ type: "text", text: formattedHosts.join("\n") }],
629
940
  };
630
941
  });
631
- server.tool("get_observed_host", "Get details of a specific observed host", {
632
- observed_host_id: zod_1.z
633
- .string()
634
- .describe("The unique identifier of the observed host"),
942
+ server.tool(getToolName("get_observed_host"), "Get details of a specific observed host", {
943
+ observed_host_id: zod_1.z.string().describe("The unique identifier of the observed host"),
635
944
  }, async ({ observed_host_id }) => {
636
945
  if (!observed_host_id) {
637
946
  return {
638
- content: [
639
- { type: "text", text: "Error: observed_host_id is required." },
640
- ],
947
+ content: [{ type: "text", text: "Error: observed_host_id is required." }],
641
948
  };
642
949
  }
643
950
  const data = await makeImpartRequest(`observed_hosts/${observed_host_id}`);
@@ -646,23 +953,24 @@ server.tool("get_observed_host", "Get details of a specific observed host", {
646
953
  content: [
647
954
  {
648
955
  type: "text",
649
- text: "Unable to fetch observed host: Unknown error",
956
+ text: `Unable to fetch observed host: ${makeImpartRequest.lastError || "Unknown error"}`,
650
957
  },
651
958
  ],
652
959
  };
653
960
  }
654
961
  const formattedHost = [
655
962
  `ID: ${data.id}`,
656
- `Hostname: ${data.hostname ?? "N/A"}`,
657
- `Port: ${data.port ?? "N/A"}`,
658
- `Created At: ${data.created_at ?? "N/A"}`,
659
- `Observed At: ${data.observed_at ?? "N/A"}`,
660
- `Suspected Healthcheck: ${data.suspected_healthcheck ?? false}`,
963
+ `Hostname: ${data.hostname}`,
964
+ `Port: ${data.port}`,
965
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
966
+ `Observed At: ${data.observedAt ? data.observedAt.toISOString() : "N/A"}`,
967
+ `Suspected: ${data.suspected}`,
968
+ `Env: ${data.env}`,
661
969
  "---",
662
970
  "Related IDs:",
663
971
  ];
664
- if (data.related_ids && data.related_ids.length > 0) {
665
- data.related_ids.forEach((id) => formattedHost.push(` - ${id}`));
972
+ if (data.relatedIds && data.relatedIds.length > 0) {
973
+ data.relatedIds.forEach((id) => formattedHost.push(` - Binding ID: ${id.bindingId}, Spec ID: ${id.specId}`));
666
974
  }
667
975
  else {
668
976
  formattedHost.push(" No related IDs");
@@ -671,34 +979,42 @@ server.tool("get_observed_host", "Get details of a specific observed host", {
671
979
  content: [{ type: "text", text: formattedHost.join("\n") }],
672
980
  };
673
981
  });
674
- server.tool("list_specs", "Get a list of specs for your Impart organization", {
982
+ server.tool(getToolName("list_specs"), "Get a list of specs for your Impart organization", {
675
983
  page: zod_1.z
676
984
  .number()
677
985
  .default(1)
678
- .describe("The page of results to return (minimum value: 1)"),
986
+ .optional()
987
+ .nullable()
988
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
679
989
  max_results: zod_1.z
680
990
  .number()
681
991
  .default(100)
682
- .describe("The maximum number of results to return"),
683
- }, async ({ page, max_results }) => {
684
- const queryParams = {
685
- page: validatePage(page),
686
- max_results,
687
- };
992
+ .optional()
993
+ .nullable()
994
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
995
+ search: zod_1.z.string().optional().describe("Search string to filter specs"),
996
+ }, async ({ page, max_results, search }) => {
997
+ const queryParams = {};
998
+ if (page !== null && page !== undefined) {
999
+ queryParams["page"] = validatePage(page);
1000
+ }
1001
+ if (max_results !== null && max_results !== undefined) {
1002
+ queryParams["max_results"] = max_results;
1003
+ }
1004
+ if (search !== null && search !== undefined) {
1005
+ queryParams["search"] = search;
1006
+ }
688
1007
  const data = await makeImpartRequest("specs", queryParams);
689
1008
  if (!data) {
1009
+ const lastError = makeImpartRequest.lastError || "Unknown error";
690
1010
  return {
691
- content: [
692
- { type: "text", text: "Unable to fetch specs: Unknown error" },
693
- ],
1011
+ content: [{ type: "text", text: `Unable to fetch specs: ${lastError}` }],
694
1012
  };
695
1013
  }
696
1014
  const specs = data.items || [];
697
1015
  if (specs.length === 0) {
698
1016
  return {
699
- content: [
700
- { type: "text", text: "No specs found for this organization." },
701
- ],
1017
+ content: [{ type: "text", text: "No specs found for this organization." }],
702
1018
  };
703
1019
  }
704
1020
  const formattedSpecs = specs.map((spec) => [
@@ -706,32 +1022,34 @@ server.tool("list_specs", "Get a list of specs for your Impart organization", {
706
1022
  `Name: ${spec.name}`,
707
1023
  `Revision: ${spec.revision}`,
708
1024
  `Score: ${spec.score}`,
709
- `Created By: ${spec.created_by}`,
710
- `Created At: ${spec.created_at}`,
711
- `Updated By: ${spec.updated_by}`,
712
- `Updated At: ${spec.updated_at}`,
1025
+ `Created By: ${spec.createdBy}`,
1026
+ `Created At: ${spec.createdAt ? spec.createdAt.toISOString() : "N/A"}`,
1027
+ `Updated By: ${spec.updatedBy}`,
1028
+ `Updated At: ${spec.updatedAt?.toISOString() ?? "N/A"}`,
713
1029
  "---",
714
1030
  ].join("\n"));
715
1031
  return {
716
1032
  content: [{ type: "text", text: formattedSpecs.join("\n") }],
717
1033
  };
718
1034
  });
719
- server.tool("get_spec", "Get details of a specific spec", {
1035
+ server.tool(getToolName("get_spec"), "Get details of a specific spec", {
720
1036
  spec_id: zod_1.z.string().describe("The unique identifier of the spec"),
721
- revision: zod_1.z.number().optional().describe("The specific revision to fetch"),
1037
+ revision: zod_1.z.number().optional().nullable().describe("The specific revision to fetch"),
722
1038
  }, async ({ spec_id, revision }) => {
723
1039
  if (!spec_id) {
724
1040
  return {
725
1041
  content: [{ type: "text", text: "Error: spec_id is required." }],
726
1042
  };
727
1043
  }
728
- const queryParams = revision ? { revision } : undefined;
1044
+ const queryParams = {};
1045
+ if (revision !== null && revision !== undefined) {
1046
+ queryParams["revision"] = revision;
1047
+ }
729
1048
  const data = await makeImpartRequest(`specs/${spec_id}`, queryParams);
730
1049
  if (!data) {
1050
+ const lastError = makeImpartRequest.lastError || "Unknown error";
731
1051
  return {
732
- content: [
733
- { type: "text", text: "Unable to fetch spec: Unknown error" },
734
- ],
1052
+ content: [{ type: "text", text: `Unable to fetch spec: ${lastError}` }],
735
1053
  };
736
1054
  }
737
1055
  const formattedSpec = [
@@ -739,16 +1057,16 @@ server.tool("get_spec", "Get details of a specific spec", {
739
1057
  `Name: ${data.name}`,
740
1058
  `Revision: ${data.revision}`,
741
1059
  `Score: ${data.score}`,
742
- `Created By: ${data.created_by}`,
743
- `Created At: ${data.created_at}`,
744
- `Updated By: ${data.updated_by}`,
745
- `Updated At: ${data.updated_at}`,
1060
+ `Created By: ${data.createdBy}`,
1061
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
1062
+ `Updated By: ${data.updatedBy}`,
1063
+ `Updated At: ${data.updatedAt?.toISOString() ?? "N/A"}`,
746
1064
  "---",
747
1065
  "Revisions:",
748
1066
  ];
749
1067
  if (data.revisions && data.revisions.length > 0) {
750
1068
  data.revisions.forEach((revision) => {
751
- formattedSpec.push(` Revision: ${revision.revision}`, ` Created By: ${revision.created_by}`, ` Created At: ${revision.created_at}`, "---");
1069
+ formattedSpec.push(` Revision: ${revision.revision}`, ` Created By: ${revision.createdBy}`, ` Created At: ${revision.createdAt ? revision.createdAt.toISOString() : "N/A"}`, "---");
752
1070
  });
753
1071
  }
754
1072
  formattedSpec.push(`Specification (Base64 Encoded):\n${data.spec ?? "N/A"}`);
@@ -756,26 +1074,44 @@ server.tool("get_spec", "Get details of a specific spec", {
756
1074
  content: [{ type: "text", text: formattedSpec.join("\n") }],
757
1075
  };
758
1076
  });
759
- server.tool("list_api_bindings", "Get a list of API bindings for the organization", {
1077
+ server.tool(getToolName("list_api_bindings"), "Get a list of API bindings for the organization", {
760
1078
  page: zod_1.z
761
1079
  .number()
762
1080
  .default(1)
763
- .describe("The page of results to return (minimum value: 1)"),
1081
+ .optional()
1082
+ .nullable()
1083
+ .describe("The page of results to return (minimum value: 1, e.g., 1, 2, 3)"),
764
1084
  max_results: zod_1.z
765
1085
  .number()
766
1086
  .default(100)
767
- .describe("The maximum number of results to return"),
768
- }, async ({ page, max_results }) => {
769
- const queryParams = {
770
- page: validatePage(page),
771
- max_results,
772
- };
1087
+ .optional()
1088
+ .nullable()
1089
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
1090
+ search: zod_1.z.string().optional().describe("The search string to filter API bindings"),
1091
+ spec_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of spec IDs"),
1092
+ collection_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("Optional list of collection IDs"),
1093
+ }, async ({ page, max_results, search, spec_id, collection_id }) => {
1094
+ const queryParams = {};
1095
+ if (page !== null && page !== undefined) {
1096
+ queryParams["page"] = validatePage(page);
1097
+ }
1098
+ if (max_results !== null && max_results !== undefined) {
1099
+ queryParams["max_results"] = max_results;
1100
+ }
1101
+ if (search !== null && search !== undefined) {
1102
+ queryParams["search"] = search;
1103
+ }
1104
+ if (spec_id !== null && spec_id !== undefined) {
1105
+ queryParams["spec_id"] = spec_id;
1106
+ }
1107
+ if (collection_id !== null && collection_id !== undefined) {
1108
+ queryParams["collection_id"] = collection_id;
1109
+ }
773
1110
  const data = await makeImpartRequest("api_bindings", queryParams);
774
1111
  if (!data) {
1112
+ const lastError = makeImpartRequest.lastError || "Unknown error";
775
1113
  return {
776
- content: [
777
- { type: "text", text: "Unable to fetch API bindings: Unknown error" },
778
- ],
1114
+ content: [{ type: "text", text: `Unable to fetch API bindings: ${lastError}` }],
779
1115
  };
780
1116
  }
781
1117
  const bindings = data.items || [];
@@ -792,104 +1128,156 @@ server.tool("list_api_bindings", "Get a list of API bindings for the organizatio
792
1128
  const formattedBindings = bindings.map((binding) => [
793
1129
  `ID: ${binding.id}`,
794
1130
  `Name: ${binding.name}`,
795
- `Type: ${binding.type}`,
796
- `Host: ${binding.host}`,
797
- `Status: ${binding.status}`,
798
- `Created At: ${binding.created_at}`,
1131
+ `Host: ${binding.hostname}`,
1132
+ `Type: ${binding.port}`,
1133
+ `Disabled: ${binding.disabled}`,
1134
+ `Created At: ${binding.createdAt ? binding.createdAt.toISOString() : "N/A"}`,
799
1135
  "---",
800
1136
  ].join("\n"));
801
1137
  return {
802
1138
  content: [{ type: "text", text: formattedBindings.join("\n") }],
803
1139
  };
804
1140
  });
805
- server.tool("create_api_binding", "Create a new API binding for the organization", {
806
- name: zod_1.z.string().describe("The name of the API binding"),
807
- binding_type: zod_1.z
808
- .string()
809
- .describe("The type of binding ('apiKey', 'basic', 'bearer', or 'oauth2')"),
810
- host: zod_1.z.string().describe("The host to bind to"),
811
- api_binding_source: zod_1.z
812
- .string()
813
- .describe("Location of the binding ('header', 'query', or 'cookie')"),
814
- binding_value: zod_1.z.string().describe("The value of the binding"),
815
- description: zod_1.z
816
- .string()
817
- .optional()
818
- .describe("Optional description of the binding"),
819
- patch_operations: zod_1.z
820
- .array(zod_1.z.record(zod_1.z.any()))
821
- .optional()
822
- .describe("Optional list of JSON patch operations"),
823
- enable_learning: zod_1.z
824
- .boolean()
825
- .default(true)
826
- .describe("Whether to enable learning for this binding"),
827
- }, async ({ name, binding_type, host, api_binding_source, binding_value, description, patch_operations, enable_learning, }) => {
1141
+ server.tool(getToolName("create_api_binding"), "Create a new API binding for the organization", {
1142
+ name: zod_1.z.string().min(1).max(64).describe("The name of the API binding"),
1143
+ hostname: zod_1.z.string().describe("The hostname to bind to"),
1144
+ port: zod_1.z.number().describe("The port to bind to"),
1145
+ base_path: zod_1.z.string().describe("The base path for the API binding"),
1146
+ spec_id: zod_1.z.string().describe("The ID of the spec to bind to"),
1147
+ disabled: zod_1.z.boolean().default(false).optional().nullable().describe("Whether the API binding is disabled"),
1148
+ upstream_origin: zod_1.z.string().optional().describe("The upstream origin URL for the API binding"),
1149
+ hops: zod_1.z.number().min(-1).max(10).optional().nullable().describe("Number of hops for the API binding"),
1150
+ use_forwarded: zod_1.z.boolean().optional().nullable().describe("Whether to use forwarded headers"),
1151
+ forwarded_for: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("List of forwarded 'for' headers"),
1152
+ forwarded_host: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("List of forwarded 'host' headers"),
1153
+ forwarded_proto: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("List of forwarded 'proto' headers"),
1154
+ forwarded_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("List of forwarded IDs"),
1155
+ }, async ({ name, hostname, port, base_path, spec_id, disabled, upstream_origin, hops, use_forwarded, forwarded_for, forwarded_host, forwarded_proto, forwarded_id, }) => {
828
1156
  const payload = {
829
1157
  name,
830
- type: binding_type,
831
- host,
832
- source: api_binding_source,
833
- value: binding_value,
834
- description,
835
- patch_operations: patch_operations ?? [],
836
- enable_learning,
1158
+ hostname,
1159
+ port,
1160
+ base_path,
1161
+ spec_id,
837
1162
  };
1163
+ if (disabled !== null && disabled !== undefined) {
1164
+ payload["disabled"] = disabled;
1165
+ }
1166
+ if (upstream_origin !== null && upstream_origin !== undefined) {
1167
+ payload["upstream_origin"] = upstream_origin;
1168
+ }
1169
+ if (hops !== null && hops !== undefined) {
1170
+ payload["hops"] = hops;
1171
+ }
1172
+ if (use_forwarded !== null && use_forwarded !== undefined) {
1173
+ payload["use_forwarded"] = use_forwarded;
1174
+ }
1175
+ if (forwarded_for !== null && forwarded_for !== undefined) {
1176
+ payload["forwarded_for"] = forwarded_for;
1177
+ }
1178
+ if (forwarded_host !== null && forwarded_host !== undefined) {
1179
+ payload["forwarded_host"] = forwarded_host;
1180
+ }
1181
+ if (forwarded_proto !== null && forwarded_proto !== undefined) {
1182
+ payload["forwarded_proto"] = forwarded_proto;
1183
+ }
1184
+ if (forwarded_id !== null && forwarded_id !== undefined) {
1185
+ payload["forwarded_id"] = forwarded_id;
1186
+ }
838
1187
  const data = await makeImpartRequest("api_bindings", undefined, "POST", payload);
839
1188
  if (!data) {
1189
+ const lastError = makeImpartRequest.lastError || "Unknown error";
840
1190
  return {
841
- content: [{ type: "text", text: "Failed to create API binding" }],
1191
+ content: [{ type: "text", text: `Unable to create API binding: ${lastError}` }],
842
1192
  };
843
1193
  }
844
1194
  const formattedBinding = [
845
1195
  "API Binding created successfully:",
846
1196
  `ID: ${data.id}`,
847
1197
  `Name: ${data.name}`,
848
- `Type: ${data.type}`,
849
- `Host: ${data.host}`,
850
- `Source: ${data.source}`,
851
- `Status: ${data.status}`,
852
- `Description: ${data.description ?? "N/A"}`,
853
- `Learning Enabled: ${data.enable_learning}`,
854
- `Created At: ${data.created_at}`,
1198
+ `Host: ${data.hostname}`,
1199
+ `Port: ${data.port}`,
1200
+ `Disabled: ${data.disabled}`,
1201
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
855
1202
  "---",
856
1203
  ].join("\n");
857
1204
  return {
858
1205
  content: [{ type: "text", text: formattedBinding }],
859
1206
  };
860
1207
  });
861
- server.tool("list_lists", "Get a list of lists for the organization", {
862
- kind: zod_1.z.string().optional().describe("The list kind to query for"),
863
- subkind: zod_1.z.string().optional().describe("The list subkind to query for"),
1208
+ server.tool(getToolName("list_lists"), "Get a list of lists for the organization. Lists are collections used for filtering and blocking/allowing specific values. Examples:\n\n" +
1209
+ " Get all lists: {} (no parameters needed)\n" +
1210
+ '• Filter by kind: {"kind": "ip"} or {"kind": "string"}\n' +
1211
+ '• Search lists: {"search": "blocklist"}\n' +
1212
+ '• With pagination: {"page": 1, "max_results": 25}\n' +
1213
+ '• Filter by labels: {"label": ["security", "waf"]}\n' +
1214
+ '• Combined filters: {"kind": "ip", "search": "suspicious", "max_results": 10}', {
1215
+ kind: zod_1.z
1216
+ .string()
1217
+ .optional()
1218
+ .nullable()
1219
+ .describe("Filter by list kind: 'string', 'uuid', 'ip', 'country', 'path', or 'asn' (e.g., \"ip\", \"string\")"),
1220
+ subkind: zod_1.z
1221
+ .string()
1222
+ .optional()
1223
+ .nullable()
1224
+ .describe("Filter by list subkind: 'spec_uuid' or 'endpoint_uuid' (e.g., \"spec_uuid\")"),
1225
+ search: zod_1.z
1226
+ .string()
1227
+ .optional()
1228
+ .nullable()
1229
+ .describe('Search in list names and descriptions (e.g., "blocklist", "suspicious")'),
864
1230
  page: zod_1.z
865
1231
  .number()
866
1232
  .default(1)
867
- .describe("The page of results to return (minimum: 1)"),
1233
+ .optional()
1234
+ .nullable()
1235
+ .describe("Page number for pagination, starting from 1 (e.g., 1, 2, 3). Default: 1"),
868
1236
  max_results: zod_1.z
869
1237
  .number()
870
- .default(100)
871
- .describe("The maximum number of results to return"),
872
- }, async ({ kind, subkind, page, max_results }) => {
873
- const queryParams = {
874
- kind,
875
- subkind,
876
- page: validatePage(page),
877
- max_results,
878
- };
1238
+ .default(25)
1239
+ .optional()
1240
+ .nullable()
1241
+ .describe("Maximum results per page, 1-100 (e.g., 10, 25, 50). Default: 25 for performance"),
1242
+ label: zod_1.z
1243
+ .array(zod_1.z.string())
1244
+ .optional()
1245
+ .nullable()
1246
+ .describe('Filter by label slugs (e.g., ["security", "waf", "blocklist"])'),
1247
+ }, async ({ kind, subkind, search, page, max_results, label }) => {
1248
+ const queryParams = {};
1249
+ if (kind !== null && kind !== undefined) {
1250
+ queryParams["kind"] = kind;
1251
+ }
1252
+ if (subkind !== null && subkind !== undefined) {
1253
+ queryParams["subkind"] = subkind;
1254
+ }
1255
+ if (search !== null && search !== undefined) {
1256
+ queryParams["search"] = search;
1257
+ }
1258
+ if (page !== null && page !== undefined) {
1259
+ queryParams["page"] = validatePage(page);
1260
+ }
1261
+ if (max_results !== null && max_results !== undefined) {
1262
+ queryParams["max_results"] = max_results;
1263
+ }
1264
+ if (max_results !== null && max_results !== undefined) {
1265
+ queryParams["max_results"] = max_results;
1266
+ }
1267
+ if (label !== null && label !== undefined) {
1268
+ queryParams["label"] = label;
1269
+ }
879
1270
  const data = await makeImpartRequest("lists", queryParams);
880
1271
  if (!data) {
1272
+ const lastError = makeImpartRequest.lastError || "Unknown error";
881
1273
  return {
882
- content: [
883
- { type: "text", text: "Unable to fetch lists: Unknown error" },
884
- ],
1274
+ content: [{ type: "text", text: `Unable to fetch lists: ${lastError}` }],
885
1275
  };
886
1276
  }
887
1277
  const lists = data.items || [];
888
1278
  if (lists.length === 0) {
889
1279
  return {
890
- content: [
891
- { type: "text", text: "No lists found for this organization." },
892
- ],
1280
+ content: [{ type: "text", text: "No lists found for this organization." }],
893
1281
  };
894
1282
  }
895
1283
  const formattedLists = lists.map((list) => [
@@ -898,24 +1286,18 @@ server.tool("list_lists", "Get a list of lists for the organization", {
898
1286
  `Kind: ${list.kind}`,
899
1287
  `Subkind: ${list.subkind ?? "N/A"}`,
900
1288
  `Description: ${list.description ?? "N/A"}`,
901
- `Created At: ${list.created_at}`,
902
- `Updated At: ${list.updated_at}`,
1289
+ `Created By: ${list.createdBy}`,
1290
+ `Created At: ${list.createdAt ? list.createdAt.toISOString() : "N/A"}`,
903
1291
  "---",
904
1292
  ].join("\n"));
905
1293
  return {
906
1294
  content: [{ type: "text", text: formattedLists.join("\n") }],
907
1295
  };
908
1296
  });
909
- server.tool("get_list_items", "Get items from a specific list", {
1297
+ server.tool(getToolName("get_list_items"), "Get items from a specific list", {
910
1298
  list_id: zod_1.z.string().describe("The ID of the list"),
911
- page: zod_1.z
912
- .number()
913
- .default(1)
914
- .describe("The page of results to return (minimum: 1)"),
915
- max_results: zod_1.z
916
- .number()
917
- .default(100)
918
- .describe("The maximum number of results to return"),
1299
+ page: zod_1.z.number().default(1).describe("The page of results to return (minimum: 1, e.g., 1, 2, 3)"),
1300
+ max_results: zod_1.z.number().default(100).describe("The maximum number of results to return (e.g., 10, 50, 100)"),
919
1301
  }, async ({ list_id, page, max_results }) => {
920
1302
  if (!list_id) {
921
1303
  return {
@@ -928,70 +1310,114 @@ server.tool("get_list_items", "Get items from a specific list", {
928
1310
  };
929
1311
  const data = await makeImpartRequest(`lists/${list_id}/items`, queryParams);
930
1312
  if (!data) {
1313
+ const lastError = makeImpartRequest.lastError || "Unknown error";
931
1314
  return {
932
- content: [
933
- { type: "text", text: "Unable to fetch list items: Unknown error" },
934
- ],
1315
+ content: [{ type: "text", text: `Unable to fetch list items: ${lastError}` }],
935
1316
  };
936
1317
  }
937
- const items = data.items || [];
1318
+ // Handle direct array response from API
1319
+ const items = Array.isArray(data) ? data : [];
938
1320
  if (items.length === 0) {
939
1321
  return {
940
1322
  content: [{ type: "text", text: "No items found in this list." }],
941
1323
  };
942
1324
  }
943
- const formattedItems = items.map((item) => [`Value: ${item.value}`, `Created At: ${item.created_at}`, "---"].join("\n"));
1325
+ const formattedItems = items.map((item) => [`Value: ${item.value}`, `Expiration: ${item.expiration?.toISOString() ?? "N/A"}`, "---"].join("\n"));
944
1326
  return {
945
1327
  content: [{ type: "text", text: formattedItems.join("\n") }],
946
1328
  };
947
1329
  });
948
- server.tool("create_list", "Creates a new list for the organization", {
949
- name: zod_1.z.string().describe("Name of the list"),
950
- kind: zod_1.z.string().describe("The kind of list"),
951
- subkind: zod_1.z.string().optional().describe("Optional subkind of list"),
952
- description: zod_1.z.string().optional().describe("Optional description"),
953
- functionality: zod_1.z
954
- .string()
955
- .default("add/remove")
956
- .describe("List functionality"),
957
- items: zod_1.z
958
- .array(zod_1.z.record(zod_1.z.any()))
1330
+ // items:
1331
+ // description: The items in the list.
1332
+ // items:
1333
+ // $ref: "#/components/schemas/list_items_inner"
1334
+ // maxItems: 100000
1335
+ // type: array
1336
+ // required:
1337
+ // - kind
1338
+ // - name
1339
+ server.tool(getToolName("create_list"), "Create a new list", {
1340
+ name: zod_1.z.string().min(1).max(64).describe("The name of the list"),
1341
+ kind: zod_1.z.enum(["string", "uuid", "ip", "country", "path", "asn"]).describe("The kind of list to create"),
1342
+ subkind: zod_1.z.enum(["spec_uuid", "endpoint_uuid"]).optional().describe("The subkind of list to create"),
1343
+ functionality: zod_1.z.enum(["add"]).default("add").optional().describe("The functionality of the list"),
1344
+ description: zod_1.z.string().optional().describe("The description of the list"),
1345
+ items: zod_1.z.array(zod_1.z.string()).optional().nullable().describe("The items to add to the list"),
1346
+ labels: zod_1.z
1347
+ .array(zod_1.z.string())
959
1348
  .optional()
960
- .describe("Optional initial list items"),
961
- }, async ({ name, kind, subkind, description, functionality, items }) => {
962
- const payload = {
963
- name,
1349
+ .nullable()
1350
+ .describe('The labels to apply to the list (e.g., ["llm-protection", "waf"])'),
1351
+ }, async ({ name, kind, subkind, functionality, description, items, labels }) => {
1352
+ if (!kind || !name) {
1353
+ return {
1354
+ content: [{ type: "text", text: "Error: name and kind are required" }],
1355
+ isError: true,
1356
+ };
1357
+ }
1358
+ const body = {
964
1359
  kind,
965
- subkind,
966
- description,
967
- functionality,
968
- items: items ?? [],
1360
+ name,
969
1361
  };
970
- const data = await makeImpartRequest("lists", undefined, "POST", payload);
1362
+ if (subkind !== null && subkind !== undefined) {
1363
+ body["subkind"] = subkind;
1364
+ }
1365
+ if (functionality !== null && functionality !== undefined) {
1366
+ body["functionality"] = functionality;
1367
+ }
1368
+ if (description !== null && description !== undefined) {
1369
+ body["description"] = description;
1370
+ }
1371
+ if (items !== null && items !== undefined) {
1372
+ body["items"] = items;
1373
+ }
1374
+ if (labels !== null && labels !== undefined) {
1375
+ body["labels"] = labels;
1376
+ }
1377
+ const data = await makeImpartRequest("lists", {}, "POST", body);
971
1378
  if (!data) {
1379
+ const lastError = makeImpartRequest.lastError || "Unknown error";
972
1380
  return {
973
- content: [{ type: "text", text: "Failed to create list" }],
1381
+ content: [{ type: "text", text: `Unable to create list: ${lastError}` }],
1382
+ isError: true,
974
1383
  };
975
1384
  }
976
- const formattedList = [
977
- "List created successfully:",
978
- `ID: ${data.id}`,
979
- `Name: ${data.name}`,
980
- `Kind: ${data.kind}`,
981
- `Subkind: ${data.subkind ?? "N/A"}`,
982
- `Description: ${data.description ?? "N/A"}`,
983
- `Created At: ${data.created_at}`,
984
- "---",
985
- ].join("\n");
986
1385
  return {
987
- content: [{ type: "text", text: formattedList }],
1386
+ content: [
1387
+ {
1388
+ type: "text",
1389
+ text: [
1390
+ "List created successfully:",
1391
+ `ID: ${data.id}`,
1392
+ `Name: ${name}`,
1393
+ `Kind: ${kind}`,
1394
+ `Subkind: ${subkind}`,
1395
+ `Description: ${description ?? "N/A"}`,
1396
+ `Created By: ${data.createdBy}`,
1397
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
1398
+ "---",
1399
+ ].join("\n"),
1400
+ },
1401
+ ],
988
1402
  };
989
1403
  });
990
- server.tool("update_list_items", "Updates items in a list", {
1404
+ // add:
1405
+ // description: The items in the list.
1406
+ // items:
1407
+ // $ref: "#/components/schemas/list_items_inner"
1408
+ // maxItems: 100000
1409
+ // type: array
1410
+ // remove:
1411
+ // description: Items to remove from the list.
1412
+ // items:
1413
+ // maxLength: 1000
1414
+ // minLength: 1
1415
+ // type: string
1416
+ // maxItems: 100000
1417
+ // type: array
1418
+ server.tool(getToolName("update_list_items"), "Updates items in a list", {
991
1419
  list_id: zod_1.z.string().describe("The ID of the list to update"),
992
- items: zod_1.z
993
- .array(zod_1.z.record(zod_1.z.any()))
994
- .describe("The items to update in the list"),
1420
+ items: zod_1.z.array(zod_1.z.record(zod_1.z.string(), zod_1.z.any())).describe("The items to update in the list"),
995
1421
  }, async ({ list_id, items }) => {
996
1422
  if (!list_id) {
997
1423
  return {
@@ -1001,85 +1427,510 @@ server.tool("update_list_items", "Updates items in a list", {
1001
1427
  const payload = { items };
1002
1428
  const data = await makeImpartRequest(`lists/${list_id}/items`, undefined, "PATCH", payload);
1003
1429
  if (!data) {
1430
+ const lastError = makeImpartRequest.lastError || "Unknown error";
1004
1431
  return {
1005
- content: [{ type: "text", text: "Failed to update list items" }],
1432
+ content: [{ type: "text", text: `Unable to update list items: ${lastError}` }],
1006
1433
  };
1007
1434
  }
1008
1435
  return {
1009
1436
  content: [{ type: "text", text: "List items updated successfully" }],
1010
1437
  };
1011
1438
  });
1012
- server.tool("create_label", "Creates a new label for the organization", {
1013
- name: zod_1.z.string().describe("Name of the label"),
1439
+ server.tool(getToolName("create_label"), "Creates a new label for the organization", {
1440
+ slug: zod_1.z.string().min(1).max(64).describe("Name of the label"),
1441
+ display_name: zod_1.z.string().optional().describe("Optional display name"),
1014
1442
  description: zod_1.z.string().optional().describe("Optional description"),
1015
- color: zod_1.z.string().default("gray").describe("Label color"),
1016
- }, async ({ name, description, color }) => {
1443
+ color: zod_1.z
1444
+ .enum(["gray", "red", "orange", "yellow", "green", "teal", "cyan", "blue", "purple", "pink"])
1445
+ .default("gray")
1446
+ .optional()
1447
+ .nullable()
1448
+ .describe("Label color"),
1449
+ }, async ({ slug, display_name, description, color }) => {
1017
1450
  const payload = {
1018
- name,
1019
- description,
1020
- color,
1451
+ slug,
1021
1452
  };
1453
+ if (display_name !== null && display_name !== undefined) {
1454
+ payload["display_name"] = display_name;
1455
+ }
1456
+ if (description !== null && description !== undefined) {
1457
+ payload["description"] = description;
1458
+ }
1459
+ if (color !== null && color !== undefined) {
1460
+ payload["color"] = color;
1461
+ }
1022
1462
  const data = await makeImpartRequest("labels", undefined, "POST", payload);
1023
1463
  if (!data) {
1464
+ const lastError = makeImpartRequest.lastError || "Unknown error";
1024
1465
  return {
1025
- content: [{ type: "text", text: "Failed to create label" }],
1466
+ content: [{ type: "text", text: `Unable to create label: ${lastError}` }],
1026
1467
  };
1027
1468
  }
1028
1469
  const formattedLabel = [
1029
1470
  "Label created successfully:",
1030
1471
  `Slug: ${data.slug}`,
1031
- `Name: ${data.name}`,
1032
- `Description: ${data.description ?? "N/A"}`,
1472
+ `Name: ${data.displayName}`,
1473
+ `Description: ${data.description}`,
1033
1474
  `Color: ${data.color}`,
1034
- `Created At: ${data.created_at}`,
1475
+ `Created At: ${data.createdAt ? data.createdAt.toISOString() : "N/A"}`,
1035
1476
  "---",
1036
1477
  ].join("\n");
1037
1478
  return {
1038
1479
  content: [{ type: "text", text: formattedLabel }],
1039
1480
  };
1040
1481
  });
1041
- server.tool("list_labels", "Get a list of labels for the organization", {
1482
+ server.tool(getToolName("list_labels"), "Get a list of labels for the organization", {
1042
1483
  page: zod_1.z
1043
1484
  .number()
1044
1485
  .default(1)
1045
- .describe("The page of results to return (minimum: 1)"),
1486
+ .optional()
1487
+ .nullable()
1488
+ .describe("The page of results to return (minimum: 1, e.g., 1, 2, 3)"),
1046
1489
  max_results: zod_1.z
1047
1490
  .number()
1048
1491
  .default(100)
1049
- .describe("The maximum number of results to return"),
1050
- }, async ({ page, max_results }) => {
1051
- const queryParams = {
1052
- page: validatePage(page),
1053
- max_results,
1054
- };
1492
+ .optional()
1493
+ .nullable()
1494
+ .describe("The maximum number of results to return (e.g., 10, 50, 100)"),
1495
+ search: zod_1.z.string().nullable().optional().describe("Search string to filter labels"),
1496
+ }, async ({ page, max_results, search }) => {
1497
+ const queryParams = {};
1498
+ if (page !== null && page !== undefined) {
1499
+ queryParams["page"] = validatePage(page);
1500
+ }
1501
+ if (max_results !== null && max_results !== undefined) {
1502
+ queryParams["max_results"] = max_results;
1503
+ }
1504
+ if (search !== null && search !== undefined) {
1505
+ queryParams["search"] = search;
1506
+ }
1055
1507
  const data = await makeImpartRequest("labels", queryParams);
1056
1508
  if (!data) {
1509
+ const lastError = makeImpartRequest.lastError || "Unknown error";
1057
1510
  return {
1058
- content: [
1059
- { type: "text", text: "Unable to fetch labels: Unknown error" },
1060
- ],
1511
+ content: [{ type: "text", text: `Unable to fetch labels: ${lastError}` }],
1061
1512
  };
1062
1513
  }
1063
1514
  const labels = data.items || [];
1064
1515
  if (labels.length === 0) {
1065
1516
  return {
1066
- content: [
1067
- { type: "text", text: "No labels found for this organization." },
1068
- ],
1517
+ content: [{ type: "text", text: "No labels found for this organization." }],
1069
1518
  };
1070
1519
  }
1071
1520
  const formattedLabels = labels.map((label) => [
1072
1521
  `Slug: ${label.slug}`,
1073
- `Name: ${label.name}`,
1522
+ `Name: ${label.displayName}`,
1074
1523
  `Description: ${label.description ?? "N/A"}`,
1075
1524
  `Color: ${label.color}`,
1076
- `Created At: ${label.created_at}`,
1525
+ `Created At: ${label.createdAt ? label.createdAt.toISOString() : "N/A"}`,
1077
1526
  "---",
1078
1527
  ].join("\n"));
1079
1528
  return {
1080
1529
  content: [{ type: "text", text: formattedLabels.join("\n") }],
1081
1530
  };
1082
1531
  });
1532
+ server.tool(getToolName("list_test_cases"), "Get a list of test cases for security rules in your Impart organization. Test cases are predefined request/response scenarios used to validate rule behavior and ensure rules work correctly. Examples:\n\n" +
1533
+ "• All test cases: {} (no parameters needed)\n" +
1534
+ '• Search test cases: {"search": "malicious"}\n' +
1535
+ '• Filter by labels: {"label": ["security", "validation"]}\n' +
1536
+ '• With pagination: {"page": 1, "max_results": 25}', {
1537
+ page: zod_1.z
1538
+ .number()
1539
+ .default(1)
1540
+ .optional()
1541
+ .nullable()
1542
+ .describe("Page number for pagination, starting from 1 (e.g., 1, 2, 3). Default: 1"),
1543
+ max_results: zod_1.z
1544
+ .number()
1545
+ .default(25)
1546
+ .optional()
1547
+ .nullable()
1548
+ .describe("Maximum test cases per page, 1-100 (e.g., 10, 25, 50). Default: 25 for performance"),
1549
+ label: zod_1.z
1550
+ .array(zod_1.z.string())
1551
+ .optional()
1552
+ .nullable()
1553
+ .describe('Filter by label slugs (e.g., ["security", "validation", "attack-simulation"])'),
1554
+ search: zod_1.z
1555
+ .string()
1556
+ .optional()
1557
+ .nullable()
1558
+ .describe('Search in test case names and descriptions (e.g., "malicious", "XSS", "SQL injection")'),
1559
+ }, async ({ page, max_results, label, search }) => {
1560
+ const queryParams = {};
1561
+ if (page !== null && page !== undefined) {
1562
+ queryParams["page"] = validatePage(page);
1563
+ }
1564
+ if (max_results !== null && max_results !== undefined) {
1565
+ queryParams["max_results"] = max_results;
1566
+ }
1567
+ if (label !== null && label !== undefined) {
1568
+ queryParams["label"] = label;
1569
+ }
1570
+ if (search !== null && search !== undefined) {
1571
+ queryParams["search"] = search;
1572
+ }
1573
+ const data = await makeImpartRequest("rules_test_cases", queryParams);
1574
+ if (!data) {
1575
+ return {
1576
+ content: [
1577
+ {
1578
+ type: "text",
1579
+ text: `Unable to fetch test cases: ${makeImpartRequest.lastError || "Unknown error"}`,
1580
+ },
1581
+ ],
1582
+ };
1583
+ }
1584
+ const testCases = data.items || [];
1585
+ if (testCases.length === 0) {
1586
+ return {
1587
+ content: [
1588
+ {
1589
+ type: "text",
1590
+ text: "No test cases found for this organization. Test cases are used to validate security rule behavior.",
1591
+ },
1592
+ ],
1593
+ };
1594
+ }
1595
+ // Limit display for performance
1596
+ const maxDisplay = Math.min(testCases.length, 15);
1597
+ const showingLimited = testCases.length > maxDisplay;
1598
+ const result = [
1599
+ `Found ${testCases.length} test cases`,
1600
+ showingLimited ? `Showing first ${maxDisplay} for performance. Use search/filters to narrow results.` : "",
1601
+ "=".repeat(50),
1602
+ "",
1603
+ ];
1604
+ testCases.slice(0, maxDisplay).forEach((testCase, index) => {
1605
+ result.push(`${index + 1}. ${testCase.name} ${testCase.required ? "(REQUIRED)" : ""}`, ` ID: ${testCase.id}`, ` Messages: ${testCase.messages.length} | Assertions: ${testCase.assertions.length}`, ` Labels: ${testCase.labels.length > 0 ? testCase.labels.join(", ") : "None"}`, ` Created: ${testCase.createdAt ? testCase.createdAt.toISOString() : "N/A"}`, "");
1606
+ });
1607
+ return {
1608
+ content: [{ type: "text", text: result.join("\n") }],
1609
+ };
1610
+ });
1611
+ server.tool(getToolName("get_test_case"), "Get detailed information about a specific test case for security rules. Test cases contain request/response scenarios and assertions to validate rule behavior. Examples:\n\n" +
1612
+ '• Get test case details: {"test_case_id": "tc-abc123"}\n' +
1613
+ '• View test scenario: {"test_case_id": "tc-def456"}\n\n' +
1614
+ "Note: Use test case IDs from the list_test_cases tool or your Impart dashboard.", {
1615
+ test_case_id: zod_1.z.string().describe("The unique identifier of the test case (e.g., 'tc-abc123', 'tc-def456')"),
1616
+ }, async ({ test_case_id }) => {
1617
+ if (!test_case_id) {
1618
+ return {
1619
+ content: [{ type: "text", text: "Error: test_case_id is required." }],
1620
+ };
1621
+ }
1622
+ const data = await makeImpartRequest(`rules_test_cases/${test_case_id}`);
1623
+ if (!data) {
1624
+ return {
1625
+ content: [
1626
+ {
1627
+ type: "text",
1628
+ text: `Unable to fetch test case: ${makeImpartRequest.lastError || "Unknown error"}`,
1629
+ },
1630
+ ],
1631
+ };
1632
+ }
1633
+ const formattedTestCase = [
1634
+ `ID: ${data.id}`,
1635
+ `Name: ${data.name}`,
1636
+ `Description: ${data.description ?? "N/A"}`,
1637
+ `Required: ${data.required}`,
1638
+ `Labels: ${data.labels && data.labels.length > 0 ? data.labels.join(", ") : "None"}`,
1639
+ "Messages:",
1640
+ ];
1641
+ (data.messages || []).forEach((message, index) => {
1642
+ formattedTestCase.push(`Message ${index + 1}:`);
1643
+ if (message.req) {
1644
+ formattedTestCase.push(" Request:", ` Method: ${message.req.method ?? "N/A"}`, ` URL: ${message.req.url ?? "N/A"}`, " Headers:");
1645
+ if (message.req.headerKeys) {
1646
+ Object.entries(message.req.headerKeys).forEach(([key, values]) => {
1647
+ formattedTestCase.push(` ${key}: ${values}`);
1648
+ });
1649
+ }
1650
+ if (message.req.body) {
1651
+ formattedTestCase.push(` Body: ${message.req.body}`);
1652
+ }
1653
+ }
1654
+ if (message.res) {
1655
+ formattedTestCase.push(" Response:", ` Status Code: ${message.res.statusCode ?? "N/A"}`, " Headers:");
1656
+ if (message.res.headerKeys) {
1657
+ Object.entries(message.res.headerKeys).forEach(([key, values]) => {
1658
+ formattedTestCase.push(` ${key}: ${values}`);
1659
+ });
1660
+ }
1661
+ if (message.res.body) {
1662
+ formattedTestCase.push(` Body: ${message.res.body}`);
1663
+ }
1664
+ }
1665
+ });
1666
+ formattedTestCase.push("Assertions:", ...(data.assertions || []).map((assertion) => ` Type: ${assertion.assertionType}, Expected: ${assertion.expected.toString()}, Indexes: ${assertion.messageIndexes && assertion.messageIndexes.length > 0 ? assertion.messageIndexes.join(", ") : "None"}`), "---");
1667
+ return {
1668
+ content: [{ type: "text", text: formattedTestCase.join("\n") }],
1669
+ };
1670
+ });
1671
+ server.tool(getToolName("get_requests"), "Get a list of HTTP requests captured by Impart. Supports extensive filtering and pagination. Examples:\n\n" +
1672
+ "• Recent requests (default): {} (gets last day, 25 results)\n" +
1673
+ '• Last 24 hours: {"from": "-24h", "max_results": 50}\n' +
1674
+ '• Last week: {"from": "-7d", "max_results": 100}\n' +
1675
+ '• Filter by API spec: {"spec_id": ["spec-abc123"], "max_results": 25}\n' +
1676
+ '• Error requests only: {"status_code": ["400", "404", "500"], "from": "-1d"}\n' +
1677
+ '• Security events: {"tag": ["attack", "blocked", "suspicious"], "from": "-24h"}\n' +
1678
+ '• Specific IP activity: {"ip": ["192.168.1.100"], "from": "-24h", "sort_order": "desc"}\n' +
1679
+ '• Multiple filters: {"status_code": ["200"], "tag": ["allowed"], "from": "-1h", "max_results": 10}\n' +
1680
+ '• Time range: {"from": "-7d", "until": "-1d", "sort_order": "desc", "max_results": 50}\n' +
1681
+ '• Next page: {"max_results": 25, "next": "eyJ0aW1lc3RhbXAiOi4uLn0="}', {
1682
+ spec_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe('Filter by spec IDs (e.g., ["spec-123", "spec-456"])'),
1683
+ collection_id: zod_1.z
1684
+ .array(zod_1.z.string())
1685
+ .optional()
1686
+ .nullable()
1687
+ .describe('Filter by collection IDs (e.g., ["collection-123"])'),
1688
+ endpoint_id: zod_1.z.array(zod_1.z.string()).optional().nullable().describe('Filter by endpoint IDs (e.g., ["endpoint-123"])'),
1689
+ tag: zod_1.z
1690
+ .array(zod_1.z.string())
1691
+ .optional()
1692
+ .nullable()
1693
+ .describe('Filter by tag names (e.g., ["attack", "blocked", "allowed"])'),
1694
+ ip: zod_1.z
1695
+ .array(zod_1.z.string())
1696
+ .optional()
1697
+ .nullable()
1698
+ .describe('Filter by IP addresses (e.g., ["192.168.1.1", "10.0.0.1"])'),
1699
+ external_request_id: zod_1.z
1700
+ .array(zod_1.z.string())
1701
+ .optional()
1702
+ .nullable()
1703
+ .describe('Filter by external request IDs (e.g., ["req-123"])'),
1704
+ rule_id: zod_1.z
1705
+ .array(zod_1.z.string())
1706
+ .optional()
1707
+ .nullable()
1708
+ .describe('Filter by rule IDs that processed the request (e.g., ["rule-123"])'),
1709
+ status_code: zod_1.z
1710
+ .array(zod_1.z.string())
1711
+ .optional()
1712
+ .nullable()
1713
+ .describe('Filter by HTTP status codes (e.g., ["200", "404", "500"])'),
1714
+ from: zod_1.z
1715
+ .string()
1716
+ .default("-1d")
1717
+ .optional()
1718
+ .nullable()
1719
+ .describe("Start time (relative like '-1d', '-24h', '-7d' or absolute timestamp)"),
1720
+ until: zod_1.z
1721
+ .string()
1722
+ .default("-0h")
1723
+ .optional()
1724
+ .nullable()
1725
+ .describe("End time (relative like '-0h', '-1h' or absolute timestamp)"),
1726
+ max_results: zod_1.z
1727
+ .number()
1728
+ .default(25)
1729
+ .optional()
1730
+ .nullable()
1731
+ .describe("Maximum number of results to return (1-1000, e.g., 10, 25, 50). Default 25 for performance"),
1732
+ sort_order: zod_1.z
1733
+ .enum(["asc", "desc"])
1734
+ .optional()
1735
+ .describe("Sort order for results: 'asc' (oldest first) or 'desc' (newest first)"),
1736
+ next: zod_1.z
1737
+ .string()
1738
+ .optional()
1739
+ .nullable()
1740
+ .describe("Pagination token for next page of results (provided in previous response)"),
1741
+ requests_source: zod_1.z
1742
+ .enum(["pg", "clickhouse"])
1743
+ .default("pg")
1744
+ .optional()
1745
+ .describe("Data source: 'pg' (PostgreSQL, default) or 'clickhouse'"),
1746
+ }, async ({ spec_id, collection_id, endpoint_id, tag, ip, external_request_id, rule_id, status_code, from, until, max_results, sort_order, next, requests_source, }) => {
1747
+ // Validate time format parameters
1748
+ if (from) {
1749
+ const fromCheck = validateTimeFormat(from);
1750
+ if (!fromCheck.valid) {
1751
+ return { content: [{ type: "text", text: fromCheck.error }] };
1752
+ }
1753
+ }
1754
+ if (until) {
1755
+ const untilCheck = validateTimeFormat(until);
1756
+ if (!untilCheck.valid) {
1757
+ return { content: [{ type: "text", text: untilCheck.error }] };
1758
+ }
1759
+ }
1760
+ // Validate max_results parameter
1761
+ const paginationCheck = validatePagination(undefined, max_results);
1762
+ if (!paginationCheck.valid) {
1763
+ return { content: [{ type: "text", text: paginationCheck.error }] };
1764
+ }
1765
+ const queryParams = {};
1766
+ // Add array parameters
1767
+ if (spec_id !== null && spec_id !== undefined) {
1768
+ queryParams["spec_id"] = spec_id;
1769
+ }
1770
+ if (collection_id !== null && collection_id !== undefined) {
1771
+ queryParams["collection_id"] = collection_id;
1772
+ }
1773
+ if (endpoint_id !== null && endpoint_id !== undefined) {
1774
+ queryParams["endpoint_id"] = endpoint_id;
1775
+ }
1776
+ if (tag !== null && tag !== undefined) {
1777
+ queryParams["tag"] = tag;
1778
+ }
1779
+ if (ip !== null && ip !== undefined) {
1780
+ queryParams["ip"] = ip;
1781
+ }
1782
+ if (external_request_id !== null && external_request_id !== undefined) {
1783
+ queryParams["external_request_id"] = external_request_id;
1784
+ }
1785
+ if (rule_id !== null && rule_id !== undefined) {
1786
+ queryParams["rule_id"] = rule_id;
1787
+ }
1788
+ if (status_code !== null && status_code !== undefined) {
1789
+ queryParams["status_code"] = status_code;
1790
+ }
1791
+ // Add single value parameters
1792
+ if (from !== null && from !== undefined) {
1793
+ queryParams["from"] = from;
1794
+ }
1795
+ if (until !== null && until !== undefined) {
1796
+ queryParams["until"] = until;
1797
+ }
1798
+ if (max_results !== null && max_results !== undefined) {
1799
+ queryParams["max_results"] = max_results;
1800
+ }
1801
+ if (sort_order !== null && sort_order !== undefined) {
1802
+ queryParams["sort_order"] = sort_order;
1803
+ }
1804
+ if (next !== null && next !== undefined) {
1805
+ queryParams["next"] = next;
1806
+ }
1807
+ if (requests_source !== null && requests_source !== undefined) {
1808
+ queryParams["requests_source"] = requests_source;
1809
+ }
1810
+ const data = await makeImpartRequest("requests", queryParams);
1811
+ if (!data) {
1812
+ const lastError = makeImpartRequest.lastError || "Unknown error";
1813
+ return {
1814
+ content: [{ type: "text", text: `Unable to fetch requests: ${lastError}` }],
1815
+ };
1816
+ }
1817
+ const requests = data.items || [];
1818
+ const meta = data.meta || {};
1819
+ if (requests.length === 0) {
1820
+ return {
1821
+ content: [{ type: "text", text: "No requests found matching the specified criteria." }],
1822
+ };
1823
+ }
1824
+ // Limit output for performance and readability
1825
+ const maxDisplay = Math.min(requests.length, 20);
1826
+ const showingLimited = requests.length > maxDisplay;
1827
+ const result = [
1828
+ `Found ${requests.length} requests (Total available: ${meta.total || "unknown"})`,
1829
+ showingLimited
1830
+ ? `Showing first ${maxDisplay} requests for performance. Use pagination or filters to get specific results.`
1831
+ : "",
1832
+ "=".repeat(60),
1833
+ "",
1834
+ ];
1835
+ // Show condensed format for better readability
1836
+ requests.slice(0, maxDisplay).forEach((request, index) => {
1837
+ const clientIp = request.client_ip?.address || "unknown";
1838
+ const tags = request.tags && request.tags.length > 0
1839
+ ? request.tags.length > 3
1840
+ ? `${request.tags.slice(0, 3).join(", ")} (+${request.tags.length - 3} more)`
1841
+ : request.tags.join(", ")
1842
+ : "None";
1843
+ result.push(`${index + 1}. ${request.method} ${request.path} (${request.status_code})`, ` ID: ${request.id} | IP: ${clientIp} | ${request.timestamp}`, ` Tags: ${tags}`, ` Rule: ${request.rule_id || "None"}`, "");
1844
+ });
1845
+ // Add pagination info
1846
+ if (data.next) {
1847
+ result.push(`Next page token: ${data.next}`);
1848
+ result.push("Use this token in the 'next' parameter to get the next page of results.");
1849
+ }
1850
+ return {
1851
+ content: [{ type: "text", text: result.join("\n") }],
1852
+ };
1853
+ });
1854
+ server.tool(getToolName("get_request_details"), "Get detailed information about a specific HTTP request captured by Impart. Provides complete request metadata, client information, and processing details. Examples:\n\n" +
1855
+ '• Basic usage: {"request_id": "123e4567-e89b-12d3-a456-426614174000"}\n' +
1856
+ '• With data source: {"request_id": "123e4567-e89b-12d3-a456-426614174000", "requests_source": "clickhouse"}\n' +
1857
+ '• PostgreSQL source: {"request_id": "abc12345-678d-90ef-1234-567890abcdef", "requests_source": "pg"}\n\n' +
1858
+ "Note: Use request IDs from the get_requests tool or your Impart dashboard.", {
1859
+ request_id: zod_1.z
1860
+ .string()
1861
+ .describe("The unique identifier of the request (UUID format, e.g., '123e4567-e89b-12d3-a456-426614174000')"),
1862
+ requests_source: zod_1.z
1863
+ .enum(["pg", "clickhouse"])
1864
+ .default("pg")
1865
+ .optional()
1866
+ .describe("Data source: 'pg' (PostgreSQL, default) or 'clickhouse'"),
1867
+ }, async ({ request_id, requests_source }) => {
1868
+ if (!request_id) {
1869
+ return {
1870
+ content: [
1871
+ {
1872
+ type: "text",
1873
+ text: "Error: request_id is required. Please provide a valid UUID for the request you want to retrieve details for.",
1874
+ },
1875
+ ],
1876
+ };
1877
+ }
1878
+ const queryParams = {};
1879
+ if (requests_source !== null && requests_source !== undefined) {
1880
+ queryParams["requests_source"] = requests_source;
1881
+ }
1882
+ const data = await makeImpartRequest(`requests/${request_id}`, queryParams);
1883
+ if (!data) {
1884
+ return {
1885
+ content: [
1886
+ {
1887
+ type: "text",
1888
+ text: `Unable to fetch request details for ID '${request_id}': Request not found or unknown error occurred.`,
1889
+ },
1890
+ ],
1891
+ };
1892
+ }
1893
+ // Format the detailed request information
1894
+ const result = [
1895
+ "Request Details",
1896
+ "=".repeat(50),
1897
+ "",
1898
+ `ID: ${data.id}`,
1899
+ `Method: ${data.method}`,
1900
+ `Path: ${data.path}`,
1901
+ `Status Code: ${data.status_code}`,
1902
+ `Timestamp: ${data.timestamp}`,
1903
+ "",
1904
+ "Client Information:",
1905
+ ` IP Address: ${data.client_ip?.address || "N/A"}`,
1906
+ ` IP Version: ${data.client_ip?.version || "N/A"}`,
1907
+ "",
1908
+ "Request Metadata:",
1909
+ ` Spec ID: ${data.spec_id}`,
1910
+ ` Endpoint ID: ${data.endpoint_id}`,
1911
+ ` External Request ID: ${data.external_request_id}`,
1912
+ ` Rule ID: ${data.rule_id || "N/A (no rule processed this request)"}`,
1913
+ "",
1914
+ "Tags:",
1915
+ data.tags && data.tags.length > 0 ? data.tags.map((tag) => ` • ${tag}`).join("\n") : " No tags applied",
1916
+ "",
1917
+ ];
1918
+ // Add external links if available
1919
+ if (data.external_links && Object.keys(data.external_links).length > 0) {
1920
+ result.push("External Links:");
1921
+ Object.entries(data.external_links).forEach(([key, value]) => {
1922
+ // Properly serialize objects and arrays
1923
+ const serializedValue = typeof value === "object" ? JSON.stringify(value) : String(value);
1924
+ result.push(` ${key}: ${serializedValue}`);
1925
+ });
1926
+ result.push("");
1927
+ }
1928
+ // Add data source info
1929
+ result.push(`Data Source: ${requests_source || "pg"} (${requests_source === "clickhouse" ? "ClickHouse" : "PostgreSQL"})`);
1930
+ return {
1931
+ content: [{ type: "text", text: result.join("\n") }],
1932
+ };
1933
+ });
1083
1934
  async function main() {
1084
1935
  const transport = new stdio_js_1.StdioServerTransport();
1085
1936
  await server.connect(transport);