@calltelemetry/cucm-mcp 0.1.8 → 0.2.3

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
@@ -2,52 +2,73 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { listNodeServiceLogs, selectLogs, selectLogsMinutes, getOneFile, getOneFileAnyWithRetry, writeDownloadedFile, } from "./dime.js";
5
+ import { mkdirSync, writeFileSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ import {
8
+ listNodeServiceLogs,
9
+ selectLogs,
10
+ selectLogsMinutes,
11
+ getOneFile,
12
+ getOneFileAnyWithRetry,
13
+ writeDownloadedFile
14
+ } from "./dime.js";
6
15
  import { guessTimezoneString } from "./time.js";
7
16
  import { PacketCaptureManager } from "./packetCapture.js";
8
17
  import { defaultStateStore } from "./state.js";
9
- import { applyPhone, updatePhonePacketCapture } from "./axl.js";
10
- // Default to accepting self-signed/invalid certs (common on CUCM lab/dev).
11
- // Opt back into strict verification with CUCM_MCP_TLS_MODE=strict.
18
+ import { applyPhone, updatePhonePacketCapture, axlExecute } from "./axl.js";
19
+ import { pcapCallSummary, pcapSipCalls, pcapScppMessages, pcapRtpStreams, pcapProtocolFilter } from "./pcap-analyze.js";
12
20
  const tlsMode = (process.env.CUCM_MCP_TLS_MODE || process.env.MCP_TLS_MODE || "").toLowerCase();
13
21
  const strictTls = tlsMode === "strict" || tlsMode === "verify";
14
- // Default: permissive TLS (accept self-signed). This is the common CUCM lab posture.
15
- // Set CUCM_MCP_TLS_MODE=strict to enforce verification.
16
- if (!strictTls)
17
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
22
+ if (!strictTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
18
23
  const server = new McpServer({ name: "cucm", version: "0.1.8" });
19
24
  const captures = new PacketCaptureManager();
20
25
  const captureState = defaultStateStore();
21
- const dimeAuthSchema = z
22
- .object({
23
- username: z.string().optional(),
24
- password: z.string().optional(),
25
- })
26
- .optional();
27
- const sshAuthSchema = z
28
- .object({
29
- username: z.string().optional(),
30
- password: z.string().optional(),
31
- })
32
- .optional();
33
- const axlAuthSchema = z
34
- .object({
35
- username: z.string().optional(),
36
- password: z.string().optional(),
37
- })
38
- .optional();
39
- server.tool("guess_timezone_string", "Build a best-effort DIME timezone string for selectLogFiles.", {}, async () => ({
40
- content: [{ type: "text", text: JSON.stringify({ timezone: guessTimezoneString(new Date()) }, null, 2) }],
41
- }));
42
- server.tool("list_node_service_logs", "List CUCM cluster nodes and their available service logs (DIME listNodeServiceLogs).", {
26
+ const dimeAuthSchema = z.object({
27
+ username: z.string().optional(),
28
+ password: z.string().optional()
29
+ }).optional();
30
+ const sshAuthSchema = z.object({
31
+ username: z.string().optional(),
32
+ password: z.string().optional()
33
+ }).optional();
34
+ const axlAuthSchema = z.object({
35
+ username: z.string().optional(),
36
+ password: z.string().optional()
37
+ }).optional();
38
+ function formatUnknownError(e) {
39
+ if (e instanceof Error) return e.message;
40
+ if (typeof e === "string") return e;
41
+ try {
42
+ return JSON.stringify(e);
43
+ } catch {
44
+ return String(e);
45
+ }
46
+ }
47
+ server.tool(
48
+ "guess_timezone_string",
49
+ "Build a best-effort DIME timezone string for selectLogFiles.",
50
+ {},
51
+ async () => ({
52
+ content: [{ type: "text", text: JSON.stringify({ timezone: guessTimezoneString(/* @__PURE__ */ new Date()) }, null, 2) }]
53
+ })
54
+ );
55
+ server.tool(
56
+ "list_node_service_logs",
57
+ "List CUCM cluster nodes and their available service logs (DIME listNodeServiceLogs).",
58
+ {
43
59
  host: z.string(),
44
60
  port: z.number().int().min(1).max(65535).optional(),
45
- auth: dimeAuthSchema,
46
- }, async ({ host, port, auth }) => {
61
+ auth: dimeAuthSchema
62
+ },
63
+ async ({ host, port, auth }) => {
47
64
  const result = await listNodeServiceLogs(host, auth, port);
48
65
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
49
- });
50
- server.tool("select_logs", "List log/trace files using DIME selectLogFiles. Supports ServiceLogs and SystemLogs.", {
66
+ }
67
+ );
68
+ server.tool(
69
+ "select_logs",
70
+ "List log/trace files using DIME selectLogFiles. Supports ServiceLogs and SystemLogs.",
71
+ {
51
72
  host: z.string(),
52
73
  port: z.number().int().min(1).max(65535).optional(),
53
74
  auth: dimeAuthSchema,
@@ -56,12 +77,22 @@ server.tool("select_logs", "List log/trace files using DIME selectLogFiles. Supp
56
77
  searchStr: z.string().optional().describe("Optional filename substring filter"),
57
78
  fromDate: z.string(),
58
79
  toDate: z.string(),
59
- timezone: z.string(),
60
- }, async ({ host, port, auth, serviceLogs, systemLogs, searchStr, fromDate, toDate, timezone }) => {
61
- const result = await selectLogs(host, { serviceLogs, systemLogs, searchStr, fromDate, toDate, timezone }, auth, port);
80
+ timezone: z.string()
81
+ },
82
+ async ({ host, port, auth, serviceLogs, systemLogs, searchStr, fromDate, toDate, timezone }) => {
83
+ const result = await selectLogs(
84
+ host,
85
+ { serviceLogs, systemLogs, searchStr, fromDate, toDate, timezone },
86
+ auth,
87
+ port
88
+ );
62
89
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
63
- });
64
- server.tool("select_logs_minutes", "Convenience wrapper: select logs using a minutes-back window.", {
90
+ }
91
+ );
92
+ server.tool(
93
+ "select_logs_minutes",
94
+ "Convenience wrapper: select logs using a minutes-back window.",
95
+ {
65
96
  host: z.string(),
66
97
  port: z.number().int().min(1).max(65535).optional(),
67
98
  auth: dimeAuthSchema,
@@ -69,333 +100,679 @@ server.tool("select_logs_minutes", "Convenience wrapper: select logs using a min
69
100
  serviceLogs: z.array(z.string()).optional(),
70
101
  systemLogs: z.array(z.string()).optional(),
71
102
  searchStr: z.string().optional(),
72
- timezone: z.string().optional(),
73
- }, async ({ host, port, auth, minutesBack, serviceLogs, systemLogs, searchStr, timezone }) => {
74
- const result = await selectLogsMinutes(host, minutesBack, { serviceLogs, systemLogs, searchStr }, timezone, auth, port);
103
+ timezone: z.string().optional()
104
+ },
105
+ async ({ host, port, auth, minutesBack, serviceLogs, systemLogs, searchStr, timezone }) => {
106
+ const result = await selectLogsMinutes(
107
+ host,
108
+ minutesBack,
109
+ { serviceLogs, systemLogs, searchStr },
110
+ timezone,
111
+ auth,
112
+ port
113
+ );
75
114
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
76
- });
77
- server.tool("select_syslog_minutes", "Convenience wrapper: select system log files (e.g. Syslog) using a minutes-back window.", {
115
+ }
116
+ );
117
+ server.tool(
118
+ "select_syslog_minutes",
119
+ "Convenience wrapper: select system log files (e.g. Syslog) using a minutes-back window.",
120
+ {
78
121
  host: z.string(),
79
122
  port: z.number().int().min(1).max(65535).optional(),
80
123
  auth: dimeAuthSchema,
81
124
  minutesBack: z.number().int().min(1).max(60 * 24 * 30),
82
- systemLog: z
83
- .string()
84
- .optional()
85
- .describe("System log selection name. Default is 'Syslog' (may vary by CUCM version)."),
125
+ systemLog: z.string().optional().describe("System log selection name. Default is 'Syslog' (may vary by CUCM version)."),
86
126
  searchStr: z.string().optional(),
87
- timezone: z.string().optional(),
88
- }, async ({ host, port, auth, minutesBack, systemLog, searchStr, timezone }) => {
89
- const result = await selectLogsMinutes(host, minutesBack, { systemLogs: [systemLog || "Syslog"], searchStr }, timezone, auth, port);
127
+ timezone: z.string().optional()
128
+ },
129
+ async ({ host, port, auth, minutesBack, systemLog, searchStr, timezone }) => {
130
+ const result = await selectLogsMinutes(
131
+ host,
132
+ minutesBack,
133
+ { systemLogs: [systemLog || "Syslog"], searchStr },
134
+ timezone,
135
+ auth,
136
+ port
137
+ );
90
138
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
91
- });
92
- server.tool("phone_packet_capture_enable", "Enable phone packet capture via CUCM AXL (updatePhone packetCaptureMode/Duration + applyPhone).", {
139
+ }
140
+ );
141
+ server.tool(
142
+ "phone_packet_capture_enable",
143
+ "Enable phone packet capture via CUCM AXL (updatePhone packetCaptureMode/Duration + applyPhone).",
144
+ {
93
145
  host: z.string().describe("CUCM host/IP"),
94
146
  port: z.number().int().min(1).max(65535).optional().describe("AXL port (default 8443)"),
95
147
  axlVersion: z.string().optional().describe("AXL API version (default env CUCM_AXL_VERSION or 15.0)"),
96
148
  auth: axlAuthSchema.describe("AXL auth (optional; defaults to CUCM_AXL_USERNAME/CUCM_AXL_PASSWORD)"),
97
149
  deviceName: z.string().min(1).describe("Phone device name (e.g. SEP505C885DF37F)"),
98
- mode: z
99
- .string()
100
- .optional()
101
- .describe('packetCaptureMode value (commonly "Batch Processing Mode")'),
102
- durationSeconds: z
103
- .number()
104
- .int()
105
- .min(1)
106
- .max(60 * 60)
107
- .optional()
108
- .describe("packetCaptureDuration in seconds (default 60)"),
150
+ mode: z.string().optional().describe('packetCaptureMode value (commonly "Batch Processing Mode")'),
151
+ durationSeconds: z.number().int().min(1).max(60 * 60).optional().describe("packetCaptureDuration in seconds (default 60)"),
109
152
  apply: z.boolean().optional().describe("Run applyPhone after updatePhone (default true)"),
110
- timeoutMs: z.number().int().min(1000).max(5 * 60_000).optional().describe("AXL request timeout"),
111
- }, async ({ host, port, axlVersion, auth, deviceName, mode, durationSeconds, apply, timeoutMs }) => {
153
+ timeoutMs: z.number().int().min(1e3).max(5 * 6e4).optional().describe("AXL request timeout")
154
+ },
155
+ async ({ host, port, axlVersion, auth, deviceName, mode, durationSeconds, apply, timeoutMs }) => {
112
156
  const update = await updatePhonePacketCapture(host, {
113
- deviceName,
114
- mode: mode || "Batch Processing Mode",
115
- durationSeconds: durationSeconds ?? 60,
116
- auth: auth,
117
- port,
118
- version: axlVersion,
119
- timeoutMs,
157
+ deviceName,
158
+ mode: mode || "Batch Processing Mode",
159
+ durationSeconds: durationSeconds ?? 60,
160
+ auth,
161
+ port,
162
+ version: axlVersion,
163
+ timeoutMs
120
164
  });
121
165
  const shouldApply = apply ?? true;
122
- const applied = shouldApply
123
- ? await applyPhone(host, {
124
- deviceName,
125
- auth: auth,
126
- port,
127
- version: axlVersion,
128
- timeoutMs,
129
- })
130
- : undefined;
166
+ const applied = shouldApply ? await applyPhone(host, {
167
+ deviceName,
168
+ auth,
169
+ port,
170
+ version: axlVersion,
171
+ timeoutMs
172
+ }) : void 0;
131
173
  return {
132
- content: [
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: JSON.stringify(
133
178
  {
134
- type: "text",
135
- text: JSON.stringify({
136
- host: update.host,
137
- deviceName,
138
- packetCaptureMode: mode || "Batch Processing Mode",
139
- packetCaptureDuration: durationSeconds ?? 60,
140
- updatePhoneReturn: update.returnValue,
141
- applied: shouldApply,
142
- applyPhoneReturn: applied?.returnValue,
143
- notes: [
144
- "Phone may need to reset to pick up config.",
145
- "Place the call during the duration window; CUCM writes the capture to its TFTP directory (CUCM behavior/version dependent).",
146
- ],
147
- }, null, 2),
179
+ host: update.host,
180
+ deviceName,
181
+ packetCaptureMode: mode || "Batch Processing Mode",
182
+ packetCaptureDuration: durationSeconds ?? 60,
183
+ updatePhoneReturn: update.returnValue,
184
+ applied: shouldApply,
185
+ applyPhoneReturn: applied?.returnValue,
186
+ notes: [
187
+ "Phone may need to reset to pick up config.",
188
+ "Place the call during the duration window; CUCM writes the capture to its TFTP directory (CUCM behavior/version dependent)."
189
+ ]
148
190
  },
149
- ],
191
+ null,
192
+ 2
193
+ )
194
+ }
195
+ ]
150
196
  };
151
- });
152
- server.tool("download_file", "Download a single file via DIME GetOneFile and write it to disk.", {
197
+ }
198
+ );
199
+ server.tool(
200
+ "axl_execute",
201
+ "Execute an arbitrary CUCM AXL SOAP operation. Accepts a JSON-ish data payload and converts it to XML. Tip: for get* operations, returnedTags can be an array of strings (supports dotted paths like 'lines.line.dirn.pattern').",
202
+ {
203
+ operation: z.string().min(1).describe("AXL operation name, e.g. getPhone, updateLine, updateCtiRoutePoint"),
204
+ data: z.any().optional().describe("Operation payload as an object. Keys become XML tags."),
205
+ cucm_host: z.string().describe("CUCM host/IP"),
206
+ cucm_port: z.number().int().min(1).max(65535).optional().describe("AXL port (default 8443)"),
207
+ cucm_version: z.string().optional().describe("AXL API version (e.g. 15.0)"),
208
+ cucm_username: z.string().optional().describe("AXL username (optional if env CUCM_AXL_USERNAME is set)"),
209
+ cucm_password: z.string().optional().describe("AXL password (optional if env CUCM_AXL_PASSWORD is set)"),
210
+ timeoutMs: z.number().int().min(1e3).max(5 * 6e4).optional().describe("AXL request timeout"),
211
+ includeRequestXml: z.boolean().optional().describe("Include SOAP request XML in response (debug)"),
212
+ includeResponseXml: z.boolean().optional().describe("Include SOAP response XML in response (debug)")
213
+ },
214
+ async ({
215
+ operation,
216
+ data,
217
+ cucm_host,
218
+ cucm_port,
219
+ cucm_version,
220
+ cucm_username,
221
+ cucm_password,
222
+ timeoutMs,
223
+ includeRequestXml,
224
+ includeResponseXml
225
+ }) => {
226
+ const auth = cucm_username && cucm_password ? { username: cucm_username, password: cucm_password } : void 0;
227
+ try {
228
+ const result = await axlExecute(cucm_host, {
229
+ operation,
230
+ data,
231
+ auth,
232
+ port: cucm_port,
233
+ version: cucm_version,
234
+ timeoutMs,
235
+ includeRequestXml,
236
+ includeResponseXml
237
+ });
238
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...result }, null, 2) }] };
239
+ } catch (e) {
240
+ const msg = formatUnknownError(e);
241
+ const guessGetOperation = (op) => {
242
+ const s = String(op || "").trim();
243
+ if (!s) return null;
244
+ const prefixes = ["update", "add", "remove", "set"];
245
+ for (const p of prefixes) {
246
+ if (s.startsWith(p) && s.length > p.length) {
247
+ return `get${s.slice(p.length)}`;
248
+ }
249
+ }
250
+ return null;
251
+ };
252
+ const getOp = guessGetOperation(operation);
253
+ const hints = [
254
+ "If the error is opaque, retry with includeRequestXml=true and includeResponseXml=true to see the SOAP fault.",
255
+ "For get* operations, try returnedTags as an array of strings (supports dotted paths).",
256
+ "For update* operations, first run the corresponding get* to copy the exact nested shape and/or uuids.",
257
+ "If you see schema/version errors, make sure cucm_version matches your CUCM AXL version (Help > About in CUCM)."
258
+ ];
259
+ if (/self-signed certificate|unable to verify|CERT/i.test(msg)) {
260
+ hints.unshift(
261
+ "TLS: this MCP defaults to permissive TLS, but your runtime may still be enforcing verification. Set CUCM_MCP_TLS_MODE=permissive (or MCP_TLS_MODE=permissive)."
262
+ );
263
+ }
264
+ if (/401|403|auth/i.test(msg)) {
265
+ hints.unshift(
266
+ "Auth: verify AXL credentials and that the CUCM user has AXL permissions for this operation (AXL role/group)."
267
+ );
268
+ }
269
+ if (/not found|does not exist|unknown/i.test(msg)) {
270
+ hints.unshift(
271
+ "Existence: confirm the target object exists by calling a get* operation first (e.g. getCtiRoutePoint/getLine)."
272
+ );
273
+ }
274
+ const nextToolCalls = [
275
+ ...getOp ? [
276
+ {
277
+ tool: "axl_execute",
278
+ args: {
279
+ operation: getOp,
280
+ cucm_host,
281
+ cucm_port,
282
+ cucm_version,
283
+ cucm_username,
284
+ cucm_password,
285
+ includeResponseXml: true
286
+ },
287
+ note: 'Fetch the current object first (add an appropriate data payload, e.g. {name: "..."} or {uuid: "..."}) to learn nesting/uuid requirements.'
288
+ }
289
+ ] : [],
290
+ {
291
+ tool: "axl_execute",
292
+ args: {
293
+ operation,
294
+ cucm_host,
295
+ cucm_port,
296
+ cucm_version,
297
+ cucm_username,
298
+ cucm_password,
299
+ includeRequestXml: true,
300
+ includeResponseXml: true
301
+ },
302
+ note: "Re-run with debug XML included to capture the SOAP Fault details."
303
+ }
304
+ ];
305
+ return {
306
+ content: [
307
+ {
308
+ type: "text",
309
+ text: JSON.stringify(
310
+ {
311
+ ok: false,
312
+ error: true,
313
+ message: msg,
314
+ operation,
315
+ cucm_host,
316
+ hints,
317
+ nextToolCalls
318
+ },
319
+ null,
320
+ 2
321
+ )
322
+ }
323
+ ]
324
+ };
325
+ }
326
+ }
327
+ );
328
+ server.tool(
329
+ "axl_download_wsdl",
330
+ "Download the CUCM AXL WSDL to a local file. Useful when you need schema hints for an operation.",
331
+ {
332
+ cucm_host: z.string().describe("CUCM host/IP"),
333
+ cucm_port: z.number().int().min(1).max(65535).optional().describe("AXL port (default 8443)"),
334
+ cucm_username: z.string().optional().describe("AXL username (optional if env CUCM_AXL_USERNAME is set)"),
335
+ cucm_password: z.string().optional().describe("AXL password (optional if env CUCM_AXL_PASSWORD is set)"),
336
+ outFile: z.string().optional().describe("Optional output file path (default /tmp/cucm-mcp/axl.wsdl)")
337
+ },
338
+ async ({ cucm_host, cucm_port, cucm_username, cucm_password, outFile }) => {
339
+ const port = cucm_port ?? 8443;
340
+ const user = cucm_username || process.env.CUCM_AXL_USERNAME || process.env.CUCM_USERNAME;
341
+ const pass = cucm_password || process.env.CUCM_AXL_PASSWORD || process.env.CUCM_PASSWORD;
342
+ if (!user || !pass) {
343
+ return {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: JSON.stringify(
348
+ {
349
+ ok: false,
350
+ error: true,
351
+ message: "Missing AXL credentials (provide cucm_username/cucm_password or set CUCM_AXL_USERNAME/CUCM_AXL_PASSWORD)"
352
+ },
353
+ null,
354
+ 2
355
+ )
356
+ }
357
+ ]
358
+ };
359
+ }
360
+ const url = `https://${cucm_host}:${port}/axl/?wsdl`;
361
+ try {
362
+ const res = await fetch(url, {
363
+ headers: {
364
+ Authorization: `Basic ${Buffer.from(`${user}:${pass}`, "utf8").toString("base64")}`,
365
+ Accept: "application/xml, text/xml, */*"
366
+ },
367
+ signal: AbortSignal.timeout(3e4)
368
+ });
369
+ const text = await res.text().catch(() => "");
370
+ const file = outFile || join("/tmp", "cucm-mcp", "axl.wsdl");
371
+ mkdirSync(dirname(file), { recursive: true });
372
+ writeFileSync(file, text, "utf8");
373
+ return {
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: JSON.stringify(
378
+ {
379
+ ok: res.ok,
380
+ url,
381
+ status: res.status,
382
+ savedPath: file,
383
+ bytes: Buffer.byteLength(text, "utf8"),
384
+ note: "WSDL often imports XSDs; you may need to fetch those referenced URLs too."
385
+ },
386
+ null,
387
+ 2
388
+ )
389
+ }
390
+ ]
391
+ };
392
+ } catch (e) {
393
+ return {
394
+ content: [
395
+ {
396
+ type: "text",
397
+ text: JSON.stringify(
398
+ {
399
+ ok: false,
400
+ error: true,
401
+ url,
402
+ message: formatUnknownError(e)
403
+ },
404
+ null,
405
+ 2
406
+ )
407
+ }
408
+ ]
409
+ };
410
+ }
411
+ }
412
+ );
413
+ server.tool(
414
+ "download_file",
415
+ "Download a single file via DIME GetOneFile and write it to disk.",
416
+ {
153
417
  host: z.string(),
154
418
  port: z.number().int().min(1).max(65535).optional(),
155
419
  auth: dimeAuthSchema,
156
420
  filePath: z.string().min(1).describe("Absolute path on CUCM"),
157
- outFile: z.string().optional().describe("Optional output path. Default: /tmp/cucm-mcp/<basename>"),
158
- }, async ({ host, port, auth, filePath, outFile }) => {
421
+ outFile: z.string().optional().describe("Optional output path. Default: /tmp/cucm-mcp/<basename>")
422
+ },
423
+ async ({ host, port, auth, filePath, outFile }) => {
159
424
  const dl = await getOneFile(host, filePath, auth, port);
160
425
  const saved = writeDownloadedFile(dl, outFile);
161
426
  return {
162
- content: [
163
- {
164
- type: "text",
165
- text: JSON.stringify({ server: dl.server, sourcePath: dl.filename, savedPath: saved.filePath, bytes: saved.bytes }, null, 2),
166
- },
167
- ],
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: JSON.stringify({ server: dl.server, sourcePath: dl.filename, savedPath: saved.filePath, bytes: saved.bytes }, null, 2)
431
+ }
432
+ ]
168
433
  };
