@impart-security/impart-mcp 0.1.3 → 0.2.1

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