@calltelemetry/cucm-mcp 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.js +9 -4
- package/dist/packetCapture.js +55 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,3 +84,8 @@ Live tests are opt-in via env vars; see `test/live.test.js`.
|
|
|
84
84
|
- `packet_capture_stop_and_download` - stop capture + download `.cap` via DIME
|
|
85
85
|
- `packet_capture_state_list` - list captures from state file
|
|
86
86
|
- `packet_capture_download_from_state` - download by captureId after restart
|
|
87
|
+
|
|
88
|
+
## Packet Capture Notes
|
|
89
|
+
|
|
90
|
+
- Use the platform/OS admin for SSH (`administrator` user on most lab systems)
|
|
91
|
+
- To request a high packet count without specifying an exact number, pass `maxPackets: true` to `packet_capture_start`
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const strictTls = tlsMode === "strict" || tlsMode === "verify";
|
|
|
14
14
|
// Set CUCM_MCP_TLS_MODE=strict to enforce verification.
|
|
15
15
|
if (!strictTls)
|
|
16
16
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
17
|
-
const server = new McpServer({ name: "cucm", version: "0.1.
|
|
17
|
+
const server = new McpServer({ name: "cucm", version: "0.1.4" });
|
|
18
18
|
const captures = new PacketCaptureManager();
|
|
19
19
|
const captureState = defaultStateStore();
|
|
20
20
|
const dimeAuthSchema = z
|
|
@@ -106,18 +106,23 @@ server.tool("packet_capture_start", "Start a packet capture on CUCM via SSH (uti
|
|
|
106
106
|
auth: sshAuthSchema,
|
|
107
107
|
iface: z.string().optional().describe("Interface (default eth0)"),
|
|
108
108
|
fileBase: z.string().optional().describe("Capture base name (no dots). Saved as <fileBase>.cap"),
|
|
109
|
-
count: z.number().int().min(1).max(
|
|
109
|
+
count: z.number().int().min(1).max(1_000_000).optional().describe("Packet count (common max is 1000000)"),
|
|
110
|
+
maxPackets: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("If true and count is omitted, uses a high capture count (1,000,000)"),
|
|
110
114
|
size: z.string().optional().describe("Packet size (e.g. all)"),
|
|
111
115
|
hostFilterIp: z.string().optional().describe("Optional filter: host ip <addr>"),
|
|
112
116
|
portFilter: z.number().int().min(1).max(65535).optional().describe("Optional filter: port <num>"),
|
|
113
|
-
}, async ({ host, sshPort, auth, iface, fileBase, count, size, hostFilterIp, portFilter }) => {
|
|
117
|
+
}, async ({ host, sshPort, auth, iface, fileBase, count, maxPackets, size, hostFilterIp, portFilter }) => {
|
|
118
|
+
const resolvedCount = count ?? (maxPackets ? 1_000_000 : undefined);
|
|
114
119
|
const result = await captures.start({
|
|
115
120
|
host,
|
|
116
121
|
sshPort,
|
|
117
122
|
auth: auth,
|
|
118
123
|
iface,
|
|
119
124
|
fileBase,
|
|
120
|
-
count,
|
|
125
|
+
count: resolvedCount,
|
|
121
126
|
size,
|
|
122
127
|
hostFilterIp,
|
|
123
128
|
portFilter,
|
package/dist/packetCapture.js
CHANGED
|
@@ -90,6 +90,33 @@ export function remoteCaptureCandidates(fileBase, maxParts = 10) {
|
|
|
90
90
|
}
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
|
+
function waitFor(channel, session, predicate, timeoutMs, timeoutMessage) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
if (predicate())
|
|
96
|
+
return resolve();
|
|
97
|
+
const onData = () => {
|
|
98
|
+
if (predicate())
|
|
99
|
+
cleanup(true);
|
|
100
|
+
};
|
|
101
|
+
let timer;
|
|
102
|
+
const cleanup = (ok) => {
|
|
103
|
+
channel.off("data", onData);
|
|
104
|
+
channel.stderr.off("data", onData);
|
|
105
|
+
if (timer)
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
if (ok)
|
|
108
|
+
resolve();
|
|
109
|
+
else {
|
|
110
|
+
const tail = (session.lastStdout || session.lastStderr || "").slice(-400);
|
|
111
|
+
reject(new Error(`${timeoutMessage}. lastOutput=${JSON.stringify(tail)}`));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
channel.on("data", onData);
|
|
115
|
+
channel.stderr.on("data", onData);
|
|
116
|
+
timer = setTimeout(() => cleanup(false), Math.max(1000, timeoutMs));
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
93
120
|
export class PacketCaptureManager {
|
|
94
121
|
active = new Map();
|
|
95
122
|
state;
|
|
@@ -136,8 +163,10 @@ export class PacketCaptureManager {
|
|
|
136
163
|
readyTimeout: 15000,
|
|
137
164
|
});
|
|
138
165
|
});
|
|
166
|
+
// CUCM SSH presents an interactive CLI shell. `exec()` is not reliably supported,
|
|
167
|
+
// so we open a shell and type commands at the prompt.
|
|
139
168
|
const channel = await new Promise((resolve, reject) => {
|
|
140
|
-
client.
|
|
169
|
+
client.shell({ term: "vt100", cols: 120, rows: 40 }, (err, ch) => {
|
|
141
170
|
if (err)
|
|
142
171
|
return reject(err);
|
|
143
172
|
resolve(ch);
|
|
@@ -165,6 +194,28 @@ export class PacketCaptureManager {
|
|
|
165
194
|
// ignore
|
|
166
195
|
}
|
|
167
196
|
});
|
|
197
|
+
// Wait for prompt before running capture command.
|
|
198
|
+
try {
|
|
199
|
+
await waitFor(channel, session, () => looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr), 30_000, "Timeout waiting for CUCM CLI prompt");
|
|
200
|
+
channel.write(`${cmd}\n`);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
try {
|
|
204
|
+
channel.end();
|
|
205
|
+
channel.close();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// ignore
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
client.end();
|
|
212
|
+
client.destroy();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// ignore
|
|
216
|
+
}
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
168
219
|
this.active.set(id, { session, client, channel });
|
|
169
220
|
return session;
|
|
170
221
|
}
|
|
@@ -174,6 +225,9 @@ export class PacketCaptureManager {
|
|
|
174
225
|
throw new Error(`Unknown captureId: ${captureId}`);
|
|
175
226
|
const { channel, client, session } = a;
|
|
176
227
|
const done = new Promise((resolve) => {
|
|
228
|
+
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
229
|
+
return resolve();
|
|
230
|
+
}
|
|
177
231
|
// CUCM CLI can keep the SSH channel open after the command finishes
|
|
178
232
|
// (it returns to a prompt rather than exiting). Treat prompt as "stopped".
|
|
179
233
|
const onData = () => {
|