169
- });
170
- server.tool("packet_capture_start", "Start a packet capture on CUCM via SSH (utils network capture). Returns a captureId.", {
434
+ }
435
+ );
436
+ server.tool(
437
+ "packet_capture_start",
438
+ "Start a packet capture on CUCM via SSH (utils network capture). Returns a captureId.",
439
+ {
171
440
  host: z.string().describe("CUCM host/IP"),
172
441
  sshPort: z.number().int().min(1).max(65535).optional(),
173
442
  auth: sshAuthSchema,
174
443
  iface: z.string().optional().describe("Interface (default eth0)"),
175
444
  fileBase: z.string().optional().describe("Capture base name (no dots). Saved as <fileBase>.cap"),
176
- count: z.number().int().min(1).max(1_000_000).optional().describe("Packet count (common max is 1000000)"),
177
- maxPackets: z
178
- .boolean()
179
- .optional()
180
- .describe("If true and count is omitted, uses a high capture count (1,000,000)"),
445
+ count: z.number().int().min(1).max(1e6).optional().describe("Packet count (common max is 1000000)"),
446
+ maxPackets: z.boolean().optional().describe("If true and count is omitted, uses a high capture count (1,000,000)"),
181
447
  size: z.string().optional().describe("Packet size (e.g. all)"),
182
448
  hostFilterIp: z.string().optional().describe("Optional filter: host ip <addr>"),
183
449
  portFilter: z.number().int().min(1).max(65535).optional().describe("Optional filter: port <num>"),
184
- maxDurationMs: z
185
- .number()
186
- .int()
187
- .min(250)
188
- .max(24 * 60 * 60_000)
189
- .optional()
190
- .describe("Stop after this duration even if packet count isn't reached"),
191
- startTimeoutMs: z
192
- .number()
193
- .int()
194
- .min(2000)
195
- .max(120_000)
196
- .optional()
197
- .describe("Timeout for starting capture (SSH connect + command start)"),
198
- }, async ({ host, sshPort, auth, iface, fileBase, count, maxPackets, size, hostFilterIp, portFilter, maxDurationMs, startTimeoutMs }) => {
199
- const resolvedCount = count ?? (maxPackets ? 1_000_000 : undefined);
450
+ maxDurationMs: z.number().int().min(250).max(24 * 60 * 6e4).optional().describe("Stop after this duration even if packet count isn't reached"),
451
+ startTimeoutMs: z.number().int().min(2e3).max(12e4).optional().describe("Timeout for starting capture (SSH connect + command start)")
452
+ },
453
+ async ({ host, sshPort, auth, iface, fileBase, count, maxPackets, size, hostFilterIp, portFilter, maxDurationMs, startTimeoutMs }) => {
454
+ const resolvedCount = count ?? (maxPackets ? 1e6 : void 0);
200
455
  const result = await captures.start({
201
- host,
202
- sshPort,
203
- auth: auth,
204
- iface,
205
- fileBase,
206
- count: resolvedCount,
207
- size,
208
- hostFilterIp,
209
- portFilter,
210
- maxDurationMs,
211
- startTimeoutMs,
456
+ host,
457
+ sshPort,
458
+ auth,
459
+ iface,
460
+ fileBase,
461
+ count: resolvedCount,
462
+ size,
463
+ hostFilterIp,
464
+ portFilter,
465
+ maxDurationMs,
466
+ startTimeoutMs
212
467
  });
213
- const summary = `Started CUCM packet capture (SSH). ` +
214
- `id=${result.id} host=${result.host} fileBase=${result.fileBase} remoteFilePath=${result.remoteFilePath}. ` +
215
- `Stops when packet count is reached, when you call packet_capture_stop, or via maxDurationMs.`;
468
+ const summary = `Started CUCM packet capture (SSH). id=${result.id} host=${result.host} fileBase=${result.fileBase} remoteFilePath=${result.remoteFilePath}. Stops when packet count is reached, when you call packet_capture_stop, or via maxDurationMs.`;
216
469
  return {
217
- content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(result, null, 2)}` }],
470
+ content: [{ type: "text", text: `${summary}
471
+
472
+ ${JSON.stringify(result, null, 2)}` }]
218
473
  };
219
- });
220
- server.tool("packet_capture_list", "List active packet captures started by this MCP server.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(captures.list(), null, 2) }] }));
221
- server.tool("packet_capture_state_list", "List packet captures from the local state file (survives MCP restarts).", {}, async () => {
474
+ }
475
+ );
476
+ server.tool(
477
+ "packet_capture_list",
478
+ "List active packet captures started by this MCP server.",
479
+ {},
480
+ async () => ({ content: [{ type: "text", text: JSON.stringify(captures.list(), null, 2) }] })
481
+ );
482
+ server.tool(
483
+ "packet_capture_state_list",
484
+ "List packet captures from the local state file (survives MCP restarts).",
485
+ {},
486
+ async () => {
222
487
  const pruned = captureState.pruneExpired(captureState.load());
223
488
  captureState.save(pruned);
224
- const items = Object.values(pruned.captures).sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1));
489
+ const items = Object.values(pruned.captures).sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
225
490
  return { content: [{ type: "text", text: JSON.stringify({ path: captureState.path, captures: items }, null, 2) }] };
226
- });
227
- server.tool("packet_capture_state_get", "Get a packet capture record from the local state file.", {
228
- captureId: z.string().min(1),
229
- }, async ({ captureId }) => {
491
+ }
492
+ );
493
+ server.tool(
494
+ "packet_capture_state_get",
495
+ "Get a packet capture record from the local state file.",
496
+ {
497
+ captureId: z.string().min(1)
498
+ },
499
+ async ({ captureId }) => {
230
500
  const pruned = captureState.pruneExpired(captureState.load());
231
501
  const rec = pruned.captures[captureId];
232
502
  if (!rec) {
233
- return {
234
- content: [
235
- {
236
- type: "text",
237
- text: JSON.stringify({ path: captureState.path, found: false, captureId }, null, 2),
238
- },
239
- ],
240
- };
503
+ return {
504
+ content: [
505
+ {
506
+ type: "text",
507
+ text: JSON.stringify({ path: captureState.path, found: false, captureId }, null, 2)
508
+ }
509
+ ]
510
+ };
241
511
  }
242
512
  return { content: [{ type: "text", text: JSON.stringify({ path: captureState.path, found: true, record: rec }, null, 2) }] };
243
- });
244
- server.tool("packet_capture_state_clear", "Delete a capture record from the local state file.", {
245
- captureId: z.string().min(1),
246
- }, async ({ captureId }) => {
513
+ }
514
+ );
515
+ server.tool(
516
+ "packet_capture_state_clear",
517
+ "Delete a capture record from the local state file.",
518
+ {
519
+ captureId: z.string().min(1)
520
+ },
521
+ async ({ captureId }) => {
247
522
  captureState.remove(captureId);
248
523
  return { content: [{ type: "text", text: JSON.stringify({ removed: true, captureId }, null, 2) }] };
249
- });
250
- server.tool("packet_capture_stop", "Stop a packet capture by captureId (sends Ctrl-C).", {
524
+ }
525
+ );
526
+ server.tool(
527
+ "packet_capture_stop",
528
+ "Stop a packet capture by captureId (sends Ctrl-C).",
529
+ {
251
530
  captureId: z.string().min(1),
252
- timeoutMs: z.number().int().min(1000).max(10 * 60_000).optional().describe("How long to wait for stop (default ~90s)"),
253
- }, async ({ captureId, timeoutMs }) => {
531
+ timeoutMs: z.number().int().min(1e3).max(10 * 6e4).optional().describe("How long to wait for stop (default ~90s)")
532
+ },
533
+ async ({ captureId, timeoutMs }) => {
254
534
  try {
255
- const result = await captures.stop(captureId, timeoutMs);
256
- const summary = `Stopped capture. id=${result.id} stopTimedOut=${Boolean(result.stopTimedOut)} remoteFilePath=${result.remoteFilePath}`;
257
- return { content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(result, null, 2)}` }] };
258
- }
259
- catch (e) {
260
- const stopError = e instanceof Error ? e.message : String(e || "");
261
- const pruned = captureState.pruneExpired(captureState.load());
262
- const rec = pruned.captures[captureId];
263
- if (!rec)
264
- throw e;
265
- const summary = `Failed to stop capture (returning state record). id=${captureId} stopError=${JSON.stringify(stopError)}`;
266
- return {
267
- content: [
268
- {
269
- type: "text",
270
- text: `${summary}\n\n${JSON.stringify({ stopError, record: rec }, null, 2)}`,
271
- },
272
- ],
273
- };
535
+ const result = await captures.stop(captureId, timeoutMs);
536
+ const summary = `Stopped capture. id=${result.id} stopTimedOut=${Boolean(result.stopTimedOut)} remoteFilePath=${result.remoteFilePath}`;
537
+ return { content: [{ type: "text", text: `${summary}
538
+
539
+ ${JSON.stringify(result, null, 2)}` }] };
540
+ } catch (e) {
541
+ const stopError = e instanceof Error ? e.message : String(e || "");
542
+ const pruned = captureState.pruneExpired(captureState.load());
543
+ const rec = pruned.captures[captureId];
544
+ if (!rec) throw e;
545
+ const summary = `Failed to stop capture (returning state record). id=${captureId} stopError=${JSON.stringify(stopError)}`;
546
+ return {
547
+ content: [
548
+ {
549
+ type: "text",
550
+ text: `${summary}
551
+
552
+ ${JSON.stringify({ stopError, record: rec }, null, 2)}`
553
+ }
554
+ ]
555
+ };
274
556
  }
275
- });
276
- server.tool("packet_capture_stop_and_download", "Stop a packet capture and download the resulting .cap file via DIME.", {
557
+ }
558
+ );
559
+ server.tool(
560
+ "packet_capture_stop_and_download",
561
+ "Stop a packet capture and download the resulting .cap file via DIME.",
562
+ {
277
563
  captureId: z.string().min(1),
278
564
  dimePort: z.number().int().min(1).max(65535).optional(),
279
565
  auth: dimeAuthSchema.describe("DIME auth (optional; defaults to CUCM_DIME_USERNAME/CUCM_DIME_PASSWORD)"),
280
566
  outFile: z.string().optional().describe("Optional output path for the downloaded .cap file"),
281
- stopTimeoutMs: z
282
- .number()
283
- .int()
284
- .min(1000)
285
- .max(10 * 60_000)
286
- .optional()
287
- .describe("How long to wait for SSH capture stop (default 300000)"),
288
- downloadTimeoutMs: z
289
- .number()
290
- .int()
291
- .min(1000)
292
- .max(10 * 60_000)
293
- .optional()
294
- .describe("How long to wait for the capture file to appear in DIME (default 300000)"),
295
- downloadPollIntervalMs: z
296
- .number()
297
- .int()
298
- .min(250)
299
- .max(30_000)
300
- .optional()
301
- .describe("How often to retry DIME GetOneFile when the file isn't there yet"),
302
- }, async ({ captureId, dimePort, auth, outFile, stopTimeoutMs, downloadTimeoutMs, downloadPollIntervalMs }) => {
303
- const stopTimeout = stopTimeoutMs ?? 300_000;
304
- const dlTimeout = downloadTimeoutMs ?? 300_000;
305
- const dlPoll = downloadPollIntervalMs ?? 2000;
567
+ stopTimeoutMs: z.number().int().min(1e3).max(10 * 6e4).optional().describe("How long to wait for SSH capture stop (default 300000)"),
568
+ downloadTimeoutMs: z.number().int().min(1e3).max(10 * 6e4).optional().describe("How long to wait for the capture file to appear in DIME (default 300000)"),
569
+ downloadPollIntervalMs: z.number().int().min(250).max(3e4).optional().describe("How often to retry DIME GetOneFile when the file isn't there yet")
570
+ },
571
+ async ({ captureId, dimePort, auth, outFile, stopTimeoutMs, downloadTimeoutMs, downloadPollIntervalMs }) => {
572
+ const stopTimeout = stopTimeoutMs ?? 3e5;
573
+ const dlTimeout = downloadTimeoutMs ?? 3e5;
574
+ const dlPoll = downloadPollIntervalMs ?? 2e3;
306
575
  let stopped;
307
576
  let stopError;
308
577
  try {
309
- stopped = await captures.stop(captureId, stopTimeout);
310
- }
311
- catch (e) {
312
- stopError = e instanceof Error ? e.message : String(e || "");
313
- // Fall back to state file (useful if stop failed or MCP restarted).
314
- const pruned = captureState.pruneExpired(captureState.load());
315
- const rec = pruned.captures[captureId];
316
- if (!rec)
317
- throw new Error(`Failed to stop capture and capture not found in state: ${captureId}. stopError=${stopError}`);
318
- stopped = rec;
578
+ stopped = await captures.stop(captureId, stopTimeout);
579
+ } catch (e) {
580
+ stopError = e instanceof Error ? e.message : String(e || "");
581
+ const pruned = captureState.pruneExpired(captureState.load());
582
+ const rec = pruned.captures[captureId];
583
+ if (!rec) throw new Error(`Failed to stop capture and capture not found in state: ${captureId}. stopError=${stopError}`);
584
+ stopped = rec;
319
585
  }
320
586
  const candidates = (stopped.remoteFileCandidates || []).length ? stopped.remoteFileCandidates : [stopped.remoteFilePath];
321
587
  const dl = await getOneFileAnyWithRetry(stopped.host, candidates, {
322
- auth: auth,
323
- port: dimePort,
324
- timeoutMs: dlTimeout,
325
- pollIntervalMs: dlPoll,
588
+ auth,
589
+ port: dimePort,
590
+ timeoutMs: dlTimeout,
591
+ pollIntervalMs: dlPoll
326
592
  });
327
593
  const saved = writeDownloadedFile(dl, outFile);
328
- const summary = `Capture downloaded. ` +
329
- `id=${captureId} stopTimedOut=${Boolean(stopped.stopTimedOut)} remoteFilePath=${dl.filename} ` +
330
- `savedPath=${saved.filePath} bytes=${saved.bytes}` +
331
- (stopError ? ` stopError=${JSON.stringify(stopError)}` : "");
594
+ const summary = `Capture downloaded. id=${captureId} stopTimedOut=${Boolean(stopped.stopTimedOut)} remoteFilePath=${dl.filename} savedPath=${saved.filePath} bytes=${saved.bytes}` + (stopError ? ` stopError=${JSON.stringify(stopError)}` : "");
332
595
  return {
333
- content: [
596
+ content: [
597
+ {
598
+ type: "text",
599
+ text: `${summary}
600
+
601
+ ${JSON.stringify(
334
602
  {
335
- type: "text",
336
- text: `${summary}\n\n${JSON.stringify({
337
- captureId: stopped.id,
338
- host: stopped.host,
339
- remoteFilePath: dl.filename,
340
- stopTimedOut: stopped.stopTimedOut || false,
341
- stopError,
342
- savedPath: saved.filePath,
343
- bytes: saved.bytes,
344
- dimeAttempts: dl.attempts,
345
- dimeWaitedMs: dl.waitedMs,
346
- }, null, 2)}`,
603
+ captureId: stopped.id,
604
+ host: stopped.host,
605
+ remoteFilePath: dl.filename,
606
+ stopTimedOut: stopped.stopTimedOut || false,
607
+ stopError,
608
+ savedPath: saved.filePath,
609
+ bytes: saved.bytes,
610
+ dimeAttempts: dl.attempts,
611
+ dimeWaitedMs: dl.waitedMs
347
612
  },
348
- ],
613
+ null,
614
+ 2
615
+ )}`
616
+ }
617
+ ]
349
618
  };
350
- });
351
- server.tool("packet_capture_download_from_state", "Download a capture file using the local state record (useful after MCP restart).", {
619
+ }
620
+ );
621
+ server.tool(
622
+ "packet_capture_download_from_state",
623
+ "Download a capture file using the local state record (useful after MCP restart).",
624
+ {
352
625
  captureId: z.string().min(1),
353
626
  dimePort: z.number().int().min(1).max(65535).optional(),
354
627
  auth: dimeAuthSchema.describe("DIME auth (optional; defaults to CUCM_DIME_USERNAME/CUCM_DIME_PASSWORD)"),
355
628
  outFile: z.string().optional().describe("Optional output path for the downloaded .cap file"),
356
- downloadTimeoutMs: z
357
- .number()
358
- .int()
359
- .min(1000)
360
- .max(10 * 60_000)
361
- .optional()
362
- .describe("How long to wait for the capture file to appear in DIME"),
363
- downloadPollIntervalMs: z
364
- .number()
365
- .int()
366
- .min(250)
367
- .max(30_000)
368
- .optional()
369
- .describe("How often to retry DIME GetOneFile when the file isn't there yet"),
370
- }, async ({ captureId, dimePort, auth, outFile, downloadTimeoutMs, downloadPollIntervalMs }) => {
629
+ downloadTimeoutMs: z.number().int().min(1e3).max(10 * 6e4).optional().describe("How long to wait for the capture file to appear in DIME"),
630
+ downloadPollIntervalMs: z.number().int().min(250).max(3e4).optional().describe("How often to retry DIME GetOneFile when the file isn't there yet")
631
+ },
632
+ async ({ captureId, dimePort, auth, outFile, downloadTimeoutMs, downloadPollIntervalMs }) => {
371
633
  const pruned = captureState.pruneExpired(captureState.load());
372
634
  const rec = pruned.captures[captureId];
373
- if (!rec)
374
- throw new Error(`Capture not found in state: ${captureId}`);
635
+ if (!rec) throw new Error(`Capture not found in state: ${captureId}`);
375
636
  const dl = await getOneFileAnyWithRetry(rec.host, rec.remoteFileCandidates?.length ? rec.remoteFileCandidates : [rec.remoteFilePath], {
376
- auth: auth,
377
- port: dimePort,
378
- timeoutMs: downloadTimeoutMs,
379
- pollIntervalMs: downloadPollIntervalMs,
637
+ auth,
638
+ port: dimePort,
639
+ timeoutMs: downloadTimeoutMs,
640
+ pollIntervalMs: downloadPollIntervalMs
380
641
  });
381
642
  const saved = writeDownloadedFile(dl, outFile);
382
643
  const summary = `Capture downloaded from state. id=${captureId} remoteFilePath=${dl.filename} savedPath=${saved.filePath} bytes=${saved.bytes}`;
383
644
  return {
384
- content: [
645
+ content: [
646
+ {
647
+ type: "text",
648
+ text: `${summary}
649
+
650
+ ${JSON.stringify(
385
651
  {
386
- type: "text",
387
- text: `${summary}\n\n${JSON.stringify({
388
- captureId,
389
- host: rec.host,
390
- remoteFilePath: dl.filename,
391
- savedPath: saved.filePath,
392
- bytes: saved.bytes,
393
- dimeAttempts: dl.attempts,
394
- dimeWaitedMs: dl.waitedMs,
395
- }, null, 2)}`,
652
+ captureId,
653
+ host: rec.host,
654
+ remoteFilePath: dl.filename,
655
+ savedPath: saved.filePath,
656
+ bytes: saved.bytes,
657
+ dimeAttempts: dl.attempts,
658
+ dimeWaitedMs: dl.waitedMs
396
659
  },
397
- ],
660
+ null,
661
+ 2
662
+ )}`
663
+ }
664
+ ]
398
665
  };
399
- });
666
+ }
667
+ );
668
+ function resolveCapturePath(input) {
669
+ if (input.includes("/") || input.includes("\\") || input.endsWith(".cap") || input.endsWith(".pcap")) {
670
+ return input;
671
+ }
672
+ const pruned = captureState.pruneExpired(captureState.load());
673
+ const rec = pruned.captures[input];
674
+ if (!rec) throw new Error(`No capture file path and no state record for: ${input}`);
675
+ const basename = rec.remoteFilePath.split("/").pop() || `${rec.fileBase}.cap`;
676
+ const localPath = `/tmp/cucm-mcp/${basename}`;
677
+ return localPath;
678
+ }
679
+ server.tool(
680
+ "pcap_call_summary",
681
+ "High-level overview of a packet capture: protocols present, SIP call count, RTP streams, endpoints. Use this first to understand what's in a capture before drilling into specific calls.",
682
+ {
683
+ filePath: z.string().min(1).describe("Path to .cap/.pcap file, or a captureId from packet_capture_start")
684
+ },
685
+ async ({ filePath }) => {
686
+ try {
687
+ const resolved = resolveCapturePath(filePath);
688
+ const result = await pcapCallSummary(resolved);
689
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
690
+ } catch (e) {
691
+ return { content: [{ type: "text", text: JSON.stringify({ error: true, message: formatUnknownError(e) }, null, 2) }] };
692
+ }
693
+ }
694
+ );
695
+ server.tool(
696
+ "pcap_sip_calls",
697
+ "Extract SIP call flows from a capture, grouped by Call-ID. Shows INVITE/100/180/200/BYE sequences, From/To, SDP media info, and call setup timing.",
698
+ {
699
+ filePath: z.string().min(1).describe("Path to .cap/.pcap file, or a captureId"),
700
+ callId: z.string().optional().describe("Filter to a specific SIP Call-ID")
701
+ },
702
+ async ({ filePath, callId }) => {
703
+ try {
704
+ const resolved = resolveCapturePath(filePath);
705
+ const result = await pcapSipCalls(resolved, callId);
706
+ const summary = `Found ${result.length} SIP call(s)` + (result.length > 0 ? `: ${result.map((c) => `${c.callId} (${c.metrics.messageCount} msgs, ${c.metrics.answered ? "answered" : "unanswered"})`).join(", ")}` : "");
707
+ return { content: [{ type: "text", text: `${summary}
708
+
709
+ ${JSON.stringify(result, null, 2)}` }] };
710
+ } catch (e) {
711
+ return { content: [{ type: "text", text: JSON.stringify({ error: true, message: formatUnknownError(e) }, null, 2) }] };
712
+ }
713
+ }
714
+ );
715
+ server.tool(
716
+ "pcap_sccp_messages",
717
+ "Extract Skinny/SCCP messages from a capture. Shows phone registration, call state changes, media channel setup (OpenReceiveChannel, StartMediaTransmission), and key presses.",
718
+ {
719
+ filePath: z.string().min(1).describe("Path to .cap/.pcap file, or a captureId"),
720
+ deviceFilter: z.string().optional().describe("Filter to a specific device IP address")
721
+ },
722
+ async ({ filePath, deviceFilter }) => {
723
+ try {
724
+ const resolved = resolveCapturePath(filePath);
725
+ const result = await pcapScppMessages(resolved, deviceFilter);
726
+ const summary = `Found ${result.totalMessages} SCCP message(s) across ${result.devices.length} device(s). Top message types: ${Object.entries(result.messageTypes).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k}(${v})`).join(", ")}`;
727
+ return { content: [{ type: "text", text: `${summary}
728
+
729
+ ${JSON.stringify(result, null, 2)}` }] };
730
+ } catch (e) {
731
+ return { content: [{ type: "text", text: JSON.stringify({ error: true, message: formatUnknownError(e) }, null, 2) }] };
732
+ }
733
+ }
734
+ );
735
+ server.tool(
736
+ "pcap_rtp_streams",
737
+ "Analyze RTP media streams in a capture. Shows per-stream jitter, packet loss, codec, duration. Use to assess call quality.",
738
+ {
739
+ filePath: z.string().min(1).describe("Path to .cap/.pcap file, or a captureId"),
740
+ ssrcFilter: z.string().optional().describe("Filter to a specific RTP SSRC (hex, e.g. 0xABCD1234)")
741
+ },
742
+ async ({ filePath, ssrcFilter }) => {
743
+ try {
744
+ const resolved = resolveCapturePath(filePath);
745
+ const result = await pcapRtpStreams(resolved, ssrcFilter);
746
+ const summary = `Found ${result.summary.totalStreams} RTP stream(s). Worst loss: ${result.summary.worstLoss}, worst jitter: ${result.summary.worstJitter}`;
747
+ return { content: [{ type: "text", text: `${summary}
748
+
749
+ ${JSON.stringify(result, null, 2)}` }] };
750
+ } catch (e) {
751
+ return { content: [{ type: "text", text: JSON.stringify({ error: true, message: formatUnknownError(e) }, null, 2) }] };
752
+ }
753
+ }
754
+ );
755
+ server.tool(
756
+ "pcap_protocol_filter",
757
+ "Run an arbitrary tshark display filter on a capture and extract specific fields. Escape hatch for deep protocol investigation. Examples: 'sip.Method == INVITE', 'skinny.callState == 12', 'rtp.ssrc == 0xABCD'.",
758
+ {
759
+ filePath: z.string().min(1).describe("Path to .cap/.pcap file, or a captureId"),
760
+ displayFilter: z.string().min(1).describe("tshark display filter expression"),
761
+ fields: z.array(z.string()).optional().describe("Specific tshark fields to extract (e.g. ['sip.Call-ID', 'sip.from.addr']). If omitted, returns frame basics."),
762
+ maxPackets: z.number().int().min(1).max(1e3).optional().describe("Max packets to return (default 100, max 1000)")
763
+ },
764
+ async ({ filePath, displayFilter, fields, maxPackets }) => {
765
+ try {
766
+ const resolved = resolveCapturePath(filePath);
767
+ const result = await pcapProtocolFilter(resolved, displayFilter, fields, maxPackets);
768
+ return { content: [{ type: "text", text: `${result.length} packet(s) matched filter "${displayFilter}"
769
+
770
+ ${JSON.stringify(result, null, 2)}` }] };
771
+ } catch (e) {
772
+ return { content: [{ type: "text", text: JSON.stringify({ error: true, message: formatUnknownError(e) }, null, 2) }] };
773
+ }
774
+ }
775
+ );
400
776
  const transport = new StdioServerTransport();
401
777
  await server.connect(transport);
778
+ //# sourceMappingURL=index.js.map